@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/history.d.ts CHANGED
@@ -1,63 +1,114 @@
1
- import type { SignedEvent } from './events.js';
2
- import { type FoldOptions, type KeyState } from './fold.js';
1
+ import type { EpochLimits } from './epoch.js';
2
+ import { type FoldResult } from './fold.js';
3
+ import { type ControllerLog, LOG_FORKED, LOG_NOT_AUTHORITATIVE } from './log.js';
4
+ export { LOG_FORKED, LOG_NOT_AUTHORITATIVE };
3
5
  /**
4
- * Where a consumer keeps the last log it accepted for a DID, so the next one can be compared against
5
- * it.
6
- *
7
- * A folded log carries no proof it is the *whole* log: a peer serving a prefix — stopping just before
8
- * the `rev` denying their device — produces one that folds cleanly and yields a deny set missing
9
- * exactly the entry that matters. Nothing inside a single log distinguishes truncation from a log
10
- * that is simply short, so the fork-detection limitation does not reach it: there is no second branch,
11
- * only an honest-looking prefix. Comparing against what this party has already seen supplies the
12
- * missing second branch — a local guarantee, not a global one (it does nothing on a first encounter),
13
- * but the difference between a suppression attack that works once and one that works every time.
14
- *
15
- * Deliberately not a high-water mark over `(gen, seq)`. Supersession legitimately *lowers* the
16
- * sequence: a thief's long current-key branch is beaten by the owner's recovering rotate at a much
17
- * lower `seq`, which a "never go backwards" mark would reject, bricking the profile as it is rescued.
18
- * A divergence is settled by {@link resolveBranchesAsync} instead.
6
+ * Where a consumer keeps the log it has accepted for a DID, so the next presentation can be compared
7
+ * against it and merged into it.
8
+ *
9
+ * A folded log carries no proof it is the *whole* log. A peer serving a prefix of the chain —
10
+ * stopping just before the settlement denying their device — or a full chain with the epoch event
11
+ * that denies it withheld, produces one that folds cleanly and yields a deny set missing exactly the
12
+ * entry that matters. Nothing inside a single log distinguishes that from a log that is simply short.
13
+ * Comparing against what this party has already seen supplies the missing half — a local guarantee,
14
+ * not a global one (it does nothing on a first encounter), but the difference between a suppression
15
+ * attack that works once and one that works every time. The two halves are kept differently because
16
+ * they fold differently: a newer chain replaces the one seen only through the branch rules, and the
17
+ * current epoch's events are never forgotten, only added to.
18
+ *
19
+ * Deliberately not a high-water mark over `(gen, seq)`. A reset legitimately replaces a longer
20
+ * lower-generation chain, which a "never go backwards" mark would reject, bricking the profile as it
21
+ * is recovered. A chain divergence is settled by {@link resolveBranches} instead.
19
22
  */
20
23
  export type LogStore = {
21
- /** The last accepted log for this DID, or undefined when none has been seen. */
22
- get(did: string): Promise<Array<SignedEvent> | undefined>;
23
- /** Record a log as the last accepted one. Called only after it has folded. */
24
- set(did: string, log: Array<SignedEvent>): Promise<void>;
24
+ /** The accepted log for this DID, or undefined when none has been seen. */
25
+ get(did: string): Promise<ControllerLog | undefined>;
26
+ /**
27
+ * Replace the log for this DID with what `update` makes of the current one, and return it. Atomic
28
+ * per DID: no two `update`s for one DID interleave, so two presentations merged concurrently both
29
+ * land rather than the second overwriting the first with a log built from a stale read. When
30
+ * `update` throws, nothing is written and the throw propagates. `update` may be invoked more than
31
+ * once (a persistent store reruns it when it loses a first-insert race), so it must be free of side
32
+ * effects; only the last result is written. An `update` returning `current` itself (the very object
33
+ * it was handed) means nothing changed, and the store skips the write: `authoritativeLog` answers
34
+ * that way whenever a resolution learned nothing new, which is most of them.
35
+ */
36
+ merge(did: string, update: (current: ControllerLog | undefined) => Promise<ControllerLog>): Promise<ControllerLog>;
25
37
  };
