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`)
|
## 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
|
```ts
|
||||||
export type ItemId = string; // 16-char hex
|
export type ItemId = string; // 16-char hex
|
||||||
export type FieldId = string;
|
export type FieldId = string;
|
||||||
export type AttachmentId = 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 {
|
export interface Item {
|
||||||
id: ItemId;
|
id: ItemId;
|
||||||
@@ -119,39 +120,47 @@ export interface Item {
|
|||||||
type: ItemType; // Rust's `r#type` serializes as `type`
|
type: ItemType; // Rust's `r#type` serializes as `type`
|
||||||
tags: string[];
|
tags: string[];
|
||||||
favorite: boolean;
|
favorite: boolean;
|
||||||
group: string | null;
|
group?: string; // omitted when None (#[serde(skip_serializing_if)])
|
||||||
notes: string | null;
|
notes?: string;
|
||||||
created: number;
|
created: number;
|
||||||
modified: number;
|
modified: number;
|
||||||
trashed_at: number | null;
|
trashed_at?: number;
|
||||||
core: ItemCore;
|
core: ItemCore; // internally-tagged on `"type"` — see below
|
||||||
sections: Section[];
|
sections: Section[];
|
||||||
attachments: AttachmentRef[];
|
attachments: AttachmentRef[];
|
||||||
field_history: Record<FieldId, FieldHistoryEntry[]>;
|
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 =
|
export type ItemCore =
|
||||||
| { Login: LoginCore }
|
| ({ type: 'login' } & LoginCore)
|
||||||
| { SecureNote: SecureNoteCore }
|
| ({ type: 'secure_note' } & SecureNoteCore)
|
||||||
| { Identity: IdentityCore }
|
| ({ type: 'identity' } & IdentityCore)
|
||||||
| { Card: CardCore }
|
| ({ type: 'card' } & CardCore)
|
||||||
| { Key: KeyCore }
|
| ({ type: 'key' } & KeyCore)
|
||||||
| { Document: DocumentCore }
|
| ({ type: 'document' } & DocumentCore)
|
||||||
| { Totp: TotpCore };
|
| ({ type: 'totp' } & TotpCore);
|
||||||
|
|
||||||
export interface LoginCore {
|
export interface LoginCore {
|
||||||
username: string | null;
|
username?: string;
|
||||||
password: string | null;
|
password?: string;
|
||||||
url: string | null;
|
url?: string; // Rust serializes `Url` as its string form
|
||||||
totp: TotpConfig | null;
|
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 {
|
export interface TotpConfig {
|
||||||
secret: number[]; // raw bytes, serde → JSON array
|
secret: number[]; // Vec<u8> → JSON number array
|
||||||
algorithm: 'Sha1' | 'Sha256' | 'Sha512';
|
algorithm: 'sha1' | 'sha256' | 'sha512';
|
||||||
digits: number;
|
digits: number;
|
||||||
period_seconds: number;
|
period_seconds: number;
|
||||||
kind: { Totp: null } | { Hotp: number } | { Steam: null };
|
kind: TotpKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populated minimally for α (structural shape only, no UI); β fills in:
|
// 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';
|
captureStyle: 'bar' | 'toast';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GeneratorRequest is internally-tagged on "kind", struct variants:
|
||||||
export type GeneratorRequest =
|
export type GeneratorRequest =
|
||||||
| { Bip39: { word_count: number; separator: string; capitalization: Capitalization } }
|
| { kind: 'bip39'; word_count: number; separator: string; capitalization: Capitalization }
|
||||||
| { Random: { length: number; classes: CharClasses; symbol_charset: SymbolCharset } };
|
| { 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 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`)
|
## Messages (`shared/messages.ts`)
|
||||||
|
|
||||||
@@ -410,27 +453,28 @@ Rename `entry-*.ts` → `item-*.ts`. List view renders from `ManifestEntry`, sho
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
case 'Login': return renderLoginDetail(app, item);
|
case 'login': return renderLoginDetail(app, item);
|
||||||
case 'SecureNote':
|
case 'secure_note':
|
||||||
case 'Identity':
|
case 'identity':
|
||||||
case 'Card':
|
case 'card':
|
||||||
case 'Key':
|
case 'key':
|
||||||
case 'Document':
|
case 'document':
|
||||||
case 'Totp': return renderComingSoonPlaceholder(app, item.type);
|
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-β".
|
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:
|
"gen" button sends:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
{ type: 'generate_password',
|
{ type: 'generate_password',
|
||||||
request: { Random: { length: 20,
|
request: { kind: 'random',
|
||||||
|
length: 20,
|
||||||
classes: { lower: true, upper: true, digits: true, symbols: true },
|
classes: { lower: true, upper: true, digits: true, symbols: true },
|
||||||
symbol_charset: 'SafeOnly' } } }
|
symbol_charset: { kind: 'safe_only' } } }
|
||||||
```
|
```
|
||||||
|
|
||||||
### Setup wizard + zxcvbn
|
### Setup wizard + zxcvbn
|
||||||
|
|||||||
Reference in New Issue
Block a user