@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.
@@ -0,0 +1,74 @@
1
+ import { type FullIdentity, type SigningIdentity } from '@kokuin/token';
2
+ import { type SignedEvent } from './events.js';
3
+ import type { FoldOptions } from './fold.js';
4
+ export type CreateControllerIdentityParams = {
5
+ seed: Uint8Array;
6
+ profile: number;
7
+ log: Array<SignedEvent>;
8
+ };
9
+ /**
10
+ * A signing identity for a `did:kokuin:` profile, whose `iss` is the profile DID.
11
+ *
12
+ * Takes the log and folds it here, so the signing key is always the one that log's current state
13
+ * establishes, never one the caller named. The log is the caller's freshness contract: an identity
14
+ * built from a stale or truncated log signs with a retired key, minting tokens a current verifier
15
+ * rejects. Rebuild from a re-read log after a rotation rather than holding one across it.
16
+ *
17
+ * The key is derived at `keyGen`/`keySeq`, not `gen`/`seq`: a revoke advances the sequence without
18
+ * establishing a key (Amendment A), so deriving at `gen`/`seq` would produce a key never in `k` — an
19
+ * unverifiable token, silently. Every token carries `kid: #<the key that signed it>`, so there is no
20
+ * `kid` parameter — one naming any other key is a request it could not honour, and is rejected on
21
+ * mismatch rather than dropped.
22
+ *
23
+ * Synchronous, and stays so (kubun's apply path depends on it): a log whose revoke carries a
24
+ * capability cannot fold without awaiting a verifier, so it throws here — use
25
+ * {@link createControllerIdentityAsync}.
26
+ *
27
+ * @throws when the log does not fold, or when the derived key is not one of the profile's current
28
+ * authority keys (a wrong `seed` or `profile` for this log).
29
+ */
30
+ export declare function createControllerIdentity({ seed, profile, log, }: CreateControllerIdentityParams): FullIdentity;
31
+ export type CreateControllerIdentityAsyncParams = CreateControllerIdentityParams & {
32
+ options?: FoldOptions;
33
+ };
34
+ /**
35
+ * Async sibling of {@link createControllerIdentity} for a log whose revoke carries a capability
36
+ * authorising a non-authority signer, which only `foldLogAsync` can check. Identical otherwise, so a
37
+ * log without such a revoke resolves to the same identity through either entry point.
38
+ *
39
+ * @param options forwarded to the async fold; without `verifyCapability` such a log still fails to
40
+ * fold rather than being trusted.
41
+ */
42
+ export declare function createControllerIdentityAsync({ seed, profile, log, options, }: CreateControllerIdentityAsyncParams): Promise<FullIdentity>;
43
+ export type CreateControllerIdentityWithKeyParams = {
44
+ privateKey: Uint8Array;
45
+ log: Array<SignedEvent>;
46
+ };
47
+ /**
48
+ * A signing identity for a `did:kokuin:` profile, from the current authority private key rather than
49
+ * the profile seed.
50
+ *
51
+ * The seed form above needs the root seed to derive the key, but the custody tiers say the daily path
52
+ * must not touch it: the root seed lives on a Ledger or cold mnemonic, and a device never receives the
53
+ * profile sub-seed (a sub-seed holder can derive the *next* key and therefore rotate, voiding
54
+ * pre-rotation). This is the same split {@link createRevokeWithKey} makes, for the same reason.
55
+ *
56
+ * What the key in hand can do is exactly the intended granularity: it signs as the profile and cannot
57
+ * rotate, since a rotate must reveal the key the log committed in `n` and only the seed derives that.
58
+ * Losing it costs a rotate-then-deny, not the identity. Takes the raw private key (no identity type
59
+ * here signs raw bytes; a `KeyStore` entry hands back exactly this); the public half is derived from
60
+ * it, so a caller cannot present one key and sign with another.
61
+ *
62
+ * @throws when the log does not fold, or when the key's public half is not one of the profile's
63
+ * current authority keys — a stale log, or a key the profile has rotated away.
64
+ */
65
+ export declare function createControllerIdentityWithKey({ privateKey, log, }: CreateControllerIdentityWithKeyParams): SigningIdentity;
66
+ export type CreateControllerIdentityWithKeyAsyncParams = CreateControllerIdentityWithKeyParams & {
67
+ options?: FoldOptions;
68
+ };
69
+ /**
70
+ * Async sibling of {@link createControllerIdentityWithKey}, for a log whose revoke carries a
71
+ * capability authorising a non-authority signer. The pairing is not incidental: a profile that uses
72
+ * the management tier is the same profile whose daily signer should not hold the seed.
73
+ */
74
+ export declare function createControllerIdentityWithKeyAsync({ privateKey, log, options, }: CreateControllerIdentityWithKeyAsyncParams): Promise<SigningIdentity>;
@@ -0,0 +1,190 @@
1
+ import { createKeyAgreementIdentityForDID, createSigningIdentityForDID } from '@kokuin/token';
2
+ import { ed25519 } from '@noble/curves/ed25519.js';
3
+ import { agreementPath, authorityPath, deriveKeyPair } from './derivation.js';
4
+ import { didFromInception } from './events.js';
5
+ import { encodeKey } from './keys.js';
6
+ import { currentState, currentStateAsync } from './state.js';
7
+ const CONTEXT = 'Controller identity';
8
+ /**
9
+ * The DID the log claims to be, read from its inception before any fold runs — the fold needs the DID
10
+ * to check the inception against, so it cannot be the thing that produces it.
11
+ */ function didFromLog(log) {
12
+ if (log.length === 0) {
13
+ throw new Error(`${CONTEXT}: empty log`);
14
+ }
15
+ const first = log[0];
16
+ if (first.event.t !== 'icp') {
17
+ throw new Error(`${CONTEXT}: first event must be an inception`);
18
+ }
19
+ return didFromInception(first.event);
20
+ }
21
+ /**
22
+ * Stamp `kid` on every token this identity signs, naming the key that produced the signature. The
23
+ * identity derives exactly one key pair, so `kid` is a fact about the signature, not an input: a
24
+ * caller-supplied one is only ever checked, never honoured — dropping a mismatched one silently would
25
+ * mint a token whose header names a key that did not sign it. Both spellings are checked:
26
+ * `options.kid` and `options.header.kid`.
27
+ */ function withKid(identity, kid) {
28
+ return {
29
+ ...identity,
30
+ async signToken (payload, options = {}) {
31
+ for (const supplied of [
32
+ options.kid,
33
+ options.header?.kid
34
+ ]){
35
+ if (supplied != null && supplied !== kid) {
36
+ throw new Error(`${CONTEXT}: cannot sign under kid ${String(supplied)}, this identity holds ${kid}`);
37
+ }
38
+ }
39
+ return identity.signToken(payload, {
40
+ ...options,
41
+ header: {
42
+ ...options.header,
43
+ kid
44
+ }
45
+ });
46
+ }
47
+ };
48
+ }
49
+ /**
50
+ * Bind a private key the caller holds to the DID, having checked the folded state publishes its
51
+ * public half. Shared by all four entry points, so the membership rule and `kid` binding are written
52
+ * once. Membership, not `keys[0]`: a set may publish several keys and the one in hand need not be
53
+ * first (co-signers' keys belong to other holders); what must hold is that the resolver can answer
54
+ * with this key. A key outside the set signs unverifiable tokens, so fail loudly at construction.
55
+ */ function identityForKey({ privateKey, publicKey, did, state, mismatch }) {
56
+ if (state.keys.length === 0) {
57
+ // Defensive, unreachable through either fold today (`verifySignatures` rejects an empty key set,
58
+ // `rev` carries `keys` forward). Kept so a future event type that can empty the set fails here
59
+ // rather than deriving a key for a controller that publishes none. `resolver.ts` holds it too.
60
+ throw new Error(`Controller ${did} has no signing key`);
61
+ }
62
+ const key = encodeKey(publicKey, 'EdDSA');
63
+ if (!state.keys.includes(key)) {
64
+ throw new Error(`${CONTEXT}: ${mismatch} of ${did}`);
65
+ }
66
+ // The resolver picks by `kid` and defaults to `keys[0]`, so a token whose key is not first is
67
+ // unverifiable unless the header names the key that signed it.
68
+ return withKid(createSigningIdentityForDID(did, privateKey), `#${key}`);
69
+ }
70
+ /**
71
+ * Derive the signing key the folded state establishes and bind it to the DID. Shared by the sync and
72
+ * async seed entry points, which differ only in how they reach the state.
73
+ */ function identityForState({ seed, profile, did, state }) {
74
+ const authority = deriveKeyPair(seed, authorityPath(profile, state.keyGen, state.keySeq), 'EdDSA');
75
+ const signing = identityForKey({
76
+ privateKey: authority.privateKey,
77
+ publicKey: authority.publicKey,
78
+ did,
79
+ state,
80
+ mismatch: 'derived key is not one of the current authority keys'
81
+ });
82
+ // The agreement key sits at the same (keyGen, keySeq) as the authority key — both icp and rot
83
+ // derive them at matching indices, and a rev carries the agreement set forward — so one position
84
+ // locates both. Verified against the folded set, failing closed like the authority membership check.
85
+ const agreement = deriveKeyPair(seed, agreementPath(profile, state.keyGen, state.keySeq), 'X25519');
86
+ const encoded = encodeKey(agreement.publicKey, 'X25519');
87
+ if (!state.agreement.includes(encoded)) {
88
+ throw new Error(`${CONTEXT}: derived agreement key is not one of the current agreement keys of ${did}`);
89
+ }
90
+ return {
91
+ ...signing,
92
+ ...createKeyAgreementIdentityForDID(did, agreement.privateKey)
93
+ };
94
+ }
95
+ /**
96
+ * A signing identity for a `did:kokuin:` profile, whose `iss` is the profile DID.
97
+ *
98
+ * Takes the log and folds it here, so the signing key is always the one that log's current state
99
+ * establishes, never one the caller named. The log is the caller's freshness contract: an identity
100
+ * built from a stale or truncated log signs with a retired key, minting tokens a current verifier
101
+ * rejects. Rebuild from a re-read log after a rotation rather than holding one across it.
102
+ *
103
+ * The key is derived at `keyGen`/`keySeq`, not `gen`/`seq`: a revoke advances the sequence without
104
+ * establishing a key (Amendment A), so deriving at `gen`/`seq` would produce a key never in `k` — an
105
+ * unverifiable token, silently. Every token carries `kid: #<the key that signed it>`, so there is no
106
+ * `kid` parameter — one naming any other key is a request it could not honour, and is rejected on
107
+ * mismatch rather than dropped.
108
+ *
109
+ * Synchronous, and stays so (kubun's apply path depends on it): a log whose revoke carries a
110
+ * capability cannot fold without awaiting a verifier, so it throws here — use
111
+ * {@link createControllerIdentityAsync}.
112
+ *
113
+ * @throws when the log does not fold, or when the derived key is not one of the profile's current
114
+ * authority keys (a wrong `seed` or `profile` for this log).
115
+ */ export function createControllerIdentity({ seed, profile, log }) {
116
+ const did = didFromLog(log);
117
+ return identityForState({
118
+ seed,
119
+ profile,
120
+ did,
121
+ state: currentState(did, log, CONTEXT)
122
+ });
123
+ }
124
+ /**
125
+ * Async sibling of {@link createControllerIdentity} for a log whose revoke carries a capability
126
+ * authorising a non-authority signer, which only `foldLogAsync` can check. Identical otherwise, so a
127
+ * log without such a revoke resolves to the same identity through either entry point.
128
+ *
129
+ * @param options forwarded to the async fold; without `verifyCapability` such a log still fails to
130
+ * fold rather than being trusted.
131
+ */ export async function createControllerIdentityAsync({ seed, profile, log, options }) {
132
+ const did = didFromLog(log);
133
+ return identityForState({
134
+ seed,
135
+ profile,
136
+ did,
137
+ state: await currentStateAsync({
138
+ did,
139
+ events: log,
140
+ context: CONTEXT,
141
+ options: options
142
+ })
143
+ });
144
+ }
145
+ /**
146
+ * A signing identity for a `did:kokuin:` profile, from the current authority private key rather than
147
+ * the profile seed.
148
+ *
149
+ * The seed form above needs the root seed to derive the key, but the custody tiers say the daily path
150
+ * must not touch it: the root seed lives on a Ledger or cold mnemonic, and a device never receives the
151
+ * profile sub-seed (a sub-seed holder can derive the *next* key and therefore rotate, voiding
152
+ * pre-rotation). This is the same split {@link createRevokeWithKey} makes, for the same reason.
153
+ *
154
+ * What the key in hand can do is exactly the intended granularity: it signs as the profile and cannot
155
+ * rotate, since a rotate must reveal the key the log committed in `n` and only the seed derives that.
156
+ * Losing it costs a rotate-then-deny, not the identity. Takes the raw private key (no identity type
157
+ * here signs raw bytes; a `KeyStore` entry hands back exactly this); the public half is derived from
158
+ * it, so a caller cannot present one key and sign with another.
159
+ *
160
+ * @throws when the log does not fold, or when the key's public half is not one of the profile's
161
+ * current authority keys — a stale log, or a key the profile has rotated away.
162
+ */ export function createControllerIdentityWithKey({ privateKey, log }) {
163
+ const did = didFromLog(log);
164
+ return identityForKey({
165
+ privateKey,
166
+ publicKey: ed25519.getPublicKey(privateKey),
167
+ did,
168
+ state: currentState(did, log, CONTEXT),
169
+ mismatch: 'the supplied key is not one of the current authority keys'
170
+ });
171
+ }
172
+ /**
173
+ * Async sibling of {@link createControllerIdentityWithKey}, for a log whose revoke carries a
174
+ * capability authorising a non-authority signer. The pairing is not incidental: a profile that uses
175
+ * the management tier is the same profile whose daily signer should not hold the seed.
176
+ */ export async function createControllerIdentityWithKeyAsync({ privateKey, log, options }) {
177
+ const did = didFromLog(log);
178
+ return identityForKey({
179
+ privateKey,
180
+ publicKey: ed25519.getPublicKey(privateKey),
181
+ did,
182
+ state: await currentStateAsync({
183
+ did,
184
+ events: log,
185
+ context: CONTEXT,
186
+ options: options
187
+ }),
188
+ mismatch: 'the supplied key is not one of the current authority keys'
189
+ });
190
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * did:kokuin controller key event log.
3
+ *
4
+ * ## Installation
5
+ *
6
+ * ```sh
7
+ * npm install @kokuin/controller
8
+ * ```
9
+ *
10
+ * @module controller
11
+ */
12
+ export { canonicalBytes, digestOf, isCanonicalizable, MAX_CANONICAL_DEPTH, verifyDigest, withinCanonicalDepth, } from './canonical.js';
13
+ export { agreementPath, authorityPath, deriveKeyMaterial, deriveKeyPair, recoveryPath, } from './derivation.js';
14
+ export { type CreateRevokeOptions, type CreateRotateOptions, createInception, createReset, createRevoke, createRevokeWithKey, createRotate, DID_PREFIX, didFor, didFromInception, type EventCommon, type EventType, type InceptionEvent, KEY_TARGET_PREFIX, keyFromTarget, keyTarget, type RevokeEvent, type RotateEvent, type SignedEvent, verifyInception, verifyReset, verifyRevoke, verifyRotate, verifySignatures, } from './events.js';
15
+ export { CAPABILITY_REVOKE_NEEDS_ASYNC_FOLD, CAPABILITY_REVOKE_NEEDS_VERIFIER, CAPABILITY_VERIFIER_FAILED, CAPABILITY_VERIFIER_MALFORMED_ANSWER, type CapabilityAuthorisation, type FoldOptions, type FoldResult, foldLog, foldLogAsync, type KeyState, keyStateAt, MAX_SKIPPED_SLACK, pruneDenySet, REVOKE_NOT_SIGNED_BY_AUDIENCE, TOO_MANY_UNKNOWN_EVENTS, type VerifyCapabilityParams, } from './fold.js';
16
+ export { authoritativeStates, createMemoryLogStore, LOG_FORKED, LOG_NOT_AUTHORITATIVE, type LogStore, } from './history.js';
17
+ export { createControllerIdentity, createControllerIdentityAsync, createControllerIdentityWithKey, createControllerIdentityWithKeyAsync, } from './identity.js';
18
+ export { decodeKey, encodeKey, type KeyAlgorithm, type TaggedKey, tryDecodeKey, } from './keys.js';
19
+ export { type ProvideControllerIdentityParams, provideControllerIdentity, } from './keystore-identity.js';
20
+ export { enumerateProfiles, handleForDID, type ProfileEntry } from './profiles.js';
21
+ export { type ControllerResolverOptions, createControllerResolver } from './resolver.js';
22
+ export { type Duplicity, type ResolveFailure, type ResolveResult, resolveBranches, resolveBranchesAsync, } from './supersede.js';
23
+ export { VERSION_TAG } from './version.js';
package/lib/index.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * did:kokuin controller key event log.
3
+ *
4
+ * ## Installation
5
+ *
6
+ * ```sh
7
+ * npm install @kokuin/controller
8
+ * ```
9
+ *
10
+ * @module controller
11
+ */ export { canonicalBytes, digestOf, isCanonicalizable, MAX_CANONICAL_DEPTH, verifyDigest, withinCanonicalDepth } from './canonical.js';
12
+ export { agreementPath, authorityPath, deriveKeyMaterial, deriveKeyPair, recoveryPath } from './derivation.js';
13
+ export { createInception, createReset, createRevoke, createRevokeWithKey, createRotate, DID_PREFIX, didFor, didFromInception, KEY_TARGET_PREFIX, keyFromTarget, keyTarget, verifyInception, verifyReset, verifyRevoke, verifyRotate, verifySignatures } from './events.js';
14
+ export { CAPABILITY_REVOKE_NEEDS_ASYNC_FOLD, CAPABILITY_REVOKE_NEEDS_VERIFIER, CAPABILITY_VERIFIER_FAILED, CAPABILITY_VERIFIER_MALFORMED_ANSWER, foldLog, foldLogAsync, keyStateAt, MAX_SKIPPED_SLACK, pruneDenySet, REVOKE_NOT_SIGNED_BY_AUDIENCE, TOO_MANY_UNKNOWN_EVENTS } from './fold.js';
15
+ export { authoritativeStates, createMemoryLogStore, LOG_FORKED, LOG_NOT_AUTHORITATIVE } from './history.js';
16
+ export { createControllerIdentity, createControllerIdentityAsync, createControllerIdentityWithKey, createControllerIdentityWithKeyAsync } from './identity.js';
17
+ export { decodeKey, encodeKey, tryDecodeKey } from './keys.js';
18
+ export { provideControllerIdentity } from './keystore-identity.js';
19
+ export { enumerateProfiles, handleForDID } from './profiles.js';
20
+ export { createControllerResolver } from './resolver.js';
21
+ export { resolveBranches, resolveBranchesAsync } from './supersede.js';
22
+ export { VERSION_TAG } from './version.js';
package/lib/keys.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export type KeyAlgorithm = 'EdDSA' | 'X25519';
2
+ export type TaggedKey = {
3
+ alg: KeyAlgorithm;
4
+ publicKey: Uint8Array;
5
+ };
6
+ export declare function encodeKey(publicKey: Uint8Array, alg: KeyAlgorithm): string;
7
+ /** Total: any malformed, unknown-codec, or wrong-length value yields undefined rather than throwing. */
8
+ export declare function tryDecodeKey(value: string): TaggedKey | undefined;
9
+ export declare function decodeKey(value: string): TaggedKey;
package/lib/keys.js ADDED
@@ -0,0 +1,61 @@
1
+ import { decodeMultibase, encodeMultibase } from '@kokuin/token';
2
+ /**
3
+ * Multicodec prefixes, following the `did:key` convention. Tagging is what makes a new algorithm
4
+ * additive: an untagged key is opaque bytes, and telling X25519 from a future algorithm of the
5
+ * same length would mean sniffing the length.
6
+ */ const CODECS = {
7
+ EdDSA: new Uint8Array([
8
+ 0xed,
9
+ 0x01
10
+ ]),
11
+ X25519: new Uint8Array([
12
+ 0xec,
13
+ 0x01
14
+ ])
15
+ };
16
+ /**
17
+ * Expected payload length per algorithm, checked after the codec prefix is stripped. A per-algorithm
18
+ * table rather than a bare `=== 32`, so a future algorithm with a different length slots in without
19
+ * touching this check. Without this, a bare untagged key can collide with a codec prefix by chance
20
+ * (about 1 in 65536) and be silently accepted at the wrong length, and a truncated payload of the
21
+ * right prefix but wrong length would mint a key that throws downstream instead of failing here.
22
+ */ const KEY_LENGTHS = {
23
+ EdDSA: 32,
24
+ X25519: 32
25
+ };
26
+ export function encodeKey(publicKey, alg) {
27
+ const codec = CODECS[alg];
28
+ const bytes = new Uint8Array(codec.length + publicKey.length);
29
+ bytes.set(codec, 0);
30
+ bytes.set(publicKey, codec.length);
31
+ return encodeMultibase(bytes);
32
+ }
33
+ /** Total: any malformed, unknown-codec, or wrong-length value yields undefined rather than throwing. */ export function tryDecodeKey(value) {
34
+ let bytes;
35
+ try {
36
+ bytes = decodeMultibase(value);
37
+ } catch {
38
+ return undefined;
39
+ }
40
+ for (const alg of Object.keys(CODECS)){
41
+ const codec = CODECS[alg];
42
+ if (bytes.length > codec.length && bytes[0] === codec[0] && bytes[1] === codec[1]) {
43
+ const publicKey = bytes.slice(codec.length);
44
+ if (publicKey.length !== KEY_LENGTHS[alg]) {
45
+ return undefined;
46
+ }
47
+ return {
48
+ alg,
49
+ publicKey
50
+ };
51
+ }
52
+ }
53
+ return undefined;
54
+ }
55
+ export function decodeKey(value) {
56
+ const key = tryDecodeKey(value);
57
+ if (key == null) {
58
+ throw new Error(`Unrecognised key encoding: ${value}`);
59
+ }
60
+ return key;
61
+ }
@@ -0,0 +1,22 @@
1
+ import type { FullIdentity, KeyEntry } from '@kokuin/token';
2
+ import type { FoldOptions } from './fold.js';
3
+ import type { LogStore } from './history.js';
4
+ export type ProvideControllerIdentityParams = {
5
+ /** The seed source: `provideAsync()`'s bytes are the controller root seed. Generates on first use. */
6
+ entry: KeyEntry<Uint8Array>;
7
+ /** Which profile under the seed to resolve. */
8
+ profile: number;
9
+ /** Where the last accepted log for the DID lives — loaded to restore, written to on first generate. */
10
+ logStore: LogStore;
11
+ /** Forwarded to the fold for a log whose revoke carries a capability only the async fold can verify. */
12
+ options?: FoldOptions;
13
+ };
14
+ /**
15
+ * Resolve a `did:kokuin:` {@link FullIdentity} from a keystore entry and a log store.
16
+ *
17
+ * Generate and restore in one call: the entry yields the seed (a fresh one on first use, the stored
18
+ * one after), and the log store yields the event log (bootstrapping the inception when the DID has no
19
+ * log yet). The DID is a pure function of `(seed, profile)`, so the same entry and profile always
20
+ * resolve the same identity. A `KeyStore` caller passes `keyStore.entry(keyID)` as `entry`.
21
+ */
22
+ export declare function provideControllerIdentity({ entry, profile, logStore, options, }: ProvideControllerIdentityParams): Promise<FullIdentity>;
@@ -0,0 +1,26 @@
1
+ import { createInception, didFor } from './events.js';
2
+ import { createControllerIdentityAsync } from './identity.js';
3
+ /**
4
+ * Resolve a `did:kokuin:` {@link FullIdentity} from a keystore entry and a log store.
5
+ *
6
+ * Generate and restore in one call: the entry yields the seed (a fresh one on first use, the stored
7
+ * one after), and the log store yields the event log (bootstrapping the inception when the DID has no
8
+ * log yet). The DID is a pure function of `(seed, profile)`, so the same entry and profile always
9
+ * resolve the same identity. A `KeyStore` caller passes `keyStore.entry(keyID)` as `entry`.
10
+ */ export async function provideControllerIdentity({ entry, profile, logStore, options }) {
11
+ const seed = await entry.provideAsync();
12
+ const did = didFor(seed, profile);
13
+ let log = await logStore.get(did);
14
+ if (log == null) {
15
+ log = [
16
+ createInception(seed, profile)
17
+ ];
18
+ await logStore.set(did, log);
19
+ }
20
+ return createControllerIdentityAsync({
21
+ seed,
22
+ profile,
23
+ log,
24
+ options
25
+ });
26
+ }
@@ -0,0 +1,21 @@
1
+ export type ProfileEntry = {
2
+ index: number;
3
+ did: string;
4
+ handle: string;
5
+ };
6
+ /**
7
+ * A three-syllable handle derived from the DID itself — no stored data, no wordlist, so a cold picker
8
+ * can label profiles with no network or cache. Each syllable is consonant-vowel-consonant, so it is
9
+ * pronounceable; ~30 bits across three, ample to tell a handful of profiles apart and not a security
10
+ * boundary. The label is a function of the identifier, never the reverse (a user label must never
11
+ * feed the DID).
12
+ */
13
+ export declare function handleForDID(did: string): string;
14
+ /**
15
+ * Enumerate the first `count` profiles for a seed.
16
+ *
17
+ * Every index yields a valid-looking DID whether or not the profile was ever used — which
18
+ * profiles were used is not seed-derived. A picker should present all of them and let a probe
19
+ * grey out the unused ones when a group, hub, or cache is reachable.
20
+ */
21
+ export declare function enumerateProfiles(seed: Uint8Array, count: number): Array<ProfileEntry>;
@@ -0,0 +1,42 @@
1
+ import { sha256 } from '@noble/hashes/sha2.js';
2
+ import { createInception, didFromInception } from './events.js';
3
+ const encoder = new TextEncoder();
4
+ // Frozen forever: a handle a user wrote down must keep resolving to the same profile. Letters
5
+ // that are easily confused when read aloud or handwritten are left out — no c/k or q, no i/l.
6
+ const CONSONANTS = 'bdfghjkmnprstvwz';
7
+ const VOWELS = 'aeou';
8
+ /**
9
+ * A three-syllable handle derived from the DID itself — no stored data, no wordlist, so a cold picker
10
+ * can label profiles with no network or cache. Each syllable is consonant-vowel-consonant, so it is
11
+ * pronounceable; ~30 bits across three, ample to tell a handful of profiles apart and not a security
12
+ * boundary. The label is a function of the identifier, never the reverse (a user label must never
13
+ * feed the DID).
14
+ */ export function handleForDID(did) {
15
+ const digest = sha256(encoder.encode(did));
16
+ const syllables = [];
17
+ for(let i = 0; i < 3; i++){
18
+ syllables.push(CONSONANTS[digest[i * 3] % CONSONANTS.length] + VOWELS[digest[i * 3 + 1] % VOWELS.length] + CONSONANTS[digest[i * 3 + 2] % CONSONANTS.length]);
19
+ }
20
+ return syllables.join('-');
21
+ }
22
+ /**
23
+ * Enumerate the first `count` profiles for a seed.
24
+ *
25
+ * Every index yields a valid-looking DID whether or not the profile was ever used — which
26
+ * profiles were used is not seed-derived. A picker should present all of them and let a probe
27
+ * grey out the unused ones when a group, hub, or cache is reachable.
28
+ */ export function enumerateProfiles(seed, count) {
29
+ if (!Number.isInteger(count) || count < 1) {
30
+ throw new Error('enumerateProfiles: count must be at least 1');
31
+ }
32
+ const entries = [];
33
+ for(let index = 0; index < count; index++){
34
+ const did = didFromInception(createInception(seed, index).event);
35
+ entries.push({
36
+ index,
37
+ did,
38
+ handle: handleForDID(did)
39
+ });
40
+ }
41
+ return entries;
42
+ }
@@ -0,0 +1,56 @@
1
+ import type { DIDMethodResolver } from '@kokuin/token';
2
+ import { type SignedEvent } from './events.js';
3
+ import type { FoldOptions } from './fold.js';
4
+ import { type LogStore } from './history.js';
5
+ export type ControllerResolverOptions = {
6
+ /**
7
+ * Load a controller's event log. Returns undefined when the DID is unknown.
8
+ *
9
+ * **Answer with the whole log**, even when a `verifyCapability` is configured. A capability
10
+ * authorising a revoke must be checked at the position the fold is at — a rotated-away key set must
11
+ * not verify a grant made under it, a device denied before that position must not author one —
12
+ * and neither question can be answered from a DID alone, all this callback receives. So neither is
13
+ * asked of it: the fold answers both by handing the verifier a resolver built from the prefix
14
+ * states. See `FoldOptions.verifyCapability`.
15
+ *
16
+ * ```ts
17
+ * const resolver = createControllerResolver({
18
+ * loadLog: async (did) => await store.log(did),
19
+ * verifyCapability: createControllerCapabilityVerifier(),
20
+ * })
21
+ * ```
22
+ *
23
+ * Reuse the instance rather than building one per resolution: concurrent resolutions of one DID
24
+ * share a single in-flight fold. Not a cache — the entry drops as soon as the fold settles.
25
+ *
26
+ * One re-entry hazard remains, not this callback's: a `verifyToken` hook or a `resolver` passed to
27
+ * the capability verifier that resolves *this* DID through *this* resolver joins the calling fold
28
+ * and awaits a promise that cannot settle. Give such a hook a resolver of its own.
29
+ */
30
+ loadLog(did: string): Promise<Array<SignedEvent> | undefined>;
31
+ /**
32
+ * Verify a capability authorising a non-authority signer to revoke, forwarded to the fold. Optional
33
+ * because most logs carry none: without it a log containing one fails to fold rather than being
34
+ * trusted. `createControllerCapabilityVerifier()` from `@kokuin/capability` is the implementation;
35
+ * it needs a registry only for a chain delegate whose DID method is not resolvable from the
36
+ * identifier alone.
37
+ */
38
+ verifyCapability?: FoldOptions['verifyCapability'];
39
+ /**
40
+ * Where to remember the last log accepted for each DID, so a later one that is *behind* it is
41
+ * refused rather than answered from. Without one, truncation is a silent revocation bypass: a peer
42
+ * serving a prefix that stops just before the `rev` denying their device produces a log that folds
43
+ * cleanly and yields a deny set missing exactly that entry.
44
+ *
45
+ * Optional, because the guarantee needs storage this package cannot provide and a first encounter
46
+ * has nothing to compare against. **Configure one if this process resolves the same DIDs more than
47
+ * once.** See {@link LogStore}, and `createMemoryLogStore` for the process-lifetime version.
48
+ */
49
+ history?: LogStore;
50
+ };
51
+ /**
52
+ * A `did:kokuin:` resolver for `@kokuin/token`. Injected rather than imported by token: this package
53
+ * depends on token for signing, so the reverse import would cycle. Token resolves `iss` through the
54
+ * interface without knowing the fold exists.
55
+ */
56
+ export declare function createControllerResolver(options: ControllerResolverOptions): DIDMethodResolver;