@cortexkit/antigravity-auth-core 2.2.1 → 3.0.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/README.md +24 -2
- package/dist/abort-forwarding.d.ts +3 -0
- package/dist/abort-forwarding.d.ts.map +1 -0
- package/dist/abort-forwarding.js +19 -0
- package/dist/abort-forwarding.js.map +1 -0
- package/dist/account-identity.d.ts +52 -0
- package/dist/account-identity.d.ts.map +1 -0
- package/dist/account-identity.js +66 -0
- package/dist/account-identity.js.map +1 -0
- package/dist/account-manager.d.ts +228 -43
- package/dist/account-manager.d.ts.map +1 -1
- package/dist/account-manager.js +942 -903
- package/dist/account-manager.js.map +1 -1
- package/dist/account-migration.d.ts +309 -0
- package/dist/account-migration.d.ts.map +1 -0
- package/dist/account-migration.js +2282 -0
- package/dist/account-migration.js.map +1 -0
- package/dist/account-repository-codecs.d.ts +82 -0
- package/dist/account-repository-codecs.d.ts.map +1 -0
- package/dist/account-repository-codecs.js +609 -0
- package/dist/account-repository-codecs.js.map +1 -0
- package/dist/account-repository-types.d.ts +831 -0
- package/dist/account-repository-types.d.ts.map +1 -0
- package/dist/account-repository-types.js +137 -0
- package/dist/account-repository-types.js.map +1 -0
- package/dist/account-repository.d.ts +442 -0
- package/dist/account-repository.d.ts.map +1 -0
- package/dist/account-repository.js +2253 -0
- package/dist/account-repository.js.map +1 -0
- package/dist/account-selector.d.ts +470 -0
- package/dist/account-selector.d.ts.map +1 -0
- package/dist/account-selector.js +1383 -0
- package/dist/account-selector.js.map +1 -0
- package/dist/account-storage.d.ts.map +1 -1
- package/dist/account-storage.js +5 -0
- package/dist/account-storage.js.map +1 -1
- package/dist/agy-transport.d.ts.map +1 -1
- package/dist/agy-transport.js +21 -35
- package/dist/agy-transport.js.map +1 -1
- package/dist/antigravity-command-menu.d.ts +171 -0
- package/dist/antigravity-command-menu.d.ts.map +1 -0
- package/dist/antigravity-command-menu.js +523 -0
- package/dist/antigravity-command-menu.js.map +1 -0
- package/dist/atomic-write.d.ts +18 -1
- package/dist/atomic-write.d.ts.map +1 -1
- package/dist/atomic-write.js +15 -3
- package/dist/atomic-write.js.map +1 -1
- package/dist/common-auth-embedded/NOTICE.txt +21 -0
- package/dist/common-auth-embedded/auth-menu/accounts.d.ts +97 -0
- package/dist/common-auth-embedded/auth-menu/accounts.js +387 -0
- package/dist/common-auth-embedded/auth-menu/ansi.d.ts +26 -0
- package/dist/common-auth-embedded/auth-menu/ansi.js +98 -0
- package/dist/common-auth-embedded/auth-menu/confirm.d.ts +7 -0
- package/dist/common-auth-embedded/auth-menu/confirm.js +21 -0
- package/dist/common-auth-embedded/auth-menu/doctor.d.ts +52 -0
- package/dist/common-auth-embedded/auth-menu/doctor.js +81 -0
- package/dist/common-auth-embedded/auth-menu/index.d.ts +17 -0
- package/dist/common-auth-embedded/auth-menu/index.js +12 -0
- package/dist/common-auth-embedded/auth-menu/login.d.ts +55 -0
- package/dist/common-auth-embedded/auth-menu/login.js +83 -0
- package/dist/common-auth-embedded/auth-menu/menu.d.ts +59 -0
- package/dist/common-auth-embedded/auth-menu/menu.js +73 -0
- package/dist/common-auth-embedded/auth-menu/opencode-v1.d.ts +49 -0
- package/dist/common-auth-embedded/auth-menu/opencode-v1.js +47 -0
- package/dist/common-auth-embedded/auth-menu/select.d.ts +21 -0
- package/dist/common-auth-embedded/auth-menu/select.js +159 -0
- package/dist/common-auth-embedded/auth-menu/terminal.d.ts +43 -0
- package/dist/common-auth-embedded/auth-menu/terminal.js +19 -0
- package/dist/common-auth-embedded/claustrum/consumer.d.ts +102 -0
- package/dist/common-auth-embedded/claustrum/consumer.js +298 -0
- package/dist/common-auth-embedded/claustrum/custody.d.ts +175 -0
- package/dist/common-auth-embedded/claustrum/custody.js +354 -0
- package/dist/common-auth-embedded/claustrum/enrollment.d.ts +158 -0
- package/dist/common-auth-embedded/claustrum/enrollment.js +759 -0
- package/dist/common-auth-embedded/claustrum/errors.d.ts +16 -0
- package/dist/common-auth-embedded/claustrum/errors.js +8 -0
- package/dist/common-auth-embedded/claustrum/host-slot.d.ts +39 -0
- package/dist/common-auth-embedded/claustrum/host-slot.js +72 -0
- package/dist/common-auth-embedded/claustrum/index.d.ts +18 -0
- package/dist/common-auth-embedded/claustrum/index.js +22 -0
- package/dist/common-auth-embedded/claustrum/interlock.d.ts +27 -0
- package/dist/common-auth-embedded/claustrum/interlock.js +29 -0
- package/dist/common-auth-embedded/claustrum/roster.d.ts +208 -0
- package/dist/common-auth-embedded/claustrum/roster.js +536 -0
- package/dist/common-auth-embedded/commands/builtins.d.ts +71 -0
- package/dist/common-auth-embedded/commands/builtins.js +534 -0
- package/dist/common-auth-embedded/commands/index.d.ts +10 -0
- package/dist/common-auth-embedded/commands/index.js +5 -0
- package/dist/common-auth-embedded/commands/menu.d.ts +61 -0
- package/dist/common-auth-embedded/commands/menu.js +300 -0
- package/dist/common-auth-embedded/commands/model.d.ts +238 -0
- package/dist/common-auth-embedded/commands/model.js +16 -0
- package/dist/common-auth-embedded/commands/pi.d.ts +23 -0
- package/dist/common-auth-embedded/commands/pi.js +206 -0
- package/dist/common-auth-embedded/commands/seam.d.ts +72 -0
- package/dist/common-auth-embedded/commands/seam.js +301 -0
- package/dist/common-auth-embedded/fs/atomic-write.d.ts +20 -0
- package/dist/common-auth-embedded/fs/atomic-write.js +80 -0
- package/dist/common-auth-embedded/fs/index.d.ts +7 -0
- package/dist/common-auth-embedded/fs/index.js +6 -0
- package/dist/common-auth-embedded/fs/lock-constants.d.ts +14 -0
- package/dist/common-auth-embedded/fs/lock-constants.js +14 -0
- package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +33 -0
- package/dist/common-auth-embedded/fs/refresh-file-lock.js +501 -0
- package/dist/common-auth-embedded/fs/with-lock.d.ts +33 -0
- package/dist/common-auth-embedded/fs/with-lock.js +48 -0
- package/dist/common-auth-embedded/logger/capture-sink.d.ts +14 -0
- package/dist/common-auth-embedded/logger/capture-sink.js +13 -0
- package/dist/common-auth-embedded/logger/engine.d.ts +100 -0
- package/dist/common-auth-embedded/logger/engine.js +246 -0
- package/dist/common-auth-embedded/logger/index.d.ts +6 -0
- package/dist/common-auth-embedded/logger/index.js +3 -0
- package/dist/common-auth-embedded/logger/redact.d.ts +11 -0
- package/dist/common-auth-embedded/logger/redact.js +83 -0
- package/dist/common-auth-embedded/pi-slot/index.d.ts +3 -0
- package/dist/common-auth-embedded/pi-slot/index.js +2 -0
- package/dist/common-auth-embedded/pi-slot/lock.js +212 -0
- package/dist/common-auth-embedded/pi-slot/slot.d.ts +49 -0
- package/dist/common-auth-embedded/pi-slot/slot.js +383 -0
- package/dist/common-auth-embedded/quota/codec.d.ts +11 -0
- package/dist/common-auth-embedded/quota/codec.js +12 -0
- package/dist/common-auth-embedded/quota/format.d.ts +46 -0
- package/dist/common-auth-embedded/quota/format.js +138 -0
- package/dist/common-auth-embedded/quota/index.d.ts +10 -0
- package/dist/common-auth-embedded/quota/index.js +5 -0
- package/dist/common-auth-embedded/quota/map.d.ts +63 -0
- package/dist/common-auth-embedded/quota/map.js +105 -0
- package/dist/common-auth-embedded/quota/merge.d.ts +48 -0
- package/dist/common-auth-embedded/quota/merge.js +162 -0
- package/dist/common-auth-embedded/quota/projection.d.ts +64 -0
- package/dist/common-auth-embedded/quota/projection.js +134 -0
- package/dist/common-auth-embedded/routing/admission.d.ts +93 -0
- package/dist/common-auth-embedded/routing/admission.js +142 -0
- package/dist/common-auth-embedded/routing/index.d.ts +8 -0
- package/dist/common-auth-embedded/routing/index.js +4 -0
- package/dist/common-auth-embedded/routing/ordered.d.ts +47 -0
- package/dist/common-auth-embedded/routing/ordered.js +58 -0
- package/dist/common-auth-embedded/routing/pins.d.ts +22 -0
- package/dist/common-auth-embedded/routing/pins.js +34 -0
- package/dist/common-auth-embedded/routing/sticky.d.ts +158 -0
- package/dist/common-auth-embedded/routing/sticky.js +339 -0
- package/dist/common-auth-embedded/source-output.json +1090 -0
- package/dist/common-auth-embedded/store/attribution.d.ts +28 -0
- package/dist/common-auth-embedded/store/attribution.js +60 -0
- package/dist/common-auth-embedded/store/errors.d.ts +55 -0
- package/dist/common-auth-embedded/store/errors.js +38 -0
- package/dist/common-auth-embedded/store/hooks.d.ts +12 -0
- package/dist/common-auth-embedded/store/hooks.js +34 -0
- package/dist/common-auth-embedded/store/identity.d.ts +39 -0
- package/dist/common-auth-embedded/store/identity.js +79 -0
- package/dist/common-auth-embedded/store/index.d.ts +21 -0
- package/dist/common-auth-embedded/store/index.js +7 -0
- package/dist/common-auth-embedded/store/mutate.d.ts +189 -0
- package/dist/common-auth-embedded/store/mutate.js +500 -0
- package/dist/common-auth-embedded/store/pool.d.ts +196 -0
- package/dist/common-auth-embedded/store/pool.js +120 -0
- package/dist/common-auth-embedded/store/provider-state.d.ts +131 -0
- package/dist/common-auth-embedded/store/provider-state.js +226 -0
- package/dist/common-auth-embedded/store/publication.d.ts +47 -0
- package/dist/common-auth-embedded/store/publication.js +304 -0
- package/dist/common-auth-embedded/store/pull.d.ts +36 -0
- package/dist/common-auth-embedded/store/pull.js +143 -0
- package/dist/common-auth-embedded/store/refresh-lock.d.ts +63 -0
- package/dist/common-auth-embedded/store/refresh-lock.js +136 -0
- package/dist/common-auth-embedded/store/refresh.d.ts +72 -0
- package/dist/common-auth-embedded/store/refresh.js +225 -0
- package/dist/common-auth-embedded/store/reserved.js +13 -0
- package/dist/common-auth-embedded/store/rows.d.ts +313 -0
- package/dist/common-auth-embedded/store/rows.js +1258 -0
- package/dist/common-auth-embedded/store/runtime.d.ts +36 -0
- package/dist/common-auth-embedded/store/runtime.js +68 -0
- package/dist/common-auth-embedded/store/schema.d.ts +443 -0
- package/dist/common-auth-embedded/store/schema.js +754 -0
- package/dist/common-auth-embedded/store/settings.d.ts +63 -0
- package/dist/common-auth-embedded/store/settings.js +120 -0
- package/dist/common-auth-embedded/store/torn.d.ts +78 -0
- package/dist/common-auth-embedded/store/torn.js +294 -0
- package/dist/common-auth-embedded/util/any-signal.js +42 -0
- package/dist/common-auth-runtime.d.ts +73 -0
- package/dist/common-auth-runtime.d.ts.map +1 -0
- package/dist/common-auth-runtime.js +65 -0
- package/dist/common-auth-runtime.js.map +1 -0
- package/dist/credential-redaction.d.ts +13 -0
- package/dist/credential-redaction.d.ts.map +1 -0
- package/dist/credential-redaction.js +230 -0
- package/dist/credential-redaction.js.map +1 -0
- package/dist/fetch-timeout.d.ts +4 -2
- package/dist/fetch-timeout.d.ts.map +1 -1
- package/dist/fetch-timeout.js +121 -14
- package/dist/fetch-timeout.js.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/quota-manager.d.ts.map +1 -1
- package/dist/quota-manager.js +35 -10
- package/dist/quota-manager.js.map +1 -1
- package/dist/quota-presentation.d.ts +10 -0
- package/dist/quota-presentation.d.ts.map +1 -0
- package/dist/quota-presentation.js +60 -0
- package/dist/quota-presentation.js.map +1 -0
- package/dist/quota-types.d.ts +2 -0
- package/dist/quota-types.d.ts.map +1 -1
- package/dist/rotation.d.ts +8 -0
- package/dist/rotation.d.ts.map +1 -1
- package/dist/rotation.js +24 -0
- package/dist/rotation.js.map +1 -1
- package/dist/transform/claude.d.ts +7 -3
- package/dist/transform/claude.d.ts.map +1 -1
- package/dist/transform/claude.js +6 -2
- package/dist/transform/claude.js.map +1 -1
- package/dist/transform/types.d.ts +1 -1
- package/dist/transform/types.d.ts.map +1 -1
- package/dist/vault-account-source.d.ts +183 -0
- package/dist/vault-account-source.d.ts.map +1 -0
- package/dist/vault-account-source.js +296 -0
- package/dist/vault-account-source.js.map +1 -0
- package/dist/vault-enrollment-authority.d.ts +16 -0
- package/dist/vault-enrollment-authority.d.ts.map +1 -0
- package/dist/vault-enrollment-authority.js +19 -0
- package/dist/vault-enrollment-authority.js.map +1 -0
- package/dist/vault-family.d.ts +11 -0
- package/dist/vault-family.d.ts.map +1 -0
- package/dist/vault-family.js +24 -0
- package/dist/vault-family.js.map +1 -0
- package/dist/vault-provider-state.d.ts +162 -0
- package/dist/vault-provider-state.d.ts.map +1 -0
- package/dist/vault-provider-state.js +422 -0
- package/dist/vault-provider-state.js.map +1 -0
- package/package.json +39 -2
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/** Top-level key of the config file that holds everything the pool adds. */
|
|
3
|
+
export const POOL_KEY = 'commonAuthPool';
|
|
4
|
+
/** The pool schema this library reads and writes. */
|
|
5
|
+
export const POOL_SCHEMA_VERSION = 1;
|
|
6
|
+
/** Property of `commonAuthPool` holding the per-row entries, keyed by local id. */
|
|
7
|
+
export const POOL_ROWS_KEY = 'rows';
|
|
8
|
+
/**
|
|
9
|
+
* Property of `commonAuthPool` (since 0.8.0) holding, per id, the highest
|
|
10
|
+
* credential epoch a row with that id held when the store last dropped it
|
|
11
|
+
* from the pool. A row added later under the same id starts past it (see
|
|
12
|
+
* `nextAddEpochIn`), so an attribution taken for the dropped row never
|
|
13
|
+
* matches the new one. Older readers ignore it, and older writers keep it
|
|
14
|
+
* as they keep every pool key they do not know.
|
|
15
|
+
*/
|
|
16
|
+
export const POOL_RETIRED_EPOCHS_KEY = 'retiredEpochs';
|
|
17
|
+
/** The `version` older readers of the same files expect at the top level. */
|
|
18
|
+
export const LEGACY_STORE_VERSION = 1;
|
|
19
|
+
/**
|
|
20
|
+
* How far ahead of the clock a stored `lastRefreshedAt` may sit and still be
|
|
21
|
+
* trusted. Older readers of the state file apply the same bound, and a stamp
|
|
22
|
+
* strictly above it counts as absent when they compare two copies of a token.
|
|
23
|
+
*/
|
|
24
|
+
export const REFRESH_STAMP_TOLERANCE_MS = 5 * 60_000;
|
|
25
|
+
/**
|
|
26
|
+
* Key, inside a state-file account entry, of the stamp naming what the
|
|
27
|
+
* credential beside it belongs to. Older readers ignore it.
|
|
28
|
+
*/
|
|
29
|
+
export const CREDENTIAL_STAMP_KEY = POOL_KEY;
|
|
30
|
+
/** Canonical JSON: recursively sorted object keys, preserved array order. */
|
|
31
|
+
export function canonicalJson(value) {
|
|
32
|
+
const normalized = JSON.parse(JSON.stringify(value));
|
|
33
|
+
const encode = (item) => {
|
|
34
|
+
if (Array.isArray(item))
|
|
35
|
+
return `[${item.map(encode).join(',')}]`;
|
|
36
|
+
if (isRecord(item))
|
|
37
|
+
return `{${Object.keys(item)
|
|
38
|
+
.sort()
|
|
39
|
+
.map((key) => `${JSON.stringify(key)}:${encode(item[key])}`)
|
|
40
|
+
.join(',')}}`;
|
|
41
|
+
return JSON.stringify(item);
|
|
42
|
+
};
|
|
43
|
+
return encode(normalized);
|
|
44
|
+
}
|
|
45
|
+
export function canonicalDigest(value) {
|
|
46
|
+
return createHash('sha256').update(canonicalJson(value)).digest('hex');
|
|
47
|
+
}
|
|
48
|
+
export function parseReservation(raw) {
|
|
49
|
+
if (!isRecord(raw) ||
|
|
50
|
+
Object.keys(raw).length !== 1 ||
|
|
51
|
+
typeof raw.reservation !== 'string' ||
|
|
52
|
+
!raw.reservation)
|
|
53
|
+
return undefined;
|
|
54
|
+
return { reservation: raw.reservation };
|
|
55
|
+
}
|
|
56
|
+
function parseStagedStamp(raw) {
|
|
57
|
+
if (!isRecord(raw) ||
|
|
58
|
+
typeof raw.reservation !== 'string' ||
|
|
59
|
+
!raw.reservation ||
|
|
60
|
+
typeof raw.disabledReason !== 'string')
|
|
61
|
+
return undefined;
|
|
62
|
+
if (Object.keys(raw).some((key) => !['reservation', 'label', 'disabledReason', 'providerState'].includes(key)))
|
|
63
|
+
return undefined;
|
|
64
|
+
if ('label' in raw && typeof raw.label !== 'string')
|
|
65
|
+
return undefined;
|
|
66
|
+
if ('providerState' in raw &&
|
|
67
|
+
(typeof raw.providerState !== 'string' ||
|
|
68
|
+
!/^[a-f0-9]{64}$/.test(raw.providerState)))
|
|
69
|
+
return undefined;
|
|
70
|
+
return raw;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Key, inside a state-file account entry, of the provider state kept beside
|
|
74
|
+
* the credential. Older readers ignore it.
|
|
75
|
+
*/
|
|
76
|
+
export const PROVIDER_STATE_KEY = 'commonAuthProviderState';
|
|
77
|
+
export function isRecord(value) {
|
|
78
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
79
|
+
}
|
|
80
|
+
const UNSAFE_IDS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
81
|
+
/**
|
|
82
|
+
* Ids are stored exactly as given. Older readers trim ids, so an id with
|
|
83
|
+
* surrounding whitespace would be renamed by them; such ids, empty ids and
|
|
84
|
+
* prototype keys are refused instead of being rewritten.
|
|
85
|
+
*/
|
|
86
|
+
export function idProblem(id) {
|
|
87
|
+
if (typeof id !== 'string' || id.length === 0)
|
|
88
|
+
return 'id must be non-empty';
|
|
89
|
+
if (id.trim() !== id)
|
|
90
|
+
return 'id must not carry surrounding whitespace';
|
|
91
|
+
if (UNSAFE_IDS.has(id))
|
|
92
|
+
return 'id is a reserved object key';
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
/** Same URL rule the older readers apply to an API-key row's `baseURL`. */
|
|
96
|
+
export function isValidBaseURL(value) {
|
|
97
|
+
const raw = typeof value === 'string' ? value.trim() : '';
|
|
98
|
+
if (!raw)
|
|
99
|
+
return false;
|
|
100
|
+
try {
|
|
101
|
+
const url = new URL(raw);
|
|
102
|
+
return ((url.protocol === 'http:' || url.protocol === 'https:') &&
|
|
103
|
+
!url.username &&
|
|
104
|
+
!url.password);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function secretOf(credential) {
|
|
111
|
+
return credential.type === 'oauth'
|
|
112
|
+
? `oauth\0${credential.refresh}`
|
|
113
|
+
: `api\0${credential.apiKey}`;
|
|
114
|
+
}
|
|
115
|
+
export function fingerprintOf(credential) {
|
|
116
|
+
return createHash('sha256').update(secretOf(credential)).digest('hex');
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The digest a credential stamp carries. It is kept apart from the
|
|
120
|
+
* fingerprint (a different input prefix) so the persisted value is never the
|
|
121
|
+
* dedupe key.
|
|
122
|
+
*/
|
|
123
|
+
export function credentialDigest(credential) {
|
|
124
|
+
return createHash('sha256')
|
|
125
|
+
.update(`credential-stamp\0${secretOf(credential)}`)
|
|
126
|
+
.digest('hex');
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The digest of everything a request made with the credential sends or is
|
|
130
|
+
* served by: the OAuth access token, refresh token and expiry (the expiry
|
|
131
|
+
* decides whether the access token is used or refreshed first), or the API
|
|
132
|
+
* key with the `baseURL` and header it is sent to. Its input prefix differs
|
|
133
|
+
* from both the fingerprint's and `credentialDigest`'s, so it never equals
|
|
134
|
+
* either. The parts are JSON-encoded as a list, so no two credentials share
|
|
135
|
+
* an input. An access token or expiry the state file cannot hold as loaded
|
|
136
|
+
* (not a string, not a finite number) counts as absent, which is how
|
|
137
|
+
* `buildRawRows` loads it back.
|
|
138
|
+
*/
|
|
139
|
+
export function dispatchDigest(credential) {
|
|
140
|
+
const parts = credential.type === 'oauth'
|
|
141
|
+
? [
|
|
142
|
+
'oauth',
|
|
143
|
+
typeof credential.access === 'string' ? credential.access : null,
|
|
144
|
+
credential.refresh,
|
|
145
|
+
typeof credential.expires === 'number' &&
|
|
146
|
+
Number.isFinite(credential.expires)
|
|
147
|
+
? credential.expires
|
|
148
|
+
: null,
|
|
149
|
+
]
|
|
150
|
+
: [
|
|
151
|
+
'api',
|
|
152
|
+
credential.apiKey,
|
|
153
|
+
credential.baseURL.trim(),
|
|
154
|
+
credential.authHeader ?? 'authorization-bearer',
|
|
155
|
+
];
|
|
156
|
+
return createHash('sha256')
|
|
157
|
+
.update(`credential-dispatch\0${JSON.stringify(parts)}`)
|
|
158
|
+
.digest('hex');
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The stamp for a credential written into a row at `credentialEpoch`, beside
|
|
162
|
+
* the config `binding` describes. `replace` is set only by a replace.
|
|
163
|
+
*/
|
|
164
|
+
export function stampFor(credential, credentialEpoch, binding, options = {}) {
|
|
165
|
+
return {
|
|
166
|
+
credentialEpoch,
|
|
167
|
+
digest: credentialDigest(credential),
|
|
168
|
+
dispatch: dispatchDigest(credential),
|
|
169
|
+
binding: { ...binding },
|
|
170
|
+
...(options.replace ? { replace: true } : {}),
|
|
171
|
+
...(options.providerState !== undefined
|
|
172
|
+
? { providerState: options.providerState }
|
|
173
|
+
: {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The digest a stamp carries for a provider state: of its credential-bound
|
|
178
|
+
* part (`ProviderStateCodec.credentialBound`, the whole value without it), as
|
|
179
|
+
* it serializes. The store only ever stores values that survive a JSON round
|
|
180
|
+
* trip unchanged, so the digest of a value read back from disk equals the one
|
|
181
|
+
* computed when it was written. Its input prefix differs from every other
|
|
182
|
+
* digest the store writes.
|
|
183
|
+
*/
|
|
184
|
+
export function providerStateDigest(codec, value) {
|
|
185
|
+
const bound = codec?.credentialBound ? codec.credentialBound(value) : value;
|
|
186
|
+
return createHash('sha256')
|
|
187
|
+
.update(`provider-state\0${JSON.stringify(bound ?? null)}`)
|
|
188
|
+
.digest('hex');
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The provider-state digest of the stamp in a state-file account entry, when
|
|
192
|
+
* that stamp binds it to this row: written with this credential (its lineage
|
|
193
|
+
* digest matches), at this credential epoch, naming this recorded identity.
|
|
194
|
+
* Whether the value beside it still has that digest is checked apart, by
|
|
195
|
+
* `providerStateFields`; this alone is what a write that keeps the value
|
|
196
|
+
* carries into the stamp it writes, so a value no stamp of this store bound
|
|
197
|
+
* to the row stays unbound rather than being vouched for, and one edited
|
|
198
|
+
* after it was bound stays detectably edited.
|
|
199
|
+
*/
|
|
200
|
+
export function boundProviderStateDigest(account, credential, credentialEpoch, identity) {
|
|
201
|
+
if (!credential || credentialEpoch === undefined)
|
|
202
|
+
return undefined;
|
|
203
|
+
if (!isRecord(account) || !Object.hasOwn(account, PROVIDER_STATE_KEY))
|
|
204
|
+
return undefined;
|
|
205
|
+
const stamp = parseStamp(account[CREDENTIAL_STAMP_KEY]);
|
|
206
|
+
if (!stamp ||
|
|
207
|
+
stamp.providerState === undefined ||
|
|
208
|
+
stamp.digest !== credentialDigest(credential) ||
|
|
209
|
+
stamp.credentialEpoch !== credentialEpoch ||
|
|
210
|
+
stamp.binding?.identity !== identity)
|
|
211
|
+
return undefined;
|
|
212
|
+
return stamp.providerState;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* A credential epoch is a positive safe integer. Above `MAX_SAFE_INTEGER`,
|
|
216
|
+
* adding one may give back the same number, so a replace would not move the
|
|
217
|
+
* epoch and nothing could tell the old credential's work from the new one's.
|
|
218
|
+
*/
|
|
219
|
+
export function isCredentialEpoch(value) {
|
|
220
|
+
return Number.isSafeInteger(value) && value >= 1;
|
|
221
|
+
}
|
|
222
|
+
/** A well-formed stamp, or undefined for anything else (which is ignored). */
|
|
223
|
+
export function parseStamp(raw) {
|
|
224
|
+
if (!isRecord(raw))
|
|
225
|
+
return undefined;
|
|
226
|
+
const epoch = raw.credentialEpoch;
|
|
227
|
+
if (!isCredentialEpoch(epoch))
|
|
228
|
+
return undefined;
|
|
229
|
+
if (typeof raw.digest !== 'string')
|
|
230
|
+
return undefined;
|
|
231
|
+
if ('dispatch' in raw && typeof raw.dispatch !== 'string')
|
|
232
|
+
return undefined;
|
|
233
|
+
if ('replace' in raw && raw.replace !== true)
|
|
234
|
+
return undefined;
|
|
235
|
+
if ('providerState' in raw && typeof raw.providerState !== 'string')
|
|
236
|
+
return undefined;
|
|
237
|
+
const staged = 'staged' in raw ? parseStagedStamp(raw.staged) : undefined;
|
|
238
|
+
if ('staged' in raw && !staged)
|
|
239
|
+
return undefined;
|
|
240
|
+
const marks = {
|
|
241
|
+
...(staged ? { staged } : {}),
|
|
242
|
+
...(typeof raw.dispatch === 'string' ? { dispatch: raw.dispatch } : {}),
|
|
243
|
+
...(raw.replace === true ? { replace: true } : {}),
|
|
244
|
+
...(typeof raw.providerState === 'string'
|
|
245
|
+
? { providerState: raw.providerState }
|
|
246
|
+
: {}),
|
|
247
|
+
};
|
|
248
|
+
// Every stamp that carries a dispatch digest or a provider-state digest is
|
|
249
|
+
// written with a binding; one without is not a stamp this store writes,
|
|
250
|
+
// and it would leave the row's identity unchecked.
|
|
251
|
+
if (!('binding' in raw))
|
|
252
|
+
return 'dispatch' in raw ||
|
|
253
|
+
'replace' in raw ||
|
|
254
|
+
'providerState' in raw ||
|
|
255
|
+
'staged' in raw
|
|
256
|
+
? undefined
|
|
257
|
+
: { credentialEpoch: epoch, digest: raw.digest };
|
|
258
|
+
const binding = raw.binding;
|
|
259
|
+
if (!isRecord(binding))
|
|
260
|
+
return undefined;
|
|
261
|
+
if ('identity' in binding &&
|
|
262
|
+
(typeof binding.identity !== 'string' || !binding.identity))
|
|
263
|
+
return undefined;
|
|
264
|
+
if ('baseURL' in binding && !isValidBaseURL(binding.baseURL))
|
|
265
|
+
return undefined;
|
|
266
|
+
if ('authHeader' in binding &&
|
|
267
|
+
binding.authHeader !== 'authorization-bearer' &&
|
|
268
|
+
binding.authHeader !== 'x-api-key')
|
|
269
|
+
return undefined;
|
|
270
|
+
return {
|
|
271
|
+
credentialEpoch: epoch,
|
|
272
|
+
digest: raw.digest,
|
|
273
|
+
...marks,
|
|
274
|
+
binding: {
|
|
275
|
+
...(typeof binding.identity === 'string'
|
|
276
|
+
? { identity: binding.identity }
|
|
277
|
+
: {}),
|
|
278
|
+
...(typeof binding.baseURL === 'string'
|
|
279
|
+
? { baseURL: binding.baseURL.trim() }
|
|
280
|
+
: {}),
|
|
281
|
+
...(binding.authHeader === 'authorization-bearer' ||
|
|
282
|
+
binding.authHeader === 'x-api-key'
|
|
283
|
+
? { authHeader: binding.authHeader }
|
|
284
|
+
: {}),
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Whether the binding of a stamp is the row's: the identity it names (or its
|
|
290
|
+
* naming none) must be exactly the row's recorded identity, and an endpoint
|
|
291
|
+
* it names must be the one the row sends its API key to.
|
|
292
|
+
*
|
|
293
|
+
* Every write that gives a row an identity (`add`, `replace`, `rotate` or a
|
|
294
|
+
* refresh that learns one, `recordIdentity`) writes the stamp naming it
|
|
295
|
+
* before the config, so this store never leaves a config identity beside a
|
|
296
|
+
* stamp that names none or another: that is another writer's doing and is
|
|
297
|
+
* `mismatched`. The reverse, a stamp naming an identity beside a config that
|
|
298
|
+
* has none, is such a write stopped between its two writes: the row as
|
|
299
|
+
* `buildRawRows` reads it from the files is `mismatched`, but `loadRows`,
|
|
300
|
+
* which every reader goes through, shows it with the identity recorded (see
|
|
301
|
+
* `torn.ts`), where it is `bound`.
|
|
302
|
+
*/
|
|
303
|
+
function bindingAgrees(binding, credential, identity) {
|
|
304
|
+
if (binding.identity !== identity)
|
|
305
|
+
return false;
|
|
306
|
+
if (binding.baseURL !== undefined &&
|
|
307
|
+
(credential.type !== 'api' || credential.baseURL !== binding.baseURL))
|
|
308
|
+
return false;
|
|
309
|
+
if (binding.authHeader !== undefined &&
|
|
310
|
+
(credential.type !== 'api' || credential.authHeader !== binding.authHeader))
|
|
311
|
+
return false;
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The stamp status of a loaded row (see `CredentialStampStatus`).
|
|
316
|
+
* `credentialEpoch` is undefined only when the row's per-row entry exists but
|
|
317
|
+
* failed validation, in which case no stamp can match it.
|
|
318
|
+
*/
|
|
319
|
+
function stampStatusOf(credential, credentialEpoch, identity, account) {
|
|
320
|
+
if (!credential)
|
|
321
|
+
return 'none';
|
|
322
|
+
if (!isRecord(account) || !Object.hasOwn(account, CREDENTIAL_STAMP_KEY))
|
|
323
|
+
return 'missing';
|
|
324
|
+
const stamp = parseStamp(account[CREDENTIAL_STAMP_KEY]);
|
|
325
|
+
if (!stamp)
|
|
326
|
+
return 'malformed';
|
|
327
|
+
if (stamp.digest !== credentialDigest(credential))
|
|
328
|
+
return 'mismatched';
|
|
329
|
+
// A stamp from 0.4.3 or earlier proves neither the token sent nor (its
|
|
330
|
+
// binding being optional and identity-lenient) the account, so it is
|
|
331
|
+
// reported as such whatever else it says, and never as bound.
|
|
332
|
+
if (stamp.dispatch === undefined)
|
|
333
|
+
return 'legacy';
|
|
334
|
+
if (stamp.credentialEpoch !== credentialEpoch)
|
|
335
|
+
return 'mismatched';
|
|
336
|
+
// `parseStamp` refuses a stamp with a dispatch digest and no binding.
|
|
337
|
+
if (!stamp.binding || !bindingAgrees(stamp.binding, credential, identity))
|
|
338
|
+
return 'mismatched';
|
|
339
|
+
if (stamp.dispatch !== dispatchDigest(credential))
|
|
340
|
+
return 'mismatched';
|
|
341
|
+
return 'bound';
|
|
342
|
+
}
|
|
343
|
+
export function classifyConfig(read) {
|
|
344
|
+
if (!read.exists)
|
|
345
|
+
return { status: 'ready', exists: false, config: {} };
|
|
346
|
+
if ('parseError' in read)
|
|
347
|
+
return { status: 'error', reason: 'config file is not valid JSON' };
|
|
348
|
+
const value = read.value;
|
|
349
|
+
if (!isRecord(value))
|
|
350
|
+
return { status: 'error', reason: 'config root is not an object' };
|
|
351
|
+
if ('accounts' in value && !Array.isArray(value.accounts))
|
|
352
|
+
return { status: 'error', reason: 'config accounts is not an array' };
|
|
353
|
+
if (!(POOL_KEY in value)) {
|
|
354
|
+
return Array.isArray(value.accounts)
|
|
355
|
+
? { status: 'pending-migration', config: value }
|
|
356
|
+
: { status: 'ready', exists: true, config: value };
|
|
357
|
+
}
|
|
358
|
+
const pool = value[POOL_KEY];
|
|
359
|
+
if (!isRecord(pool))
|
|
360
|
+
return { status: 'error', reason: `${POOL_KEY} is not an object` };
|
|
361
|
+
const version = pool.schemaVersion;
|
|
362
|
+
if (typeof version !== 'number' || !Number.isInteger(version) || version < 1)
|
|
363
|
+
return { status: 'error', reason: `${POOL_KEY}.schemaVersion is invalid` };
|
|
364
|
+
if (version > POOL_SCHEMA_VERSION)
|
|
365
|
+
return {
|
|
366
|
+
status: 'error',
|
|
367
|
+
reason: `${POOL_KEY}.schemaVersion ${version} is newer than ${POOL_SCHEMA_VERSION}`,
|
|
368
|
+
};
|
|
369
|
+
if (POOL_ROWS_KEY in pool && !isRecord(pool[POOL_ROWS_KEY]))
|
|
370
|
+
return {
|
|
371
|
+
status: 'error',
|
|
372
|
+
reason: `${POOL_KEY}.${POOL_ROWS_KEY} is not an object`,
|
|
373
|
+
};
|
|
374
|
+
return { status: 'ready', exists: true, config: value };
|
|
375
|
+
}
|
|
376
|
+
export function classifyState(read) {
|
|
377
|
+
if (!read.exists)
|
|
378
|
+
return { status: 'ready', exists: false, state: {} };
|
|
379
|
+
if ('parseError' in read)
|
|
380
|
+
return { status: 'error', reason: 'state file is not valid JSON' };
|
|
381
|
+
if (!isRecord(read.value))
|
|
382
|
+
return { status: 'error', reason: 'state root is not an object' };
|
|
383
|
+
if ('accounts' in read.value && !isRecord(read.value.accounts))
|
|
384
|
+
return { status: 'error', reason: 'state accounts is not an object' };
|
|
385
|
+
return { status: 'ready', exists: true, state: read.value };
|
|
386
|
+
}
|
|
387
|
+
export function rosterOf(config) {
|
|
388
|
+
return Array.isArray(config.accounts) ? config.accounts : [];
|
|
389
|
+
}
|
|
390
|
+
export function entriesOf(config) {
|
|
391
|
+
const pool = config[POOL_KEY];
|
|
392
|
+
if (!isRecord(pool))
|
|
393
|
+
return {};
|
|
394
|
+
const rows = pool[POOL_ROWS_KEY];
|
|
395
|
+
return isRecord(rows) ? rows : {};
|
|
396
|
+
}
|
|
397
|
+
function stateAccountsOf(state) {
|
|
398
|
+
return isRecord(state.accounts) ? state.accounts : {};
|
|
399
|
+
}
|
|
400
|
+
/** The reason a roster row cannot be loaded, mirroring the older readers. */
|
|
401
|
+
function rosterRowProblem(raw) {
|
|
402
|
+
if (!isRecord(raw))
|
|
403
|
+
return 'not an object';
|
|
404
|
+
const problem = idProblem(raw.id);
|
|
405
|
+
if (problem)
|
|
406
|
+
return problem;
|
|
407
|
+
if (raw.type === 'api')
|
|
408
|
+
return isValidBaseURL(raw.baseURL) ? undefined : 'invalid baseURL';
|
|
409
|
+
if (raw.type === 'oauth')
|
|
410
|
+
return undefined;
|
|
411
|
+
return 'unknown type';
|
|
412
|
+
}
|
|
413
|
+
function parseEntry(raw, codec) {
|
|
414
|
+
if (!isRecord(raw))
|
|
415
|
+
return undefined;
|
|
416
|
+
const epoch = raw.credentialEpoch;
|
|
417
|
+
if (!isCredentialEpoch(epoch))
|
|
418
|
+
return undefined;
|
|
419
|
+
if ('needsFirstReading' in raw && typeof raw.needsFirstReading !== 'boolean')
|
|
420
|
+
return undefined;
|
|
421
|
+
if ('disabledReason' in raw && typeof raw.disabledReason !== 'string')
|
|
422
|
+
return undefined;
|
|
423
|
+
if ('quota' in raw && !codec.validate(raw.quota))
|
|
424
|
+
return undefined;
|
|
425
|
+
if ('staged' in raw && !parseReservation(raw.staged))
|
|
426
|
+
return undefined;
|
|
427
|
+
return {
|
|
428
|
+
credentialEpoch: epoch,
|
|
429
|
+
needsFirstReading: raw.needsFirstReading === true,
|
|
430
|
+
...(typeof raw.disabledReason === 'string'
|
|
431
|
+
? { disabledReason: raw.disabledReason }
|
|
432
|
+
: {}),
|
|
433
|
+
...('quota' in raw ? { quota: raw.quota } : {}),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function credentialFor(raw, stateEntry) {
|
|
437
|
+
if (!isRecord(stateEntry))
|
|
438
|
+
return undefined;
|
|
439
|
+
if (raw.type === 'api') {
|
|
440
|
+
const apiKey = typeof stateEntry.apiKey === 'string' ? stateEntry.apiKey.trim() : '';
|
|
441
|
+
if (!apiKey)
|
|
442
|
+
return undefined;
|
|
443
|
+
return {
|
|
444
|
+
type: 'api',
|
|
445
|
+
apiKey,
|
|
446
|
+
baseURL: String(raw.baseURL).trim(),
|
|
447
|
+
authHeader: raw.authHeader === 'x-api-key' ? 'x-api-key' : 'authorization-bearer',
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (raw.corrupt === true)
|
|
451
|
+
return undefined;
|
|
452
|
+
const refresh = stateEntry.refresh;
|
|
453
|
+
if (typeof refresh !== 'string' || !refresh.trim())
|
|
454
|
+
return undefined;
|
|
455
|
+
return {
|
|
456
|
+
type: 'oauth',
|
|
457
|
+
refresh,
|
|
458
|
+
...(typeof stateEntry.access === 'string'
|
|
459
|
+
? { access: stateEntry.access }
|
|
460
|
+
: {}),
|
|
461
|
+
...(typeof stateEntry.expires === 'number'
|
|
462
|
+
? { expires: stateEntry.expires }
|
|
463
|
+
: {}),
|
|
464
|
+
...(typeof stateEntry.lastRefreshedAt === 'number'
|
|
465
|
+
? { lastRefreshedAt: stateEntry.lastRefreshedAt }
|
|
466
|
+
: {}),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
/** The first roster row with this id (the one the pool loads). */
|
|
470
|
+
export function rosterRowIn(config, id) {
|
|
471
|
+
return rosterOf(config).find((raw) => isRecord(raw) && raw.id === id);
|
|
472
|
+
}
|
|
473
|
+
/** The pool object of a config, created (empty) when absent. */
|
|
474
|
+
function ensurePool(config) {
|
|
475
|
+
if (!isRecord(config[POOL_KEY]))
|
|
476
|
+
config[POOL_KEY] = {};
|
|
477
|
+
return config[POOL_KEY];
|
|
478
|
+
}
|
|
479
|
+
/** The per-row entries of a config, created (empty) when absent. */
|
|
480
|
+
export function ensureEntries(config) {
|
|
481
|
+
const pool = ensurePool(config);
|
|
482
|
+
if (!isRecord(pool[POOL_ROWS_KEY]))
|
|
483
|
+
pool[POOL_ROWS_KEY] = {};
|
|
484
|
+
return pool[POOL_ROWS_KEY];
|
|
485
|
+
}
|
|
486
|
+
export function entryIn(config, id) {
|
|
487
|
+
const entries = entriesOf(config);
|
|
488
|
+
const entry = Object.hasOwn(entries, id) ? entries[id] : undefined;
|
|
489
|
+
return isRecord(entry) ? entry : undefined;
|
|
490
|
+
}
|
|
491
|
+
/** Sets an entry as an own property, so an id such as `toString` is safe. */
|
|
492
|
+
export function setEntryIn(config, id, entry) {
|
|
493
|
+
Object.defineProperty(ensureEntries(config), id, {
|
|
494
|
+
value: entry,
|
|
495
|
+
enumerable: true,
|
|
496
|
+
writable: true,
|
|
497
|
+
configurable: true,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* The credential epoch recorded for a dropped id (see
|
|
502
|
+
* `POOL_RETIRED_EPOCHS_KEY`), or undefined when none is. A value that is not
|
|
503
|
+
* a credential epoch counts as none.
|
|
504
|
+
*/
|
|
505
|
+
export function retiredEpochIn(config, id) {
|
|
506
|
+
const pool = config[POOL_KEY];
|
|
507
|
+
if (!isRecord(pool))
|
|
508
|
+
return undefined;
|
|
509
|
+
const retired = pool[POOL_RETIRED_EPOCHS_KEY];
|
|
510
|
+
if (!isRecord(retired) || !Object.hasOwn(retired, id))
|
|
511
|
+
return undefined;
|
|
512
|
+
const epoch = retired[id];
|
|
513
|
+
return isCredentialEpoch(epoch) ? epoch : undefined;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* The epoch a per-row entry claims, read without validating the rest of the
|
|
517
|
+
* entry, or undefined when it names none a reader would accept.
|
|
518
|
+
*/
|
|
519
|
+
function entryEpochIn(config, id) {
|
|
520
|
+
const epoch = entryIn(config, id)?.credentialEpoch;
|
|
521
|
+
return isCredentialEpoch(epoch) ? epoch : undefined;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* The credential epoch `add` gives a new row with this id: one past the
|
|
525
|
+
* highest epoch the id is known to have held, which is the epoch recorded
|
|
526
|
+
* when the store dropped it, or the epoch of an entry left behind by a writer
|
|
527
|
+
* that removed only its roster row; 1 for an id the pool never held.
|
|
528
|
+
*
|
|
529
|
+
* An attribution names a row by id and credential epoch (and identity), and
|
|
530
|
+
* an id is chosen by the plugin, so it is often the same one again (`main`).
|
|
531
|
+
* Were a re-added row to start at epoch 1 again, an attribution taken for the
|
|
532
|
+
* removed row's credential would match the new credential exactly, in this
|
|
533
|
+
* process or any other. Starting past every earlier epoch makes such an
|
|
534
|
+
* attribution fail as it does after a `replace`. The result may lie past the
|
|
535
|
+
* safe integers (an id whose last row was at `Number.MAX_SAFE_INTEGER`);
|
|
536
|
+
* `add` refuses such an id.
|
|
537
|
+
*/
|
|
538
|
+
export function nextAddEpochIn(config, id) {
|
|
539
|
+
return (Math.max(retiredEpochIn(config, id) ?? 0, entryEpochIn(config, id) ?? 0) + 1);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Records, in a config being written, the epochs of the ids it drops: for
|
|
543
|
+
* each, the epoch its entry claims (1 for a row without one, the epoch such a
|
|
544
|
+
* row is at), kept only when above what is already recorded, so the record
|
|
545
|
+
* for an id never goes down. Valid recorded values of other ids are kept; a
|
|
546
|
+
* record that is not an object, or a value in it that is not an epoch, says
|
|
547
|
+
* nothing and is replaced. An entry whose epoch cannot be read records 1.
|
|
548
|
+
*/
|
|
549
|
+
export function retireEpochsIn(config, dropped) {
|
|
550
|
+
const ids = [...dropped];
|
|
551
|
+
if (ids.length === 0)
|
|
552
|
+
return;
|
|
553
|
+
const pool = ensurePool(config);
|
|
554
|
+
const previous = isRecord(pool[POOL_RETIRED_EPOCHS_KEY])
|
|
555
|
+
? pool[POOL_RETIRED_EPOCHS_KEY]
|
|
556
|
+
: {};
|
|
557
|
+
const next = {};
|
|
558
|
+
const define = (id, epoch) => Object.defineProperty(next, id, {
|
|
559
|
+
value: epoch,
|
|
560
|
+
enumerable: true,
|
|
561
|
+
writable: true,
|
|
562
|
+
configurable: true,
|
|
563
|
+
});
|
|
564
|
+
for (const [id, epoch] of Object.entries(previous))
|
|
565
|
+
if (isCredentialEpoch(epoch))
|
|
566
|
+
define(id, epoch);
|
|
567
|
+
for (const id of ids)
|
|
568
|
+
define(id, Math.max(retiredEpochIn(config, id) ?? 0, entryEpochIn(config, id) ?? 1));
|
|
569
|
+
pool[POOL_RETIRED_EPOCHS_KEY] = next;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Builds the rows of a ready pool from the files exactly as they are, without
|
|
573
|
+
* looking at credential stamps (see `loadRows` for the rows every reader
|
|
574
|
+
* gets). A roster row the older readers would reject, a duplicate id, or a
|
|
575
|
+
* malformed per-row entry makes that one row invalid (never a candidate) and
|
|
576
|
+
* blocks nothing else.
|
|
577
|
+
*/
|
|
578
|
+
export function buildRawRows(config, state, codec, providerCodec) {
|
|
579
|
+
const entries = entriesOf(config);
|
|
580
|
+
const stateAccounts = stateAccountsOf(state);
|
|
581
|
+
const seen = new Set();
|
|
582
|
+
const rows = [];
|
|
583
|
+
for (const raw of rosterOf(config)) {
|
|
584
|
+
const problem = rosterRowProblem(raw);
|
|
585
|
+
const id = isRecord(raw) && typeof raw.id === 'string' ? raw.id : undefined;
|
|
586
|
+
const rawEntry = id === undefined ? undefined : entries[id];
|
|
587
|
+
const reserved = isRecord(rawEntry) && Object.hasOwn(rawEntry, 'staged');
|
|
588
|
+
const staged = reserved ? parseReservation(rawEntry.staged) : undefined;
|
|
589
|
+
if (problem || !isRecord(raw) || id === undefined || seen.has(id)) {
|
|
590
|
+
if (id !== undefined && !seen.has(id))
|
|
591
|
+
seen.add(id);
|
|
592
|
+
if (id !== undefined)
|
|
593
|
+
rows.push({
|
|
594
|
+
id,
|
|
595
|
+
type: isRecord(raw) && raw.type === 'api' ? 'api' : 'oauth',
|
|
596
|
+
enabled: false,
|
|
597
|
+
needsFirstReading: true,
|
|
598
|
+
hasEntry: Object.hasOwn(entries, id),
|
|
599
|
+
candidate: false,
|
|
600
|
+
invalid: 'roster',
|
|
601
|
+
stamp: 'none',
|
|
602
|
+
...(staged ? { staged } : {}),
|
|
603
|
+
});
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
seen.add(id);
|
|
607
|
+
const type = raw.type === 'api' ? 'api' : 'oauth';
|
|
608
|
+
const hasEntry = Object.hasOwn(entries, id);
|
|
609
|
+
const entry = hasEntry ? parseEntry(entries[id], codec) : undefined;
|
|
610
|
+
const credential = credentialFor(raw, stateAccounts[id]);
|
|
611
|
+
const enabled = raw.enabled !== false;
|
|
612
|
+
// A row without a per-row entry is at credential epoch 1 (the epoch the
|
|
613
|
+
// store stamps and later gives it), so its stamp is checked against 1.
|
|
614
|
+
const stampEpoch = entry ? entry.credentialEpoch : hasEntry ? undefined : 1;
|
|
615
|
+
const identity = typeof raw.accountId === 'string' && raw.accountId
|
|
616
|
+
? raw.accountId
|
|
617
|
+
: undefined;
|
|
618
|
+
const row = {
|
|
619
|
+
id,
|
|
620
|
+
type,
|
|
621
|
+
enabled,
|
|
622
|
+
needsFirstReading: entry ? entry.needsFirstReading : type === 'oauth',
|
|
623
|
+
hasEntry,
|
|
624
|
+
candidate: false,
|
|
625
|
+
...(typeof raw.label === 'string' ? { label: raw.label } : {}),
|
|
626
|
+
...(typeof raw.addedAt === 'number' ? { addedAt: raw.addedAt } : {}),
|
|
627
|
+
...(identity !== undefined ? { identity } : {}),
|
|
628
|
+
...(credential
|
|
629
|
+
? { credential, fingerprint: fingerprintOf(credential) }
|
|
630
|
+
: {}),
|
|
631
|
+
...(entry ? { credentialEpoch: entry.credentialEpoch } : {}),
|
|
632
|
+
...(staged ? { staged } : {}),
|
|
633
|
+
...(entry?.disabledReason !== undefined
|
|
634
|
+
? { disabledReason: entry.disabledReason }
|
|
635
|
+
: {}),
|
|
636
|
+
...(entry && 'quota' in entry ? { quota: entry.quota } : {}),
|
|
637
|
+
...providerStateFields(stateAccounts[id], credential, stampEpoch, identity, providerCodec),
|
|
638
|
+
stamp: stampStatusOf(credential, stampEpoch, identity, stateAccounts[id]),
|
|
639
|
+
};
|
|
640
|
+
if (hasEntry && !entry) {
|
|
641
|
+
row.invalid = 'entry';
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
row.candidate = enabled && credential !== undefined && !reserved;
|
|
645
|
+
}
|
|
646
|
+
rows.push(row);
|
|
647
|
+
}
|
|
648
|
+
return rows;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* The provider-state fields of a loaded row. A value on disk is shown only
|
|
652
|
+
* when the stamp beside the credential binds a provider state to this row's
|
|
653
|
+
* credential, epoch and identity (see `boundProviderStateDigest`), the codec
|
|
654
|
+
* accepts the value, and the value's credential-bound part has the digest
|
|
655
|
+
* that stamp names. Otherwise the row says why it shows none and stays
|
|
656
|
+
* usable: the value is the plugin's own derived data, which it can rebuild,
|
|
657
|
+
* and the credential beside it may well be sound.
|
|
658
|
+
*/
|
|
659
|
+
function providerStateFields(account, credential, credentialEpoch, identity, codec) {
|
|
660
|
+
if (!isRecord(account) || !Object.hasOwn(account, PROVIDER_STATE_KEY))
|
|
661
|
+
return {};
|
|
662
|
+
const digest = boundProviderStateDigest(account, credential, credentialEpoch, identity);
|
|
663
|
+
if (digest === undefined)
|
|
664
|
+
return { providerStateDropped: 'uncovered' };
|
|
665
|
+
const value = account[PROVIDER_STATE_KEY];
|
|
666
|
+
// The value is validated before it is projected, so the projection only
|
|
667
|
+
// ever sees a value of the shape the codec accepts.
|
|
668
|
+
if (!codec?.validate(value))
|
|
669
|
+
return { providerStateDropped: 'invalid' };
|
|
670
|
+
if (providerStateDigest(codec, value) !== digest)
|
|
671
|
+
return { providerStateDropped: 'uncovered' };
|
|
672
|
+
return { providerState: value };
|
|
673
|
+
}
|
|
674
|
+
/** The row-lock key: recorded wire identity when known, else the local id. */
|
|
675
|
+
export function rowLockKey(row) {
|
|
676
|
+
return row.identity ?? row.id;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* The refresh stamp a write persists: the clock, raised to one past a prior
|
|
680
|
+
* stamp that is still trusted, so a stale copy of the previous token can never
|
|
681
|
+
* compare newer than the rotation. A prior stamp beyond the trust bound says
|
|
682
|
+
* nothing and is ignored.
|
|
683
|
+
*/
|
|
684
|
+
export function rotationStamp(prior, now) {
|
|
685
|
+
if (prior === undefined || prior > now + REFRESH_STAMP_TOLERANCE_MS)
|
|
686
|
+
return now;
|
|
687
|
+
return Math.max(now, prior + 1);
|
|
688
|
+
}
|
|
689
|
+
/** True when a rotation stamped now would itself be past the trust bound. */
|
|
690
|
+
export function rotationStampUntrusted(prior, now) {
|
|
691
|
+
return rotationStamp(prior, now) > now + REFRESH_STAMP_TOLERANCE_MS;
|
|
692
|
+
}
|
|
693
|
+
/** The legacy-shaped roster row for a new row. */
|
|
694
|
+
export function rosterRowFor(input) {
|
|
695
|
+
const { id, credential, identity, label, addedAt } = input;
|
|
696
|
+
return {
|
|
697
|
+
id,
|
|
698
|
+
...(label !== undefined ? { label } : {}),
|
|
699
|
+
type: credential.type,
|
|
700
|
+
addedAt,
|
|
701
|
+
...(identity !== undefined ? { accountId: identity } : {}),
|
|
702
|
+
...(credential.type === 'api'
|
|
703
|
+
? {
|
|
704
|
+
baseURL: credential.baseURL.trim(),
|
|
705
|
+
authHeader: credential.authHeader ?? 'authorization-bearer',
|
|
706
|
+
}
|
|
707
|
+
: {}),
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
/** The state-file fields a credential occupies, in the older readers' names. */
|
|
711
|
+
export function stateFieldsFor(credential, lastRefreshedAt) {
|
|
712
|
+
if (credential.type === 'api')
|
|
713
|
+
return { apiKey: credential.apiKey };
|
|
714
|
+
return {
|
|
715
|
+
...(credential.access !== undefined ? { access: credential.access } : {}),
|
|
716
|
+
refresh: credential.refresh,
|
|
717
|
+
...(credential.expires !== undefined
|
|
718
|
+
? { expires: credential.expires }
|
|
719
|
+
: {}),
|
|
720
|
+
...(lastRefreshedAt !== undefined ? { lastRefreshedAt } : {}),
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
export function storedCredential(credential, lastRefreshedAt) {
|
|
724
|
+
if (credential.type === 'api')
|
|
725
|
+
return {
|
|
726
|
+
...credential,
|
|
727
|
+
baseURL: credential.baseURL.trim(),
|
|
728
|
+
authHeader: credential.authHeader ?? 'authorization-bearer',
|
|
729
|
+
};
|
|
730
|
+
return {
|
|
731
|
+
...credential,
|
|
732
|
+
...(lastRefreshedAt !== undefined ? { lastRefreshedAt } : {}),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
export function credentialProblem(credential, options = {}) {
|
|
736
|
+
if (!isRecord(credential))
|
|
737
|
+
return 'credential must be an object';
|
|
738
|
+
if (credential.type === 'oauth') {
|
|
739
|
+
if (typeof credential.refresh !== 'string' || !credential.refresh.trim())
|
|
740
|
+
return 'oauth credential needs a refresh token';
|
|
741
|
+
return undefined;
|
|
742
|
+
}
|
|
743
|
+
if (credential.type === 'api') {
|
|
744
|
+
if (typeof credential.apiKey !== 'string' || !credential.apiKey.trim())
|
|
745
|
+
return 'api credential needs an api key';
|
|
746
|
+
if (credential.apiKey.trim() !== credential.apiKey)
|
|
747
|
+
return 'api key must not carry surrounding whitespace';
|
|
748
|
+
const omitted = options.baseURLOptional && credential.baseURL === undefined;
|
|
749
|
+
if (!omitted && !isValidBaseURL(credential.baseURL))
|
|
750
|
+
return 'api credential needs a valid baseURL';
|
|
751
|
+
return undefined;
|
|
752
|
+
}
|
|
753
|
+
return 'credential type must be oauth or api';
|
|
754
|
+
}
|