@kubun/store-credential 0.17.3 → 0.17.4
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 +20 -1
- package/lib/api.js +12 -3
- package/package.json +1 -1
package/lib/api.d.ts
CHANGED
|
@@ -235,6 +235,11 @@ export type CredentialStoreAPI = {
|
|
|
235
235
|
* whatever it means by "key" before calling.
|
|
236
236
|
*/
|
|
237
237
|
getAliasEntryID(namespace: string, key: string): Promise<string | null>;
|
|
238
|
+
/**
|
|
239
|
+
* The provider namespace this entry is aliased under, or null when the entry
|
|
240
|
+
* has no alias row. UNIQUE(entry_id) makes this a 1:1 reverse lookup.
|
|
241
|
+
*/
|
|
242
|
+
aliasNamespaceForEntry(entryID: string): Promise<string | null>;
|
|
238
243
|
/** Point `(namespace, key)` at `entryID`, replacing any prior target. */
|
|
239
244
|
setAliasEntryID(namespace: string, key: string, entryID: string): Promise<void>;
|
|
240
245
|
/**
|
|
@@ -243,8 +248,22 @@ export type CredentialStoreAPI = {
|
|
|
243
248
|
* existing alias and never binds a nonexistent entry. Classifies the outcome
|
|
244
249
|
* instead of throwing, since both unique constraints are expected outcomes a
|
|
245
250
|
* caller branches on, not exceptional ones.
|
|
251
|
+
*
|
|
252
|
+
* When `expectedKeyID` is given, the bind is additionally conditioned, in the
|
|
253
|
+
* same statement, on the entry belonging to that key. This closes a
|
|
254
|
+
* time-of-check/time-of-use gap for callers that cannot wrap the check and the
|
|
255
|
+
* bind in one transaction: an entry verified to belong to a key can be moved to
|
|
256
|
+
* another key (a supported manager operation) before an unconditioned insert
|
|
257
|
+
* fires, which would point this alias at an entry now owned by a different key.
|
|
258
|
+
* Folding the key check into the insert refuses atomically. When omitted, only
|
|
259
|
+
* entry existence is required (owner-gated callers verify authority elsewhere).
|
|
246
260
|
*/
|
|
247
|
-
bindAliasIfAbsent(
|
|
261
|
+
bindAliasIfAbsent(params: {
|
|
262
|
+
namespace: string;
|
|
263
|
+
aliasKey: string;
|
|
264
|
+
entryID: string;
|
|
265
|
+
expectedKeyID?: string;
|
|
266
|
+
}): Promise<'inserted' | 'entry-missing' | {
|
|
248
267
|
conflict: 'alias';
|
|
249
268
|
existing: string;
|
|
250
269
|
} | {
|
package/lib/api.js
CHANGED
|
@@ -566,6 +566,10 @@ export function createCredentialStore(db, adapter) {
|
|
|
566
566
|
const row = await db.selectFrom('kubun_credential_aliases').select('entry_id').where('namespace', '=', namespace).where('alias_key', '=', key).executeTakeFirst();
|
|
567
567
|
return row?.entry_id ?? null;
|
|
568
568
|
},
|
|
569
|
+
async aliasNamespaceForEntry (entryID) {
|
|
570
|
+
const row = await db.selectFrom('kubun_credential_aliases').select('namespace').where('entry_id', '=', entryID).executeTakeFirst();
|
|
571
|
+
return row?.namespace ?? null;
|
|
572
|
+
},
|
|
569
573
|
async setAliasEntryID (namespace, key, entryID) {
|
|
570
574
|
await gatedIfAmbient(()=>db.insertInto('kubun_credential_aliases').values({
|
|
571
575
|
namespace,
|
|
@@ -578,7 +582,7 @@ export function createCredentialStore(db, adapter) {
|
|
|
578
582
|
entry_id: entryID
|
|
579
583
|
})).execute());
|
|
580
584
|
},
|
|
581
|
-
async bindAliasIfAbsent (namespace, aliasKey, entryID) {
|
|
585
|
+
async bindAliasIfAbsent ({ namespace, aliasKey, entryID, expectedKeyID }) {
|
|
582
586
|
// Entry-existence-conditioned insert: the alias row is written only in the same
|
|
583
587
|
// statement that confirms the entry exists. ON CONFLICT DO NOTHING fires for either
|
|
584
588
|
// the (namespace, alias_key) PK or the UNIQUE(entry_id); the read-back names which.
|
|
@@ -595,7 +599,10 @@ export function createCredentialStore(db, adapter) {
|
|
|
595
599
|
'namespace',
|
|
596
600
|
'alias_key',
|
|
597
601
|
'entry_id'
|
|
598
|
-
]).expression((eb)=>eb.selectFrom('kubun_credential_entries').where('entry_id', '=', entryID)
|
|
602
|
+
]).expression((eb)=>eb.selectFrom('kubun_credential_entries').where('entry_id', '=', entryID)// Same-statement key gate (see doc): bind only while the entry
|
|
603
|
+
// still belongs to expectedKeyID, so a concurrent cross-key move
|
|
604
|
+
// cannot slip an alias onto an entry that has left this key.
|
|
605
|
+
.$if(expectedKeyID != null, (qb)=>qb.where('key_id', '=', expectedKeyID)).select((eb2)=>[
|
|
599
606
|
eb2.val(namespace).as('namespace'),
|
|
600
607
|
eb2.val(aliasKey).as('alias_key'),
|
|
601
608
|
eb2.ref('entry_id').as('entry_id')
|
|
@@ -627,7 +634,9 @@ export function createCredentialStore(db, adapter) {
|
|
|
627
634
|
.where('entry_id', '=', observedExisting)// The observed pointer must still be dangling (its entry absent)...
|
|
628
635
|
.where((eb)=>eb.not(eb.exists(eb.selectFrom('kubun_credential_entries as e').select(sql`1`.as('one')).whereRef('e.entry_id', '=', 'kubun_credential_aliases.entry_id'))))// ...and the target must be present, so repair can never form a NEW dangling
|
|
629
636
|
// pointer even if the target is deleted between the pre-check and here.
|
|
630
|
-
|
|
637
|
+
// When key-gated, the target must also still belong to expectedKeyID, so a
|
|
638
|
+
// repair cannot repoint onto an entry that has moved to another key.
|
|
639
|
+
.where((eb)=>eb.exists(eb.selectFrom('kubun_credential_entries as t').select(sql`1`.as('one')).where('t.entry_id', '=', entryID).$if(expectedKeyID != null, (qb)=>qb.where('t.key_id', '=', expectedKeyID)))).executeTakeFirst();
|
|
631
640
|
return (res.numUpdatedRows ?? 0n) > 0n;
|
|
632
641
|
};
|
|
633
642
|
if (!db.isTransaction) {
|