@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/schema.js
CHANGED
|
@@ -18,6 +18,11 @@ export const REFRESH_STAMP_TOLERANCE_MS = 5 * 60_000;
|
|
|
18
18
|
* credential beside it belongs to. Older readers ignore it.
|
|
19
19
|
*/
|
|
20
20
|
export const CREDENTIAL_STAMP_KEY = POOL_KEY;
|
|
21
|
+
/**
|
|
22
|
+
* Key, inside a state-file account entry, of the provider state kept beside
|
|
23
|
+
* the credential. Older readers ignore it.
|
|
24
|
+
*/
|
|
25
|
+
export const PROVIDER_STATE_KEY = 'commonAuthProviderState';
|
|
21
26
|
export function isRecord(value) {
|
|
22
27
|
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
23
28
|
}
|
|
@@ -112,8 +117,49 @@ export function stampFor(credential, credentialEpoch, binding, options = {}) {
|
|
|
112
117
|
dispatch: dispatchDigest(credential),
|
|
113
118
|
binding: { ...binding },
|
|
114
119
|
...(options.replace ? { replace: true } : {}),
|
|
120
|
+
...(options.providerState !== undefined
|
|
121
|
+
? { providerState: options.providerState }
|
|
122
|
+
: {}),
|
|
115
123
|
};
|
|
116
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* The digest a stamp carries for a provider state: of its credential-bound
|
|
127
|
+
* part (`ProviderStateCodec.credentialBound`, the whole value without it), as
|
|
128
|
+
* it serializes. The store only ever stores values that survive a JSON round
|
|
129
|
+
* trip unchanged, so the digest of a value read back from disk equals the one
|
|
130
|
+
* computed when it was written. Its input prefix differs from every other
|
|
131
|
+
* digest the store writes.
|
|
132
|
+
*/
|
|
133
|
+
export function providerStateDigest(codec, value) {
|
|
134
|
+
const bound = codec?.credentialBound ? codec.credentialBound(value) : value;
|
|
135
|
+
return createHash('sha256')
|
|
136
|
+
.update(`provider-state\0${JSON.stringify(bound ?? null)}`)
|
|
137
|
+
.digest('hex');
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The provider-state digest of the stamp in a state-file account entry, when
|
|
141
|
+
* that stamp binds it to this row: written with this credential (its lineage
|
|
142
|
+
* digest matches), at this credential epoch, naming this recorded identity.
|
|
143
|
+
* Whether the value beside it still has that digest is checked apart, by
|
|
144
|
+
* `providerStateFields`; this alone is what a write that keeps the value
|
|
145
|
+
* carries into the stamp it writes, so a value no stamp of this store bound
|
|
146
|
+
* to the row stays unbound rather than being vouched for, and one edited
|
|
147
|
+
* after it was bound stays detectably edited.
|
|
148
|
+
*/
|
|
149
|
+
export function boundProviderStateDigest(account, credential, credentialEpoch, identity) {
|
|
150
|
+
if (!credential || credentialEpoch === undefined)
|
|
151
|
+
return undefined;
|
|
152
|
+
if (!isRecord(account) || !Object.hasOwn(account, PROVIDER_STATE_KEY))
|
|
153
|
+
return undefined;
|
|
154
|
+
const stamp = parseStamp(account[CREDENTIAL_STAMP_KEY]);
|
|
155
|
+
if (!stamp ||
|
|
156
|
+
stamp.providerState === undefined ||
|
|
157
|
+
stamp.digest !== credentialDigest(credential) ||
|
|
158
|
+
stamp.credentialEpoch !== credentialEpoch ||
|
|
159
|
+
stamp.binding?.identity !== identity)
|
|
160
|
+
return undefined;
|
|
161
|
+
return stamp.providerState;
|
|
162
|
+
}
|
|
117
163
|
/**
|
|
118
164
|
* A credential epoch is a positive safe integer. Above `MAX_SAFE_INTEGER`,
|
|
119
165
|
* adding one may give back the same number, so a replace would not move the
|
|
@@ -135,15 +181,20 @@ export function parseStamp(raw) {
|
|
|
135
181
|
return undefined;
|
|
136
182
|
if ('replace' in raw && raw.replace !== true)
|
|
137
183
|
return undefined;
|
|
184
|
+
if ('providerState' in raw && typeof raw.providerState !== 'string')
|
|
185
|
+
return undefined;
|
|
138
186
|
const marks = {
|
|
139
187
|
...(typeof raw.dispatch === 'string' ? { dispatch: raw.dispatch } : {}),
|
|
140
188
|
...(raw.replace === true ? { replace: true } : {}),
|
|
189
|
+
...(typeof raw.providerState === 'string'
|
|
190
|
+
? { providerState: raw.providerState }
|
|
191
|
+
: {}),
|
|
141
192
|
};
|
|
142
|
-
// Every stamp that carries a dispatch digest
|
|
143
|
-
// without is not a stamp this store writes,
|
|
144
|
-
// identity unchecked.
|
|
193
|
+
// Every stamp that carries a dispatch digest or a provider-state digest is
|
|
194
|
+
// written with a binding; one without is not a stamp this store writes,
|
|
195
|
+
// and it would leave the row's identity unchecked.
|
|
145
196
|
if (!('binding' in raw))
|
|
146
|
-
return 'dispatch' in raw || 'replace' in raw
|
|
197
|
+
return 'dispatch' in raw || 'replace' in raw || 'providerState' in raw
|
|
147
198
|
? undefined
|
|
148
199
|
: { credentialEpoch: epoch, digest: raw.digest };
|
|
149
200
|
const binding = raw.binding;
|
|
@@ -389,7 +440,7 @@ export function setEntryIn(config, id, entry) {
|
|
|
389
440
|
* malformed per-row entry makes that one row invalid (never a candidate) and
|
|
390
441
|
* blocks nothing else.
|
|
391
442
|
*/
|
|
392
|
-
export function buildRawRows(config, state, codec) {
|
|
443
|
+
export function buildRawRows(config, state, codec, providerCodec) {
|
|
393
444
|
const entries = entriesOf(config);
|
|
394
445
|
const stateAccounts = stateAccountsOf(state);
|
|
395
446
|
const seen = new Set();
|
|
@@ -419,6 +470,9 @@ export function buildRawRows(config, state, codec) {
|
|
|
419
470
|
const entry = hasEntry ? parseEntry(entries[id], codec) : undefined;
|
|
420
471
|
const credential = credentialFor(raw, stateAccounts[id]);
|
|
421
472
|
const enabled = raw.enabled !== false;
|
|
473
|
+
// A row without a per-row entry is at credential epoch 1 (the epoch the
|
|
474
|
+
// store stamps and later gives it), so its stamp is checked against 1.
|
|
475
|
+
const stampEpoch = entry ? entry.credentialEpoch : hasEntry ? undefined : 1;
|
|
422
476
|
const identity = typeof raw.accountId === 'string' && raw.accountId
|
|
423
477
|
? raw.accountId
|
|
424
478
|
: undefined;
|
|
@@ -440,9 +494,8 @@ export function buildRawRows(config, state, codec) {
|
|
|
440
494
|
? { disabledReason: entry.disabledReason }
|
|
441
495
|
: {}),
|
|
442
496
|
...(entry && 'quota' in entry ? { quota: entry.quota } : {}),
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
stamp: stampStatusOf(credential, entry ? entry.credentialEpoch : hasEntry ? undefined : 1, identity, stateAccounts[id]),
|
|
497
|
+
...providerStateFields(stateAccounts[id], credential, stampEpoch, identity, providerCodec),
|
|
498
|
+
stamp: stampStatusOf(credential, stampEpoch, identity, stateAccounts[id]),
|
|
446
499
|
};
|
|
447
500
|
if (hasEntry && !entry) {
|
|
448
501
|
row.invalid = 'entry';
|
|
@@ -454,6 +507,30 @@ export function buildRawRows(config, state, codec) {
|
|
|
454
507
|
}
|
|
455
508
|
return rows;
|
|
456
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* The provider-state fields of a loaded row. A value on disk is shown only
|
|
512
|
+
* when the stamp beside the credential binds a provider state to this row's
|
|
513
|
+
* credential, epoch and identity (see `boundProviderStateDigest`), the codec
|
|
514
|
+
* accepts the value, and the value's credential-bound part has the digest
|
|
515
|
+
* that stamp names. Otherwise the row says why it shows none and stays
|
|
516
|
+
* usable: the value is the plugin's own derived data, which it can rebuild,
|
|
517
|
+
* and the credential beside it may well be sound.
|
|
518
|
+
*/
|
|
519
|
+
function providerStateFields(account, credential, credentialEpoch, identity, codec) {
|
|
520
|
+
if (!isRecord(account) || !Object.hasOwn(account, PROVIDER_STATE_KEY))
|
|
521
|
+
return {};
|
|
522
|
+
const digest = boundProviderStateDigest(account, credential, credentialEpoch, identity);
|
|
523
|
+
if (digest === undefined)
|
|
524
|
+
return { providerStateDropped: 'uncovered' };
|
|
525
|
+
const value = account[PROVIDER_STATE_KEY];
|
|
526
|
+
// The value is validated before it is projected, so the projection only
|
|
527
|
+
// ever sees a value of the shape the codec accepts.
|
|
528
|
+
if (!codec?.validate(value))
|
|
529
|
+
return { providerStateDropped: 'invalid' };
|
|
530
|
+
if (providerStateDigest(codec, value) !== digest)
|
|
531
|
+
return { providerStateDropped: 'uncovered' };
|
|
532
|
+
return { providerState: value };
|
|
533
|
+
}
|
|
457
534
|
/** The row-lock key: recorded wire identity when known, else the local id. */
|
|
458
535
|
export function rowLockKey(row) {
|
|
459
536
|
return row.identity ?? row.id;
|
package/dist/store/torn.d.ts
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { type RowEditor } from './identity.js';
|
|
2
|
-
import { type CredentialBinding, type CredentialStamp, type PoolRow, type QuotaCodec } from './schema.js';
|
|
2
|
+
import { type CredentialBinding, type CredentialStamp, type PoolRow, type ProviderStateCodec, type QuotaCodec } from './schema.js';
|
|
3
|
+
/** Key, inside a credential stamp, of an attributed enable or disable. */
|
|
4
|
+
export declare const TRANSITION_STAMP_KEY = "transition";
|
|
5
|
+
/**
|
|
6
|
+
* Key, inside a per-row config entry, of the mark of the last transition the
|
|
7
|
+
* config carries out. Older readers ignore it.
|
|
8
|
+
*/
|
|
9
|
+
export declare const TRANSITION_MARK_KEY = "transitionMark";
|
|
10
|
+
/**
|
|
11
|
+
* An attributed enable or disable as its state write records it: `mark` is
|
|
12
|
+
* unique to that write, `enabled` is the flag it sets, and `reason` the
|
|
13
|
+
* disabled reason (a disable's only).
|
|
14
|
+
*/
|
|
15
|
+
export interface StampedTransition {
|
|
16
|
+
mark: string;
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
reason?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Carries a transition out in a config being edited: the row's enabled flag
|
|
22
|
+
* and reason as the transition says, and its mark recorded in the entry (a
|
|
23
|
+
* row without an entry gets one at epoch 1, as `disableIn` gives it). An
|
|
24
|
+
* enable then applies the duplicate-identity rule, as every write that
|
|
25
|
+
* enables an OAuth row with a known identity does: the earlier row in roster
|
|
26
|
+
* order keeps the identity.
|
|
27
|
+
*/
|
|
28
|
+
export declare function applyTransition(editor: RowEditor, id: string, transition: StampedTransition): void;
|
|
3
29
|
/** How a row left between the two writes of an operation is completed. */
|
|
4
30
|
export type TornCompletion = {
|
|
5
31
|
kind: 'replace';
|
|
@@ -9,6 +35,9 @@ export type TornCompletion = {
|
|
|
9
35
|
} | {
|
|
10
36
|
kind: 'identity';
|
|
11
37
|
identity: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'transition';
|
|
40
|
+
transition: StampedTransition;
|
|
12
41
|
};
|
|
13
42
|
/** Rows left between the two writes of an operation, by row id. */
|
|
14
43
|
export declare function tornStamps(config: Record<string, unknown>, state: Record<string, unknown>, codec: QuotaCodec, options?: {
|
|
@@ -45,4 +74,5 @@ export declare function completeTornRows(config: Record<string, unknown>, state:
|
|
|
45
74
|
*/
|
|
46
75
|
export declare function loadRows(config: Record<string, unknown>, state: Record<string, unknown>, codec: QuotaCodec, options?: {
|
|
47
76
|
requireCredentialStamps?: boolean;
|
|
77
|
+
providerState?: ProviderStateCodec;
|
|
48
78
|
}): PoolRow[];
|
package/dist/store/torn.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { disableIdentityDuplicates, recordIdentityIn, } from './identity.js';
|
|
1
|
+
import { disableIdentityDuplicates, disableIn, enableIn, recordIdentityIn, } from './identity.js';
|
|
2
2
|
import { buildRawRows, CREDENTIAL_STAMP_KEY, credentialDigest, dispatchDigest, entryIn, isRecord, parseStamp, rosterRowIn, setEntryIn, } from './schema.js';
|
|
3
3
|
/*
|
|
4
4
|
* A replace changes both files: the state file gets the new credential and
|
|
@@ -46,7 +46,69 @@ import { buildRawRows, CREDENTIAL_STAMP_KEY, credentialDigest, dispatchDigest, e
|
|
|
46
46
|
* exactly (digest and dispatch digest), that names an identity the config
|
|
47
47
|
* does not record; that is completed forward the same way, by recording the
|
|
48
48
|
* identity.
|
|
49
|
+
*
|
|
50
|
+
* An attributed `disable` or `enable` that also changes the provider state
|
|
51
|
+
* follows the same order. Its state write carries the new value and, in the
|
|
52
|
+
* stamp, the transition (`transition: {mark, enabled, reason?}`, see
|
|
53
|
+
* `StampedTransition`); its config write then flips the row and records the
|
|
54
|
+
* transition's mark in the row's entry (`transitionMark`). A stamp bound to
|
|
55
|
+
* the row (this credential, epoch and identity, the binding that also shows
|
|
56
|
+
* the provider state beside it) whose transition mark the entry does not
|
|
57
|
+
* record is such a write stopped between its two writes, and is completed
|
|
58
|
+
* forward the same way: readers are shown the row disabled or enabled as the
|
|
59
|
+
* transition says, beside the value written with it. Once the config records
|
|
60
|
+
* the mark the transition says nothing more, so a later `enable` or
|
|
61
|
+
* `disable` of the row is never undone by it; a credential write drops it
|
|
62
|
+
* with the rest of the old stamp.
|
|
63
|
+
*/
|
|
64
|
+
/** Key, inside a credential stamp, of an attributed enable or disable. */
|
|
65
|
+
export const TRANSITION_STAMP_KEY = 'transition';
|
|
66
|
+
/**
|
|
67
|
+
* Key, inside a per-row config entry, of the mark of the last transition the
|
|
68
|
+
* config carries out. Older readers ignore it.
|
|
49
69
|
*/
|
|
70
|
+
export const TRANSITION_MARK_KEY = 'transitionMark';
|
|
71
|
+
/** The well-formed transition in a raw stamp, or undefined. */
|
|
72
|
+
function stampedTransition(rawStamp) {
|
|
73
|
+
if (!isRecord(rawStamp))
|
|
74
|
+
return undefined;
|
|
75
|
+
const raw = rawStamp[TRANSITION_STAMP_KEY];
|
|
76
|
+
if (!isRecord(raw))
|
|
77
|
+
return undefined;
|
|
78
|
+
if (typeof raw.mark !== 'string' || raw.mark.length === 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
if (raw.enabled === true)
|
|
81
|
+
return { mark: raw.mark, enabled: true };
|
|
82
|
+
if (raw.enabled === false && typeof raw.reason === 'string')
|
|
83
|
+
return { mark: raw.mark, enabled: false, reason: raw.reason };
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Carries a transition out in a config being edited: the row's enabled flag
|
|
88
|
+
* and reason as the transition says, and its mark recorded in the entry (a
|
|
89
|
+
* row without an entry gets one at epoch 1, as `disableIn` gives it). An
|
|
90
|
+
* enable then applies the duplicate-identity rule, as every write that
|
|
91
|
+
* enables an OAuth row with a known identity does: the earlier row in roster
|
|
92
|
+
* order keeps the identity.
|
|
93
|
+
*/
|
|
94
|
+
export function applyTransition(editor, id, transition) {
|
|
95
|
+
if (transition.enabled)
|
|
96
|
+
enableIn(editor, id);
|
|
97
|
+
else
|
|
98
|
+
disableIn(editor, id, transition.reason ?? '');
|
|
99
|
+
if (!editor.rosterRow(id))
|
|
100
|
+
return;
|
|
101
|
+
const entry = editor.entry(id) ?? {
|
|
102
|
+
credentialEpoch: 1,
|
|
103
|
+
needsFirstReading: true,
|
|
104
|
+
};
|
|
105
|
+
editor.setEntry(id, { ...entry, [TRANSITION_MARK_KEY]: transition.mark });
|
|
106
|
+
if (!transition.enabled)
|
|
107
|
+
return;
|
|
108
|
+
const row = editor.rows().find((candidate) => candidate.id === id);
|
|
109
|
+
if (row?.type === 'oauth' && row.identity !== undefined)
|
|
110
|
+
disableIdentityDuplicates(editor, row.identity);
|
|
111
|
+
}
|
|
50
112
|
/**
|
|
51
113
|
* The credential a torn replace leaves once its config write lands: the
|
|
52
114
|
* credential loaded beside the stamp, with the endpoint the stamp's binding
|
|
@@ -122,6 +184,13 @@ export function tornStamps(config, state, codec, options = {}) {
|
|
|
122
184
|
row.identity === undefined) {
|
|
123
185
|
torn.set(row.id, { kind: 'identity', identity: stamp.binding.identity });
|
|
124
186
|
}
|
|
187
|
+
else if (stamp.credentialEpoch === epoch &&
|
|
188
|
+
stamp.binding.identity === row.identity) {
|
|
189
|
+
const transition = stampedTransition(account[CREDENTIAL_STAMP_KEY]);
|
|
190
|
+
if (transition &&
|
|
191
|
+
entryIn(config, row.id)?.[TRANSITION_MARK_KEY] !== transition.mark)
|
|
192
|
+
torn.set(row.id, { kind: 'transition', transition });
|
|
193
|
+
}
|
|
125
194
|
}
|
|
126
195
|
return torn;
|
|
127
196
|
}
|
|
@@ -175,6 +244,8 @@ export function completeTornRows(config, state, codec, options = {}) {
|
|
|
175
244
|
for (const [id, completion] of completions) {
|
|
176
245
|
if (completion.kind === 'identity')
|
|
177
246
|
recordIdentityIn(editor, id, completion.identity);
|
|
247
|
+
else if (completion.kind === 'transition')
|
|
248
|
+
applyTransition(editor, id, completion.transition);
|
|
178
249
|
else if (completion.stamp.binding.identity !== undefined)
|
|
179
250
|
disableIdentityDuplicates(editor, completion.stamp.binding.identity);
|
|
180
251
|
}
|
|
@@ -192,7 +263,10 @@ export function completeTornRows(config, state, codec, options = {}) {
|
|
|
192
263
|
*/
|
|
193
264
|
export function loadRows(config, state, codec, options = {}) {
|
|
194
265
|
const { config: whole, torn } = completeTornRows(config, state, codec, options);
|
|
195
|
-
|
|
266
|
+
// A torn replace wrote its provider state beside the new credential, under
|
|
267
|
+
// the stamp naming the new epoch, so the completed row shows the new
|
|
268
|
+
// credential with the state written for it, never with the old one.
|
|
269
|
+
const rows = buildRawRows(whole, state, codec, options.providerState);
|
|
196
270
|
for (const row of rows) {
|
|
197
271
|
if (torn.includes(row.id)) {
|
|
198
272
|
row.torn = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/common-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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": {
|