@rynx-ai/daemon 0.1.11-beta.43 → 0.1.11-beta.45
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/daemon-server.js
CHANGED
|
@@ -23,7 +23,6 @@ import { APP_BROWSER_HOST_BOOTSTRAP_TYPE, AppBrowserHostSupervisor, parseAppBrow
|
|
|
23
23
|
import { buildControlDeps, listControlAgentModels } from "./control-deps.js";
|
|
24
24
|
import { createBrowserArtifactManagementService } from "./browser-artifact-management.js";
|
|
25
25
|
import { createDaemonBuildStatus } from "./daemon-build-status.js";
|
|
26
|
-
import { MaintenanceLeaseStore } from "./maintenance-lease.js";
|
|
27
26
|
import { BrowserRunnerSessionContextProvider } from "./browser-runner-session-context.js";
|
|
28
27
|
import { removeLegacyPluginSkillSnapshot, snapshotEnabledPluginSkillSources, } from "./plugin-skill-context.js";
|
|
29
28
|
import { installedDaemonVersion } from "./entry-path.js";
|
|
@@ -259,13 +258,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
|
|
|
259
258
|
const appConfig = config ?? loadConfig();
|
|
260
259
|
const admissionGate = new CloseableAdmissionGate();
|
|
261
260
|
let shutdownAccepted = false;
|
|
262
|
-
const
|
|
263
|
-
// Expiry restores the gate primitive for a process that outlives its
|
|
264
|
-
// upgrader. The independent accepted-shutdown fence below still prevents
|
|
265
|
-
// new work after an irreversible server shutdown has begun.
|
|
266
|
-
onExpired: () => admissionGate.reopen(),
|
|
267
|
-
});
|
|
268
|
-
const admissionOpen = () => !shutdownAccepted && !maintenanceLease.isActive();
|
|
261
|
+
const admissionOpen = () => !shutdownAccepted;
|
|
269
262
|
const reserveAdmission = () => !admissionOpen()
|
|
270
263
|
? undefined
|
|
271
264
|
: admissionGate.tryReserve();
|
|
@@ -606,35 +599,12 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
|
|
|
606
599
|
});
|
|
607
600
|
let requestServerShutdown;
|
|
608
601
|
let shutdownAttempt;
|
|
609
|
-
|
|
610
|
-
const shutdownIfIdle = (maintenanceLeaseToken) => {
|
|
611
|
-
if (maintenanceLease.isActive()) {
|
|
612
|
-
if (!maintenanceLeaseToken || !maintenanceLease.isHeldBy(maintenanceLeaseToken)) {
|
|
613
|
-
return Promise.reject(new Error("a different maintenance lease is already active"));
|
|
614
|
-
}
|
|
615
|
-
// The previous daemon may have persisted the lease and exited after
|
|
616
|
-
// its 202 response was lost. A replacement process holding the same
|
|
617
|
-
// token must perform its own close/drain/shutdown; returning accepted
|
|
618
|
-
// without scheduling this PID would strand the updater waiting for a
|
|
619
|
-
// process that can never exit.
|
|
620
|
-
}
|
|
602
|
+
const shutdownIfIdle = () => {
|
|
621
603
|
if (shutdownAccepted) {
|
|
622
|
-
if (maintenanceLeaseToken) {
|
|
623
|
-
maintenanceLease.acquire(maintenanceLeaseToken);
|
|
624
|
-
return Promise.resolve({
|
|
625
|
-
outcome: "accepted",
|
|
626
|
-
maintenanceLease: "active",
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
604
|
return Promise.resolve({ outcome: "accepted" });
|
|
630
605
|
}
|
|
631
|
-
if (shutdownAttempt)
|
|
632
|
-
|
|
633
|
-
return shutdownAttempt;
|
|
634
|
-
}
|
|
635
|
-
return Promise.reject(new Error("another idle shutdown check is already in progress"));
|
|
636
|
-
}
|
|
637
|
-
shutdownAttemptLeaseToken = maintenanceLeaseToken;
|
|
606
|
+
if (shutdownAttempt)
|
|
607
|
+
return shutdownAttempt;
|
|
638
608
|
const attempt = (async () => {
|
|
639
609
|
await admissionGate.closeAndDrain();
|
|
640
610
|
const runtimeActivity = sessionRuntimeServices.sessionRuntimeIndex.activitySnapshot();
|
|
@@ -658,37 +628,25 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
|
|
|
658
628
|
admissionGate.reopen();
|
|
659
629
|
throw new Error("idle daemon shutdown was requested before server lifecycle was ready");
|
|
660
630
|
}
|
|
661
|
-
const acquiredLease = maintenanceLeaseToken
|
|
662
|
-
? maintenanceLease.acquire(maintenanceLeaseToken)
|
|
663
|
-
: false;
|
|
664
631
|
shutdownAccepted = true;
|
|
665
632
|
try {
|
|
666
633
|
void shutdown().catch((error) => {
|
|
667
634
|
shutdownAccepted = false;
|
|
668
|
-
if (acquiredLease && maintenanceLeaseToken) {
|
|
669
|
-
maintenanceLease.release(maintenanceLeaseToken);
|
|
670
|
-
}
|
|
671
635
|
admissionGate.reopen();
|
|
672
636
|
warn(`idle daemon shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
673
637
|
});
|
|
674
638
|
}
|
|
675
639
|
catch (error) {
|
|
676
640
|
shutdownAccepted = false;
|
|
677
|
-
if (acquiredLease && maintenanceLeaseToken) {
|
|
678
|
-
maintenanceLease.release(maintenanceLeaseToken);
|
|
679
|
-
}
|
|
680
641
|
admissionGate.reopen();
|
|
681
642
|
throw error;
|
|
682
643
|
}
|
|
683
|
-
return
|
|
684
|
-
? { outcome: "accepted", maintenanceLease: "active" }
|
|
685
|
-
: { outcome: "accepted" };
|
|
644
|
+
return { outcome: "accepted" };
|
|
686
645
|
})();
|
|
687
646
|
shutdownAttempt = attempt;
|
|
688
647
|
void attempt.finally(() => {
|
|
689
648
|
if (shutdownAttempt === attempt) {
|
|
690
649
|
shutdownAttempt = undefined;
|
|
691
|
-
shutdownAttemptLeaseToken = undefined;
|
|
692
650
|
}
|
|
693
651
|
}).catch(() => undefined);
|
|
694
652
|
return attempt;
|
|
@@ -701,14 +659,6 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
|
|
|
701
659
|
publicKey: installationIdentity.publicKey,
|
|
702
660
|
}),
|
|
703
661
|
buildStatus: daemonBuildStatus,
|
|
704
|
-
releaseMaintenanceLease: (token) => ({
|
|
705
|
-
outcome: (() => {
|
|
706
|
-
const outcome = maintenanceLease.release(token);
|
|
707
|
-
if (outcome === "released")
|
|
708
|
-
admissionGate.reopen();
|
|
709
|
-
return outcome;
|
|
710
|
-
})(),
|
|
711
|
-
}),
|
|
712
662
|
pluginManagement: {
|
|
713
663
|
listPlugins: () => pluginManagement.list(),
|
|
714
664
|
setPluginEnabled: (pluginId, enabled) => pluginManagement.setEnabled(pluginId, enabled),
|
|
@@ -914,6 +864,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
|
|
|
914
864
|
server.shutdown = () => {
|
|
915
865
|
if (shutdownPromise)
|
|
916
866
|
return shutdownPromise;
|
|
867
|
+
shutdownAccepted = true;
|
|
917
868
|
// Fence Local admission without closing the Desktop Browser Host WS.
|
|
918
869
|
// Browser handles can therefore close gracefully before full teardown.
|
|
919
870
|
server.stopAccepting();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rynx-ai/daemon",
|
|
3
|
-
"version": "0.1.11-beta.
|
|
3
|
+
"version": "0.1.11-beta.45",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/rynx-ai/rynx.git",
|
|
@@ -51,18 +51,18 @@
|
|
|
51
51
|
"tar": "^7.5.19",
|
|
52
52
|
"undici": "^7.28.0",
|
|
53
53
|
"ws": "^8.21.0",
|
|
54
|
-
"@rynx-ai/
|
|
55
|
-
"@rynx-ai/plugin-runner": "0.1.11-beta.
|
|
56
|
-
"@rynx-ai/plugin-sdk": "0.1.11-beta.
|
|
57
|
-
"@rynx-ai/protocol": "0.1.11-beta.
|
|
58
|
-
"@rynx-ai/remote-runtime-client": "0.1.11-beta.
|
|
59
|
-
"@rynx-ai/
|
|
60
|
-
"@rynx-ai/server": "0.1.11-beta.
|
|
54
|
+
"@rynx-ai/emulator": "0.1.11-beta.45",
|
|
55
|
+
"@rynx-ai/plugin-runner": "0.1.11-beta.45",
|
|
56
|
+
"@rynx-ai/plugin-sdk": "0.1.11-beta.45",
|
|
57
|
+
"@rynx-ai/protocol": "0.1.11-beta.45",
|
|
58
|
+
"@rynx-ai/remote-runtime-client": "0.1.11-beta.45",
|
|
59
|
+
"@rynx-ai/core": "0.1.11-beta.45",
|
|
60
|
+
"@rynx-ai/server": "0.1.11-beta.45"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/ws": "^8.18.1",
|
|
64
|
-
"@rynx-ai/plugin-channel-lark": "0.1.11-beta.
|
|
65
|
-
"@rynx-ai/browser-cdp": "0.1.11-beta.
|
|
64
|
+
"@rynx-ai/plugin-channel-lark": "0.1.11-beta.45",
|
|
65
|
+
"@rynx-ai/browser-cdp": "0.1.11-beta.45"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "rm -rf dist bundled-plugins && tsc -p tsconfig.json && chmod +x dist/index-daemon.js && node ../../scripts/stage-bundled-plugins.mjs",
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
export type MaintenanceLeaseReleaseOutcome = "released" | "not_active" | "denied";
|
|
2
|
-
export interface MaintenanceLeaseStoreOptions {
|
|
3
|
-
filePath?: string;
|
|
4
|
-
now?: () => number;
|
|
5
|
-
ttlMs?: number;
|
|
6
|
-
/** Test seam; production fsyncs the lease directory after rename/remove. */
|
|
7
|
-
syncDirectory?: (directory: string) => void;
|
|
8
|
-
/** Process-local recovery hook, invoked once when a live lease is observed expired. */
|
|
9
|
-
onExpired?: () => void;
|
|
10
|
-
}
|
|
11
|
-
export declare function maintenanceLeaseFile(env?: NodeJS.ProcessEnv): string;
|
|
12
|
-
/**
|
|
13
|
-
* Cross-process admission fence used only during daemon replacement.
|
|
14
|
-
*
|
|
15
|
-
* The raw bearer token is never persisted. The bounded expiry means a crashed
|
|
16
|
-
* upgrader cannot leave the daemon permanently unavailable, while atomic rename
|
|
17
|
-
* ensures a replacement daemon sees either the complete lease or no lease.
|
|
18
|
-
*/
|
|
19
|
-
export declare class MaintenanceLeaseStore {
|
|
20
|
-
private readonly filePath;
|
|
21
|
-
private readonly now;
|
|
22
|
-
private readonly ttlMs;
|
|
23
|
-
private readonly syncDirectory;
|
|
24
|
-
private readonly onExpired;
|
|
25
|
-
private lease;
|
|
26
|
-
private expiryTimer;
|
|
27
|
-
constructor(options?: MaintenanceLeaseStoreOptions);
|
|
28
|
-
isActive(): boolean;
|
|
29
|
-
isHeldBy(token: string): boolean;
|
|
30
|
-
acquire(token: string): boolean;
|
|
31
|
-
release(token: string): MaintenanceLeaseReleaseOutcome;
|
|
32
|
-
private readPersistedLease;
|
|
33
|
-
private writePersistedLease;
|
|
34
|
-
private clear;
|
|
35
|
-
private clearFile;
|
|
36
|
-
private expire;
|
|
37
|
-
private scheduleExpiry;
|
|
38
|
-
private cancelExpiry;
|
|
39
|
-
}
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { rynxHome } from "@rynx-ai/core";
|
|
5
|
-
const LEASE_SCHEMA_VERSION = 1;
|
|
6
|
-
// The lease is acquired only after shutdown-if-idle has closed admission and
|
|
7
|
-
// confirmed the daemon is idle. Keep a bounded cutover/rollback window without
|
|
8
|
-
// fencing a recovered daemon for hours when an updater disappears.
|
|
9
|
-
const DEFAULT_LEASE_TTL_MS = 15 * 60 * 1_000;
|
|
10
|
-
// Accept the previous production bound during rolling upgrades, then shorten
|
|
11
|
-
// it on load. Reject anything beyond that historical contract as corrupt.
|
|
12
|
-
const LEGACY_MAX_LEASE_TTL_MS = 30 * 60 * 60 * 1_000;
|
|
13
|
-
const EXPIRY_RETRY_MS = 1_000;
|
|
14
|
-
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
15
|
-
const LEASE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,256}$/;
|
|
16
|
-
export function maintenanceLeaseFile(env = process.env) {
|
|
17
|
-
return join(rynxHome(env), "maintenance-lease.json");
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Cross-process admission fence used only during daemon replacement.
|
|
21
|
-
*
|
|
22
|
-
* The raw bearer token is never persisted. The bounded expiry means a crashed
|
|
23
|
-
* upgrader cannot leave the daemon permanently unavailable, while atomic rename
|
|
24
|
-
* ensures a replacement daemon sees either the complete lease or no lease.
|
|
25
|
-
*/
|
|
26
|
-
export class MaintenanceLeaseStore {
|
|
27
|
-
filePath;
|
|
28
|
-
now;
|
|
29
|
-
ttlMs;
|
|
30
|
-
syncDirectory;
|
|
31
|
-
onExpired;
|
|
32
|
-
lease;
|
|
33
|
-
expiryTimer;
|
|
34
|
-
constructor(options = {}) {
|
|
35
|
-
this.filePath = options.filePath ?? maintenanceLeaseFile();
|
|
36
|
-
this.now = options.now ?? (() => Date.now());
|
|
37
|
-
this.ttlMs = options.ttlMs ?? DEFAULT_LEASE_TTL_MS;
|
|
38
|
-
this.syncDirectory = options.syncDirectory ?? fsyncDirectory;
|
|
39
|
-
this.onExpired = options.onExpired ?? (() => undefined);
|
|
40
|
-
if (!Number.isSafeInteger(this.ttlMs) || this.ttlMs <= 0) {
|
|
41
|
-
throw new Error("maintenance lease TTL must be a positive safe integer");
|
|
42
|
-
}
|
|
43
|
-
this.lease = this.readPersistedLease();
|
|
44
|
-
this.scheduleExpiry();
|
|
45
|
-
}
|
|
46
|
-
isActive() {
|
|
47
|
-
if (!this.lease)
|
|
48
|
-
return false;
|
|
49
|
-
if (this.lease.expiresAt > this.now())
|
|
50
|
-
return true;
|
|
51
|
-
this.expire();
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
isHeldBy(token) {
|
|
55
|
-
let tokenHash;
|
|
56
|
-
try {
|
|
57
|
-
tokenHash = hashLeaseToken(token);
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
return Boolean(this.isActive()
|
|
63
|
-
&& this.lease
|
|
64
|
-
&& hashesEqual(this.lease.tokenHash, tokenHash));
|
|
65
|
-
}
|
|
66
|
-
acquire(token) {
|
|
67
|
-
const tokenHash = hashLeaseToken(token);
|
|
68
|
-
if (this.isActive()) {
|
|
69
|
-
if (this.lease && hashesEqual(this.lease.tokenHash, tokenHash)) {
|
|
70
|
-
const createdAt = this.now();
|
|
71
|
-
const renewed = {
|
|
72
|
-
schemaVersion: LEASE_SCHEMA_VERSION,
|
|
73
|
-
tokenHash,
|
|
74
|
-
createdAt,
|
|
75
|
-
expiresAt: createdAt + this.ttlMs,
|
|
76
|
-
};
|
|
77
|
-
// Fail closed in memory if persistence reaches an indeterminate I/O
|
|
78
|
-
// outcome; the caller sees the error, but this process must not reopen
|
|
79
|
-
// admission around a lease file that may already have been renamed.
|
|
80
|
-
this.lease = renewed;
|
|
81
|
-
try {
|
|
82
|
-
this.writePersistedLease(renewed);
|
|
83
|
-
}
|
|
84
|
-
finally {
|
|
85
|
-
this.scheduleExpiry();
|
|
86
|
-
}
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
throw new Error("a different maintenance lease is already active");
|
|
90
|
-
}
|
|
91
|
-
const createdAt = this.now();
|
|
92
|
-
const lease = {
|
|
93
|
-
schemaVersion: LEASE_SCHEMA_VERSION,
|
|
94
|
-
tokenHash,
|
|
95
|
-
createdAt,
|
|
96
|
-
expiresAt: createdAt + this.ttlMs,
|
|
97
|
-
};
|
|
98
|
-
this.lease = lease;
|
|
99
|
-
try {
|
|
100
|
-
this.writePersistedLease(lease);
|
|
101
|
-
}
|
|
102
|
-
finally {
|
|
103
|
-
this.scheduleExpiry();
|
|
104
|
-
}
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
release(token) {
|
|
108
|
-
if (!this.isActive())
|
|
109
|
-
return "not_active";
|
|
110
|
-
const lease = this.lease;
|
|
111
|
-
if (!lease)
|
|
112
|
-
return "not_active";
|
|
113
|
-
let tokenHash;
|
|
114
|
-
try {
|
|
115
|
-
tokenHash = hashLeaseToken(token);
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return "denied";
|
|
119
|
-
}
|
|
120
|
-
if (!hashesEqual(lease.tokenHash, tokenHash))
|
|
121
|
-
return "denied";
|
|
122
|
-
this.clear();
|
|
123
|
-
return "released";
|
|
124
|
-
}
|
|
125
|
-
readPersistedLease() {
|
|
126
|
-
let parsed;
|
|
127
|
-
try {
|
|
128
|
-
parsed = JSON.parse(readFileSync(this.filePath, "utf8"));
|
|
129
|
-
}
|
|
130
|
-
catch (error) {
|
|
131
|
-
if (error
|
|
132
|
-
&& typeof error === "object"
|
|
133
|
-
&& "code" in error
|
|
134
|
-
&& error.code === "ENOENT")
|
|
135
|
-
return undefined;
|
|
136
|
-
throw new Error("maintenance lease state is unreadable", { cause: error });
|
|
137
|
-
}
|
|
138
|
-
if (!isPersistedLease(parsed, Math.max(this.ttlMs, LEGACY_MAX_LEASE_TTL_MS))) {
|
|
139
|
-
throw new Error("maintenance lease state is invalid");
|
|
140
|
-
}
|
|
141
|
-
const now = this.now();
|
|
142
|
-
if (parsed.expiresAt <= now) {
|
|
143
|
-
this.clearFile();
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|
|
146
|
-
if (parsed.expiresAt - parsed.createdAt > this.ttlMs) {
|
|
147
|
-
const shortened = {
|
|
148
|
-
...parsed,
|
|
149
|
-
createdAt: now,
|
|
150
|
-
expiresAt: Math.min(parsed.expiresAt, now + this.ttlMs),
|
|
151
|
-
};
|
|
152
|
-
this.writePersistedLease(shortened);
|
|
153
|
-
return shortened;
|
|
154
|
-
}
|
|
155
|
-
return parsed;
|
|
156
|
-
}
|
|
157
|
-
writePersistedLease(lease) {
|
|
158
|
-
const parent = dirname(this.filePath);
|
|
159
|
-
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
160
|
-
const temporary = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
161
|
-
let descriptor;
|
|
162
|
-
try {
|
|
163
|
-
descriptor = openSync(temporary, "wx", 0o600);
|
|
164
|
-
writeFileSync(descriptor, `${JSON.stringify(lease)}\n`, "utf8");
|
|
165
|
-
fsyncSync(descriptor);
|
|
166
|
-
closeSync(descriptor);
|
|
167
|
-
descriptor = undefined;
|
|
168
|
-
renameSync(temporary, this.filePath);
|
|
169
|
-
this.syncDirectory(parent);
|
|
170
|
-
}
|
|
171
|
-
finally {
|
|
172
|
-
if (descriptor !== undefined)
|
|
173
|
-
closeSync(descriptor);
|
|
174
|
-
rmSync(temporary, { force: true });
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
clear() {
|
|
178
|
-
this.clearFile();
|
|
179
|
-
this.lease = undefined;
|
|
180
|
-
this.cancelExpiry();
|
|
181
|
-
}
|
|
182
|
-
clearFile() {
|
|
183
|
-
rmSync(this.filePath, { force: true });
|
|
184
|
-
this.syncDirectory(dirname(this.filePath));
|
|
185
|
-
}
|
|
186
|
-
expire() {
|
|
187
|
-
this.clear();
|
|
188
|
-
this.onExpired();
|
|
189
|
-
}
|
|
190
|
-
scheduleExpiry(delayOverride) {
|
|
191
|
-
this.cancelExpiry();
|
|
192
|
-
if (!this.lease)
|
|
193
|
-
return;
|
|
194
|
-
const delay = delayOverride ?? Math.max(0, this.lease.expiresAt - this.now());
|
|
195
|
-
this.expiryTimer = setTimeout(() => {
|
|
196
|
-
this.expiryTimer = undefined;
|
|
197
|
-
if (!this.lease)
|
|
198
|
-
return;
|
|
199
|
-
if (this.lease.expiresAt > this.now()) {
|
|
200
|
-
this.scheduleExpiry();
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
try {
|
|
204
|
-
this.expire();
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
// Keep the in-memory fence and retry cleanup. A failed durability
|
|
208
|
-
// operation must not turn an expired lease into open admission.
|
|
209
|
-
this.scheduleExpiry(EXPIRY_RETRY_MS);
|
|
210
|
-
}
|
|
211
|
-
}, Math.min(delay, MAX_TIMER_DELAY_MS));
|
|
212
|
-
this.expiryTimer.unref?.();
|
|
213
|
-
}
|
|
214
|
-
cancelExpiry() {
|
|
215
|
-
if (this.expiryTimer)
|
|
216
|
-
clearTimeout(this.expiryTimer);
|
|
217
|
-
this.expiryTimer = undefined;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
function fsyncDirectory(directory) {
|
|
221
|
-
let descriptor;
|
|
222
|
-
try {
|
|
223
|
-
descriptor = openSync(directory, "r");
|
|
224
|
-
fsyncSync(descriptor);
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
if (error
|
|
228
|
-
&& typeof error === "object"
|
|
229
|
-
&& "code" in error
|
|
230
|
-
&& error.code === "ENOENT")
|
|
231
|
-
return;
|
|
232
|
-
throw error;
|
|
233
|
-
}
|
|
234
|
-
finally {
|
|
235
|
-
if (descriptor !== undefined)
|
|
236
|
-
closeSync(descriptor);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
function hashLeaseToken(token) {
|
|
240
|
-
if (!LEASE_TOKEN_PATTERN.test(token)) {
|
|
241
|
-
throw new Error("maintenance lease token must be 32-256 base64url characters");
|
|
242
|
-
}
|
|
243
|
-
return createHash("sha256").update(token, "utf8").digest("hex");
|
|
244
|
-
}
|
|
245
|
-
function hashesEqual(left, right) {
|
|
246
|
-
const leftBuffer = Buffer.from(left, "hex");
|
|
247
|
-
const rightBuffer = Buffer.from(right, "hex");
|
|
248
|
-
return leftBuffer.length === rightBuffer.length
|
|
249
|
-
&& timingSafeEqual(leftBuffer, rightBuffer);
|
|
250
|
-
}
|
|
251
|
-
function isPersistedLease(value, maximumTtlMs) {
|
|
252
|
-
if (!value || typeof value !== "object")
|
|
253
|
-
return false;
|
|
254
|
-
const candidate = value;
|
|
255
|
-
return candidate.schemaVersion === LEASE_SCHEMA_VERSION
|
|
256
|
-
&& typeof candidate.tokenHash === "string"
|
|
257
|
-
&& /^[a-f0-9]{64}$/.test(candidate.tokenHash)
|
|
258
|
-
&& Number.isSafeInteger(candidate.createdAt)
|
|
259
|
-
&& Number.isSafeInteger(candidate.expiresAt)
|
|
260
|
-
&& candidate.createdAt >= 0
|
|
261
|
-
&& candidate.expiresAt > candidate.createdAt
|
|
262
|
-
&& candidate.expiresAt - candidate.createdAt <= maximumTtlMs;
|
|
263
|
-
}
|