@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/epoch-events.d.ts +139 -0
- package/lib/epoch-events.js +177 -0
- package/lib/epoch.d.ts +90 -0
- package/lib/epoch.js +505 -0
- package/lib/events.d.ts +97 -143
- package/lib/events.js +94 -143
- package/lib/fold.d.ts +78 -100
- package/lib/fold.js +200 -225
- package/lib/history.d.ts +102 -51
- package/lib/history.js +160 -71
- package/lib/identity.d.ts +13 -36
- package/lib/identity.js +27 -63
- package/lib/index.d.ts +11 -6
- package/lib/index.js +10 -5
- package/lib/keystore-identity.d.ts +1 -4
- package/lib/keystore-identity.js +11 -8
- package/lib/limits.d.ts +29 -0
- package/lib/limits.js +27 -0
- package/lib/log.d.ts +124 -0
- package/lib/log.js +268 -0
- package/lib/members.d.ts +103 -0
- package/lib/members.js +152 -0
- package/lib/profiles.js +7 -1
- package/lib/resolver.d.ts +34 -29
- package/lib/resolver.js +64 -68
- package/lib/state-resolver.d.ts +63 -27
- package/lib/state-resolver.js +130 -53
- package/lib/state.d.ts +10 -26
- package/lib/state.js +23 -28
- package/lib/supersede.d.ts +22 -63
- package/lib/supersede.js +100 -186
- package/package.json +12 -12
package/lib/history.d.ts
CHANGED
|
@@ -1,63 +1,114 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { type
|
|
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
|
|
5
|
-
* it.
|
|
6
|
-
*
|
|
7
|
-
* A folded log carries no proof it is the *whole* log
|
|
8
|
-
* the
|
|
9
|
-
*
|
|
10
|
-
* that
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
22
|
-
get(did: string): Promise<
|
|
23
|
-
/**
|
|
24
|
-
|
|
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
|
-
|
|
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:
|
|
57
|
-
seen:
|
|
51
|
+
loaded: ControllerLog;
|
|
52
|
+
seen: ControllerLog | undefined;
|
|
58
53
|
context: string;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
81
|
+
* Fold the loaded log against the one already seen, and say what to remember.
|
|
23
82
|
*
|
|
24
|
-
* The
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
90
|
-
if (!
|
|
91
|
-
throw
|
|
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:
|
|
95
|
-
|
|
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
|
|
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:
|
|
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
|
|
13
|
-
* establishes, never one the caller named.
|
|
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`,
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `kid` parameter — one naming any other key is a request it could not honour, and is
|
|
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
|
|
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:
|
|
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).
|
|
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>;
|