@kubun/store-credential 0.17.1 → 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
@@ -361,5 +361,48 @@ export type CredentialStoreAPI = {
361
361
  * `listProvisioningExpectations` can't give on its own (it's per-owner).
362
362
  */
363
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>;
364
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;
365
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
  *
@@ -696,6 +701,42 @@ export function createCredentialStore(db, adapter) {
696
701
  return [
697
702
  ...owners
698
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
+ };
699
740
  }
700
741
  };
701
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.1",
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": {