@kubun/store-credential 0.17.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,412 @@
1
+ import type { MethodRegistry } from '@kokuin/token';
2
+ import type { RecordKey, SignedRecord } from '@kubun/records';
3
+ import type { CredentialEntry, CredentialEntryWithProvenance, CredentialFactorDescriptor, CredentialKey, CredentialKeyBranch, CredentialKeyState, CredentialManifestKey, CredentialRecordRow, CredentialRevocation, CredentialWrapping, CredentialWrappingWithBranch } from './tables.js';
4
+ /**
5
+ * The persisted half of a credential manifest — a structural subset of the
6
+ * plugin's signed `CredentialManifestClaim`, so a verified claim can pass
7
+ * straight to {@link CredentialStoreAPI.putManifest} without mapping. This
8
+ * package neither signs nor verifies; the raw token rides alongside as `token`.
9
+ */
10
+ export type CredentialManifestRecord = {
11
+ ownerDID: string;
12
+ recipientDID: string;
13
+ grantorDID: string;
14
+ epoch: number;
15
+ sequence: number;
16
+ keys: Array<CredentialManifestKey>;
17
+ };
18
+ export type ListManifestsParams = {
19
+ ownerDID: string;
20
+ recipientDID: string;
21
+ };
22
+ /** One stored manifest: its persisted record, the signed token that authored it, and delegation tokens. */
23
+ export type StoredCredentialManifest = {
24
+ record: CredentialManifestRecord;
25
+ token: string;
26
+ delegationTokens: Array<string>;
27
+ };
28
+ export type PutWrappingAuthorityParams = {
29
+ wrappingID: string;
30
+ ownerDID?: string;
31
+ envelope: string;
32
+ refreshSeq: number;
33
+ };
34
+ export type WrappingAuthority = {
35
+ envelope: string;
36
+ refreshSeq: number;
37
+ };
38
+ export type NextGrantorSequenceParams = {
39
+ ownerDID: string;
40
+ recipientDID: string;
41
+ grantorDID: string;
42
+ };
43
+ export type NextProvisioningEpochParams = {
44
+ ownerDID: string;
45
+ recipientDID: string;
46
+ grantorDID: string;
47
+ minEpoch: number;
48
+ };
49
+ export type ListDistributableKeysParams = {
50
+ ownerDID: string;
51
+ grantorDID: string;
52
+ };
53
+ export type PutCredentialKeyParams = {
54
+ keyID: string;
55
+ ownerDID: string;
56
+ suite: number;
57
+ version: number;
58
+ state: CredentialKeyState;
59
+ };
60
+ export type PutCredentialWrappingParams = {
61
+ wrappingID: string;
62
+ keyID: string;
63
+ ownerDID?: string;
64
+ keyVersion: number;
65
+ factors: Array<CredentialFactorDescriptor>;
66
+ iv: Uint8Array;
67
+ wrappedKey: Uint8Array;
68
+ recipientDID?: string | null;
69
+ authority?: WrappingAuthority;
70
+ };
71
+ export type PutCredentialManifestParams = {
72
+ record: CredentialManifestRecord;
73
+ token: string;
74
+ delegationTokens?: Array<string>;
75
+ };
76
+ export type PutCredentialEntryParams = {
77
+ entryID: string;
78
+ keyID: string;
79
+ ownerDID?: string;
80
+ keyVersion: number;
81
+ iv: Uint8Array;
82
+ ciphertext: Uint8Array;
83
+ hlc: string;
84
+ };
85
+ export type UpdateStoredEntryIfUnchangedParams = PutCredentialEntryParams & {
86
+ expectedHLC: string;
87
+ };
88
+ /** The address of a device's own grant: which provider, for which owner, on which device. */
89
+ export type CredentialAliasKey = {
90
+ providerName: string;
91
+ ownerDID: string;
92
+ deviceDID: string;
93
+ };
94
+ export type SetDeviceAliasParams = CredentialAliasKey & {
95
+ entryID: string;
96
+ /** Owner of the entry; defaults to `ownerDID`. */
97
+ entryOwnerDID?: string;
98
+ };
99
+ /** Deletes only when the stored `entryID` matches. */
100
+ export type DeleteDeviceAliasIfEntryParams = CredentialAliasKey & {
101
+ entryID: string;
102
+ };
103
+ export type ListDeviceAliasOwnersParams = {
104
+ providerName: string;
105
+ deviceDID: string;
106
+ };
107
+ /**
108
+ * Per-device grant pointers. Local, like the two-key aliases: each device points at the entry
109
+ * holding its own grant. Values are stored and compared verbatim, so a caller normalizes.
110
+ */
111
+ export type CredentialDeviceAliasAPI = {
112
+ getDeviceAliasEntry(key: CredentialAliasKey): Promise<{
113
+ entryID: string;
114
+ entryOwnerDID: string;
115
+ } | null>;
116
+ getDeviceAliasEntryID(key: CredentialAliasKey): Promise<string | null>;
117
+ /** Point `(provider, owner, device)` at `entryID`, replacing any prior target. */
118
+ setDeviceAliasEntryID(params: SetDeviceAliasParams): Promise<void>;
119
+ deleteDeviceAliasEntryID(key: CredentialAliasKey): Promise<void>;
120
+ deleteDeviceAliasIfEntry(params: DeleteDeviceAliasIfEntryParams): Promise<void>;
121
+ listDeviceAliasOwnersForProvider(params: ListDeviceAliasOwnersParams): Promise<Array<string>>;
122
+ /** Every device alias of one owner, ordered by provider then device. */
123
+ listDeviceAliasesForOwner(ownerDID: string): Promise<Array<CredentialAliasKey & {
124
+ entryID: string;
125
+ }>>;
126
+ };
127
+ /**
128
+ * Persistence for credential keys, wrappings and entries.
129
+ *
130
+ * Storage only: nothing here wraps, unwraps, encrypts or decrypts, and nothing
131
+ * here interprets a ciphertext. `ciphertext` and `wrapped_key` are opaque byte
132
+ * strings that come back exactly as they went in — not parsed as JSON, not
133
+ * decoded as UTF-8, not validated in any way. A caller owning the crypto is what
134
+ * lets the suite change without a migration.
135
+ *
136
+ * Replicated writes use signed records; local GraphQL writes use these row verbs.
137
+ * Owner-qualified arguments address one projection. An omitted owner is accepted
138
+ * only when the stored ID has one owner; ambiguous IDs fail closed. Legacy local
139
+ * writes without a key row use the empty owner namespace.
140
+ */
141
+ export type CredentialStoreAPI = CredentialDeviceAliasAPI & {
142
+ /** Whether this store is bound to a caller-owned database transaction. */
143
+ readonly isTransaction?: boolean;
144
+ /**
145
+ * Run `fn` against a `CredentialStoreAPI` scoped to one atomic transaction.
146
+ * Nesting-safe: if `this` is already bound to an open Kysely transaction,
147
+ * `fn` runs inline against `this` instead of opening a second one (Kysely
148
+ * has no nested transactions), so callers can invoke this unconditionally.
149
+ */
150
+ transaction<R>(fn: (store: CredentialStoreAPI) => Promise<R>): Promise<R>;
151
+ /**
152
+ * Serialize writers of one key's version, wrappings and entries.
153
+ * Call first inside a transaction, before reading those rows. The lock lasts
154
+ * until that transaction commits or rolls back, even if the key row is absent.
155
+ * Call `lockKeyRows` instead when a transaction can touch more than one key.
156
+ */
157
+ lockKeyRow(keyID: string): Promise<void>;
158
+ /** Acquire a complete key set before authoritative reads; Postgres orders by advisory ID. */
159
+ lockKeyRows(keyIDs: ReadonlyArray<string>): Promise<void>;
160
+ putKey(params: PutCredentialKeyParams): Promise<void>;
161
+ getKey(keyID: string, ownerDID?: string): Promise<CredentialKey | undefined>;
162
+ listKeys(ownerDID: string): Promise<Array<CredentialKey>>;
163
+ setKeyState(keyID: string, state: CredentialKeyState, ownerDID?: string): Promise<void>;
164
+ /** Bump a key to a new version. Wrappings and entries at earlier versions stay. */
165
+ setKeyVersion(keyID: string, version: number, ownerDID?: string): Promise<void>;
166
+ deleteKey(keyID: string, ownerDID?: string): Promise<void>;
167
+ listKeyBranches(keyID: string, keyVersion: number, ownerDID?: string): Promise<Array<CredentialKeyBranch>>;
168
+ /**
169
+ * `authority`, when present, upserts the wrapping's grantor-authority envelope
170
+ * in the SAME transaction as the wrapping row, so a crash can't
171
+ * leave a held wrapping this device can't later prove was authorized. Omitted
172
+ * for an owner-signed wrapping. Gated strictly-greater on `refreshSeq`, same
173
+ * as {@link CredentialStoreAPI.putWrappingAuthority}.
174
+ */
175
+ putWrapping(params: PutCredentialWrappingParams): Promise<void>;
176
+ getWrapping(wrappingID: string, ownerDID?: string): Promise<CredentialWrapping | undefined>;
177
+ /** Every wrapping for a key, or only those at one version. */
178
+ listWrappings(keyID: string, keyVersion?: number, ownerDID?: string): Promise<Array<CredentialWrapping>>;
179
+ /**
180
+ * As {@link listWrappings}, each row carrying the branch of its replicated record.
181
+ * The fork-aware read path
182
+ * needs the label to resolve which content-key branch a device opens.
183
+ */
184
+ listWrappingsWithBranch(keyID: string, keyVersion?: number, ownerDID?: string): Promise<Array<CredentialWrappingWithBranch>>;
185
+ deleteWrapping(wrappingID: string, ownerDID?: string): Promise<void>;
186
+ /**
187
+ * Drop every wrapping for a key below `keyVersion` — what a rotation leaves
188
+ * behind. Replicated deletion uses record tombstones through the record adapter.
189
+ */
190
+ deleteWrappingsBelowVersion(keyID: string, keyVersion: number, ownerDID?: string): Promise<void>;
191
+ putEntry(params: PutCredentialEntryParams): Promise<void>;
192
+ /**
193
+ * Explicitly change an entry's key association. Callers must authorize both
194
+ * source and target keys before invoking this capability.
195
+ */
196
+ moveEntry(params: PutCredentialEntryParams): Promise<void>;
197
+ /**
198
+ * Write over an entry's ciphertext ONLY if the row already exists under the
199
+ * same key — never inserts, never moves it. Returns 'absent' when nothing was updated.
200
+ */
201
+ updateEntryIfPresent(params: PutCredentialEntryParams): Promise<'updated' | 'absent'>;
202
+ /**
203
+ * As {@link updateEntryIfPresent}, and also conditional on the entry row still
204
+ * carrying `expectedHLC`, the stamp its writer read it at. The check and the
205
+ * write are one atomic section, so a write that landed since that read (a new
206
+ * consent, say) is never overwritten: it reports `'superseded'` and touches
207
+ * nothing.
208
+ */
209
+ updateEntryIfUnchanged(params: UpdateStoredEntryIfUnchangedParams): Promise<'updated' | 'absent' | 'superseded'>;
210
+ getEntry(entryID: string, ownerDID?: string): Promise<CredentialEntry | undefined>;
211
+ listEntryOwners(entryID: string): Promise<Array<string>>;
212
+ getEntryWithProvenance(entryID: string, ownerDID?: string): Promise<CredentialEntryWithProvenance | undefined>;
213
+ listEntries(keyID: string, ownerDID?: string): Promise<Array<CredentialEntry>>;
214
+ /**
215
+ * Greatest stored entry HLC, or `null` when there are none. Credential writes
216
+ * advance the device clock but leave no row in the graph mutation log, so this
217
+ * is the only trace they leave for a restart floor to read.
218
+ */
219
+ getMaxEntryHLC(): Promise<string | null>;
220
+ deleteEntry(entryID: string, ownerDID?: string): Promise<void>;
221
+ /** Store a wrapping's grantor-authority envelope; gated strictly-greater on `refreshSeq` (stale/replayed is a silent no-op). */
222
+ putWrappingAuthority(params: PutWrappingAuthorityParams): Promise<void>;
223
+ /** The stored envelope and its `refreshSeq`, or `undefined` when unset. */
224
+ getWrappingAuthority(wrappingID: string, ownerDID?: string): Promise<WrappingAuthority | undefined>;
225
+ /**
226
+ * Mint the next `refreshSeq` for one `(ownerDID, recipientDID, grantorDID)`
227
+ * triple: 1 on the first call, strictly increasing thereafter. Persisted, so
228
+ * a restart never reissues a number already handed out.
229
+ */
230
+ nextGrantorSequence(params: NextGrantorSequenceParams): Promise<number>;
231
+ /**
232
+ * Allocate the next `epoch` for one `(ownerDID, recipientDID, grantorDID)`
233
+ * triple: returns `max(stored, minEpoch) + 1` on each call, so the value is
234
+ * strictly increasing but a floor can jump it forward. Persisted; never
235
+ * reissues an epoch already handed out. Triple is keyed on normalized DIDs,
236
+ * so a `did:peer:4` short/long form converge on one row.
237
+ */
238
+ nextProvisioningEpoch(params: NextProvisioningEpochParams): Promise<number>;
239
+ /**
240
+ * Store a grantor's manifest, gated lexicographically on `(epoch, sequence)`
241
+ * per `(ownerDID, recipientDID, grantorDID)` triple — higher epoch always
242
+ * wins, sequence breaks ties within an epoch; a stale/replayed value is a
243
+ * silent no-op. Triple keyed on normalized DIDs. `token` is the raw signed
244
+ * manifest; `delegationTokens` are optional wrappings that authorized it.
245
+ */
246
+ putManifest(params: PutCredentialManifestParams): Promise<void>;
247
+ /**
248
+ * Every grantor's manifest for one `(ownerDID, recipientDID)`, one row per
249
+ * grantor. Arguments are normalized before query, matching
250
+ * {@link CredentialStoreAPI.putManifest}'s normalize-on-write.
251
+ */
252
+ listManifests(params: ListManifestsParams): Promise<Array<StoredCredentialManifest>>;
253
+ /**
254
+ * Take the write lock on the manifest row so a held-epoch read later in the
255
+ * SAME transaction can't race a concurrent {@link CredentialStoreAPI.putManifest}.
256
+ * A value-preserving UPDATE, not `FOR UPDATE` (SQLite rejects that syntax) —
257
+ * it blocks a concurrent Postgres writer the same way; SQLite is already
258
+ * single-writer-serialized. No-op if the row doesn't exist yet; that
259
+ * first-write race is benign (held epoch 0).
260
+ */
261
+ lockManifestRow(params: {
262
+ ownerDID: string;
263
+ recipientDID: string;
264
+ grantorDID: string;
265
+ }): Promise<void>;
266
+ /**
267
+ * Every active owner key for which a wrapping exists at the key's current
268
+ * version addressed (after normalization) to the grantor. One snapshot read;
269
+ * in-memory DID normalization. Deduped by keyID.
270
+ */
271
+ listDistributableKeys(params: ListDistributableKeysParams): Promise<Array<{
272
+ keyID: string;
273
+ version: number;
274
+ }>>;
275
+ /**
276
+ * Begin tracking a provisioning attempt, idempotent on the id. Ensures an
277
+ * expectation floor row exists (at 0) if none yet. The attempt counts toward
278
+ * outstanding and blocks readiness until cleared.
279
+ */
280
+ beginProvisioningAttempt(params: {
281
+ ownerDID: string;
282
+ grantorDID: string;
283
+ attemptID: string;
284
+ }): Promise<void>;
285
+ /**
286
+ * Record a provisioning attempt's outcome, atomically. If `epoch > heldEpoch`,
287
+ * clears the attempt row and raises the expectation floor to
288
+ * `max(stored, epoch)`; otherwise leaves the attempt row and returns retry.
289
+ */
290
+ recordProvisioningAttempt(params: {
291
+ ownerDID: string;
292
+ grantorDID: string;
293
+ attemptID: string;
294
+ epoch: number;
295
+ heldEpoch: number;
296
+ }): Promise<{
297
+ status: 'recorded';
298
+ } | {
299
+ status: 'retry';
300
+ minEpoch: number;
301
+ }>;
302
+ /**
303
+ * Abandon a provisioning attempt. Deletes ONLY the attempt row, never touches
304
+ * the expectation floor.
305
+ */
306
+ abandonProvisioningAttempt(params: {
307
+ ownerDID: string;
308
+ grantorDID: string;
309
+ attemptID: string;
310
+ }): Promise<void>;
311
+ /**
312
+ * Abandon ALL outstanding provisioning attempts for one (owner, grantor)
313
+ * scope. Deletes every attempt row for the pair, never touches the
314
+ * expectation floor. The recovery escape hatch when the attempt id is lost.
315
+ *
316
+ * Exclusive-recovery contract: this also clears any OTHER live coordinator's
317
+ * attempt on the same scope (whose later `recordProvisioningAttempt` would
318
+ * then hit the phantom-attempt guard and not raise the floor). Callers must
319
+ * ensure no other live attempt exists for the scope before calling. When the
320
+ * id is known, prefer `abandonProvisioningAttempt`, which is safe under
321
+ * concurrency.
322
+ */
323
+ abandonProvisioningAttempts(params: {
324
+ ownerDID: string;
325
+ grantorDID: string;
326
+ }): Promise<void>;
327
+ /**
328
+ * List provisioning expectations for one owner: one row per (owner, grantor)
329
+ * pair currently held. Returns the expectation floor and count of outstanding
330
+ * attempts. Normalizes ownerDID on read.
331
+ */
332
+ listProvisioningExpectations(params: {
333
+ ownerDID: string;
334
+ }): Promise<Array<{
335
+ grantorDID: string;
336
+ epochFloor: number;
337
+ outstanding: number;
338
+ }>>;
339
+ /**
340
+ * Every distinct owner DID with a provisioning expectation row, normalized
341
+ * and de-duped — the catalog-independent enumeration
342
+ * `listProvisioningExpectations` can't give on its own (it's per-owner).
343
+ */
344
+ listProvisioningExpectationOwners(): Promise<Array<string>>;
345
+ /**
346
+ * Refresh a controller-owned key's `credential/administer` delegation chain,
347
+ * gated strictly-greater on `refreshSeq` (stale/replayed is a silent no-op,
348
+ * same shape as {@link CredentialStoreAPI.putWrappingAuthority}). Handles
349
+ * both the absent-row insert and the monotone race. Returns whether a row
350
+ * was inserted or updated.
351
+ */
352
+ refreshKeyOwnerDelegation(params: {
353
+ keyID: string;
354
+ ownerDID?: string;
355
+ delegationTokens: Array<string>;
356
+ refreshSeq: number;
357
+ }): Promise<boolean>;
358
+ /**
359
+ * Compare-and-swap a key-owner delegation chain, guarded on the `revision`
360
+ * the caller read. Replaces the row only when its CURRENT `revision` still
361
+ * equals `expectedRevision`, so a writer that read an invalid-present chain
362
+ * and revalidated it can install a replacement without racing another such
363
+ * writer or a concurrent refresh. The guard is `revision`, NOT `refreshSeq`:
364
+ * two distinct valid chains can share a `refreshSeq` (min-exp ms), so guarding
365
+ * on the seq has an ABA hole. Unlike {@link refreshKeyOwnerDelegation} this
366
+ * never inserts and is not monotone — it is the escape hatch for the
367
+ * invalid-present case, where the incoming chain's `refreshSeq` may be lower.
368
+ * Absent row is a no-op false, same as a revision mismatch. A successful
369
+ * write bumps `revision`.
370
+ */
371
+ replaceKeyOwnerDelegationIfUnchanged(params: {
372
+ keyID: string;
373
+ ownerDID?: string;
374
+ expectedRevision: number;
375
+ delegationTokens: Array<string>;
376
+ refreshSeq: number;
377
+ }): Promise<boolean>;
378
+ /** The stored delegation chain, its `refreshSeq`, and its `revision`, or `undefined` when unset. */
379
+ getKeyOwnerDelegation(keyID: string, ownerDID?: string): Promise<{
380
+ delegationTokens: Array<string>;
381
+ refreshSeq: number;
382
+ revision: number;
383
+ } | undefined>;
384
+ /**
385
+ * The stored row of one replicated record, tombstone included, as the table holds it.
386
+ * `credentialRecordAdapter` reads records through this; nothing here decodes the row.
387
+ */
388
+ getRecordRow(type: string, id: string): Promise<CredentialRecordRow | undefined>;
389
+ getRecordRows(key: RecordKey): Promise<Array<CredentialRecordRow>>;
390
+ /** `getRecordRows` of each key, keyed by `recordKeyText`; a key without rows is absent. */
391
+ getRecordRowsForKeys(keys: ReadonlyArray<RecordKey>): Promise<Map<string, Array<CredentialRecordRow>>>;
392
+ deleteRecordCopy(key: RecordKey, opHash: string): Promise<void>;
393
+ /**
394
+ * The stored rows of every record of the given types. Their order is the adapter's
395
+ * collation, which differs between SQLite and Postgres, so a caller that needs an order
396
+ * sorts them itself.
397
+ */
398
+ listRecordRows(types: ReadonlyArray<string>): Promise<Array<CredentialRecordRow>>;
399
+ /**
400
+ * Store a signed record as the current op of its `(type, id)` with its indexed row, in
401
+ * one transaction. See `putCredentialRecord` for what it checks and throws.
402
+ */
403
+ putRecord(record: SignedRecord, methods?: MethodRegistry): Promise<void>;
404
+ /** All authenticated entry candidates, including losing branch operations. */
405
+ listEntryCandidates(keyID: string, keyVersion: number, ownerDID?: string): Promise<Array<SignedRecord>>;
406
+ /** Earlier signed tombstones that must participate in fork repair entry ordering. */
407
+ listPriorEntryTombstones(keyID: string, beforeVersion: number, ownerDID?: string): Promise<Array<SignedRecord>>;
408
+ /** Called after record authorization, including when the candidate loses the merge. */
409
+ retainEntryCandidate(record: SignedRecord, methods?: MethodRegistry): Promise<void>;
410
+ /** The recipients revoked from a key, ordered by recipient. */
411
+ listRevocations(keyID: string, ownerDID?: string): Promise<Array<CredentialRevocation>>;
412
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Persistence for credential keys, wrappings and entries.
3
+ *
4
+ * Storage only: nothing here wraps, unwraps, encrypts or decrypts, and nothing
5
+ * here interprets a ciphertext. `ciphertext` and `wrapped_key` are opaque byte
6
+ * strings that come back exactly as they went in — not parsed as JSON, not
7
+ * decoded as UTF-8, not validated in any way. A caller owning the crypto is what
8
+ * lets the suite change without a migration.
9
+ *
10
+ * Replicated writes use signed records; local GraphQL writes use these row verbs.
11
+ * Owner-qualified arguments address one projection. An omitted owner is accepted
12
+ * only when the stored ID has one owner; ambiguous IDs fail closed. Legacy local
13
+ * writes without a key row use the empty owner namespace.
14
+ */ export { };