From ad6d8af2f6d097fe708f08f3e0f608b6b8e59167 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Mon, 20 Apr 2026 19:19:44 -0400 Subject: [PATCH] docs(1c-alpha): correct TS type definitions to match actual serde shapes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ...-20-relicario-extension-1c-alpha-design.md | 118 ++++++++++++------ 1 file changed, 81 insertions(+), 37 deletions(-) diff --git a/docs/superpowers/specs/2026-04-20-relicario-extension-1c-alpha-design.md b/docs/superpowers/specs/2026-04-20-relicario-extension-1c-alpha-design.md index 3fd6c51..8938926 100644 --- a/docs/superpowers/specs/2026-04-20-relicario-extension-1c-alpha-design.md +++ b/docs/superpowers/specs/2026-04-20-relicario-extension-1c-alpha-design.md @@ -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; } +// 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 → 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` 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