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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user