Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG_PANEL_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,12 @@ const isUnread = entryVersion > seenVersion; // String comparison works for semv
### Optimization Techniques Used

1. **useMemo for grouping**

- Prevents recalculation on every render
- Only recalculates when entries change

2. **useCallback for handlers**

- `markAllSeen` wrapped in useCallback
- `isEntryUnread` wrapped in useCallback
- Prevents unnecessary function recreations
Expand Down Expand Up @@ -340,17 +342,20 @@ const isUnread = entryVersion > seenVersion; // String comparison works for semv
### Unit Test Suggestions

1. **Component rendering**

- Header displays correctly
- Entries render
- Empty state shows when entries empty
- Categories badges have correct styling

2. **State management**

- markAllSeen() updates localStorage
- isEntryUnread() correctly identifies unread
- hasUnread flag works correctly

3. **Grouping logic**

- Entries grouped by version|date
- Groups appear in correct order
- Entries within group maintain order
Expand All @@ -364,13 +369,15 @@ const isUnread = entryVersion > seenVersion; // String comparison works for semv
### E2E Test Suggestions

1. **User flows**

- Navigate to changelog
- Verify entries displayed
- Hover over entry
- Click external link
- Navigate away and back

2. **Interaction**

- Keyboard navigation
- Focus management
- Link functionality
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG_PANEL_BEFORE_AFTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,17 +402,20 @@ ChangelogPanel
### WCAG Compliance Improvements

1. **Semantic HTML**

- `<article>` for entries
- `<section>` for groups
- `<time>` for dates
- `<h3>`, `<h4>`, `<h5>` for headings

2. **Keyboard Navigation**

- `focus-visible:ring-1 focus-visible:ring-ring` on interactive elements
- Proper button component for links
- Tab order naturally follows DOM

3. **Screen Reader Support**

