@cortexkit/common-auth 0.5.0 → 0.7.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/dist/fs/index.d.ts +2 -1
- package/dist/fs/refresh-file-lock.d.ts +15 -4
- package/dist/fs/refresh-file-lock.js +67 -7
- package/dist/fs/with-lock.d.ts +9 -8
- package/dist/store/errors.d.ts +2 -2
- package/dist/store/identity.d.ts +5 -0
- package/dist/store/identity.js +16 -0
- package/dist/store/index.d.ts +5 -3
- package/dist/store/index.js +2 -1
- package/dist/store/mutate.d.ts +3 -1
- package/dist/store/mutate.js +4 -0
- package/dist/store/pool.d.ts +31 -16
- package/dist/store/pool.js +3 -0
- package/dist/store/provider-state.d.ts +131 -0
- package/dist/store/provider-state.js +223 -0
- package/dist/store/refresh.d.ts +8 -0
- package/dist/store/refresh.js +9 -0
- package/dist/store/rows.d.ts +65 -15
- package/dist/store/rows.js +195 -59
- package/dist/store/schema.d.ts +114 -4
- package/dist/store/schema.js +85 -8
- package/dist/store/torn.d.ts +31 -1
- package/dist/store/torn.js +76 -2
- package/package.json +1 -1
package/dist/store/rows.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { PoolOperationError } from './errors.js';
|
|
2
3
|
import { assertNotInsideHook, runInsideHook } from './hooks.js';
|
|
3
|
-
import { DUPLICATE_IDENTITY_REASON, disableIdentityDuplicates, disableIn, recordIdentityIn, } from './identity.js';
|
|
4
|
+
import { DUPLICATE_IDENTITY_REASON, disableIdentityDuplicates, disableIn, enableIn, recordIdentityIn, } from './identity.js';
|
|
4
5
|
import { notReadyError, readPool, runOperation, withTransaction, } from './mutate.js';
|
|
6
|
+
import { acceptProviderState, mergedProviderState, planProviderStateIn, providerStateCoverage, replacementProviderState, } from './provider-state.js';
|
|
5
7
|
import { readRow, refusal, requireBound, rowLockSpec, unknownRow, } from './runtime.js';
|
|
6
|
-
import { CREDENTIAL_STAMP_KEY, credentialProblem, fingerprintOf, idProblem, isCredentialEpoch, isRecord, rosterRowFor, rotationStamp, rowLockKey, stampFor, stateFieldsFor, storedCredential, } from './schema.js';
|
|
7
|
-
import { bindReplacement } from './torn.js';
|
|
8
|
+
import { boundProviderStateDigest, CREDENTIAL_STAMP_KEY, credentialProblem, fingerprintOf, idProblem, isCredentialEpoch, isRecord, PROVIDER_STATE_KEY, rosterRowFor, rotationStamp, rowLockKey, stampFor, stateFieldsFor, storedCredential, } from './schema.js';
|
|
9
|
+
import { applyTransition, bindReplacement, TRANSITION_STAMP_KEY, } from './torn.js';
|
|
8
10
|
/** Fields of a state entry that belong to the credential it replaces. */
|
|
9
11
|
const CREDENTIAL_STATE_FIELDS = [
|
|
10
12
|
'access',
|
|
@@ -90,6 +92,14 @@ function bindingInTx(tx, id, stored, learnt) {
|
|
|
90
92
|
export async function rotateIn(rt, tx, id, given, extra = {}) {
|
|
91
93
|
const credential = onRowEndpoint(tx, id, given);
|
|
92
94
|
const prior = tx.stateAccount(id);
|
|
95
|
+
const stateWrite = extra.providerState ?? { kind: 'keep' };
|
|
96
|
+
const epoch = tx.entry(id)?.credentialEpoch;
|
|
97
|
+
const credentialEpoch = typeof epoch === 'number' ? epoch : 1;
|
|
98
|
+
// The provider state goes in the same state write as the credential and
|
|
99
|
+
// its stamp, so no reader ever sees one without the other. A kept value is
|
|
100
|
+
// bound by the new stamp only if the old one bound it to this row at the
|
|
101
|
+
// epoch being written; a replace moves the epoch, so it never keeps one.
|
|
102
|
+
const providerStateBinding = providerStateCoverage(rt.ctx.providerState, stateWrite, prior, prior && Object.hasOwn(prior, PROVIDER_STATE_KEY) ? tx.row(id) : undefined, credentialEpoch);
|
|
93
103
|
const priorStamp = typeof prior?.lastRefreshedAt === 'number'
|
|
94
104
|
? prior.lastRefreshedAt
|
|
95
105
|
: undefined;
|
|
@@ -104,12 +114,20 @@ export async function rotateIn(rt, tx, id, given, extra = {}) {
|
|
|
104
114
|
delete kept.lastQuotaRefreshError;
|
|
105
115
|
delete kept.quota;
|
|
106
116
|
}
|
|
117
|
+
if (stateWrite.kind === 'clear')
|
|
118
|
+
delete kept[PROVIDER_STATE_KEY];
|
|
119
|
+
else if (stateWrite.kind === 'set')
|
|
120
|
+
kept[PROVIDER_STATE_KEY] = stateWrite.value;
|
|
107
121
|
const stored = storedCredential(credential, stamp);
|
|
108
|
-
const epoch = tx.entry(id)?.credentialEpoch;
|
|
109
122
|
tx.setStateAccount(id, {
|
|
110
123
|
...kept,
|
|
111
124
|
...stateFieldsFor(credential, stamp),
|
|
112
|
-
[CREDENTIAL_STAMP_KEY]: stampFor(stored,
|
|
125
|
+
[CREDENTIAL_STAMP_KEY]: stampFor(stored, credentialEpoch, extra.binding ?? bindingInTx(tx, id, stored, extra.identity), {
|
|
126
|
+
replace: extra.binding !== undefined,
|
|
127
|
+
...(providerStateBinding !== undefined
|
|
128
|
+
? { providerState: providerStateBinding }
|
|
129
|
+
: {}),
|
|
130
|
+
}),
|
|
113
131
|
});
|
|
114
132
|
await tx.commitState(stored);
|
|
115
133
|
return stored;
|
|
@@ -126,9 +144,14 @@ export async function rotateIn(rt, tx, id, given, extra = {}) {
|
|
|
126
144
|
async function stampIdentityIn(tx, row, identity) {
|
|
127
145
|
if (!row.credential || row.stamp !== 'bound')
|
|
128
146
|
return;
|
|
147
|
+
const account = tx.stateAccount(row.id);
|
|
148
|
+
// The provider state stays bound across the identity being learnt: the old
|
|
149
|
+
// stamp bound it to the row with no identity, the new one to the identity
|
|
150
|
+
// the config is about to record.
|
|
151
|
+
const providerState = boundProviderStateDigest(account, row.credential, row.credentialEpoch ?? 1, row.identity);
|
|
129
152
|
tx.setStateAccount(row.id, {
|
|
130
|
-
...(
|
|
131
|
-
[CREDENTIAL_STAMP_KEY]: stampFor(row.credential, row.credentialEpoch ?? 1, bindingInTx(tx, row.id, row.credential, identity)),
|
|
153
|
+
...(account ?? {}),
|
|
154
|
+
[CREDENTIAL_STAMP_KEY]: stampFor(row.credential, row.credentialEpoch ?? 1, bindingInTx(tx, row.id, row.credential, identity), providerState !== undefined ? { providerState } : {}),
|
|
132
155
|
});
|
|
133
156
|
await tx.commitState();
|
|
134
157
|
}
|
|
@@ -165,6 +188,9 @@ export async function addRow(rt, input, options = {}) {
|
|
|
165
188
|
const { id, credential, identity, label } = input;
|
|
166
189
|
const result = await runOperation(ctx, 'add', id, options.onFailure, async (locks, progress) => {
|
|
167
190
|
checkInput('add', id, credential);
|
|
191
|
+
const incoming = input.providerState === undefined
|
|
192
|
+
? undefined
|
|
193
|
+
: acceptProviderState(ctx.providerState, 'add', id, input.providerState);
|
|
168
194
|
if (ctx.removedIds.has(id))
|
|
169
195
|
throw refusal('add', id, 'id-removed', `id ${id} was removed from the roster in this process and is not reused`);
|
|
170
196
|
await locks.acquire(rowLockSpec(rt, { id, identity }));
|
|
@@ -190,7 +216,13 @@ export async function addRow(rt, input, options = {}) {
|
|
|
190
216
|
same.identity !== undefined &&
|
|
191
217
|
identity !== same.identity)
|
|
192
218
|
throw identityMismatch('add', same.id);
|
|
193
|
-
const stored = await rotateIn(rt, tx, same.id, credential
|
|
219
|
+
const stored = await rotateIn(rt, tx, same.id, credential, {
|
|
220
|
+
...(incoming !== undefined
|
|
221
|
+
? {
|
|
222
|
+
providerState: mergedProviderState(ctx.providerState, 'add', same.id, same.providerState, incoming),
|
|
223
|
+
}
|
|
224
|
+
: {}),
|
|
225
|
+
});
|
|
194
226
|
return { id: same.id, outcome: 'rotated', credential: stored };
|
|
195
227
|
}
|
|
196
228
|
const existing = rows.find((row) => row.id === id);
|
|
@@ -219,6 +251,9 @@ export async function addRow(rt, input, options = {}) {
|
|
|
219
251
|
});
|
|
220
252
|
const stored = await rotateIn(rt, tx, id, credential, {
|
|
221
253
|
clearErrors: true,
|
|
254
|
+
...(incoming !== undefined
|
|
255
|
+
? { providerState: { kind: 'set', value: incoming } }
|
|
256
|
+
: {}),
|
|
222
257
|
});
|
|
223
258
|
if (!existing.hasEntry)
|
|
224
259
|
await tx.commitConfig();
|
|
@@ -253,7 +288,11 @@ export async function addRow(rt, input, options = {}) {
|
|
|
253
288
|
// roster row could load beside a credential left under its id by an
|
|
254
289
|
// interrupted removal. Nothing of such a leftover entry is kept.
|
|
255
290
|
tx.dropStateAccount(id);
|
|
256
|
-
const stored = await rotateIn(rt, tx, id, credential
|
|
291
|
+
const stored = await rotateIn(rt, tx, id, credential, {
|
|
292
|
+
...(incoming !== undefined
|
|
293
|
+
? { providerState: { kind: 'set', value: incoming } }
|
|
294
|
+
: {}),
|
|
295
|
+
});
|
|
257
296
|
await tx.commitConfig();
|
|
258
297
|
return { id, outcome, credential: stored };
|
|
259
298
|
});
|
|
@@ -268,6 +307,9 @@ export async function replaceRow(rt, id, credential, input = {}, options = {}) {
|
|
|
268
307
|
const { ctx } = rt;
|
|
269
308
|
const result = await runOperation(ctx, 'replace', id, options.onFailure, async (locks, progress) => {
|
|
270
309
|
checkInput('replace', id, credential);
|
|
310
|
+
const incoming = input.providerState === undefined
|
|
311
|
+
? undefined
|
|
312
|
+
: acceptProviderState(ctx.providerState, 'replace', id, input.providerState);
|
|
271
313
|
const { row: seen } = await readRow(rt, 'replace', id);
|
|
272
314
|
await locks.acquire(rowLockSpec(rt, seen));
|
|
273
315
|
if (credential.type === 'oauth')
|
|
@@ -285,6 +327,9 @@ export async function replaceRow(rt, id, credential, input = {}, options = {}) {
|
|
|
285
327
|
// Refused before anything is written; the row keeps its credential.
|
|
286
328
|
if (!isCredentialEpoch(credentialEpoch))
|
|
287
329
|
throw refusal('replace', id, 'invalid-row', `row ${id}'s credential epoch cannot advance past ${Number.MAX_SAFE_INTEGER}; remove the row and add the new credential as a new row`);
|
|
330
|
+
// Decided before anything is written: a hook that throws or
|
|
331
|
+
// returns a value the codec rejects leaves the row as it was.
|
|
332
|
+
const providerState = replacementProviderState(ctx.providerState, row, credentialEpoch, input.identity, incoming);
|
|
288
333
|
const binding = {
|
|
289
334
|
...(input.identity !== undefined
|
|
290
335
|
? { identity: input.identity }
|
|
@@ -308,6 +353,7 @@ export async function replaceRow(rt, id, credential, input = {}, options = {}) {
|
|
|
308
353
|
const stored = await rotateIn(rt, tx, id, credential, {
|
|
309
354
|
clearErrors: true,
|
|
310
355
|
binding,
|
|
356
|
+
providerState,
|
|
311
357
|
});
|
|
312
358
|
await tx.commitConfig();
|
|
313
359
|
return { id, credential: stored, credentialEpoch };
|
|
@@ -322,6 +368,9 @@ export async function rotateRow(rt, id, credential, input = {}, options = {}) {
|
|
|
322
368
|
const { ctx } = rt;
|
|
323
369
|
return runOperation(ctx, 'rotate', id, options.onFailure, async (locks, progress) => {
|
|
324
370
|
checkInput('rotate', id, credential);
|
|
371
|
+
const incoming = input.providerState === undefined
|
|
372
|
+
? undefined
|
|
373
|
+
: acceptProviderState(ctx.providerState, 'rotate', id, input.providerState);
|
|
325
374
|
const { row: seen } = await readRow(rt, 'rotate', id);
|
|
326
375
|
await locks.acquire(rowLockSpec(rt, seen));
|
|
327
376
|
if (input.identity !== undefined && credential.type === 'oauth')
|
|
@@ -347,6 +396,11 @@ export async function rotateRow(rt, id, credential, input = {}, options = {}) {
|
|
|
347
396
|
: undefined;
|
|
348
397
|
const stored = await rotateIn(rt, tx, id, credential, {
|
|
349
398
|
identity: learnt,
|
|
399
|
+
...(incoming !== undefined
|
|
400
|
+
? {
|
|
401
|
+
providerState: mergedProviderState(ctx.providerState, 'rotate', id, row.providerState, incoming),
|
|
402
|
+
}
|
|
403
|
+
: {}),
|
|
350
404
|
});
|
|
351
405
|
let configChanged = false;
|
|
352
406
|
if (!row.hasEntry) {
|
|
@@ -367,72 +421,154 @@ export async function rotateRow(rt, id, credential, input = {}, options = {}) {
|
|
|
367
421
|
});
|
|
368
422
|
}
|
|
369
423
|
/**
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
424
|
+
* `disable` and `enable` in one place. Takes the row lock, then the extra
|
|
425
|
+
* locks, then the store locks, as every row write does, so it waits for a
|
|
426
|
+
* refresh of the row instead of landing during its provider call.
|
|
427
|
+
*
|
|
428
|
+
* With a provider-state mutator that changes the value, the transition is
|
|
429
|
+
* written as a replace is: the state file first, carrying the value and, in
|
|
430
|
+
* the stamp, the transition itself; then the config, flipping the row and
|
|
431
|
+
* recording the transition's mark (see `torn.ts`). A stop between the two
|
|
432
|
+
* leaves a row every reader shows transitioned beside its new value. When
|
|
433
|
+
* the config already says what the transition would write (an `enable` of
|
|
434
|
+
* an enabled row), the value alone is written, in one state write.
|
|
373
435
|
*/
|
|
374
|
-
|
|
375
|
-
assertNotInsideHook(
|
|
376
|
-
|
|
377
|
-
|
|
436
|
+
async function transitionRow(rt, operation, id, flag, options) {
|
|
437
|
+
assertNotInsideHook(operation);
|
|
438
|
+
const { ctx } = rt;
|
|
439
|
+
const codec = ctx.providerState;
|
|
440
|
+
const fence = options.attribution;
|
|
441
|
+
const mutator = options.providerState;
|
|
442
|
+
return runOperation(ctx, operation, id, options.onFailure, async (locks, progress) => {
|
|
443
|
+
if (fence !== undefined &&
|
|
444
|
+
!isCredentialEpoch(isRecord(fence) ? fence.credentialEpoch : undefined))
|
|
445
|
+
throw refusal(operation, id, 'invalid-input', 'the attribution must name a credential epoch that is a positive safe integer');
|
|
446
|
+
if (mutator !== undefined) {
|
|
447
|
+
if (typeof mutator !== 'function')
|
|
448
|
+
throw refusal(operation, id, 'invalid-input', 'the provider-state mutator must be a function');
|
|
449
|
+
// A provider-state value belongs to one credential, so a change to
|
|
450
|
+
// it must say which credential it was decided for.
|
|
451
|
+
if (fence === undefined)
|
|
452
|
+
throw refusal(operation, id, 'invalid-input', 'a provider-state change needs the attribution of the credential it is for');
|
|
453
|
+
if (!codec)
|
|
454
|
+
throw refusal(operation, id, 'invalid-input', 'the store was opened without a provider-state codec');
|
|
455
|
+
}
|
|
456
|
+
const { row: seen } = await readRow(rt, operation, id);
|
|
378
457
|
await locks.acquire(rowLockSpec(rt, seen));
|
|
379
458
|
for (const extra of options.extraLocks ?? [])
|
|
380
459
|
await locks.acquire(extra);
|
|
381
|
-
return withTransaction(
|
|
382
|
-
const
|
|
460
|
+
return withTransaction(ctx, locks, progress, { operation, rowId: id }, async (tx) => {
|
|
461
|
+
const loaded = tx.row(id);
|
|
462
|
+
const row = flag.enabled
|
|
463
|
+
? requireUsableRow('enable', id, loaded)
|
|
464
|
+
: loaded;
|
|
383
465
|
if (!row || !tx.rosterRow(id))
|
|
384
|
-
throw unknownRow(
|
|
466
|
+
throw unknownRow(operation, id);
|
|
385
467
|
if (rowLockKey(row) !== rowLockKey(seen))
|
|
386
|
-
throw keyChanged(
|
|
387
|
-
|
|
468
|
+
throw keyChanged(operation, id);
|
|
469
|
+
if (fence !== undefined) {
|
|
470
|
+
// An invalid entry has no epoch to compare the fence with.
|
|
471
|
+
if (row.invalid)
|
|
472
|
+
throw refusal(operation, id, 'invalid-row', `row ${id} failed validation`);
|
|
473
|
+
if ((row.credentialEpoch ?? 1) !== fence.credentialEpoch ||
|
|
474
|
+
row.identity !== fence.identity)
|
|
475
|
+
throw refusal(operation, id, 'attribution', `the ${operation} of ${id} was issued for a credential or account the row no longer holds`, true);
|
|
476
|
+
}
|
|
477
|
+
// An enable of a row that is already enabled has nothing to write
|
|
478
|
+
// to the config; a disable always rewrites it, as it always has.
|
|
479
|
+
const writesConfig = !flag.enabled || !row.enabled || row.disabledReason !== undefined;
|
|
480
|
+
if (flag.enabled && writesConfig && row.type === 'oauth') {
|
|
481
|
+
const holder = row.identity === undefined
|
|
482
|
+
? undefined
|
|
483
|
+
: tx
|
|
484
|
+
.rows()
|
|
485
|
+
.find((other) => other.id !== id &&
|
|
486
|
+
other.invalid === undefined &&
|
|
487
|
+
other.type === 'oauth' &&
|
|
488
|
+
other.enabled &&
|
|
489
|
+
other.identity === row.identity);
|
|
490
|
+
if (holder)
|
|
491
|
+
throw refusal('enable', id, 'duplicate-identity', `row ${holder.id} is enabled with the same identity as row ${id}`);
|
|
492
|
+
}
|
|
493
|
+
if (mutator === undefined || codec === undefined) {
|
|
494
|
+
if (!writesConfig)
|
|
495
|
+
return { id };
|
|
496
|
+
if (flag.enabled)
|
|
497
|
+
enableIn(tx, id);
|
|
498
|
+
else
|
|
499
|
+
disableIn(tx, id, flag.reason);
|
|
500
|
+
await tx.commitConfig();
|
|
501
|
+
return { id };
|
|
502
|
+
}
|
|
503
|
+
if (!row.credential)
|
|
504
|
+
throw refusal(operation, id, 'no-credential', `row ${id} holds no credential`);
|
|
505
|
+
// A strict store refuses here, but not for an attribution alone:
|
|
506
|
+
// disabling or enabling a row whose credential the store cannot
|
|
507
|
+
// prove is harmless (an unbound row is never a candidate), while
|
|
508
|
+
// writing a provider state would vouch for that credential.
|
|
509
|
+
requireBound(operation, row);
|
|
510
|
+
const plan = await planProviderStateIn(tx, codec, operation, row, mutator, true);
|
|
511
|
+
if (plan.kind === 'declined')
|
|
512
|
+
return { id, declined: true };
|
|
513
|
+
const result = {
|
|
514
|
+
id,
|
|
515
|
+
providerStateOutcome: plan.kind === 'unchanged'
|
|
516
|
+
? 'unchanged'
|
|
517
|
+
: plan.value === undefined
|
|
518
|
+
? 'cleared'
|
|
519
|
+
: 'updated',
|
|
520
|
+
...(plan.value !== undefined ? { providerState: plan.value } : {}),
|
|
521
|
+
};
|
|
522
|
+
if (plan.kind === 'unchanged') {
|
|
523
|
+
if (writesConfig) {
|
|
524
|
+
if (flag.enabled)
|
|
525
|
+
enableIn(tx, id);
|
|
526
|
+
else
|
|
527
|
+
disableIn(tx, id, flag.reason);
|
|
528
|
+
await tx.commitConfig();
|
|
529
|
+
}
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
532
|
+
if (!writesConfig) {
|
|
533
|
+
tx.setStateAccount(id, plan.account);
|
|
534
|
+
await tx.commitState();
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
const transition = flag.enabled
|
|
538
|
+
? { mark: randomUUID(), enabled: true }
|
|
539
|
+
: { mark: randomUUID(), enabled: false, reason: flag.reason };
|
|
540
|
+
const stamp = plan.account[CREDENTIAL_STAMP_KEY];
|
|
541
|
+
tx.setStateAccount(id, {
|
|
542
|
+
...plan.account,
|
|
543
|
+
[CREDENTIAL_STAMP_KEY]: {
|
|
544
|
+
...stamp,
|
|
545
|
+
[TRANSITION_STAMP_KEY]: transition,
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
await tx.commitState();
|
|
549
|
+
applyTransition(tx, id, transition);
|
|
388
550
|
await tx.commitConfig();
|
|
389
|
-
return
|
|
551
|
+
return result;
|
|
390
552
|
});
|
|
391
553
|
});
|
|
392
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Marks a row disabled with a reason. See `RowTransitionOptions` for the
|
|
557
|
+
* attributed form, which may change the provider state with it.
|
|
558
|
+
*/
|
|
559
|
+
export function disableRow(rt, id, reason, options = {}) {
|
|
560
|
+
return transitionRow(rt, 'disable', id, { enabled: false, reason }, options);
|
|
561
|
+
}
|
|
393
562
|
/**
|
|
394
563
|
* Clears a row's `enabled: false` and its `disabledReason` in one config
|
|
395
564
|
* write. An OAuth row whose recorded identity another enabled OAuth row holds
|
|
396
565
|
* stays disabled and the call refuses (`duplicate-identity`): the same rule
|
|
397
566
|
* that makes `add` store such a row disabled. Enabling a row that is already
|
|
398
|
-
* enabled writes nothing.
|
|
567
|
+
* enabled writes nothing. See `RowTransitionOptions` for the attributed
|
|
568
|
+
* form, which may change the provider state with it.
|
|
399
569
|
*/
|
|
400
|
-
export
|
|
401
|
-
|
|
402
|
-
return runOperation(rt.ctx, 'enable', id, options.onFailure, async (locks, progress) => {
|
|
403
|
-
const { row: seen } = await readRow(rt, 'enable', id);
|
|
404
|
-
await locks.acquire(rowLockSpec(rt, seen));
|
|
405
|
-
for (const extra of options.extraLocks ?? [])
|
|
406
|
-
await locks.acquire(extra);
|
|
407
|
-
return withTransaction(rt.ctx, locks, progress, { operation: 'enable', rowId: id }, async (tx) => {
|
|
408
|
-
const row = requireUsableRow('enable', id, tx.row(id));
|
|
409
|
-
if (rowLockKey(row) !== rowLockKey(seen))
|
|
410
|
-
throw keyChanged('enable', id);
|
|
411
|
-
if (row.enabled && row.disabledReason === undefined)
|
|
412
|
-
return { id };
|
|
413
|
-
if (row.type === 'oauth' && row.identity !== undefined) {
|
|
414
|
-
const holder = tx
|
|
415
|
-
.rows()
|
|
416
|
-
.find((other) => other.id !== id &&
|
|
417
|
-
other.invalid === undefined &&
|
|
418
|
-
other.type === 'oauth' &&
|
|
419
|
-
other.enabled &&
|
|
420
|
-
other.identity === row.identity);
|
|
421
|
-
if (holder)
|
|
422
|
-
throw refusal('enable', id, 'duplicate-identity', `row ${holder.id} is enabled with the same identity as row ${id}`);
|
|
423
|
-
}
|
|
424
|
-
const raw = tx.rosterRow(id);
|
|
425
|
-
raw.enabled = true;
|
|
426
|
-
const entry = tx.entry(id);
|
|
427
|
-
if (entry && 'disabledReason' in entry) {
|
|
428
|
-
const next = { ...entry };
|
|
429
|
-
delete next.disabledReason;
|
|
430
|
-
tx.setEntry(id, next);
|
|
431
|
-
}
|
|
432
|
-
await tx.commitConfig();
|
|
433
|
-
return { id };
|
|
434
|
-
});
|
|
435
|
-
});
|
|
570
|
+
export function enableRow(rt, id, options = {}) {
|
|
571
|
+
return transitionRow(rt, 'enable', id, { enabled: true }, options);
|
|
436
572
|
}
|
|
437
573
|
/**
|
|
438
574
|
* Deletes a row: its roster row and per-row entry (quota, epoch; the identity
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -42,6 +42,69 @@ export interface QuotaCodec {
|
|
|
42
42
|
validate(value: unknown): boolean;
|
|
43
43
|
merge(stored: unknown | undefined, observation: unknown): unknown;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* What `ProviderStateCodec.onReplace` is told about a replacement: the row,
|
|
47
|
+
* the credential epoch the new credential starts, the identity the replace
|
|
48
|
+
* records (absent: none), and the provider state the caller handed to
|
|
49
|
+
* `replace`, if any.
|
|
50
|
+
*/
|
|
51
|
+
export interface ProviderStateReplacement {
|
|
52
|
+
id: string;
|
|
53
|
+
credentialEpoch: number;
|
|
54
|
+
identity?: string;
|
|
55
|
+
incoming?: unknown;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Codec for the provider state a plugin keeps beside each row's credential
|
|
59
|
+
* (a project id, a device fingerprint, eligibility times: whatever belongs to
|
|
60
|
+
* that credential). The store never interprets the value: it stores it as
|
|
61
|
+
* JSON, asks `validate` before every write and on every load, and calls the
|
|
62
|
+
* hooks below where two values meet. Every hook is synchronous and runs
|
|
63
|
+
* under the store locks.
|
|
64
|
+
*/
|
|
65
|
+
export interface ProviderStateCodec {
|
|
66
|
+
validate(value: unknown): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* The part of a (valid) value that belongs to the credential: what is true
|
|
69
|
+
* of the account the credential signs in to, such as a project id or a
|
|
70
|
+
* device fingerprint, as opposed to what the plugin merely tracks about
|
|
71
|
+
* its use, such as a cooldown or a cursor. The credential stamp covers the
|
|
72
|
+
* digest of exactly this projection, so a foreign edit of it hides the
|
|
73
|
+
* value, while the rest may change through `updateProviderState` without
|
|
74
|
+
* the stamp being rewritten. It must be a pure function of the value that
|
|
75
|
+
* returns JSON with a deterministic key order. Without it the whole value
|
|
76
|
+
* is credential-bound.
|
|
77
|
+
*/
|
|
78
|
+
credentialBound?(value: unknown): unknown;
|
|
79
|
+
/**
|
|
80
|
+
* Combines the value on disk with one a write brings (`add` of a secret the
|
|
81
|
+
* pool already holds, `rotate`, or a refresh whose provider returned a
|
|
82
|
+
* state). Called only when the row has a value on disk; without it the
|
|
83
|
+
* incoming value replaces the stored one. Its result is validated.
|
|
84
|
+
*/
|
|
85
|
+
merge?(onDisk: unknown, incoming: unknown): unknown;
|
|
86
|
+
/**
|
|
87
|
+
* The provider state of a row once `replace` gives it a new credential
|
|
88
|
+
* epoch. `previous` is the old credential's value (undefined when the row
|
|
89
|
+
* shows none). Returning undefined clears it. Without this hook a replace
|
|
90
|
+
* keeps the value handed to `replace` and otherwise clears the state.
|
|
91
|
+
*/
|
|
92
|
+
onReplace?(previous: unknown | undefined, replacement: ProviderStateReplacement): unknown | undefined;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Why a row shows no provider state although the state file holds one for it.
|
|
96
|
+
*
|
|
97
|
+
* `uncovered`: the value's credential-bound part (see
|
|
98
|
+
* `ProviderStateCodec.credentialBound`) is not the one this store last
|
|
99
|
+
* wrote beside the row's credential at its credential epoch and recorded
|
|
100
|
+
* identity. Another writer edited it, or
|
|
101
|
+
* wrote the credential beside it without knowing about it (an older version
|
|
102
|
+
* of this library, which drops the coverage, or a writer that does not know
|
|
103
|
+
* the pool at all), or it belongs to an earlier credential of the row.
|
|
104
|
+
* `invalid`: the value is covered but the codec rejects it, or the store was
|
|
105
|
+
* opened without a provider-state codec.
|
|
106
|
+
*/
|
|
107
|
+
export type ProviderStateDrop = 'uncovered' | 'invalid';
|
|
45
108
|
/**
|
|
46
109
|
* What the stamp beside a row's credential proves (see `CredentialStamp`).
|
|
47
110
|
*
|
|
@@ -82,6 +145,18 @@ export interface PoolRow {
|
|
|
82
145
|
disabledReason?: string;
|
|
83
146
|
/** The opaque quota map, as validated by the codec. */
|
|
84
147
|
quota?: unknown;
|
|
148
|
+
/**
|
|
149
|
+
* The opaque provider state kept beside the credential, as validated by
|
|
150
|
+
* the provider-state codec. Absent when the row has none, or when the one
|
|
151
|
+
* on disk is not shown (see `providerStateDropped`).
|
|
152
|
+
*/
|
|
153
|
+
providerState?: unknown;
|
|
154
|
+
/**
|
|
155
|
+
* Set when the state file holds a provider state for the row that is not
|
|
156
|
+
* shown, and why. The value stays on disk untouched until a write on the
|
|
157
|
+
* row sets or clears it; the row itself stays usable.
|
|
158
|
+
*/
|
|
159
|
+
providerStateDropped?: ProviderStateDrop;
|
|
85
160
|
hasEntry: boolean;
|
|
86
161
|
/** A row that may be refreshed, pulled for, or admitted. */
|
|
87
162
|
candidate: boolean;
|
|
@@ -93,9 +168,11 @@ export interface PoolRow {
|
|
|
93
168
|
* it belongs to, and the config still holds the replaced row. Also set when
|
|
94
169
|
* a write that gives a row its first identity (`recordIdentity`, or a
|
|
95
170
|
* `rotate` or refresh that learns one) stopped after stamping the identity
|
|
96
|
-
* and before recording it in the config
|
|
97
|
-
*
|
|
98
|
-
*
|
|
171
|
+
* and before recording it in the config, and when an attributed `disable`
|
|
172
|
+
* or `enable` that changed the provider state stopped after its state
|
|
173
|
+
* write and before flipping the row in the config. The row is shown as the
|
|
174
|
+
* write leaves it once completed, is never a candidate, and the next store
|
|
175
|
+
* write on it writes the config to match.
|
|
99
176
|
*/
|
|
100
177
|
torn?: true;
|
|
101
178
|
/**
|
|
@@ -147,6 +224,12 @@ export interface CredentialBinding {
|
|
|
147
224
|
* only on replace. `replace` marks a stamp written by a replace, which is
|
|
148
225
|
* what lets a reader complete a replace that stopped after writing the
|
|
149
226
|
* credential: a stamp from any other write is never completed as torn.
|
|
227
|
+
*
|
|
228
|
+
* `providerState` is the digest of the credential-bound part of the provider
|
|
229
|
+
* state beside the credential (see `providerStateDigest`); it is absent when
|
|
230
|
+
* the row has none, so the stamp of a row without provider state is exactly
|
|
231
|
+
* what 0.5.0 wrote. It is apart from `digest` and `dispatch`, which keep
|
|
232
|
+
* their meaning: the credential's stamp status never depends on it.
|
|
150
233
|
*/
|
|
151
234
|
export interface CredentialStamp {
|
|
152
235
|
credentialEpoch: number;
|
|
@@ -154,7 +237,13 @@ export interface CredentialStamp {
|
|
|
154
237
|
dispatch?: string;
|
|
155
238
|
binding?: CredentialBinding;
|
|
156
239
|
replace?: true;
|
|
240
|
+
providerState?: string;
|
|
157
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Key, inside a state-file account entry, of the provider state kept beside
|
|
244
|
+
* the credential. Older readers ignore it.
|
|
245
|
+
*/
|
|
246
|
+
export declare const PROVIDER_STATE_KEY = "commonAuthProviderState";
|
|
158
247
|
export type ConfigClassification = {
|
|
159
248
|
status: 'ready';
|
|
160
249
|
exists: boolean;
|
|
@@ -208,7 +297,28 @@ export declare function dispatchDigest(credential: PoolCredential | StoredCreden
|
|
|
208
297
|
*/
|
|
209
298
|
export declare function stampFor(credential: PoolCredential | StoredCredential, credentialEpoch: number, binding: CredentialBinding, options?: {
|
|
210
299
|
replace?: boolean;
|
|
300
|
+
providerState?: string;
|
|
211
301
|
}): CredentialStamp;
|
|
302
|
+
/**
|
|
303
|
+
* The digest a stamp carries for a provider state: of its credential-bound
|
|
304
|
+
* part (`ProviderStateCodec.credentialBound`, the whole value without it), as
|
|
305
|
+
* it serializes. The store only ever stores values that survive a JSON round
|
|
306
|
+
* trip unchanged, so the digest of a value read back from disk equals the one
|
|
307
|
+
* computed when it was written. Its input prefix differs from every other
|
|
308
|
+
* digest the store writes.
|
|
309
|
+
*/
|
|
310
|
+
export declare function providerStateDigest(codec: Pick<ProviderStateCodec, 'credentialBound'> | undefined, value: unknown): string;
|
|
311
|
+
/**
|
|
312
|
+
* The provider-state digest of the stamp in a state-file account entry, when
|
|
313
|
+
* that stamp binds it to this row: written with this credential (its lineage
|
|
314
|
+
* digest matches), at this credential epoch, naming this recorded identity.
|
|
315
|
+
* Whether the value beside it still has that digest is checked apart, by
|
|
316
|
+
* `providerStateFields`; this alone is what a write that keeps the value
|
|
317
|
+
* carries into the stamp it writes, so a value no stamp of this store bound
|
|
318
|
+
* to the row stays unbound rather than being vouched for, and one edited
|
|
319
|
+
* after it was bound stays detectably edited.
|
|
320
|
+
*/
|
|
321
|
+
export declare function boundProviderStateDigest(account: unknown, credential: PoolCredential | StoredCredential | undefined, credentialEpoch: number | undefined, identity: string | undefined): string | undefined;
|
|
212
322
|
/**
|
|
213
323
|
* A credential epoch is a positive safe integer. Above `MAX_SAFE_INTEGER`,
|
|
214
324
|
* adding one may give back the same number, so a replace would not move the
|
|
@@ -245,7 +355,7 @@ export declare function setEntryIn(config: Record<string, unknown>, id: string,
|
|
|
245
355
|
* malformed per-row entry makes that one row invalid (never a candidate) and
|
|
246
356
|
* blocks nothing else.
|
|
247
357
|
*/
|
|
248
|
-
export declare function buildRawRows(config: Record<string, unknown>, state: Record<string, unknown>, codec: QuotaCodec): PoolRow[];
|
|
358
|
+
export declare function buildRawRows(config: Record<string, unknown>, state: Record<string, unknown>, codec: QuotaCodec, providerCodec?: ProviderStateCodec): PoolRow[];
|
|
249
359
|
/** The row-lock key: recorded wire identity when known, else the local id. */
|
|
250
360
|
export declare function rowLockKey(row: Pick<PoolRow, 'id' | 'identity'>): string;
|
|
251
361
|
/**
|