@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 +62 -3
- package/lib/api.js +51 -4
- package/lib/index.d.ts +2 -2
- package/lib/index.js +1 -1
- package/lib/migrations.js +6 -0
- package/lib/tables.d.ts +16 -0
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
25
|
+
"@kubun/db-postgres": "^0.17.1",
|
|
26
26
|
"@testcontainers/postgresql": "^12.1.0"
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|