- `aria-label` on visual indicators
- `title` attributes for tooltips
- Semantic elements provide context
Expand Down
7 changes: 7 additions & 0 deletions SETTLEMENT_IDEMPOTENCY_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ if (rawIdempotencyKey) {
**Test Coverage** (from `tests/unit/api/postage-settlement-idempotency.test.ts`):

1. **Retry after success**:

- `"handles retry after successful settlement (same idempotency key)"`
- Verifies replayed response matches original
- Confirms no double-settlement occurs

2. **Retry after terminal state**:

- `"returns deterministic error when settling already-settled postage"`
- `"returns deterministic error when settling already-refunded postage"`
- `"handles retry after terminal-state error (same idempotency key)"`
Expand Down Expand Up @@ -100,11 +102,13 @@ if (postage.status !== "pending") {
### Files Modified

1. **`src/routes/api/v1/postage/$messageId/settle.ts`**

- Added idempotency key handling
- Implemented response caching and replay
- Added comprehensive documentation

2. **`src/server/api/postage-service.ts`**

- Enhanced `resolvePostage` with detailed error messages
- Improved terminal state explanations

Expand All @@ -114,11 +118,13 @@ if (postage.status !== "pending") {
### Files Created

1. **`tests/unit/api/postage-settlement-idempotency.test.ts`**

- 17 comprehensive test cases
- Covers all retry scenarios
- Validates actor isolation and security

2. **`docs/api/SETTLEMENT_IDEMPOTENCY.md`**

- Complete idempotency documentation
- Request flow diagrams
- Client best practices
Expand Down Expand Up @@ -157,6 +163,7 @@ if (postage.status !== "pending") {
## Commits

1. **f607e2c8** - `feat: add idempotency support to postage settlement endpoint`

- Core implementation
- Test suite
- Error message improvements
Expand Down
2 changes: 1 addition & 1 deletion docs/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The TanStack Start worker exposes versioned endpoints under `/api/v1`.

## Endpoint groups

- Operations: `GET /health`, `GET /protocol`, `GET /openapi.json`
- Operations: `GET /health`, `GET /health?check=readiness`, `GET /protocol`, `GET /openapi.json`
- Policy: read or replace mailbox defaults, manage sender overrides, and evaluate admission
- Postage: quote, submit, retrieve, settle, and refund message postage
- Receipts: record delivery, retrieve participant state, and acknowledge reads
Expand Down
3 changes: 1 addition & 2 deletions docs/security/metadata-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ To govern the lifecycle of metadata, Stealth enforces strict rules across all co

## 3. On-Chain Address Rule

> [!IMPORTANT]
> **Plaintext addresses are never written on-chain.**
> [!IMPORTANT] > **Plaintext addresses are never written on-chain.**
>
> Senders, recipients, and delegates are identified on the Stellar blockchain exclusively by cryptographically generated public keys or smart contract hashes (represented by the Soroban `Address` type). Human-readable names, email addresses, and federation handles (e.g. `alice*stealth.demo`) are strictly resolved off-chain prior to submitting any transaction.

Expand Down
4 changes: 3 additions & 1 deletion scripts/generate-contract-bindings.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,9 @@ function emitClient(spec, contractName, xdrBase64Entries) {
lines.push(`export async function ${camelCase(fn.name)}(`);
lines.push(` ${fullParamList}`);
lines.push(
`): Promise<contract.Ok<${ok === "void" ? "void" : ok}> | contract.Err<{ message: string }>> {`,
`): Promise<contract.Ok<${
ok === "void" ? "void" : ok
}> | contract.Err<{ message: string }>> {`,
);
} else {
lines.push(`export async function ${camelCase(fn.name)}(`);
Expand Down
4 changes: 3 additions & 1 deletion src/components/mail/EmailList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,9 @@ export function EmailList({
)}
>
<img
src={`https://api.dicebear.com/7.x/identicon/svg?seed=${encodeURIComponent(e.from)}&backgroundColor=1a1a1d`}
src={`https://api.dicebear.com/7.x/identicon/svg?seed=${encodeURIComponent(
e.from,
)}&backgroundColor=1a1a1d`}
alt={e.from}
loading="lazy"
className="h-full w-full object-cover"
Expand Down
4 changes: 3 additions & 1 deletion src/components/mail/MobileMailCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ export function MobileMailCard({
<div className="relative shrink-0">
<div className="h-9 w-9 overflow-hidden rounded-full ring-1 ring-white/15 shadow-[0_8px_18px_-12px_rgba(0,0,0,0.9)]">
<img
src={`https://api.dicebear.com/7.x/identicon/svg?seed=${encodeURIComponent(email.from)}&backgroundColor=1a1a1d`}
src={`https://api.dicebear.com/7.x/identicon/svg?seed=${encodeURIComponent(
email.from,
)}&backgroundColor=1a1a1d`}
alt={email.from}
loading="lazy"
className="h-full w-full object-cover"
Expand Down
4 changes: 3 additions & 1 deletion src/components/mail/RightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ export function RightPanel({
}
if (action === "summarize") {
setSummary(
`${email.from} is writing about ${email.subject.toLowerCase()}. The next step is to respond or review the attached context.`,
`${
email.from
} is writing about ${email.subject.toLowerCase()}. The next step is to respond or review the attached context.`,
);
}
onAction(action, email);
Expand Down
4 changes: 3 additions & 1 deletion src/components/mail/Topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,9 @@ export function Topbar({
current === "personal" ? "protocol" : "personal",
);
onShowToast(
`Switched to ${account === "personal" ? "Protocol" : "Personal"} mailbox`,
`Switched to ${
account === "personal" ? "Protocol" : "Personal"
} mailbox`,
);
}}
/>
Expand Down
4 changes: 3 additions & 1 deletion src/components/mail/provenance-a11y.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,7 @@ export function technicalProvenanceToggleLabel(expanded: boolean): string {

/** Full step announcement for timeline list items. */
export function timelineStepAriaLabel(item: ProvenanceTimelineItem): string {
return `${item.title}, ${timelineStepStatusLabel(item.status)}${item.timestamp ? `, ${item.timestamp}` : ""}`;
return `${item.title}, ${timelineStepStatusLabel(item.status)}${
item.timestamp ? `, ${item.timestamp}` : ""
}`;
}
34 changes: 16 additions & 18 deletions src/features/compose/INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,7 @@
import { Compose } from "@/components/mail/Compose";

export function MailUI() {
return (
<Compose
open={true}
onClose={() => {}}
blockedRecipients={["blocked@example.com"]}
/>
);
return <Compose open={true} onClose={() => {}} blockedRecipients={["blocked@example.com"]} />;
}
```

Expand All @@ -32,22 +26,26 @@ const context: RecipientResolutionContext = {
// Resolve from local contact database
resolveContact: async (input: string) => {
const contact = await contactDB.query(input);
return contact ? {
id: contact.id,
name: contact.name,
address: contact.address,
publicKey: contact.encryptionKey,
trusted: contact.isTrusted,
} : null;
return contact
? {
id: contact.id,
name: contact.name,
address: contact.address,
publicKey: contact.encryptionKey,
trusted: contact.isTrusted,
}
: null;
},

// Resolve federation addresses (alice*stellar.org)
resolveFederation: async (address: string) => {
const result = await stellarFederation.resolve(address);
return result ? {
publicKey: result.accountId,
domain: result.domain,
} : null;
return result
? {
publicKey: result.accountId,
domain: result.domain,
}
: null;
},

// Check if recipient is blocked
Expand Down
4 changes: 3 additions & 1 deletion src/features/contacts/import/identityMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ export function matchIdentity(row: ImportedContactRow, knownContacts: ContactRef
matchedAddress: best.contact.address,
matchedName: best.contact.name,
confidence: 1,
reason: `Name and address match existing contact${best.contact.trusted ? " (trusted)" : ""}.`,
reason: `Name and address match existing contact${
best.contact.trusted ? " (trusted)" : ""
}.`,
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/features/demo-admin-dashboard/README-inbox-preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ src/features/demo-admin-dashboard/
### Basic Integration

```typescript
import { DemoInboxPreview } from '@/features/demo-admin-dashboard';
import { DemoInboxPreview } from "@/features/demo-admin-dashboard";

function AdminDashboard() {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ describe("summarizeBulkFolderMove", () => {
it("reports partial skips", () => {
const result = applyBulkFolderMove(sampleMessages(), ["m1", "m2"], "drafts");
expect(summarizeBulkFolderMove(result)).toBe(
`Moved 1 message to ${getMessageFolderLabel("drafts")} (1 skipped — already in ${getMessageFolderLabel("drafts")}).`,
`Moved 1 message to ${getMessageFolderLabel(
"drafts",
)} (1 skipped — already in ${getMessageFolderLabel("drafts")}).`,
);
});
});
12 changes: 9 additions & 3 deletions src/features/demo-admin-dashboard/auditLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,21 @@ export const formatAuditEntry = (entry: AuditLogEntry): string => {
return `${actor.name} created the ${target.type.toLowerCase()} "${target.name}".`;
case "update":
if (details && details.field) {
return `${actor.name} updated the ${details.field} of ${target.type.toLowerCase()} "${target.name}" from "${details.from}" to "${details.to}".`;
return `${actor.name} updated the ${details.field} of ${target.type.toLowerCase()} "${
target.name
}" from "${details.from}" to "${details.to}".`;
}
return `${actor.name} updated the ${target.type.toLowerCase()} "${target.name}".`;
case "assign":
return `${actor.name} assigned the ${target.type.toLowerCase()} "${target.name}" to ${details?.assignee || "a team member"}.`;
return `${actor.name} assigned the ${target.type.toLowerCase()} "${target.name}" to ${
details?.assignee || "a team member"
}.`;
case "publish":
return `${actor.name} published the ${target.type.toLowerCase()} "${target.name}".`;
case "rollback":
return `${actor.name} rolled back the ${target.type.toLowerCase()} "${target.name}" to version ${details?.version || "a previous state"}.`;
return `${actor.name} rolled back the ${target.type.toLowerCase()} "${
target.name
}" to version ${details?.version || "a previous state"}.`;
default:
return `An unknown action was performed by ${actor.name} on "${target.name}".`;
}
Expand Down
36 changes: 30 additions & 6 deletions src/features/demo-admin-dashboard/campaignPublishChecklist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,11 @@ export function buildCampaignPublishChecklist(
message:
draftsMissingSubject === 0
? "All drafts have a subject."
: `${draftsMissingSubject} ${pluralize(draftsMissingSubject, "draft is", "drafts are")} missing a subject.`,
: `${draftsMissingSubject} ${pluralize(
draftsMissingSubject,
"draft is",
"drafts are",
)} missing a subject.`,
},
{
id: "draft-bodies",
Expand All @@ -152,7 +156,11 @@ export function buildCampaignPublishChecklist(
message:
draftsMissingBody === 0
? "All drafts have body content."
: `${draftsMissingBody} ${pluralize(draftsMissingBody, "draft is", "drafts are")} missing body content.`,
: `${draftsMissingBody} ${pluralize(
draftsMissingBody,
"draft is",
"drafts are",
)} missing body content.`,
},
{
id: "draft-recipients",
Expand All @@ -162,7 +170,11 @@ export function buildCampaignPublishChecklist(
message:
draftsWithoutRecipients === 0
? "All drafts have at least one recipient."
: `${draftsWithoutRecipients} ${pluralize(draftsWithoutRecipients, "draft has", "drafts have")} no recipients.`,
: `${draftsWithoutRecipients} ${pluralize(
draftsWithoutRecipients,
"draft has",
"drafts have",
)} no recipients.`,
},
{
id: "no-secret-keys",
Expand All @@ -172,7 +184,11 @@ export function buildCampaignPublishChecklist(
message:
draftsWithSecretKeys === 0
? "No Stellar secret keys detected in demo drafts."
: `${draftsWithSecretKeys} ${pluralize(draftsWithSecretKeys, "draft appears", "drafts appear")} to contain a Stellar secret key.`,
: `${draftsWithSecretKeys} ${pluralize(
draftsWithSecretKeys,
"draft appears",
"drafts appear",
)} to contain a Stellar secret key.`,
},
{
id: "has-tags",
Expand All @@ -192,7 +208,11 @@ export function buildCampaignPublishChecklist(
message:
draftsWithDuplicateRecipients === 0
? "No duplicate recipients detected."
: `${draftsWithDuplicateRecipients} ${pluralize(draftsWithDuplicateRecipients, "draft has", "drafts have")} duplicate recipients.`,
: `${draftsWithDuplicateRecipients} ${pluralize(
draftsWithDuplicateRecipients,
"draft has",
"drafts have",
)} duplicate recipients.`,
},
{
id: "batch-size",
Expand All @@ -212,7 +232,11 @@ export function buildCampaignPublishChecklist(
message:
draftsWithLongSubject === 0
? "All draft subjects are within the recommended length."
: `${draftsWithLongSubject} ${pluralize(draftsWithLongSubject, "draft subject exceeds", "draft subjects exceed")} ${MAX_SUBJECT_LENGTH} characters.`,
: `${draftsWithLongSubject} ${pluralize(
draftsWithLongSubject,
"draft subject exceeds",
"draft subjects exceed",
)} ${MAX_SUBJECT_LENGTH} characters.`,
},
];

Expand Down
Loading
Loading