@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.
Files changed (230) hide show
  1. package/README.md +24 -2
  2. package/dist/abort-forwarding.d.ts +3 -0
  3. package/dist/abort-forwarding.d.ts.map +1 -0
  4. package/dist/abort-forwarding.js +19 -0
  5. package/dist/abort-forwarding.js.map +1 -0
  6. package/dist/account-identity.d.ts +52 -0
  7. package/dist/account-identity.d.ts.map +1 -0
  8. package/dist/account-identity.js +66 -0
  9. package/dist/account-identity.js.map +1 -0
  10. package/dist/account-manager.d.ts +228 -43
  11. package/dist/account-manager.d.ts.map +1 -1
  12. package/dist/account-manager.js +942 -903
  13. package/dist/account-manager.js.map +1 -1
  14. package/dist/account-migration.d.ts +309 -0
  15. package/dist/account-migration.d.ts.map +1 -0
  16. package/dist/account-migration.js +2282 -0
  17. package/dist/account-migration.js.map +1 -0
  18. package/dist/account-repository-codecs.d.ts +82 -0
  19. package/dist/account-repository-codecs.d.ts.map +1 -0
  20. package/dist/account-repository-codecs.js +609 -0
  21. package/dist/account-repository-codecs.js.map +1 -0
  22. package/dist/account-repository-types.d.ts +831 -0
  23. package/dist/account-repository-types.d.ts.map +1 -0
  24. package/dist/account-repository-types.js +137 -0
  25. package/dist/account-repository-types.js.map +1 -0
  26. package/dist/account-repository.d.ts +442 -0
  27. package/dist/account-repository.d.ts.map +1 -0
  28. package/dist/account-repository.js +2253 -0
  29. package/dist/account-repository.js.map +1 -0
  30. package/dist/account-selector.d.ts +470 -0
  31. package/dist/account-selector.d.ts.map +1 -0
  32. package/dist/account-selector.js +1383 -0
  33. package/dist/account-selector.js.map +1 -0
  34. package/dist/account-storage.d.ts.map +1 -1
  35. package/dist/account-storage.js +5 -0
  36. package/dist/account-storage.js.map +1 -1
  37. package/dist/agy-transport.d.ts.map +1 -1
  38. package/dist/agy-transport.js +21 -35
  39. package/dist/agy-transport.js.map +1 -1
  40. package/dist/antigravity-command-menu.d.ts +171 -0
  41. package/dist/antigravity-command-menu.d.ts.map +1 -0
  42. package/dist/antigravity-command-menu.js +523 -0
  43. package/dist/antigravity-command-menu.js.map +1 -0
  44. package/dist/atomic-write.d.ts +18 -1
  45. package/dist/atomic-write.d.ts.map +1 -1
  46. package/dist/atomic-write.js +15 -3
  47. package/dist/atomic-write.js.map +1 -1
  48. package/dist/common-auth-embedded/NOTICE.txt +21 -0
  49. package/dist/common-auth-embedded/auth-menu/accounts.d.ts +97 -0
  50. package/dist/common-auth-embedded/auth-menu/accounts.js +387 -0
  51. package/dist/common-auth-embedded/auth-menu/ansi.d.ts +26 -0
  52. package/dist/common-auth-embedded/auth-menu/ansi.js +98 -0
  53. package/dist/common-auth-embedded/auth-menu/confirm.d.ts +7 -0
  54. package/dist/common-auth-embedded/auth-menu/confirm.js +21 -0
  55. package/dist/common-auth-embedded/auth-menu/doctor.d.ts +52 -0
  56. package/dist/common-auth-embedded/auth-menu/doctor.js +81 -0
  57. package/dist/common-auth-embedded/auth-menu/index.d.ts +17 -0
  58. package/dist/common-auth-embedded/auth-menu/index.js +12 -0
  59. package/dist/common-auth-embedded/auth-menu/login.d.ts +55 -0
  60. package/dist/common-auth-embedded/auth-menu/login.js +83 -0
  61. package/dist/common-auth-embedded/auth-menu/menu.d.ts +59 -0
  62. package/dist/common-auth-embedded/auth-menu/menu.js +73 -0
  63. package/dist/common-auth-embedded/auth-menu/opencode-v1.d.ts +49 -0
  64. package/dist/common-auth-embedded/auth-menu/opencode-v1.js +47 -0
  65. package/dist/common-auth-embedded/auth-menu/select.d.ts +21 -0
  66. package/dist/common-auth-embedded/auth-menu/select.js +159 -0
  67. package/dist/common-auth-embedded/auth-menu/terminal.d.ts +43 -0
  68. package/dist/common-auth-embedded/auth-menu/terminal.js +19 -0
  69. package/dist/common-auth-embedded/claustrum/consumer.d.ts +102 -0
  70. package/dist/common-auth-embedded/claustrum/consumer.js +298 -0
  71. package/dist/common-auth-embedded/claustrum/custody.d.ts +175 -0
  72. package/dist/common-auth-embedded/claustrum/custody.js +354 -0
  73. package/dist/common-auth-embedded/claustrum/enrollment.d.ts +158 -0
  74. package/dist/common-auth-embedded/claustrum/enrollment.js +759 -0
  75. package/dist/common-auth-embedded/claustrum/errors.d.ts +16 -0
  76. package/dist/common-auth-embedded/claustrum/errors.js +8 -0
  77. package/dist/common-auth-embedded/claustrum/host-slot.d.ts +39 -0
  78. package/dist/common-auth-embedded/claustrum/host-slot.js +72 -0
  79. package/dist/common-auth-embedded/claustrum/index.d.ts +18 -0
  80. package/dist/common-auth-embedded/claustrum/index.js +22 -0
  81. package/dist/common-auth-embedded/claustrum/interlock.d.ts +27 -0
  82. package/dist/common-auth-embedded/claustrum/interlock.js +29 -0
  83. package/dist/common-auth-embedded/claustrum/roster.d.ts +208 -0
  84. package/dist/common-auth-embedded/claustrum/roster.js +536 -0
  85. package/dist/common-auth-embedded/commands/builtins.d.ts +71 -0
  86. package/dist/common-auth-embedded/commands/builtins.js +534 -0
  87. package/dist/common-auth-embedded/commands/index.d.ts +10 -0
  88. package/dist/common-auth-embedded/commands/index.js +5 -0
  89. package/dist/common-auth-embedded/commands/menu.d.ts +61 -0
  90. package/dist/common-auth-embedded/commands/menu.js +300 -0
  91. package/dist/common-auth-embedded/commands/model.d.ts +238 -0
  92. package/dist/common-auth-embedded/commands/model.js +16 -0
  93. package/dist/common-auth-embedded/commands/pi.d.ts +23 -0
  94. package/dist/common-auth-embedded/commands/pi.js +206 -0
  95. package/dist/common-auth-embedded/commands/seam.d.ts +72 -0
  96. package/dist/common-auth-embedded/commands/seam.js +301 -0
  97. package/dist/common-auth-embedded/fs/atomic-write.d.ts +20 -0
  98. package/dist/common-auth-embedded/fs/atomic-write.js +80 -0
  99. package/dist/common-auth-embedded/fs/index.d.ts +7 -0
  100. package/dist/common-auth-embedded/fs/index.js +6 -0
  101. package/dist/common-auth-embedded/fs/lock-constants.d.ts +14 -0
  102. package/dist/common-auth-embedded/fs/lock-constants.js +14 -0
  103. package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +33 -0
  104. package/dist/common-auth-embedded/fs/refresh-file-lock.js +501 -0
  105. package/dist/common-auth-embedded/fs/with-lock.d.ts +33 -0
  106. package/dist/common-auth-embedded/fs/with-lock.js +48 -0
  107. package/dist/common-auth-embedded/logger/capture-sink.d.ts +14 -0
  108. package/dist/common-auth-embedded/logger/capture-sink.js +13 -0
  109. package/dist/common-auth-embedded/logger/engine.d.ts +100 -0
  110. package/dist/common-auth-embedded/logger/engine.js +246 -0
  111. package/dist/common-auth-embedded/logger/index.d.ts +6 -0
  112. package/dist/common-auth-embedded/logger/index.js +3 -0
  113. package/dist/common-auth-embedded/logger/redact.d.ts +11 -0
  114. package/dist/common-auth-embedded/logger/redact.js +83 -0
  115. package/dist/common-auth-embedded/pi-slot/index.d.ts +3 -0
  116. package/dist/common-auth-embedded/pi-slot/index.js +2 -0
  117. package/dist/common-auth-embedded/pi-slot/lock.js +212 -0
  118. package/dist/common-auth-embedded/pi-slot/slot.d.ts +49 -0
  119. package/dist/common-auth-embedded/pi-slot/slot.js +383 -0
  120. package/dist/common-auth-embedded/quota/codec.d.ts +11 -0
  121. package/dist/common-auth-embedded/quota/codec.js +12 -0
  122. package/dist/common-auth-embedded/quota/format.d.ts +46 -0
  123. package/dist/common-auth-embedded/quota/format.js +138 -0
  124. package/dist/common-auth-embedded/quota/index.d.ts +10 -0
  125. package/dist/common-auth-embedded/quota/index.js +5 -0
  126. package/dist/common-auth-embedded/quota/map.d.ts +63 -0
  127. package/dist/common-auth-embedded/quota/map.js +105 -0
  128. package/dist/common-auth-embedded/quota/merge.d.ts +48 -0
  129. package/dist/common-auth-embedded/quota/merge.js +162 -0
  130. package/dist/common-auth-embedded/quota/projection.d.ts +64 -0
  131. package/dist/common-auth-embedded/quota/projection.js +134 -0
  132. package/dist/common-auth-embedded/routing/admission.d.ts +93 -0
  133. package/dist/common-auth-embedded/routing/admission.js +142 -0
  134. package/dist/common-auth-embedded/routing/index.d.ts +8 -0
  135. package/dist/common-auth-embedded/routing/index.js +4 -0
  136. package/dist/common-auth-embedded/routing/ordered.d.ts +47 -0
  137. package/dist/common-auth-embedded/routing/ordered.js +58 -0
  138. package/dist/common-auth-embedded/routing/pins.d.ts +22 -0
  139. package/dist/common-auth-embedded/routing/pins.js +34 -0
  140. package/dist/common-auth-embedded/routing/sticky.d.ts +158 -0
  141. package/dist/common-auth-embedded/routing/sticky.js +339 -0
  142. package/dist/common-auth-embedded/source-output.json +1090 -0
  143. package/dist/common-auth-embedded/store/attribution.d.ts +28 -0
  144. package/dist/common-auth-embedded/store/attribution.js +60 -0
  145. package/dist/common-auth-embedded/store/errors.d.ts +55 -0
  146. package/dist/common-auth-embedded/store/errors.js +38 -0
  147. package/dist/common-auth-embedded/store/hooks.d.ts +12 -0
  148. package/dist/common-auth-embedded/store/hooks.js +34 -0
  149. package/dist/common-auth-embedded/store/identity.d.ts +39 -0
  150. package/dist/common-auth-embedded/store/identity.js +79 -0
  151. package/dist/common-auth-embedded/store/index.d.ts +21 -0
  152. package/dist/common-auth-embedded/store/index.js +7 -0
  153. package/dist/common-auth-embedded/store/mutate.d.ts +189 -0
  154. package/dist/common-auth-embedded/store/mutate.js +500 -0
  155. package/dist/common-auth-embedded/store/pool.d.ts +196 -0
  156. package/dist/common-auth-embedded/store/pool.js +120 -0
  157. package/dist/common-auth-embedded/store/provider-state.d.ts +131 -0
  158. package/dist/common-auth-embedded/store/provider-state.js +226 -0
  159. package/dist/common-auth-embedded/store/publication.d.ts +47 -0
  160. package/dist/common-auth-embedded/store/publication.js +304 -0
  161. package/dist/common-auth-embedded/store/pull.d.ts +36 -0
  162. package/dist/common-auth-embedded/store/pull.js +143 -0
  163. package/dist/common-auth-embedded/store/refresh-lock.d.ts +63 -0
  164. package/dist/common-auth-embedded/store/refresh-lock.js +136 -0
  165. package/dist/common-auth-embedded/store/refresh.d.ts +72 -0
  166. package/dist/common-auth-embedded/store/refresh.js +225 -0
  167. package/dist/common-auth-embedded/store/reserved.js +13 -0
  168. package/dist/common-auth-embedded/store/rows.d.ts +313 -0
  169. package/dist/common-auth-embedded/store/rows.js +1258 -0
  170. package/dist/common-auth-embedded/store/runtime.d.ts +36 -0
  171. package/dist/common-auth-embedded/store/runtime.js +68 -0
  172. package/dist/common-auth-embedded/store/schema.d.ts +443 -0
  173. package/dist/common-auth-embedded/store/schema.js +754 -0
  174. package/dist/common-auth-embedded/store/settings.d.ts +63 -0
  175. package/dist/common-auth-embedded/store/settings.js +120 -0
  176. package/dist/common-auth-embedded/store/torn.d.ts +78 -0
  177. package/dist/common-auth-embedded/store/torn.js +294 -0
  178. package/dist/common-auth-embedded/util/any-signal.js +42 -0
  179. package/dist/common-auth-runtime.d.ts +73 -0
  180. package/dist/common-auth-runtime.d.ts.map +1 -0
  181. package/dist/common-auth-runtime.js +65 -0
  182. package/dist/common-auth-runtime.js.map +1 -0
  183. package/dist/credential-redaction.d.ts +13 -0
  184. package/dist/credential-redaction.d.ts.map +1 -0
  185. package/dist/credential-redaction.js +230 -0
  186. package/dist/credential-redaction.js.map +1 -0
  187. package/dist/fetch-timeout.d.ts +4 -2
  188. package/dist/fetch-timeout.d.ts.map +1 -1
  189. package/dist/fetch-timeout.js +121 -14
  190. package/dist/fetch-timeout.js.map +1 -1
  191. package/dist/index.d.ts +16 -1
  192. package/dist/index.d.ts.map +1 -1
  193. package/dist/index.js +15 -0
  194. package/dist/index.js.map +1 -1
  195. package/dist/quota-manager.d.ts.map +1 -1
  196. package/dist/quota-manager.js +35 -10
  197. package/dist/quota-manager.js.map +1 -1
  198. package/dist/quota-presentation.d.ts +10 -0
  199. package/dist/quota-presentation.d.ts.map +1 -0
  200. package/dist/quota-presentation.js +60 -0
  201. package/dist/quota-presentation.js.map +1 -0
  202. package/dist/quota-types.d.ts +2 -0
  203. package/dist/quota-types.d.ts.map +1 -1
  204. package/dist/rotation.d.ts +8 -0
  205. package/dist/rotation.d.ts.map +1 -1
  206. package/dist/rotation.js +24 -0
  207. package/dist/rotation.js.map +1 -1
  208. package/dist/transform/claude.d.ts +7 -3
  209. package/dist/transform/claude.d.ts.map +1 -1
  210. package/dist/transform/claude.js +6 -2
  211. package/dist/transform/claude.js.map +1 -1
  212. package/dist/transform/types.d.ts +1 -1
  213. package/dist/transform/types.d.ts.map +1 -1
  214. package/dist/vault-account-source.d.ts +183 -0
  215. package/dist/vault-account-source.d.ts.map +1 -0
  216. package/dist/vault-account-source.js +296 -0
  217. package/dist/vault-account-source.js.map +1 -0
  218. package/dist/vault-enrollment-authority.d.ts +16 -0
  219. package/dist/vault-enrollment-authority.d.ts.map +1 -0
  220. package/dist/vault-enrollment-authority.js +19 -0
  221. package/dist/vault-enrollment-authority.js.map +1 -0
  222. package/dist/vault-family.d.ts +11 -0
  223. package/dist/vault-family.d.ts.map +1 -0
  224. package/dist/vault-family.js +24 -0
  225. package/dist/vault-family.js.map +1 -0
  226. package/dist/vault-provider-state.d.ts +162 -0
  227. package/dist/vault-provider-state.d.ts.map +1 -0
  228. package/dist/vault-provider-state.js +422 -0
  229. package/dist/vault-provider-state.js.map +1 -0
  230. 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
+ }