@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.
package/lib/api.d.ts CHANGED
@@ -1,436 +1,12 @@
1
1
  import type { Adapter } from '@kubun/db-adapter';
2
- import { type Kysely } from 'kysely';
3
- import type { CredentialEntry, CredentialEntryWithProvenance, CredentialFactorDescriptor, CredentialKey, CredentialKeyBranch, CredentialKeyState, CredentialManifestKey, CredentialOp, CredentialOpSubjectKind, CredentialTables, 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
- envelope: string;
31
- refreshSeq: number;
32
- };
33
- export type WrappingAuthority = {
34
- envelope: string;
35
- refreshSeq: number;
36
- };
37
- export type NextGrantorSequenceParams = {
38
- ownerDID: string;
39
- recipientDID: string;
40
- grantorDID: string;
41
- };
42
- export type NextProvisioningEpochParams = {
43
- ownerDID: string;
44
- recipientDID: string;
45
- grantorDID: string;
46
- minEpoch: number;
47
- };
48
- export type ListDistributableKeysParams = {
49
- ownerDID: string;
50
- grantorDID: string;
51
- };
52
- /**
53
- * The signed op that produced a row's current state, as the store holds it.
54
- *
55
- * `opJWT` is opaque here — this package neither signs nor verifies, the same way
56
- * it neither encrypts nor decrypts. `@kubun/credential` mints these and is the
57
- * only thing that can read one.
58
- */
59
- export type CredentialOpInput = {
60
- keyID: string;
61
- authorDID: string;
62
- hlc: string;
63
- opJWT: string;
64
- opHash: string;
65
- /**
66
- * The content-key branch this op joins — `${hlc}:${opHash}` of the branch's
67
- * introducing key-op. Empty when the op is minted outside a rotation/create
68
- * and so does not resolve the branch it belongs to.
69
- */
70
- branchID: string;
71
- };
72
- /** A tombstone for one subject of a multi-row delete, which names its own. */
73
- export type CredentialTombstoneInput = CredentialOpInput & {
74
- subjectID: string;
75
- };
76
- export type PutCredentialKeyParams = {
77
- keyID: string;
78
- ownerDID: string;
79
- suite: number;
80
- version: number;
81
- state: CredentialKeyState;
82
- };
83
- export type PutCredentialWrappingParams = {
84
- wrappingID: string;
85
- keyID: string;
86
- keyVersion: number;
87
- factors: Array<CredentialFactorDescriptor>;
88
- iv: Uint8Array;
89
- wrappedKey: Uint8Array;
90
- recipientDID?: string | null;
91
- };
92
- export type PutCredentialEntryParams = {
93
- entryID: string;
94
- keyID: string;
95
- keyVersion: number;
96
- iv: Uint8Array;
97
- ciphertext: Uint8Array;
98
- hlc: string;
99
- };
100
- export type HeldCredentialSubject = {
101
- subjectKind: CredentialOpSubjectKind;
102
- subjectID: string;
103
- opHash: string;
104
- };
105
- /**
106
- * Persistence for credential keys, wrappings and entries.
107
- *
108
- * Storage only: nothing here wraps, unwraps, encrypts or decrypts, and nothing
109
- * here interprets a ciphertext. `ciphertext` and `wrapped_key` are opaque byte
110
- * strings that come back exactly as they went in — not parsed as JSON, not
111
- * decoded as UTF-8, not validated in any way. A caller owning the crypto is what
112
- * lets the suite change without a migration.
113
- *
114
- * A deletion is NOT the absence of a row. It is a tombstone in
115
- * `kubun_credential_ops`, and the row is gone from its own table — so asking
116
- * whether a wrapping exists means reading two tables, not one. Absence alone
117
- * cannot travel over a lane that only adds: a peer holding the deleted row would
118
- * ship it back and this device would store it again. So a `put` carrying an op no
119
- * newer than the tombstone writes NOTHING, silently — that is the resend.
120
- *
121
- * The op is REQUIRED, and `null` is how a writer says it has none rather than
122
- * how it says nothing. A row written with `null` is stored but speaks for
123
- * itself, invisible to the sync lane; making the parameter mandatory is what
124
- * stops a write path added later from being silently unsigned. Only the GraphQL
125
- * query context passes it today: it holds a viewer's DID and no signing key.
126
- */
127
- export type CredentialStoreAPI = {
128
- /**
129
- * Run `fn` against a `CredentialStoreAPI` scoped to one atomic transaction.
130
- * Nesting-safe: if `this` is already bound to an open Kysely transaction,
131
- * `fn` runs inline against `this` instead of opening a second one (Kysely
132
- * has no nested transactions), so callers can invoke this unconditionally.
133
- */
134
- transaction<R>(fn: (store: CredentialStoreAPI) => Promise<R>): Promise<R>;
135
- putKey(params: PutCredentialKeyParams, op: CredentialOpInput | null): Promise<void>;
136
- getKey(keyID: string): Promise<CredentialKey | undefined>;
137
- listKeys(ownerDID: string): Promise<Array<CredentialKey>>;
138
- setKeyState(keyID: string, state: CredentialKeyState, op: CredentialOpInput | null): Promise<void>;
139
- /** Bump a key to a new version. Wrappings and entries at earlier versions stay. */
140
- setKeyVersion(keyID: string, version: number, op: CredentialOpInput | null): Promise<void>;
141
- deleteKey(keyID: string, tombstone: CredentialOpInput | null): Promise<void>;
142
- recordKeyBranch(params: {
143
- keyID: string;
144
- keyVersion: number;
145
- authorDID: string;
146
- hlc: string;
147
- opJWT: string;
148
- opHash: string;
149
- branchID: string;
150
- }): Promise<void>;
151
- listKeyBranches(keyID: string, keyVersion: number): Promise<Array<CredentialKeyBranch>>;
152
- /**
153
- * `authority`, when present, upserts the wrapping's grantor-authority envelope
154
- * in the SAME transaction as the wrapping row and its op, so a crash can't
155
- * leave a held wrapping this device can't later prove was authorized. Omitted
156
- * for an owner-signed wrapping. Gated strictly-greater on `refreshSeq`, same
157
- * as {@link CredentialStoreAPI.putWrappingAuthority}.
158
- */
159
- putWrapping(params: PutCredentialWrappingParams, op: CredentialOpInput | null, authority?: {
160
- envelope: string;
161
- refreshSeq: number;
162
- }): Promise<void>;
163
- getWrapping(wrappingID: string): Promise<CredentialWrapping | undefined>;
164
- /** Every wrapping for a key, or only those at one version. */
165
- listWrappings(keyID: string, keyVersion?: number): Promise<Array<CredentialWrapping>>;
166
- /**
167
- * As {@link listWrappings}, each row carrying its op's `branch_id`. The
168
- * fork-aware read path needs the label to resolve which content-key branch a
169
- * device opens; every other caller reads none, so the join stays off the
170
- * common path.
171
- */
172
- listWrappingsWithBranch(keyID: string, keyVersion?: number): Promise<Array<CredentialWrappingWithBranch>>;
173
- deleteWrapping(wrappingID: string, tombstone: CredentialOpInput | null): Promise<void>;
174
- /**
175
- * Drop every wrapping for a key below `keyVersion` — what a rotation leaves
176
- * behind. The tombstones name their own subjects, because the caller decides
177
- * which rows it is accounting for and the delete predicate spans many.
178
- */
179
- deleteWrappingsBelowVersion(keyID: string, keyVersion: number, tombstones: Array<CredentialTombstoneInput> | null): Promise<void>;
180
- putEntry(params: PutCredentialEntryParams, op: CredentialOpInput | null): Promise<void>;
181
- /**
182
- * Explicitly change an entry's key association. Callers must authorize both
183
- * source and target keys before invoking this capability.
184
- */
185
- moveEntry(params: PutCredentialEntryParams, op: CredentialOpInput | null): Promise<void>;
186
- /**
187
- * Write over an entry's ciphertext ONLY if the row already exists under the
188
- * same key — never inserts, never moves it. Op-first like putEntry: the op is
189
- * written under the op-conflict lock, then the row; if no matching row is
190
- * present (concurrently deleted, or re-keyed away) the op is rolled back, so a
191
- * refresh writeback can neither revive a concurrently-deleted entry nor leave
192
- * an op stranded without its row. Returns 'absent' when nothing was updated.
193
- */
194
- updateEntryIfPresent(params: PutCredentialEntryParams, op: CredentialOpInput | null): Promise<'updated' | 'absent'>;
195
- getEntry(entryID: string): Promise<CredentialEntry | undefined>;
196
- getEntryWithProvenance(entryID: string): Promise<CredentialEntryWithProvenance | undefined>;
197
- listEntries(keyID: string): Promise<Array<CredentialEntry>>;
198
- /**
199
- * Greatest stored entry HLC, or `null` when there are none. Credential writes
200
- * advance the device clock but leave no row in the graph mutation log, so this
201
- * is the only trace they leave for a restart floor to read.
202
- */
203
- getMaxEntryHLC(): Promise<string | null>;
204
- deleteEntry(entryID: string, tombstone: CredentialOpInput | null): Promise<void>;
205
- /** The op a subject's current state came from, tombstone included. */
206
- getOp(subjectKind: CredentialOpSubjectKind, subjectID: string): Promise<CredentialOp | undefined>;
207
- /**
208
- * Ops in stamp order — every one, or one key's. This is the merkle lane's leaf
209
- * scan, which is why the ops of all three subject kinds share a table.
210
- */
211
- listOps(keyID?: string): Promise<Array<CredentialOp>>;
212
- /**
213
- * The leaf scan scoped to what a requesting peer may hold.
214
- *
215
- * The document lane serves a tree scoped by read-ACL, but a credential is not
216
- * readable by scope: a group co-member holds no wrapping under someone else's
217
- * key, so serving them its rows ships ciphertext they can never open and reveals
218
- * the key exists. The predicate is recipient-addressed instead. A key's row and
219
- * its entry ops travel to any recipient of the key — a peer holding a wrapping
220
- * under it. A wrapping's op travels only to the DID it names, so a recipient
221
- * never receives a co-recipient's wrapping. Recipient match folds both sides
222
- * with `normalizeDID`, because `recipient_did` is stored un-normalized.
223
- */
224
- listOpsForRequester(requesterDID: string, keyID?: string): Promise<Array<CredentialOp>>;
225
- /**
226
- * The live (non-tombstoned) subject ids this device holds — keys, wrappings and
227
- * entries alike. What a reconcile caller advertises as `held`: the server reads
228
- * it to skip rows the caller already has and to route a tombstone for a held id
229
- * that is now revoked.
230
- */
231
- listHeldSubjects(): Promise<Array<HeldCredentialSubject>>;
232
- /**
233
- * The entry id currently pointed to by an opaque `(namespace, key)`, or
234
- * `null` when unset. Neither argument is interpreted — a caller normalizes
235
- * whatever it means by "key" before calling.
236
- */
237
- getAliasEntryID(namespace: string, key: string): Promise<string | null>;
238
- /** Point `(namespace, key)` at `entryID`, replacing any prior target. */
239
- setAliasEntryID(namespace: string, key: string, entryID: string): Promise<void>;
240
- /**
241
- * Bind `(namespace, aliasKey)` to `entryID` only if the alias slot is free and
242
- * the entry exists — unlike {@link setAliasEntryID}, this never overwrites an
243
- * existing alias and never binds a nonexistent entry. Classifies the outcome
244
- * instead of throwing, since both unique constraints are expected outcomes a
245
- * caller branches on, not exceptional ones.
246
- */
247
- bindAliasIfAbsent(namespace: string, aliasKey: string, entryID: string): Promise<'inserted' | 'entry-missing' | {
248
- conflict: 'alias';
249
- existing: string;
250
- } | {
251
- conflict: 'entry';
252
- }>;
253
- /** Clear an alias. A no-op, not an error, when none is set. */
254
- deleteAliasEntryID(namespace: string, key: string): Promise<void>;
255
- /**
256
- * Clear an alias only while it still points at `entryID`. A concurrent rebind
257
- * that repaired the pointer onto a fresh entry in the gap after the old entry
258
- * was deleted is left intact; a no-op when the pointer already moved or is unset.
259
- */
260
- deleteAliasIfEntry(namespace: string, key: string, entryID: string): Promise<void>;
261
- /**
262
- * The distinct keys currently set within `namespace`. Keys stay opaque here —
263
- * a caller interprets them (e.g. connector credentials store the normalized
264
- * owner DID as the key under a provider namespace). Empty for an unused
265
- * namespace.
266
- */
267
- listAliasKeys(namespace: string): Promise<Array<string>>;
268
- /** Store a wrapping's grantor-authority envelope; gated strictly-greater on `refreshSeq` (stale/replayed is a silent no-op). */
269
- putWrappingAuthority(params: PutWrappingAuthorityParams): Promise<void>;
270
- /** The stored envelope and its `refreshSeq`, or `undefined` when unset. */
271
- getWrappingAuthority(wrappingID: string): Promise<WrappingAuthority | undefined>;
272
- /**
273
- * Mint the next `refreshSeq` for one `(ownerDID, recipientDID, grantorDID)`
274
- * triple: 1 on the first call, strictly increasing thereafter. Persisted, so
275
- * a restart never reissues a number already handed out.
276
- */
277
- nextGrantorSequence(params: NextGrantorSequenceParams): Promise<number>;
278
- /**
279
- * Allocate the next `epoch` for one `(ownerDID, recipientDID, grantorDID)`
280
- * triple: returns `max(stored, minEpoch) + 1` on each call, so the value is
281
- * strictly increasing but a floor can jump it forward. Persisted; never
282
- * reissues an epoch already handed out. Triple is keyed on normalized DIDs,
283
- * so a `did:peer:4` short/long form converge on one row.
284
- */
285
- nextProvisioningEpoch(params: NextProvisioningEpochParams): Promise<number>;
286
- /**
287
- * Store a grantor's manifest, gated lexicographically on `(epoch, sequence)`
288
- * per `(ownerDID, recipientDID, grantorDID)` triple — higher epoch always
289
- * wins, sequence breaks ties within an epoch; a stale/replayed value is a
290
- * silent no-op. Triple keyed on normalized DIDs. `token` is the raw signed
291
- * manifest; `delegationTokens` are optional wrappings that authorized it.
292
- */
293
- putManifest(record: CredentialManifestRecord, token: string, delegationTokens?: Array<string>): Promise<void>;
294
- /**
295
- * Every grantor's manifest for one `(ownerDID, recipientDID)`, one row per
296
- * grantor. Arguments are normalized before query, matching
297
- * {@link CredentialStoreAPI.putManifest}'s normalize-on-write.
298
- */
299
- listManifests(params: ListManifestsParams): Promise<Array<StoredCredentialManifest>>;
300
- /**
301
- * Take the write lock on the manifest row so a held-epoch read later in the
302
- * SAME transaction can't race a concurrent {@link CredentialStoreAPI.putManifest}.
303
- * A value-preserving UPDATE, not `FOR UPDATE` (SQLite rejects that syntax) —
304
- * it blocks a concurrent Postgres writer the same way; SQLite is already
305
- * single-writer-serialized. No-op if the row doesn't exist yet; that
306
- * first-write race is benign (held epoch 0).
307
- */
308
- lockManifestRow(params: {
309
- ownerDID: string;
310
- recipientDID: string;
311
- grantorDID: string;
312
- }): Promise<void>;
313
- /**
314
- * Every active owner key for which a wrapping exists at the key's current
315
- * version addressed (after normalization) to the grantor. One snapshot read;
316
- * in-memory DID normalization. Deduped by keyID.
317
- */
318
- listDistributableKeys(params: ListDistributableKeysParams): Promise<Array<{
319
- keyID: string;
320
- version: number;
321
- }>>;
322
- /**
323
- * Begin tracking a provisioning attempt, idempotent on the id. Ensures an
324
- * expectation floor row exists (at 0) if none yet. The attempt counts toward
325
- * outstanding and blocks readiness until cleared.
326
- */
327
- beginProvisioningAttempt(params: {
328
- ownerDID: string;
329
- grantorDID: string;
330
- attemptID: string;
331
- }): Promise<void>;
332
- /**
333
- * Record a provisioning attempt's outcome, atomically. If `epoch > heldEpoch`,
334
- * clears the attempt row and raises the expectation floor to
335
- * `max(stored, epoch)`; otherwise leaves the attempt row and returns retry.
336
- */
337
- recordProvisioningAttempt(params: {
338
- ownerDID: string;
339
- grantorDID: string;
340
- attemptID: string;
341
- epoch: number;
342
- heldEpoch: number;
343
- }): Promise<{
344
- status: 'recorded';
345
- } | {
346
- status: 'retry';
347
- minEpoch: number;
348
- }>;
349
- /**
350
- * Abandon a provisioning attempt. Deletes ONLY the attempt row, never touches
351
- * the expectation floor.
352
- */
353
- abandonProvisioningAttempt(params: {
354
- ownerDID: string;
355
- grantorDID: string;
356
- attemptID: string;
357
- }): Promise<void>;
358
- /**
359
- * Abandon ALL outstanding provisioning attempts for one (owner, grantor)
360
- * scope. Deletes every attempt row for the pair, never touches the
361
- * expectation floor. The recovery escape hatch when the attempt id is lost.
362
- *
363
- * Exclusive-recovery contract: this also clears any OTHER live coordinator's
364
- * attempt on the same scope (whose later `recordProvisioningAttempt` would
365
- * then hit the phantom-attempt guard and not raise the floor). Callers must
366
- * ensure no other live attempt exists for the scope before calling. When the
367
- * id is known, prefer `abandonProvisioningAttempt`, which is safe under
368
- * concurrency.
369
- */
370
- abandonProvisioningAttempts(params: {
371
- ownerDID: string;
372
- grantorDID: string;
373
- }): Promise<void>;
374
- /**
375
- * List provisioning expectations for one owner: one row per (owner, grantor)
376
- * pair currently held. Returns the expectation floor and count of outstanding
377
- * attempts. Normalizes ownerDID on read.
378
- */
379
- listProvisioningExpectations(params: {
380
- ownerDID: string;
381
- }): Promise<Array<{
382
- grantorDID: string;
383
- epochFloor: number;
384
- outstanding: number;
385
- }>>;
386
- /**
387
- * Every distinct owner DID with a provisioning expectation row, normalized
388
- * and de-duped — the catalog-independent enumeration
389
- * `listProvisioningExpectations` can't give on its own (it's per-owner).
390
- */
391
- listProvisioningExpectationOwners(): Promise<Array<string>>;
392
- /**
393
- * Refresh a controller-owned key's `credential/administer` delegation chain,
394
- * gated strictly-greater on `refreshSeq` (stale/replayed is a silent no-op,
395
- * same shape as {@link CredentialStoreAPI.putWrappingAuthority}). Handles
396
- * both the absent-row insert and the monotone race. Returns whether a row
397
- * was inserted or updated.
398
- */
399
- refreshKeyOwnerDelegation(params: {
400
- keyID: string;
401
- delegationTokens: Array<string>;
402
- refreshSeq: number;
403
- }): Promise<boolean>;
404
- /**
405
- * Compare-and-swap a key-owner delegation chain, guarded on the `revision`
406
- * the caller read. Replaces the row only when its CURRENT `revision` still
407
- * equals `expectedRevision`, so a writer that read an invalid-present chain
408
- * and revalidated it can install a replacement without racing another such
409
- * writer or a concurrent refresh. The guard is `revision`, NOT `refreshSeq`:
410
- * two distinct valid chains can share a `refreshSeq` (min-exp ms), so guarding
411
- * on the seq has an ABA hole. Unlike {@link refreshKeyOwnerDelegation} this
412
- * never inserts and is not monotone — it is the escape hatch for the
413
- * invalid-present case, where the incoming chain's `refreshSeq` may be lower.
414
- * Absent row is a no-op false, same as a revision mismatch. A successful
415
- * write bumps `revision`.
416
- */
417
- replaceKeyOwnerDelegationIfUnchanged(params: {
418
- keyID: string;
419
- expectedRevision: number;
420
- delegationTokens: Array<string>;
421
- refreshSeq: number;
422
- }): Promise<boolean>;
423
- /** The stored delegation chain, its `refreshSeq`, and its `revision`, or `undefined` when unset. */
424
- getKeyOwnerDelegation(keyID: string): Promise<{
425
- delegationTokens: Array<string>;
426
- refreshSeq: number;
427
- revision: number;
428
- } | undefined>;
429
- };
2
+ import type { Kysely } from 'kysely';
3
+ import type { CredentialStoreAPI } from './api-types.js';
4
+ import type { CredentialTables } from './tables.js';
5
+ export type * from './api-types.js';
430
6
  /**
431
7
  * `refreshSeq` sentinel for a delegation chain that has no future refresh —
432
8
  * larger than any real ms timestamp, so a bounded refresh can never overtake
433
9
  * it. Consumed by `@kubun/plugin-p2p`.
434
10
  */
435
11
  export declare const UNBOUNDED_REFRESH_SEQ = 8640000000000000;
436
- export declare function createCredentialStore(db: Kysely<CredentialTables>, adapter: Adapter): CredentialStoreAPI;
12
+ export declare function createCredentialStore(db: Kysely<CredentialTables>, adapter: Adapter, advisoryIDForKey?: (keyID: string) => bigint | Promise<bigint>): CredentialStoreAPI;