@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,136 @@
1
+ import { acquireRefreshFileLock } from '../fs/refresh-file-lock.js';
2
+ import { LockContentionError } from '../fs/with-lock.js';
3
+ /**
4
+ * Store lock defaults: a 15 000 ms bounded wait retried every 50 ms plus
5
+ * jitter, and a renewed 10 000 ms lease.
6
+ */
7
+ export const POOL_LOCK_DEFAULTS = Object.freeze({
8
+ ttlMs: 10_000,
9
+ timeoutMs: 15_000,
10
+ retryMs: 50,
11
+ renew: true,
12
+ });
13
+ /**
14
+ * Takes one lock, retrying while a live holder has it until `timeoutMs` has
15
+ * passed on the real clock, then throws `LockContentionError`. The lease is
16
+ * asserted as owned once acquired, so a caller never proceeds on a lease that
17
+ * expired during the wait.
18
+ */
19
+ export async function acquirePoolLock(spec, defaults, env) {
20
+ const emit = (type) => {
21
+ try {
22
+ const result = env.onLockEvent?.({
23
+ type,
24
+ name: spec.name,
25
+ path: spec.path,
26
+ });
27
+ if (result &&
28
+ (typeof result === 'object' || typeof result === 'function') &&
29
+ 'then' in result &&
30
+ typeof result.then === 'function') {
31
+ void Promise.resolve(result).catch(() => { });
32
+ }
33
+ }
34
+ catch {
35
+ // Lock observers must not affect acquisition or release.
36
+ }
37
+ };
38
+ const options = { ...defaults, ...definedOnly(spec) };
39
+ const started = performance.now();
40
+ for (;;) {
41
+ const lock = await acquireRefreshFileLock({
42
+ name: spec.name,
43
+ path: spec.path,
44
+ ttlMs: options.ttlMs,
45
+ now: env.now,
46
+ renew: options.renew,
47
+ onContended: () => emit('contended'),
48
+ ...(options.renewIntervalMs !== undefined
49
+ ? { renewIntervalMs: options.renewIntervalMs }
50
+ : {}),
51
+ ...(env.onLockStep
52
+ ? {
53
+ onStep: (step) => env.onLockStep?.({ name: spec.name, path: spec.path }, step),
54
+ }
55
+ : {}),
56
+ });
57
+ if (lock) {
58
+ emit('acquired');
59
+ const held = {
60
+ name: spec.name,
61
+ path: spec.path,
62
+ assertOwned: () => lock.assertOwned(),
63
+ release: async () => {
64
+ await lock.release();
65
+ emit('released');
66
+ },
67
+ };
68
+ try {
69
+ await held.assertOwned();
70
+ }
71
+ catch (error) {
72
+ await held.release();
73
+ throw error;
74
+ }
75
+ return held;
76
+ }
77
+ const remaining = options.timeoutMs - (performance.now() - started);
78
+ if (remaining <= 0) {
79
+ throw new LockContentionError({
80
+ target: spec.path,
81
+ name: spec.name,
82
+ timeoutMs: options.timeoutMs,
83
+ });
84
+ }
85
+ const jitter = Math.floor(Math.random() * (options.retryMs + 1));
86
+ await new Promise((resolve) => setTimeout(resolve, Math.min(options.retryMs + jitter, remaining)));
87
+ }
88
+ }
89
+ function definedOnly(spec) {
90
+ const out = {};
91
+ if (spec.ttlMs !== undefined)
92
+ out.ttlMs = spec.ttlMs;
93
+ if (spec.timeoutMs !== undefined)
94
+ out.timeoutMs = spec.timeoutMs;
95
+ if (spec.retryMs !== undefined)
96
+ out.retryMs = spec.retryMs;
97
+ if (spec.renew !== undefined)
98
+ out.renew = spec.renew;
99
+ if (spec.renewIntervalMs !== undefined)
100
+ out.renewIntervalMs = spec.renewIntervalMs;
101
+ return out;
102
+ }
103
+ /**
104
+ * Locks taken in order and released in reverse. Every acquisition re-asserts
105
+ * the leases already held, because a wait is exactly when an earlier lease
106
+ * can expire unnoticed.
107
+ */
108
+ export class LockStack {
109
+ defaults;
110
+ env;
111
+ held = [];
112
+ constructor(defaults, env) {
113
+ this.defaults = defaults;
114
+ this.env = env;
115
+ }
116
+ async acquire(spec) {
117
+ const lock = await acquirePoolLock(spec, this.defaults, this.env);
118
+ this.held.push(lock);
119
+ await this.assertAll();
120
+ return lock;
121
+ }
122
+ async assertAll() {
123
+ for (const lock of this.held)
124
+ await lock.assertOwned();
125
+ }
126
+ /** Releases every lock taken after `mark` (a length of `held`), newest first. */
127
+ async releaseTo(mark) {
128
+ while (this.held.length > mark) {
129
+ const lock = this.held.pop();
130
+ await lock?.release().catch(() => { });
131
+ }
132
+ }
133
+ async releaseAll() {
134
+ await this.releaseTo(0);
135
+ }
136
+ }
@@ -0,0 +1,72 @@
1
+ import type { PoolLockSpec } from './refresh-lock.js';
2
+ import { type FailureHook } from './rows.js';
3
+ import { type StoreRuntime } from './runtime.js';
4
+ import { type OAuthCredential, type PoolRow, type StoredCredential } from './schema.js';
5
+ /** What the injected provider refresh function returns. */
6
+ export interface ProviderRefreshResult {
7
+ access: string;
8
+ refresh: string;
9
+ expires: number;
10
+ expiresIn?: number;
11
+ /** The account's wire identity, when the provider reports one. */
12
+ identity?: string;
13
+ /**
14
+ * Provider state that changes with the new token (needs the store's
15
+ * provider-state codec). It is merged with the row's value on disk
16
+ * (`ProviderStateCodec.merge`) and written in the same state write as the
17
+ * rotated credential, under the same commit fence. Left out, the row keeps
18
+ * its value.
19
+ */
20
+ providerState?: unknown;
21
+ }
22
+ export type ProviderRefresh = (credential: OAuthCredential & {
23
+ lastRefreshedAt?: number;
24
+ }, row: PoolRow) => Promise<ProviderRefreshResult>;
25
+ export interface RefreshOptions {
26
+ /** The one provider-wide lock serialising every provider call. */
27
+ providerLock?: PoolLockSpec;
28
+ /** Taken after the provider-wide lock in this order, released in reverse. */
29
+ extraLocks?: readonly PoolLockSpec[];
30
+ /**
31
+ * Awaited once an ordinary rotation is persisted and the store locks released.
32
+ * Never called for `identity-contradicted`: its credential must not propagate
33
+ * as the row's recorded account.
34
+ */
35
+ onPersisted?: (rowId: string, credential: StoredCredential) => void | Promise<void>;
36
+ onFailure?: FailureHook;
37
+ /**
38
+ * Awaited before any lock is taken, on the locked re-read, and at commit
39
+ * time under the store locks; a reason refuses the refresh there, leaving
40
+ * the stored credential untouched and discarding any rotated material.
41
+ */
42
+ refuse?: (row: PoolRow) => string | undefined | Promise<string | undefined>;
43
+ }
44
+ export type RefreshOutcome = {
45
+ status: 'rotated';
46
+ rowId: string;
47
+ credential: StoredCredential;
48
+ identity?: string;
49
+ }
50
+ /**
51
+ * The provider's successor credential is stored bound to expectedIdentity,
52
+ * not returnedIdentity. The row stays disabled until the adapter supplies
53
+ * an identity-validated replacement credential through replace.
54
+ */
55
+ | {
56
+ status: 'identity-contradicted';
57
+ rowId: string;
58
+ expectedIdentity: string;
59
+ returnedIdentity: string;
60
+ credential: StoredCredential;
61
+ } | {
62
+ status: 'refused';
63
+ rowId: string;
64
+ reason: string;
65
+ };
66
+ /**
67
+ * Refreshes one OAuth row. Locks are taken in the fixed order row lock,
68
+ * provider-wide lock, extra locks, and the store locks only around the
69
+ * capture and the commit; ownership of every held lease is asserted after
70
+ * each wait, immediately before the provider call and before each write.
71
+ */
72
+ export declare function refreshRow(rt: StoreRuntime, id: string, provider: ProviderRefresh, options?: RefreshOptions): Promise<RefreshOutcome>;
@@ -0,0 +1,225 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { PoolOperationError } from './errors.js';
3
+ import { assertNotInsideHook, runInsideHook } from './hooks.js';
4
+ import { IDENTITY_CONTRADICTED_REASON_PREFIX, recordIdentityIn, } from './identity.js';
5
+ import { runOperation, withTransaction } from './mutate.js';
6
+ import { acceptProviderState, mergedProviderState } from './provider-state.js';
7
+ import { assertNotReserved } from './reserved.js';
8
+ import { rotateIn } from './rows.js';
9
+ import { readRow, refusal, requireBound, rowLockSpec, } from './runtime.js';
10
+ import { rotationStamp, rotationStampUntrusted, rowLockKey, } from './schema.js';
11
+ import { applyTransition } from './torn.js';
12
+ function requireRefreshable(id, row) {
13
+ assertNotReserved('refresh', id, row?.staged);
14
+ if (!row)
15
+ throw refusal('refresh', id, 'unknown-row', `no row ${id} in the pool`);
16
+ if (row.invalid)
17
+ throw refusal('refresh', id, 'invalid-row', `row ${id} is invalid`);
18
+ if (row.type !== 'oauth' || row.credential?.type !== 'oauth')
19
+ throw refusal('refresh', id, 'no-credential', `row ${id} holds no OAuth credential`);
20
+ if (!row.enabled)
21
+ throw refusal('refresh', id, 'row-disabled', `row ${id} is disabled`);
22
+ return row;
23
+ }
24
+ function stampAhead(id) {
25
+ return refusal('refresh', id, 'refresh-stamp-ahead', `row ${id}'s stored refresh stamp is ahead of the clock; a rotation now could not be stamped newer within the trust bound`, true);
26
+ }
27
+ /**
28
+ * Refreshes one OAuth row. Locks are taken in the fixed order row lock,
29
+ * provider-wide lock, extra locks, and the store locks only around the
30
+ * capture and the commit; ownership of every held lease is asserted after
31
+ * each wait, immediately before the provider call and before each write.
32
+ */
33
+ export async function refreshRow(rt, id, provider, options = {}) {
34
+ assertNotInsideHook('refresh');
35
+ const { ctx } = rt;
36
+ return runOperation(ctx, 'refresh', id, options.onFailure, async (locks, progress) => {
37
+ const { row: seen } = await readRow(rt, 'refresh', id);
38
+ requireRefreshable(id, seen);
39
+ const early = await options.refuse?.(seen);
40
+ if (early !== undefined)
41
+ return { status: 'refused', rowId: id, reason: early };
42
+ let key = rowLockKey(seen);
43
+ let captured;
44
+ for (let attempt = 0;; attempt++) {
45
+ await locks.acquire(rowLockSpec(rt, { id, identity: key === id ? undefined : key }));
46
+ await locks.acquire(options.providerLock ?? rt.providerLock);
47
+ for (const extra of options.extraLocks ?? [])
48
+ await locks.acquire(extra);
49
+ // The capture may write the config once, to give a row its per-row
50
+ // entry. That write is setup, not the rotation, so it is counted apart
51
+ // and never makes a later failure report `after-first-write`.
52
+ const captureProgress = { writes: 0 };
53
+ const read = await withTransaction(ctx, locks, captureProgress, { operation: 'refresh', rowId: id }, async (tx) => {
54
+ let row = requireRefreshable(id, tx.row(id));
55
+ requireBound('refresh', row);
56
+ if (!row.hasEntry) {
57
+ tx.setEntry(id, { credentialEpoch: 1, needsFirstReading: true });
58
+ await tx.commitConfig();
59
+ row = requireRefreshable(id, tx.row(id));
60
+ }
61
+ if (rowLockKey(row) !== key)
62
+ return { keyNow: rowLockKey(row) };
63
+ const reason = await options.refuse?.(row);
64
+ if (reason !== undefined)
65
+ return { refused: reason };
66
+ return {
67
+ row,
68
+ credential: row.credential,
69
+ credentialEpoch: row.credentialEpoch,
70
+ identity: row.identity,
71
+ };
72
+ });
73
+ if ('refused' in read)
74
+ return { status: 'refused', rowId: id, reason: read.refused };
75
+ if ('keyNow' in read) {
76
+ await locks.releaseAll();
77
+ if (attempt >= 1)
78
+ throw refusal('refresh', id, 'row-key-changed', `row ${id}'s wire identity changed twice while its lock was held`, true);
79
+ key = read.keyNow;
80
+ continue;
81
+ }
82
+ captured = read;
83
+ break;
84
+ }
85
+ if (rotationStampUntrusted(captured.credential.lastRefreshedAt, ctx.now()))
86
+ throw stampAhead(id);
87
+ await ctx.hold?.('refresh-before-provider', id);
88
+ await locks.assertAll();
89
+ let result;
90
+ try {
91
+ result = await provider(captured.credential, captured.row);
92
+ }
93
+ catch (cause) {
94
+ throw new PoolOperationError({
95
+ operation: 'refresh',
96
+ rowId: id,
97
+ phase: 'before-first-write',
98
+ retryable: true,
99
+ kind: 'provider',
100
+ message: 'the provider refresh failed',
101
+ cause,
102
+ });
103
+ }
104
+ if (typeof result?.refresh !== 'string' || !result.refresh.trim())
105
+ throw refusal('refresh', id, 'provider', 'the provider returned no refresh token', true);
106
+ const incoming = result.providerState === undefined
107
+ ? undefined
108
+ : acceptProviderState(ctx.providerState, 'refresh', id, result.providerState, 'the provider state the provider returned');
109
+ const commit = await withTransaction(ctx, locks, progress, { operation: 'refresh', rowId: id }, async (tx) => {
110
+ const current = tx.row(id);
111
+ const entry = tx.entry(id);
112
+ if (!current ||
113
+ current.invalid ||
114
+ !entry ||
115
+ entry.credentialEpoch !== captured.credentialEpoch ||
116
+ current.identity !== captured.identity)
117
+ throw new PoolOperationError({
118
+ operation: 'refresh',
119
+ rowId: id,
120
+ phase: 'before-first-write',
121
+ retryable: true,
122
+ kind: 'attribution',
123
+ message: `row ${id} changed credential while its refresh was in flight; the rotation is discarded`,
124
+ });
125
+ // The epoch fence above does not see a credential another writer
126
+ // swapped in under the same epoch; the stamp does, and committing
127
+ // the rotation would stamp the swapped row as bound.
128
+ requireBound('refresh', current);
129
+ const reason = await options.refuse?.(current);
130
+ if (reason !== undefined)
131
+ return { refused: reason };
132
+ const now = ctx.now();
133
+ const prior = current.credential?.type === 'oauth'
134
+ ? current.credential.lastRefreshedAt
135
+ : undefined;
136
+ if (rotationStampUntrusted(prior, now))
137
+ throw stampAhead(id);
138
+ const credential = {
139
+ type: 'oauth',
140
+ access: result.access,
141
+ refresh: result.refresh,
142
+ expires: result.expires,
143
+ };
144
+ const contradiction = current.identity !== undefined &&
145
+ result.identity !== undefined &&
146
+ current.identity !== result.identity
147
+ ? {
148
+ expectedIdentity: current.identity,
149
+ returnedIdentity: result.identity,
150
+ }
151
+ : undefined;
152
+ const transition = contradiction
153
+ ? {
154
+ mark: randomUUID(),
155
+ enabled: false,
156
+ reason: `${IDENTITY_CONTRADICTED_REASON_PREFIX}${JSON.stringify(contradiction)}`,
157
+ }
158
+ : undefined;
159
+ const learnt = current.identity === undefined && result.identity
160
+ ? result.identity
161
+ : undefined;
162
+ // The rotated credential's stamp names a learnt identity before the
163
+ // config records it, so a crash between the two writes is completed
164
+ // forward rather than leaving an identity no stamp proves.
165
+ const stored = await rotateIn(rt, tx, id, credential, {
166
+ stamp: rotationStamp(prior, now),
167
+ // Write state first with the disable marker, then config below.
168
+ // Once the successor is durable, recovery projects the disable even
169
+ // if config has not landed. A crash before the first rename still
170
+ // loses an in-memory provider reply, as in any ordinary refresh.
171
+ transition,
172
+ identity: learnt,
173
+ ...(incoming !== undefined
174
+ ? {
175
+ providerState: mergedProviderState(ctx.providerState, 'refresh', id, current.providerState, incoming),
176
+ }
177
+ : {}),
178
+ });
179
+ if (transition !== undefined) {
180
+ applyTransition(tx, id, transition);
181
+ await tx.commitConfig();
182
+ }
183
+ let identity = current.identity;
184
+ if (learnt !== undefined) {
185
+ recordIdentityIn(tx, id, learnt);
186
+ identity = learnt;
187
+ await tx.commitConfig();
188
+ }
189
+ return { stored, identity, contradiction, refused: undefined };
190
+ });
191
+ if (commit.refused !== undefined)
192
+ return { status: 'refused', rowId: id, reason: commit.refused };
193
+ if (commit.contradiction !== undefined)
194
+ return {
195
+ status: 'identity-contradicted',
196
+ rowId: id,
197
+ ...commit.contradiction,
198
+ credential: commit.stored,
199
+ };
200
+ if (options.onPersisted) {
201
+ try {
202
+ const persisted = options.onPersisted;
203
+ await runInsideHook('refresh', () => persisted(id, commit.stored));
204
+ }
205
+ catch (cause) {
206
+ throw new PoolOperationError({
207
+ operation: 'refresh',
208
+ rowId: id,
209
+ phase: 'after-first-write',
210
+ retryable: false,
211
+ kind: 'after-persist-hook',
212
+ committed: commit.stored,
213
+ message: 'the after-persist hook threw; the rotation stays committed',
214
+ cause,
215
+ });
216
+ }
217
+ }
218
+ return {
219
+ status: 'rotated',
220
+ rowId: id,
221
+ credential: commit.stored,
222
+ ...(commit.identity !== undefined ? { identity: commit.identity } : {}),
223
+ };
224
+ });
225
+ }
@@ -0,0 +1,13 @@
1
+ import { PoolOperationError } from './errors.js';
2
+ /** A stored marker reserves the row even when the rest of its entry is invalid. */
3
+ export function assertNotReserved(operation, id, reservation) {
4
+ if (reservation !== undefined)
5
+ throw new PoolOperationError({
6
+ operation,
7
+ rowId: id,
8
+ kind: 'row-staged',
9
+ phase: operation === 'pull' ? 'pull' : 'before-first-write',
10
+ retryable: false,
11
+ message: `row ${id} is reserved for roster publication`,
12
+ });
13
+ }