@cortexkit/common-auth 0.4.2 → 0.4.3
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/store/attribution.js +2 -1
- package/dist/store/errors.d.ts +1 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/mutate.d.ts +6 -0
- package/dist/store/mutate.js +6 -2
- package/dist/store/pool.d.ts +13 -0
- package/dist/store/pool.js +1 -0
- package/dist/store/pull.d.ts +1 -1
- package/dist/store/pull.js +9 -0
- package/dist/store/refresh.js +6 -1
- package/dist/store/rows.js +14 -1
- package/dist/store/runtime.d.ts +8 -0
- package/dist/store/runtime.js +11 -0
- package/dist/store/schema.d.ts +30 -0
- package/dist/store/schema.js +47 -3
- package/dist/store/torn.d.ts +7 -1
- package/dist/store/torn.js +13 -7
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PoolOperationError } from './errors.js';
|
|
2
2
|
import { toFailure, withTransaction } from './mutate.js';
|
|
3
3
|
import { LockStack } from './refresh-lock.js';
|
|
4
|
-
import { refusal, unknownRow } from './runtime.js';
|
|
4
|
+
import { refusal, requireBound, unknownRow, } from './runtime.js';
|
|
5
5
|
import { isCredentialEpoch } from './schema.js';
|
|
6
6
|
/**
|
|
7
7
|
* Merges a quota observation into a row's stored map under the store locks,
|
|
@@ -26,6 +26,7 @@ export async function recordQuota(rt, id, attribution, observation) {
|
|
|
26
26
|
// has no such triple on disk, so no reading is recorded for it.
|
|
27
27
|
if (!row.credential)
|
|
28
28
|
throw refusal('pull', id, 'no-credential', `row ${id} holds no credential`);
|
|
29
|
+
requireBound('pull', row);
|
|
29
30
|
const entry = tx.entry(id);
|
|
30
31
|
if (row.torn ||
|
|
31
32
|
!entry ||
|
package/dist/store/errors.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type PoolFailurePhase = 'before-first-write' | 'after-first-write' | 'pul
|
|
|
19
19
|
* lock outcomes (a wait that ran out, and a lease found lost); the rest are
|
|
20
20
|
* refusals and failures of the operation itself.
|
|
21
21
|
*/
|
|
22
|
-
export type PoolFailureKind = 'lock-contention' | 'lock-ownership' | 'pending-migration' | 'load-error' | 'unknown-row' | 'invalid-row' | 'invalid-input' | 'id-exists' | 'id-removed' | 'type-mismatch' | 'no-credential' | 'row-disabled' | 'row-protected' | 'duplicate-identity' | 'identity-mismatch' | 'endpoint-mismatch' | 'row-key-changed' | 'invalid-order' | 'refresh-stamp-ahead' | 'attribution' | 'provider' | 'pull' | 'invalid-quota' | 'after-persist-hook' | 'unexpected';
|
|
22
|
+
export type PoolFailureKind = 'lock-contention' | 'lock-ownership' | 'pending-migration' | 'load-error' | 'unknown-row' | 'invalid-row' | 'invalid-input' | 'id-exists' | 'id-removed' | 'type-mismatch' | 'no-credential' | 'row-disabled' | 'row-protected' | 'duplicate-identity' | 'identity-mismatch' | 'endpoint-mismatch' | 'row-key-changed' | 'invalid-order' | 'refresh-stamp-ahead' | 'unbound-credential' | 'attribution' | 'provider' | 'pull' | 'invalid-quota' | 'after-persist-hook' | 'unexpected';
|
|
23
23
|
/**
|
|
24
24
|
* The single failure value of every store operation. `committed` is present
|
|
25
25
|
* only when the operation had already written a credential to the state file
|
package/dist/store/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type { LockEvent, PoolLockOptions, PoolLockSpec, } from './refresh-lock.j
|
|
|
12
12
|
export { POOL_LOCK_DEFAULTS } from './refresh-lock.js';
|
|
13
13
|
export type { AddInput, AddResult, FailureHook, RemoveOptions, RemoveResult, RemoveView, ReorderOptions, ReorderResult, RowOperationOptions, RowToggleOptions, } from './rows.js';
|
|
14
14
|
export type { PullReason } from './runtime.js';
|
|
15
|
-
export type { ApiKeyCredential, OAuthCredential, PoolCredential, PoolRow, QuotaCodec, RotateCredential, StoredCredential, } from './schema.js';
|
|
15
|
+
export type { ApiKeyCredential, CredentialStampStatus, OAuthCredential, PoolCredential, PoolRow, QuotaCodec, RotateCredential, StoredCredential, } from './schema.js';
|
|
16
16
|
export { fingerprintOf, LEGACY_STORE_VERSION, POOL_KEY, POOL_ROWS_KEY, POOL_SCHEMA_VERSION, REFRESH_STAMP_TOLERANCE_MS, rowLockKey, } from './schema.js';
|
|
17
17
|
export type { PoolSettings, SettingsMutator, SettingsRead, UpdateSettingsOptions, UpdateSettingsResult, } from './settings.js';
|
|
18
18
|
export { POOL_OWNED_KEYS } from './settings.js';
|
package/dist/store/mutate.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export interface StoreContext {
|
|
|
23
23
|
hold?: (point: HoldPoint, rowId: string) => void | Promise<void>;
|
|
24
24
|
/** Ids whose per-row entry a library write dropped in this process. */
|
|
25
25
|
removedIds: Set<string>;
|
|
26
|
+
/**
|
|
27
|
+
* When true, rows whose credential stamp is not bound load `unbound` and
|
|
28
|
+
* the operations that use a credential refuse them (see
|
|
29
|
+
* `OpenPoolStoreOptions.requireCredentialStamps`).
|
|
30
|
+
*/
|
|
31
|
+
requireCredentialStamps?: boolean;
|
|
26
32
|
}
|
|
27
33
|
export interface Snapshot {
|
|
28
34
|
configExists: boolean;
|
package/dist/store/mutate.js
CHANGED
|
@@ -39,7 +39,9 @@ export async function readPool(ctx) {
|
|
|
39
39
|
stateExists: state.exists,
|
|
40
40
|
config: config.config,
|
|
41
41
|
state: state.state,
|
|
42
|
-
rows: loadRows(config.config, state.state, ctx.codec
|
|
42
|
+
rows: loadRows(config.config, state.state, ctx.codec, {
|
|
43
|
+
requireCredentialStamps: ctx.requireCredentialStamps === true,
|
|
44
|
+
}),
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
47
|
/** The refusal for a pool that is not ready, as a failure value. */
|
|
@@ -86,7 +88,9 @@ export class Transaction {
|
|
|
86
88
|
* `PoolRow.torn`).
|
|
87
89
|
*/
|
|
88
90
|
rows() {
|
|
89
|
-
return loadRows(this.config, this.state, this.ctx.codec
|
|
91
|
+
return loadRows(this.config, this.state, this.ctx.codec, {
|
|
92
|
+
requireCredentialStamps: this.ctx.requireCredentialStamps === true,
|
|
93
|
+
});
|
|
90
94
|
}
|
|
91
95
|
row(id) {
|
|
92
96
|
return this.rows().find((row) => row.id === id);
|
package/dist/store/pool.d.ts
CHANGED
|
@@ -14,6 +14,19 @@ export interface OpenPoolStoreOptions {
|
|
|
14
14
|
configPath: string;
|
|
15
15
|
statePath: string;
|
|
16
16
|
quota: QuotaCodec;
|
|
17
|
+
/**
|
|
18
|
+
* Refuse every credential this store did not stamp (default false, which
|
|
19
|
+
* loads unstamped and mis-stamped credentials as older writers left them).
|
|
20
|
+
* When true, a row whose `stamp` is not `bound` loads `unbound` and is no
|
|
21
|
+
* candidate, and every operation that would use or keep its credential or
|
|
22
|
+
* what was observed about it (`refresh`, quota pulls, `recordQuota`,
|
|
23
|
+
* `recordIdentity`, `rotate`, and an `add` of the same secret or onto the
|
|
24
|
+
* same credential-less row) refuses with `unbound-credential` before any
|
|
25
|
+
* provider call or write, checked again under the locks and at commit.
|
|
26
|
+
* Nothing makes such a row bound except `replace`, which starts a new
|
|
27
|
+
* credential epoch and drops what was observed about the old one.
|
|
28
|
+
*/
|
|
29
|
+
requireCredentialStamps?: boolean;
|
|
17
30
|
/** Injected clock for leases, refresh stamps and `addedAt`. */
|
|
18
31
|
now?: () => number;
|
|
19
32
|
/**
|
package/dist/store/pool.js
CHANGED
|
@@ -59,6 +59,7 @@ export function openPoolStore(options) {
|
|
|
59
59
|
...(options.onLockStep ? { onLockStep: options.onLockStep } : {}),
|
|
60
60
|
},
|
|
61
61
|
removedIds: memory.removedIds,
|
|
62
|
+
requireCredentialStamps: options.requireCredentialStamps === true,
|
|
62
63
|
...(options.logger ? { logger: options.logger } : {}),
|
|
63
64
|
...(options.onStep ? { onStep: options.onStep } : {}),
|
|
64
65
|
...(options.hold ? { hold: options.hold } : {}),
|
package/dist/store/pull.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PoolOperationError } from './errors.js';
|
|
2
2
|
import type { PoolLogger } from './hooks.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type PullReason, type StoreRuntime } from './runtime.js';
|
|
4
4
|
import type { StoredCredential } from './schema.js';
|
|
5
5
|
/** What a pull is issued with: the credential and its attribution tuple. */
|
|
6
6
|
export interface PullRequest {
|
package/dist/store/pull.js
CHANGED
|
@@ -2,6 +2,7 @@ import { recordQuota } from './attribution.js';
|
|
|
2
2
|
import { PoolOperationError } from './errors.js';
|
|
3
3
|
import { toFailure, withTransaction } from './mutate.js';
|
|
4
4
|
import { LockStack } from './refresh-lock.js';
|
|
5
|
+
import { requireBound } from './runtime.js';
|
|
5
6
|
/**
|
|
6
7
|
* Fires quota pulls without ever making a caller wait for one. A pull first
|
|
7
8
|
* gives a row without a per-row entry its entry at epoch 1 (its own locked
|
|
@@ -50,6 +51,14 @@ export class PullScheduler {
|
|
|
50
51
|
try {
|
|
51
52
|
const request = await withTransaction(ctx, locks, progress, { operation: 'pull', rowId: id }, async (tx) => {
|
|
52
53
|
const row = tx.row(id);
|
|
54
|
+
// A row that would pull but for its unbound credential refuses, so
|
|
55
|
+
// the failure hook hears of it instead of the pull vanishing.
|
|
56
|
+
if (row?.unbound &&
|
|
57
|
+
row.enabled &&
|
|
58
|
+
row.type === 'oauth' &&
|
|
59
|
+
row.credential &&
|
|
60
|
+
!row.invalid)
|
|
61
|
+
requireBound('pull', row);
|
|
53
62
|
// Disabled, API-key and credential-less rows never pull.
|
|
54
63
|
if (!row?.candidate || row.type !== 'oauth')
|
|
55
64
|
return undefined;
|
package/dist/store/refresh.js
CHANGED
|
@@ -3,7 +3,7 @@ import { assertNotInsideHook, runInsideHook } from './hooks.js';
|
|
|
3
3
|
import { recordIdentityIn } from './identity.js';
|
|
4
4
|
import { runOperation, withTransaction } from './mutate.js';
|
|
5
5
|
import { rotateIn } from './rows.js';
|
|
6
|
-
import { readRow, refusal, rowLockSpec } from './runtime.js';
|
|
6
|
+
import { readRow, refusal, requireBound, rowLockSpec, } from './runtime.js';
|
|
7
7
|
import { rotationStamp, rotationStampUntrusted, rowLockKey, } from './schema.js';
|
|
8
8
|
function requireRefreshable(id, row) {
|
|
9
9
|
if (!row)
|
|
@@ -47,6 +47,7 @@ export async function refreshRow(rt, id, provider, options = {}) {
|
|
|
47
47
|
const captureProgress = { writes: 0 };
|
|
48
48
|
const read = await withTransaction(ctx, locks, captureProgress, { operation: 'refresh', rowId: id }, async (tx) => {
|
|
49
49
|
let row = requireRefreshable(id, tx.row(id));
|
|
50
|
+
requireBound('refresh', row);
|
|
50
51
|
if (!row.hasEntry) {
|
|
51
52
|
tx.setEntry(id, { credentialEpoch: 1, needsFirstReading: true });
|
|
52
53
|
await tx.commitConfig();
|
|
@@ -113,6 +114,10 @@ export async function refreshRow(rt, id, provider, options = {}) {
|
|
|
113
114
|
kind: 'attribution',
|
|
114
115
|
message: `row ${id} changed credential while its refresh was in flight; the rotation is discarded`,
|
|
115
116
|
});
|
|
117
|
+
// The epoch fence above does not see a credential another writer
|
|
118
|
+
// swapped in under the same epoch; the stamp does, and committing
|
|
119
|
+
// the rotation would stamp the swapped row as bound.
|
|
120
|
+
requireBound('refresh', current);
|
|
116
121
|
const reason = await options.refuse?.(current);
|
|
117
122
|
if (reason !== undefined)
|
|
118
123
|
return { refused: reason };
|
package/dist/store/rows.js
CHANGED
|
@@ -2,7 +2,7 @@ import { PoolOperationError } from './errors.js';
|
|
|
2
2
|
import { assertNotInsideHook, runInsideHook } from './hooks.js';
|
|
3
3
|
import { DUPLICATE_IDENTITY_REASON, disableIdentityDuplicates, disableIn, recordIdentityIn, } from './identity.js';
|
|
4
4
|
import { notReadyError, readPool, runOperation, withTransaction, } from './mutate.js';
|
|
5
|
-
import { readRow, refusal, rowLockSpec, unknownRow, } from './runtime.js';
|
|
5
|
+
import { readRow, refusal, requireBound, rowLockSpec, unknownRow, } from './runtime.js';
|
|
6
6
|
import { CREDENTIAL_STAMP_KEY, credentialProblem, fingerprintOf, idProblem, isCredentialEpoch, isRecord, rosterRowFor, rotationStamp, rowLockKey, stampFor, stateFieldsFor, storedCredential, } from './schema.js';
|
|
7
7
|
import { bindReplacement } from './torn.js';
|
|
8
8
|
/** Fields of a state entry that belong to the credential it replaces. */
|
|
@@ -129,6 +129,11 @@ export async function addRow(rt, input, options = {}) {
|
|
|
129
129
|
const fingerprint = fingerprintOf(credential);
|
|
130
130
|
const same = rows.find((row) => row.invalid === undefined && row.fingerprint === fingerprint);
|
|
131
131
|
if (same) {
|
|
132
|
+
// Re-adding a secret whose stamp is not proved would rotate it in
|
|
133
|
+
// and keep the identity and quota recorded beside it, making that
|
|
134
|
+
// unproved record look bound. The add is refused instead, and the
|
|
135
|
+
// caller replaces the row, which starts a new credential epoch.
|
|
136
|
+
requireBound('add', same);
|
|
132
137
|
// The same secret is the same credential, so re-adding it rotates
|
|
133
138
|
// that row. An identity or endpoint given with it must match the
|
|
134
139
|
// row's; a different one is refused rather than silently replaced
|
|
@@ -146,6 +151,10 @@ export async function addRow(rt, input, options = {}) {
|
|
|
146
151
|
throw refusal('add', id, 'invalid-row', `row ${id} is invalid`);
|
|
147
152
|
if (existing.credential)
|
|
148
153
|
throw refusal('add', id, 'id-exists', `row ${id} already holds a credential`);
|
|
154
|
+
// Completing a credential-less row keeps its epoch, identity and
|
|
155
|
+
// quota; when stamps are required those belong to no proved
|
|
156
|
+
// credential, so the row must be replaced instead.
|
|
157
|
+
requireBound('add', existing);
|
|
149
158
|
if (existing.type !== credential.type)
|
|
150
159
|
throw refusal('add', id, 'type-mismatch', `row ${id} is a ${existing.type} row`);
|
|
151
160
|
if (identity !== undefined &&
|
|
@@ -275,6 +284,9 @@ export async function rotateRow(rt, id, credential, input = {}, options = {}) {
|
|
|
275
284
|
const row = requireUsableRow('rotate', id, tx.row(id), credential);
|
|
276
285
|
if (rowLockKey(row) !== rowLockKey(seen))
|
|
277
286
|
throw keyChanged('rotate', id);
|
|
287
|
+
// A rotation keeps the row's epoch, identity and quota, so it must
|
|
288
|
+
// never be what stamps an unproved row as bound.
|
|
289
|
+
requireBound('rotate', row);
|
|
278
290
|
// A rotation stays with one account: it may record the first
|
|
279
291
|
// identity the row learns, but a credential of another known
|
|
280
292
|
// account is a replacement (new epoch, quota and errors dropped).
|
|
@@ -553,6 +565,7 @@ export async function recordRowIdentity(rt, id, identity, attribution, options =
|
|
|
553
565
|
const row = requireUsableRow('recordIdentity', id, tx.row(id));
|
|
554
566
|
if (rowLockKey(row) !== rowLockKey(seen))
|
|
555
567
|
throw keyChanged('recordIdentity', id);
|
|
568
|
+
requireBound('recordIdentity', row);
|
|
556
569
|
if ((row.credentialEpoch ?? 1) !== captured)
|
|
557
570
|
throw refusal('recordIdentity', id, 'attribution', `the identity for ${id} was looked up for a credential the row no longer holds`, true);
|
|
558
571
|
if (row.identity !== undefined && row.identity !== identity)
|
package/dist/store/runtime.d.ts
CHANGED
|
@@ -25,4 +25,12 @@ export declare function readRow(rt: StoreRuntime, operation: PoolOperation, id:
|
|
|
25
25
|
row: PoolRow;
|
|
26
26
|
}>;
|
|
27
27
|
export declare function unknownRow(operation: PoolOperation, id: string): PoolOperationError;
|
|
28
|
+
/**
|
|
29
|
+
* Refuses a row the store was told to distrust: opened with
|
|
30
|
+
* `requireCredentialStamps`, a row whose credential stamp is not bound loads
|
|
31
|
+
* `unbound` (see `PoolRow.unbound`). Called on every locked read of the row
|
|
32
|
+
* an operation acts on, before it calls a provider or writes, so a credential
|
|
33
|
+
* another writer swapped in while the operation waited is refused too.
|
|
34
|
+
*/
|
|
35
|
+
export declare function requireBound(operation: PoolOperation, row: PoolRow): void;
|
|
28
36
|
export declare function refusal(operation: PoolOperation, id: string, kind: PoolOperationError['kind'], message: string, retryable?: boolean): PoolOperationError;
|
package/dist/store/runtime.js
CHANGED
|
@@ -33,6 +33,17 @@ export function unknownRow(operation, id) {
|
|
|
33
33
|
message: `no row ${id} in the pool`,
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Refuses a row the store was told to distrust: opened with
|
|
38
|
+
* `requireCredentialStamps`, a row whose credential stamp is not bound loads
|
|
39
|
+
* `unbound` (see `PoolRow.unbound`). Called on every locked read of the row
|
|
40
|
+
* an operation acts on, before it calls a provider or writes, so a credential
|
|
41
|
+
* another writer swapped in while the operation waited is refused too.
|
|
42
|
+
*/
|
|
43
|
+
export function requireBound(operation, row) {
|
|
44
|
+
if (row.unbound)
|
|
45
|
+
throw refusal(operation, row.id, 'unbound-credential', `row ${row.id}'s credential is not the one this store stamped for it (stamp ${row.stamp}); replace it with fresh material`);
|
|
46
|
+
}
|
|
36
47
|
export function refusal(operation, id, kind, message, retryable = false) {
|
|
37
48
|
return new PoolOperationError({
|
|
38
49
|
operation,
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -42,6 +42,21 @@ export interface QuotaCodec {
|
|
|
42
42
|
validate(value: unknown): boolean;
|
|
43
43
|
merge(stored: unknown | undefined, observation: unknown): unknown;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* What the stamp beside a row's credential proves (see `CredentialStamp`).
|
|
47
|
+
*
|
|
48
|
+
* `none`: the row holds no credential, so there is nothing to stamp.
|
|
49
|
+
* `bound`: the stamp was written with this credential (its digest matches),
|
|
50
|
+
* names the row's credential epoch (1 for a row without a per-row entry),
|
|
51
|
+
* and any identity or endpoint it records is the row's.
|
|
52
|
+
* `missing`: the credential carries no stamp (written by a writer that does
|
|
53
|
+
* not know about stamps, or one that dropped it).
|
|
54
|
+
* `malformed`: the stamp is not one this store writes (wrong shape, or an
|
|
55
|
+
* epoch outside the positive safe integers).
|
|
56
|
+
* `mismatched`: the stamp was written for another credential, epoch,
|
|
57
|
+
* identity or endpoint than the row now holds.
|
|
58
|
+
*/
|
|
59
|
+
export type CredentialStampStatus = 'none' | 'bound' | 'missing' | 'malformed' | 'mismatched';
|
|
45
60
|
/** One row of the pool as loaded. */
|
|
46
61
|
export interface PoolRow {
|
|
47
62
|
id: string;
|
|
@@ -74,6 +89,21 @@ export interface PoolRow {
|
|
|
74
89
|
* the next store write on it writes the config to match.
|
|
75
90
|
*/
|
|
76
91
|
torn?: true;
|
|
92
|
+
/**
|
|
93
|
+
* Whether the credential is the one the store last stamped for this row
|
|
94
|
+
* (see `CredentialStampStatus`). Every row the store loads carries it; a
|
|
95
|
+
* torn row reports the stamp of the row as it is shown completed. It is
|
|
96
|
+
* optional only so that rows built by hand (test fixtures) still type.
|
|
97
|
+
*/
|
|
98
|
+
stamp?: CredentialStampStatus;
|
|
99
|
+
/**
|
|
100
|
+
* Set only when the store was opened with `requireCredentialStamps` and
|
|
101
|
+
* the row's `stamp` is not `bound`: the row is never a candidate, and
|
|
102
|
+
* `refresh`, quota pulls, `recordQuota`, `recordIdentity`, `rotate` and a
|
|
103
|
+
* re-`add` onto it refuse with `unbound-credential`. Only `replace` (or a
|
|
104
|
+
* new row) makes it usable again.
|
|
105
|
+
*/
|
|
106
|
+
unbound?: true;
|
|
77
107
|
}
|
|
78
108
|
/**
|
|
79
109
|
* Key, inside a state-file account entry, of the stamp naming what the
|
package/dist/store/schema.js
CHANGED
|
@@ -124,6 +124,45 @@ export function parseStamp(raw) {
|
|
|
124
124
|
},
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Whether a binding a stamp records is the row's: an identity it names must
|
|
129
|
+
* be the row's recorded identity, and an endpoint it names must be the one
|
|
130
|
+
* the row sends its API key to. A binding without an identity says none was
|
|
131
|
+
* known when it was written, so an identity learnt since does not contradict
|
|
132
|
+
* it.
|
|
133
|
+
*/
|
|
134
|
+
function bindingAgrees(binding, credential, identity) {
|
|
135
|
+
if (binding.identity !== undefined && binding.identity !== identity)
|
|
136
|
+
return false;
|
|
137
|
+
if (binding.baseURL !== undefined &&
|
|
138
|
+
(credential.type !== 'api' || credential.baseURL !== binding.baseURL))
|
|
139
|
+
return false;
|
|
140
|
+
if (binding.authHeader !== undefined &&
|
|
141
|
+
(credential.type !== 'api' || credential.authHeader !== binding.authHeader))
|
|
142
|
+
return false;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The stamp status of a loaded row (see `CredentialStampStatus`).
|
|
147
|
+
* `credentialEpoch` is undefined only when the row's per-row entry exists but
|
|
148
|
+
* failed validation, in which case no stamp can match it.
|
|
149
|
+
*/
|
|
150
|
+
function stampStatusOf(credential, credentialEpoch, identity, account) {
|
|
151
|
+
if (!credential)
|
|
152
|
+
return 'none';
|
|
153
|
+
if (!isRecord(account) || !Object.hasOwn(account, CREDENTIAL_STAMP_KEY))
|
|
154
|
+
return 'missing';
|
|
155
|
+
const stamp = parseStamp(account[CREDENTIAL_STAMP_KEY]);
|
|
156
|
+
if (!stamp)
|
|
157
|
+
return 'malformed';
|
|
158
|
+
if (stamp.digest !== credentialDigest(credential))
|
|
159
|
+
return 'mismatched';
|
|
160
|
+
if (stamp.credentialEpoch !== credentialEpoch)
|
|
161
|
+
return 'mismatched';
|
|
162
|
+
if (stamp.binding && !bindingAgrees(stamp.binding, credential, identity))
|
|
163
|
+
return 'mismatched';
|
|
164
|
+
return 'bound';
|
|
165
|
+
}
|
|
127
166
|
export function classifyConfig(read) {
|
|
128
167
|
if (!read.exists)
|
|
129
168
|
return { status: 'ready', exists: false, config: {} };
|
|
@@ -302,6 +341,7 @@ export function buildRawRows(config, state, codec) {
|
|
|
302
341
|
hasEntry: Object.hasOwn(entries, id),
|
|
303
342
|
candidate: false,
|
|
304
343
|
invalid: 'roster',
|
|
344
|
+
stamp: 'none',
|
|
305
345
|
});
|
|
306
346
|
continue;
|
|
307
347
|
}
|
|
@@ -311,6 +351,9 @@ export function buildRawRows(config, state, codec) {
|
|
|
311
351
|
const entry = hasEntry ? parseEntry(entries[id], codec) : undefined;
|
|
312
352
|
const credential = credentialFor(raw, stateAccounts[id]);
|
|
313
353
|
const enabled = raw.enabled !== false;
|
|
354
|
+
const identity = typeof raw.accountId === 'string' && raw.accountId
|
|
355
|
+
? raw.accountId
|
|
356
|
+
: undefined;
|
|
314
357
|
const row = {
|
|
315
358
|
id,
|
|
316
359
|
type,
|
|
@@ -320,9 +363,7 @@ export function buildRawRows(config, state, codec) {
|
|
|
320
363
|
candidate: false,
|
|
321
364
|
...(typeof raw.label === 'string' ? { label: raw.label } : {}),
|
|
322
365
|
...(typeof raw.addedAt === 'number' ? { addedAt: raw.addedAt } : {}),
|
|
323
|
-
...(
|
|
324
|
-
? { identity: raw.accountId }
|
|
325
|
-
: {}),
|
|
366
|
+
...(identity !== undefined ? { identity } : {}),
|
|
326
367
|
...(credential
|
|
327
368
|
? { credential, fingerprint: fingerprintOf(credential) }
|
|
328
369
|
: {}),
|
|
@@ -331,6 +372,9 @@ export function buildRawRows(config, state, codec) {
|
|
|
331
372
|
? { disabledReason: entry.disabledReason }
|
|
332
373
|
: {}),
|
|
333
374
|
...(entry && 'quota' in entry ? { quota: entry.quota } : {}),
|
|
375
|
+
// A row without a per-row entry is at credential epoch 1 (the epoch the
|
|
376
|
+
// store stamps and later gives it), so its stamp is checked against 1.
|
|
377
|
+
stamp: stampStatusOf(credential, entry ? entry.credentialEpoch : hasEntry ? undefined : 1, identity, stateAccounts[id]),
|
|
334
378
|
};
|
|
335
379
|
if (hasEntry && !entry) {
|
|
336
380
|
row.invalid = 'entry';
|
package/dist/store/torn.d.ts
CHANGED
|
@@ -25,5 +25,11 @@ export declare function completeTornRows(config: Record<string, unknown>, state:
|
|
|
25
25
|
* The rows every reader gets. A torn row is shown completed (the identity,
|
|
26
26
|
* endpoint and epoch its stamp names, beside the credential it stamps), is
|
|
27
27
|
* marked `torn` and is never a candidate; every other row is as on disk.
|
|
28
|
+
* With `requireCredentialStamps`, a row whose stamp is not `bound` is marked
|
|
29
|
+
* `unbound` and is never a candidate either. A torn row is shown with the
|
|
30
|
+
* replacement's stamp, which binds the completed row, so it is not unbound:
|
|
31
|
+
* it stays out of routing only until its completion is written.
|
|
28
32
|
*/
|
|
29
|
-
export declare function loadRows(config: Record<string, unknown>, state: Record<string, unknown>, codec: QuotaCodec
|
|
33
|
+
export declare function loadRows(config: Record<string, unknown>, state: Record<string, unknown>, codec: QuotaCodec, options?: {
|
|
34
|
+
requireCredentialStamps?: boolean;
|
|
35
|
+
}): PoolRow[];
|
package/dist/store/torn.js
CHANGED
|
@@ -97,17 +97,23 @@ export function completeTornRows(config, state, codec) {
|
|
|
97
97
|
* The rows every reader gets. A torn row is shown completed (the identity,
|
|
98
98
|
* endpoint and epoch its stamp names, beside the credential it stamps), is
|
|
99
99
|
* marked `torn` and is never a candidate; every other row is as on disk.
|
|
100
|
+
* With `requireCredentialStamps`, a row whose stamp is not `bound` is marked
|
|
101
|
+
* `unbound` and is never a candidate either. A torn row is shown with the
|
|
102
|
+
* replacement's stamp, which binds the completed row, so it is not unbound:
|
|
103
|
+
* it stays out of routing only until its completion is written.
|
|
100
104
|
*/
|
|
101
|
-
export function loadRows(config, state, codec) {
|
|
105
|
+
export function loadRows(config, state, codec, options = {}) {
|
|
102
106
|
const { config: whole, torn } = completeTornRows(config, state, codec);
|
|
103
107
|
const rows = buildRawRows(whole, state, codec);
|
|
104
|
-
if (torn.length === 0)
|
|
105
|
-
return rows;
|
|
106
108
|
for (const row of rows) {
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
if (torn.includes(row.id)) {
|
|
110
|
+
row.torn = true;
|
|
111
|
+
row.candidate = false;
|
|
112
|
+
}
|
|
113
|
+
if (options.requireCredentialStamps && row.stamp !== 'bound') {
|
|
114
|
+
row.unbound = true;
|
|
115
|
+
row.candidate = false;
|
|
116
|
+
}
|
|
111
117
|
}
|
|
112
118
|
return rows;
|
|
113
119
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/common-auth",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Shared code for the CortexKit auth plugins: account pool, quota and routing, commands and auth menu, OpenCode 2 hooks, Claustrum custody, and plumbing (loopback RPC, file locks, logger, sidebar state, TUI preferences and build).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|