@kokuin/controller 0.1.0 → 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/lib/events.d.ts CHANGED
@@ -1,6 +1,13 @@
1
- import type { DIDString, ResolvedSigningKey } from '@kokuin/token';
1
+ import type { DIDString } from '@kokuin/token';
2
+ import type { EpochEventCommon } from './epoch-events.js';
2
3
  export declare const DID_PREFIX = "did:kokuin:";
3
- export type EventType = 'icp' | 'rot' | 'rev';
4
+ /**
5
+ * The root chain's event types, and only those: it holds the key-establishing events. Management
6
+ * actions — member adds, renewals, removals and revokes — live in the epoch a chain event opens,
7
+ * never on the chain. A chain entry of any other `t`, a `rev` from an older log included, takes the
8
+ * fold's unknown-type path, so a critical one fails the fold closed.
9
+ */
10
+ export type EventType = 'icp' | 'rot';
4
11
  export type EventCommon = {
5
12
  /** Inception format version. Absent must never be inferred; always written. */
6
13
  v: 1;
@@ -41,7 +48,11 @@ export type InceptionEvent = EventCommon & {
41
48
  */
42
49
  r: string;
43
50
  };
44
- export type SignedEvent<E extends EventCommon = EventCommon> = {
51
+ /**
52
+ * Generic over both tiers: a root-chain event and an epoch event share the envelope and signatures
53
+ * over canonical bytes, and nothing else.
54
+ */
55
+ export type SignedEvent<E extends EventCommon | EpochEventCommon = EventCommon> = {
45
56
  event: E;
46
57
  /** base64url ed25519 signatures over the canonical event bytes, positional against `event.k`. */
47
58
  sigs: Array<string>;
@@ -51,7 +62,14 @@ export type SignedEvent<E extends EventCommon = EventCommon> = {
51
62
  */
52
63
  recoveryKey?: string;
53
64
  };
54
- export declare function signEvent(event: EventCommon, privateKeys: Array<Uint8Array>): Array<string>;
65
+ export declare function signEvent(event: EventCommon | EpochEventCommon, privateKeys: Array<Uint8Array>): Array<string>;
66
+ /**
67
+ * A published key list: an array of strings. `k`, `n`, `ka` and the deny snapshot are carried into
68
+ * the folded state, so a bad shape must be rejected where it is published, not one event away where
69
+ * it is next read (a non-array `n` would throw in the *next* rotate; a non-array `k` is a `KeyState`
70
+ * whose type lies).
71
+ */
72
+ export declare function isKeyList(value: unknown): value is Array<string>;
55
73
  /**
56
74
  * Total: a malformed signature or key yields false rather than throwing, including a body the
57
75
  * canonicalizer refuses. `canonicalBytes` throws on a non-finite number or nesting past
@@ -60,17 +78,7 @@ export declare function signEvent(event: EventCommon, privateKeys: Array<Uint8Ar
60
78
  * total and a direct caller reaches it with a parsed body. `false` is honest: a body with no
61
79
  * canonical encoding has no bytes for a signature to be over.
62
80
  */
63
- export declare function verifySignatures(event: EventCommon, sigs: Array<string>, keys: Array<string>): boolean;
64
- /**
65
- * Whether one of an event's signatures was made by `key` — the counterpart of
66
- * {@link verifySignatures} for a signer the *event* does not name (resolved from a DID elsewhere). A
67
- * capability-authorised revoke needs exactly this: its author is the capability's `aud`, which the
68
- * fold cannot resolve, so it is handed the key and checks the signature itself.
69
- *
70
- * Total, EdDSA only — all {@link signEvent} produces. An ES256 audience therefore cannot author a
71
- * revoke, which fails closed until events grow a second signature algorithm.
72
- */
73
- export declare function verifyEventSignedBy(signed: SignedEvent, key: ResolvedSigningKey): boolean;
81
+ export declare function verifySignatures(event: EventCommon | EpochEventCommon, sigs: Array<string>, keys: Array<string>): boolean;
74
82
  /**
75
83
  * Deterministic inception. Only seed-derived, canonical material — no timestamp, nonce, or label —
76
84
  * so the DID is a pure function of the seed and profile index. Never add a label: a mistyped one on
@@ -81,6 +89,36 @@ export declare function didFromInception(event: InceptionEvent): DIDString;
81
89
  /** The did:kokuin: identifier a seed and profile produce, without signing an inception. */
82
90
  export declare function didFor(seed: Uint8Array, profile: number): DIDString;
83
91
  export declare function verifyInception(signed: SignedEvent<InceptionEvent>, did: string): boolean;
92
+ /**
93
+ * One management member as a snapshot carries it: the DID it acts as, the Ed25519 key that DID is
94
+ * pinned to (in the spelling `k` uses), the digest of the `add` that admitted it — its incarnation,
95
+ * which renewals and removals name — and its lease end in unix seconds.
96
+ *
97
+ * The fold never reads `lease` against a clock: it bounds the grants the member mints, nothing on the
98
+ * log. Only a removal ends a member's log authority.
99
+ */
100
+ export type MemberEntry = {
101
+ did: string;
102
+ key: string;
103
+ add: string;
104
+ lease: number;
105
+ };
106
+ /**
107
+ * The whole management state a root-chain event leaves behind: the member set and the deny set. The
108
+ * state after the event **is** this snapshot — nothing before it contributes — so it is root's
109
+ * chosen replacement state, not a proven merge of everything that happened before it.
110
+ */
111
+ export type StateSnapshot = {
112
+ members: Array<MemberEntry>;
113
+ deny: Array<string>;
114
+ };
115
+ /**
116
+ * The shape of one snapshot member, nothing more: whether the DID carries the key, whether the key
117
+ * is Ed25519, and whether it collides with a root key are rules over the whole snapshot and its chain,
118
+ * checked by the fold. `lease` must be a non-negative safe integer — unix seconds — because grants
119
+ * compare `exp` against it, and a fraction, a negative or an unsafe integer has no single reading.
120
+ */
121
+ export declare function isMemberEntry(value: unknown): value is MemberEntry;
84
122
  export type RotateEvent = EventCommon & {
85
123
  t: 'rot';
86
124
  /** Keys the prior event pre-committed, now revealed, multicodec-tagged and multibase-encoded. */
@@ -100,57 +138,47 @@ export type RotateEvent = EventCommon & {
100
138
  * {@link isPublishedRotate}). Not surfaced in `KeyState`: it belongs to a position, not the keys.
101
139
  */
102
140
  a?: string;
103
- /** Deny-set snapshot. Replaces the accumulated set, pruning it. */
104
- d?: Array<string>;
105
- };
106
- export type CreateRotateOptions = {
107
- seal?: string;
108
141
  /**
109
- * The complete deny set this rotate leaves behind — **a replacement, not an addition**. `d` present
110
- * replaces the accumulated set, `d` absent carries it forward. A caller writing one entry meaning
111
- * "also deny this" silently drops every accumulated entry — un-revoking devices and un-retiring
112
- * leaked keys — with no error and nothing in the log saying anything was lost. Build it from the
113
- * folded state with {@link pruneDenySet}. `[]` is the deliberate "clear everything" a cold rotate
114
- * does to recover from a bad management tier. A rotate cannot establish a key its own snapshot
115
- * denies; the fold refuses such an event.
142
+ * Member half of the state snapshot — see {@link StateSnapshot}. Required, with `d`, on every
143
+ * rotate that is not a reset: every rotate is a settlement. A reset carries both or neither.
116
144
  */
117
- denySnapshot?: Array<string>;
118
- /**
119
- * Where the currently-active authority key lives — the position of the last `icp`/`rot`, i.e. the
120
- * fold's `keyGen`/`keySeq`.
121
- *
122
- * Defaults to `prior`'s position, right only while sequence and derivation index still coincide: a
123
- * `rev` advances `s` and establishes no key, so after the first revoke of a generation the two part
124
- * company **and never rejoin**. Every rotate after a revoke needs this, not just the one directly
125
- * on it. Optional rather than required because {@link createRotate} verifies the key it reveals
126
- * against `prior`'s commitment and throws on disagreement — provably right where omitted, an error
127
- * (not an unfoldable event) where not. Take it from `KeyState.keyGen`/`keySeq`.
128
- */
129
- keyPosition?: {
130
- gen: number;
131
- seq: number;
132
- };
145
+ m?: Array<MemberEntry>;
146
+ /** Deny half of the state snapshot. Replaces the set the prior state held outright. */
147
+ d?: Array<string>;
133
148
  };
134
149
  export type CreateRotateParams = {
135
150
  seed: Uint8Array;
136
151
  profile: number;
137
152
  did: string;
138
153
  prior: EventCommon;
139
- options?: CreateRotateOptions;
154
+ /**
155
+ * The complete state this rotate leaves behind — **a replacement, not an addition**. The state
156
+ * after a rotate is its snapshot and nothing before it, so a caller writing one deny entry meaning
157
+ * "also deny this" silently drops every other entry and every member — un-revoking devices and
158
+ * dismissing the management tier — with nothing in the log saying anything was lost. Build it from
159
+ * the folded state with {@link snapshotOf}, which also carries every member forward.
160
+ *
161
+ * Required: there is no "carry the prior state forward" rotate, because the prior state may hold
162
+ * epoch effects a verifier holding only the chain cannot reproduce. Two rules the fold enforces
163
+ * here: a member may not pin a key on, or committed by, this generation's root chain; and a
164
+ * retired-key denial an earlier rotate in the generation carried may not be dropped.
165
+ */
166
+ snapshot: StateSnapshot;
167
+ /** Seal: an anchored external digest — see {@link RotateEvent.a}. */
168
+ seal?: string;
140
169
  };
141
170
  /**
142
171
  * A rotate reveals the keys the prior event pre-committed and commits the next set, signed by the
143
172
  * newly revealed keys (per KERI — what makes a stolen current key unable to rotate). Reproducible
144
- * from the seed alone unless it carries a seal or deny snapshot.
173
+ * from the seed and the snapshot unless it carries a seal.
174
+ *
175
+ * The root chain holds only key-establishing events, so a rotate's sequence and its derivation index
176
+ * are the same number: the key it reveals sits one past `prior`'s position.
145
177
  *
146
178
  * **Throws rather than emitting an event the fold will reject.** The revealed key must be the one
147
- * `prior` committed in `n`, and `options.keyPosition` decides which key gets derived — so a wrong
148
- * position used to produce a well-formed, signed event no fold would accept. The two are checked
149
- * against each other here. When `prior` carries no commitment (a `rev`), `keyPosition` is required:
150
- * the default would derive a key one past the *revoke*, which nothing committed, and this is the one
151
- * case unverifiable from `prior` alone.
179
+ * `prior` committed in `n`, which a prior not built from this chain's own events need not be.
152
180
  */
153
- export declare function createRotate({ seed, profile, did, prior, options, }: CreateRotateParams): SignedEvent<RotateEvent>;
181
+ export declare function createRotate({ seed, profile, did, prior, snapshot, seal, }: CreateRotateParams): SignedEvent<RotateEvent>;
154
182
  /**
155
183
  * A rotate is valid when it chains to the prior digest, its revealed keys match the prior event's
156
184
  * pre-rotation commitment, and its signatures verify against those keys.
@@ -165,15 +193,25 @@ export declare function verifyRotate(signed: SignedEvent<RotateEvent>, prior: {
165
193
  *
166
194
  * Anchored to the inception, not the log head: the recovery key lives on the root-retained
167
195
  * derivation branch and its digest is committed in the deterministic inception, so a restored
168
- * mnemonic can author one with no log knowledge or availability. Carries no options — no seal, `d`
169
- * always `[]` — so a reset is a pure function of `(seed, profile, gen)`, and two blind resets at one
170
- * generation produce identical bytes that resolve as idempotent re-derivation, not duplicity.
196
+ * mnemonic can author one with no log knowledge or availability.
197
+ *
198
+ * **Blind** (no `snapshot`): no seal, `d: []` and no `m`, so a blind reset is a pure function of
199
+ * `(seed, profile, gen)` and two blind resets at one generation produce identical bytes that resolve
200
+ * as idempotent re-derivation, not duplicity. The new epoch starts empty and root re-adds members.
201
+ * This is the path for a root that does not know the log's state.
202
+ *
203
+ * **With a snapshot**: the same reset carries the member and deny sets, so one recovery-key
204
+ * signature is the whole recovery. Two resets at one generation with different bytes are then
205
+ * duplicity — which only the recovery-key holder can cause, and a reset at a higher generation
206
+ * resolves. Members keep their `add` references, but every grant names its generation, so every
207
+ * grant minted under the prior one still dies. The snapshot is checked against the **new**
208
+ * generation's keys, and no retired-key denial of the old one need carry over.
171
209
  *
172
210
  * The root need not know the current generation, only eventually exceed it (no attacker can author a
173
211
  * competing reset at any generation): a blind root starts at `gen = 1` and retries higher if it
174
212
  * learns of one — the cost is a round trip, never loss of control.
175
213
  */
176
- export declare function createReset(seed: Uint8Array, profile: number, gen: number): SignedEvent<RotateEvent>;
214
+ export declare function createReset(seed: Uint8Array, profile: number, gen: number, snapshot?: StateSnapshot): SignedEvent<RotateEvent>;
177
215
  /**
178
216
  * A reset verifies against the committed recovery digest, not the pre-rotation set. Both values it
179
217
  * needs — the anchor digest and the recovery commitment — come from the inception, the *only* place
@@ -185,102 +223,18 @@ export declare function createReset(seed: Uint8Array, profile: number, gen: numb
185
223
  */
186
224
  export declare function verifyReset(signed: SignedEvent<RotateEvent>, inception: InceptionEvent): boolean;
187
225
  /**
188
- * How a `rev` target names a **key** rather than a DID: `#<the multibase key exactly as it appears in
226
+ * How a deny entry names a **key** rather than a DID: `#<the multibase key exactly as it appears in
189
227
  * `k`>`, the same spelling a token's `kid` uses. One spelling for one thing — unambiguous against
190
228
  * `did:…` on sight, so a bare multibase string is not a second accepted encoding, any more than for
191
229
  * `kid`.
192
230
  */
193
231
  export declare const KEY_TARGET_PREFIX = "#";
194
- /** The deny-set spelling of `key` — what {@link createRevoke} takes as its target to deny it. */
232
+ /** The deny-set spelling of `key` — what a snapshot's `d` carries to deny it. */
195
233
  export declare function keyTarget(key: string): string;
196
234
  /**
197
- * The key a `rev` target names, or `null` when it names a DID instead. Total on any string: a target
198
- * of neither form lands in the deny set as the opaque identifier it is. The body is not validated
235
+ * The key a deny entry names, or `null` when it names a DID instead. Total on any string: an entry of
236
+ * neither form lands in the deny set as the opaque identifier it is. The body is not validated
199
237
  * against multibase — a `#` naming nothing denies nothing, exactly as a `did:` naming nobody does,
200
238
  * and rejecting it would only add a way for a well-formed log to stop folding.
201
239
  */
202
240
  export declare function keyFromTarget(target: string): string | null;
203
- export type RevokeEvent = EventCommon & {
204
- t: 'rev';
205
- /**
206
- * What to deny, in one of two spellings, never a capability `jti`.
207
- *
208
- * A **DID** (`did:…`) denies a holder: no capability whose `aud` is that DID is valid from this
209
- * position onward — one entry covers a device's life. A **key** ({@link keyTarget}) denies a
210
- * signer: nothing this profile signed with that key verifies from this position onward, and it
211
- * names a key of *this* profile (the deny set is the subject's own).
212
- *
213
- * One field carries both because they are enforced at opposite ends of the same set: the DID form
214
- * by `@kokuin/capability` against a capability's `aud`, the key form by the resolver against the
215
- * key a `kid` selects. Neither spelling can be mistaken for the other.
216
- */
217
- x: string;
218
- /** A serialized capability authorising a non-authority signer. Verified in the fold. */
219
- cap?: string;
220
- };
221
- export type CreateRevokeParams = {
222
- seed: Uint8Array;
223
- profile: number;
224
- did: string;
225
- prior: EventCommon;
226
- target: string;
227
- keyPosition: {
228
- gen: number;
229
- seq: number;
230
- };
231
- /** A serialized capability authorising this signer, when the signer is not an authority key. */
232
- cap?: string;
233
- };
234
- /**
235
- * Revoke a DID or a key — see {@link RevokeEvent.x} for the two spellings.
236
- *
237
- * Naming the device DID rather than a `jti` makes this one entry per device for its life — covering
238
- * capabilities the verifier has never seen and future re-mints, where per-`jti` revocation would grow
239
- * with every renewal. A key target retires a leaked key for material it *already* signed: `rotate`
240
- * only retires it for new issuance (`resolve` is head-only), but already-issued material verifies
241
- * through `resolveHistoric`, which survives a rotate by design, so a thief holding a rotated-away key
242
- * could still mint one that verified. Retirement is therefore explicit, keeping the promise that a
243
- * routine rotate does not invalidate already-issued material intact.
244
- *
245
- * **A key the profile currently publishes cannot be denied** — the fold rejects it (see the `rev`
246
- * branch of `stepEvent`). Rotate first, then deny the key the rotate retired.
247
- *
248
- * `prior` answers "next sequence, what to chain to"; `keyPosition` separately answers "where the
249
- * active authority key lives". They coincide only for an `icp`/`rot` prior — a `rev` establishes no
250
- * key, so a revoke on a revoke still points at the last `icp`/`rot` position.
251
- */
252
- export declare function createRevoke({ seed, profile, did, prior, target, keyPosition, cap, }: CreateRevokeParams): SignedEvent<RevokeEvent>;
253
- export type CreateRevokeOptions = {
254
- /** A serialized capability authorising this signer, when the signer is not an authority key. */
255
- cap?: string;
256
- };
257
- export type CreateRevokeWithKeyParams = {
258
- privateKey: Uint8Array;
259
- did: string;
260
- prior: EventCommon;
261
- target: string;
262
- /** A serialized capability authorising this signer, when the signer is not an authority key. */
263
- cap?: string;
264
- };
265
- /**
266
- * Revoke a DID, signing with an Ed25519 key the caller already holds rather than deriving one from
267
- * the profile seed.
268
- *
269
- * The builder for the actor a capability-authorised revoke exists for: a device holding a management
270
- * capability, which by design never receives the profile sub-seed. `createRevoke` can only sign as
271
- * the profile itself, so without this the feature had no API but re-implementing the signing
272
- * convention per consumer — the duplication `createControllerCapabilityVerifier` prevents on the
273
- * verifying side.
274
- *
275
- * The key is the audience key the capability pins in `cnf`, and the fold checks the event's signature
276
- * against exactly that, so the two must match. Ed25519 only. Takes the private key rather than an
277
- * identity because no identity type here signs raw bytes — a `KeyStore` entry hands back exactly this
278
- * shape. Byte-identical to {@link createRevoke} given the same key; no `keyPosition`, which exists
279
- * only to derive a key from a seed.
280
- */
281
- export declare function createRevokeWithKey({ privateKey, did, prior, target, cap, }: CreateRevokeWithKeyParams): SignedEvent<RevokeEvent>;
282
- /** Authority-signed revoke. Capability-authorised revokes are checked in the fold. */
283
- export declare function verifyRevoke(signed: SignedEvent<RevokeEvent>, prior: {
284
- digest: string;
285
- keys: Array<string>;
286
- }): boolean;