@kubun/store-credential 0.17.0 → 0.17.2

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
@@ -299,7 +299,7 @@ export type CredentialStoreAPI = {
299
299
  beginProvisioningAttempt(params: {
300
300
  ownerDID: string;
301
301
  grantorDID: string;
302
- attemptId: string;
302
+ attemptID: string;
303
303
  }): Promise<void>;
304
304
  /**
305
305
  * Record a provisioning attempt's outcome, atomically. If `epoch > heldEpoch`,
@@ -309,7 +309,7 @@ export type CredentialStoreAPI = {
309
309
  recordProvisioningAttempt(params: {
310
310
  ownerDID: string;
311
311
  grantorDID: string;
312
- attemptId: string;
312
+ attemptID: string;
313
313
  epoch: number;
314
314
  heldEpoch: number;
315
315
  }): Promise<{
@@ -325,7 +325,23 @@ export type CredentialStoreAPI = {
325
325
  abandonProvisioningAttempt(params: {
326
326
  ownerDID: string;
327
327
  grantorDID: string;
328
- attemptId: string;
328
+ attemptID: string;
329
+ }): Promise<void>;
330
+ /**
331
+ * Abandon ALL outstanding provisioning attempts for one (owner, grantor)
332
+ * scope. Deletes every attempt row for the pair, never touches the
333
+ * expectation floor. The recovery escape hatch when the attempt id is lost.
334
+ *
335
+ * Exclusive-recovery contract: this also clears any OTHER live coordinator's
336
+ * attempt on the same scope (whose later `recordProvisioningAttempt` would
337
+ * then hit the phantom-attempt guard and not raise the floor). Callers must
338
+ * ensure no other live attempt exists for the scope before calling. When the
339
+ * id is known, prefer `abandonProvisioningAttempt`, which is safe under
340
+ * concurrency.
341
+ */
342
+ abandonProvisioningAttempts(params: {
343
+ ownerDID: string;
344
+ grantorDID: string;
329
345
  }): Promise<void>;
330
346
  /**
331
347
  * List provisioning expectations for one owner: one row per (owner, grantor)
@@ -345,5 +361,48 @@ export type CredentialStoreAPI = {
345
361
  * `listProvisioningExpectations` can't give on its own (it's per-owner).
346
362
  */
347
363
  listProvisioningExpectationOwners(): Promise<Array<string>>;
364
+ /**
365
+ * Refresh a controller-owned key's `credential/administer` delegation chain,
366
+ * gated strictly-greater on `refreshSeq` (stale/replayed is a silent no-op,
367
+ * same shape as {@link CredentialStoreAPI.putWrappingAuthority}). Handles
368
+ * both the absent-row insert and the monotone race. Returns whether a row
369
+ * was inserted or updated.
370
+ */
371
+ refreshKeyOwnerDelegation(params: {
372
+ keyID: string;
373
+ delegationTokens: Array<string>;
374
+ refreshSeq: number;
375
+ }): Promise<boolean>;
376
+ /**
377
+ * Compare-and-swap a key-owner delegation chain, guarded on the `revision`
378
+ * the caller read. Replaces the row only when its CURRENT `revision` still
379
+ * equals `expectedRevision`, so a writer that read an invalid-present chain
380
+ * and revalidated it can install a replacement without racing another such
381
+ * writer or a concurrent refresh. The guard is `revision`, NOT `refreshSeq`:
382
+ * two distinct valid chains can share a `refreshSeq` (min-exp ms), so guarding
383
+ * on the seq has an ABA hole. Unlike {@link refreshKeyOwnerDelegation} this
384
+ * never inserts and is not monotone — it is the escape hatch for the
385
+ * invalid-present case, where the incoming chain's `refreshSeq` may be lower.
386
+ * Absent row is a no-op false, same as a revision mismatch. A successful
387
+ * write bumps `revision`.
388
+ */
389
+ replaceKeyOwnerDelegationIfUnchanged(params: {
390
+ keyID: string;
391
+ expectedRevision: number;
392
+ delegationTokens: Array<string>;
393
+ refreshSeq: number;
394
+ }): Promise<boolean>;
395
+ /** The stored delegation chain, its `refreshSeq`, and its `revision`, or `undefined` when unset. */
396
+ getKeyOwnerDelegation(keyID: string): Promise<{
397
+ delegationTokens: Array<string>;
398
+ refreshSeq: number;
399
+ revision: number;
400
+ } | undefined>;
348
401
  };
