@cortexkit/common-auth 0.4.0 → 0.4.1
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/claustrum/roster.js +38 -11
- package/dist/store/attribution.js +3 -0
- package/dist/store/errors.d.ts +1 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/mutate.d.ts +4 -1
- package/dist/store/pool.d.ts +8 -2
- package/dist/store/rows.d.ts +5 -4
- package/dist/store/rows.js +64 -18
- package/dist/store/schema.d.ts +17 -1
- package/dist/store/schema.js +13 -4
- package/package.json +1 -1
package/dist/claustrum/roster.js
CHANGED
|
@@ -359,22 +359,42 @@ export function acceptVaultRoute(path, routeId) {
|
|
|
359
359
|
};
|
|
360
360
|
});
|
|
361
361
|
}
|
|
362
|
-
|
|
362
|
+
/**
|
|
363
|
+
* What a receipt may do to a row's quota: `hold` (the row holds the receipt's
|
|
364
|
+
* credential and account), `bind` (the row has never had a known account and
|
|
365
|
+
* the receipt proves one, so the row takes that account with the reading) or
|
|
366
|
+
* `refuse`.
|
|
367
|
+
*/
|
|
368
|
+
function receiptFence(row, receipt) {
|
|
363
369
|
if (!memberIds(row).includes(receipt.credentialId))
|
|
364
|
-
return
|
|
370
|
+
return 'refuse';
|
|
365
371
|
// While the vault makes no claim, the roster's own expectation proves
|
|
366
372
|
// nothing about which account the reading came from.
|
|
367
373
|
if (row.unclaimed &&
|
|
368
374
|
(receipt.accountIdentitySource === 'expected' ||
|
|
369
375
|
receipt.accountIdentitySource === 'none'))
|
|
370
|
-
return
|
|
376
|
+
return 'refuse';
|
|
371
377
|
// Equality, never compatibility: a known account on one side does not match
|
|
372
|
-
// an absent one on the other. The
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
+
// an absent one on the other. The only absent-absent match is a row that
|
|
379
|
+
// has never had a known account and a receipt that names none either.
|
|
380
|
+
if (row.accountIdentity === receipt.accountIdentity)
|
|
381
|
+
return 'hold';
|
|
382
|
+
// A row that has never had a known account, reached by a receipt issued for
|
|
383
|
+
// it in that state whose served or parsed identity names an account. The
|
|
384
|
+
// reading belongs to that account, so keeping it without the account would
|
|
385
|
+
// let the next list that names a different account inherit it as if that
|
|
386
|
+
// account had been learned for the first time. The row takes the account
|
|
387
|
+
// with the reading instead: a later list naming another account is then a
|
|
388
|
+
// known replacement and inherits nothing. Only a single-credential row is
|
|
389
|
+
// bound, since one credential's account says nothing about the others.
|
|
390
|
+
if (row.accountIdentity === undefined &&
|
|
391
|
+
receipt.expectedAccountIdentity === undefined &&
|
|
392
|
+
receipt.accountIdentity !== undefined &&
|
|
393
|
+
(receipt.accountIdentitySource === 'asserted' ||
|
|
394
|
+
receipt.accountIdentitySource === 'parsed') &&
|
|
395
|
+
memberIds(row).length === 1)
|
|
396
|
+
return 'bind';
|
|
397
|
+
return 'refuse';
|
|
378
398
|
}
|
|
379
399
|
/**
|
|
380
400
|
* Merge a quota observation into a vault row through `/quota`'s merge. The
|
|
@@ -386,13 +406,20 @@ function holdsReceipt(row, receipt) {
|
|
|
386
406
|
export function recordVaultQuota(path, input) {
|
|
387
407
|
return mutateVaultRoster(path, (current) => {
|
|
388
408
|
const row = current?.rows.find((entry) => entry.routeId === input.routeId);
|
|
389
|
-
|
|
409
|
+
const fence = !current || !row ? 'refuse' : receiptFence(row, input);
|
|
410
|
+
if (!current || !row || fence === 'refuse')
|
|
390
411
|
return { result: false };
|
|
391
412
|
const quota = mergeQuotaObservation(row.quota, input.observation);
|
|
413
|
+
// A bound account is `unclaimed`: the vault's list has not named it, and
|
|
414
|
+
// the next projection keeps it as the account this credential last
|
|
415
|
+
// logged into until the vault names one.
|
|
416
|
+
const bound = fence === 'bind'
|
|
417
|
+
? { accountIdentity: input.accountIdentity, unclaimed: true }
|
|
418
|
+
: {};
|
|
392
419
|
return {
|
|
393
420
|
next: {
|
|
394
421
|
...current,
|
|
395
|
-
rows: current.rows.map((entry) => entry === row ? { ...entry, quota } : entry),
|
|
422
|
+
rows: current.rows.map((entry) => entry === row ? { ...entry, ...bound, quota } : entry),
|
|
396
423
|
},
|
|
397
424
|
result: true,
|
|
398
425
|
};
|
|
@@ -2,6 +2,7 @@ import { PoolOperationError } from './errors.js';
|
|
|
2
2
|
import { toFailure, withTransaction } from './mutate.js';
|
|
3
3
|
import { LockStack } from './refresh-lock.js';
|
|
4
4
|
import { refusal, unknownRow } from './runtime.js';
|
|
5
|
+
import { isCredentialEpoch } from './schema.js';
|
|
5
6
|
/**
|
|
6
7
|
* Merges a quota observation into a row's stored map under the store locks,
|
|
7
8
|
* after attribution passes. Needs no row lock, so it is permitted from
|
|
@@ -12,6 +13,8 @@ export async function recordQuota(rt, id, attribution, observation) {
|
|
|
12
13
|
const locks = new LockStack(ctx.lockDefaults, ctx.lockEnv);
|
|
13
14
|
const progress = { writes: 0 };
|
|
14
15
|
try {
|
|
16
|
+
if (!isCredentialEpoch(attribution?.credentialEpoch))
|
|
17
|
+
throw refusal('pull', id, 'invalid-input', 'the credential epoch the reading was issued for must be a positive safe integer');
|
|
15
18
|
await withTransaction(ctx, locks, progress, { operation: 'pull', rowId: id }, async (tx) => {
|
|
16
19
|
const row = tx.row(id);
|
|
17
20
|
if (!row)
|
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' | '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' | '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, StoredCredential, } from './schema.js';
|
|
15
|
+
export type { ApiKeyCredential, 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
|
@@ -64,7 +64,10 @@ export declare class Transaction {
|
|
|
64
64
|
readonly snapshot: Snapshot;
|
|
65
65
|
private readonly locks;
|
|
66
66
|
private readonly progress;
|
|
67
|
-
|
|
67
|
+
readonly info: {
|
|
68
|
+
operation: PoolOperation;
|
|
69
|
+
rowId: string | undefined;
|
|
70
|
+
};
|
|
68
71
|
config: Record<string, unknown>;
|
|
69
72
|
state: Record<string, unknown>;
|
|
70
73
|
constructor(ctx: StoreContext, snapshot: Snapshot, locks: LockStack, progress: Progress, info: {
|
package/dist/store/pool.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { type PullHook } from './pull.js';
|
|
|
6
6
|
import { type ProviderRefresh, type RefreshOptions, type RefreshOutcome } from './refresh.js';
|
|
7
7
|
import { type LockEnvironment, type PoolLockOptions, type PoolLockSpec } from './refresh-lock.js';
|
|
8
8
|
import { type AddInput, type AddResult, type RemoveOptions, type RemoveResult, type ReorderOptions, type ReorderResult, type RowOperationOptions, type RowToggleOptions } from './rows.js';
|
|
9
|
-
import { type PoolCredential, type PoolRow, type QuotaCodec, type StoredCredential } from './schema.js';
|
|
9
|
+
import { type PoolCredential, type PoolRow, type QuotaCodec, type RotateCredential, type StoredCredential } from './schema.js';
|
|
10
10
|
import { type SettingsMutator, type SettingsRead, type UpdateSettingsOptions, type UpdateSettingsResult } from './settings.js';
|
|
11
11
|
export interface OpenPoolStoreOptions {
|
|
12
12
|
/** The provider every row of this pool belongs to; keys the provider-wide lock. */
|
|
@@ -75,7 +75,13 @@ export interface PoolStore {
|
|
|
75
75
|
credential: StoredCredential;
|
|
76
76
|
credentialEpoch: number;
|
|
77
77
|
}>;
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Refreshes the secret a row holds without changing its account or
|
|
80
|
+
* endpoint. Since 0.4.1 an API key may leave out `baseURL` and `authHeader`
|
|
81
|
+
* to keep the row's, and one that gives another is refused
|
|
82
|
+
* (`endpoint-mismatch`) before writing: that is a `replace`.
|
|
83
|
+
*/
|
|
84
|
+
rotate(id: string, credential: RotateCredential, input?: {
|
|
79
85
|
identity?: string;
|
|
80
86
|
}, options?: RowOperationOptions): Promise<{
|
|
81
87
|
id: string;
|
package/dist/store/rows.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { PoolOperationError } from './errors.js';
|
|
|
3
3
|
import { type Transaction } from './mutate.js';
|
|
4
4
|
import type { PoolLockSpec } from './refresh-lock.js';
|
|
5
5
|
import { type StoreRuntime } from './runtime.js';
|
|
6
|
-
import { type CredentialBinding, type PoolCredential, type PoolRow, type StoredCredential } from './schema.js';
|
|
6
|
+
import { type CredentialBinding, type PoolCredential, type PoolRow, type RotateCredential, type StoredCredential } from './schema.js';
|
|
7
7
|
export type FailureHook = (rowId: string, error: PoolOperationError) => void | Promise<void>;
|
|
8
8
|
export interface RowOperationOptions {
|
|
9
9
|
/** Called once, awaited, on every non-success path, before locks release. */
|
|
@@ -91,9 +91,10 @@ export type AddResult = {
|
|
|
91
91
|
* Writes a credential into the state file (one write), stamped with the
|
|
92
92
|
* credential epoch the row's entry holds in `tx` (1 without an entry) and,
|
|
93
93
|
* for a replace, the binding the config is about to get. A rotation is the
|
|
94
|
-
* same lineage: no epoch bump, no identity or quota change.
|
|
94
|
+
* same lineage: no epoch bump, no identity or quota change. An API key must
|
|
95
|
+
* belong to the endpoint the row holds in `tx` (see `onRowEndpoint`).
|
|
95
96
|
*/
|
|
96
|
-
export declare function rotateIn(rt: StoreRuntime, tx: Transaction, id: string,
|
|
97
|
+
export declare function rotateIn(rt: StoreRuntime, tx: Transaction, id: string, given: RotateCredential, extra?: {
|
|
97
98
|
stamp?: number;
|
|
98
99
|
clearErrors?: boolean;
|
|
99
100
|
binding?: CredentialBinding;
|
|
@@ -106,7 +107,7 @@ export declare function replaceRow(rt: StoreRuntime, id: string, credential: Poo
|
|
|
106
107
|
credential: StoredCredential;
|
|
107
108
|
credentialEpoch: number;
|
|
108
109
|
}>;
|
|
109
|
-
export declare function rotateRow(rt: StoreRuntime, id: string, credential:
|
|
110
|
+
export declare function rotateRow(rt: StoreRuntime, id: string, credential: RotateCredential, input?: {
|
|
110
111
|
identity?: string;
|
|
111
112
|
}, options?: RowOperationOptions): Promise<{
|
|
112
113
|
id: string;
|
package/dist/store/rows.js
CHANGED
|
@@ -3,7 +3,7 @@ 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
5
|
import { readRow, refusal, rowLockSpec, unknownRow, } from './runtime.js';
|
|
6
|
-
import { CREDENTIAL_STAMP_KEY, credentialProblem, fingerprintOf, idProblem, isRecord, rosterRowFor, rotationStamp, rowLockKey, stampFor, stateFieldsFor, storedCredential, } from './schema.js';
|
|
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. */
|
|
9
9
|
const CREDENTIAL_STATE_FIELDS = [
|
|
@@ -13,13 +13,52 @@ const CREDENTIAL_STATE_FIELDS = [
|
|
|
13
13
|
'lastRefreshedAt',
|
|
14
14
|
'apiKey',
|
|
15
15
|
];
|
|
16
|
+
/**
|
|
17
|
+
* The endpoint an API-key roster row sends its key to, as `buildRawRows` loads
|
|
18
|
+
* it: the trimmed `baseURL`, and a bearer header unless the row names
|
|
19
|
+
* `x-api-key`.
|
|
20
|
+
*/
|
|
21
|
+
function rowEndpoint(raw) {
|
|
22
|
+
return {
|
|
23
|
+
baseURL: String(raw.baseURL).trim(),
|
|
24
|
+
authHeader: raw.authHeader === 'x-api-key' ? 'x-api-key' : 'authorization-bearer',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The credential as it will sit in the row. The state file holds only an API
|
|
29
|
+
* key; its endpoint lives in the roster row. So an API key written into a row
|
|
30
|
+
* is sent wherever that row says, and one given for another `baseURL` or
|
|
31
|
+
* `authHeader` would silently be paired with an endpoint it was not issued
|
|
32
|
+
* for. A part the caller leaves out is the row's; a part it gives must equal
|
|
33
|
+
* the row's, else the write is refused (`endpoint-mismatch`) before anything
|
|
34
|
+
* is written. Moving a row to another endpoint is a `replace`, which writes
|
|
35
|
+
* the new endpoint first.
|
|
36
|
+
*/
|
|
37
|
+
function onRowEndpoint(tx, id, credential) {
|
|
38
|
+
if (credential.type !== 'api')
|
|
39
|
+
return credential;
|
|
40
|
+
const raw = tx.rosterRow(id);
|
|
41
|
+
if (!isRecord(raw)) {
|
|
42
|
+
if (credential.baseURL === undefined)
|
|
43
|
+
throw refusal(tx.info.operation, id, 'invalid-input', 'api credential needs a valid baseURL');
|
|
44
|
+
return { ...credential, baseURL: credential.baseURL };
|
|
45
|
+
}
|
|
46
|
+
const endpoint = rowEndpoint(raw);
|
|
47
|
+
const baseURL = credential.baseURL?.trim() ?? endpoint.baseURL;
|
|
48
|
+
const authHeader = credential.authHeader ?? endpoint.authHeader;
|
|
49
|
+
if (baseURL !== endpoint.baseURL || authHeader !== endpoint.authHeader)
|
|
50
|
+
throw refusal(tx.info.operation, id, 'endpoint-mismatch', `row ${id} sends its key to another endpoint or header; a key for another endpoint is a replacement`);
|
|
51
|
+
return { ...credential, baseURL, authHeader };
|
|
52
|
+
}
|
|
16
53
|
/**
|
|
17
54
|
* Writes a credential into the state file (one write), stamped with the
|
|
18
55
|
* credential epoch the row's entry holds in `tx` (1 without an entry) and,
|
|
19
56
|
* for a replace, the binding the config is about to get. A rotation is the
|
|
20
|
-
* same lineage: no epoch bump, no identity or quota change.
|
|
57
|
+
* same lineage: no epoch bump, no identity or quota change. An API key must
|
|
58
|
+
* belong to the endpoint the row holds in `tx` (see `onRowEndpoint`).
|
|
21
59
|
*/
|
|
22
|
-
export async function rotateIn(rt, tx, id,
|
|
60
|
+
export async function rotateIn(rt, tx, id, given, extra = {}) {
|
|
61
|
+
const credential = onRowEndpoint(tx, id, given);
|
|
23
62
|
const prior = tx.stateAccount(id);
|
|
24
63
|
const priorStamp = typeof prior?.lastRefreshedAt === 'number'
|
|
25
64
|
? prior.lastRefreshedAt
|
|
@@ -35,17 +74,7 @@ export async function rotateIn(rt, tx, id, credential, extra = {}) {
|
|
|
35
74
|
delete kept.lastQuotaRefreshError;
|
|
36
75
|
delete kept.quota;
|
|
37
76
|
}
|
|
38
|
-
const
|
|
39
|
-
const stored = credential.type === 'api' && isRecord(raw)
|
|
40
|
-
? storedCredential({
|
|
41
|
-
...credential,
|
|
42
|
-
baseURL: String(raw.baseURL ?? credential.baseURL),
|
|
43
|
-
...(raw.authHeader === 'x-api-key' ||
|
|
44
|
-
raw.authHeader === 'authorization-bearer'
|
|
45
|
-
? { authHeader: raw.authHeader }
|
|
46
|
-
: {}),
|
|
47
|
-
}, stamp)
|
|
48
|
-
: storedCredential(credential, stamp);
|
|
77
|
+
const stored = storedCredential(credential, stamp);
|
|
49
78
|
const epoch = tx.entry(id)?.credentialEpoch;
|
|
50
79
|
tx.setStateAccount(id, {
|
|
51
80
|
...kept,
|
|
@@ -59,7 +88,9 @@ function checkInput(operation, id, credential) {
|
|
|
59
88
|
const idIssue = operation === 'add' ? idProblem(id) : undefined;
|
|
60
89
|
if (idIssue)
|
|
61
90
|
throw refusal(operation, id, 'invalid-input', idIssue);
|
|
62
|
-
const credentialIssue = credentialProblem(credential
|
|
91
|
+
const credentialIssue = credentialProblem(credential, {
|
|
92
|
+
baseURLOptional: operation === 'rotate',
|
|
93
|
+
});
|
|
63
94
|
if (credentialIssue)
|
|
64
95
|
throw refusal(operation, id, 'invalid-input', credentialIssue);
|
|
65
96
|
}
|
|
@@ -98,6 +129,14 @@ export async function addRow(rt, input, options = {}) {
|
|
|
98
129
|
const fingerprint = fingerprintOf(credential);
|
|
99
130
|
const same = rows.find((row) => row.invalid === undefined && row.fingerprint === fingerprint);
|
|
100
131
|
if (same) {
|
|
132
|
+
// The same secret is the same credential, so re-adding it rotates
|
|
133
|
+
// that row. An identity or endpoint given with it must match the
|
|
134
|
+
// row's; a different one is refused rather than silently replaced
|
|
135
|
+
// by what the row already holds.
|
|
136
|
+
if (identity !== undefined &&
|
|
137
|
+
same.identity !== undefined &&
|
|
138
|
+
identity !== same.identity)
|
|
139
|
+
throw identityMismatch('add', same.id);
|
|
101
140
|
const stored = await rotateIn(rt, tx, same.id, credential);
|
|
102
141
|
return { id: same.id, outcome: 'rotated', credential: stored };
|
|
103
142
|
}
|
|
@@ -109,6 +148,10 @@ export async function addRow(rt, input, options = {}) {
|
|
|
109
148
|
throw refusal('add', id, 'id-exists', `row ${id} already holds a credential`);
|
|
110
149
|
if (existing.type !== credential.type)
|
|
111
150
|
throw refusal('add', id, 'type-mismatch', `row ${id} is a ${existing.type} row`);
|
|
151
|
+
if (identity !== undefined &&
|
|
152
|
+
existing.identity !== undefined &&
|
|
153
|
+
identity !== existing.identity)
|
|
154
|
+
throw identityMismatch('add', id);
|
|
112
155
|
// A roster row without a credential (left by another writer, or
|
|
113
156
|
// by an add of an earlier version that stopped between its
|
|
114
157
|
// writes): writing the credential now completes it at epoch 1.
|
|
@@ -180,6 +223,11 @@ export async function replaceRow(rt, id, credential, input = {}, options = {}) {
|
|
|
180
223
|
throw keyChanged('replace', id);
|
|
181
224
|
const priorEpoch = tx.entry(id)?.credentialEpoch;
|
|
182
225
|
const credentialEpoch = (typeof priorEpoch === 'number' ? priorEpoch : 1) + 1;
|
|
226
|
+
// An epoch past the safe integer range could equal the one before
|
|
227
|
+
// it, so readers could not tell the new credential from the old.
|
|
228
|
+
// Refused before anything is written; the row keeps its credential.
|
|
229
|
+
if (!isCredentialEpoch(credentialEpoch))
|
|
230
|
+
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`);
|
|
183
231
|
const binding = {
|
|
184
232
|
...(input.identity !== undefined
|
|
185
233
|
? { identity: input.identity }
|
|
@@ -494,9 +542,7 @@ export async function recordRowIdentity(rt, id, identity, attribution, options =
|
|
|
494
542
|
const captured = isRecord(attribution)
|
|
495
543
|
? attribution.credentialEpoch
|
|
496
544
|
: undefined;
|
|
497
|
-
if (
|
|
498
|
-
!Number.isInteger(captured) ||
|
|
499
|
-
captured < 1)
|
|
545
|
+
if (!isCredentialEpoch(captured))
|
|
500
546
|
throw refusal('recordIdentity', id, 'invalid-input', 'the credential epoch the identity lookup was issued for is required');
|
|
501
547
|
const { row: seen } = await readRow(rt, 'recordIdentity', id);
|
|
502
548
|
await locks.acquire(rowLockSpec(rt, seen));
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -25,6 +25,14 @@ export type ApiKeyCredential = {
|
|
|
25
25
|
authHeader?: 'authorization-bearer' | 'x-api-key';
|
|
26
26
|
};
|
|
27
27
|
export type PoolCredential = OAuthCredential | ApiKeyCredential;
|
|
28
|
+
/**
|
|
29
|
+
* What `rotate` takes. Rotation refreshes the secret of the endpoint a row
|
|
30
|
+
* already has, so an API key may leave out `baseURL` (and `authHeader`) to
|
|
31
|
+
* keep the row's; one it gives must equal the row's.
|
|
32
|
+
*/
|
|
33
|
+
export type RotateCredential = OAuthCredential | (Omit<ApiKeyCredential, 'baseURL'> & {
|
|
34
|
+
baseURL?: string;
|
|
35
|
+
});
|
|
28
36
|
/** A credential as stored: an OAuth credential also carries its refresh stamp. */
|
|
29
37
|
export type StoredCredential = (OAuthCredential & {
|
|
30
38
|
lastRefreshedAt?: number;
|
|
@@ -130,6 +138,12 @@ export declare function fingerprintOf(credential: PoolCredential | StoredCredent
|
|
|
130
138
|
*/
|
|
131
139
|
export declare function credentialDigest(credential: PoolCredential | StoredCredential): string;
|
|
132
140
|
export declare function stampFor(credential: PoolCredential | StoredCredential, credentialEpoch: number, binding?: CredentialBinding): CredentialStamp;
|
|
141
|
+
/**
|
|
142
|
+
* A credential epoch is a positive safe integer. Above `MAX_SAFE_INTEGER`,
|
|
143
|
+
* adding one may give back the same number, so a replace would not move the
|
|
144
|
+
* epoch and nothing could tell the old credential's work from the new one's.
|
|
145
|
+
*/
|
|
146
|
+
export declare function isCredentialEpoch(value: unknown): value is number;
|
|
133
147
|
/** A well-formed stamp, or undefined for anything else (which is ignored). */
|
|
134
148
|
export declare function parseStamp(raw: unknown): CredentialStamp | undefined;
|
|
135
149
|
/** A parsed file, or the reason it could not be parsed. */
|
|
@@ -183,4 +197,6 @@ export declare function rosterRowFor(input: {
|
|
|
183
197
|
/** The state-file fields a credential occupies, in the older readers' names. */
|
|
184
198
|
export declare function stateFieldsFor(credential: PoolCredential, lastRefreshedAt: number | undefined): Record<string, unknown>;
|
|
185
199
|
export declare function storedCredential(credential: PoolCredential, lastRefreshedAt: number | undefined): StoredCredential;
|
|
186
|
-
export declare function credentialProblem(credential: unknown
|
|
200
|
+
export declare function credentialProblem(credential: unknown, options?: {
|
|
201
|
+
baseURLOptional?: boolean;
|
|
202
|
+
}): string | undefined;
|
package/dist/store/schema.js
CHANGED
|
@@ -76,12 +76,20 @@ export function stampFor(credential, credentialEpoch, binding) {
|
|
|
76
76
|
...(binding ? { binding: { ...binding } } : {}),
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* A credential epoch is a positive safe integer. Above `MAX_SAFE_INTEGER`,
|
|
81
|
+
* adding one may give back the same number, so a replace would not move the
|
|
82
|
+
* epoch and nothing could tell the old credential's work from the new one's.
|
|
83
|
+
*/
|
|
84
|
+
export function isCredentialEpoch(value) {
|
|
85
|
+
return Number.isSafeInteger(value) && value >= 1;
|
|
86
|
+
}
|
|
79
87
|
/** A well-formed stamp, or undefined for anything else (which is ignored). */
|
|
80
88
|
export function parseStamp(raw) {
|
|
81
89
|
if (!isRecord(raw))
|
|
82
90
|
return undefined;
|
|
83
91
|
const epoch = raw.credentialEpoch;
|
|
84
|
-
if (
|
|
92
|
+
if (!isCredentialEpoch(epoch))
|
|
85
93
|
return undefined;
|
|
86
94
|
if (typeof raw.digest !== 'string')
|
|
87
95
|
return undefined;
|
|
@@ -190,7 +198,7 @@ function parseEntry(raw, codec) {
|
|
|
190
198
|
if (!isRecord(raw))
|
|
191
199
|
return undefined;
|
|
192
200
|
const epoch = raw.credentialEpoch;
|
|
193
|
-
if (
|
|
201
|
+
if (!isCredentialEpoch(epoch))
|
|
194
202
|
return undefined;
|
|
195
203
|
if ('needsFirstReading' in raw && typeof raw.needsFirstReading !== 'boolean')
|
|
196
204
|
return undefined;
|
|
@@ -395,7 +403,7 @@ export function storedCredential(credential, lastRefreshedAt) {
|
|
|
395
403
|
...(lastRefreshedAt !== undefined ? { lastRefreshedAt } : {}),
|
|
396
404
|
};
|
|
397
405
|
}
|
|
398
|
-
export function credentialProblem(credential) {
|
|
406
|
+
export function credentialProblem(credential, options = {}) {
|
|
399
407
|
if (!isRecord(credential))
|
|
400
408
|
return 'credential must be an object';
|
|
401
409
|
if (credential.type === 'oauth') {
|
|
@@ -408,7 +416,8 @@ export function credentialProblem(credential) {
|
|
|
408
416
|
return 'api credential needs an api key';
|
|
409
417
|
if (credential.apiKey.trim() !== credential.apiKey)
|
|
410
418
|
return 'api key must not carry surrounding whitespace';
|
|
411
|
-
|
|
419
|
+
const omitted = options.baseURLOptional && credential.baseURL === undefined;
|
|
420
|
+
if (!omitted && !isValidBaseURL(credential.baseURL))
|
|
412
421
|
return 'api credential needs a valid baseURL';
|
|
413
422
|
return undefined;
|
|
414
423
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/common-auth",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
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": {
|