docs(1c-alpha): correct TS type definitions to match actual serde shapes

Verified against the Plan 1A Rust sources:
- ItemType / ItemCore use snake_case with tag="type" internal tagging
  (not the external tagging I initially wrote)
- TotpKind is default-externally-tagged (no tag attr), so it serializes
  as bare "totp"/"steam" for unit variants and { hotp: { counter } }
- GeneratorRequest uses tag="kind" internal tagging
- FieldValue / TrashRetention / HistoryRetention / SymbolCharset use
  adjacent tagging { tag: "kind", content: "value" }
- Fix Login form TOTP parse example and "gen" button payload

No scope change — this is a bookkeeping correction so the plan
author references the correct wire shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-20 19:19:44 -04:00
parent a1d733ddeb
commit ad6d8af2f6

View File

@@ -104,14 +104,15 @@ extension/wasm/idfoto_wasm* # stale pre-rename artif
## Shared Types (`shared/types.ts`)
Mirror the Rust core verbatim through serde serialization. The Rust-side `ItemCore` enum is externally-tagged by default, producing `{ "Login": {...} }` style JSON:
Mirror the Rust core verbatim through serde serialization. The Rust-side shapes use a mix of `snake_case`, internal tagging (`#[serde(tag = "type")]` for `ItemCore`, `tag = "kind"` for `GeneratorRequest`), adjacent tagging (`tag = "kind", content = "value"` for `FieldValue`, `TrashRetention`, `HistoryRetention`, `SymbolCharset`), and default external tagging (`TotpKind`). The TS types must match exactly:
```ts
export type ItemId = string; // 16-char hex
export type FieldId = string;
export type AttachmentId = string;
export type ItemType = 'Login' | 'SecureNote' | 'Identity' | 'Card' | 'Key' | 'Document' | 'Totp';
// snake_case strings, matches serde rename_all = "snake_case"
export type ItemType = 'login' | 'secure_note' | 'identity' | 'card' | 'key' | 'document' | 'totp';
export interface Item {
id: ItemId;
@@ -119,39 +120,47 @@ export interface Item {
type: ItemType; // Rust's `r#type` serializes as `type`
tags: string[];
favorite: boolean;
group: string | null;
notes: string | null;
group?: string; // omitted when None (#[serde(skip_serializing_if)])
notes?: string;
created: number;
modified: number;
trashed_at: number | null;
core: ItemCore;
trashed_at?: number;
core: ItemCore; // internally-tagged on `"type"` — see below
sections: Section[];
attachments: AttachmentRef[];
field_history: Record<FieldId, FieldHistoryEntry[]>;
}
// Internally-tagged: ItemCore variant's fields get merged with `"type"` discriminant.
// Example wire format for Login: { "type": "login", "username": "...", ... }
export type ItemCore =
| { Login: LoginCore }
| { SecureNote: SecureNoteCore }
| { Identity: IdentityCore }
| { Card: CardCore }
| { Key: KeyCore }
| { Document: DocumentCore }
| { Totp: TotpCore };
| ({ type: 'login' } & LoginCore)
| ({ type: 'secure_note' } & SecureNoteCore)
| ({ type: 'identity' } & IdentityCore)
| ({ type: 'card' } & CardCore)
| ({ type: 'key' } & KeyCore)
| ({ type: 'document' } & DocumentCore)
| ({ type: 'totp' } & TotpCore);
export interface LoginCore {
username: string | null;
password: string | null;
url: string | null;
totp: TotpConfig | null;
username?: string;
password?: string;
url?: string; // Rust serializes `Url` as its string form
totp?: TotpConfig;
}
// TotpKind is externally-tagged (default for enums without #[serde(tag)]):
// Totp → "totp" (unit variant serializes as bare string)
// Hotp{counter}→ { "hotp": { "counter": 42 } }
// Steam → "steam"
export type TotpKind = 'totp' | 'steam' | { hotp: { counter: number } };
export interface TotpConfig {
secret: number[]; // raw bytes, serde → JSON array
algorithm: 'Sha1' | 'Sha256' | 'Sha512';
secret: number[]; // Vec<u8> → JSON number array
algorithm: 'sha1' | 'sha256' | 'sha512';
digits: number;
period_seconds: number;
kind: { Totp: null } | { Hotp: number } | { Steam: null };
kind: TotpKind;
}
// Populated minimally for α (structural shape only, no UI); β fills in:
@@ -193,18 +202,52 @@ export interface DeviceSettings { // chrome.storage.local shape (was Relic
captureStyle: 'bar' | 'toast';
}
// GeneratorRequest is internally-tagged on "kind", struct variants:
export type GeneratorRequest =
| { Bip39: { word_count: number; separator: string; capitalization: Capitalization } }
| { Random: { length: number; classes: CharClasses; symbol_charset: SymbolCharset } };
| { kind: 'bip39'; word_count: number; separator: string; capitalization: Capitalization }
| { kind: 'random'; length: number; classes: CharClasses; symbol_charset: SymbolCharset };
export type Capitalization = 'Lower' | 'Upper' | 'FirstOfEach' | 'Title' | 'Mixed';
export type Capitalization = 'lower' | 'upper' | 'first_of_each' | 'title' | 'mixed';
export interface CharClasses { lower: boolean; upper: boolean; digits: boolean; symbols: boolean; }
export type SymbolCharset = 'SafeOnly' | 'Extended' | { Custom: string };
// SymbolCharset is adjacently-tagged { tag: "kind", content: "value" }:
export type SymbolCharset =
| { kind: 'safe_only' }
| { kind: 'extended' }
| { kind: 'custom'; value: string };
// TrashRetention / HistoryRetention use the same adjacent tagging:
export type TrashRetention =
| { kind: 'forever' }
| { kind: 'days'; value: number };
export type HistoryRetention =
| { kind: 'forever' }
| { kind: 'last_n'; value: number }
| { kind: 'days'; value: number };
// FieldValue adjacently-tagged { tag: "kind", content: "value" }, snake_case:
export type FieldValue =
| { kind: 'text'; value: string }
| { kind: 'multiline'; value: string }
| { kind: 'password'; value: string }
| { kind: 'concealed'; value: string }
| { kind: 'url'; value: string } // Url → string
| { kind: 'email'; value: string }
| { kind: 'phone'; value: string }
| { kind: 'date'; value: string } // chrono NaiveDate → "YYYY-MM-DD"
| { kind: 'month_year'; value: { month: number; year: number } }
| { kind: 'totp'; value: TotpConfig }
| { kind: 'reference'; value: AttachmentId };
export type FieldKind =
| 'text' | 'multiline' | 'password' | 'concealed' | 'url' | 'email'
| 'phone' | 'date' | 'month_year' | 'totp' | 'reference';
```
Plus `Section`, `Field`, `FieldKind`, `FieldValue`, `AttachmentRef`, `AttachmentSummary`, `FieldHistoryEntry` as declared. Most are unused by α's UI but present so the type-check catches drift with the Rust side.
Plus `Section`, `Field`, `AttachmentRef`, `AttachmentSummary`, `FieldHistoryEntry` as declared. Most are unused by α's UI but present so the type-check catches drift with the Rust side.
The ItemCore external-tagging assumption gets verified in the first SW slice's smoke test: `item_encrypt(handle, JSON.stringify(loginItem))` round-trips through `item_decrypt` with structural equality.
The serialization shapes above are verified in slice 3's smoke test: `item_encrypt(handle, JSON.stringify(loginItem))` round-trips through `item_decrypt` with structural equality against a Rust-side item written by the CLI.
## Messages (`shared/messages.ts`)
@@ -410,27 +453,28 @@ Rename `entry-*.ts` → `item-*.ts`. List view renders from `ManifestEntry`, sho
```ts
switch (item.type) {
case 'Login': return renderLoginDetail(app, item);
case 'SecureNote':
case 'Identity':
case 'Card':
case 'Key':
case 'Document':
case 'Totp': return renderComingSoonPlaceholder(app, item.type);
case 'login': return renderLoginDetail(app, item);
case 'secure_note':
case 'identity':
case 'card':
case 'key':
case 'document':
case 'totp': return renderComingSoonPlaceholder(app, item.type);
}
```
Add flow: "New…" menu lists all seven types; picking Login opens the form, picking any other type shows "Coming in 1C-β".
Existing Login form (username/url/password/totp/group/notes) maps 1:1 to `LoginCore` + `Item` envelope. TOTP field takes a base32 string; `shared/base32.ts` parses it to `Uint8Array` → bytes in `TotpConfig { secret, algorithm: Sha1, digits: 6, period_seconds: 30, kind: { Totp: null } }`. Display is base32 re-encoded with a reveal toggle.
Existing Login form (username/url/password/totp/group/notes) maps 1:1 to `LoginCore` + `Item` envelope. TOTP field takes a base32 string; `shared/base32.ts` parses it to a `number[]` (the `secret: Vec<u8>` on the Rust side) and emits `TotpConfig { secret, algorithm: 'sha1', digits: 6, period_seconds: 30, kind: 'totp' }`. Display is base32 re-encoded with a reveal toggle.
"gen" button sends:
```ts
{ type: 'generate_password',
request: { Random: { length: 20,
classes: { lower: true, upper: true, digits: true, symbols: true },
symbol_charset: 'SafeOnly' } } }
request: { kind: 'random',
length: 20,
classes: { lower: true, upper: true, digits: true, symbols: true },
symbol_charset: { kind: 'safe_only' } } }
```
### Setup wizard + zxcvbn