@cortexkit/antigravity-auth-core 3.0.38 → 3.0.39
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/common-auth-embedded/claustrum/availability.d.ts +7 -1
- package/dist/common-auth-embedded/claustrum/availability.js +9 -1
- package/dist/common-auth-embedded/claustrum/custody.d.ts +11 -2
- package/dist/common-auth-embedded/claustrum/custody.js +89 -31
- package/dist/common-auth-embedded/claustrum/enrollment.d.ts +4 -0
- package/dist/common-auth-embedded/claustrum/enrollment.js +33 -2
- package/dist/common-auth-embedded/claustrum/host.js +11 -7
- package/dist/common-auth-embedded/claustrum/index.d.ts +1 -1
- package/dist/common-auth-embedded/claustrum/index.js +1 -1
- package/dist/common-auth-embedded/fs/lock-record.js +55 -0
- package/dist/common-auth-embedded/fs/refresh-file-lock.js +148 -92
- package/dist/common-auth-embedded/fs/with-lock.js +19 -13
- package/dist/common-auth-embedded/source-output.json +48 -40
- package/dist/model-registry.d.ts +3 -0
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js +59 -0
- package/dist/model-registry.js.map +1 -1
- package/dist/transform/agy-generation.d.ts.map +1 -1
- package/dist/transform/agy-generation.js +7 -4
- package/dist/transform/agy-generation.js.map +1 -1
- package/dist/transform/model-resolver.d.ts.map +1 -1
- package/dist/transform/model-resolver.js +8 -3
- package/dist/transform/model-resolver.js.map +1 -1
- package/package.json +2 -2
|
@@ -85,6 +85,12 @@ export declare const VAULT_FAILURE_CLASSES: Readonly<{
|
|
|
85
85
|
* kind stays `unavailable` for callers handling ClaustrumConsumerError.
|
|
86
86
|
*/
|
|
87
87
|
export declare class ClaustrumUnreachableError extends ClaustrumConsumerError {
|
|
88
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Non-secret host connection diagnostic. A missing accessor can recover
|
|
90
|
+
* once the host bridge installs; a failed accessor could not provide a client.
|
|
91
|
+
* Other unreachable failures leave this unset. Never use adapter error text.
|
|
92
|
+
*/
|
|
93
|
+
readonly reason?: 'host-accessor-missing' | 'host-accessor-failed';
|
|
94
|
+
constructor(reason?: ClaustrumUnreachableError['reason']);
|
|
89
95
|
}
|
|
90
96
|
export declare function classifyVaultFailure(error: unknown): VaultFailureClass;
|
|
@@ -87,9 +87,17 @@ export const VAULT_FAILURE_CLASSES = Object.freeze({
|
|
|
87
87
|
* kind stays `unavailable` for callers handling ClaustrumConsumerError.
|
|
88
88
|
*/
|
|
89
89
|
export class ClaustrumUnreachableError extends ClaustrumConsumerError {
|
|
90
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Non-secret host connection diagnostic. A missing accessor can recover
|
|
92
|
+
* once the host bridge installs; a failed accessor could not provide a client.
|
|
93
|
+
* Other unreachable failures leave this unset. Never use adapter error text.
|
|
94
|
+
*/
|
|
95
|
+
reason;
|
|
96
|
+
constructor(reason) {
|
|
91
97
|
super('unavailable', 'Claustrum scoped operation unavailable');
|
|
92
98
|
this.name = 'ClaustrumUnreachableError';
|
|
99
|
+
if (reason === 'host-accessor-missing' || reason === 'host-accessor-failed')
|
|
100
|
+
this.reason = reason;
|
|
93
101
|
}
|
|
94
102
|
}
|
|
95
103
|
export function classifyVaultFailure(error) {
|
|
@@ -279,6 +279,15 @@ export declare class ClaustrumScopedCustody {
|
|
|
279
279
|
setTimeoutImpl?: typeof setTimeout;
|
|
280
280
|
clearTimeoutImpl?: typeof clearTimeout;
|
|
281
281
|
});
|
|
282
|
+
/**
|
|
283
|
+
* Replace the connection to the same vault without discarding receipts or
|
|
284
|
+
* their enrollment tokens. Open roster watches restart on the new client;
|
|
285
|
+
* their existing handles remain usable. Passing the current client does nothing.
|
|
286
|
+
* Neither client is closed here: the caller closes the old one, and custody's
|
|
287
|
+
* close()/dispose() still closes the client it holds at shutdown.
|
|
288
|
+
* While hosted, next must come from connectVaultHost, as in the constructor.
|
|
289
|
+
*/
|
|
290
|
+
replaceClient(next: ClaustrumScopedClient): void;
|
|
282
291
|
/** A watch losing its transport is the same outage as an unreachable read. */
|
|
283
292
|
transportLost(error?: unknown): void;
|
|
284
293
|
/** Any vault answer ends transport fallback, even when it refuses access. */
|
|
@@ -312,8 +321,8 @@ export declare class ClaustrumScopedCustody {
|
|
|
312
321
|
* Report that a send this consumer actually made was rejected. Only a 401
|
|
313
322
|
* is reported (the vault refuses 429, 402 and 5xx reports), addressed by
|
|
314
323
|
* credential id with the exact record version that send was served, and
|
|
315
|
-
* sent through the client
|
|
316
|
-
* this custody did not issue is refused rather than reported.
|
|
324
|
+
* sent through the current client with the enrollment token that authorized
|
|
325
|
+
* it. A receipt this custody did not issue is refused rather than reported.
|
|
317
326
|
*/
|
|
318
327
|
reportFailure(attempt: ClaustrumScopedAttempt, status: number, reporterSource: ClaustrumReporterSource): Promise<void>;
|
|
319
328
|
/**
|
|
@@ -238,8 +238,15 @@ export class ClaustrumScopedCustody {
|
|
|
238
238
|
this.#warnShutdown = shutdownWarning(this.#logger);
|
|
239
239
|
this.#setTimeout = options.setTimeoutImpl ?? setTimeout;
|
|
240
240
|
this.#clearTimeout = options.clearTimeoutImpl ?? clearTimeout;
|
|
241
|
+
const currentClient = () => this.#client;
|
|
241
242
|
this.#usage = new ScopedUsageReporter({
|
|
242
|
-
client:
|
|
243
|
+
client: {
|
|
244
|
+
// Queued readings follow a replacement without losing client capability checks.
|
|
245
|
+
get reportUsageScoped() {
|
|
246
|
+
const client = currentClient();
|
|
247
|
+
return client.reportUsageScoped?.bind(client);
|
|
248
|
+
},
|
|
249
|
+
},
|
|
243
250
|
owns: (id) => this.#usageServed.has(id),
|
|
244
251
|
token: async () => hasVaultHost() ? '' : checkedEnrollmentToken(await this.#readToken()),
|
|
245
252
|
logger: this.#logger,
|
|
@@ -247,6 +254,24 @@ export class ClaustrumScopedCustody {
|
|
|
247
254
|
clearTimeout: this.#clearTimeout,
|
|
248
255
|
});
|
|
249
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Replace the connection to the same vault without discarding receipts or
|
|
259
|
+
* their enrollment tokens. Open roster watches restart on the new client;
|
|
260
|
+
* their existing handles remain usable. Passing the current client does nothing.
|
|
261
|
+
* Neither client is closed here: the caller closes the old one, and custody's
|
|
262
|
+
* close()/dispose() still closes the client it holds at shutdown.
|
|
263
|
+
* While hosted, next must come from connectVaultHost, as in the constructor.
|
|
264
|
+
*/
|
|
265
|
+
replaceClient(next) {
|
|
266
|
+
this.#check();
|
|
267
|
+
if (next === this.#client)
|
|
268
|
+
return;
|
|
269
|
+
if (hasVaultHost() && !isVaultHostClient(next))
|
|
270
|
+
throw new ClaustrumUnreachableError();
|
|
271
|
+
this.#client = next;
|
|
272
|
+
for (const watch of this.#watches)
|
|
273
|
+
watch.restart();
|
|
274
|
+
}
|
|
250
275
|
/** A watch losing its transport is the same outage as an unreachable read. */
|
|
251
276
|
transportLost(error) {
|
|
252
277
|
if (this.#closed || this.#outage)
|
|
@@ -255,7 +280,9 @@ export class ClaustrumScopedCustody {
|
|
|
255
280
|
this.#outageError =
|
|
256
281
|
error instanceof ClaustrumCredentialError
|
|
257
282
|
? error
|
|
258
|
-
: new ClaustrumUnreachableError(
|
|
283
|
+
: new ClaustrumUnreachableError(error instanceof ClaustrumUnreachableError
|
|
284
|
+
? error.reason
|
|
285
|
+
: undefined);
|
|
259
286
|
this.#logger.warn('serving kept vault receipt', { reason: 'unreachable' });
|
|
260
287
|
this.#scheduleProbe();
|
|
261
288
|
}
|
|
@@ -367,7 +394,7 @@ export class ClaustrumScopedCustody {
|
|
|
367
394
|
if (error instanceof ClaustrumCredentialError)
|
|
368
395
|
throw error;
|
|
369
396
|
if (classifyVaultFailure(error) === 'unreachable')
|
|
370
|
-
throw new ClaustrumUnreachableError();
|
|
397
|
+
throw new ClaustrumUnreachableError(error instanceof ClaustrumUnreachableError ? error.reason : undefined);
|
|
371
398
|
throw new ClaustrumConsumerError('unavailable', 'Claustrum scoped operation unavailable');
|
|
372
399
|
}
|
|
373
400
|
finally {
|
|
@@ -603,7 +630,7 @@ export class ClaustrumScopedCustody {
|
|
|
603
630
|
if (kept.since === null)
|
|
604
631
|
kept.since = this.#now();
|
|
605
632
|
const attempt = Object.freeze(Object.defineProperty({ ...kept.attempt, fromKeptReceipt: true }, 'accessToken', { value: kept.attempt.accessToken, enumerable: false }));
|
|
606
|
-
this.#provenance.set(attempt, this.#provenance.get(kept.attempt) ?? { token
|
|
633
|
+
this.#provenance.set(attempt, this.#provenance.get(kept.attempt) ?? { token });
|
|
607
634
|
this.#keptAttempts.set(attempt, routeId);
|
|
608
635
|
return attempt;
|
|
609
636
|
}
|
|
@@ -684,15 +711,15 @@ export class ClaustrumScopedCustody {
|
|
|
684
711
|
recordVersion: served.recordVersion,
|
|
685
712
|
expiresAtMs,
|
|
686
713
|
}, 'accessToken', { value: accessToken, enumerable: false }));
|
|
687
|
-
this.#provenance.set(attempt, { token
|
|
714
|
+
this.#provenance.set(attempt, { token });
|
|
688
715
|
return attempt;
|
|
689
716
|
}
|
|
690
717
|
/**
|
|
691
718
|
* Report that a send this consumer actually made was rejected. Only a 401
|
|
692
719
|
* is reported (the vault refuses 429, 402 and 5xx reports), addressed by
|
|
693
720
|
* credential id with the exact record version that send was served, and
|
|
694
|
-
* sent through the client
|
|
695
|
-
* this custody did not issue is refused rather than reported.
|
|
721
|
+
* sent through the current client with the enrollment token that authorized
|
|
722
|
+
* it. A receipt this custody did not issue is refused rather than reported.
|
|
696
723
|
*/
|
|
697
724
|
async reportFailure(attempt, status, reporterSource) {
|
|
698
725
|
if (status !== 401)
|
|
@@ -719,7 +746,7 @@ export class ClaustrumScopedCustody {
|
|
|
719
746
|
const pending = this.#reports.get(attempt);
|
|
720
747
|
if (pending)
|
|
721
748
|
return pending;
|
|
722
|
-
const report = this.#call(() =>
|
|
749
|
+
const report = this.#call(() => this.#client.reportAuthFailureScoped({
|
|
723
750
|
credentialId: attempt.credentialId,
|
|
724
751
|
enrollmentToken: provenance.token,
|
|
725
752
|
providerStatus: 401,
|
|
@@ -761,36 +788,67 @@ export class ClaustrumScopedCustody {
|
|
|
761
788
|
*/
|
|
762
789
|
watchRoster(options) {
|
|
763
790
|
this.#check();
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
791
|
+
const currentView = options.view ?? (() => this.#discoveredView);
|
|
792
|
+
let discovering;
|
|
793
|
+
let watch;
|
|
794
|
+
const create = (resubscribed = false) => {
|
|
795
|
+
const opened = new ScopedRosterWatch({
|
|
796
|
+
client: this.#client,
|
|
797
|
+
enrollmentToken: () => this.#token(),
|
|
798
|
+
// Nothing observed the gap between clients, even if the view is unchanged.
|
|
799
|
+
view: () => (resubscribed ? undefined : currentView()),
|
|
800
|
+
discover: async (view) => {
|
|
801
|
+
// An old watch's discovery may still be committing the host's roster.
|
|
802
|
+
while (discovering)
|
|
803
|
+
await discovering.catch(() => { });
|
|
804
|
+
if (watch !== opened)
|
|
805
|
+
return;
|
|
806
|
+
resubscribed = false;
|
|
807
|
+
discovering = Promise.resolve().then(() => options.onView(view));
|
|
808
|
+
try {
|
|
809
|
+
await discovering;
|
|
810
|
+
}
|
|
811
|
+
finally {
|
|
812
|
+
discovering = undefined;
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
onRefused: (error) => {
|
|
816
|
+
// Apply a refused watch as discover() applies a refused account
|
|
817
|
+
// listing: every kept receipt is dropped.
|
|
818
|
+
this.listFailed(error);
|
|
819
|
+
options.onRefused?.(error);
|
|
820
|
+
},
|
|
821
|
+
onTransportLost: () => this.transportLost(),
|
|
822
|
+
onReachable: () => this.reachable(),
|
|
823
|
+
onError: options.onError,
|
|
824
|
+
onMode: options.onMode,
|
|
825
|
+
logger: this.#logger,
|
|
826
|
+
now: this.#now,
|
|
827
|
+
setTimeoutImpl: this.#setTimeout,
|
|
828
|
+
clearTimeoutImpl: this.#clearTimeout,
|
|
829
|
+
});
|
|
830
|
+
return opened;
|
|
831
|
+
};
|
|
832
|
+
watch = create();
|
|
833
|
+
const tracked = {
|
|
834
|
+
restart: () => {
|
|
835
|
+
const previousMode = watch.mode;
|
|
836
|
+
watch.stop();
|
|
837
|
+
watch = create(true);
|
|
838
|
+
if (previousMode !== watch.mode)
|
|
839
|
+
options.onMode?.(watch.mode);
|
|
840
|
+
watch.start();
|
|
776
841
|
},
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
onMode: options.onMode,
|
|
781
|
-
logger: this.#logger,
|
|
782
|
-
now: this.#now,
|
|
783
|
-
setTimeoutImpl: this.#setTimeout,
|
|
784
|
-
clearTimeoutImpl: this.#clearTimeout,
|
|
785
|
-
});
|
|
786
|
-
this.#watches.add(watch);
|
|
842
|
+
stop: () => watch.stop(),
|
|
843
|
+
};
|
|
844
|
+
this.#watches.add(tracked);
|
|
787
845
|
watch.start();
|
|
788
846
|
return {
|
|
789
847
|
get mode() {
|
|
790
848
|
return watch.mode;
|
|
791
849
|
},
|
|
792
850
|
close: () => {
|
|
793
|
-
this.#watches.delete(
|
|
851
|
+
this.#watches.delete(tracked);
|
|
794
852
|
watch.stop();
|
|
795
853
|
},
|
|
796
854
|
};
|
|
@@ -29,10 +29,13 @@ export declare function classifyEnrollmentError(error: unknown): {
|
|
|
29
29
|
* Each host enrolls separately so the operator can revoke one without the other.
|
|
30
30
|
*/
|
|
31
31
|
export declare function enrollmentName(plugin: string, host: string): string;
|
|
32
|
+
/** Match the vault's plain-text, code-point limit before proposing a display name. */
|
|
33
|
+
export declare function validEnrollmentDisplayName(displayName: string): boolean;
|
|
32
34
|
export interface ClaustrumEnrollmentClient {
|
|
33
35
|
enrollPropose(input: {
|
|
34
36
|
name: string;
|
|
35
37
|
requestSecretHash: string;
|
|
38
|
+
displayName?: string;
|
|
36
39
|
}): Promise<{
|
|
37
40
|
requestId: string;
|
|
38
41
|
}>;
|
|
@@ -142,6 +145,7 @@ export declare class ClaustrumEnrollmentManager {
|
|
|
142
145
|
client: ClaustrumEnrollmentClient;
|
|
143
146
|
paths: ClaustrumEnrollmentPaths;
|
|
144
147
|
proposedName: string;
|
|
148
|
+
displayName?: string;
|
|
145
149
|
now?: () => number;
|
|
146
150
|
mintSecret?: () => string;
|
|
147
151
|
writeTokenFile?: typeof writeEnrollmentTokenFile;
|
|
@@ -68,6 +68,13 @@ export function enrollmentName(plugin, host) {
|
|
|
68
68
|
throw new ClaustrumConsumerError('invalid-state', 'Claustrum enrollment names use lowercase letters, digits and dashes');
|
|
69
69
|
return `${plugin}-${host}`;
|
|
70
70
|
}
|
|
71
|
+
/** Match the vault's plain-text, code-point limit before proposing a display name. */
|
|
72
|
+
export function validEnrollmentDisplayName(displayName) {
|
|
73
|
+
const length = Array.from(displayName).length;
|
|
74
|
+
return (length >= 1 &&
|
|
75
|
+
length <= 40 &&
|
|
76
|
+
!/[\p{Cc}\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069\u200b-\u200d\u2060\ufeff]/u.test(displayName));
|
|
77
|
+
}
|
|
71
78
|
/**
|
|
72
79
|
* Connect the enrollment ceremony. Setup calls this; the request path never
|
|
73
80
|
* does. `connectionFile` is required because this library reads no
|
|
@@ -559,15 +566,20 @@ export class ClaustrumEnrollmentManager {
|
|
|
559
566
|
#client;
|
|
560
567
|
#paths;
|
|
561
568
|
#proposedName;
|
|
569
|
+
#displayName;
|
|
562
570
|
#now;
|
|
563
571
|
#mintSecret;
|
|
564
572
|
#writeTokenFile;
|
|
565
573
|
#ancestorOptions;
|
|
566
574
|
constructor(options) {
|
|
575
|
+
if (options.displayName !== undefined &&
|
|
576
|
+
!validEnrollmentDisplayName(options.displayName))
|
|
577
|
+
throw new ClaustrumConsumerError('invalid-state', 'invalid Claustrum enrollment display name');
|
|
567
578
|
this.#ancestorOptions = options.ancestorOptions ?? {};
|
|
568
579
|
this.#client = options.client;
|
|
569
580
|
this.#paths = options.paths;
|
|
570
581
|
this.#proposedName = options.proposedName;
|
|
582
|
+
this.#displayName = options.displayName;
|
|
571
583
|
this.#now = options.now ?? Date.now;
|
|
572
584
|
this.#mintSecret =
|
|
573
585
|
options.mintSecret ?? (() => randomBytes(32).toString('hex'));
|
|
@@ -665,10 +677,29 @@ export class ClaustrumEnrollmentManager {
|
|
|
665
677
|
const requestSecretHash = createHash('sha256')
|
|
666
678
|
.update(Buffer.from(state.requestSecret, 'hex'))
|
|
667
679
|
.digest('hex');
|
|
668
|
-
const
|
|
680
|
+
const proposal = {
|
|
669
681
|
name: state.proposedName,
|
|
670
682
|
requestSecretHash,
|
|
671
|
-
}
|
|
683
|
+
};
|
|
684
|
+
let proposed;
|
|
685
|
+
try {
|
|
686
|
+
proposed = await this.#client.enrollPropose({
|
|
687
|
+
...proposal,
|
|
688
|
+
...(this.#displayName !== undefined && {
|
|
689
|
+
displayName: this.#displayName,
|
|
690
|
+
}),
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
catch (error) {
|
|
694
|
+
if (this.#displayName === undefined ||
|
|
695
|
+
!(error instanceof ClaustrumCredentialError) ||
|
|
696
|
+
error.code !== 'invalid_params')
|
|
697
|
+
throw error;
|
|
698
|
+
// Locally validated names can only be refused by vaults that do
|
|
699
|
+
// not support displayName. A refused proposal stores nothing, so
|
|
700
|
+
// retry once with the same hash and let normal handling decide.
|
|
701
|
+
proposed = await this.#client.enrollPropose(proposal);
|
|
702
|
+
}
|
|
672
703
|
state = {
|
|
673
704
|
...state,
|
|
674
705
|
requestId: proposed.requestId,
|
|
@@ -29,7 +29,10 @@ function hostError(error) {
|
|
|
29
29
|
(typeof value.code === 'string' &&
|
|
30
30
|
classifyVaultFailure(Object.assign(new Error(), { code: value.code })) ===
|
|
31
31
|
'unreachable'))
|
|
32
|
-
return new ClaustrumUnreachableError(
|
|
32
|
+
return new ClaustrumUnreachableError(value.reason === 'host-accessor-missing' ||
|
|
33
|
+
value.reason === 'host-accessor-failed'
|
|
34
|
+
? value.reason
|
|
35
|
+
: undefined);
|
|
33
36
|
return error;
|
|
34
37
|
}
|
|
35
38
|
function call(operation) {
|
|
@@ -101,13 +104,14 @@ export function connectVaultHost(pluginId) {
|
|
|
101
104
|
throw new ClaustrumUnreachableError();
|
|
102
105
|
try {
|
|
103
106
|
const accessor = globalThis[Symbol.for('cortexkit.vault')];
|
|
104
|
-
if (typeof accessor
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
if (typeof accessor === 'function') {
|
|
108
|
+
const client = wrapHostClient(accessor(pluginId));
|
|
109
|
+
hostClients.add(client);
|
|
110
|
+
return client;
|
|
111
|
+
}
|
|
109
112
|
}
|
|
110
113
|
catch {
|
|
111
|
-
throw new ClaustrumUnreachableError();
|
|
114
|
+
throw new ClaustrumUnreachableError('host-accessor-failed');
|
|
112
115
|
}
|
|
116
|
+
throw new ClaustrumUnreachableError('host-accessor-missing');
|
|
113
117
|
}
|
|
@@ -4,7 +4,7 @@ export { ClaustrumCredentialError, type ClaustrumReporterSource, type Enrollment
|
|
|
4
4
|
export { ClaustrumUnreachableError, classifyVaultFailure, VAULT_FAILURE_CLASSES, type VaultFailureClass, } from './availability.js';
|
|
5
5
|
export { ClaustrumConsumer, type ClaustrumConsumerOptions, type ClaustrumWatchState, DEFAULT_CONNECT_TIMEOUT_MS, type SendOptions, } from './consumer.js';
|
|
6
6
|
export { type AccountIdentitySource, type ClassifyingClaustrumFamily, type ClaustrumFamily, type ClaustrumFamilyInput, type ClaustrumRosterWatch, type ClaustrumRosterWatchOptions, type ClaustrumScopedAttempt, type ClaustrumScopedClient, ClaustrumScopedCustody, type ClaustrumScopedIdentity, decideScopedRetryAfter401, type IdentityParser, isScopedCredentialRotation, KEPT_RECEIPT_MARGIN_MS, type KeptReceiptState, type ScopedRetryReason, SERVING_MARGIN_MS, type SkippedVaultReason, type SkippedVaultRecord, type VaultCredential, type VaultCredentialType, type VaultGrantReach, type VaultGrantTuple, type VaultInventory, type VaultRowFacts, } from './custody.js';
|
|
7
|
-
export { type ClaustrumEnrollmentClient, type ClaustrumEnrollmentConnection, type ClaustrumEnrollmentDisconnectResult, ClaustrumEnrollmentManager, type ClaustrumEnrollmentPaths, type ClaustrumEnrollmentResetResult, type ClaustrumEnrollmentStatus, classifyEnrollmentError, connectClaustrumEnrollmentClient, disconnectClaustrumEnrollment, type EnrollmentDisposition, enrollmentAuthority, enrollmentName, getClaustrumEnrollmentPaths, hostEnrollmentPaths, RETRYABLE_ENROLLMENT_CODES, readClaustrumEnrollmentStatus, readClaustrumEnrollmentToken, resetClaustrumEnrollmentState, TERMINAL_ENROLLMENT_CODES, vaultAuthority, } from './enrollment.js';
|
|
7
|
+
export { type ClaustrumEnrollmentClient, type ClaustrumEnrollmentConnection, type ClaustrumEnrollmentDisconnectResult, ClaustrumEnrollmentManager, type ClaustrumEnrollmentPaths, type ClaustrumEnrollmentResetResult, type ClaustrumEnrollmentStatus, classifyEnrollmentError, connectClaustrumEnrollmentClient, disconnectClaustrumEnrollment, type EnrollmentDisposition, enrollmentAuthority, enrollmentName, getClaustrumEnrollmentPaths, hostEnrollmentPaths, RETRYABLE_ENROLLMENT_CODES, readClaustrumEnrollmentStatus, readClaustrumEnrollmentToken, resetClaustrumEnrollmentState, TERMINAL_ENROLLMENT_CODES, validEnrollmentDisplayName, vaultAuthority, } from './enrollment.js';
|
|
8
8
|
export { ClaustrumConsumerError, type ClaustrumConsumerFailureKind, type ClaustrumLogger, } from './errors.js';
|
|
9
9
|
export { connectVaultHost, hasVaultHost, VAULT_HOST_CONNECT_REFUSAL, VAULT_HOST_STATUS, } from './host.js';
|
|
10
10
|
export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHOLDER_PREFIX, classifyHostSlot, custodyPlaceholder, custodyPlaceholderKey, type HostSlotContent, isCustodyPlaceholder, isCustodyPlaceholderValue, } from './host-slot.js';
|
|
@@ -10,7 +10,7 @@ export { ClaustrumCredentialError, } from '@cortexkit/claustrum-client';
|
|
|
10
10
|
export { ClaustrumUnreachableError, classifyVaultFailure, VAULT_FAILURE_CLASSES, } from './availability.js';
|
|
11
11
|
export { ClaustrumConsumer, DEFAULT_CONNECT_TIMEOUT_MS, } from './consumer.js';
|
|
12
12
|
export { ClaustrumScopedCustody, decideScopedRetryAfter401, isScopedCredentialRotation, KEPT_RECEIPT_MARGIN_MS, SERVING_MARGIN_MS, } from './custody.js';
|
|
13
|
-
export { ClaustrumEnrollmentManager, classifyEnrollmentError, connectClaustrumEnrollmentClient, disconnectClaustrumEnrollment, enrollmentAuthority, enrollmentName, getClaustrumEnrollmentPaths, hostEnrollmentPaths, RETRYABLE_ENROLLMENT_CODES, readClaustrumEnrollmentStatus, readClaustrumEnrollmentToken, resetClaustrumEnrollmentState, TERMINAL_ENROLLMENT_CODES, vaultAuthority, } from './enrollment.js';
|
|
13
|
+
export { ClaustrumEnrollmentManager, classifyEnrollmentError, connectClaustrumEnrollmentClient, disconnectClaustrumEnrollment, enrollmentAuthority, enrollmentName, getClaustrumEnrollmentPaths, hostEnrollmentPaths, RETRYABLE_ENROLLMENT_CODES, readClaustrumEnrollmentStatus, readClaustrumEnrollmentToken, resetClaustrumEnrollmentState, TERMINAL_ENROLLMENT_CODES, validEnrollmentDisplayName, vaultAuthority, } from './enrollment.js';
|
|
14
14
|
export { ClaustrumConsumerError, } from './errors.js';
|
|
15
15
|
export { connectVaultHost, hasVaultHost, VAULT_HOST_CONNECT_REFUSAL, VAULT_HOST_STATUS, } from './host.js';
|
|
16
16
|
export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHOLDER_PREFIX, classifyHostSlot, custodyPlaceholder, custodyPlaceholderKey, isCustodyPlaceholder, isCustodyPlaceholderValue, } from './host-slot.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { PrivateFileError } from './private-files.js';
|
|
3
|
+
/** A failed open is not evidence of deletion until the same path disappears. */
|
|
4
|
+
export class DisappearedLockRecordError extends Error {
|
|
5
|
+
path;
|
|
6
|
+
code = 'ENOENT';
|
|
7
|
+
constructor(path, cause) {
|
|
8
|
+
super(`Lock record disappeared: ${path}`, { cause });
|
|
9
|
+
this.path = path;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function isWindowsAccessDeniedRead(error, paths) {
|
|
13
|
+
return (error instanceof PrivateFileError &&
|
|
14
|
+
error.reason === 'io-error' &&
|
|
15
|
+
error.win32Code === 5 &&
|
|
16
|
+
(error.stage === 'read' || error.stage === 'verify') &&
|
|
17
|
+
paths.some((path) => resolve(path) === error.path));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* An exclusive create at the lock or eviction-directory name can fail with
|
|
21
|
+
* ACCESS_DENIED (5) while a holder's delete of that name is still pending. No
|
|
22
|
+
* reread settles a create refusal, so callers retry only under their existing
|
|
23
|
+
* bound and never infer staleness from it. owner.json is deliberately not a
|
|
24
|
+
* candidate: a marker directory created exclusively a moment ago cannot hold a
|
|
25
|
+
* delete-pending old owner.json, so a 5 there is a real refusal and the caller
|
|
26
|
+
* removes the directory it just created.
|
|
27
|
+
*/
|
|
28
|
+
export function isWindowsAccessDeniedCreate(error, paths, stage) {
|
|
29
|
+
return (error instanceof PrivateFileError &&
|
|
30
|
+
error.reason === 'io-error' &&
|
|
31
|
+
error.win32Code === 5 &&
|
|
32
|
+
error.stage === stage &&
|
|
33
|
+
paths.some((path) => resolve(path) === error.path));
|
|
34
|
+
}
|
|
35
|
+
export async function readLockRecord(path, read) {
|
|
36
|
+
try {
|
|
37
|
+
return await read();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (!isWindowsAccessDeniedRead(error, [path]))
|
|
41
|
+
throw error;
|
|
42
|
+
// Windows commonly maps STATUS_DELETE_PENDING to ACCESS_DENIED (5),
|
|
43
|
+
// which can also mean a permission refusal, not a sharing violation.
|
|
44
|
+
// Only a successful fresh read or ENOENT resolves that ambiguity. A
|
|
45
|
+
// persistent refusal escapes to the caller's existing deadline/backoff.
|
|
46
|
+
try {
|
|
47
|
+
return await read();
|
|
48
|
+
}
|
|
49
|
+
catch (recheckError) {
|
|
50
|
+
if (recheckError.code === 'ENOENT')
|
|
51
|
+
throw new DisappearedLockRecordError(path, error);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|