@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/identity.js CHANGED
@@ -3,16 +3,20 @@ import { ed25519 } from '@noble/curves/ed25519.js';
3
3
  import { agreementPath, authorityPath, deriveKeyPair } from './derivation.js';
4
4
  import { didFromInception } from './events.js';
5
5
  import { encodeKey } from './keys.js';
6
- import { currentState, currentStateAsync } from './state.js';
6
+ import { isControllerLog, MALFORMED_LOG } from './log.js';
7
+ import { currentState } from './state.js';
7
8
  const CONTEXT = 'Controller identity';
8
9
  /**
9
10
  * The DID the log claims to be, read from its inception before any fold runs — the fold needs the DID
10
11
  * to check the inception against, so it cannot be the thing that produces it.
11
12
  */ function didFromLog(log) {
12
- if (log.length === 0) {
13
+ if (!isControllerLog(log)) {
14
+ throw new Error(`${CONTEXT}: ${MALFORMED_LOG}`);
15
+ }
16
+ if (log.chain.length === 0) {
13
17
  throw new Error(`${CONTEXT}: empty log`);
14
18
  }
15
- const first = log[0];
19
+ const first = log.chain[0];
16
20
  if (first.event.t !== 'icp') {
17
21
  throw new Error(`${CONTEXT}: first event must be an inception`);
18
22
  }
@@ -48,15 +52,16 @@ const CONTEXT = 'Controller identity';
48
52
  }
49
53
  /**
50
54
  * 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
55
+ * public half. Shared by both entry points (the seed one through {@link identityForState}), so the
56
+ * membership rule and `kid` binding are written once. Membership, not `keys[0]`: a set may publish several keys and the one in hand need not be
53
57
  * first (co-signers' keys belong to other holders); what must hold is that the resolver can answer
54
58
  * with this key. A key outside the set signs unverifiable tokens, so fail loudly at construction.
55
59
  */ function identityForKey({ privateKey, publicKey, did, state, mismatch }) {
56
60
  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.
61
+ // Defensive, unreachable through the fold today (`verifySignatures` rejects an empty key set, and
62
+ // the chain holds only key-establishing events). Kept so a future event type that can empty the
63
+ // set fails here rather than deriving a key for a controller that publishes none. `resolver.ts`
64
+ // holds it too.
60
65
  throw new Error(`Controller ${did} has no signing key`);
61
66
  }
62
67
  const key = encodeKey(publicKey, 'EdDSA');
@@ -68,8 +73,8 @@ const CONTEXT = 'Controller identity';
68
73
  return withKid(createSigningIdentityForDID(did, privateKey), `#${key}`);
69
74
  }
