@plane31/blind-relay 0.1.2 → 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
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account succession (protocol section 4, revision 9; amendment A1): the host's authority binds
|
|
3
|
+
* an account to new keys with continuity of its relationships. This module is the verifier a
|
|
4
|
+
* peer runs on a type 9 document and the chain it keeps per account, in the order the
|
|
5
|
+
* specification gives: an accepted transition stays trusted; a received document is
|
|
6
|
+
* authenticated under the current key list; its transition is compared with the recorded ones;
|
|
7
|
+
* only a new transition meets the pin, generation, retirement and certificate checks.
|
|
8
|
+
*/
|
|
9
|
+
import { equalBytes, toHex } from "./bytes.js";
|
|
10
|
+
import { DocumentType, expectType, parseDocument, verifyDocument, type AccountDevice, type AccountSuccession, type Parsed } from "./codec.js";
|
|
11
|
+
import { identityId } from "./crypto.js";
|
|
12
|
+
import { KavachError } from "./errors.js";
|
|
13
|
+
import { pinFromCertificate, type AccountPin } from "./identity.js";
|
|
14
|
+
import { acceptsSuccession, type RetentionPolicy } from "./policy.js";
|
|
15
|
+
import type { ResetTrust } from "./reset.js";
|
|
16
|
+
import { big, pinToRow, rowToPin, unbig } from "./rows.js";
|
|
17
|
+
import { t3ApplyChange } from "./transactions.js";
|
|
18
|
+
import type { KeyChangeRow, SuccessionRow, Tx } from "./store.js";
|
|
19
|
+
|
|
20
|
+
/** The immutable part of a succession: what happened to the account. Two documents with one transition are one succession. */
|
|
21
|
+
export interface SuccessionTransition {
|
|
22
|
+
readonly accountName: string;
|
|
23
|
+
readonly accountId: string;
|
|
24
|
+
readonly successionId: string;
|
|
25
|
+
readonly previousIdentityId: string;
|
|
26
|
+
readonly newIdentityId: string;
|
|
27
|
+
readonly previousGeneration: bigint;
|
|
28
|
+
readonly newGeneration: bigint;
|
|
29
|
+
readonly newCertificateHash: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function transitionOf(doc: AccountSuccession): SuccessionTransition {
|
|
33
|
+
return {
|
|
34
|
+
accountName: doc.accountName,
|
|
35
|
+
accountId: toHex(doc.accountId),
|
|
36
|
+
successionId: toHex(doc.successionId),
|
|
37
|
+
previousIdentityId: toHex(doc.previousIdentityId),
|
|
38
|
+
newIdentityId: toHex(doc.newIdentityId),
|
|
39
|
+
previousGeneration: doc.previousGeneration,
|
|
40
|
+
newGeneration: doc.newGeneration,
|
|
41
|
+
newCertificateHash: toHex(doc.newCertificateHash)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sameTransition(a: SuccessionTransition, b: SuccessionTransition): boolean {
|
|
46
|
+
return (
|
|
47
|
+
a.accountName === b.accountName &&
|
|
48
|
+
a.accountId === b.accountId &&
|
|
49
|
+
a.successionId === b.successionId &&
|
|
50
|
+
a.previousIdentityId === b.previousIdentityId &&
|
|
51
|
+
a.newIdentityId === b.newIdentityId &&
|
|
52
|
+
a.previousGeneration === b.previousGeneration &&
|
|
53
|
+
a.newGeneration === b.newGeneration &&
|
|
54
|
+
a.newCertificateHash === b.newCertificateHash
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function rowTransition(r: SuccessionRow): SuccessionTransition {
|
|
59
|
+
return { accountName: r.accountName, accountId: r.accountId, successionId: r.successionId, previousIdentityId: r.previousIdentityId, newIdentityId: r.newIdentityId, previousGeneration: unbig(r.previousGeneration), newGeneration: unbig(r.newGeneration), newCertificateHash: r.newCertificateHash };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function successionKey(accountId: string, previousGeneration: bigint): string {
|
|
63
|
+
return `${accountId}:${previousGeneration.toString().padStart(20, "0")}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The state a chain is validated against, the same for a pinned contact, a first contact and a
|
|
68
|
+
* restoring device: where the account stands and what it has used up. Advancing it is pure, so
|
|
69
|
+
* the state after a verified prefix is recomputed from the prefix and never persisted separately.
|
|
70
|
+
*/
|
|
71
|
+
export interface ChainState {
|
|
72
|
+
readonly accountId: string;
|
|
73
|
+
readonly accountName: string;
|
|
74
|
+
/** The identity a transition must leave from. */
|
|
75
|
+
readonly identityId: string;
|
|
76
|
+
/** The generation a transition may not fall below (0 at a root nobody has pinned). */
|
|
77
|
+
readonly generation: bigint;
|
|
78
|
+
/** Identities no transition may leave from or arrive at: retired by successions or by resets. */
|
|
79
|
+
readonly retired: ReadonlySet<string>;
|
|
80
|
+
/** Device ids the account has used, hex. */
|
|
81
|
+
readonly deviceIds: ReadonlySet<string>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The state of an account nobody has pinned yet: its root, with what the reset trust already retired. */
|
|
85
|
+
export function rootState(accountId: Uint8Array, accountName: string, trust: ResetTrust): ChainState {
|
|
86
|
+
return { accountId: toHex(accountId), accountName, identityId: toHex(accountId), generation: 0n, retired: new Set(trust.state.retired), deviceIds: new Set() };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The state at a pin: its current identity and generation, the identities this store retired, and its device ids. */
|
|
90
|
+
export function pinnedState(tx: Pick<Tx, "get" | "list">, pin: AccountPin, trust: ResetTrust): ChainState {
|
|
91
|
+
const accountId = toHex(pin.accountId);
|
|
92
|
+
const retired = new Set(trust.state.retired);
|
|
93
|
+
for (const [k, r] of tx.list("identities")) if (r.accountId === accountId && r.retired) retired.add(k);
|
|
94
|
+
return { accountId, accountName: pin.accountName, identityId: toHex(pin.identityId), generation: pin.generation, retired, deviceIds: new Set(pin.deviceIds.map(toHex)) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The transition rules (section 4, order four), applied to one parsed succession and the
|
|
99
|
+
* certificate served with it against a state. Throws the protocol code; returns the next state.
|
|
100
|
+
* Authentication (order two) and the comparison with recorded transitions (orders one and three)
|
|
101
|
+
* are the caller's, since only a store knows what it recorded.
|
|
102
|
+
*/
|
|
103
|
+
export function validateTransition(doc: AccountSuccession, certificate: Uint8Array, state: ChainState): { next: ChainState; cert: Parsed<AccountDevice> } {
|
|
104
|
+
const stage = "succession";
|
|
105
|
+
const t = transitionOf(doc);
|
|
106
|
+
if (t.accountName !== state.accountName || t.accountId !== state.accountId) throw new KavachError("SUCCESSION_BINDING", stage, "account name or id does not match");
|
|
107
|
+
if (t.previousIdentityId !== state.identityId) throw new KavachError("SUCCESSION_BINDING", stage, "previous identity is not the account's current identity");
|
|
108
|
+
if (t.previousGeneration < state.generation) throw new KavachError("ROLLBACK", stage, "previous generation below the current one");
|
|
109
|
+
if (state.retired.has(t.previousIdentityId) || state.retired.has(t.newIdentityId)) throw new KavachError("SUPERSEDED_IDENTITY", stage, "a retired identity cannot be previous or new");
|
|
110
|
+
const cert = expectType<AccountDevice>(verifyDocument(certificate), DocumentType.AccountDevice, stage);
|
|
111
|
+
if (toHex(cert.bodyHash) !== t.newCertificateHash || cert.accountName !== t.accountName || cert.generation !== t.newGeneration || toHex(identityId(cert.accountName, cert.accountKey)) !== t.newIdentityId) {
|
|
112
|
+
throw new KavachError("SUCCESSION_BINDING", stage, "the certificate does not bind to the succession");
|
|
113
|
+
}
|
|
114
|
+
if (state.deviceIds.has(toHex(cert.deviceId))) throw new KavachError("DEVICE_ID_REUSED", stage);
|
|
115
|
+
const retired = new Set(state.retired);
|
|
116
|
+
retired.add(t.previousIdentityId);
|
|
117
|
+
const deviceIds = new Set(state.deviceIds);
|
|
118
|
+
deviceIds.add(toHex(cert.deviceId));
|
|
119
|
+
return { next: { ...state, identityId: t.newIdentityId, generation: t.newGeneration, retired, deviceIds }, cert };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Authenticate an attestation under the current list (order two). Returns the verified document. */
|
|
123
|
+
export function authenticateSuccession(wire: Uint8Array, trust: ResetTrust, now: bigint | undefined): AccountSuccession {
|
|
124
|
+
const stage = "succession";
|
|
125
|
+
const parsed = expectType<AccountSuccession>(parseDocument(wire), DocumentType.AccountSuccession, stage);
|
|
126
|
+
if (now === undefined || now < trust.state.timeHighWater) throw new KavachError("RESET_CLOCK_UNTRUSTED", stage);
|
|
127
|
+
const list = trust.state.current;
|
|
128
|
+
if (!list || !trust.fresh(now)) throw new KavachError("RESET_KEYS_REQUIRED", stage, "no fresh key list");
|
|
129
|
+
const signer = list.list.keys.find((k) => equalBytes(k.keyId, parsed.authorityKeyId));
|
|
130
|
+
if (!signer) throw new KavachError("RESET_KEY_REVOKED", stage, "the signing key is not in the current list");
|
|
131
|
+
return expectType<AccountSuccession>(verifyDocument(wire, signer.publicKey), DocumentType.AccountSuccession, stage);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface SuccessionEffect {
|
|
135
|
+
readonly outcome: "ACCEPTED" | "DUPLICATE";
|
|
136
|
+
readonly accountId: Uint8Array;
|
|
137
|
+
readonly accountName: string;
|
|
138
|
+
readonly previousIdentityId: Uint8Array;
|
|
139
|
+
readonly newIdentityId: Uint8Array;
|
|
140
|
+
readonly generation: bigint;
|
|
141
|
+
readonly transition: SuccessionTransition;
|
|
142
|
+
/** The pin after the succession (the new one, or the current one for a duplicate). */
|
|
143
|
+
readonly pin: AccountPin;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface SuccessionInput {
|
|
147
|
+
readonly wire: Uint8Array;
|
|
148
|
+
/** The successor's certificate, fetched by `newCertificateHash`. */
|
|
149
|
+
readonly newCertificate: Uint8Array;
|
|
150
|
+
readonly policy: RetentionPolicy;
|
|
151
|
+
readonly trust: ResetTrust;
|
|
152
|
+
/** The trusted clock reading, or `undefined` when the clock cannot be trusted. */
|
|
153
|
+
readonly now: bigint | undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The account id an identity belongs to, as the chain knows it; an identity with no row is its own account (a root never succeeded, or one pinned before revision 9). */
|
|
157
|
+
export function accountIdOf(tx: Pick<Tx, "get">, identityHex: string): string {
|
|
158
|
+
return tx.get("identities", identityHex)?.accountId ?? identityHex;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Every identity retired by a succession or a reset, as hex. */
|
|
162
|
+
export function retiredIdentities(tx: Pick<Tx, "list">): Set<string> {
|
|
163
|
+
return new Set(tx.list("identities").filter(([, r]) => r.retired).map(([k]) => k));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Apply one succession document inside the caller's transaction. Returns the effect for an
|
|
168
|
+
* accepted or duplicate document; throws the protocol code for anything else, having written
|
|
169
|
+
* nothing (an equivocation is recorded by the caller's quarantine).
|
|
170
|
+
*/
|
|
171
|
+
export interface VerifiedLink {
|
|
172
|
+
readonly doc: AccountSuccession;
|
|
173
|
+
readonly wire: Uint8Array;
|
|
174
|
+
readonly certificate: Uint8Array;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Verify links in order from a state, with the same rules a pinned contact applies: each
|
|
179
|
+
* attested under the current list, each transition valid against the state the previous one
|
|
180
|
+
* produced. Nothing is written; the caller records the links once the chain is complete.
|
|
181
|
+
*/
|
|
182
|
+
export function verifyChain(links: readonly { wire: Uint8Array; certificate: Uint8Array }[], input: { state: ChainState; trust: ResetTrust; now: bigint }): { links: VerifiedLink[]; state: ChainState } {
|
|
183
|
+
const out: VerifiedLink[] = [];
|
|
184
|
+
let state = input.state;
|
|
185
|
+
for (const link of links) {
|
|
186
|
+
const doc = authenticateSuccession(link.wire, input.trust, input.now);
|
|
187
|
+
state = validateTransition(doc, link.certificate, state).next;
|
|
188
|
+
out.push({ doc, wire: link.wire, certificate: link.certificate });
|
|
189
|
+
}
|
|
190
|
+
return { links: out, state };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Keep a verified chain as evidence: succession rows, identity rows (retired ones with the keys the chain shows) and key-change rows. */
|
|
194
|
+
export function recordChainIn(tx: Tx, chain: readonly VerifiedLink[], now: bigint): void {
|
|
195
|
+
for (const { doc, wire, certificate } of chain) {
|
|
196
|
+
const t = transitionOf(doc);
|
|
197
|
+
const cert = expectType<AccountDevice>(parseDocument(certificate), DocumentType.AccountDevice, "chain");
|
|
198
|
+
const row: SuccessionRow = { accountId: t.accountId, accountName: t.accountName, successionId: t.successionId, previousIdentityId: t.previousIdentityId, newIdentityId: t.newIdentityId, previousGeneration: big(t.previousGeneration), newGeneration: big(t.newGeneration), newCertificateHash: t.newCertificateHash, authorityKeyId: toHex(doc.authorityKeyId), wire: toHex(wire), acceptedAt: big(now) };
|
|
199
|
+
tx.put("successions", successionKey(t.accountId, t.previousGeneration), row);
|
|
200
|
+
const prev = tx.get("identities", t.previousIdentityId);
|
|
201
|
+
tx.put("identities", t.previousIdentityId, { accountId: t.accountId, accountName: t.accountName, ...(prev?.accountKey ? { accountKey: prev.accountKey } : {}), sinceGeneration: prev?.sinceGeneration ?? big(1n), retired: true });
|
|
202
|
+
const next = tx.get("identities", t.newIdentityId);
|
|
203
|
+
tx.put("identities", t.newIdentityId, { accountId: t.accountId, accountName: t.accountName, accountKey: toHex(cert.accountKey), sinceGeneration: big(t.newGeneration), retired: next?.retired ?? false });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The root identity's row with its key, from a certificate under the root key the host served, when it is one. */
|
|
208
|
+
export function rootIdentityIn(tx: Tx, accountId: Uint8Array, accountName: string, rootCertificate: Uint8Array | undefined): void {
|
|
209
|
+
if (!rootCertificate) return;
|
|
210
|
+
let cert: AccountDevice & { bodyHash: Uint8Array };
|
|
211
|
+
try {
|
|
212
|
+
cert = expectType<AccountDevice>(verifyDocument(rootCertificate), DocumentType.AccountDevice, "chain");
|
|
213
|
+
} catch {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (cert.accountName !== accountName || !equalBytes(identityId(cert.accountName, cert.accountKey), accountId)) return;
|
|
217
|
+
const key = toHex(accountId);
|
|
218
|
+
const row = tx.get("identities", key);
|
|
219
|
+
// Whether the root is retired is the chain's to say (recordChainIn runs first); a root with no
|
|
220
|
+
// succession recorded yet is current.
|
|
221
|
+
tx.put("identities", key, { accountId: key, accountName, accountKey: toHex(cert.accountKey), sinceGeneration: row?.sinceGeneration ?? big(1n), retired: row?.retired ?? false, ...(row?.resetAt !== undefined ? { resetAt: row.resetAt } : {}) });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function applySuccession(tx: Tx, input: SuccessionInput): SuccessionEffect {
|
|
225
|
+
const stage = "succession";
|
|
226
|
+
if (!acceptsSuccession(input.policy)) throw new KavachError("UNSUPPORTED_TYPE", stage, "this deployment does not accept successions");
|
|
227
|
+
const parsed = expectType<AccountSuccession>(parseDocument(input.wire), DocumentType.AccountSuccession, stage);
|
|
228
|
+
const t = transitionOf(parsed);
|
|
229
|
+
|
|
230
|
+
// Order 1: authenticate the attestation under the current list. A failure here records nothing
|
|
231
|
+
// and changes nothing, so an obsolete attestation of an accepted transition is merely dropped.
|
|
232
|
+
const now = input.now;
|
|
233
|
+
authenticateSuccession(input.wire, input.trust, now);
|
|
234
|
+
if (now === undefined) throw new KavachError("RESET_CLOCK_UNTRUSTED", stage);
|
|
235
|
+
|
|
236
|
+
// Order 2: compare the transition with the recorded ones for this account.
|
|
237
|
+
const recorded = tx.list("successions", t.accountId + ":").map(([, r]) => r);
|
|
238
|
+
const atState = recorded.find((r) => unbig(r.previousGeneration) === t.previousGeneration && r.previousIdentityId === t.previousIdentityId);
|
|
239
|
+
const byId = recorded.find((r) => r.successionId === t.successionId);
|
|
240
|
+
const pinRow = tx.get("pins", t.accountId);
|
|
241
|
+
if (atState) {
|
|
242
|
+
if (sameTransition(rowTransition(atState), t)) {
|
|
243
|
+
if (!pinRow) throw new KavachError("STORAGE_FAILURE", stage, "a recorded succession without its pin");
|
|
244
|
+
return { outcome: "DUPLICATE", accountId: parsed.accountId, accountName: t.accountName, previousIdentityId: parsed.previousIdentityId, newIdentityId: parsed.newIdentityId, generation: t.newGeneration, transition: t, pin: rowToPin(pinRow) };
|
|
245
|
+
}
|
|
246
|
+
throw new KavachError("EQUIVOCATION", stage, "two successors for one account state");
|
|
247
|
+
}
|
|
248
|
+
if (byId && !sameTransition(rowTransition(byId), t)) throw new KavachError("EQUIVOCATION", stage, "a known succession id with a different transition");
|
|
249
|
+
|
|
250
|
+
// Order 3: a new transition against the pinned state, with the one validator every path uses.
|
|
251
|
+
if (!pinRow) throw new KavachError("SUCCESSION_BINDING", stage, "no pin for the account");
|
|
252
|
+
const pin = rowToPin(pinRow);
|
|
253
|
+
const { cert } = validateTransition(parsed, input.newCertificate, pinnedState(tx, pin, input.trust));
|
|
254
|
+
|
|
255
|
+
// Accept, all in this transaction: the pin, the certificates, the identities, the transition,
|
|
256
|
+
// the durable notice, and the rotation (T3) that moves the audience to the new key.
|
|
257
|
+
const next = pinFromCertificate(cert, pin);
|
|
258
|
+
tx.put("pins", t.accountId, pinToRow(next));
|
|
259
|
+
const previousCert = tx.get("certificates", toHex(pin.certificateHash));
|
|
260
|
+
if (previousCert) tx.put("certificates", toHex(pin.certificateHash), { ...previousCert, current: false });
|
|
261
|
+
tx.put("certificates", t.newCertificateHash, { identityId: t.accountId, certificateHash: t.newCertificateHash, signingKey: toHex(next.signingKey), boxKey: toHex(next.boxKey), streamId: toHex(next.streamId), generation: big(next.generation), wire: toHex(input.newCertificate), current: true });
|
|
262
|
+
const previousIdentity = tx.get("identities", t.previousIdentityId);
|
|
263
|
+
tx.put("identities", t.previousIdentityId, { accountId: t.accountId, accountName: t.accountName, accountKey: previousIdentity?.accountKey ?? toHex(pin.accountKey), sinceGeneration: previousIdentity?.sinceGeneration ?? big(1n), retired: true });
|
|
264
|
+
tx.put("identities", t.newIdentityId, { accountId: t.accountId, accountName: t.accountName, accountKey: toHex(cert.accountKey), sinceGeneration: big(t.newGeneration), retired: false });
|
|
265
|
+
tx.put("successions", successionKey(t.accountId, t.previousGeneration), { accountId: t.accountId, accountName: t.accountName, successionId: t.successionId, previousIdentityId: t.previousIdentityId, newIdentityId: t.newIdentityId, previousGeneration: big(t.previousGeneration), newGeneration: big(t.newGeneration), newCertificateHash: t.newCertificateHash, authorityKeyId: toHex(parsed.authorityKeyId), wire: toHex(input.wire), acceptedAt: big(now) });
|
|
266
|
+
const notice: KeyChangeRow = { accountId: t.accountId, accountName: t.accountName, previousIdentityId: t.previousIdentityId, newIdentityId: t.newIdentityId, generation: big(t.newGeneration), reason: "succession", successionId: t.successionId, acceptedAt: big(now) };
|
|
267
|
+
tx.put("keyChanges", `${now.toString().padStart(20, "0")}:${t.accountId}:${t.successionId}`, notice);
|
|
268
|
+
t3ApplyChange(tx, { kind: "deviceChanged", identityId: parsed.accountId, certificateHash: cert.bodyHash });
|
|
269
|
+
return { outcome: "ACCEPTED", accountId: parsed.accountId, accountName: t.accountName, previousIdentityId: parsed.previousIdentityId, newIdentityId: parsed.newIdentityId, generation: t.newGeneration, transition: t, pin: next };
|
|
270
|
+
}
|
package/src/testing/clock.ts
CHANGED