@cortexkit/antigravity-auth-core 3.0.16 → 3.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/account-migration.d.ts +17 -0
- package/dist/account-migration.d.ts.map +1 -1
- package/dist/account-migration.js +765 -16
- package/dist/account-migration.js.map +1 -1
- package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +13 -0
- package/dist/common-auth-embedded/fs/refresh-file-lock.js +159 -7
- package/dist/common-auth-embedded/pi-slot/lock.js +26 -1
- package/dist/common-auth-embedded/source-output.json +15 -15
- package/package.json +2 -2
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
export declare function isLostMarkerRaceError(error: unknown): boolean;
|
|
2
|
+
/** Test-only fault injection between marker mkdir and owner.json write. */
|
|
3
|
+
export interface EvictionMarkerTestHooks {
|
|
4
|
+
/** Runs after the exclusive mkdir (and identity capture) before owner write. */
|
|
5
|
+
beforeOwnerWrite?: (evictPath: string, ownerPath: string) => Promise<void> | void;
|
|
6
|
+
/** Throw to fail the owner write; return an error to throw it. */
|
|
7
|
+
failOwnerWrite?: (evictPath: string, ownerPath: string) => Promise<unknown> | unknown;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Tests only: inject a failure or a directory replacement between the
|
|
11
|
+
* eviction-marker mkdir and its owner.json write. Production never sets this.
|
|
12
|
+
* Not exported from the fs entry point, so it is not part of the public API.
|
|
13
|
+
*/
|
|
14
|
+
export declare function setEvictionMarkerTestHooksForTest(hooks?: EvictionMarkerTestHooks): void;
|
|
2
15
|
export interface LockLoss {
|
|
3
16
|
readonly reason: 'taken-over' | 'expired' | 'unreadable' | 'renewal-failed' | 'marker-lost';
|
|
4
17
|
readonly expectedOwnerId: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, open, readFile, rename, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { lstat, mkdir, open, readFile, rename, rm, stat, } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { createPrivateBytes, createPrivateDirectoryExclusive, ensurePrivateDirectory, PrivateFileError, privateOperation, readPrivateText, verifyPrivate, windowsPrivateFiles, writePrivateAtomic, } from './private-files.js';
|
|
5
5
|
import { LockOwnershipError, lockPathFor } from './with-lock.js';
|
|
@@ -13,9 +13,35 @@ import { LockOwnershipError, lockPathFor } from './with-lock.js';
|
|
|
13
13
|
*/
|
|
14
14
|
async function createExclusiveText(path, text) {
|
|
15
15
|
const handle = await open(path, 'wx', 0o600);
|
|
16
|
+
let createdStat;
|
|
17
|
+
try {
|
|
18
|
+
createdStat = await handle.stat();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
createdStat = undefined;
|
|
22
|
+
}
|
|
16
23
|
try {
|
|
17
24
|
await handle.writeFile(text, 'utf8');
|
|
18
25
|
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
// A failed write after an exclusive create leaves a partial file behind.
|
|
28
|
+
// Remove only the file just created: when a contender has meanwhile
|
|
29
|
+
// replaced the path, the identities differ and the successor is left
|
|
30
|
+
// alone. Removal failures must never mask the original write error.
|
|
31
|
+
try {
|
|
32
|
+
const current = await lstat(path).catch(() => undefined);
|
|
33
|
+
if (createdStat &&
|
|
34
|
+
current &&
|
|
35
|
+
current.dev === createdStat.dev &&
|
|
36
|
+
current.ino === createdStat.ino) {
|
|
37
|
+
await rm(path, { force: true }).catch(() => { });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Best effort only; the original error below explains the failure.
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
19
45
|
finally {
|
|
20
46
|
await handle.close();
|
|
21
47
|
}
|
|
@@ -31,6 +57,31 @@ export function isLostMarkerRaceError(error) {
|
|
|
31
57
|
return (code === 'ENOENT' ||
|
|
32
58
|
(!windowsPrivateFiles && (code === 'EINVAL' || code === 'ENOTDIR')));
|
|
33
59
|
}
|
|
60
|
+
let evictionMarkerTestHooks;
|
|
61
|
+
/**
|
|
62
|
+
* Tests only: inject a failure or a directory replacement between the
|
|
63
|
+
* eviction-marker mkdir and its owner.json write. Production never sets this.
|
|
64
|
+
* Not exported from the fs entry point, so it is not part of the public API.
|
|
65
|
+
*/
|
|
66
|
+
export function setEvictionMarkerTestHooksForTest(hooks) {
|
|
67
|
+
evictionMarkerTestHooks = hooks;
|
|
68
|
+
}
|
|
69
|
+
function sameMarkerDirectory(created, current) {
|
|
70
|
+
if (created.dev !== current.dev || created.ino !== current.ino)
|
|
71
|
+
return false;
|
|
72
|
+
// Windows reports the directory identity as a file id that can be 0; fall
|
|
73
|
+
// back to creation time so a reused id does not look like the same directory.
|
|
74
|
+
if (created.ino === 0 && created.birthtimeMs !== current.birthtimeMs)
|
|
75
|
+
return false;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* An ownerless marker older than this is recoverable without waiting for the
|
|
80
|
+
* full eviction TTL. Honest holders write owner.json within tens of
|
|
81
|
+
* milliseconds of the mkdir, so one second leaves wide margin for slow Windows
|
|
82
|
+
* filesystems while still bounding the stall from a crashed holder.
|
|
83
|
+
*/
|
|
84
|
+
const OWNERLESS_MARKER_GRACE_MS = 1_000;
|
|
34
85
|
/** Filesystem leases share wall time across all callers and processes. */
|
|
35
86
|
export function acquireRefreshFileLock(options) {
|
|
36
87
|
return acquireRefreshFileLockWithClock({ ...options, now: Date.now });
|
|
@@ -321,12 +372,66 @@ export async function acquireRefreshFileLockWithClock(options) {
|
|
|
321
372
|
await removeLeaseObject(evictPath);
|
|
322
373
|
}
|
|
323
374
|
}
|
|
375
|
+
// Returns whether the directory at the marker path was still the one this
|
|
376
|
+
// process created when its owner write failed. Only then is the failure ours
|
|
377
|
+
// to report: a gone or replaced directory is a lost race.
|
|
378
|
+
async function removeOwnMarkerAfterFailedOwnerWrite(created) {
|
|
379
|
+
// Best effort only: never throws, so the original owner-write error below
|
|
380
|
+
// keeps explaining the failure. Removes the directory this process just
|
|
381
|
+
// created when its owner write failed, and leaves a contender's
|
|
382
|
+
// replacement alone when the identities differ.
|
|
383
|
+
if (!created)
|
|
384
|
+
return false;
|
|
385
|
+
let current;
|
|
386
|
+
try {
|
|
387
|
+
const st = await lstat(evictPath);
|
|
388
|
+
current = { dev: st.dev, ino: st.ino, birthtimeMs: st.birthtimeMs };
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// Renamed away by a contender's steal: nothing of ours left to clean.
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
if (!sameMarkerDirectory(created, current))
|
|
395
|
+
return false;
|
|
396
|
+
// Remove only an ownerless directory. An owner file that appeared after
|
|
397
|
+
// our failed write belongs to someone: ours goes through
|
|
398
|
+
// releaseEvictionMarker in the caller, and a contender's must stay, since
|
|
399
|
+
// it may be holding the marker right now.
|
|
400
|
+
try {
|
|
401
|
+
await stat(evictOwnerPath);
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
if (error.code !== 'ENOENT')
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
await removeLeaseObject(evictPath);
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// A removal failure keeps the stale marker recoverable later; it must
|
|
413
|
+
// never mask the original owner-write error.
|
|
414
|
+
}
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
324
417
|
async function tryAcquireEvictionMarker() {
|
|
325
418
|
if (windowsPrivateFiles)
|
|
326
419
|
await createPrivateDirectoryExclusive(evictPath);
|
|
327
420
|
else
|
|
328
421
|
await mkdir(evictPath);
|
|
422
|
+
let created;
|
|
423
|
+
try {
|
|
424
|
+
const st = await lstat(evictPath);
|
|
425
|
+
created = { dev: st.dev, ino: st.ino, birthtimeMs: st.birthtimeMs };
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
created = undefined;
|
|
429
|
+
}
|
|
329
430
|
try {
|
|
431
|
+
await evictionMarkerTestHooks?.beforeOwnerWrite?.(evictPath, evictOwnerPath);
|
|
432
|
+
const injected = await evictionMarkerTestHooks?.failOwnerWrite?.(evictPath, evictOwnerPath);
|
|
433
|
+
if (injected !== undefined)
|
|
434
|
+
throw injected;
|
|
330
435
|
const bytes = `${JSON.stringify({ ownerId: evictOwnerId, createdAt: now() })}\n`;
|
|
331
436
|
if (windowsPrivateFiles)
|
|
332
437
|
await createPrivateBytes(evictOwnerPath, bytes);
|
|
@@ -339,9 +444,26 @@ export async function acquireRefreshFileLockWithClock(options) {
|
|
|
339
444
|
// path below does exactly that). That is a lost race, not a failure, so
|
|
340
445
|
// report it as such and let the caller back off and retry rather than
|
|
341
446
|
// failing the whole lock acquisition.
|
|
342
|
-
|
|
447
|
+
//
|
|
448
|
+
// The race codes alone cannot prove the directory is gone: the same
|
|
449
|
+
// codes can surface while our directory is still in place (for example
|
|
450
|
+
// a permission failure while writing the owner file, which used to
|
|
451
|
+
// leave an ownerless marker behind). The identity fence
|
|
452
|
+
// above removes only the directory this process created, and is a no-op
|
|
453
|
+
// when the contender renamed it away or replaced it, so run it on both
|
|
454
|
+
// branches before deciding between a retry and a failure.
|
|
455
|
+
// A race code while our own directory is still in place is not a lost
|
|
456
|
+
// race but a write that fails here every time: report it, rather than
|
|
457
|
+
// retrying the same failure until a deadline hides the cause.
|
|
458
|
+
const stillOurs = await removeOwnMarkerAfterFailedOwnerWrite(created);
|
|
459
|
+
if (isLostMarkerRaceError(error) && !stillOurs)
|
|
343
460
|
return false;
|
|
344
|
-
|
|
461
|
+
try {
|
|
462
|
+
await releaseEvictionMarker();
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// Removal must never mask the original owner-write error.
|
|
466
|
+
}
|
|
345
467
|
throw error;
|
|
346
468
|
}
|
|
347
469
|
if (options.onStep)
|
|
@@ -371,8 +493,29 @@ export async function acquireRefreshFileLockWithClock(options) {
|
|
|
371
493
|
throw error;
|
|
372
494
|
}
|
|
373
495
|
}
|
|
374
|
-
if (evictStat.mtimeMs + EVICT_TTL > now())
|
|
375
|
-
|
|
496
|
+
if (evictStat.mtimeMs + EVICT_TTL > now()) {
|
|
497
|
+
// A fresh marker with no owner is either this process's orphan (its
|
|
498
|
+
// owner write just failed) or a holder that crashed between mkdir and
|
|
499
|
+
// owner write. Honest holders publish owner.json within tens of
|
|
500
|
+
// milliseconds, so recover ownerless markers after a short grace instead
|
|
501
|
+
// of stalling for the full TTL. Stealing one is safe: a live holder
|
|
502
|
+
// stalled past the grace sees its pending owner write fail with ENOENT
|
|
503
|
+
// (a lost race) and retries, rather than acting on a stolen marker.
|
|
504
|
+
let hasOwner = true;
|
|
505
|
+
try {
|
|
506
|
+
await stat(evictOwnerPath);
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
if (error.code === 'ENOENT')
|
|
510
|
+
hasOwner = false;
|
|
511
|
+
else
|
|
512
|
+
return 'fresh';
|
|
513
|
+
}
|
|
514
|
+
if (hasOwner)
|
|
515
|
+
return 'fresh';
|
|
516
|
+
if (evictStat.mtimeMs + OWNERLESS_MARKER_GRACE_MS > now())
|
|
517
|
+
return 'fresh';
|
|
518
|
+
}
|
|
376
519
|
if (options.onStep)
|
|
377
520
|
await options.onStep('stale-marker-stat');
|
|
378
521
|
const claimedPath = `${evictPath}.${randomUUID()}`;
|
|
@@ -699,7 +842,12 @@ export async function acquireRefreshFileLockWithClock(options) {
|
|
|
699
842
|
released = true;
|
|
700
843
|
cancelRenewalTimer();
|
|
701
844
|
await renewalInFlight;
|
|
702
|
-
|
|
845
|
+
// A fresh marker may outlive a few quick retries, especially on Windows.
|
|
846
|
+
// Allow its full TTL plus recovery time, using a monotonic deadline so
|
|
847
|
+
// wall-clock adjustments cannot extend the retry deadline.
|
|
848
|
+
const deadline = performance.now() + EVICT_TTL + 1_000;
|
|
849
|
+
let delayMs = 5;
|
|
850
|
+
for (;;) {
|
|
703
851
|
try {
|
|
704
852
|
let removalConfirmed = false;
|
|
705
853
|
const markerAcquired = await withEvictionMarker(async () => {
|
|
@@ -741,7 +889,11 @@ export async function acquireRefreshFileLockWithClock(options) {
|
|
|
741
889
|
throw error;
|
|
742
890
|
return;
|
|
743
891
|
}
|
|
744
|
-
|
|
892
|
+
const remainingMs = deadline - performance.now();
|
|
893
|
+
if (remainingMs <= 0)
|
|
894
|
+
break;
|
|
895
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, remainingMs)));
|
|
896
|
+
delayMs = Math.min(delayMs * 2, 50);
|
|
745
897
|
}
|
|
746
898
|
// Do not delete by pathname without the marker: bounded retries leave the
|
|
747
899
|
// lease to expire rather than risking removal of a successor's lock.
|
|
@@ -75,6 +75,16 @@ async function acquire(path, stale) {
|
|
|
75
75
|
await remove(path);
|
|
76
76
|
return acquire(path, 0);
|
|
77
77
|
}
|
|
78
|
+
// The exclusive mkdir above succeeded: capture this directory's identity
|
|
79
|
+
// before any further step, so cleanup below removes only what we created.
|
|
80
|
+
let created;
|
|
81
|
+
try {
|
|
82
|
+
const st = await stat(path);
|
|
83
|
+
created = { dev: st.dev, ino: st.ino };
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
created = undefined;
|
|
87
|
+
}
|
|
78
88
|
try {
|
|
79
89
|
if (!precision) {
|
|
80
90
|
const probe = new Date(Math.ceil(Date.now() / 1_000) * 1_000 + 5);
|
|
@@ -86,7 +96,22 @@ async function acquire(path, stale) {
|
|
|
86
96
|
return { dev: observed.dev, ino: observed.ino, mtime };
|
|
87
97
|
}
|
|
88
98
|
catch (error) {
|
|
89
|
-
|
|
99
|
+
// Only remove the directory this call created. A contender can rename it
|
|
100
|
+
// away and publish its own lock between the mkdir above and this failure;
|
|
101
|
+
// an unfenced rmdir would then delete the successor. The directory holds
|
|
102
|
+
// no owner file (mtime is the lease), so identity alone is the fence.
|
|
103
|
+
try {
|
|
104
|
+
if (created) {
|
|
105
|
+
const current = await stat(path).catch(() => undefined);
|
|
106
|
+
if (current &&
|
|
107
|
+
current.dev === created.dev &&
|
|
108
|
+
current.ino === created.ino)
|
|
109
|
+
await remove(path).catch(() => { });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Best effort only; the original error below explains the failure.
|
|
114
|
+
}
|
|
90
115
|
throw error;
|
|
91
116
|
}
|
|
92
117
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
3
|
"package": "@cortexkit/common-auth",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.29.0",
|
|
5
5
|
"artifactStatus": "published",
|
|
6
6
|
"publicRoots": {
|
|
7
7
|
"./store": {
|
|
@@ -37,15 +37,15 @@
|
|
|
37
37
|
"import": "./dist/pi-slot/index.js"
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
|
-
"input": "tools/common-auth-build/inputs/cortexkit-common-auth-0.
|
|
41
|
-
"tarballSha256": "
|
|
42
|
-
"sri": "sha512-
|
|
40
|
+
"input": "tools/common-auth-build/inputs/cortexkit-common-auth-0.29.0.tgz",
|
|
41
|
+
"tarballSha256": "6d8795d0672353ab897f938cad86c63556ceab7704bd60498a8c8e1a62dbc207",
|
|
42
|
+
"sri": "sha512-XnNFY5ppNQRDP09lUTDDONGHwiJ8Z93w8/NP16pmWpC6lzBPkC7XJcHhld+B6iLATaNvbcenfhecitB8fS9nbQ==",
|
|
43
43
|
"canonicalFiles": 147,
|
|
44
|
-
"canonicalBytes":
|
|
44
|
+
"canonicalBytes": 994153,
|
|
45
45
|
"generator": {
|
|
46
46
|
"path": "packages/opencode/scripts/embed-common-auth.ts",
|
|
47
47
|
"version": 3,
|
|
48
|
-
"sha256": "
|
|
48
|
+
"sha256": "ece06fd4117208e4b276fee17386d3a0f93e641cee49c0d30d985c169c551ad2"
|
|
49
49
|
},
|
|
50
50
|
"files": [
|
|
51
51
|
{
|
|
@@ -595,17 +595,17 @@
|
|
|
595
595
|
{
|
|
596
596
|
"source": "package/dist/fs/refresh-file-lock.d.ts",
|
|
597
597
|
"output": "fs/refresh-file-lock.d.ts",
|
|
598
|
-
"bytes":
|
|
599
|
-
"sourceSha256": "
|
|
600
|
-
"outputSha256": "
|
|
598
|
+
"bytes": 2946,
|
|
599
|
+
"sourceSha256": "b649e67813eb4086ca5642c99d318f90ccccae9702b23ad8fb0616f8d62f5a06",
|
|
600
|
+
"outputSha256": "b649e67813eb4086ca5642c99d318f90ccccae9702b23ad8fb0616f8d62f5a06",
|
|
601
601
|
"transform": "verbatim"
|
|
602
602
|
},
|
|
603
603
|
{
|
|
604
604
|
"source": "package/dist/fs/refresh-file-lock.js",
|
|
605
605
|
"output": "fs/refresh-file-lock.js",
|
|
606
|
-
"bytes":
|
|
607
|
-
"sourceSha256": "
|
|
608
|
-
"outputSha256": "
|
|
606
|
+
"bytes": 37753,
|
|
607
|
+
"sourceSha256": "38bc20ae71a195818f9cf7e42954e8305f92a55ff15dd855da33ccdec6b34fa1",
|
|
608
|
+
"outputSha256": "38bc20ae71a195818f9cf7e42954e8305f92a55ff15dd855da33ccdec6b34fa1",
|
|
609
609
|
"transform": "verbatim"
|
|
610
610
|
},
|
|
611
611
|
{
|
|
@@ -707,9 +707,9 @@
|
|
|
707
707
|
{
|
|
708
708
|
"source": "package/dist/pi-slot/lock.js",
|
|
709
709
|
"output": "pi-slot/lock.js",
|
|
710
|
-
"bytes":
|
|
711
|
-
"sourceSha256": "
|
|
712
|
-
"outputSha256": "
|
|
710
|
+
"bytes": 8420,
|
|
711
|
+
"sourceSha256": "533de03f6b53624301e75565b7d677382acec3c9eda9cfdb7265a0e868aaeb82",
|
|
712
|
+
"outputSha256": "533de03f6b53624301e75565b7d677382acec3c9eda9cfdb7265a0e868aaeb82",
|
|
713
713
|
"transform": "verbatim"
|
|
714
714
|
},
|
|
715
715
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/antigravity-auth-core",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.17",
|
|
4
4
|
"description": "Harness-agnostic core for Google Antigravity OAuth: transport, fingerprint, request transforms, accounts, quota",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@cortexkit/claustrum-client": "0.12.0",
|
|
82
82
|
"@cortexkit/subc-client": "0.22.5",
|
|
83
|
-
"@cortexkit/private-files": "0.2.
|
|
83
|
+
"@cortexkit/private-files": "0.2.2",
|
|
84
84
|
"xdg-basedir": "^5.1.0",
|
|
85
85
|
"zod": "^4.0.0"
|
|
86
86
|
},
|