@happyvertical/smrt-app-runtime 0.43.9 → 0.44.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.
- package/AGENTS.md +57 -0
- package/README.md +96 -0
- package/dist/index.d.ts +229 -0
- package/dist/index.js +876 -5
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/package.json +11 -9
package/dist/index.js
CHANGED
|
@@ -6,11 +6,826 @@ import { isAbsolute, parse, relative, resolve, sep } from "node:path";
|
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { Worker } from "node:worker_threads";
|
|
8
8
|
import { resolveApplicationRuntime } from "@happyvertical/smrt-config";
|
|
9
|
-
import { TaskRunner } from "@happyvertical/smrt-jobs";
|
|
9
|
+
import { ScheduleRunner, TaskRunner } from "@happyvertical/smrt-jobs";
|
|
10
10
|
import { Person, ProfileTypeCollection } from "@happyvertical/smrt-profiles";
|
|
11
11
|
import { withSystemContext } from "@happyvertical/smrt-tenancy";
|
|
12
12
|
import { DEFAULT_ROLE_SLUGS, MembershipCollection, MembershipStatus, RoleCollection, SessionCollection, SessionService, TenantService, UserCollection, UserStatus } from "@happyvertical/smrt-users";
|
|
13
13
|
import { getDatabase } from "@happyvertical/sql";
|
|
14
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
15
|
+
//#region src/deployed-runtime.ts
|
|
16
|
+
var DeployedRuntimeError = class extends Error {
|
|
17
|
+
constructor(code, message, component) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.component = component;
|
|
21
|
+
this.name = "DeployedRuntimeError";
|
|
22
|
+
}
|
|
23
|
+
code;
|
|
24
|
+
component;
|
|
25
|
+
};
|
|
26
|
+
var DeployedRuntimeCleanupError = class extends DeployedRuntimeError {
|
|
27
|
+
constructor(cleanup) {
|
|
28
|
+
super("provider_unavailable", "The database connection could not be closed after startup failed; retry cleanup and inspect the provider's private logs.", "database");
|
|
29
|
+
this.cleanup = cleanup;
|
|
30
|
+
this.name = "DeployedRuntimeCleanupError";
|
|
31
|
+
}
|
|
32
|
+
cleanup;
|
|
33
|
+
cleaned = false;
|
|
34
|
+
cleanupAttempt;
|
|
35
|
+
async retryCleanup() {
|
|
36
|
+
if (this.cleaned) return;
|
|
37
|
+
if (!this.cleanupAttempt) this.cleanupAttempt = Promise.resolve().then(() => this.cleanup()).then(() => {
|
|
38
|
+
this.cleaned = true;
|
|
39
|
+
}).catch(() => {
|
|
40
|
+
throw this;
|
|
41
|
+
}).finally(() => {
|
|
42
|
+
this.cleanupAttempt = void 0;
|
|
43
|
+
});
|
|
44
|
+
return this.cleanupAttempt;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
async function initializeDeployedApplicationRuntime(options) {
|
|
48
|
+
const resolvedRuntime = resolveDeployedRuntime(options);
|
|
49
|
+
const bindings = validateBindings(options, resolvedRuntime);
|
|
50
|
+
const prepareDatabase = snapshotPrepareDatabase(options);
|
|
51
|
+
await checkProvider("authentication", () => bindings.authentication.readiness());
|
|
52
|
+
await checkProvider("assets", () => bindings.assets.readiness());
|
|
53
|
+
await checkProvider("secrets", () => bindings.secrets.readiness());
|
|
54
|
+
let db;
|
|
55
|
+
try {
|
|
56
|
+
db = await bindings.database.connect();
|
|
57
|
+
} catch {
|
|
58
|
+
throw unavailable("database");
|
|
59
|
+
}
|
|
60
|
+
let validDatabase = false;
|
|
61
|
+
try {
|
|
62
|
+
validDatabase = isDatabaseInterface(db);
|
|
63
|
+
} catch {}
|
|
64
|
+
if (!validDatabase) await cleanupAfterStartupFailure(bindings.database, db, new DeployedRuntimeError("invalid_configuration", "The database adapter did not return a valid database connection.", "database"));
|
|
65
|
+
try {
|
|
66
|
+
await checkDatabase(bindings.database, db);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
await cleanupAfterStartupFailure(bindings.database, db, error);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await prepareDatabase?.(db);
|
|
72
|
+
} catch {
|
|
73
|
+
await cleanupAfterStartupFailure(bindings.database, db, new DeployedRuntimeError("provider_unavailable", "The PostgreSQL migration step failed; inspect the application migration logs.", "database"));
|
|
74
|
+
}
|
|
75
|
+
return new InitializedDeployedApplicationRuntime(db, resolvedRuntime, bindings);
|
|
76
|
+
}
|
|
77
|
+
function resolveDeployedRuntime(options) {
|
|
78
|
+
let plainOptions = false;
|
|
79
|
+
try {
|
|
80
|
+
plainOptions = isPlainRecord(options);
|
|
81
|
+
} catch {}
|
|
82
|
+
if (!plainOptions) throw new DeployedRuntimeError("invalid_configuration", "Deployed runtime options must be an object.");
|
|
83
|
+
let profile;
|
|
84
|
+
let providers;
|
|
85
|
+
try {
|
|
86
|
+
profile = options.profile;
|
|
87
|
+
providers = options.providers;
|
|
88
|
+
} catch {
|
|
89
|
+
throw new DeployedRuntimeError("invalid_configuration", "Deployed runtime options could not be read safely.");
|
|
90
|
+
}
|
|
91
|
+
if (profile !== "self-hosted" && profile !== "cloud") throw new DeployedRuntimeError("invalid_configuration", "Deployed runtime initialization requires the self-hosted or cloud profile.");
|
|
92
|
+
try {
|
|
93
|
+
return resolveApplicationRuntime({
|
|
94
|
+
profile,
|
|
95
|
+
providers
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
throw new DeployedRuntimeError("invalid_configuration", "Deployed runtime provider options could not be read safely.");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function validateBindings(options, runtime) {
|
|
102
|
+
const database = requireObjectBinding(readBindingOption(options, "database", "database"), "database");
|
|
103
|
+
let engine;
|
|
104
|
+
let connect;
|
|
105
|
+
let readiness;
|
|
106
|
+
let close;
|
|
107
|
+
try {
|
|
108
|
+
engine = database.engine;
|
|
109
|
+
connect = database.connect;
|
|
110
|
+
readiness = database.readiness;
|
|
111
|
+
close = database.close;
|
|
112
|
+
} catch {
|
|
113
|
+
throw invalidBinding("database");
|
|
114
|
+
}
|
|
115
|
+
requireFunction(connect, "database", "connect");
|
|
116
|
+
requireOptionalFunction(readiness, "database", "readiness");
|
|
117
|
+
requireFunction(close, "database", "close");
|
|
118
|
+
if (engine !== runtime.providers.database.engine) throw mismatch("database", runtime.providers.database.engine);
|
|
119
|
+
return {
|
|
120
|
+
database: Object.freeze({
|
|
121
|
+
engine,
|
|
122
|
+
connect: bindCallback(connect, database, "database"),
|
|
123
|
+
readiness: typeof readiness === "function" ? bindCallback(readiness, database, "database") : void 0,
|
|
124
|
+
close: bindCallback(close, database, "database")
|
|
125
|
+
}),
|
|
126
|
+
authentication: requireProviderBinding(readBindingOption(options, "authentication", "authentication"), "authentication", runtime.providers.authentication.provider),
|
|
127
|
+
assets: requireProviderBinding(readBindingOption(options, "assets", "assets"), "assets", runtime.providers.assets.provider),
|
|
128
|
+
secrets: requireProviderBinding(readBindingOption(options, "secrets", "secrets"), "secrets", runtime.providers.secrets.provider)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function snapshotPrepareDatabase(options) {
|
|
132
|
+
let prepareDatabase;
|
|
133
|
+
try {
|
|
134
|
+
prepareDatabase = options.prepareDatabase;
|
|
135
|
+
} catch {
|
|
136
|
+
throw invalidBinding("database");
|
|
137
|
+
}
|
|
138
|
+
requireOptionalFunction(prepareDatabase, "database", "prepareDatabase");
|
|
139
|
+
return typeof prepareDatabase === "function" ? bindCallback(prepareDatabase, options, "database") : void 0;
|
|
140
|
+
}
|
|
141
|
+
function readBindingOption(options, field, component) {
|
|
142
|
+
try {
|
|
143
|
+
return options[field];
|
|
144
|
+
} catch {
|
|
145
|
+
throw invalidBinding(component);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function requireProviderBinding(value, component, expectedProvider) {
|
|
149
|
+
const binding = requireObjectBinding(value, component);
|
|
150
|
+
let provider;
|
|
151
|
+
let readiness;
|
|
152
|
+
try {
|
|
153
|
+
provider = binding.provider;
|
|
154
|
+
readiness = binding.readiness;
|
|
155
|
+
} catch {
|
|
156
|
+
throw invalidBinding(component);
|
|
157
|
+
}
|
|
158
|
+
if (provider !== expectedProvider) throw mismatch(component, expectedProvider);
|
|
159
|
+
requireFunction(readiness, component, "readiness");
|
|
160
|
+
return Object.freeze({
|
|
161
|
+
provider,
|
|
162
|
+
readiness: bindCallback(readiness, binding, component)
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function requireObjectBinding(value, component) {
|
|
166
|
+
let valid = false;
|
|
167
|
+
try {
|
|
168
|
+
valid = typeof value === "object" && value !== null && !Array.isArray(value);
|
|
169
|
+
} catch {}
|
|
170
|
+
if (!valid) throw new DeployedRuntimeError("invalid_configuration", `The ${component} provider binding is required.`, component);
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
function invalidBinding(component) {
|
|
174
|
+
return new DeployedRuntimeError("invalid_configuration", `The ${component} provider binding could not be read safely.`, component);
|
|
175
|
+
}
|
|
176
|
+
function bindCallback(callback, receiver, component) {
|
|
177
|
+
try {
|
|
178
|
+
return Reflect.apply(Function.prototype.bind, callback, [receiver]);
|
|
179
|
+
} catch {
|
|
180
|
+
throw invalidBinding(component);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function requireFunction(value, component, field) {
|
|
184
|
+
if (typeof value !== "function") throw new DeployedRuntimeError("invalid_configuration", `The ${component} provider requires a ${field} callback.`, component);
|
|
185
|
+
}
|
|
186
|
+
function requireOptionalFunction(value, component, field) {
|
|
187
|
+
if (value !== void 0) requireFunction(value, component, field);
|
|
188
|
+
}
|
|
189
|
+
function mismatch(component, expectedProvider) {
|
|
190
|
+
return new DeployedRuntimeError("provider_mismatch", `The ${component} binding does not match the selected ${expectedProvider} provider.`, component);
|
|
191
|
+
}
|
|
192
|
+
function unavailable(component) {
|
|
193
|
+
return new DeployedRuntimeError("provider_unavailable", `The ${component} provider is not ready; inspect that provider's private logs.`, component);
|
|
194
|
+
}
|
|
195
|
+
async function checkProvider(component, check) {
|
|
196
|
+
try {
|
|
197
|
+
await check();
|
|
198
|
+
} catch {
|
|
199
|
+
throw unavailable(component);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function checkDatabase(binding, db) {
|
|
203
|
+
try {
|
|
204
|
+
await binding.readiness?.(db);
|
|
205
|
+
if (!isValidPostgresProbe(await db.query("SELECT current_setting('server_version_num') AS smrt_postgres_version"))) throw new Error("invalid probe");
|
|
206
|
+
} catch {
|
|
207
|
+
throw unavailable("database");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
var InitializedDeployedApplicationRuntime = class {
|
|
211
|
+
constructor(db, resolvedRuntime, bindings) {
|
|
212
|
+
this.db = db;
|
|
213
|
+
this.resolvedRuntime = resolvedRuntime;
|
|
214
|
+
this.bindings = bindings;
|
|
215
|
+
this.snapshot = createDiagnostics(resolvedRuntime);
|
|
216
|
+
this.sessionService = new SessionService({ db });
|
|
217
|
+
}
|
|
218
|
+
db;
|
|
219
|
+
resolvedRuntime;
|
|
220
|
+
bindings;
|
|
221
|
+
state = "running";
|
|
222
|
+
closeAttempt;
|
|
223
|
+
pendingReadinessChecks = /* @__PURE__ */ new Set();
|
|
224
|
+
pendingSessionRestorations = /* @__PURE__ */ new Set();
|
|
225
|
+
pendingWorkerInitializations = /* @__PURE__ */ new Set();
|
|
226
|
+
pendingWorkerOperations = /* @__PURE__ */ new Set();
|
|
227
|
+
lifecycleOperationContext = new AsyncLocalStorage();
|
|
228
|
+
activeLifecycleOperations = /* @__PURE__ */ new Set();
|
|
229
|
+
workerCleanups = /* @__PURE__ */ new Set();
|
|
230
|
+
sessionService;
|
|
231
|
+
sessionServiceReady;
|
|
232
|
+
snapshot;
|
|
233
|
+
async restoreSession(sessionId) {
|
|
234
|
+
this.assertRunning();
|
|
235
|
+
const operation = this.beginLifecycleOperation(() => this.restoreSessionWhileRunning(sessionId));
|
|
236
|
+
this.pendingSessionRestorations.add(operation);
|
|
237
|
+
try {
|
|
238
|
+
return await operation;
|
|
239
|
+
} finally {
|
|
240
|
+
this.pendingSessionRestorations.delete(operation);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async restoreSessionWhileRunning(sessionId) {
|
|
244
|
+
let context;
|
|
245
|
+
try {
|
|
246
|
+
await this.initializeSessionService();
|
|
247
|
+
context = await this.sessionService.loadSessionContext(sessionId);
|
|
248
|
+
} catch {
|
|
249
|
+
throw unavailable("authentication");
|
|
250
|
+
}
|
|
251
|
+
this.assertRunning();
|
|
252
|
+
if (context && this.resolvedRuntime.providers.tenancy.context === "required") {
|
|
253
|
+
let tenantId;
|
|
254
|
+
try {
|
|
255
|
+
tenantId = context.tenantId;
|
|
256
|
+
} catch {
|
|
257
|
+
throw unavailable("authentication");
|
|
258
|
+
}
|
|
259
|
+
if (typeof tenantId !== "string" || tenantId.trim().length === 0) throw new DeployedRuntimeError("tenant_context_required", "The authenticated session does not include the required tenant context.", "authentication");
|
|
260
|
+
let tenantAuthorized = false;
|
|
261
|
+
try {
|
|
262
|
+
const membership = context.membership;
|
|
263
|
+
const authorization = context.tenantAuthorization;
|
|
264
|
+
const membershipId = authorization?.membershipId;
|
|
265
|
+
const validMembershipId = typeof membershipId === "string" && membershipId.trim().length > 0;
|
|
266
|
+
if (membership?.isActive() === true) tenantAuthorized = validMembershipId && membership.id === membershipId && authorization?.inheritedFromTenantId === null;
|
|
267
|
+
else if (membership === null) tenantAuthorized = validMembershipId && typeof authorization?.inheritedFromTenantId === "string" && authorization.inheritedFromTenantId.trim().length > 0;
|
|
268
|
+
} catch {
|
|
269
|
+
throw unavailable("authentication");
|
|
270
|
+
}
|
|
271
|
+
if (!tenantAuthorized) throw new DeployedRuntimeError("tenant_context_unauthorized", "The authenticated session is not authorized for its tenant context.", "authentication");
|
|
272
|
+
}
|
|
273
|
+
return context;
|
|
274
|
+
}
|
|
275
|
+
async createTaskWorker(config = {}) {
|
|
276
|
+
this.assertRunning();
|
|
277
|
+
const operation = this.beginLifecycleOperation(() => this.initializeTaskWorker(config));
|
|
278
|
+
this.pendingWorkerInitializations.add(operation);
|
|
279
|
+
try {
|
|
280
|
+
const runner = await operation;
|
|
281
|
+
return this.retainWorker(runner);
|
|
282
|
+
} finally {
|
|
283
|
+
this.pendingWorkerInitializations.delete(operation);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async createScheduleWorker(config = {}) {
|
|
287
|
+
this.assertRunning();
|
|
288
|
+
const operation = this.beginLifecycleOperation(() => this.initializeScheduleWorker(config));
|
|
289
|
+
this.pendingWorkerInitializations.add(operation);
|
|
290
|
+
try {
|
|
291
|
+
const runner = await operation;
|
|
292
|
+
return this.retainWorker(runner);
|
|
293
|
+
} finally {
|
|
294
|
+
this.pendingWorkerInitializations.delete(operation);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
diagnostics() {
|
|
298
|
+
return this.snapshot;
|
|
299
|
+
}
|
|
300
|
+
health() {
|
|
301
|
+
return Object.freeze({
|
|
302
|
+
schemaVersion: 1,
|
|
303
|
+
profile: this.resolvedRuntime.profile,
|
|
304
|
+
status: this.state === "running" ? "healthy" : "stopped"
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
async readiness() {
|
|
308
|
+
if (this.state !== "running") return createStoppedReadiness(this.resolvedRuntime.profile);
|
|
309
|
+
const operation = this.beginLifecycleOperation(() => this.checkReadinessWhileRunning());
|
|
310
|
+
this.pendingReadinessChecks.add(operation);
|
|
311
|
+
try {
|
|
312
|
+
return await operation;
|
|
313
|
+
} finally {
|
|
314
|
+
this.pendingReadinessChecks.delete(operation);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async checkReadinessWhileRunning() {
|
|
318
|
+
const results = await Promise.all([
|
|
319
|
+
probe(() => checkDatabase(this.bindings.database, this.db)),
|
|
320
|
+
probe(() => this.bindings.authentication.readiness()),
|
|
321
|
+
probe(() => this.bindings.assets.readiness()),
|
|
322
|
+
probe(() => this.bindings.secrets.readiness())
|
|
323
|
+
]);
|
|
324
|
+
if (this.state !== "running") return createStoppedReadiness(this.resolvedRuntime.profile);
|
|
325
|
+
const [database, authentication, assets, secrets] = results;
|
|
326
|
+
return freezeReadiness(this.resolvedRuntime.profile, {
|
|
327
|
+
database,
|
|
328
|
+
authentication,
|
|
329
|
+
assets,
|
|
330
|
+
secrets
|
|
331
|
+
}, results.every((result) => result.status === "ready"));
|
|
332
|
+
}
|
|
333
|
+
async close() {
|
|
334
|
+
const operationToken = this.lifecycleOperationContext.getStore();
|
|
335
|
+
const requestedFromLifecycleOperation = operationToken !== void 0 && this.activeLifecycleOperations.has(operationToken);
|
|
336
|
+
if (this.state === "stopped") return;
|
|
337
|
+
if (this.closeAttempt) {
|
|
338
|
+
if (requestedFromLifecycleOperation) return;
|
|
339
|
+
return this.closeAttempt;
|
|
340
|
+
}
|
|
341
|
+
this.state = "closing";
|
|
342
|
+
const pendingOperations = [
|
|
343
|
+
...this.pendingReadinessChecks,
|
|
344
|
+
...this.pendingSessionRestorations,
|
|
345
|
+
...this.pendingWorkerInitializations,
|
|
346
|
+
...this.pendingWorkerOperations
|
|
347
|
+
];
|
|
348
|
+
const shutdownToken = /* @__PURE__ */ Symbol("deployed-runtime-shutdown");
|
|
349
|
+
this.activeLifecycleOperations.add(shutdownToken);
|
|
350
|
+
const attempt = Promise.resolve().then(() => this.lifecycleOperationContext.run(shutdownToken, async () => {
|
|
351
|
+
await Promise.allSettled(pendingOperations);
|
|
352
|
+
await this.cleanupWorkers();
|
|
353
|
+
await this.closeConnection();
|
|
354
|
+
})).finally(() => {
|
|
355
|
+
this.activeLifecycleOperations.delete(shutdownToken);
|
|
356
|
+
});
|
|
357
|
+
this.closeAttempt = attempt.finally(() => {
|
|
358
|
+
this.closeAttempt = void 0;
|
|
359
|
+
});
|
|
360
|
+
if (requestedFromLifecycleOperation) {
|
|
361
|
+
this.closeAttempt.catch(() => void 0);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
return this.closeAttempt;
|
|
365
|
+
}
|
|
366
|
+
async initializeTaskWorker(config) {
|
|
367
|
+
let runner;
|
|
368
|
+
try {
|
|
369
|
+
runner = new TaskRunner(config);
|
|
370
|
+
await runner.initialize(this.db);
|
|
371
|
+
} catch {
|
|
372
|
+
if (runner) await this.stopOrRetainUnreturnedWorker(runner);
|
|
373
|
+
throw unavailable("database");
|
|
374
|
+
}
|
|
375
|
+
if (this.state !== "running") {
|
|
376
|
+
await this.stopOrRetainUnreturnedWorker(runner);
|
|
377
|
+
this.assertRunning();
|
|
378
|
+
}
|
|
379
|
+
return runner;
|
|
380
|
+
}
|
|
381
|
+
async initializeScheduleWorker(config) {
|
|
382
|
+
let runner;
|
|
383
|
+
try {
|
|
384
|
+
runner = new ScheduleRunner(config);
|
|
385
|
+
await runner.initialize(this.db);
|
|
386
|
+
} catch {
|
|
387
|
+
if (runner) await this.stopOrRetainUnreturnedWorker(runner);
|
|
388
|
+
throw unavailable("database");
|
|
389
|
+
}
|
|
390
|
+
if (this.state !== "running") {
|
|
391
|
+
await this.stopOrRetainUnreturnedWorker(runner);
|
|
392
|
+
this.assertRunning();
|
|
393
|
+
}
|
|
394
|
+
return runner;
|
|
395
|
+
}
|
|
396
|
+
async stopOrRetainUnreturnedWorker(runner) {
|
|
397
|
+
const cleanup = () => runner.stop();
|
|
398
|
+
try {
|
|
399
|
+
await cleanup();
|
|
400
|
+
} catch {
|
|
401
|
+
this.workerCleanups.add(cleanup);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
retainWorker(runner) {
|
|
405
|
+
const start = runner.start.bind(runner);
|
|
406
|
+
const stop = runner.stop.bind(runner);
|
|
407
|
+
let operationTail = Promise.resolve();
|
|
408
|
+
const queueOperation = (callback) => {
|
|
409
|
+
const previous = operationTail;
|
|
410
|
+
const operation = this.beginLifecycleOperation(async () => {
|
|
411
|
+
await previous;
|
|
412
|
+
await callback();
|
|
413
|
+
});
|
|
414
|
+
operationTail = operation.catch(() => void 0);
|
|
415
|
+
this.pendingWorkerOperations.add(operation);
|
|
416
|
+
operation.then(() => {
|
|
417
|
+
this.pendingWorkerOperations.delete(operation);
|
|
418
|
+
}, () => {
|
|
419
|
+
this.pendingWorkerOperations.delete(operation);
|
|
420
|
+
});
|
|
421
|
+
return operation;
|
|
422
|
+
};
|
|
423
|
+
const ownedStart = async () => {
|
|
424
|
+
this.assertRunning();
|
|
425
|
+
return queueOperation(async () => {
|
|
426
|
+
this.assertRunning();
|
|
427
|
+
try {
|
|
428
|
+
await start();
|
|
429
|
+
this.assertRunning();
|
|
430
|
+
} catch {
|
|
431
|
+
if (this.state === "running") try {
|
|
432
|
+
await stop();
|
|
433
|
+
} catch {}
|
|
434
|
+
if (this.state !== "running") this.assertRunning();
|
|
435
|
+
throw unavailable("database");
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
};
|
|
439
|
+
const ownedStop = () => queueOperation(async () => {
|
|
440
|
+
try {
|
|
441
|
+
await stop();
|
|
442
|
+
} catch {
|
|
443
|
+
throw unavailable("database");
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
Object.defineProperties(runner, {
|
|
447
|
+
start: {
|
|
448
|
+
configurable: false,
|
|
449
|
+
enumerable: false,
|
|
450
|
+
value: ownedStart,
|
|
451
|
+
writable: false
|
|
452
|
+
},
|
|
453
|
+
stop: {
|
|
454
|
+
configurable: false,
|
|
455
|
+
enumerable: false,
|
|
456
|
+
value: ownedStop,
|
|
457
|
+
writable: false
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
this.workerCleanups.add(ownedStop);
|
|
461
|
+
return runner;
|
|
462
|
+
}
|
|
463
|
+
async cleanupWorkers() {
|
|
464
|
+
const cleanups = [...this.workerCleanups];
|
|
465
|
+
const results = await Promise.allSettled(cleanups.map((cleanup) => Promise.resolve().then(cleanup)));
|
|
466
|
+
let cleanupFailed = false;
|
|
467
|
+
for (const [index, result] of results.entries()) if (result.status === "fulfilled") {
|
|
468
|
+
const cleanup = cleanups[index];
|
|
469
|
+
if (cleanup) this.workerCleanups.delete(cleanup);
|
|
470
|
+
} else cleanupFailed = true;
|
|
471
|
+
if (cleanupFailed) {
|
|
472
|
+
this.state = "close-failed";
|
|
473
|
+
throw unavailable("database");
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
initializeSessionService() {
|
|
477
|
+
if (!this.sessionServiceReady) this.sessionServiceReady = this.sessionService.initialize().catch((error) => {
|
|
478
|
+
this.sessionServiceReady = void 0;
|
|
479
|
+
throw error;
|
|
480
|
+
});
|
|
481
|
+
return this.sessionServiceReady;
|
|
482
|
+
}
|
|
483
|
+
beginLifecycleOperation(operation) {
|
|
484
|
+
const token = /* @__PURE__ */ Symbol("deployed-runtime-operation");
|
|
485
|
+
this.activeLifecycleOperations.add(token);
|
|
486
|
+
return Promise.resolve().then(() => this.lifecycleOperationContext.run(token, operation)).finally(() => {
|
|
487
|
+
this.activeLifecycleOperations.delete(token);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
async closeConnection() {
|
|
491
|
+
try {
|
|
492
|
+
await this.bindings.database.close(this.db);
|
|
493
|
+
this.state = "stopped";
|
|
494
|
+
} catch {
|
|
495
|
+
this.state = "close-failed";
|
|
496
|
+
throw new DeployedRuntimeError("provider_unavailable", "The database connection could not be closed; inspect the provider's private logs.", "database");
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
assertRunning() {
|
|
500
|
+
if (this.state !== "running") throw new DeployedRuntimeError("runtime_stopped", "The deployed application runtime has already stopped.");
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
function createDiagnostics(runtime) {
|
|
504
|
+
const provider = (value) => Object.freeze({
|
|
505
|
+
provider: value,
|
|
506
|
+
configured: true
|
|
507
|
+
});
|
|
508
|
+
return deepFreeze$1({
|
|
509
|
+
schemaVersion: 1,
|
|
510
|
+
runtime,
|
|
511
|
+
providers: {
|
|
512
|
+
database: provider(runtime.providers.database.engine),
|
|
513
|
+
authentication: provider(runtime.providers.authentication.provider),
|
|
514
|
+
assets: provider(runtime.providers.assets.provider),
|
|
515
|
+
secrets: provider(runtime.providers.secrets.provider)
|
|
516
|
+
},
|
|
517
|
+
tenancy: {
|
|
518
|
+
mode: runtime.providers.tenancy.mode,
|
|
519
|
+
context: runtime.providers.tenancy.context,
|
|
520
|
+
isolation: runtime.providers.tenancy.isolation,
|
|
521
|
+
rootTenantFallback: "disabled"
|
|
522
|
+
},
|
|
523
|
+
workers: {
|
|
524
|
+
topology: runtime.providers.jobs.topology,
|
|
525
|
+
taskProcess: "separate",
|
|
526
|
+
scheduleProcess: "separate",
|
|
527
|
+
replicas: runtime.providers.jobs.topology === "scalable" ? "horizontal" : "operator-managed"
|
|
528
|
+
},
|
|
529
|
+
secretValuesIncluded: false
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
async function probe(check) {
|
|
533
|
+
try {
|
|
534
|
+
await check();
|
|
535
|
+
return Object.freeze({ status: "ready" });
|
|
536
|
+
} catch {
|
|
537
|
+
return Object.freeze({ status: "not-ready" });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function createStoppedReadiness(profile) {
|
|
541
|
+
const stopped = Object.freeze({ status: "not-ready" });
|
|
542
|
+
return freezeReadiness(profile, {
|
|
543
|
+
database: stopped,
|
|
544
|
+
authentication: stopped,
|
|
545
|
+
assets: stopped,
|
|
546
|
+
secrets: stopped
|
|
547
|
+
}, false);
|
|
548
|
+
}
|
|
549
|
+
function freezeReadiness(profile, components, ready) {
|
|
550
|
+
return deepFreeze$1({
|
|
551
|
+
schemaVersion: 1,
|
|
552
|
+
profile,
|
|
553
|
+
status: ready ? "ready" : "not-ready",
|
|
554
|
+
components,
|
|
555
|
+
secretValuesIncluded: false
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
function isPlainRecord(value) {
|
|
559
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
560
|
+
const prototype = Object.getPrototypeOf(value);
|
|
561
|
+
return prototype === Object.prototype || prototype === null;
|
|
562
|
+
}
|
|
563
|
+
function isDatabaseInterface(value) {
|
|
564
|
+
return typeof value === "object" && value !== null && typeof value.query === "function";
|
|
565
|
+
}
|
|
566
|
+
function isValidPostgresProbe(value) {
|
|
567
|
+
if (typeof value !== "object" || value === null) return false;
|
|
568
|
+
const rows = value.rows;
|
|
569
|
+
if (!Array.isArray(rows) || rows.length !== 1) return false;
|
|
570
|
+
const row = rows[0];
|
|
571
|
+
if (typeof row !== "object" || row === null) return false;
|
|
572
|
+
const version = row.smrt_postgres_version;
|
|
573
|
+
return typeof version === "string" && /^\d+$/.test(version);
|
|
574
|
+
}
|
|
575
|
+
async function cleanupAfterStartupFailure(binding, db, startupError) {
|
|
576
|
+
const cleanup = () => binding.close(db);
|
|
577
|
+
try {
|
|
578
|
+
await cleanup();
|
|
579
|
+
} catch {
|
|
580
|
+
throw new DeployedRuntimeCleanupError(cleanup);
|
|
581
|
+
}
|
|
582
|
+
throw startupError;
|
|
583
|
+
}
|
|
584
|
+
function deepFreeze$1(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
585
|
+
if (value && typeof value === "object") {
|
|
586
|
+
if (seen.has(value)) return value;
|
|
587
|
+
seen.add(value);
|
|
588
|
+
if (!Object.isFrozen(value)) Object.freeze(value);
|
|
589
|
+
for (const child of Object.values(value)) deepFreeze$1(child, seen);
|
|
590
|
+
}
|
|
591
|
+
return value;
|
|
592
|
+
}
|
|
593
|
+
//#endregion
|
|
594
|
+
//#region src/runtime-diagnostics.ts
|
|
595
|
+
var RUNTIME_DIAGNOSTICS_SCHEMA_VERSION = 1;
|
|
596
|
+
var MAX_RUNTIME_DIAGNOSTIC_TOOLS = 64;
|
|
597
|
+
var MAX_RUNTIME_DIAGNOSTIC_ERRORS = 8;
|
|
598
|
+
var RUNTIME_WORKER_FRESHNESS_SECONDS = 120;
|
|
599
|
+
var CAPABILITY_IDS = [
|
|
600
|
+
"asset-storage",
|
|
601
|
+
"authentication",
|
|
602
|
+
"background-jobs",
|
|
603
|
+
"database",
|
|
604
|
+
"paid-capabilities",
|
|
605
|
+
"secret-storage"
|
|
606
|
+
];
|
|
607
|
+
var ERROR_CODES = [
|
|
608
|
+
"authentication_unavailable",
|
|
609
|
+
"database_unavailable",
|
|
610
|
+
"migration_required",
|
|
611
|
+
"provider_unavailable",
|
|
612
|
+
"runtime_stopped",
|
|
613
|
+
"worker_stale"
|
|
614
|
+
];
|
|
615
|
+
var PROHIBITED_IDENTIFIER_PARTS = /* @__PURE__ */ new Set([
|
|
616
|
+
"authorization",
|
|
617
|
+
"cookie",
|
|
618
|
+
"constructor",
|
|
619
|
+
"credential",
|
|
620
|
+
"email",
|
|
621
|
+
"hash",
|
|
622
|
+
"key",
|
|
623
|
+
"log",
|
|
624
|
+
"logs",
|
|
625
|
+
"password",
|
|
626
|
+
"path",
|
|
627
|
+
"record",
|
|
628
|
+
"prototype",
|
|
629
|
+
"secret",
|
|
630
|
+
"session",
|
|
631
|
+
"stack",
|
|
632
|
+
"tenant",
|
|
633
|
+
"token",
|
|
634
|
+
"url",
|
|
635
|
+
"user"
|
|
636
|
+
]);
|
|
637
|
+
function projectRuntimeDiagnostics(input) {
|
|
638
|
+
const source = plainRecord(input);
|
|
639
|
+
const observedAtValue = read(source, "observedAt");
|
|
640
|
+
const observedAtMilliseconds = timestampMilliseconds(observedAtValue);
|
|
641
|
+
const observedAt = coarseTimestamp(observedAtValue) ?? EPOCH;
|
|
642
|
+
const profile = runtimeProfile(read(source, "profile"));
|
|
643
|
+
const worker = projectWorker(read(source, "worker"), observedAt, observedAtMilliseconds);
|
|
644
|
+
const capabilities = projectCapabilities(read(source, "capabilities"));
|
|
645
|
+
const tools = canonicalTools(read(source, "toolNames"));
|
|
646
|
+
return deepFreeze({
|
|
647
|
+
schemaVersion: 1,
|
|
648
|
+
profile,
|
|
649
|
+
health: oneOf(read(source, "health"), HEALTH_VALUES, "unknown"),
|
|
650
|
+
schema: projectSchema(read(source, "schema")),
|
|
651
|
+
capabilities,
|
|
652
|
+
tools: {
|
|
653
|
+
names: tools,
|
|
654
|
+
count: tools.length,
|
|
655
|
+
digest: createHash("sha256").update(tools.join("\n")).digest("hex")
|
|
656
|
+
},
|
|
657
|
+
operationalDifferences: {
|
|
658
|
+
backgroundJobs: capabilityStatus(capabilities, "background-jobs") === "available" ? "enabled" : "disabled",
|
|
659
|
+
workerTopology: worker.topology
|
|
660
|
+
},
|
|
661
|
+
worker,
|
|
662
|
+
recentErrors: projectErrors(read(source, "recentErrors"))
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
666
|
+
var PROFILES = [
|
|
667
|
+
"local",
|
|
668
|
+
"self-hosted",
|
|
669
|
+
"cloud",
|
|
670
|
+
"unknown"
|
|
671
|
+
];
|
|
672
|
+
var HEALTH_VALUES = [
|
|
673
|
+
"healthy",
|
|
674
|
+
"degraded",
|
|
675
|
+
"stopped",
|
|
676
|
+
"unknown"
|
|
677
|
+
];
|
|
678
|
+
var SCHEMA_VALUES = [
|
|
679
|
+
"ready",
|
|
680
|
+
"not-ready",
|
|
681
|
+
"unknown"
|
|
682
|
+
];
|
|
683
|
+
var MIGRATION_VALUES = [
|
|
684
|
+
"current",
|
|
685
|
+
"failed",
|
|
686
|
+
"pending",
|
|
687
|
+
"unknown"
|
|
688
|
+
];
|
|
689
|
+
var CAPABILITY_VALUES = [
|
|
690
|
+
"available",
|
|
691
|
+
"disabled",
|
|
692
|
+
"unavailable",
|
|
693
|
+
"unknown"
|
|
694
|
+
];
|
|
695
|
+
var WORKER_TOPOLOGIES = [
|
|
696
|
+
"embedded",
|
|
697
|
+
"external",
|
|
698
|
+
"inline",
|
|
699
|
+
"scalable"
|
|
700
|
+
];
|
|
701
|
+
function runtimeProfile(value) {
|
|
702
|
+
return oneOf(value, PROFILES, "unknown");
|
|
703
|
+
}
|
|
704
|
+
function projectSchema(value) {
|
|
705
|
+
const source = plainRecord(value);
|
|
706
|
+
return {
|
|
707
|
+
status: oneOf(read(source, "status"), SCHEMA_VALUES, "unknown"),
|
|
708
|
+
migrations: oneOf(read(source, "migrations"), MIGRATION_VALUES, "unknown")
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
function projectCapabilities(value) {
|
|
712
|
+
const source = plainRecord(value);
|
|
713
|
+
return CAPABILITY_IDS.map((id) => ({
|
|
714
|
+
id,
|
|
715
|
+
status: oneOf(read(source, id), CAPABILITY_VALUES, "unknown")
|
|
716
|
+
}));
|
|
717
|
+
}
|
|
718
|
+
function canonicalTools(value) {
|
|
719
|
+
const names = /* @__PURE__ */ new Set();
|
|
720
|
+
for (const candidate of safeArrayItems(value, 256)) {
|
|
721
|
+
if (typeof candidate !== "string" || !isPublicIdentifier(candidate)) continue;
|
|
722
|
+
names.add(candidate);
|
|
723
|
+
}
|
|
724
|
+
return [...names].sort().slice(0, 64);
|
|
725
|
+
}
|
|
726
|
+
function isPublicIdentifier(value) {
|
|
727
|
+
if (value.length < 1 || value.length > 80 || !/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(value)) return false;
|
|
728
|
+
return !value.split(/[._-]/).some((part) => PROHIBITED_IDENTIFIER_PARTS.has(part));
|
|
729
|
+
}
|
|
730
|
+
function projectWorker(value, observedAt, observedAtMilliseconds) {
|
|
731
|
+
const source = plainRecord(value);
|
|
732
|
+
const topology = oneOf(read(source, "topology"), WORKER_TOPOLOGIES, "inline");
|
|
733
|
+
const required = read(source, "required") === true;
|
|
734
|
+
const heartbeatValue = read(source, "heartbeatAt");
|
|
735
|
+
const heartbeatMilliseconds = timestampMilliseconds(heartbeatValue);
|
|
736
|
+
const heartbeatAt = coarseTimestamp(heartbeatValue);
|
|
737
|
+
let liveness = "not-required";
|
|
738
|
+
if (required) {
|
|
739
|
+
liveness = "unknown";
|
|
740
|
+
if (heartbeatMilliseconds !== null && observedAtMilliseconds !== null) {
|
|
741
|
+
const ageSeconds = (observedAtMilliseconds - heartbeatMilliseconds) / 1e3;
|
|
742
|
+
if (ageSeconds >= 0) liveness = ageSeconds <= 120 ? "alive" : "stale";
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
topology,
|
|
747
|
+
liveness,
|
|
748
|
+
heartbeatAt,
|
|
749
|
+
observedAt
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function projectErrors(value) {
|
|
753
|
+
const errors = [];
|
|
754
|
+
for (const candidate of safeArrayItems(value, 32)) {
|
|
755
|
+
const source = plainRecord(candidate);
|
|
756
|
+
const at = coarseTimestamp(read(source, "at"));
|
|
757
|
+
if (!at) continue;
|
|
758
|
+
const rawCode = read(source, "code");
|
|
759
|
+
const code = ERROR_CODES.includes(rawCode) ? rawCode : "runtime_error";
|
|
760
|
+
errors.push({
|
|
761
|
+
code,
|
|
762
|
+
at
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
return errors.sort((left, right) => left.at === right.at ? left.code.localeCompare(right.code) : right.at.localeCompare(left.at)).slice(0, 8);
|
|
766
|
+
}
|
|
767
|
+
function capabilityStatus(capabilities, id) {
|
|
768
|
+
return capabilities.find((capability) => capability.id === id)?.status ?? "unknown";
|
|
769
|
+
}
|
|
770
|
+
function coarseTimestamp(value) {
|
|
771
|
+
const milliseconds = timestampMilliseconds(value);
|
|
772
|
+
if (milliseconds === null) return null;
|
|
773
|
+
const minute = Math.floor(milliseconds / 6e4) * 6e4;
|
|
774
|
+
return new Date(minute).toISOString();
|
|
775
|
+
}
|
|
776
|
+
function timestampMilliseconds(value) {
|
|
777
|
+
try {
|
|
778
|
+
if (!(typeof value === "string" || value instanceof Date)) return null;
|
|
779
|
+
const milliseconds = value instanceof Date ? value.getTime() : Date.parse(value);
|
|
780
|
+
return Number.isFinite(milliseconds) ? milliseconds : null;
|
|
781
|
+
} catch {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function oneOf(value, values, fallback) {
|
|
786
|
+
return typeof value === "string" && values.includes(value) ? value : fallback;
|
|
787
|
+
}
|
|
788
|
+
function plainRecord(value) {
|
|
789
|
+
try {
|
|
790
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
791
|
+
const prototype = Object.getPrototypeOf(value);
|
|
792
|
+
return prototype === Object.prototype || prototype === null ? value : null;
|
|
793
|
+
} catch {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function read(source, key) {
|
|
798
|
+
try {
|
|
799
|
+
if (!source || !Object.hasOwn(source, key)) return;
|
|
800
|
+
return source[key];
|
|
801
|
+
} catch {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function safeArrayItems(value, maximum) {
|
|
806
|
+
try {
|
|
807
|
+
if (!Array.isArray(value)) return [];
|
|
808
|
+
const length = Math.min(value.length, maximum);
|
|
809
|
+
if (!Number.isSafeInteger(length) || length < 0) return [];
|
|
810
|
+
const items = [];
|
|
811
|
+
for (let index = 0; index < length; index += 1) try {
|
|
812
|
+
items.push(value[index]);
|
|
813
|
+
} catch {
|
|
814
|
+
items.push(void 0);
|
|
815
|
+
}
|
|
816
|
+
return items;
|
|
817
|
+
} catch {
|
|
818
|
+
return [];
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function deepFreeze(value) {
|
|
822
|
+
if (value && typeof value === "object") {
|
|
823
|
+
Object.freeze(value);
|
|
824
|
+
for (const child of Object.values(value)) if (child && typeof child === "object" && !Object.isFrozen(child)) deepFreeze(child);
|
|
825
|
+
}
|
|
826
|
+
return value;
|
|
827
|
+
}
|
|
828
|
+
//#endregion
|
|
14
829
|
//#region src/index.ts
|
|
15
830
|
var DEFAULT_BIND_HOST = "127.0.0.1";
|
|
16
831
|
var DEFAULT_BOOTSTRAP_TTL_SECONDS = 600;
|
|
@@ -65,6 +880,44 @@ var INITIALIZATION_LOCK_WORKER = `
|
|
|
65
880
|
});
|
|
66
881
|
`;
|
|
67
882
|
var execFileAsync = promisify(execFile);
|
|
883
|
+
async function prepareLocalDatabaseStorage(options) {
|
|
884
|
+
const paths = resolveLocalRuntimePaths(options);
|
|
885
|
+
const sourceRoot = resolve(options.sourceRoot ?? process.cwd());
|
|
886
|
+
const appId = validateApplicationId(options.appId);
|
|
887
|
+
await preflightLocalApplicationRoot(paths.root, sourceRoot, appId);
|
|
888
|
+
const initializationLock = await acquireInitializationLock(paths.root);
|
|
889
|
+
let db;
|
|
890
|
+
let failure;
|
|
891
|
+
try {
|
|
892
|
+
await preflightLocalApplicationRoot(paths.root, sourceRoot, appId);
|
|
893
|
+
db = (await acquireLocalDatabase(paths, sourceRoot, appId)).db;
|
|
894
|
+
await db.close?.();
|
|
895
|
+
db = void 0;
|
|
896
|
+
} catch (error) {
|
|
897
|
+
failure = db ? await closeDatabaseAfterFailure(db, error) : error;
|
|
898
|
+
db = void 0;
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
await initializationLock.release();
|
|
902
|
+
} catch (releaseError) {
|
|
903
|
+
failure = failure ? preservePrimaryFailure(failure, releaseError, "initializationLockReleaseError", "Local database storage preparation and initialization-lock release both failed.") : releaseError;
|
|
904
|
+
}
|
|
905
|
+
if (failure) throw failure;
|
|
906
|
+
return paths;
|
|
907
|
+
}
|
|
908
|
+
async function validateLocalDatabaseStorage(options) {
|
|
909
|
+
const paths = resolveLocalRuntimePaths(options);
|
|
910
|
+
const sourceRoot = resolve(options.sourceRoot ?? process.cwd());
|
|
911
|
+
const appId = validateApplicationId(options.appId);
|
|
912
|
+
await preflightLocalApplicationRoot(paths.root, sourceRoot, appId);
|
|
913
|
+
const canonicalSourceRoot = await realpath(sourceRoot);
|
|
914
|
+
await validateRootMarker(resolve(paths.root, `${ROOT_MARKER_PREFIX}${appId}`), canonicalSourceRoot);
|
|
915
|
+
await validateExistingPathChain(paths.database, canonicalSourceRoot, "file");
|
|
916
|
+
const database = await lstat(paths.database);
|
|
917
|
+
const currentUid = process.getuid?.();
|
|
918
|
+
if (database.isSymbolicLink() || !database.isFile() || currentUid !== void 0 && database.uid !== currentUid || (database.mode & 511) !== 384) throw unsafeFilesystemEntry(`Local application database must be a current-user-owned mode-0600 file: ${paths.database}`);
|
|
919
|
+
return paths;
|
|
920
|
+
}
|
|
68
921
|
var LocalRuntimeError = class extends Error {
|
|
69
922
|
constructor(code, message) {
|
|
70
923
|
super(message);
|
|
@@ -74,7 +927,7 @@ var LocalRuntimeError = class extends Error {
|
|
|
74
927
|
code;
|
|
75
928
|
};
|
|
76
929
|
function resolveLocalRuntimePaths(options) {
|
|
77
|
-
const appId =
|
|
930
|
+
const appId = validateApplicationId(options.appId);
|
|
78
931
|
const runtimePlatform = options.platform ?? platform();
|
|
79
932
|
const runtimeHome = options.homeDirectory ?? homedir();
|
|
80
933
|
const defaultRoot = defaultApplicationDataRoot(runtimePlatform, runtimeHome, options.env ?? process.env);
|
|
@@ -99,7 +952,7 @@ async function initializeLocalApplicationRuntime(options) {
|
|
|
99
952
|
});
|
|
100
953
|
const paths = resolveLocalRuntimePaths(options);
|
|
101
954
|
const sourceRoot = resolve(options.sourceRoot ?? process.cwd());
|
|
102
|
-
const appId =
|
|
955
|
+
const appId = validateApplicationId(options.appId);
|
|
103
956
|
await preflightLocalApplicationRoot(paths.root, sourceRoot, appId);
|
|
104
957
|
const initializationLock = await acquireInitializationLock(paths.root);
|
|
105
958
|
let db;
|
|
@@ -495,13 +1348,16 @@ async function acquireLocalDatabase(paths, sourceRoot, appId) {
|
|
|
495
1348
|
root: paths.root
|
|
496
1349
|
}
|
|
497
1350
|
});
|
|
1351
|
+
await enforcePrivateDatabaseMode(paths.database, canonicalSourceRoot);
|
|
498
1352
|
} catch (error) {
|
|
1353
|
+
if (db) throw await closeDatabaseAfterFailure(db, error);
|
|
499
1354
|
if (prepared.pendingMarkerCreatedByAttempt) {
|
|
500
1355
|
await removeMarkerIfPresent(prepared.pendingMarker, canonicalSourceRoot);
|
|
501
1356
|
await removeCreatedDirectories(createdDirectories);
|
|
502
1357
|
}
|
|
503
1358
|
throw error;
|
|
504
1359
|
}
|
|
1360
|
+
if (!db) throw new Error("Local runtime database acquisition returned no database.");
|
|
505
1361
|
try {
|
|
506
1362
|
await publishRootMarker(prepared.finalMarker, canonicalSourceRoot);
|
|
507
1363
|
await removeMarkerIfPresent(prepared.pendingMarker, canonicalSourceRoot);
|
|
@@ -513,6 +1369,15 @@ async function acquireLocalDatabase(paths, sourceRoot, appId) {
|
|
|
513
1369
|
db
|
|
514
1370
|
};
|
|
515
1371
|
}
|
|
1372
|
+
async function enforcePrivateDatabaseMode(path, canonicalSourceRoot) {
|
|
1373
|
+
const database = await openSafeFile(path, constants.O_RDWR, 384, "Local application database", canonicalSourceRoot);
|
|
1374
|
+
try {
|
|
1375
|
+
await database.chmod(384);
|
|
1376
|
+
await database.sync();
|
|
1377
|
+
} finally {
|
|
1378
|
+
await database.close();
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
516
1381
|
async function closeDatabaseAfterFailure(db, primaryFailure) {
|
|
517
1382
|
try {
|
|
518
1383
|
await db.close?.();
|
|
@@ -895,11 +1760,17 @@ function normalizeLoopbackHost(value) {
|
|
|
895
1760
|
if (octets.length === 4 && octets.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255) && Number(octets[0]) === 127) return host;
|
|
896
1761
|
throw new LocalRuntimeError("unsafe_public_exposure", "Owner bootstrap is restricted to a loopback IP literal. Use ::1 or 127.0.0.0/8.");
|
|
897
1762
|
}
|
|
898
|
-
function
|
|
1763
|
+
function validateApplicationId(value) {
|
|
899
1764
|
const appId = value.trim().toLowerCase();
|
|
900
1765
|
if (!/^[a-z0-9][a-z0-9.-]{0,62}$/.test(appId)) throw new LocalRuntimeError("invalid_configuration", "appId must contain only lowercase letters, digits, dots, or hyphens.");
|
|
901
1766
|
return appId;
|
|
902
1767
|
}
|
|
1768
|
+
function encodeApplicationId(value) {
|
|
1769
|
+
const source = value.trim().toLowerCase();
|
|
1770
|
+
if (/^[a-z0-9][a-z0-9.-]{0,62}$/.test(source)) return source;
|
|
1771
|
+
const digest = createHash("sha256").update(value.trim()).digest("hex").slice(0, 10);
|
|
1772
|
+
return validateApplicationId(`${source.replace(/[^a-z0-9.-]+/g, "-").replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, "").slice(0, 52).replace(/[^a-z0-9]+$/g, "") || "app"}-${digest}`);
|
|
1773
|
+
}
|
|
903
1774
|
function defaultApplicationDataRoot(runtimePlatform, runtimeHome, env) {
|
|
904
1775
|
if (runtimePlatform === "darwin") return resolve(runtimeHome, "Library", "Application Support");
|
|
905
1776
|
if (runtimePlatform === "win32") return resolve(env.LOCALAPPDATA ?? env.APPDATA ?? runtimeHome);
|
|
@@ -931,6 +1802,6 @@ function isDirectoryNotEmpty(error) {
|
|
|
931
1802
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOTEMPTY";
|
|
932
1803
|
}
|
|
933
1804
|
//#endregion
|
|
934
|
-
export { LocalRuntimeError, initializeLocalApplicationRuntime, resolveLocalRuntimePaths };
|
|
1805
|
+
export { DeployedRuntimeCleanupError, DeployedRuntimeError, LocalRuntimeError, MAX_RUNTIME_DIAGNOSTIC_ERRORS, MAX_RUNTIME_DIAGNOSTIC_TOOLS, RUNTIME_DIAGNOSTICS_SCHEMA_VERSION, RUNTIME_WORKER_FRESHNESS_SECONDS, encodeApplicationId, initializeDeployedApplicationRuntime, initializeLocalApplicationRuntime, prepareLocalDatabaseStorage, projectRuntimeDiagnostics, resolveLocalRuntimePaths, validateApplicationId, validateLocalDatabaseStorage };
|
|
935
1806
|
|
|
936
1807
|
//# sourceMappingURL=index.js.map
|