402
+ /**
403
+ * `refreshSeq` sentinel for a delegation chain that has no future refresh —
404
+ * larger than any real ms timestamp, so a bounded refresh can never overtake
405
+ * it. Consumed by `@kubun/plugin-p2p`.
406
+ */
407
+ export declare const UNBOUNDED_REFRESH_SEQ = 8640000000000000;
349
408
  export declare function createCredentialStore(db: Kysely<CredentialTables>, adapter: Adapter): CredentialStoreAPI;
package/lib/api.js CHANGED
@@ -1,4 +1,9 @@
1
1
  import { normalizeDID } from '@kokuin/token';
2
+ /**
3
+ * `refreshSeq` sentinel for a delegation chain that has no future refresh —
4
+ * larger than any real ms timestamp, so a bounded refresh can never overtake
5
+ * it. Consumed by `@kubun/plugin-p2p`.
6
+ */ export const UNBOUNDED_REFRESH_SEQ = 8_640_000_000_000_000;
2
7
  /**
3
8
  * Run `write` as one atomic unit.
4
9
  *
@@ -623,7 +628,7 @@ export function createCredentialStore(db, adapter) {
623
628
  await tx.insertInto('kubun_credential_provisioning_attempts').values({
624
629
  owner_did: ownerDid,
625
630
  grantor_did: grantorDid,
626
- attempt_id: params.attemptId
631
+ attempt_id: params.attemptID
627
632
  }).onConflict((oc)=>oc.doNothing()).execute();
628
633
  });
629
634
  },
@@ -639,12 +644,12 @@ export function createCredentialStore(db, adapter) {
639
644
  if (params.epoch > params.heldEpoch) {
640
645
  // Phantom-attempt guard: if the id names no live attempt, move
641
646
  // nothing — raising the floor for it would wedge readiness forever.
642
- const existing = await tx.selectFrom('kubun_credential_provisioning_attempts').select('attempt_id').where('owner_did', '=', ownerDid).where('grantor_did', '=', grantorDid).where('attempt_id', '=', params.attemptId).executeTakeFirst();
647
+ const existing = await tx.selectFrom('kubun_credential_provisioning_attempts').select('attempt_id').where('owner_did', '=', ownerDid).where('grantor_did', '=', grantorDid).where('attempt_id', '=', params.attemptID).executeTakeFirst();
643
648
  if (existing == null) {
644
649
  return;
645
650
  }
646
651
  // Delete the attempt row.
647
- await tx.deleteFrom('kubun_credential_provisioning_attempts').where('owner_did', '=', ownerDid).where('grantor_did', '=', grantorDid).where('attempt_id', '=', params.attemptId).execute();
652
+ await tx.deleteFrom('kubun_credential_provisioning_attempts').where('owner_did', '=', ownerDid).where('grantor_did', '=', grantorDid).where('attempt_id', '=', params.attemptID).execute();
648
653
  // Raise the floor to max(stored, epoch).
649
654
  await tx.updateTable('kubun_credential_provisioning_expectations').set((eb)=>({
650
655
  epoch_floor: eb.case().when('epoch_floor', '<', params.epoch).then(params.epoch).else(eb.ref('epoch_floor')).end()
@@ -660,7 +665,13 @@ export function createCredentialStore(db, adapter) {
660
665
  const ownerDid = normalizeDID(params.ownerDID);
661
666
  const grantorDid = normalizeDID(params.grantorDID);
662
667
  // Delete ONLY the attempt row, never the floor.
663
- await db.deleteFrom('kubun_credential_provisioning_attempts').where('owner_did', '=', ownerDid).where('grantor_did', '=', grantorDid).where('attempt_id', '=', params.attemptId).execute();
668
+ await db.deleteFrom('kubun_credential_provisioning_attempts').where('owner_did', '=', ownerDid).where('grantor_did', '=', grantorDid).where('attempt_id', '=', params.attemptID).execute();
669
+ },
670
+ async abandonProvisioningAttempts (params) {
671
+ const ownerDid = normalizeDID(params.ownerDID);
672
+ const grantorDid = normalizeDID(params.grantorDID);
673
+ // Delete every attempt row for the scope, never the floor.
674
+ await db.deleteFrom('kubun_credential_provisioning_attempts').where('owner_did', '=', ownerDid).where('grantor_did', '=', grantorDid).execute();
664
675
  },
665
676
  async listProvisioningExpectations (params) {
666
677
  const ownerDid = normalizeDID(params.ownerDID);
@@ -690,6 +701,42 @@ export function createCredentialStore(db, adapter) {
690
701
  return [
691
702
  ...owners
692
703
  ];
704
+ },
705
+ async refreshKeyOwnerDelegation (params) {
706
+ const values = {
707
+ key_id: params.keyID,
708
+ delegation_tokens: adapter.encodeJSON(params.delegationTokens),
709
+ refresh_seq: params.refreshSeq,
710
+ revision: 1
711
+ };
712
+ const result = await db.insertInto('kubun_credential_key_owner_delegation').values(values).onConflict((oc)=>oc.column('key_id').doUpdateSet({
713
+ delegation_tokens: values.delegation_tokens,
714
+ refresh_seq: values.refresh_seq,
715
+ // Bump so a concurrent replace CAS on the pre-refresh revision fails.
716
+ revision: (eb)=>eb('kubun_credential_key_owner_delegation.revision', '+', 1)
717
+ }).where('kubun_credential_key_owner_delegation.refresh_seq', '<', values.refresh_seq)).executeTakeFirst();
718
+ return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
719
+ },
720
+ async replaceKeyOwnerDelegationIfUnchanged (params) {
721
+ const result = await db.updateTable('kubun_credential_key_owner_delegation').set({
722
+ delegation_tokens: adapter.encodeJSON(params.delegationTokens),
723
+ refresh_seq: params.refreshSeq,
724
+ revision: (eb)=>eb('kubun_credential_key_owner_delegation.revision', '+', 1)
725
+ }).where('key_id', '=', params.keyID).where('revision', '=', params.expectedRevision).executeTakeFirst();
726
+ return (result.numUpdatedRows ?? 0n) > 0n;
727
+ },
728
+ async getKeyOwnerDelegation (keyID) {
729
+ const row = await db.selectFrom('kubun_credential_key_owner_delegation').select([
730
+ 'delegation_tokens',
731
+ 'refresh_seq',
732
+ 'revision'
733
+ ]).where('key_id', '=', keyID).executeTakeFirst();
734
+ if (row == null) return undefined;
735
+ return {
736
+ delegationTokens: row.delegation_tokens,
737
+ refreshSeq: row.refresh_seq,
738
+ revision: row.revision
739
+ };
693
740
  }
694
741
  };
695
742
  return api;
package/lib/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { StoreProvider } from '@kubun/db';
2
2
  import type { CredentialStoreAPI } from './api.js';
3
3
  export type { CredentialManifestRecord, CredentialOpInput, CredentialStoreAPI, CredentialTombstoneInput, HeldCredentialSubject, ListManifestsParams, NextGrantorSequenceParams, PutCredentialEntryParams, PutCredentialKeyParams, PutCredentialWrappingParams, PutWrappingAuthorityParams, StoredCredentialManifest, WrappingAuthority, } from './api.js';
4
- export { createCredentialStore } from './api.js';
4
+ export { createCredentialStore, UNBOUNDED_REFRESH_SEQ } from './api.js';
5
5
  export { credentialStoreDefinition } from './definition.js';
6
6
  export declare const CREDENTIAL_STORE: "credential";
7
7
  export declare function getCredentialStore(provider: StoreProvider): Promise<CredentialStoreAPI>;
8
- export type { CredentialAlias, CredentialEntry, CredentialEntryTable, CredentialEntryWithProvenance, CredentialFactorDescriptor, CredentialGrantorCounter, CredentialGrantorCounterTable, CredentialKey, CredentialKeyBranch, CredentialKeyBranchTable, CredentialKeyState, CredentialKeyTable, CredentialManifest, CredentialManifestKey, CredentialManifestTable, CredentialOp, CredentialOpSubjectKind, CredentialOpTable, CredentialTables, CredentialWrapping, CredentialWrappingAuthority, CredentialWrappingAuthorityTable, CredentialWrappingTable, CredentialWrappingWithBranch, InsertCredentialAlias, InsertCredentialEntry, InsertCredentialGrantorCounter, InsertCredentialKey, InsertCredentialManifest, InsertCredentialOp, InsertCredentialWrapping, InsertCredentialWrappingAuthority, KubunCredentialAliasTable, } from './tables.js';
8
+ export type { CredentialAlias, CredentialEntry, CredentialEntryTable, CredentialEntryWithProvenance, CredentialFactorDescriptor, CredentialGrantorCounter, CredentialGrantorCounterTable, CredentialKey, CredentialKeyBranch, CredentialKeyBranchTable, CredentialKeyOwnerDelegation, CredentialKeyOwnerDelegationTable, CredentialKeyState, CredentialKeyTable, CredentialManifest, CredentialManifestKey, CredentialManifestTable, CredentialOp, CredentialOpSubjectKind, CredentialOpTable, CredentialTables, CredentialWrapping, CredentialWrappingAuthority, CredentialWrappingAuthorityTable, CredentialWrappingTable, CredentialWrappingWithBranch, InsertCredentialAlias, InsertCredentialEntry, InsertCredentialGrantorCounter, InsertCredentialKey, InsertCredentialKeyOwnerDelegation, InsertCredentialManifest, InsertCredentialOp, InsertCredentialWrapping, InsertCredentialWrappingAuthority, KubunCredentialAliasTable, } from './tables.js';
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { createCredentialStore } from './api.js';
1
+ export { createCredentialStore, UNBOUNDED_REFRESH_SEQ } from './api.js';
2
2
  export { credentialStoreDefinition } from './definition.js';
3
3
  export const CREDENTIAL_STORE = 'credential';
4
4
  export function getCredentialStore(provider) {
package/lib/migrations.js CHANGED
@@ -80,8 +80,14 @@ export function getCredentialMigrations(ctx) {
80
80
  'recipient_did',
81
81
  'grantor_did'
82
82
  ]).execute();
83
+ // A controller-owned key's administer delegation chain, keyed on the key
84
+ // itself; `refresh_seq` gates every upsert strictly-greater.
85
+ await db.schema.createTable('kubun_credential_key_owner_delegation').ifNotExists().addColumn('key_id', t.text, (col)=>col.notNull().primaryKey()).addColumn('delegation_tokens', t.json, (col)=>col.notNull()).addColumn('refresh_seq', t.bigint, (col)=>col.notNull())// Per-write revision for the replace CAS; `refresh_seq` is not a unique
86
+ // version (see CredentialKeyOwnerDelegationTable).
87
+ .addColumn('revision', t.bigint, (col)=>col.notNull()).execute();
83
88
  },
84
89
  async down (db) {
90
+ await db.schema.dropTable('kubun_credential_key_owner_delegation').ifExists().execute();
85
91
  await db.schema.dropTable('kubun_credential_manifests').ifExists().execute();
86
92
  await db.schema.dropTable('kubun_credential_provisioning_attempts').ifExists().execute();
87
93
  await db.schema.dropTable('kubun_credential_provisioning_expectations').ifExists().execute();
package/lib/tables.d.ts CHANGED
@@ -259,6 +259,21 @@ export type CredentialManifestTable = {
259
259
  };
260
260
  export type CredentialManifest = Selectable<CredentialManifestTable>;
261
261
  export type InsertCredentialManifest = Insertable<CredentialManifestTable>;
262
+ /**
263
+ * A controller-owned key's `credential/administer` delegation chain, keyed on
264
+ * the key itself rather than a triple — the chain authorizes whoever holds it
265
+ * to administer THIS key, independent of any one (owner, recipient, grantor)
266
+ * relationship. `refresh_seq` gates every `refresh` strictly-greater, same
267
+ * shape as {@link CredentialWrappingAuthorityTable}.
268
+ */
269
+ export type CredentialKeyOwnerDelegationTable = {
270
+ key_id: string;
271
+ delegation_tokens: JSONArrayColumn<string>;
272
+ refresh_seq: number;
273
+ revision: number;
274
+ };
275
+ export type CredentialKeyOwnerDelegation = Selectable<CredentialKeyOwnerDelegationTable>;
276
+ export type InsertCredentialKeyOwnerDelegation = Insertable<CredentialKeyOwnerDelegationTable>;
262
277
  export type CredentialTables = {
263
278
  kubun_credential_keys: CredentialKeyTable;
264
279
  kubun_credential_key_branches: CredentialKeyBranchTable;
@@ -272,5 +287,6 @@ export type CredentialTables = {
272
287
  kubun_credential_provisioning_expectations: CredentialProvisioningExpectationTable;
273
288
  kubun_credential_provisioning_attempts: CredentialProvisioningAttemptTable;
274
289
  kubun_credential_manifests: CredentialManifestTable;
290
+ kubun_credential_key_owner_delegation: CredentialKeyOwnerDelegationTable;
275
291
  };
276
292
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/store-credential",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
4
4
  "keywords": [],
5
5
  "license": "see LICENSE.md",
6
6
  "sideEffects": false,
@@ -18,11 +18,11 @@
18
18
  "@kokuin/token": "^0.5.0",
19
19
  "@kubun/db": "^0.17.0",
20
20
  "@kubun/db-adapter": "^0.17.0",
21
- "kysely": "^0.29.5"
21
+ "kysely": "^0.29.6"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@kubun/db-better-sqlite": "^0.17.0",
25
- "@kubun/db-postgres": "^0.17.0",
25
+ "@kubun/db-postgres": "^0.17.1",
26
26
  "@testcontainers/postgresql": "^12.1.0"
27
27
  },
28
28
  "publishConfig": {