@kokuin/records 0.0.0-stage → 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/apply.d.ts ADDED
@@ -0,0 +1,211 @@
1
+ import type { MethodRegistry } from '@kokuin/token';
2
+ import type { HLCTimestamp } from '@sozai/hlc';
3
+ import type { RecordDecision } from './decision.js';
4
+ import { type VerifiedRecordOp } from './op.js';
5
+ import { type EqualVersionDecision, type MergePolicy } from './policy.js';
6
+ import { type RecordKey, type SignedRecord } from './record.js';
7
+ import { type RecordLockKey, type RecordStoreAdapter } from './store.js';
8
+ /** The authenticated peer asking for records, and the controller it acts for, if any. */
9
+ export type RecordRequester = {
10
+ peerDID: string;
11
+ controllerDID: string | null;
12
+ };
13
+ /**
14
+ * What a responding peer knows when it decides whether a requester may receive a record.
15
+ *
16
+ * - `proofs` are records the requester presented as its own, such as its wrapping of a key.
17
+ * They are hostile input: the responder checks them and never stores or forwards them.
18
+ * - `store` is the responder's own store, read for its holdings and for proof dependencies.
19
+ * - `authorizeProof` is the authorizer apply would run on the proof, bound to the resolvers
20
+ * and to this context's `store`, which it passes as the authorizer's `store`.
21
+ * `stored` is the responder's verified row for the proof's record, or `null` when it holds
22
+ * none, as apply passes it.
23
+ * - `currentKeyVersion` is the responder's current version of the key `keyID` of `owner` (a
24
+ * normalized DID), `null` when it holds none. A key id names a key only within its owner.
25
+ * - `holderTypes` are the types whose records show that their recipient holds a key, keyed by
26
+ * type; a proof's dependencies come from its definition. A record of any other type is
27
+ * never a holding, whatever its `meta` says, so a revocation naming a key and a recipient
28
+ * cannot pass for a wrapping. Empty, no one is a key holder.
29
+ */
30
+ export type VisibilityContext = {
31
+ requester: RecordRequester;
32
+ opKind: string;
33
+ /** Caller-owned transport state, passed unchanged and never signed. */
34
+ transportContext?: unknown;
35
+ proofs: ReadonlyArray<SignedRecord>;
36
+ store: RecordStoreAdapter;
37
+ authorizeProof: (record: SignedRecord, stored: SignedRecord | null) => Promise<boolean>;
38
+ currentKeyVersion: (owner: string, keyID: string) => Promise<number | null>;
39
+ isGroupMember: (groupID: string, did: string) => Promise<boolean>;
40
+ holderTypes: ReadonlyMap<string, RecordTypeDefinition>;
41
+ methods?: MethodRegistry;
42
+ };
43
+ /**
44
+ * What an authorizer reads beside the verified op.
45
+ *
46
+ * - `record` is the record being authorized. Its payload has not been checked against the
47
+ * signed digest yet: an authorizer that reads the payload must call `recordPayloadMatches`
48
+ * first, or it reasons over bytes nobody signed.
49
+ * - `stored` is the record's current verified row, or `null` on a first write.
50
+ * - `store` is the store the decision runs against: the chunk's transaction in apply, the
51
+ * responder's store for a holder proof. An authorizer reads other records through it,
52
+ * never through a store bound outside the transaction.
53
+ */
54
+ export type RecordReceiveContext = {
55
+ /** Caller-owned transport state, passed unchanged and never signed. */
56
+ transportContext?: unknown;
57
+ };
58
+ export type RecordAuthorizeContext = RecordReceiveContext & {
59
+ record: SignedRecord;
60
+ stored: SignedRecord | null;
61
+ store: RecordStoreAdapter;
62
+ /**
63
+ * Another record's decision as the chunk has left it, records stored earlier in the chunk
64
+ * included. Apply serves it from the chunk's batched reads. Absent outside apply: read
65
+ * `store` instead.
66
+ */
67
+ decision?: (key: RecordKey) => Promise<RecordDecision | null>;
68
+ resolveController: (did: string) => Promise<string | null>;
69
+ verifyCapability: (tokens: ReadonlyArray<string>, owner: string, issuer: string) => Promise<boolean>;
70
+ };
71
+ /**
72
+ * A record this one needs before it can be authorized. It is satisfied when the store holds
73
+ * a verified op for `(type, id)` and, if given, `satisfiedBy` accepts that op — a wrapping for
74
+ * key version 3 is not satisfied by version 2 of the key. An unsatisfied dependency parks the
75
+ * record, the same as a missing one, until a later op satisfies it.
76
+ */
77
+ export type RecordDependency = RecordKey & {
78
+ satisfiedBy?: (stored: VerifiedRecordOp) => boolean;
79
+ };
80
+ /**
81
+ * How the apply engine treats one record type.
82
+ *
83
+ * - `authorize` decides whether the op's verified issuer may write this record. `stored` is
84
+ * the record's current op, or `null` on a first write. The engine itself refuses an owner
85
+ * change under `lww` and `monotonic`, whatever `authorize` answers. `immutable` skips this
86
+ * owner-continuity check. Owner-qualified keys keep different owners' records separate.
87
+ * WARNING: bind `claim.owner` to the owner of the record an immutable record depends on.
88
+ * Checking only the issuer against `claim.owner` can admit a dependent under the wrong owner.
89
+ * - `precheck` is a stateless issuer and signed-shape check before dependencies are read.
90
+ * - `dependsOn` lists the types whose records this one may need, and orders a chunk.
91
+ * - `dependencies` names the exact records this one needs before it can be authorized. It
92
+ * runs only after the signature has verified, and should read the signed claim in `op`:
93
+ * the payload is not checked against its digest until after the merge decision.
94
+ * - `onEqual` must answer the same for (a, b) as for (b, a).
95
+ * - `afterApply` runs inside the chunk's transaction, once the record is stored.
96
+ *
97
+ * Apply keeps every decision it reads for the rest of the chunk and updates it with its own
98
+ * writes. Hooks may write other state through the store, never a record decision.
99
+ */
100
+ export type RecordTypeDefinition = {
101
+ type: string;
102
+ policy: MergePolicy;
103
+ dependsOn: ReadonlyArray<string>;
104
+ /**
105
+ * Whether the op's issuer may write this record. For an `immutable` type the engine skips
106
+ * the owner-continuity check, so this must bind `op.claim.owner` to the owner of the
107
+ * record it depends on.
108
+ */
109
+ authorize(op: VerifiedRecordOp, ctx: RecordAuthorizeContext): Promise<boolean>;
110
+ precheck?: (op: VerifiedRecordOp, record: SignedRecord) => Promise<boolean>;
111
+ visibleTo?: (record: SignedRecord, ctx: VisibilityContext) => Promise<boolean>;
112
+ dependencies(record: SignedRecord, op: VerifiedRecordOp): ReadonlyArray<RecordDependency>;
113
+ onEqual?: (incoming: VerifiedRecordOp, stored: VerifiedRecordOp) => EqualVersionDecision;
114
+ afterApply?: (record: SignedRecord, store: RecordStoreAdapter) => Promise<void>;
115
+ /** Retain authenticated branch evidence even when the merge keeps another current op. */
116
+ afterAuthorize?: (record: SignedRecord, store: RecordStoreAdapter) => Promise<void>;
117
+ /** Keys this write can affect; `stored` is a speculative read before the chunk locks. */
118
+ lockKeys?: (incoming: SignedRecord, stored: RecordDecision | null) => ReadonlyArray<RecordLockKey> | Promise<ReadonlyArray<RecordLockKey>>;
119
+ };
120
+ /**
121
+ * Two different ops met at one version of a record. `record` is the incoming one; whichever
122
+ * op won is the one the store now holds. `forked` when the type's `onEqual` hook kept both as
123
+ * branches: the store still holds only the greater `opHash`, and retaining the other branch
124
+ * is the type's work.
125
+ */
126
+ export type RecordConflict = {
127
+ record: SignedRecord;
128
+ storedOpHash: string;
129
+ incomingOpHash: string;
130
+ forked: boolean;
131
+ };
132
+ /**
133
+ * - `applied`: records stored by this chunk.
134
+ * - `parked`: records whose dependencies are not stored, or not yet satisfied. They passed the clock and
135
+ * signature and precheck checks; the caller keeps them and passes them to a later
136
+ * chunk or session, since another peer may hold the dependency.
137
+ * - `rejected`: `reason` is the first check that failed — `clock`, `unknown-type`,
138
+ * `signature`, `owner`, `unauthorized`, the policy's own reason (such as `version-invalid`),
139
+ * or `digest`.
140
+ * - `conflicts`: every equal-version meeting of two different ops, in either outcome.
141
+ *
142
+ * A record that loses the merge, or repeats the stored op, appears in none of them.
143
+ */
144
+ export type ApplyResult = {
145
+ applied: number;
146
+ parked: Array<SignedRecord>;
147
+ rejected: Array<{
148
+ record: SignedRecord;
149
+ reason: string;
150
+ }>;
151
+ conflicts: Array<RecordConflict>;
152
+ };
153
+ export type ApplyRecordChunkParams = RecordReceiveContext & {
154
+ opKind: string;
155
+ records: ReadonlyArray<SignedRecord>;
156
+ store: RecordStoreAdapter;
157
+ types: ReadonlyMap<string, RecordTypeDefinition>;
158
+ nowMS: number;
159
+ maxDriftMS: number;
160
+ receiveClock: (stamp: HLCTimestamp) => void;
161
+ methods?: MethodRegistry;
162
+ /** Called for several DIDs at once before the chunk locks; must accept concurrent calls. */
163
+ resolveController: (did: string) => Promise<string | null>;
164
+ verifyCapability: (tokens: ReadonlyArray<string>, owner: string, issuer: string) => Promise<boolean>;
165
+ };
166
+ /** Thrown when the registered types' `dependsOn` is cyclic or names an unregistered type. */
167
+ export declare class RecordTypeGraphError extends Error {
168
+ constructor(message: string);
169
+ }
170
+ /** A speculative lock set changed; an ambient transaction must roll back before retrying. */
171
+ export declare class RecordSourceChangedError extends Error {
172
+ constructor();
173
+ }
174
+ /** A type hook refused one signed record's content permanently. */
175
+ export declare class RecordContentRefusalError extends Error {
176
+ constructor(message: string);
177
+ }
178
+ /** Whether the store lacks a dependency, or holds an op for it that does not satisfy it. */
179
+ export declare function isDependencyUnsatisfied(dependency: RecordDependency, tx: RecordStoreAdapter, opKind: string, methods?: MethodRegistry): Promise<boolean>;
180
+ /** As {@link isDependencyUnsatisfied}, against a decision already read. */
181
+ export declare function isDependencyUnsatisfiedBy(dependency: RecordDependency, stored: RecordDecision | null, opKind: string, methods?: MethodRegistry): Promise<boolean>;
182
+ /**
183
+ * Apply a chunk of records received from a peer, in one transaction.
184
+ *
185
+ * Each record passes checks in order, and stops at the first it fails:
186
+ * 1. its `hlc` is canonical and within `maxDriftMS` of `nowMS`;
187
+ * 2. its op verifies, and signs exactly the record's `type`, `id`, `owner`, `hlc` and
188
+ * `opHash`;
189
+ * 3. its type's stateless precheck accepts the issuer, before dependency lookup;
190
+ * 4. dependencies are present and satisfied, or the record parks;
191
+ * 5. its type's authorizer accepts the issuer after the owner binding;
192
+ * 6. its type's merge policy prefers it to the stored op, or admits it as a first write;
193
+ * 7. its payload matches the signed digest.
194
+ *
195
+ * Nothing about a record is read before the first check — not the store, not its signature
196
+ * — and `receiveClock` advances the local clock from stored records after the chunk's
197
+ * transaction returns. An owned transaction has committed at that point. Inside a
198
+ * caller-owned transaction, apply advances `receiveClock` before the enclosing transaction
199
+ * commits. Callers needing commit-only clock effects must defer them until that commit.
200
+ * Records apply in the types' dependency order; one whose dependencies are not
201
+ * stored or not yet satisfied is retried after each pass that stored something, and returned
202
+ * as parked when a pass stores nothing.
203
+ *
204
+ * An error from the store, a hook or a resolver rolls the chunk back and propagates. A type
205
+ * hook may use `RecordContentRefusalError` for a deterministic refusal of one record's content.
206
+ *
207
+ * @throws RecordTypeGraphError when the types' dependencies are cyclic or unregistered.
208
+ * @throws RecordSourceChangedError when a changed lock set requires an ambient transaction
209
+ * to roll back, or five owned transaction attempts all observe a changed lock set.
210
+ */
211
+ export declare function applyRecordChunk(params: ApplyRecordChunkParams): Promise<ApplyResult>;