@plane31/blind-relay 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapters/index.d.ts +3 -3
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/audience.d.ts +1 -1
- package/dist/audience.d.ts.map +1 -1
- package/dist/audience.js +7 -6
- package/dist/audience.js.map +1 -1
- package/dist/client.d.ts +78 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +903 -85
- package/dist/client.js.map +1 -1
- package/dist/codec.d.ts +25 -1
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +44 -0
- package/dist/codec.js.map +1 -1
- package/dist/crypto.d.ts +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js.map +1 -1
- package/dist/errors.d.ts +9 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/identity.d.ts +8 -2
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +7 -3
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/policy.d.ts +13 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +12 -1
- package/dist/policy.js.map +1 -1
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +2 -0
- package/dist/rows.js.map +1 -1
- package/dist/store.d.ts +114 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js.map +1 -1
- package/dist/succession.d.ts +108 -0
- package/dist/succession.d.ts.map +1 -0
- package/dist/succession.js +205 -0
- package/dist/succession.js.map +1 -0
- package/dist/testing/clock.d.ts.map +1 -1
- package/dist/testing/clock.js +1 -0
- package/dist/testing/clock.js.map +1 -1
- package/dist/testing/fakeHost.d.ts +60 -1
- package/dist/testing/fakeHost.d.ts.map +1 -1
- package/dist/testing/fakeHost.js +201 -31
- package/dist/testing/fakeHost.js.map +1 -1
- package/dist/testing/faults.d.ts +5 -3
- package/dist/testing/faults.d.ts.map +1 -1
- package/dist/testing/faults.js +24 -3
- package/dist/testing/faults.js.map +1 -1
- package/dist/transactions.d.ts +6 -0
- package/dist/transactions.d.ts.map +1 -1
- package/dist/transactions.js +11 -5
- package/dist/transactions.js.map +1 -1
- package/dist/transport.d.ts +66 -2
- package/dist/transport.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/index.ts +13 -0
- package/src/audience.ts +7 -6
- package/src/client.ts +881 -90
- package/src/codec.ts +64 -1
- package/src/crypto.ts +1 -0
- package/src/errors.ts +12 -1
- package/src/identity.ts +13 -3
- package/src/index.ts +5 -3
- package/src/policy.ts +23 -1
- package/src/rows.ts +2 -0
- package/src/store.ts +110 -3
- package/src/succession.ts +270 -0
- package/src/testing/clock.ts +1 -0
- package/src/testing/fakeHost.ts +204 -28
- package/src/testing/faults.ts +28 -7
- package/src/transactions.ts +13 -6
- package/src/transport.ts +69 -2
package/src/codec.ts
CHANGED
|
@@ -69,6 +69,8 @@ export enum DocumentType {
|
|
|
69
69
|
EncryptedRecord = 4,
|
|
70
70
|
AccountVault = 6,
|
|
71
71
|
AccountResetReceipt = 7,
|
|
72
|
+
/** Revision 9 (amendment A1): the host's authority binds an account to new keys with continuity of its relationships. */
|
|
73
|
+
AccountSuccession = 9,
|
|
72
74
|
ResetKeyList = 8
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -136,6 +138,27 @@ export interface AccountResetReceipt {
|
|
|
136
138
|
readonly authorityKeyId: Uint8Array;
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Type 9. The transition (every field but the last two and the signature) says what happened to
|
|
143
|
+
* the account; the attestation (`issuedAt`, `authorityKeyId`, the signature) says who vouched for
|
|
144
|
+
* it. Two documents with one transition are one succession.
|
|
145
|
+
*/
|
|
146
|
+
export interface AccountSuccession {
|
|
147
|
+
readonly type: DocumentType.AccountSuccession;
|
|
148
|
+
readonly accountName: string;
|
|
149
|
+
readonly accountId: Uint8Array;
|
|
150
|
+
readonly previousIdentityId: Uint8Array;
|
|
151
|
+
readonly newIdentityId: Uint8Array;
|
|
152
|
+
readonly previousGeneration: bigint;
|
|
153
|
+
readonly newGeneration: bigint;
|
|
154
|
+
readonly newCertificateHash: Uint8Array;
|
|
155
|
+
readonly successionId: Uint8Array;
|
|
156
|
+
readonly issuedAt: bigint;
|
|
157
|
+
readonly authorityKeyId: Uint8Array;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const SUCCESSION_WIRE_BYTES_WITHOUT_NAME = 251;
|
|
161
|
+
|
|
139
162
|
export interface ResetKeyEntry {
|
|
140
163
|
readonly keyId: Uint8Array;
|
|
141
164
|
readonly publicKey: Uint8Array;
|
|
@@ -149,7 +172,7 @@ export interface ResetKeyList {
|
|
|
149
172
|
readonly keys: readonly ResetKeyEntry[];
|
|
150
173
|
}
|
|
151
174
|
|
|
152
|
-
export type SignedDocument = AccountDevice | CircleSnapshot | EpochManifest | EncryptedRecord | AccountResetReceipt | ResetKeyList;
|
|
175
|
+
export type SignedDocument = AccountDevice | CircleSnapshot | EpochManifest | EncryptedRecord | AccountResetReceipt | ResetKeyList | AccountSuccession;
|
|
153
176
|
|
|
154
177
|
/** A parsed signed document together with the exact body and signature bytes. */
|
|
155
178
|
export type Parsed<T extends SignedDocument = SignedDocument> = T & {
|
|
@@ -164,6 +187,7 @@ export const LABEL_BY_TYPE: Readonly<Record<number, Label>> = {
|
|
|
164
187
|
[DocumentType.EpochManifest]: "epoch-sig",
|
|
165
188
|
[DocumentType.EncryptedRecord]: "record-sig",
|
|
166
189
|
[DocumentType.AccountResetReceipt]: "reset-sig",
|
|
190
|
+
[DocumentType.AccountSuccession]: "succession-sig",
|
|
167
191
|
[DocumentType.ResetKeyList]: "reset-keys-sig"
|
|
168
192
|
};
|
|
169
193
|
|
|
@@ -268,6 +292,23 @@ export function parseDocument(wire: Uint8Array): Parsed {
|
|
|
268
292
|
fields = { type, accountName, oldIdentityId, newIdentityId, resetId, issuedAt, authorityKeyId };
|
|
269
293
|
break;
|
|
270
294
|
}
|
|
295
|
+
case DocumentType.AccountSuccession: {
|
|
296
|
+
const accountName = r.name();
|
|
297
|
+
const accountId = r.take(HASH_BYTES);
|
|
298
|
+
const previousIdentityId = r.take(HASH_BYTES);
|
|
299
|
+
const newIdentityId = r.take(HASH_BYTES);
|
|
300
|
+
const previousGeneration = r.u64();
|
|
301
|
+
const newGeneration = r.u64();
|
|
302
|
+
const newCertificateHash = r.take(HASH_BYTES);
|
|
303
|
+
const successionId = r.take(16);
|
|
304
|
+
const issuedAt = r.u64();
|
|
305
|
+
const authorityKeyId = r.take(16);
|
|
306
|
+
if (equalBytes(previousIdentityId, newIdentityId)) throw malformed(stage, "previous and new identity equal");
|
|
307
|
+
if (previousGeneration < 1n || newGeneration !== previousGeneration + 1n) throw malformed(stage, "new generation must be previous plus one");
|
|
308
|
+
if (successionId.every((b) => b === 0)) throw malformed(stage, "succession id zero");
|
|
309
|
+
fields = { type, accountName, accountId, previousIdentityId, newIdentityId, previousGeneration, newGeneration, newCertificateHash, successionId, issuedAt, authorityKeyId };
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
271
312
|
case DocumentType.ResetKeyList: {
|
|
272
313
|
const version = r.u64();
|
|
273
314
|
const issuedAt = r.u64();
|
|
@@ -469,6 +510,28 @@ export function buildResetReceipt(f: Omit<AccountResetReceipt, "type">, resetSec
|
|
|
469
510
|
return signed(encodeResetReceiptBody(f), "reset-sig", resetSecretKey);
|
|
470
511
|
}
|
|
471
512
|
|
|
513
|
+
export function encodeSuccessionBody(f: Omit<AccountSuccession, "type">): Uint8Array {
|
|
514
|
+
return new Writer()
|
|
515
|
+
.u8(WIRE_VERSION)
|
|
516
|
+
.u8(DocumentType.AccountSuccession)
|
|
517
|
+
.name(f.accountName)
|
|
518
|
+
.bytes(f.accountId)
|
|
519
|
+
.bytes(f.previousIdentityId)
|
|
520
|
+
.bytes(f.newIdentityId)
|
|
521
|
+
.u64(f.previousGeneration)
|
|
522
|
+
.u64(f.newGeneration)
|
|
523
|
+
.bytes(f.newCertificateHash)
|
|
524
|
+
.bytes(f.successionId)
|
|
525
|
+
.u64(f.issuedAt)
|
|
526
|
+
.bytes(f.authorityKeyId)
|
|
527
|
+
.finish();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Signed by an operational key the current reset-key list allows; the host signs it when it commits a succession. */
|
|
531
|
+
export function buildSuccession(f: Omit<AccountSuccession, "type">, authoritySecretKey: Uint8Array): Uint8Array {
|
|
532
|
+
return signed(encodeSuccessionBody(f), "succession-sig", authoritySecretKey);
|
|
533
|
+
}
|
|
534
|
+
|
|
472
535
|
export function encodeResetKeyListBody(f: Omit<ResetKeyList, "type">): Uint8Array {
|
|
473
536
|
const keys = [...f.keys].sort((a, b) => compareBytes(a.keyId, b.keyId));
|
|
474
537
|
const w = new Writer().u8(WIRE_VERSION).u8(DocumentType.ResetKeyList).u64(f.version).u64(f.issuedAt).u64(f.expiresAt).u8(keys.length);
|
package/src/crypto.ts
CHANGED
package/src/errors.ts
CHANGED
|
@@ -31,6 +31,16 @@ export type ErrorCode =
|
|
|
31
31
|
| "ROTATION_REQUIRED"
|
|
32
32
|
| "COUNTER_EXHAUSTED"
|
|
33
33
|
| "RECIPIENT_LIMIT"
|
|
34
|
+
/** A succession does not bind: certificate, identity, generation, account or previous state do not match (revision 9). */
|
|
35
|
+
| "SUCCESSION_BINDING"
|
|
36
|
+
/** A succession names a retired identity as previous or new; retirement is permanent. */
|
|
37
|
+
| "SUPERSEDED_IDENTITY"
|
|
38
|
+
/** Account recovery: the proposal has no valid authorization, or it expired, or a definitive conflict ended the proposal. */
|
|
39
|
+
| "AUTHORIZATION_REQUIRED"
|
|
40
|
+
| "AUTHORIZATION_EXPIRED"
|
|
41
|
+
| "REPLACEMENT_CONFLICT"
|
|
42
|
+
/** The outcome of a submitted succession is not yet known; retry or look it up. */
|
|
43
|
+
| "SUCCESSION_PENDING"
|
|
34
44
|
// Delivery
|
|
35
45
|
| "CLOCK_SKEW"
|
|
36
46
|
| "EXPIRED"
|
|
@@ -51,7 +61,8 @@ const RETRYABLE: ReadonlySet<ErrorCode> = new Set<ErrorCode>([
|
|
|
51
61
|
"ROTATION_REQUIRED",
|
|
52
62
|
"LOCKED",
|
|
53
63
|
"STORAGE_FAILURE",
|
|
54
|
-
"NETWORK"
|
|
64
|
+
"NETWORK",
|
|
65
|
+
"SUCCESSION_PENDING"
|
|
55
66
|
]);
|
|
56
67
|
|
|
57
68
|
/**
|
package/src/identity.ts
CHANGED
|
@@ -32,7 +32,10 @@ import { KavachError } from "./errors.js";
|
|
|
32
32
|
export interface AccountPin {
|
|
33
33
|
readonly accountName: string;
|
|
34
34
|
readonly accountKey: Uint8Array;
|
|
35
|
+
/** The identity of the current key: `H(C("identity") || name || AK)`. Changes at a succession. */
|
|
35
36
|
readonly identityId: Uint8Array;
|
|
37
|
+
/** The account id: the identity at enrollment, what every document names. Constant across successions. */
|
|
38
|
+
readonly accountId: Uint8Array;
|
|
36
39
|
readonly generation: bigint;
|
|
37
40
|
readonly certificateHash: Uint8Array;
|
|
38
41
|
readonly certificate: Uint8Array;
|
|
@@ -48,7 +51,7 @@ export type CertificateOutcome =
|
|
|
48
51
|
| { readonly outcome: "ACCEPTED"; readonly pin: AccountPin; readonly rotate: boolean; readonly previous: AccountPin | undefined }
|
|
49
52
|
| { readonly outcome: "DUPLICATE"; readonly pin: AccountPin };
|
|
50
53
|
|
|
51
|
-
export function pinFromCertificate(doc: Parsed<AccountDevice>, previous?: AccountPin): AccountPin {
|
|
54
|
+
export function pinFromCertificate(doc: Parsed<AccountDevice>, previous?: AccountPin, accountId?: Uint8Array): AccountPin {
|
|
52
55
|
const iid = identityId(doc.accountName, doc.accountKey);
|
|
53
56
|
const ids = previous ? [...previous.deviceIds] : [];
|
|
54
57
|
if (!ids.some((d) => equalBytes(d, doc.deviceId))) ids.push(doc.deviceId);
|
|
@@ -56,6 +59,9 @@ export function pinFromCertificate(doc: Parsed<AccountDevice>, previous?: Accoun
|
|
|
56
59
|
accountName: doc.accountName,
|
|
57
60
|
accountKey: doc.accountKey,
|
|
58
61
|
identityId: iid,
|
|
62
|
+
// The account id is carried from the previous pin (a succession keeps it) or given by the
|
|
63
|
+
// caller who resolved the chain; a first contact is its own root.
|
|
64
|
+
accountId: accountId ?? previous?.accountId ?? iid,
|
|
59
65
|
generation: doc.generation,
|
|
60
66
|
certificateHash: doc.bodyHash,
|
|
61
67
|
certificate: concat(doc.body, doc.signature),
|
|
@@ -111,13 +117,17 @@ export interface OwnAccount {
|
|
|
111
117
|
readonly accountName: string;
|
|
112
118
|
readonly accountSeed: Uint8Array;
|
|
113
119
|
readonly account: SigningKeypair;
|
|
120
|
+
/** The identity of this key. */
|
|
114
121
|
readonly identityId: Uint8Array;
|
|
122
|
+
/** The account id: this identity for an account enrolled under this key, the root of the chain otherwise. */
|
|
123
|
+
readonly accountId: Uint8Array;
|
|
115
124
|
}
|
|
116
125
|
|
|
117
|
-
export function openAccount(accountName: string, accountSeed: Uint8Array): OwnAccount {
|
|
126
|
+
export function openAccount(accountName: string, accountSeed: Uint8Array, accountId?: Uint8Array): OwnAccount {
|
|
118
127
|
if (accountSeed.length !== SEED_BYTES) throw new KavachError("INVALID_KEY", "openAccount");
|
|
119
128
|
const account = accountKeypair(accountSeed);
|
|
120
|
-
|
|
129
|
+
const iid = identityId(accountName, account.publicKey);
|
|
130
|
+
return { accountName, accountSeed, account, identityId: iid, accountId: accountId ?? iid };
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
export function newAccountSeed(): Uint8Array {
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @plane31/blind-relay: Kavach v1 protocol (revision
|
|
2
|
+
* @plane31/blind-relay: Kavach v1 protocol (revision 9, wire version 5).
|
|
3
3
|
*
|
|
4
4
|
* This entry is what an application imports: the client, its types, the errors and the few
|
|
5
5
|
* helpers an app needs around it. Adapter and host authors import `@plane31/blind-relay/adapters`;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `@plane31/blind-relay/sodium`. Everything else is internal and reachable only by relative import.
|
|
8
8
|
*/
|
|
9
9
|
export const PROTOCOL_NAME = "Kavach";
|
|
10
|
-
export const PROTOCOL_REVISION =
|
|
10
|
+
export const PROTOCOL_REVISION = 9;
|
|
11
11
|
|
|
12
12
|
// The client ---------------------------------------------------------------------------------------
|
|
13
13
|
export {
|
|
@@ -23,10 +23,12 @@ export {
|
|
|
23
23
|
type HistoryPage,
|
|
24
24
|
type HistoryQuery,
|
|
25
25
|
type Inspection,
|
|
26
|
+
type KeyChangeEntry,
|
|
26
27
|
type Newest,
|
|
27
28
|
type OpenOptions,
|
|
28
29
|
type PauseReason,
|
|
29
30
|
type ReceiveResult,
|
|
31
|
+
type ReplacementProposal,
|
|
30
32
|
type SyncError,
|
|
31
33
|
type SyncOptions,
|
|
32
34
|
type SyncReport
|
|
@@ -39,7 +41,7 @@ export { KavachError, isKavachError, type ErrorCode } from "./errors.js";
|
|
|
39
41
|
export type { RecordKind } from "./codec.js";
|
|
40
42
|
|
|
41
43
|
// Configuration ------------------------------------------------------------------------------------
|
|
42
|
-
export { validatePolicy, type KindDeclaration, type RetentionPolicy } from "./policy.js";
|
|
44
|
+
export { validatePolicy, type KindDeclaration, type ReplacementModel, type RetentionPolicy } from "./policy.js";
|
|
43
45
|
export type { ResetTrustConfig } from "./reset.js";
|
|
44
46
|
export { setCryptoBackend, type CryptoBackend } from "./crypto.js";
|
|
45
47
|
|
package/src/policy.ts
CHANGED
|
@@ -21,8 +21,18 @@ export interface KindDeclaration {
|
|
|
21
21
|
readonly headerClass: 0 | 1 | 2 | 3;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Which replacement models the product enables (protocol section 4, revision 9): `continuity`,
|
|
26
|
+
* the device holds the account key, recovered through the platform or a code, and the host can
|
|
27
|
+
* never rebind a key; `recovery`, the host's authority signs a succession to fresh keys after
|
|
28
|
+
* the product's fresh authentication; or `both`. A property of the app build, enforced by every
|
|
29
|
+
* verifying peer: a client without `recovery` rejects every succession document.
|
|
30
|
+
*/
|
|
31
|
+
export type ReplacementModel = "continuity" | "recovery" | "both";
|
|
32
|
+
|
|
24
33
|
export interface RetentionPolicy {
|
|
25
34
|
readonly version: number;
|
|
35
|
+
readonly replacement: ReplacementModel;
|
|
26
36
|
readonly deliveryWindowMs: bigint;
|
|
27
37
|
readonly appendRetryWindowMs: bigint;
|
|
28
38
|
readonly hygieneRotationMs: bigint;
|
|
@@ -34,7 +44,7 @@ export interface RetentionPolicy {
|
|
|
34
44
|
/** The largest kind the envelope carries. */
|
|
35
45
|
export const MAX_WIRE_KIND = 255;
|
|
36
46
|
|
|
37
|
-
const FIELDS: readonly (keyof RetentionPolicy)[] = ["version", "deliveryWindowMs", "appendRetryWindowMs", "hygieneRotationMs", "maxOutboxRecords", "maxOutboxBytes", "kinds"];
|
|
47
|
+
const FIELDS: readonly (keyof RetentionPolicy)[] = ["version", "replacement", "deliveryWindowMs", "appendRetryWindowMs", "hygieneRotationMs", "maxOutboxRecords", "maxOutboxBytes", "kinds"];
|
|
38
48
|
|
|
39
49
|
/** A missing or malformed policy is rejected; the SDK never substitutes defaults. */
|
|
40
50
|
export function validatePolicy(policy: unknown): RetentionPolicy {
|
|
@@ -66,8 +76,10 @@ export function validatePolicy(policy: unknown): RetentionPolicy {
|
|
|
66
76
|
if (headerClass !== 0 && headerClass !== 1 && headerClass !== 2 && headerClass !== 3) throw new KavachError("MALFORMED", stage, `kinds[${i}].headerClass must be 0 to 3`);
|
|
67
77
|
return { kind, retentionMs: big(`kinds[${i}].retentionMs`, k.retentionMs), queue: k.queue, history: k.history, newest: k.newest, headerClass };
|
|
68
78
|
});
|
|
79
|
+
if (p.replacement !== "continuity" && p.replacement !== "recovery" && p.replacement !== "both") throw new KavachError("MALFORMED", stage, "replacement must be continuity, recovery or both");
|
|
69
80
|
return {
|
|
70
81
|
version: int("version", p.version),
|
|
82
|
+
replacement: p.replacement,
|
|
71
83
|
deliveryWindowMs: big("deliveryWindowMs", p.deliveryWindowMs),
|
|
72
84
|
appendRetryWindowMs: big("appendRetryWindowMs", p.appendRetryWindowMs),
|
|
73
85
|
hygieneRotationMs: big("hygieneRotationMs", p.hygieneRotationMs),
|
|
@@ -86,3 +98,13 @@ export function kindOf(policy: RetentionPolicy, kind: number): KindDeclaration |
|
|
|
86
98
|
export function historyKinds(policy: RetentionPolicy): readonly number[] {
|
|
87
99
|
return policy.kinds.filter((k) => k.history).map((k) => k.kind);
|
|
88
100
|
}
|
|
101
|
+
|
|
102
|
+
/** Whether a deployment accepts successions at all; a peer without it rejects every type 9 document. */
|
|
103
|
+
export function acceptsSuccession(policy: RetentionPolicy): boolean {
|
|
104
|
+
return policy.replacement !== "continuity";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Whether a device may replace itself by holding the account key (restoreAccount). */
|
|
108
|
+
export function allowsContinuity(policy: RetentionPolicy): boolean {
|
|
109
|
+
return policy.replacement !== "recovery";
|
|
110
|
+
}
|
package/src/rows.ts
CHANGED
|
@@ -14,6 +14,7 @@ export function pinToRow(p: AccountPin): PinRow {
|
|
|
14
14
|
accountName: p.accountName,
|
|
15
15
|
accountKey: toHex(p.accountKey),
|
|
16
16
|
identityId: toHex(p.identityId),
|
|
17
|
+
accountId: toHex(p.accountId),
|
|
17
18
|
generation: big(p.generation),
|
|
18
19
|
certificateHash: toHex(p.certificateHash),
|
|
19
20
|
certificate: toHex(p.certificate),
|
|
@@ -30,6 +31,7 @@ export function rowToPin(r: PinRow): AccountPin {
|
|
|
30
31
|
accountName: r.accountName,
|
|
31
32
|
accountKey: fromHex(r.accountKey),
|
|
32
33
|
identityId: fromHex(r.identityId),
|
|
34
|
+
accountId: fromHex(r.accountId ?? r.identityId),
|
|
33
35
|
generation: unbig(r.generation),
|
|
34
36
|
certificateHash: fromHex(r.certificateHash),
|
|
35
37
|
certificate: fromHex(r.certificate),
|
package/src/store.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface SecretStore {
|
|
|
17
17
|
delete(name: SecretName): Promise<void>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export type SecretName = "accountSeed" | "deviceSeed" | "recoveryKey" | "storageKey" | "instanceId" | "pendingReset";
|
|
20
|
+
export type SecretName = "accountSeed" | "deviceSeed" | "recoveryKey" | "storageKey" | "instanceId" | "pendingReset" | "pendingReplacement";
|
|
21
21
|
|
|
22
22
|
// Logical tables. Rows are JSON-safe: bytes as hex strings, u64 as decimal strings.
|
|
23
23
|
|
|
@@ -29,11 +29,15 @@ export interface MetaRow {
|
|
|
29
29
|
historyIndex?: 1;
|
|
30
30
|
/** Set once the newest slots are built from history; a store from before the slot table is rebuilt on open. */
|
|
31
31
|
newest?: 1;
|
|
32
|
+
/** The account id (revision 9): the identity at enrollment, constant across successions. Absent on a store from before revision 9, where it equals `identityId`. */
|
|
33
|
+
accountId?: string;
|
|
32
34
|
instanceId: string;
|
|
33
35
|
accountName: string;
|
|
34
36
|
identityId: string;
|
|
35
37
|
certificateHash: string;
|
|
36
38
|
state: StoreState;
|
|
39
|
+
/** Set by account recovery: discovery of this account's circles through the host, page by page, until complete; failed circles wait in `retry`. */
|
|
40
|
+
bootstrap?: { cursor?: string; complete: boolean; retry?: BootstrapRetryRow[]; pagesDone?: boolean };
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export interface SelfRow {
|
|
@@ -47,7 +51,10 @@ export interface SelfRow {
|
|
|
47
51
|
export interface PinRow {
|
|
48
52
|
accountName: string;
|
|
49
53
|
accountKey: string;
|
|
54
|
+
/** The account's current identity (revision 9); before it, the one identity an account ever had. */
|
|
50
55
|
identityId: string;
|
|
56
|
+
/** The account id: the identity at enrollment. Absent on a row from before revision 9, where it is `identityId`. */
|
|
57
|
+
accountId?: string;
|
|
51
58
|
generation: string;
|
|
52
59
|
certificateHash: string;
|
|
53
60
|
certificate: string;
|
|
@@ -244,8 +251,9 @@ export interface PendingResetRow {
|
|
|
244
251
|
resetId: string;
|
|
245
252
|
oldIdentityId: string;
|
|
246
253
|
certificate: string;
|
|
247
|
-
|
|
248
|
-
|
|
254
|
+
/** Absent on a recovery-only profile without a provider: no vault. */
|
|
255
|
+
vault: string | undefined;
|
|
256
|
+
vaultHash: string | undefined;
|
|
249
257
|
}
|
|
250
258
|
|
|
251
259
|
/**
|
|
@@ -265,6 +273,99 @@ export interface EnqueueKeyRow {
|
|
|
265
273
|
sentAt: string;
|
|
266
274
|
}
|
|
267
275
|
|
|
276
|
+
/** One accepted succession transition, keyed `<accountId>:<previousGeneration padded to 20>`; the wire is the latest attestation seen. */
|
|
277
|
+
export interface SuccessionRow {
|
|
278
|
+
accountId: string;
|
|
279
|
+
accountName: string;
|
|
280
|
+
successionId: string;
|
|
281
|
+
previousIdentityId: string;
|
|
282
|
+
newIdentityId: string;
|
|
283
|
+
previousGeneration: string;
|
|
284
|
+
newGeneration: string;
|
|
285
|
+
newCertificateHash: string;
|
|
286
|
+
authorityKeyId: string;
|
|
287
|
+
wire: string;
|
|
288
|
+
acceptedAt: string;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Every identity an account has had, keyed by identity id: which account it belongs to and whether it is retired. */
|
|
292
|
+
export interface IdentityRow {
|
|
293
|
+
accountId: string;
|
|
294
|
+
accountName: string;
|
|
295
|
+
/** The account key of this identity, so a snapshot signed under a retired owner key still verifies. Absent for a chain root whose certificate this device never saw. */
|
|
296
|
+
accountKey?: string;
|
|
297
|
+
/** The generation at which this identity became current (1 for a root). */
|
|
298
|
+
sinceGeneration: string;
|
|
299
|
+
/** Retired as a key identity, by a succession or a reset. The account itself may live on under a successor. */
|
|
300
|
+
retired: boolean;
|
|
301
|
+
/** On the root row only: the account was reset at this time; no member of the chain is a recipient any more. */
|
|
302
|
+
resetAt?: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The durable key-change notice, written in the transaction that accepts a succession or a reset; keyed `<acceptedAt padded>:<accountId>`. */
|
|
306
|
+
export interface KeyChangeRow {
|
|
307
|
+
accountId: string;
|
|
308
|
+
accountName: string;
|
|
309
|
+
previousIdentityId: string;
|
|
310
|
+
newIdentityId: string;
|
|
311
|
+
generation: string;
|
|
312
|
+
reason: "succession" | "reset";
|
|
313
|
+
successionId: string | undefined;
|
|
314
|
+
acceptedAt: string;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** T11: a replacement proposal prepared on a fresh device, kept until it commits or is definitively conflicted. */
|
|
318
|
+
export interface ReplacementRow {
|
|
319
|
+
successionId: string;
|
|
320
|
+
accountName: string;
|
|
321
|
+
accountId: string;
|
|
322
|
+
expectedCertificateHash: string;
|
|
323
|
+
expectedGeneration: string;
|
|
324
|
+
/** The exact certificate bytes under the new key, at expectedGeneration + 1. */
|
|
325
|
+
certificate: string;
|
|
326
|
+
/** The fresh vault at revision 1 the succession installs, so a retry sends identical bytes; absent without a provider. */
|
|
327
|
+
vault: string | undefined;
|
|
328
|
+
vaultHash: string | undefined;
|
|
329
|
+
deviceId: string;
|
|
330
|
+
/** Set by a definitive conflict: the proposal can never commit and the next prepare replaces it. */
|
|
331
|
+
stale: boolean;
|
|
332
|
+
conflictGeneration: string | undefined;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** A circle's sharing preference as the host holds it, with the ordering that makes the latest user action win. */
|
|
336
|
+
export interface PreferenceRow {
|
|
337
|
+
circleId: string;
|
|
338
|
+
enabled: boolean;
|
|
339
|
+
/** The host's revision the device last confirmed; 0 before any confirmation. */
|
|
340
|
+
revision: string;
|
|
341
|
+
/** A change the host has not confirmed, with the local sequence it carries; cleared only by the response to that sequence. */
|
|
342
|
+
pending: { enabled: boolean; sequence: string } | undefined;
|
|
343
|
+
/** The next local sequence to assign. */
|
|
344
|
+
nextSequence: string;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Paged chain retrieval in progress for an account: verified pages persist, the pin moves only when complete. */
|
|
348
|
+
export interface ChainProgressRow {
|
|
349
|
+
accountId: string;
|
|
350
|
+
/** The identity the verified prefix starts from (the pinned identity, or the account id at first contact). */
|
|
351
|
+
startIdentityId: string;
|
|
352
|
+
/** The links verified so far, in order; the pin has not moved for any of them. */
|
|
353
|
+
links: { wire: string; certificate: string }[];
|
|
354
|
+
/** The generation to continue from. */
|
|
355
|
+
nextGeneration: string;
|
|
356
|
+
complete: boolean;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** One circle the host listed during bootstrap that this device could not adopt yet; retried by every sync until it lands. */
|
|
360
|
+
export interface BootstrapRetryRow {
|
|
361
|
+
circleId: string;
|
|
362
|
+
ownerAccountId: string;
|
|
363
|
+
role: "owner" | "member" | "pending";
|
|
364
|
+
snapshot: string | undefined;
|
|
365
|
+
enabled: boolean | undefined;
|
|
366
|
+
preferenceRevision: string;
|
|
367
|
+
}
|
|
368
|
+
|
|
268
369
|
export interface Tables {
|
|
269
370
|
meta: MetaRow;
|
|
270
371
|
self: SelfRow;
|
|
@@ -290,6 +391,12 @@ export interface Tables {
|
|
|
290
391
|
retiredIdentities: { identityId: string };
|
|
291
392
|
clock: ClockRow;
|
|
292
393
|
pendingReset: PendingResetRow;
|
|
394
|
+
successions: SuccessionRow;
|
|
395
|
+
identities: IdentityRow;
|
|
396
|
+
keyChanges: KeyChangeRow;
|
|
397
|
+
replacement: ReplacementRow;
|
|
398
|
+
preferences: PreferenceRow;
|
|
399
|
+
chainProgress: ChainProgressRow;
|
|
293
400
|
}
|
|
294
401
|
|
|
295
402
|
export type TableName = keyof Tables;
|