@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,6 @@
1
+ export { writeJsonAtomic } from './atomic-write.js';
2
+ export { WRITER_LOCK_CONSTANTS } from './lock-constants.js';
3
+ // The try-once lock underneath withLock, for callers that hold a lock across
4
+ // several steps or poll instead of waiting (for example a refresh lease).
5
+ export { acquireRefreshFileLock, isLostMarkerRaceError, } from './refresh-file-lock.js';
6
+ export { LockContentionError, LockOwnershipError, lockPathFor, withLock, } from './with-lock.js';
@@ -0,0 +1,14 @@
1
+ export declare const WRITER_LOCK_CONSTANTS: Readonly<{
2
+ sidebar: Readonly<{
3
+ name: "sidebar-write";
4
+ ttlMs: 10000;
5
+ timeoutMs: 15000;
6
+ renew: true;
7
+ }>;
8
+ preferences: Readonly<{
9
+ name: "preferences";
10
+ ttlMs: 10000;
11
+ timeoutMs: 2000;
12
+ renew: true;
13
+ }>;
14
+ }>;
@@ -0,0 +1,14 @@
1
+ export const WRITER_LOCK_CONSTANTS = Object.freeze({
2
+ sidebar: Object.freeze({
3
+ name: 'sidebar-write',
4
+ ttlMs: 10_000,
5
+ timeoutMs: 15_000,
6
+ renew: true,
7
+ }),
8
+ preferences: Object.freeze({
9
+ name: 'preferences',
10
+ ttlMs: 10_000,
11
+ timeoutMs: 2_000,
12
+ renew: true,
13
+ }),
14
+ });
@@ -0,0 +1,33 @@
1
+ export declare function isLostMarkerRaceError(error: unknown): boolean;
2
+ export interface LockLoss {
3
+ readonly reason: 'taken-over' | 'expired' | 'unreadable' | 'renewal-failed' | 'marker-lost';
4
+ readonly expectedOwnerId: string;
5
+ readonly observedOwnerId?: string;
6
+ readonly observedExpiresAt?: number;
7
+ }
8
+ export interface RefreshFileLock {
9
+ readonly ownerId: string;
10
+ assertOwned(): Promise<void>;
11
+ release(): Promise<void>;
12
+ /** Resolves once on detected loss; remains pending after an owner's release. */
13
+ whenLost(): Promise<LockLoss>;
14
+ hasLost(): boolean;
15
+ }
16
+ export declare function acquireRefreshFileLock(options: {
17
+ name: string;
18
+ ttlMs: number;
19
+ /**
20
+ * File the lock is named after. Required: the lock and the write it guards
21
+ * must target the same file, and a default resolved in here could only ever
22
+ * be one host's path.
23
+ */
24
+ path: string;
25
+ now?: () => number;
26
+ renew?: boolean;
27
+ renewIntervalMs?: number;
28
+ /** Notifies a live-owner refusal without awaiting; observer failures are ignored. */
29
+ onContended?: () => void;
30
+ /** Synchronous, never awaited; throws and returned promise/thenable rejections are isolated. */
31
+ onRenewalTimer?: (event: 'scheduled' | 'cancelled') => void;
32
+ onStep?: (step: 'stale-marker-stat' | 'stale-marker-claimed' | 'stale-lock-observed' | 'stale-lock-confirmed' | 'stale-lock-removed' | 'stale-lock-recreated' | 'eviction-marker-acquired' | 'renewal-owner-confirmed' | 'renewal-marker-unavailable' | 'renewal-write-fenced' | 'renewal-write-ready' | 'relinquish-read' | 'renewal-finished' | 'release-owner-confirmed') => void | Promise<void>;
33
+ }): Promise<RefreshFileLock | null>;
@@ -0,0 +1,501 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, open, readFile, rename, rm, stat, writeFile, } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { LockOwnershipError, lockPathFor } from './with-lock.js';
5
+ const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis);
6
+ const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis);
7
+ // A concurrent contender renaming the freshly-created eviction-marker directory
8
+ // away surfaces the vanished parent differently per platform: ENOENT on Linux,
9
+ // EINVAL or ENOTDIR on macOS/APFS. All three mean the marker is no longer ours
10
+ // to hold — a lost race the caller should retry, not a fatal lock error.
11
+ export function isLostMarkerRaceError(error) {
12
+ const code = error?.code;
13
+ return code === 'ENOENT' || code === 'EINVAL' || code === 'ENOTDIR';
14
+ }
15
+ export async function acquireRefreshFileLock(options) {
16
+ const lockPath = lockPathFor(options.path, options.name);
17
+ const legacyOwnerPath = join(lockPath, 'owner.json');
18
+ const ownerId = randomUUID();
19
+ const now = options.now ?? Date.now;
20
+ let renewTimer = null;
21
+ let released = false;
22
+ let loss;
23
+ let resolveLoss;
24
+ const lostPromise = new Promise((resolve) => {
25
+ resolveLoss = resolve;
26
+ });
27
+ function notifyTimerStep(event) {
28
+ try {
29
+ const result = options.onRenewalTimer?.(event);
30
+ if (result &&
31
+ (typeof result === 'object' || typeof result === 'function') &&
32
+ 'then' in result &&
33
+ typeof result.then === 'function') {
34
+ void Promise.resolve(result).catch(() => { });
35
+ }
36
+ }
37
+ catch {
38
+ // Timer observer failures must not replace the lock operation result.
39
+ }
40
+ }
41
+ function cancelRenewalTimer() {
42
+ if (!renewTimer)
43
+ return;
44
+ clearRefreshLockRenewalTimeout(renewTimer);
45
+ renewTimer = null;
46
+ notifyTimerStep('cancelled');
47
+ }
48
+ function recordLoss(reason, owner) {
49
+ if (loss || released)
50
+ return;
51
+ loss = Object.freeze({
52
+ reason,
53
+ expectedOwnerId: ownerId,
54
+ ...(typeof owner?.ownerId === 'string'
55
+ ? { observedOwnerId: owner.ownerId }
56
+ : {}),
57
+ ...(typeof owner?.expiresAt === 'number'
58
+ ? { observedExpiresAt: owner.expiresAt }
59
+ : {}),
60
+ });
61
+ cancelRenewalTimer();
62
+ resolveLoss(loss);
63
+ }
64
+ let renewalInFlight = null;
65
+ // Only the owner of this exclusively-created marker may remove or renew a
66
+ // lock. A contender recovering a stale marker can accidentally rename a newer
67
+ // marker instead; checking its owner file before each mutation prevents the
68
+ // displaced owner from continuing to act on the lock.
69
+ const evictPath = `${lockPath}.evicting`;
70
+ const evictOwnerPath = join(evictPath, 'owner.json');
71
+ const evictOwnerId = randomUUID();
72
+ const EVICT_TTL = 5_000;
73
+ const MAX_STEAL_ATTEMPTS = 8;
74
+ async function readOwner() {
75
+ try {
76
+ return JSON.parse(await readFile(lockPath, 'utf8'));
77
+ }
78
+ catch (error) {
79
+ const code = error.code;
80
+ if (code !== 'EISDIR')
81
+ throw error;
82
+ return JSON.parse(await readFile(legacyOwnerPath, 'utf8'));
83
+ }
84
+ }
85
+ async function writeOwner() {
86
+ // Readers must see a complete lease, even while renewal is writing.
87
+ const tempPath = `${lockPath}.${randomUUID()}.tmp`;
88
+ let handle;
89
+ let created = false;
90
+ try {
91
+ handle = await open(tempPath, 'wx', 0o600);
92
+ created = true;
93
+ await handle.writeFile(`${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`, 'utf8');
94
+ await handle.chmod(0o600);
95
+ await handle.close();
96
+ handle = undefined;
97
+ await rename(tempPath, lockPath);
98
+ created = false;
99
+ }
100
+ finally {
101
+ await handle?.close().catch(() => { });
102
+ if (created)
103
+ await rm(tempPath, { force: true }).catch(() => { });
104
+ }
105
+ }
106
+ async function tryAcquire() {
107
+ try {
108
+ await writeFile(lockPath, `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
109
+ return true;
110
+ }
111
+ catch (error) {
112
+ const code = error.code;
113
+ if (code === 'EEXIST' || code === 'EISDIR')
114
+ return false;
115
+ if (code === 'ENOENT') {
116
+ // Reboots can clear runtime temp directories before the next lock acquisition.
117
+ await mkdir(dirname(lockPath), { recursive: true });
118
+ try {
119
+ await writeFile(lockPath, `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
120
+ return true;
121
+ }
122
+ catch (retryError) {
123
+ const retryCode = retryError.code;
124
+ if (retryCode === 'EEXIST' || retryCode === 'EISDIR')
125
+ return false;
126
+ throw retryError;
127
+ }
128
+ }
129
+ throw error;
130
+ }
131
+ }
132
+ async function backoff() {
133
+ await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 4)));
134
+ }
135
+ async function lockIsLive() {
136
+ try {
137
+ const currentOwner = await readOwner();
138
+ return Number(currentOwner?.expiresAt) > now();
139
+ }
140
+ catch {
141
+ try {
142
+ const current = await stat(lockPath);
143
+ return current.mtimeMs + options.ttlMs > now();
144
+ }
145
+ catch {
146
+ // Lock doesn't exist — safe to acquire.
147
+ return false;
148
+ }
149
+ }
150
+ }
151
+ // Fail-closed: any read error means we do NOT own the marker.
152
+ async function ownsEvictionMarker() {
153
+ try {
154
+ const owner = JSON.parse(await readFile(evictOwnerPath, 'utf8'));
155
+ return owner?.ownerId === evictOwnerId;
156
+ }
157
+ catch {
158
+ return false;
159
+ }
160
+ }
161
+ async function releaseEvictionMarker() {
162
+ if (await ownsEvictionMarker()) {
163
+ await rm(evictPath, { recursive: true, force: true }).catch(() => { });
164
+ }
165
+ }
166
+ async function tryAcquireEvictionMarker() {
167
+ await mkdir(evictPath);
168
+ try {
169
+ await writeFile(evictOwnerPath, `${JSON.stringify({ ownerId: evictOwnerId, createdAt: now() })}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
170
+ }
171
+ catch (error) {
172
+ // A competing contender can rename our just-created marker directory
173
+ // away between the mkdir above and this write (the stale-marker steal
174
+ // path below does exactly that). That is a lost race, not a failure, so
175
+ // report it as such and let the caller back off and retry rather than
176
+ // failing the whole lock acquisition.
177
+ if (isLostMarkerRaceError(error))
178
+ return false;
179
+ await releaseEvictionMarker();
180
+ throw error;
181
+ }
182
+ if (options.onStep)
183
+ await options.onStep('eviction-marker-acquired');
184
+ return true;
185
+ }
186
+ async function recoverStaleEvictionMarker() {
187
+ let evictStat;
188
+ try {
189
+ evictStat = await stat(evictPath);
190
+ }
191
+ catch (error) {
192
+ if (error.code === 'ENOENT')
193
+ return 'missing';
194
+ throw error;
195
+ }
196
+ if (evictStat.mtimeMs + EVICT_TTL > now())
197
+ return 'fresh';
198
+ if (options.onStep)
199
+ await options.onStep('stale-marker-stat');
200
+ const claimedPath = `${evictPath}.${randomUUID()}`;
201
+ try {
202
+ await rename(evictPath, claimedPath);
203
+ }
204
+ catch (error) {
205
+ if (error.code === 'ENOENT')
206
+ return 'missing';
207
+ throw error;
208
+ }
209
+ if (options.onStep)
210
+ await options.onStep('stale-marker-claimed');
211
+ await rm(claimedPath, { recursive: true, force: true }).catch(() => { });
212
+ return 'recovered';
213
+ }
214
+ async function withEvictionMarker(action) {
215
+ try {
216
+ if (!(await tryAcquireEvictionMarker()))
217
+ return false;
218
+ }
219
+ catch (error) {
220
+ if (error.code === 'EEXIST')
221
+ return false;
222
+ throw error;
223
+ }
224
+ try {
225
+ await action();
226
+ }
227
+ finally {
228
+ await releaseEvictionMarker();
229
+ }
230
+ return true;
231
+ }
232
+ // Marker loss after a write may mean another contender has taken over.
233
+ // Delete only a record still owned by us, so an already-installed
234
+ // successor is preserved. The owner read and the removal are not atomic:
235
+ // a successor record written between them can still be deleted.
236
+ async function relinquishLockAfterMarkerLoss() {
237
+ for (let attempt = 0; attempt < MAX_STEAL_ATTEMPTS; attempt++) {
238
+ if (options.onStep)
239
+ await options.onStep('relinquish-read');
240
+ let owner;
241
+ try {
242
+ owner = await readOwner();
243
+ }
244
+ catch {
245
+ return;
246
+ }
247
+ if (owner?.ownerId !== ownerId)
248
+ return;
249
+ try {
250
+ await rm(lockPath, { recursive: true, force: true });
251
+ return;
252
+ }
253
+ catch {
254
+ await backoff();
255
+ }
256
+ }
257
+ }
258
+ function scheduleRenewal() {
259
+ if (!options.renew || released || loss)
260
+ return;
261
+ const intervalMs = options.renewIntervalMs ?? Math.max(1_000, Math.floor(options.ttlMs / 3));
262
+ renewTimer = setRefreshLockRenewalTimeout(() => {
263
+ renewTimer = null;
264
+ const renewal = (async () => {
265
+ let shouldReschedule = !released;
266
+ try {
267
+ const markerAcquired = await withEvictionMarker(async () => {
268
+ const owner = await readOwner();
269
+ const currentNow = now();
270
+ if (released || loss) {
271
+ shouldReschedule = false;
272
+ return;
273
+ }
274
+ if (owner?.ownerId !== ownerId) {
275
+ recordLoss('taken-over', owner);
276
+ shouldReschedule = false;
277
+ return;
278
+ }
279
+ // An expired lease is no longer ours to extend; a contender may
280
+ // already be eligible to acquire it.
281
+ if (!(Number(owner?.expiresAt) > currentNow)) {
282
+ recordLoss('expired', owner);
283
+ shouldReschedule = false;
284
+ return;
285
+ }
286
+ if (options.onStep)
287
+ await options.onStep('renewal-owner-confirmed');
288
+ if (released || loss) {
289
+ shouldReschedule = false;
290
+ return;
291
+ }
292
+ if (!(await ownsEvictionMarker()))
293
+ return;
294
+ if (options.onStep)
295
+ await options.onStep('renewal-write-fenced');
296
+ if (released || loss) {
297
+ shouldReschedule = false;
298
+ return;
299
+ }
300
+ if (!(await ownsEvictionMarker()))
301
+ return;
302
+ if (options.onStep)
303
+ await options.onStep('renewal-write-ready');
304
+ if (released || loss) {
305
+ shouldReschedule = false;
306
+ return;
307
+ }
308
+ await writeOwner();
309
+ if (!(await ownsEvictionMarker())) {
310
+ // If marker ownership cannot be read, stop claiming the lease
311
+ // and remove only a record that still carries our owner id.
312
+ recordLoss('marker-lost');
313
+ shouldReschedule = false;
314
+ await relinquishLockAfterMarkerLoss();
315
+ return;
316
+ }
317
+ });
318
+ if (!markerAcquired && options.onStep) {
319
+ await options.onStep('renewal-marker-unavailable');
320
+ }
321
+ }
322
+ catch {
323
+ // Retry transient failures only while the lease can still be verified.
324
+ try {
325
+ const owner = await readOwner();
326
+ if (owner?.ownerId !== ownerId)
327
+ recordLoss('taken-over', owner);
328
+ else if (!(Number(owner?.expiresAt) > now()))
329
+ recordLoss('expired', owner);
330
+ }
331
+ catch {
332
+ recordLoss('renewal-failed');
333
+ }
334
+ }
335
+ finally {
336
+ if (options.onStep) {
337
+ try {
338
+ await options.onStep('renewal-finished');
339
+ }
340
+ catch {
341
+ // Errors from the onStep observer must not reject the renewal.
342
+ }
343
+ }
344
+ if (shouldReschedule && !released && !loss)
345
+ scheduleRenewal();
346
+ }
347
+ })();
348
+ renewalInFlight = renewal;
349
+ void renewal.finally(() => {
350
+ if (renewalInFlight === renewal)
351
+ renewalInFlight = null;
352
+ });
353
+ }, intervalMs);
354
+ if ('unref' in renewTimer)
355
+ renewTimer.unref();
356
+ notifyTimerStep('scheduled');
357
+ }
358
+ function contended() {
359
+ try {
360
+ const result = options.onContended?.();
361
+ if (result &&
362
+ (typeof result === 'object' || typeof result === 'function') &&
363
+ 'then' in result &&
364
+ typeof result.then === 'function') {
365
+ void Promise.resolve(result).catch(() => { });
366
+ }
367
+ }
368
+ catch {
369
+ // Observers cannot change whether the lock is acquired.
370
+ }
371
+ return null;
372
+ }
373
+ let acquired = await tryAcquire();
374
+ if (!acquired) {
375
+ for (let attempt = 0; attempt < MAX_STEAL_ATTEMPTS; attempt++) {
376
+ acquired = await tryAcquire();
377
+ if (acquired)
378
+ break;
379
+ if (await lockIsLive())
380
+ return contended();
381
+ if (options.onStep)
382
+ await options.onStep('stale-lock-observed');
383
+ try {
384
+ if (!(await tryAcquireEvictionMarker())) {
385
+ await backoff();
386
+ continue;
387
+ }
388
+ }
389
+ catch (evictError) {
390
+ const code = evictError.code;
391
+ if (code !== 'EEXIST')
392
+ throw evictError;
393
+ const recovered = await recoverStaleEvictionMarker();
394
+ if (recovered === 'fresh')
395
+ return null;
396
+ await backoff();
397
+ continue;
398
+ }
399
+ try {
400
+ if (await lockIsLive())
401
+ return contended();
402
+ // Verify marker ownership before removing a lock found not live.
403
+ if (!(await ownsEvictionMarker()))
404
+ return null;
405
+ if (options.onStep)
406
+ await options.onStep('stale-lock-confirmed');
407
+ // The onStep callback can pause while another contender renames
408
+ // our marker, so verify ownership again after the callback.
409
+ if (!(await ownsEvictionMarker()))
410
+ return null;
411
+ await rm(lockPath, { recursive: true, force: true }).catch(() => { });
412
+ // Fence check 3: re-verify ownership after removing the stale lock.
413
+ if (!(await ownsEvictionMarker()))
414
+ return null;
415
+ if (options.onStep)
416
+ await options.onStep('stale-lock-removed');
417
+ acquired = await tryAcquire();
418
+ if (!acquired)
419
+ return null;
420
+ if (options.onStep)
421
+ await options.onStep('stale-lock-recreated');
422
+ // Fence check 4: re-verify ownership after acquiring the lock. If the
423
+ // marker was stolen between tryAcquire and this check, release the
424
+ // just-acquired lock and return null (fail-closed).
425
+ if (!(await ownsEvictionMarker())) {
426
+ await relinquishLockAfterMarkerLoss();
427
+ acquired = false;
428
+ return null;
429
+ }
430
+ break;
431
+ }
432
+ finally {
433
+ await releaseEvictionMarker();
434
+ }
435
+ }
436
+ }
437
+ if (!acquired)
438
+ return null;
439
+ scheduleRenewal();
440
+ return {
441
+ ownerId,
442
+ whenLost: () => lostPromise,
443
+ hasLost: () => loss !== undefined,
444
+ assertOwned: async () => {
445
+ let observed;
446
+ try {
447
+ const owner = await readOwner();
448
+ observed = owner;
449
+ if (!released &&
450
+ !loss &&
451
+ owner?.ownerId === ownerId &&
452
+ Number(owner?.expiresAt) > now())
453
+ return;
454
+ recordLoss(owner?.ownerId !== ownerId ? 'taken-over' : 'expired', owner);
455
+ }
456
+ catch {
457
+ // Unreadable ownership is not evidence of a valid lease.
458
+ recordLoss('unreadable');
459
+ }
460
+ throw new LockOwnershipError({
461
+ target: options.path,
462
+ name: options.name,
463
+ expectedOwnerId: ownerId,
464
+ ...(typeof observed?.ownerId === 'string'
465
+ ? { observedOwnerId: observed.ownerId }
466
+ : {}),
467
+ ...(typeof observed?.expiresAt === 'number'
468
+ ? { observedExpiresAt: observed.expiresAt }
469
+ : {}),
470
+ });
471
+ },
472
+ release: async () => {
473
+ released = true;
474
+ cancelRenewalTimer();
475
+ await renewalInFlight;
476
+ for (let attempt = 0; attempt < MAX_STEAL_ATTEMPTS; attempt++) {
477
+ try {
478
+ const markerAcquired = await withEvictionMarker(async () => {
479
+ const owner = await readOwner();
480
+ if (owner?.ownerId !== ownerId)
481
+ return;
482
+ if (options.onStep)
483
+ await options.onStep('release-owner-confirmed');
484
+ if (!(await ownsEvictionMarker()))
485
+ return;
486
+ await rm(lockPath, { recursive: true, force: true }).catch(() => { });
487
+ });
488
+ if (markerAcquired)
489
+ return;
490
+ await recoverStaleEvictionMarker();
491
+ }
492
+ catch {
493
+ return;
494
+ }
495
+ await backoff();
496
+ }
497
+ // Do not delete by pathname without the marker: bounded retries leave the
498
+ // lease to expire rather than risking removal of a successor's lock.
499
+ },
500
+ };
501
+ }
@@ -0,0 +1,33 @@
1
+ export declare function lockPathFor(target: string, name: string): string;
2
+ export declare class LockContentionError extends Error {
3
+ readonly details: {
4
+ target: string;
5
+ name: string;
6
+ timeoutMs: number;
7
+ };
8
+ constructor(details: {
9
+ target: string;
10
+ name: string;
11
+ timeoutMs: number;
12
+ });
13
+ }
14
+ export interface LockOwnershipDetails {
15
+ target: string;
16
+ name: string;
17
+ expectedOwnerId?: string;
18
+ observedOwnerId?: string;
19
+ observedExpiresAt?: number;
20
+ }
21
+ export declare class LockOwnershipError extends Error {
22
+ readonly details: LockOwnershipDetails;
23
+ constructor(details: LockOwnershipDetails);
24
+ }
25
+ export interface LockOptions {
26
+ name: string;
27
+ ttlMs: number;
28
+ timeoutMs: number;
29
+ renew?: boolean;
30
+ }
31
+ export declare function withLock<T>(target: string, options: LockOptions, fn: (lock: {
32
+ assertOwned(): Promise<void>;
33
+ }) => Promise<T>): Promise<T>;
@@ -0,0 +1,48 @@
1
+ import { acquireRefreshFileLock } from './refresh-file-lock.js';
2
+ export function lockPathFor(target, name) {
3
+ return `${target}.${name}.lock`;
4
+ }
5
+ export class LockContentionError extends Error {
6
+ details;
7
+ constructor(details) {
8
+ super(`Timed out acquiring ${details.name} lock for ${details.target}`);
9
+ this.name = 'LockContentionError';
10
+ this.details = details;
11
+ }
12
+ }
13
+ export class LockOwnershipError extends Error {
14
+ details;
15
+ constructor(details) {
16
+ super(`Lost ${details.name} lock for ${details.target}`);
17
+ this.name = 'LockOwnershipError';
18
+ this.details = details;
19
+ }
20
+ }
21
+ export async function withLock(target, options, fn) {
22
+ const started = performance.now();
23
+ for (;;) {
24
+ const lock = await acquireRefreshFileLock({
25
+ path: target,
26
+ name: options.name,
27
+ ttlMs: options.ttlMs,
28
+ renew: options.renew ?? false,
29
+ });
30
+ if (lock) {
31
+ try {
32
+ return await fn(lock);
33
+ }
34
+ finally {
35
+ await lock.release();
36
+ }
37
+ }
38
+ const remaining = options.timeoutMs - (performance.now() - started);
39
+ if (remaining <= 0) {
40
+ throw new LockContentionError({
41
+ target,
42
+ name: options.name,
43
+ timeoutMs: options.timeoutMs,
44
+ });
45
+ }
46
+ await new Promise((resolve) => setTimeout(resolve, Math.min(25, Math.ceil(remaining))));
47
+ }
48
+ }
@@ -0,0 +1,14 @@
1
+ import type { Level } from './engine.js';
2
+ export interface LogTestRecord {
3
+ channel: string;
4
+ level: Level;
5
+ message: string;
6
+ data?: unknown;
7
+ }
8
+ /** The engine delivers scrubbed records only; this sink never sees raw credentials. */
9
+ export type CaptureSink = (record: LogTestRecord) => void;
10
+ export declare function createCaptureSink(): {
11
+ records: LogTestRecord[];
12
+ sink: CaptureSink;
13
+ clear: () => void;
14
+ };
@@ -0,0 +1,13 @@
1
+ export function createCaptureSink() {
2
+ const records = [];
3
+ const sink = (record) => {
4
+ records.push(record);
5
+ };
6
+ return {
7
+ records,
8
+ sink,
9
+ clear: () => {
10
+ records.length = 0;
11
+ },
12
+ };
13
+ }