@kokuin/controller 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ # @kokuin/controller
2
+
3
+ ## Installation
4
+
5
+ ```sh
6
+ npm install @kokuin/controller
7
+ ```
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Deepest nesting {@link canonicalBytes} will encode (top-level value is depth 1). The RFC 8785
3
+ * encoder recurses once per level and, like `JSON.stringify`, only fails at native stack exhaustion —
4
+ * a few kilobytes of `[[[[…]]]]` on the wire would be a `RangeError` thrown from a function every
5
+ * event passes through, and from inside `resolveBranches` where one hostile branch would take
6
+ * duplicity detection down for every honest one. So `canonicalBytes` rejects on this bound *before*
7
+ * encoding rather than catching a `RangeError` (which still drives the stack to exhaustion once per
8
+ * event). 64 is far above anything this stack canonicalizes (an event body reaches depth 3) and far
9
+ * below the ~10⁴ frames a JS stack holds. Raising it is safe; lowering below 3 is not.
10
+ */
11
+ export declare const MAX_CANONICAL_DEPTH = 64;
12
+ /**
13
+ * Whether {@link canonicalBytes} could encode this value without exceeding
14
+ * {@link MAX_CANONICAL_DEPTH} — the total counterpart of the bound above, turning "too deep" into a
15
+ * rejection. Bounded by the same constant, so it is safe even on a cyclic object (which parsed JSON
16
+ * cannot produce but a caller can). Answers only the depth question; prefer {@link isCanonicalizable}
17
+ * for untrusted input, since a non-finite number is reachable from `JSON.parse` and this ignores it.
18
+ */
19
+ export declare function withinCanonicalDepth(value: unknown, depth?: number): boolean;
20
+ /**
21
+ * Whether {@link canonicalBytes} could encode this value at all — every reason it throws, answered
22
+ * instead of raised. The total counterpart of the whole canonicalizer. {@link withinCanonicalDepth}
23
+ * answers only depth, which left a hole: `1e400` on the wire parses to `Infinity`, is a `number`,
24
+ * nests no deeper than anything, and reached `canonicalize` where it *threw* — breaking the total
25
+ * contract of `foldLog`/`verifySignatures` and taking `resolveBranches` down for every honest branch.
26
+ *
27
+ * Answers `false` for exactly what `canonicalize` refuses: a non-finite number (`Infinity`/`NaN`,
28
+ * the first reachable from `JSON.parse` via `1e400`); a type with no encoding (`bigint`, `symbol`,
29
+ * `function`, top-level `undefined` — all caller-only); a non-plain object (caller-only); a value
30
+ * nesting past {@link MAX_CANONICAL_DEPTH}, kept here so one call covers the whole contract.
31
+ *
32
+ * Deliberately **not** rejected: `-0` (encodes as `0`, same value under every comparison this package
33
+ * makes), and integers past `Number.MAX_SAFE_INTEGER` (`JSON.parse` collapses the two wire spellings
34
+ * to one double before anything here sees them, so the shared digest names one value). Bounded by the
35
+ * same constant, so it is safe even on a cyclic object.
36
+ */
37
+ export declare function isCanonicalizable(value: unknown, depth?: number): boolean;
38
+ /**
39
+ * RFC 8785 (JCS) canonical bytes via `@sozai/json`: keys sorted, no insignificant whitespace,
40
+ * `undefined` properties dropped so an absent optional field never encodes as `null`. The DID is the
41
+ * hash of these bytes, so any change here moves every identifier the stack has issued — frozen.
42
+ *
43
+ * Validated by {@link isCanonicalizable} before encoding, not left to the encoder: `@sozai/json`
44
+ * fails a deep value only at native stack exhaustion (see {@link MAX_CANONICAL_DEPTH}) and *encodes*
45
+ * a `Date`/`Map`/typed array rather than refusing it. The guard rejects every value the old encoder
46
+ * did — non-finite, non-plain, unrepresentable, or past the depth bound — so a wrong digest fails
47
+ * loud here instead of silently naming a value the wire form can never carry. It also makes the
48
+ * throw set exactly {@link isCanonicalizable}'s `false` set, the totality `verifyDigest` relies on.
49
+ */
50
+ export declare function canonicalBytes(value: unknown): Uint8Array;
51
+ /** Self-addressing digest: multibase(multihash(canonical bytes)). */
52
+ export declare function digestOf(value: unknown): string;
53
+ /**
54
+ * Total: a malformed digest *or* a value this file cannot canonicalize returns false rather than
55
+ * throwing. The value side is the untrusted one in real use ("is this the body that digest names" is
56
+ * asked about wire bytes), and `false` is correct, not convenient: a digest this package produced can
57
+ * only name a value it could canonicalize. Uses {@link isCanonicalizable}, not
58
+ * {@link withinCanonicalDepth}, since a non-finite number is another way `canonicalize` throws.
59
+ * `canonicalBytes` keeps its throw — its contract is "these exact bytes or nothing".
60
+ */
61
+ export declare function verifyDigest(digest: string, value: unknown): boolean;
@@ -0,0 +1,114 @@
1
+ import { decodeMultibase, encodeMultibase, multihashSHA256, verifyMultihash } from '@kokuin/token';
2
+ import { canonicalize } from '@sozai/json';
3
+ const encoder = new TextEncoder();
4
+ /**
5
+ * Deepest nesting {@link canonicalBytes} will encode (top-level value is depth 1). The RFC 8785
6
+ * encoder recurses once per level and, like `JSON.stringify`, only fails at native stack exhaustion —
7
+ * a few kilobytes of `[[[[…]]]]` on the wire would be a `RangeError` thrown from a function every
8
+ * event passes through, and from inside `resolveBranches` where one hostile branch would take
9
+ * duplicity detection down for every honest one. So `canonicalBytes` rejects on this bound *before*
10
+ * encoding rather than catching a `RangeError` (which still drives the stack to exhaustion once per
11
+ * event). 64 is far above anything this stack canonicalizes (an event body reaches depth 3) and far
12
+ * below the ~10⁴ frames a JS stack holds. Raising it is safe; lowering below 3 is not.
13
+ */ export const MAX_CANONICAL_DEPTH = 64;
14
+ /**
15
+ * Whether {@link canonicalBytes} could encode this value without exceeding
16
+ * {@link MAX_CANONICAL_DEPTH} — the total counterpart of the bound above, turning "too deep" into a
17
+ * rejection. Bounded by the same constant, so it is safe even on a cyclic object (which parsed JSON
18
+ * cannot produce but a caller can). Answers only the depth question; prefer {@link isCanonicalizable}
19
+ * for untrusted input, since a non-finite number is reachable from `JSON.parse` and this ignores it.
20
+ */ export function withinCanonicalDepth(value, depth = 1) {
21
+ // Same comparison at the same point as `canonicalize`'s, so the two agree exactly on the boundary.
22
+ if (depth > MAX_CANONICAL_DEPTH) {
23
+ return false;
24
+ }
25
+ if (value == null || typeof value !== 'object') {
26
+ return true;
27
+ }
28
+ const entries = Array.isArray(value) ? value : Object.values(value);
29
+ return entries.every((entry)=>withinCanonicalDepth(entry, depth + 1));
30
+ }
31
+ /**
32
+ * Whether {@link canonicalBytes} could encode this value at all — every reason it throws, answered
33
+ * instead of raised. The total counterpart of the whole canonicalizer. {@link withinCanonicalDepth}
34
+ * answers only depth, which left a hole: `1e400` on the wire parses to `Infinity`, is a `number`,
35
+ * nests no deeper than anything, and reached `canonicalize` where it *threw* — breaking the total
36
+ * contract of `foldLog`/`verifySignatures` and taking `resolveBranches` down for every honest branch.
37
+ *
38
+ * Answers `false` for exactly what `canonicalize` refuses: a non-finite number (`Infinity`/`NaN`,
39
+ * the first reachable from `JSON.parse` via `1e400`); a type with no encoding (`bigint`, `symbol`,
40
+ * `function`, top-level `undefined` — all caller-only); a non-plain object (caller-only); a value
41
+ * nesting past {@link MAX_CANONICAL_DEPTH}, kept here so one call covers the whole contract.
42
+ *
43
+ * Deliberately **not** rejected: `-0` (encodes as `0`, same value under every comparison this package
44
+ * makes), and integers past `Number.MAX_SAFE_INTEGER` (`JSON.parse` collapses the two wire spellings
45
+ * to one double before anything here sees them, so the shared digest names one value). Bounded by the
46
+ * same constant, so it is safe even on a cyclic object.
47
+ */ export function isCanonicalizable(value, depth = 1) {
48
+ // Same comparison at the same point as `canonicalize`'s, so the two agree exactly on the boundary.
49
+ if (depth > MAX_CANONICAL_DEPTH) {
50
+ return false;
51
+ }
52
+ if (value === null) {
53
+ return true;
54
+ }
55
+ if (typeof value === 'number') {
56
+ return Number.isFinite(value);
57
+ }
58
+ if (typeof value === 'string' || typeof value === 'boolean') {
59
+ return true;
60
+ }
61
+ if (Array.isArray(value)) {
62
+ return value.every((entry)=>isCanonicalizable(entry, depth + 1));
63
+ }
64
+ if (typeof value === 'object') {
65
+ const proto = Object.getPrototypeOf(value);
66
+ if (proto !== Object.prototype && proto !== null) {
67
+ return false;
68
+ }
69
+ // `undefined` members are dropped by `canonicalize` rather than encoded, so they are not a
70
+ // reason to refuse — mirroring the `filter` there.
71
+ return Object.values(value).every((entry)=>entry === undefined || isCanonicalizable(entry, depth + 1));
72
+ }
73
+ return false;
74
+ }
75
+ /**
76
+ * RFC 8785 (JCS) canonical bytes via `@sozai/json`: keys sorted, no insignificant whitespace,
77
+ * `undefined` properties dropped so an absent optional field never encodes as `null`. The DID is the
78
+ * hash of these bytes, so any change here moves every identifier the stack has issued — frozen.
79
+ *
80
+ * Validated by {@link isCanonicalizable} before encoding, not left to the encoder: `@sozai/json`
81
+ * fails a deep value only at native stack exhaustion (see {@link MAX_CANONICAL_DEPTH}) and *encodes*
82
+ * a `Date`/`Map`/typed array rather than refusing it. The guard rejects every value the old encoder
83
+ * did — non-finite, non-plain, unrepresentable, or past the depth bound — so a wrong digest fails
84
+ * loud here instead of silently naming a value the wire form can never carry. It also makes the
85
+ * throw set exactly {@link isCanonicalizable}'s `false` set, the totality `verifyDigest` relies on.
86
+ */ export function canonicalBytes(value) {
87
+ if (!isCanonicalizable(value)) {
88
+ throw new Error('Canonicalization: value is not canonicalizable (non-finite, non-plain, unrepresentable, or ' + `nests deeper than ${MAX_CANONICAL_DEPTH})`);
89
+ }
90
+ // Never `undefined`: `isCanonicalizable` has already rejected the values with no JSON form.
91
+ return encoder.encode(canonicalize(value));
92
+ }
93
+ /** Self-addressing digest: multibase(multihash(canonical bytes)). */ export function digestOf(value) {
94
+ return encodeMultibase(multihashSHA256(canonicalBytes(value)));
95
+ }
96
+ /**
97
+ * Total: a malformed digest *or* a value this file cannot canonicalize returns false rather than
98
+ * throwing. The value side is the untrusted one in real use ("is this the body that digest names" is
99
+ * asked about wire bytes), and `false` is correct, not convenient: a digest this package produced can
100
+ * only name a value it could canonicalize. Uses {@link isCanonicalizable}, not
101
+ * {@link withinCanonicalDepth}, since a non-finite number is another way `canonicalize` throws.
102
+ * `canonicalBytes` keeps its throw — its contract is "these exact bytes or nothing".
103
+ */ export function verifyDigest(digest, value) {
104
+ let expected;
105
+ try {
106
+ expected = decodeMultibase(digest);
107
+ } catch {
108
+ return false;
109
+ }
110
+ if (!isCanonicalizable(value)) {
111
+ return false;
112
+ }
113
+ return verifyMultihash(expected, canonicalBytes(value));
114
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Authority signing key at a position. Lives under the delegable branch, so handing out the
3
+ * profile sub-seed delegates it.
4
+ */
5
+ export declare function authorityPath(profile: number, gen: number, seq: number): string;
6
+ /** Profile-level key agreement key. Durable data encrypts to these, never to device keys. */
7
+ export declare function agreementPath(profile: number, gen: number, seq: number): string;
8
+ /**
9
+ * Recovery key, on the root-retained branch — a sibling of the delegable subtree, never a
10
+ * descendant. Hardened derivation is one-way, so a sub-seed holder cannot reach it and the root
11
+ * always keeps the one key that can supersede.
12
+ */
13
+ export declare function recoveryPath(profile: number): string;
14
+ /**
15
+ * Key material for an algorithm at a path. SLIP-0010 fixes the tree position; HKDF supplies
16
+ * algorithm separation and arbitrary lengths, so adding an algorithm needs a new `info` string
17
+ * and no path change.
18
+ */
19
+ export declare function deriveKeyMaterial(seed: Uint8Array, path: string, alg: string, length?: number): Uint8Array;
20
+ export declare function deriveKeyPair(seed: Uint8Array, path: string, alg: string): {
21
+ privateKey: Uint8Array;
22
+ publicKey: Uint8Array;
23
+ };
@@ -0,0 +1,75 @@
1
+ import { ed25519, x25519 } from '@noble/curves/ed25519.js';
2
+ import { hkdf } from '@noble/hashes/hkdf.js';
3
+ import { sha256 } from '@noble/hashes/sha2.js';
4
+ import HDKey from 'micro-key-producer/slip10.js';
5
+ import { VERSION_TAG } from './version.js';
6
+ const BASE_PATH = "m/44'/876'";
7
+ const DELEGABLE = "0'";
8
+ const ROOT_ONLY = "1'";
9
+ const ROLE_AUTHORITY = "0'";
10
+ const ROLE_AGREEMENT = "1'";
11
+ function assertIndex(name, value) {
12
+ if (!Number.isInteger(value) || value < 0) {
13
+ throw new Error(`Derivation: ${name} must be a non-negative integer, got ${value}`);
14
+ }
15
+ }
16
+ function profilePath(profile) {
17
+ assertIndex('profile', profile);
18
+ return `${BASE_PATH}/${DELEGABLE}/${profile}'`;
19
+ }
20
+ /**
21
+ * Authority signing key at a position. Lives under the delegable branch, so handing out the
22
+ * profile sub-seed delegates it.
23
+ */ export function authorityPath(profile, gen, seq) {
24
+ assertIndex('gen', gen);
25
+ assertIndex('seq', seq);
26
+ return `${profilePath(profile)}/${ROLE_AUTHORITY}/${gen}'/${seq}'`;
27
+ }
28
+ /** Profile-level key agreement key. Durable data encrypts to these, never to device keys. */ export function agreementPath(profile, gen, seq) {
29
+ assertIndex('gen', gen);
30
+ assertIndex('seq', seq);
31
+ return `${profilePath(profile)}/${ROLE_AGREEMENT}/${gen}'/${seq}'`;
32
+ }
33
+ /**
34
+ * Recovery key, on the root-retained branch — a sibling of the delegable subtree, never a
35
+ * descendant. Hardened derivation is one-way, so a sub-seed holder cannot reach it and the root
36
+ * always keeps the one key that can supersede.
37
+ */ export function recoveryPath(profile) {
38
+ assertIndex('profile', profile);
39
+ return `${BASE_PATH}/${ROOT_ONLY}/${profile}'`;
40
+ }
41
+ const KEY_LENGTHS = {
42
+ EdDSA: 32,
43
+ X25519: 32,
44
+ 'ML-DSA-65': 32,
45
+ 'ML-KEM-768': 64
46
+ };
47
+ /**
48
+ * Key material for an algorithm at a path. SLIP-0010 fixes the tree position; HKDF supplies
49
+ * algorithm separation and arbitrary lengths, so adding an algorithm needs a new `info` string
50
+ * and no path change.
51
+ */ export function deriveKeyMaterial(seed, path, alg, length = KEY_LENGTHS[alg] ?? 32) {
52
+ const node = HDKey.fromMasterSeed(seed).derive(path);
53
+ const ikm = node.privateKey;
54
+ if (ikm == null) {
55
+ throw new Error(`Derivation: no private key at path ${path}`);
56
+ }
57
+ const info = new TextEncoder().encode(`${VERSION_TAG}|${alg}`);
58
+ return hkdf(sha256, ikm, undefined, info, length);
59
+ }
60
+ export function deriveKeyPair(seed, path, alg) {
61
+ const privateKey = deriveKeyMaterial(seed, path, alg);
62
+ if (alg === 'EdDSA') {
63
+ return {
64
+ privateKey,
65
+ publicKey: ed25519.getPublicKey(privateKey)
66
+ };
67
+ }
68
+ if (alg === 'X25519') {
69
+ return {
70
+ privateKey,
71
+ publicKey: x25519.getPublicKey(privateKey)
72
+ };
73
+ }
74
+ throw new Error(`Derivation: Unsupported algorithm for key pair derivation: ${alg}`);
75
+ }
@@ -0,0 +1,286 @@
1
+ import type { DIDString, ResolvedSigningKey } from '@kokuin/token';
2
+ export declare const DID_PREFIX = "did:kokuin:";
3
+ export type EventType = 'icp' | 'rot' | 'rev';
4
+ export type EventCommon = {
5
+ /** Inception format version. Absent must never be inferred; always written. */
6
+ v: 1;
7
+ t: EventType;
8
+ /** Profile DID. Omitted from an inception, whose hash *is* the DID. */
9
+ i?: string;
10
+ /** Generation. Incremented only by a reset. */
11
+ g: number;
12
+ /** Sequence within the generation. */
13
+ s: number;
14
+ /** Digest of the previous event. Absent on inception. */
15
+ p?: string;
16
+ /**
17
+ * Criticality. In the common envelope so a verifier reads it without understanding `t`. An unknown
18
+ * event fails the fold closed when true, and is skipped when false.
19
+ */
20
+ crit: boolean;
21
+ };
22
+ export type InceptionEvent = EventCommon & {
23
+ t: 'icp';
24
+ /** Current authority public keys, multicodec-tagged and multibase-encoded. */
25
+ k: Array<string>;
26
+ /**
27
+ * Key agreement public keys -- an OR set, never combined. Carries no pre-rotation commitment: an
28
+ * exposed agreement key discloses past ciphertexts but confers no authority.
29
+ */
30
+ ka: Array<string>;
31
+ /** Digests of the next public keys — pre-rotation. */
32
+ n: Array<string>;
33
+ /** Signing threshold: how many of `k` must sign. **Must equal `k.length`** — see {@link isEnforcedThreshold}. */
34
+ kt: number;
35
+ /** Rotation threshold: how many of `n` must sign the next rotate. **Must equal `n.length`.** */
36
+ nt: number;
37
+ /**
38
+ * Digest of the recovery key. Root-retained, **immutable for the life of the DID** — the only
39
+ * event that can carry one is this one, and this one is the DID. A rotate carrying `r` is refused
40
+ * rather than ignored — see {@link RotateEvent}.
41
+ */
42
+ r: string;
43
+ };
44
+ export type SignedEvent<E extends EventCommon = EventCommon> = {
45
+ event: E;
46
+ /** base64url ed25519 signatures over the canonical event bytes, positional against `event.k`. */
47
+ sigs: Array<string>;
48
+ /**
49
+ * The revealed recovery public key, present only on a reset. The recovery key is committed as a
50
+ * digest and unpublished until used, so a reset must reveal it for the commitment to be checkable.
51
+ */
52
+ recoveryKey?: string;
53
+ };
54
+ export declare function signEvent(event: EventCommon, privateKeys: Array<Uint8Array>): Array<string>;
55
+ /**
56
+ * Total: a malformed signature or key yields false rather than throwing, including a body the
57
+ * canonicalizer refuses. `canonicalBytes` throws on a non-finite number or nesting past
58
+ * `MAX_CANONICAL_DEPTH` (both from `JSON.parse`); the fold's envelope guard rejects such a body one
59
+ * layer out, so this is unreachable through `foldLog` but stays because the export is documented
60
+ * total and a direct caller reaches it with a parsed body. `false` is honest: a body with no
61
+ * canonical encoding has no bytes for a signature to be over.
62
+ */
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;
74
+ /**
75
+ * Deterministic inception. Only seed-derived, canonical material — no timestamp, nonce, or label —
76
+ * so the DID is a pure function of the seed and profile index. Never add a label: a mistyped one on
77
+ * recovery would reproduce a different DID.
78
+ */
79
+ export declare function createInception(seed: Uint8Array, profile: number): SignedEvent<InceptionEvent>;
80
+ export declare function didFromInception(event: InceptionEvent): DIDString;
81
+ /** The did:kokuin: identifier a seed and profile produce, without signing an inception. */
82
+ export declare function didFor(seed: Uint8Array, profile: number): DIDString;
83
+ export declare function verifyInception(signed: SignedEvent<InceptionEvent>, did: string): boolean;
84
+ export type RotateEvent = EventCommon & {
85
+ t: 'rot';
86
+ /** Keys the prior event pre-committed, now revealed, multicodec-tagged and multibase-encoded. */
87
+ k: Array<string>;
88
+ /** Key agreement public keys — an OR set, never combined. Carries no pre-rotation commitment. */
89
+ ka: Array<string>;
90
+ /** Digests of the next public keys — pre-rotation. */
91
+ n: Array<string>;
92
+ /** Signing threshold: how many of `k` must sign. **Must equal `k.length`.** */
93
+ kt: number;
94
+ /** Rotation threshold: how many of `n` must sign the next rotate. **Must equal `n.length`.** */
95
+ nt: number;
96
+ /**
97
+ * Seal: an anchored external digest pinning a high-value grant to this position. Opaque to the fold
98
+ * by design — its meaning belongs to whatever produced it, and nothing in the key state can
99
+ * contradict it. The fold owes it only that it cannot ride in as a non-string (checked in
100
+ * {@link isPublishedRotate}). Not surfaced in `KeyState`: it belongs to a position, not the keys.
101
+ */
102
+ 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
+ /**
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.
116
+ */
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
+ };
133
+ };
134
+ export type CreateRotateParams = {
135
+ seed: Uint8Array;
136
+ profile: number;
137
+ did: string;
138
+ prior: EventCommon;
139
+ options?: CreateRotateOptions;
140
+ };
141
+ /**
142
+ * A rotate reveals the keys the prior event pre-committed and commits the next set, signed by the
143
+ * 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.
145
+ *
146
+ * **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.
152
+ */
153
+ export declare function createRotate({ seed, profile, did, prior, options, }: CreateRotateParams): SignedEvent<RotateEvent>;
154
+ /**
155
+ * A rotate is valid when it chains to the prior digest, its revealed keys match the prior event's
156
+ * pre-rotation commitment, and its signatures verify against those keys.
157
+ */
158
+ export declare function verifyRotate(signed: SignedEvent<RotateEvent>, prior: {
159
+ digest: string;
160
+ n: Array<string>;
161
+ }): boolean;
162
+ /**
163
+ * A reset: a rotate signed by the recovery key that increments the generation and discards
164
+ * everything under the prior one, including every capability minted there.
165
+ *
166
+ * Anchored to the inception, not the log head: the recovery key lives on the root-retained
167
+ * 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.
171
+ *
172
+ * The root need not know the current generation, only eventually exceed it (no attacker can author a
173
+ * competing reset at any generation): a blind root starts at `gen = 1` and retries higher if it
174
+ * learns of one — the cost is a round trip, never loss of control.
175
+ */
176
+ export declare function createReset(seed: Uint8Array, profile: number, gen: number): SignedEvent<RotateEvent>;
177
+ /**
178
+ * A reset verifies against the committed recovery digest, not the pre-rotation set. Both values it
179
+ * needs — the anchor digest and the recovery commitment — come from the inception, the *only* place
180
+ * either can: that is what lets a root holding nothing but its seed author one with no log
181
+ * availability. If the commitment could move, the root would have to read the log to know which key
182
+ * to sign with, and the one recovery path that survives losing the log would be gone.
183
+ * `recoveryPath(profile)` carries no index for the same reason — one recovery key per profile, for
184
+ * its lifetime. See {@link RotateEvent} for what was removed.
185
+ */
186
+ export declare function verifyReset(signed: SignedEvent<RotateEvent>, inception: InceptionEvent): boolean;
187
+ /**
188
+ * How a `rev` target names a **key** rather than a DID: `#<the multibase key exactly as it appears in
189
+ * `k`>`, the same spelling a token's `kid` uses. One spelling for one thing — unambiguous against
190
+ * `did:…` on sight, so a bare multibase string is not a second accepted encoding, any more than for
191
+ * `kid`.
192
+ */
193
+ export declare const KEY_TARGET_PREFIX = "#";
194
+ /** The deny-set spelling of `key` — what {@link createRevoke} takes as its target to deny it. */
195
+ export declare function keyTarget(key: string): string;
196
+ /**
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
199
+ * against multibase — a `#` naming nothing denies nothing, exactly as a `did:` naming nobody does,
200
+ * and rejecting it would only add a way for a well-formed log to stop folding.
201
+ */
202
+ 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;