26
- /** What a resolver answers with when the log it loaded loses to the one already seen. */
27
- export declare const LOG_NOT_AUTHORITATIVE = "the loaded log does not supersede the one already seen";
28
- /** What a resolver answers with when the loaded log and the one already seen are a genuine fork. */
29
- export declare const LOG_FORKED = "the loaded log and the one already seen are a fork";
30
38
  /**
31
39
  * An in-memory {@link LogStore}. Enough for a process that resolves the same DIDs repeatedly, and for
32
40
  * tests; a consumer that survives restarts wants a persistent one, since the guarantee is only as
33
41
  * durable as the memory of what was seen.
42
+ *
43
+ * `merge` is serialised per DID by a promise chain: each update starts only after the previous one for
44
+ * that DID settled, failed or not. A failed update must not wedge the chain, so the link kept is one
45
+ * that never rejects; the caller still sees the failure on the promise `merge` returned.
34
46
  */
35
47
  export declare function createMemoryLogStore(): LogStore;
36
- /**
37
- * Fold the loaded log, having established that it is not behind one this party has already seen.
38
- *
39
- * The two rejections are throws because a resolver has no other way to say "I will not answer for this
40
- * DID", and answering from the loser is the failure this prevents: a prefix loses to the log it is a
41
- * prefix of (truncation refused), and a fork is refused outright (resolving to either side is a
42
- * guess). The comparison is on the **folded head digest**, never array identity — the same history
43
- * re-loaded is a different array, and identity would reject the commonest call there is.
44
- *
45
- * A stored log that no longer folds is treated as no memory, but only when it *genuinely* no longer
46
- * folds: local corruption or a tightened fold rule is one round of the guarantee to give up, since
47
- * only a folded log was ever stored and refusing forever would brick every cached profile. A stored
48
- * log that fails only because this call has no `verifyCapability` for a cap-authorised revoke it
49
- * carries is the opposite — nothing changed, the call is simply not equipped — so it fails closed;
50
- * treating it as no memory would disable the truncation guard for management-tier profiles.
51
- *
52
- * The states are returned so the caller does not fold twice; the log travels with them for the store.
53
- */
54
- export declare function authoritativeStates({ did, loaded, seen, context, options, }: {
48
+ /** What {@link authoritativeLog} takes. */
49
+ export type AuthoritativeLogParams = {
55
50
  did: string;
56
- loaded: Array<SignedEvent>;
57
- seen: Array<SignedEvent> | undefined;
51
+ loaded: ControllerLog;
52
+ seen: ControllerLog | undefined;
58
53
  context: string;
59
- options: FoldOptions;
60
- }): Promise<{
61
- log: Array<SignedEvent>;
62
- states: Array<KeyState>;
63
- }>;
54
+ };
55
+ /**
56
+ * What {@link authoritativeLog} answers: the log to remember, and the fold to answer from. `log` is
57
+ * `seen` itself, the same object, when the resolution learned nothing new; see `LogStore.merge`.
58
+ */
59
+ export type AuthoritativeLog = {
60
+ log: ControllerLog;
61
+ result: Extract<FoldResult, {
62
+ ok: true;
63
+ }>;
64
+ };
65
+ /**
66
+ * Fold the loaded log against the one already seen, and say what to remember.
67
+ *
68
+ * **The chain** is settled exactly as before epochs existed. The loaded chain must extend (or be) the
69
+ * one seen — its fold passes through the seen head — or win against it under
70
+ * {@link resolveBranches}; a chain that loses is refused ({@link LOG_NOT_AUTHORITATIVE}: truncation),
71
+ * and a fork is refused outright ({@link LOG_FORKED}: answering from either side is a guess). The
72
+ * comparison is on folded head digests, never array identity — the same history re-loaded is a
73
+ * different array, and identity would reject the commonest call there is. These are throws because a
74
+ * resolver has no other way to say "I will not answer for this DID".
75
+ *
76
+ * **The epoch** is the union of what was seen and what was loaded, folded on the settled chain: a
77
+ * presentation that withholds an event this party has seen cannot make it forget it. The returned
78
+ * `result` answers for that union, not for the presentation alone.
79
+ *
80
+ * **What to remember** is the settled chain and the current epoch's applied events — nothing else.
81
+ * From `loaded`, an unsigned, forged, malformed or still-pending entry was never authenticated, so it
82
+ * never reaches the store: a relay flooding a presentation changes nothing remembered (and, counting
83
+ * only applied events, cannot saturate the epoch either). A pending event is not lost forever by
84
+ * this, only not vouched for: once its parents are known, presenting it again applies it. Events of
85
+ * any other epoch are dropped, seen or not: an event naming another epoch has no effect, its effect is
86
+ * in the newer snapshot or was dropped by it, and a verifier never needs to ingest a superseded epoch
87
+ * to fold the current one. That is what bounds memory: one epoch, at most `MAX_EPOCH_EVENTS + 1`
88
+ * events once saturated (see `appliedCopies`), so a held log always passes the request limit when
89
+ * forwarded, and a flooded epoch is gone the moment a settlement supersedes it. A saturated epoch's
90
+ * refusal persists through a later presentation of a subset, until that settlement.
91
+ *
92
+ * A stored log whose chain no longer folds is treated as no memory: local corruption or a tightened
93
+ * fold rule is one round of the guarantee to give up, since only a folded log was ever stored and
94
+ * refusing forever would brick every cached profile. A stored *saturated* epoch is not that case — it
95
+ * folds, saturated, and is merged like any other memory, so its refusal stands. Stored epoch events are
96
+ * folded together with the presentation without the request limit, which bounds a single input, not
97
+ * the union of two inputs that each passed it.
98
+ *
99
+ * **What is checked.** Stored epoch events of the settled chain's current epoch are not
100
+ * authenticated again: they were when they were stored, and a copy that verified under an epoch
101
+ * verifies under it every time (see `FoldEpochParams.trusted`). They are still authorised afresh,
102
+ * and a stored event of another epoch is simply ignored. Only what `loaded` adds is checked, and
103
+ * `loaded` is refused as a whole, memory untouched, once more than `MAX_FAILED_SIGNATURES` of its
104
+ * signatures fail (`TOO_MANY_FAILED_SIGNATURES`). This trusts the store for signatures, not only
105
+ * for completeness: whoever can write it can plant an unsigned event of the current epoch and have
106
+ * it applied (a member added, say), not merely make this party forget. Only ever write what this
107
+ * function returned for the same DID.
108
+ */
109
+ export declare function authoritativeLog(params: AuthoritativeLogParams): AuthoritativeLog;
110
+ /**
111
+ * {@link authoritativeLog} under lowered epoch limits. Test-only: saturation is then reachable
112
+ * without thousands of signatures. Not exported from the package.
113
+ */
114
+ export declare function authoritativeLogWithin({ did, loaded, seen, context }: AuthoritativeLogParams, limits?: Partial<EpochLimits>): AuthoritativeLog;
package/lib/history.js CHANGED
@@ -1,97 +1,186 @@
1
- import { foldLogAsync } from './fold.js';
2
- import { allStatesAsync } from './state.js';
3
- import { needsCapabilityVerification, resolveBranchesAsync } from './supersede.js';
4
- /** What a resolver answers with when the log it loaded loses to the one already seen. */ export const LOG_NOT_AUTHORITATIVE = 'the loaded log does not supersede the one already seen';
5
- /** What a resolver answers with when the loaded log and the one already seen are a genuine fork. */ export const LOG_FORKED = 'the loaded log and the one already seen are a fork';
1
+ import { digestOf, isCanonicalizable } from './canonical.js';
2
+ import { foldLog } from './fold.js';
3
+ import { MAX_LOG_EVENTS } from './limits.js';
4
+ import { appliedCopies, isControllerLog, LOG_FORKED, LOG_NOT_AUTHORITATIVE, LOG_TOO_LARGE, MALFORMED_LOG, withEpoch } from './log.js';
5
+ import { resolveBranches } from './supersede.js';
6
+ export { LOG_FORKED, LOG_NOT_AUTHORITATIVE };
6
7
  /**
7
8
  * An in-memory {@link LogStore}. Enough for a process that resolves the same DIDs repeatedly, and for
8
9
  * tests; a consumer that survives restarts wants a persistent one, since the guarantee is only as
9
10
  * durable as the memory of what was seen.
11
+ *
12
+ * `merge` is serialised per DID by a promise chain: each update starts only after the previous one for
13
+ * that DID settled, failed or not. A failed update must not wedge the chain, so the link kept is one
14
+ * that never rejects; the caller still sees the failure on the promise `merge` returned.
10
15
  */ export function createMemoryLogStore() {
11
16
  const logs = new Map();
17
+ const tails = new Map();
12
18
  return {
13
19
  async get (did) {
14
20
  return logs.get(did);
15
21
  },
16
- async set (did, log) {
17
- logs.set(did, log);
22
+ merge (did, update) {
23
+ const previous = tails.get(did) ?? Promise.resolve();
24
+ const run = previous.then(async ()=>{
25
+ const current = logs.get(did);
26
+ const next = await update(current);
27
+ if (next !== current) logs.set(did, next);
28
+ return next;
29
+ });
30
+ const tail = run.then(()=>undefined, ()=>undefined);
31
+ tails.set(did, tail);
32
+ // Drop the link once nothing is queued behind it, so the map holds only DIDs mid-merge.
33
+ void tail.then(()=>{
34
+ if (tails.get(did) === tail) {
35
+ tails.delete(did);
36
+ }
37
+ });
38
+ return run;
18
39
  }
19
40
  };
20
41
  }
42
+ /** The stored log and its chain's fold, or undefined when there is no usable memory. */ function recall(did, seen) {
43
+ if (seen == null || !isControllerLog(seen) || seen.chain.length === 0) {
44
+ return undefined;
45
+ }
46
+ const folded = foldLog(did, {
47
+ chain: seen.chain,
48
+ events: []
49
+ });
50
+ const head = folded.ok ? folded.states[folded.states.length - 1] : undefined;
51
+ return head === undefined ? undefined : {
52
+ log: seen,
53
+ head
54
+ };
55
+ }
56
+ /** One log entry and another hold the same bytes: the same body, signatures and recovery key. */ function sameEntry(entry, other) {
57
+ if (other == null || typeof other !== 'object') return false;
58
+ const { event, sigs, recoveryKey } = other;
59
+ if (recoveryKey !== entry.recoveryKey || !Array.isArray(sigs)) return false;
60
+ if (sigs.length !== entry.sigs.length || sigs.some((sig, i)=>sig !== entry.sigs[i])) {
61
+ return false;
62
+ }
63
+ // Digests, not object identity or JSON text: a store hands back a fresh copy, possibly with its
64
+ // keys reordered (Postgres `jsonb`), and the canonical form is what both sides agree on.
65
+ return isCanonicalizable(event) && digestOf(event) === digestOf(entry.event);
66
+ }
67
+ /** Whether `next` is `seen` again, entry for entry, so remembering it would change nothing. */ function sameLog(next, seen) {
68
+ const pairs = [
69
+ [
70
+ next.chain,
71
+ seen.chain
72
+ ],
73
+ [
74
+ next.events,
75
+ seen.events
76
+ ]
77
+ ];
78
+ return pairs.every(([mine, theirs])=>mine.length === theirs.length && mine.every((entry, i)=>sameEntry(entry, theirs[i])));
79
+ }
21
80
  /**
22
- * Fold the loaded log, having established that it is not behind one this party has already seen.
81
+ * Fold the loaded log against the one already seen, and say what to remember.
23
82
  *
24
- * The two rejections are throws because a resolver has no other way to say "I will not answer for this
25
- * DID", and answering from the loser is the failure this prevents: a prefix loses to the log it is a
26
- * prefix of (truncation refused), and a fork is refused outright (resolving to either side is a
27
- * guess). The comparison is on the **folded head digest**, never array identity — the same history
28
- * re-loaded is a different array, and identity would reject the commonest call there is.
83
+ * **The chain** is settled exactly as before epochs existed. The loaded chain must extend (or be) the
84
+ * one seen — its fold passes through the seen head — or win against it under
85
+ * {@link resolveBranches}; a chain that loses is refused ({@link LOG_NOT_AUTHORITATIVE}: truncation),
86
+ * and a fork is refused outright ({@link LOG_FORKED}: answering from either side is a guess). The
87
+ * comparison is on folded head digests, never array identity — the same history re-loaded is a
88
+ * different array, and identity would reject the commonest call there is. These are throws because a
89
+ * resolver has no other way to say "I will not answer for this DID".
29
90
  *
30
- * A stored log that no longer folds is treated as no memory, but only when it *genuinely* no longer
31
- * folds: local corruption or a tightened fold rule is one round of the guarantee to give up, since
32
- * only a folded log was ever stored and refusing forever would brick every cached profile. A stored
33
- * log that fails only because this call has no `verifyCapability` for a cap-authorised revoke it
34
- * carries is the opposite — nothing changed, the call is simply not equipped — so it fails closed;
35
- * treating it as no memory would disable the truncation guard for management-tier profiles.
91
+ * **The epoch** is the union of what was seen and what was loaded, folded on the settled chain: a
92
+ * presentation that withholds an event this party has seen cannot make it forget it. The returned
93
+ * `result` answers for that union, not for the presentation alone.
36
94
  *
37
- * The states are returned so the caller does not fold twice; the log travels with them for the store.
38
- */ export async function authoritativeStates({ did, loaded, seen, context, options }) {
39
- const states = await allStatesAsync({
40
- did,
41
- events: loaded,
42
- context,
43
- options: options
44
- });
45
- if (seen == null || seen.length === 0) {
46
- return {
47
- log: loaded,
48
- states
49
- };
95
+ * **What to remember** is the settled chain and the current epoch's applied events — nothing else.
96
+ * From `loaded`, an unsigned, forged, malformed or still-pending entry was never authenticated, so it
97
+ * never reaches the store: a relay flooding a presentation changes nothing remembered (and, counting
98
+ * only applied events, cannot saturate the epoch either). A pending event is not lost forever by
99
+ * this, only not vouched for: once its parents are known, presenting it again applies it. Events of
100
+ * any other epoch are dropped, seen or not: an event naming another epoch has no effect, its effect is
101
+ * in the newer snapshot or was dropped by it, and a verifier never needs to ingest a superseded epoch
102
+ * to fold the current one. That is what bounds memory: one epoch, at most `MAX_EPOCH_EVENTS + 1`
103
+ * events once saturated (see `appliedCopies`), so a held log always passes the request limit when
104
+ * forwarded, and a flooded epoch is gone the moment a settlement supersedes it. A saturated epoch's
105
+ * refusal persists through a later presentation of a subset, until that settlement.
106
+ *
107
+ * A stored log whose chain no longer folds is treated as no memory: local corruption or a tightened
108
+ * fold rule is one round of the guarantee to give up, since only a folded log was ever stored and
109
+ * refusing forever would brick every cached profile. A stored *saturated* epoch is not that case — it
110
+ * folds, saturated, and is merged like any other memory, so its refusal stands. Stored epoch events are
111
+ * folded together with the presentation without the request limit, which bounds a single input, not
112
+ * the union of two inputs that each passed it.
113
+ *
114
+ * **What is checked.** Stored epoch events of the settled chain's current epoch are not
115
+ * authenticated again: they were when they were stored, and a copy that verified under an epoch
116
+ * verifies under it every time (see `FoldEpochParams.trusted`). They are still authorised afresh,
117
+ * and a stored event of another epoch is simply ignored. Only what `loaded` adds is checked, and
118
+ * `loaded` is refused as a whole, memory untouched, once more than `MAX_FAILED_SIGNATURES` of its
119
+ * signatures fail (`TOO_MANY_FAILED_SIGNATURES`). This trusts the store for signatures, not only
120
+ * for completeness: whoever can write it can plant an unsigned event of the current epoch and have
121
+ * it applied (a member added, say), not merely make this party forget. Only ever write what this
122
+ * function returned for the same DID.
123
+ */ export function authoritativeLog(params) {
124
+ return authoritativeLogWithin(params);
125
+ }
126
+ /**
127
+ * {@link authoritativeLog} under lowered epoch limits. Test-only: saturation is then reachable
128
+ * without thousands of signatures. Not exported from the package.
129
+ */ export function authoritativeLogWithin({ did, loaded, seen, context }, limits) {
130
+ const invalid = (reason, index)=>new Error(`${context}: invalid log for ${did}: ${reason} at event ${index}`);
131
+ // The request limit, before any work: `loaded` is the one untrusted input here.
132
+ if (!isControllerLog(loaded)) {
133
+ throw invalid(MALFORMED_LOG, 0);
50
134
  }
51
- const previous = await foldLogAsync(did, seen, options);
52
- if (!previous.ok) {
53
- // "Fold rule tightened" is safe to treat as no memory (only a folded log was stored, so refusing
54
- // forever would brick every cached profile). "I remember a management-tier log and got no verifier
55
- // for it" is not: accepting the loaded log there disables the truncation guard for exactly those
56
- // profiles, letting an attacker's prefix that stops before the cap-authorised `rev` be accepted
57
- // because the fuller stored log is the one that could not be folded. Fail closed — what could not
58
- // be checked is not evidence its target is absent.
59
- if (needsCapabilityVerification(previous)) {
60
- throw new Error(`${LOG_NOT_AUTHORITATIVE}: ${did}`);
61
- }
62
- return {
63
- log: loaded,
64
- states
65
- };
135
+ if (loaded.events.length > MAX_LOG_EVENTS) {
136
+ throw invalid(LOG_TOO_LARGE, -1);
66
137
  }
67
- const head = states[states.length - 1].digest;
68
- // The loaded log extends, or is, the history seen: its fold passed through that head. Ordinary path.
69
- if (states.some((state)=>state.digest === previous.states[previous.states.length - 1].digest)) {
70
- return {
71
- log: loaded,
72
- states
73
- };
138
+ const loadedChain = foldLog(did, {
139
+ chain: loaded.chain,
140
+ events: []
141
+ });
142
+ if (!loadedChain.ok) {
143
+ throw invalid(loadedChain.reason, loadedChain.index);
74
144
  }
75
- // They diverge. Which is authoritative is what branch resolution answers, and the only thing that
76
- // knows a superseding rotate outranks a longer branch of current-key events.
77
- const resolved = await resolveBranchesAsync(did, [
78
- seen,
79
- loaded
80
- ], options);
81
- if (!resolved.ok) {
82
- throw new Error(`${resolved.failure === 'duplicity' ? LOG_FORKED : LOG_NOT_AUTHORITATIVE}: ${did}`);
145
+ const loadedHead = loadedChain.states[loadedChain.states.length - 1];
146
+ if (loadedHead === undefined) {
147
+ throw invalid('no state produced', 0);
83
148
  }
84
- // A comparison that did not happen is a refusal, not a pass: `unverified` means one side could not
85
- // be folded for want of a verifier. Always zero with one configured.
86
- if (resolved.unverified > 0) {
87
- throw new Error(`${LOG_NOT_AUTHORITATIVE}: ${did}`);
149
+ let chain = loaded.chain;
150
+ let states = loadedChain.states;
151
+ const memory = recall(did, seen);
152
+ if (memory !== undefined && !states.some((state)=>state.digest === memory.head.digest)) {
153
+ // They diverge. Which is authoritative is what branch resolution answers: a higher generation (a
154
+ // reset) wins outright, a chain that only repeats the seen one's prefix loses, and two rotates at
155
+ // one position are a fork.
156
+ const resolved = resolveBranches(did, [
157
+ memory.log.chain,
158
+ loaded.chain
159
+ ]);
160
+ if (!resolved.ok) {
161
+ throw new Error(`${resolved.failure === 'duplicity' ? LOG_FORKED : LOG_NOT_AUTHORITATIVE}: ${did}`);
162
+ }
163
+ const winner = foldLog(did, {
164
+ chain: resolved.winner,
165
+ events: []
166
+ });
167
+ const winnerHead = winner.ok ? winner.states[winner.states.length - 1] : undefined;
168
+ if (!winner.ok || winnerHead === undefined || winnerHead.digest !== loadedHead.digest) {
169
+ throw new Error(`${LOG_NOT_AUTHORITATIVE}: ${did}`);
170
+ }
171
+ chain = resolved.winner;
172
+ states = winner.states;
88
173
  }
89
- const winner = await foldLogAsync(did, resolved.winner, options);
90
- if (!winner.ok || winner.states[winner.states.length - 1].digest !== head) {
91
- throw new Error(`${LOG_NOT_AUTHORITATIVE}: ${did}`);
174
+ const held = withEpoch(did, states, loaded.events, limits, memory?.log.events);
175
+ if (!('result' in held)) {
176
+ throw invalid(held.reason, held.index);
92
177
  }
178
+ const log = {
179
+ chain,
180
+ events: appliedCopies(held, limits)
181
+ };
93
182
  return {
94
- log: loaded,
95
- states
183
+ log: memory !== undefined && sameLog(log, memory.log) ? memory.log : log,
184
+ result: held.result
96
185
  };
97
186
  }
package/lib/identity.d.ts CHANGED
@@ -1,48 +1,34 @@
1
1
  import { type FullIdentity, type SigningIdentity } from '@kokuin/token';
2
- import { type SignedEvent } from './events.js';
3
- import type { FoldOptions } from './fold.js';
2
+ import { type ControllerLog } from './log.js';
4
3
  export type CreateControllerIdentityParams = {
5
4
  seed: Uint8Array;
6
5
  profile: number;
7
- log: Array<SignedEvent>;
6
+ log: ControllerLog;
8
7
  };
9
8
  /**
10
9
  * A signing identity for a `did:kokuin:` profile, whose `iss` is the profile DID.
11
10
  *
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
11
+ * Takes the log and folds its chain here, so the signing key is always the one that log's current
12
+ * state establishes, never one the caller named. Its epoch events are not folded: they never change a
13
+ * root key (see `currentState`). The log is the caller's freshness contract: an identity
14
14
  * built from a stale or truncated log signs with a retired key, minting tokens a current verifier
15
15
  * rejects. Rebuild from a re-read log after a rotation rather than holding one across it.
16
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.
17
+ * The key is derived at `keyGen`/`keySeq`, the derivation index the fold names for the current keys
18
+ * — on a chain of key-establishing events the same number as `seq`, but the question asked is where
19
+ * the key lives, not where the event sits. Every token carries `kid: #<the key that signed it>`, so
20
+ * there is no `kid` parameter — one naming any other key is a request it could not honour, and is
21
+ * rejected on mismatch rather than dropped.
22
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}.
23
+ * Synchronous, and stays so: kubun's apply path depends on it.
26
24
  *
27
25
  * @throws when the log does not fold, or when the derived key is not one of the profile's current
28
26
  * authority keys (a wrong `seed` or `profile` for this log).
29
27
  */
30
28
  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
29
  export type CreateControllerIdentityWithKeyParams = {
44
30
  privateKey: Uint8Array;
45
- log: Array<SignedEvent>;
31
+ log: ControllerLog;
46
32
  };
47
33
  /**
48
34
  * A signing identity for a `did:kokuin:` profile, from the current authority private key rather than
@@ -51,7 +37,7 @@ export type CreateControllerIdentityWithKeyParams = {
51
37
  * The seed form above needs the root seed to derive the key, but the custody tiers say the daily path
52
38
  * must not touch it: the root seed lives on a Ledger or cold mnemonic, and a device never receives the
53
39
  * 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.
40
+ * pre-rotation).
55
41
  *
56
42
  * What the key in hand can do is exactly the intended granularity: it signs as the profile and cannot
57
43
  * rotate, since a rotate must reveal the key the log committed in `n` and only the seed derives that.
@@ -63,12 +49,3 @@ export type CreateControllerIdentityWithKeyParams = {
63
49
  * current authority keys — a stale log, or a key the profile has rotated away.
64
50
  */
65
51
  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>;