70
75
  /**
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.
76
+ * Derive the signing key the folded state establishes and bind it to the DID: the seed entry point's
77
+ * half, kept apart from folding the log so it reads as the derivation alone.
73
78
  */ function identityForState({ seed, profile, did, state }) {
74
79
  const authority = deriveKeyPair(seed, authorityPath(profile, state.keyGen, state.keySeq), 'EdDSA');
75
80
  const signing = identityForKey({
@@ -80,8 +85,8 @@ const CONTEXT = 'Controller identity';
80
85
  mismatch: 'derived key is not one of the current authority keys'
81
86
  });
82
87
  // 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.
88
+ // derive them at matching indices — so one position locates both. Verified against the folded
89
+ // set, failing closed like the authority membership check.
85
90
  const agreement = deriveKeyPair(seed, agreementPath(profile, state.keyGen, state.keySeq), 'X25519');
86
91
  const encoded = encodeKey(agreement.publicKey, 'X25519');
87
92
  if (!state.agreement.includes(encoded)) {
@@ -95,20 +100,19 @@ const CONTEXT = 'Controller identity';
95
100
  /**
96
101
  * A signing identity for a `did:kokuin:` profile, whose `iss` is the profile DID.
97
102
  *
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
103
+ * Takes the log and folds its chain here, so the signing key is always the one that log's current
104
+ * state establishes, never one the caller named. Its epoch events are not folded: they never change a
105
+ * root key (see `currentState`). The log is the caller's freshness contract: an identity
100
106
  * built from a stale or truncated log signs with a retired key, minting tokens a current verifier
101
107
  * rejects. Rebuild from a re-read log after a rotation rather than holding one across it.
102
108
  *
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.
109
+ * The key is derived at `keyGen`/`keySeq`, the derivation index the fold names for the current keys
110
+ * — on a chain of key-establishing events the same number as `seq`, but the question asked is where
111
+ * the key lives, not where the event sits. Every token carries `kid: #<the key that signed it>`, so
112
+ * there is no `kid` parameter — one naming any other key is a request it could not honour, and is
113
+ * rejected on mismatch rather than dropped.
108
114
  *
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}.
115
+ * Synchronous, and stays so: kubun's apply path depends on it.
112
116
  *
113
117
  * @throws when the log does not fold, or when the derived key is not one of the profile's current
114
118
  * authority keys (a wrong `seed` or `profile` for this log).
@@ -121,27 +125,6 @@ const CONTEXT = 'Controller identity';
121
125
  state: currentState(did, log, CONTEXT)
122
126
  });
123
127
  }
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
128
  /**
146
129
  * A signing identity for a `did:kokuin:` profile, from the current authority private key rather than
147
130
  * the profile seed.
@@ -149,7 +132,7 @@ const CONTEXT = 'Controller identity';
149
132
  * The seed form above needs the root seed to derive the key, but the custody tiers say the daily path
150
133
  * must not touch it: the root seed lives on a Ledger or cold mnemonic, and a device never receives the
151
134
  * 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.
135
+ * pre-rotation).
153
136
  *
154
137
  * What the key in hand can do is exactly the intended granularity: it signs as the profile and cannot
155
138
  * rotate, since a rotate must reveal the key the log committed in `n` and only the seed derives that.
@@ -169,22 +152,3 @@ const CONTEXT = 'Controller identity';
169
152
  mismatch: 'the supplied key is not one of the current authority keys'
170
153
  });
171
154
  }
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 CHANGED
@@ -11,13 +11,18 @@
11
11
  */
12
12
  export { canonicalBytes, digestOf, isCanonicalizable, MAX_CANONICAL_DEPTH, verifyDigest, withinCanonicalDepth, } from './canonical.js';
13
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';
14
+ export { TOO_MANY_FAILED_SIGNATURES } from './epoch.js';
15
+ export { type AddEvent, type CreateAddParams, type CreateRemoveParams, type CreateRenewParams, type CreateRevokeParams, createAdd, createRemove, createRenew, createRevoke, type EpochEvent, type EpochEventCommon, type EpochEventType, type EpochSigner, isEpochEventShape, type RemoveEvent, type RenewEvent, type RevokeEvent, verifyEpochSignature, } from './epoch-events.js';
16
+ export { type CreateRotateParams, createInception, createReset, createRotate, DID_PREFIX, didFor, didFromInception, type EventCommon, type EventType, type InceptionEvent, isMemberEntry, KEY_TARGET_PREFIX, keyFromTarget, keyTarget, type MemberEntry, type RotateEvent, type SignedEvent, type StateSnapshot, verifyInception, verifyReset, verifyRotate, verifySignatures, } from './events.js';
17
+ export { type FoldResult, foldLog, type KeyState, keyStateAt, MAX_SKIPPED_SLACK, type MemberState, ROTATE_NEEDS_SNAPSHOT, SNAPSHOT_DROPS_RETIRED_KEY, SNAPSHOT_MALFORMED, SNAPSHOT_MEMBER_DID_MISMATCH, SNAPSHOT_MEMBER_KEY_IS_ROOT, snapshotOf, TOO_MANY_UNKNOWN_EVENTS, } from './fold.js';
18
+ export { type AuthoritativeLog, type AuthoritativeLogParams, authoritativeLog, createMemoryLogStore, LOG_FORKED, LOG_NOT_AUTHORITATIVE, type LogStore, } from './history.js';
19
+ export { createControllerIdentity, createControllerIdentityWithKey, } from './identity.js';
18
20
  export { decodeKey, encodeKey, type KeyAlgorithm, type TaggedKey, tryDecodeKey, } from './keys.js';
19
21
  export { type ProvideControllerIdentityParams, provideControllerIdentity, } from './keystore-identity.js';
22
+ export { DEFAULT_LEASE_SECONDS, MAX_EPOCH_EVENTS, MAX_EVENT_BYTES, MAX_EVENT_PARENTS, MAX_EVENT_SIGNATURES, MAX_FAILED_SIGNATURES, MAX_LOG_EVENTS, } from './limits.js';
23
+ export { type ControllerLog, epochHeads, isControllerLog, LOG_TOO_LARGE, LOG_UNFOLDABLE, MALFORMED_LOG, mergeLogs, pruneLog, } from './log.js';
20
24
  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';
25
+ export { type ControllerResolver, type ControllerResolverOptions, createControllerResolver, } from './resolver.js';
26
+ export { EPOCH_SATURATED, MEMBER_NOT_EFFECTIVE } from './state-resolver.js';
27
+ export { type Duplicity, type ResolveFailure, type ResolveResult, resolveBranches, } from './supersede.js';
23
28
  export { VERSION_TAG } from './version.js';
package/lib/index.js CHANGED
@@ -10,13 +10,18 @@
10
10
  * @module controller
11
11
  */ export { canonicalBytes, digestOf, isCanonicalizable, MAX_CANONICAL_DEPTH, verifyDigest, withinCanonicalDepth } from './canonical.js';
12
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';
13
+ export { TOO_MANY_FAILED_SIGNATURES } from './epoch.js';
14
+ export { createAdd, createRemove, createRenew, createRevoke, isEpochEventShape, verifyEpochSignature } from './epoch-events.js';
15
+ export { createInception, createReset, createRotate, DID_PREFIX, didFor, didFromInception, isMemberEntry, KEY_TARGET_PREFIX, keyFromTarget, keyTarget, verifyInception, verifyReset, verifyRotate, verifySignatures } from './events.js';
16
+ export { foldLog, keyStateAt, MAX_SKIPPED_SLACK, ROTATE_NEEDS_SNAPSHOT, SNAPSHOT_DROPS_RETIRED_KEY, SNAPSHOT_MALFORMED, SNAPSHOT_MEMBER_DID_MISMATCH, SNAPSHOT_MEMBER_KEY_IS_ROOT, snapshotOf, TOO_MANY_UNKNOWN_EVENTS } from './fold.js';
17
+ export { authoritativeLog, createMemoryLogStore, LOG_FORKED, LOG_NOT_AUTHORITATIVE } from './history.js';
18
+ export { createControllerIdentity, createControllerIdentityWithKey } from './identity.js';
17
19
  export { decodeKey, encodeKey, tryDecodeKey } from './keys.js';
18
20
  export { provideControllerIdentity } from './keystore-identity.js';
21
+ export { DEFAULT_LEASE_SECONDS, MAX_EPOCH_EVENTS, MAX_EVENT_BYTES, MAX_EVENT_PARENTS, MAX_EVENT_SIGNATURES, MAX_FAILED_SIGNATURES, MAX_LOG_EVENTS } from './limits.js';
22
+ export { epochHeads, isControllerLog, LOG_TOO_LARGE, LOG_UNFOLDABLE, MALFORMED_LOG, mergeLogs, pruneLog } from './log.js';
19
23
  export { enumerateProfiles, handleForDID } from './profiles.js';
20
24
  export { createControllerResolver } from './resolver.js';
21
- export { resolveBranches, resolveBranchesAsync } from './supersede.js';
25
+ export { EPOCH_SATURATED, MEMBER_NOT_EFFECTIVE } from './state-resolver.js';
26
+ export { resolveBranches } from './supersede.js';
22
27
  export { VERSION_TAG } from './version.js';
@@ -1,5 +1,4 @@
1
1
  import type { FullIdentity, KeyEntry } from '@kokuin/token';
2
- import type { FoldOptions } from './fold.js';
3
2
  import type { LogStore } from './history.js';
4
3
  export type ProvideControllerIdentityParams = {
5
4
  /** The seed source: `provideAsync()`'s bytes are the controller root seed. Generates on first use. */
@@ -8,8 +7,6 @@ export type ProvideControllerIdentityParams = {
8
7
  profile: number;
9
8
  /** Where the last accepted log for the DID lives — loaded to restore, written to on first generate. */
10
9
  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
10
  };
14
11
  /**
15
12
  * Resolve a `did:kokuin:` {@link FullIdentity} from a keystore entry and a log store.
@@ -19,4 +16,4 @@ export type ProvideControllerIdentityParams = {
19
16
  * log yet). The DID is a pure function of `(seed, profile)`, so the same entry and profile always
20
17
  * resolve the same identity. A `KeyStore` caller passes `keyStore.entry(keyID)` as `entry`.
21
18
  */
22
- export declare function provideControllerIdentity({ entry, profile, logStore, options, }: ProvideControllerIdentityParams): Promise<FullIdentity>;
19
+ export declare function provideControllerIdentity({ entry, profile, logStore, }: ProvideControllerIdentityParams): Promise<FullIdentity>;
@@ -1,5 +1,5 @@
1
1
  import { createInception, didFor } from './events.js';
2
- import { createControllerIdentityAsync } from './identity.js';
2
+ import { createControllerIdentity } from './identity.js';
3
3
  /**
4
4
  * Resolve a `did:kokuin:` {@link FullIdentity} from a keystore entry and a log store.
5
5
  *
@@ -7,20 +7,23 @@ import { createControllerIdentityAsync } from './identity.js';
7
7
  * one after), and the log store yields the event log (bootstrapping the inception when the DID has no
8
8
  * log yet). The DID is a pure function of `(seed, profile)`, so the same entry and profile always
9
9
  * resolve the same identity. A `KeyStore` caller passes `keyStore.entry(keyID)` as `entry`.
10
- */ export async function provideControllerIdentity({ entry, profile, logStore, options }) {
10
+ */ export async function provideControllerIdentity({ entry, profile, logStore }) {
11
11
  const seed = await entry.provideAsync();
12
12
  const did = didFor(seed, profile);
13
13
  let log = await logStore.get(did);
14
- if (log == null) {
15
- log = [
14
+ if (log == null || log.chain.length === 0) {
15
+ const inception = [
16
16
  createInception(seed, profile)
17
17
  ];
18
- await logStore.set(did, log);
18
+ // Through the merge, so a log another writer stored meanwhile is kept rather than overwritten.
19
+ log = await logStore.merge(did, async (current)=>current != null && current.chain.length > 0 ? current : {
20
+ chain: inception,
21
+ events: []
22
+ });
19
23
  }
20
- return createControllerIdentityAsync({
24
+ return createControllerIdentity({
21
25
  seed,
22
26
  profile,
23
- log,
24
- options
27
+ log
25
28
  });
26
29
  }
@@ -0,0 +1,29 @@
1
+ /** Authenticated, causally valid events one epoch may carry before it saturates. */
2
+ export declare const MAX_EPOCH_EVENTS = 4096;
3
+ /**
4
+ * Request limit: epoch events in one input. Above {@link MAX_EPOCH_EVENTS}, so a saturating epoch can
5
+ * still be presented whole and remembered as saturated rather than turned away as oversized.
6
+ */
7
+ export declare const MAX_LOG_EVENTS = 8192;
8
+ /**
9
+ * Request limit: signatures that fail to verify in one input, counted over every distinct copy the
10
+ * fold checks, each charged for every signature it carries. Past it the input is refused as a whole.
11
+ *
12
+ * Nothing honest fails a signature check: every copy a verifier holds verified when it was kept, and
13
+ * Ed25519 signing is deterministic, so a relay passing on what it holds carries no failures at all. So
14
+ * the limit can be small, and is: 64 checks cost a few tens of milliseconds, against the seconds an
15
+ * unbounded flood of forgeries cost when the limit was only {@link MAX_LOG_EVENTS}. It is above zero
16
+ * so that a little corruption in transit does not cost a whole presentation.
17
+ */
18
+ export declare const MAX_FAILED_SIGNATURES = 64;
19
+ /** Request limit: parents one epoch event may list. */
20
+ export declare const MAX_EVENT_PARENTS = 16;
21
+ /** Request limit: canonical bytes of one epoch event body. */
22
+ export declare const MAX_EVENT_BYTES = 8192;
23
+ /** A member's lease when the caller names none: 90 days, in seconds. */
24
+ export declare const DEFAULT_LEASE_SECONDS = 7776000;
25
+ /**
26
+ * Request limit: signatures one epoch event may carry. Not counted in {@link MAX_EVENT_BYTES}, so it
27
+ * bounds the walk separately; a member event carries exactly one, a root event one per chain key.
28
+ */
29
+ export declare const MAX_EVENT_SIGNATURES = 16;
package/lib/limits.js ADDED
@@ -0,0 +1,27 @@
1
+ // Two kinds of limit, never confused. A *request* limit rejects input: unauthenticated or invalid
2
+ // events only ever get turned away, they never change remembered state. *Saturation* is the one
3
+ // limit authenticated input can reach: an epoch carrying more distinct, causally valid events than
4
+ // it allows fails closed until root settles into a fresh one. Work is counted in events, edges and
5
+ // signatures, never elapsed time, so every verifier draws the same line.
6
+ /** Authenticated, causally valid events one epoch may carry before it saturates. */ export const MAX_EPOCH_EVENTS = 4096;
7
+ /**
8
+ * Request limit: epoch events in one input. Above {@link MAX_EPOCH_EVENTS}, so a saturating epoch can
9
+ * still be presented whole and remembered as saturated rather than turned away as oversized.
10
+ */ export const MAX_LOG_EVENTS = 8192;
11
+ /**
12
+ * Request limit: signatures that fail to verify in one input, counted over every distinct copy the
13
+ * fold checks, each charged for every signature it carries. Past it the input is refused as a whole.
14
+ *
15
+ * Nothing honest fails a signature check: every copy a verifier holds verified when it was kept, and
16
+ * Ed25519 signing is deterministic, so a relay passing on what it holds carries no failures at all. So
17
+ * the limit can be small, and is: 64 checks cost a few tens of milliseconds, against the seconds an
18
+ * unbounded flood of forgeries cost when the limit was only {@link MAX_LOG_EVENTS}. It is above zero
19
+ * so that a little corruption in transit does not cost a whole presentation.
20
+ */ export const MAX_FAILED_SIGNATURES = 64;
21
+ /** Request limit: parents one epoch event may list. */ export const MAX_EVENT_PARENTS = 16;
22
+ /** Request limit: canonical bytes of one epoch event body. */ export const MAX_EVENT_BYTES = 8192;
23
+ /** A member's lease when the caller names none: 90 days, in seconds. */ export const DEFAULT_LEASE_SECONDS = 7_776_000;
24
+ /**
25
+ * Request limit: signatures one epoch event may carry. Not counted in {@link MAX_EVENT_BYTES}, so it
26
+ * bounds the walk separately; a member event carries exactly one, a root event one per chain key.
27
+ */ export const MAX_EVENT_SIGNATURES = 16;
package/lib/log.d.ts ADDED
@@ -0,0 +1,124 @@
1
+ import { type EpochLimits } from './epoch.js';
2
+ import { type EpochEvent } from './epoch-events.js';
3
+ import type { SignedEvent } from './events.js';
4
+ import { type FoldResult, type KeyState } from './fold.js';
5
+ /**
6
+ * Everything a controller's log holds: the root chain, which fixes the keys and every settlement, and
7
+ * the epoch events that built up on the chain's last event since. They are separate lists because
8
+ * they fold by different rules -- the chain is a sequence, order is meaning; the epoch is a set,
9
+ * order is nothing -- and a presentation of one is no claim about the other.
10
+ */
11
+ export type ControllerLog = {
12
+ chain: Array<SignedEvent>;
13
+ events: Array<SignedEvent<EpochEvent>>;
14
+ };
15
+ /** What a request carrying more epoch events than {@link MAX_LOG_EVENTS} fails with. */
16
+ export declare const LOG_TOO_LARGE = "controller log exceeds request limits";
17
+ /** What an input that is not a {@link ControllerLog} at all fails with. */
18
+ export declare const MALFORMED_LOG = "malformed log";
19
+ /**
20
+ * Whether `value` has the two lists a log is made of. Only the outer shape: every entry is untrusted
21
+ * and is checked by the fold that reads it, which reports a bad one rather than throwing.
22
+ */
23
+ export declare function isControllerLog(value: unknown): value is ControllerLog;
24
+ /** What a resolver answers with when the loaded log loses to the one already seen. */
25
+ export declare const LOG_NOT_AUTHORITATIVE = "the loaded log does not supersede the one already seen";
26
+ /** What a resolver answers with when the loaded log and the one already seen are a genuine fork. */
27
+ export declare const LOG_FORKED = "the loaded log and the one already seen are a fork";
28
+ /** What {@link mergeLogs} and {@link pruneLog} fail with when no chain they were given folds. */
29
+ export declare const LOG_UNFOLDABLE = "no controller log chain folds";
30
+ /** A fold of a held log, with the copy of each applied event whose signature the fold verified. */
31
+ export type HeldFold = {
32
+ result: Extract<FoldResult, {
33
+ ok: true;
34
+ }>;
35
+ /** Per applied digest, the copy that verified. See {@link appliedCopies}. */
36
+ authentic: ReadonlyMap<string, SignedEvent<EpochEvent>>;
37
+ };
38
+ /**
39
+ * Fold a log this party already holds: the chain through the full fold, the epoch under the held
40
+ * request limit rather than `MAX_LOG_EVENTS`. The request limit bounds what one *input* may make a
41
+ * verifier do; a held log is the union of inputs that each passed it and is already bounded by what
42
+ * memory keeps (one epoch, at most `MAX_EPOCH_EVENTS + 1` events once saturated), but the union with a
43
+ * fresh presentation can exceed it. Folding through {@link foldLog} would turn that into
44
+ * `LOG_TOO_LARGE` — "no memory" — which is exactly the forgetting the history exists to prevent.
45
+ *
46
+ * Nothing in `log` is trusted: every signature is checked, under the failed-signature limit.
47
+ */
48
+ export declare function foldHeld(did: string, log: ControllerLog, limits?: Partial<EpochLimits>): HeldFold | Extract<FoldResult, {
49
+ ok: false;
50
+ }>;
51
+ /**
52
+ * The chain's states merged with `events` as the current epoch, as {@link foldLog} reports it.
53
+ * `trusted` is this party's own memory (see `FoldEpochParams.trusted`), never a presentation. Refused,
54
+ * at index -1, when `events` passes the failed-signature limit.
55
+ */
56
+ export declare function withEpoch(did: string, states: Array<KeyState>, events: Array<unknown>, limits?: Partial<EpochLimits>, trusted?: Array<unknown>): HeldFold | Extract<FoldResult, {
57
+ ok: false;
58
+ }>;
59
+ /**
60
+ * The parents an epoch event built now should name: the current epoch's applied events that no
61
+ * applied event names as a parent, sorted ascending, or `[head.epoch]` when none applied. An event
62
+ * naming every tip is causally after everything its author has seen, and authorisation is judged
63
+ * against exactly that causal past -- naming fewer leaves the author's own view out of it.
64
+ *
65
+ * Pending and rejected events are never heads: they are not part of the folded epoch. Throws, with
66
+ * the fold's reason, when the log does not fold; a builder has nothing to extend then.
67
+ *
68
+ * At most `MAX_EVENT_PARENTS` are returned, so the answer is always a valid `parents`: when the epoch
69
+ * has more tips (concurrent events are free to make), the smallest digests. The event built on them is
70
+ * causally after those alone; the rest stay tips, and the next event built from `epochHeads` names
71
+ * them, so repeated building folds every tip in. Throws `LOG_TOO_LARGE` on a log larger than any a
72
+ * verifier holds.
73
+ */
74
+ export declare function epochHeads(did: string, log: ControllerLog): Array<string>;
75
+ /**
76
+ * One copy of each event the fold applied, in the fold's own order (parents first): all that may be
77
+ * remembered of an epoch. An entry the fold did not apply was never authenticated, so it never
78
+ * reaches memory however it was presented.
79
+ *
80
+ * The copy is the one the fold itself verified, never re-derived here. Copies of one body share a
81
+ * digest and differ only in signatures, and the fold applies the event if *any* copy verifies — so
82
+ * "applied" alone does not say which copy to keep. Keeping the first one presented would let a relay
83
+ * put a forged copy ahead of the genuine one and have memory keep the forgery, losing the event on
84
+ * the next fold; re-checking signatures against a signer looked up afresh would answer a subtly
85
+ * different question from the one the fold answered, and drop the event whenever they disagree.
86
+ *
87
+ * Saturation: an epoch holding more than `MAX_EPOCH_EVENTS` applied events is kept as only the
88
+ * first `MAX_EPOCH_EVENTS + 1` of them. A prefix of the fold's topological order is closed under
89
+ * ancestors, and an event's authorisation is a function of its ancestors alone, so the prefix refolds
90
+ * to the same events applied — one more than the limit, saturated on every refold, never more held.
91
+ */
92
+ export declare function appliedCopies({ result, authentic }: HeldFold, limits?: Partial<EpochLimits>): Array<SignedEvent<EpochEvent>>;
93
+ /**
94
+ * Two views of one controller's log, combined. A pure merge, for a party joining what it holds with
95
+ * what it was handed (a grant's proof, a peer's copy) before folding.
96
+ *
97
+ * The chain is settled by the branch rules: `b`'s when it extends `a`'s, else the winner of
98
+ * {@link resolveBranches}, and a fork throws rather than merging two histories root never wrote as
99
+ * one. The events are a union, deduplicated by digest *and signatures*: a copy carrying different
100
+ * signatures is kept beside the first, since the fold applies an event when any copy verifies and
101
+ * keeping only the first would let a forged copy shadow the genuine one. Entries that are not epoch
102
+ * events at all are dropped; the fold would only have refused them.
103
+ *
104
+ * Nothing here authenticates an event. The result is input to a fold, not memory: persist what
105
+ * `authoritativeLog` returns instead. A union of two inputs can exceed the request limit a single
106
+ * input is held to, which {@link foldLog} then reports. Each input may be at most what a verifier
107
+ * holds plus one presentation; a larger one throws `LOG_TOO_LARGE` before any work.
108
+ */
109
+ export declare function mergeLogs(did: string, a: ControllerLog, b: ControllerLog): ControllerLog;
110
+ /**
111
+ * The least of a log that still folds to the same head: the inception, the current generation's
112
+ * chain, and the current epoch's applied events.
113
+ *
114
+ * Earlier generations go because a reset chains to the inception, not to what preceded it, and the
115
+ * generation's keys and tombstones are read off the generation alone. Unknown events the chain fold
116
+ * skipped go too: they moved no state, so nothing chains to them. Earlier epochs' events go because
117
+ * an event naming another epoch has no effect; unapplied ones because they were never authenticated.
118
+ * A saturated epoch keeps exactly enough to stay saturated (see {@link appliedCopies}).
119
+ *
120
+ * Meant for a log this party already holds: the epoch is folded under the held request limit, not
121
+ * `MAX_LOG_EVENTS` (see {@link foldHeld}). Throws {@link LOG_UNFOLDABLE} when the chain does not fold,
122
+ * and the request limit's reason when the log passes one.
123
+ */
124
+ export declare function pruneLog(did: string, log: ControllerLog): ControllerLog;