@happyvertical/smrt-app-runtime 0.43.10 → 0.44.1
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 +26 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +299 -8
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/package.json +8 -8
package/AGENTS.md
CHANGED
|
@@ -5,6 +5,9 @@ Application infrastructure composition for the validated runtime profiles in
|
|
|
5
5
|
|
|
6
6
|
## Local profile
|
|
7
7
|
|
|
8
|
+
- `validateApplicationId()` is the canonical strict validator for explicit app
|
|
9
|
+
IDs. `encodeApplicationId()` derives a length-bounded, collision-resistant
|
|
10
|
+
ID from package names; generated operational surfaces must share these APIs.
|
|
8
11
|
- `initializeLocalApplicationRuntime()` owns user data paths, SQLite tuning,
|
|
9
12
|
local application-secret creation, and the single-use owner bootstrap flow.
|
|
10
13
|
- SQLite is acquired through `@happyvertical/sql`'s explicit
|
|
@@ -12,6 +15,13 @@ Application infrastructure composition for the validated runtime profiles in
|
|
|
12
15
|
runtime establishes its user-owned mode-0700 data root.
|
|
13
16
|
- Application migrations are explicit through `prepareDatabase`; runtime never
|
|
14
17
|
creates application model tables implicitly.
|
|
18
|
+
- Standalone migration commands must call `prepareLocalDatabaseStorage()`
|
|
19
|
+
before opening SQLite so custody is established without creating schema or
|
|
20
|
+
bootstrap records.
|
|
21
|
+
- Read-only/copy/import operator commands must call
|
|
22
|
+
`validateLocalDatabaseStorage()` before opening SQLite; it verifies the
|
|
23
|
+
app-bound marker, source separation, real path chain, ownership, and mode
|
|
24
|
+
without creating or repairing storage.
|
|
15
25
|
- Owner bootstrap creates normal `Person`, `User`, `Tenant`, owner `Role` /
|
|
16
26
|
`Membership`, and `Session` records in one transaction.
|
|
17
27
|
- Bootstrap is loopback-only. Only an HMAC of the short-lived token is stored.
|
|
@@ -47,6 +57,22 @@ Application infrastructure composition for the validated runtime profiles in
|
|
|
47
57
|
- Cloud must keep required tenant context and must never introduce a root or
|
|
48
58
|
unscoped tenant fallback. RLS remains an explicit deployment/migration choice.
|
|
49
59
|
|
|
60
|
+
## Public runtime diagnostics
|
|
61
|
+
|
|
62
|
+
- `projectRuntimeDiagnostics()` is the only public diagnostic projection. It
|
|
63
|
+
constructs the schema-version-1 allowlist and never serializes the private
|
|
64
|
+
local or deployed diagnostics objects.
|
|
65
|
+
- The allowlist is limited to active profile/coarse health, schema and
|
|
66
|
+
migration readiness, six fixed capability statuses, a sorted/capped public
|
|
67
|
+
tool inventory plus digest, explicit operational topology differences,
|
|
68
|
+
coarse worker heartbeat state, and eight stable code/timestamp errors.
|
|
69
|
+
- Callers supply an explicit clock and worker-heartbeat seam. A missing
|
|
70
|
+
heartbeat is `unknown`; web-process liveness never proves worker liveness.
|
|
71
|
+
- Error ingestion maps unknown codes to `runtime_error`, truncates timestamps
|
|
72
|
+
to the minute, and ignores messages, stacks, logs, records, and nested state.
|
|
73
|
+
- Authentication and diagnostics-read authorization belong before every call
|
|
74
|
+
to the projector or its runtime probes.
|
|
75
|
+
|
|
50
76
|
## Invariants
|
|
51
77
|
|
|
52
78
|
- Never expose application-secret bytes or bootstrap token hashes in diagnostics.
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ import { SessionService } from '@happyvertical/smrt-users';
|
|
|
11
11
|
import { TaskRunner } from '@happyvertical/smrt-jobs';
|
|
12
12
|
import { TaskRunnerConfig } from '@happyvertical/smrt-jobs';
|
|
13
13
|
|
|
14
|
+
declare const CAPABILITY_IDS: readonly ["asset-storage", "authentication", "background-jobs", "database", "paid-capabilities", "secret-storage"];
|
|
15
|
+
|
|
16
|
+
declare type CapabilityId = (typeof CAPABILITY_IDS)[number];
|
|
17
|
+
|
|
18
|
+
declare type CapabilityStatus = 'available' | 'disabled' | 'unavailable' | 'unknown';
|
|
19
|
+
|
|
14
20
|
export declare interface ClaimLocalOwnerInput {
|
|
15
21
|
token: string;
|
|
16
22
|
name: string;
|
|
@@ -136,6 +142,11 @@ export declare interface DeployedRuntimeReadiness {
|
|
|
136
142
|
readonly secretValuesIncluded: false;
|
|
137
143
|
}
|
|
138
144
|
|
|
145
|
+
/** Encode a package/project identity into a collision-resistant runtime ID. */
|
|
146
|
+
export declare function encodeApplicationId(value: string): string;
|
|
147
|
+
|
|
148
|
+
declare const ERROR_CODES: readonly ["authentication_unavailable", "database_unavailable", "migration_required", "provider_unavailable", "runtime_stopped", "worker_stale"];
|
|
149
|
+
|
|
139
150
|
/**
|
|
140
151
|
* Initialize a self-hosted or cloud application composition.
|
|
141
152
|
*
|
|
@@ -263,6 +274,23 @@ export declare interface LocalRuntimePaths {
|
|
|
263
274
|
readonly applicationSecret: string;
|
|
264
275
|
}
|
|
265
276
|
|
|
277
|
+
export declare const MAX_RUNTIME_DIAGNOSTIC_ERRORS = 8;
|
|
278
|
+
|
|
279
|
+
export declare const MAX_RUNTIME_DIAGNOSTIC_TOOLS = 64;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Claim the secure local database root before a standalone migration command
|
|
283
|
+
* opens SQLite. This does not create application schema or bootstrap records.
|
|
284
|
+
*/
|
|
285
|
+
export declare function prepareLocalDatabaseStorage(options: ResolveLocalRuntimePathsOptions): Promise<LocalRuntimePaths>;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Project private local/deployed state into the deterministic public contract.
|
|
289
|
+
* Unknown, oversized, nested, accessor-backed, or prohibited values are never
|
|
290
|
+
* copied. Invalid error codes become `runtime_error` at ingestion.
|
|
291
|
+
*/
|
|
292
|
+
export declare function projectRuntimeDiagnostics(input: RuntimeDiagnosticsProjectionInput): RuntimeDiagnostics;
|
|
293
|
+
|
|
266
294
|
export declare type PublicAuthenticationProvider = Exclude<AuthenticationProvider, 'owner-bootstrap'>;
|
|
267
295
|
|
|
268
296
|
/**
|
|
@@ -285,4 +313,72 @@ export declare interface ResolveLocalRuntimePathsOptions {
|
|
|
285
313
|
env?: NodeJS.ProcessEnv;
|
|
286
314
|
}
|
|
287
315
|
|
|
316
|
+
export declare const RUNTIME_DIAGNOSTICS_SCHEMA_VERSION: 1;
|
|
317
|
+
|
|
318
|
+
export declare const RUNTIME_WORKER_FRESHNESS_SECONDS = 120;
|
|
319
|
+
|
|
320
|
+
declare type RuntimeDiagnosticErrorCode = (typeof ERROR_CODES)[number] | 'runtime_error';
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Versioned public diagnostics returned only after application authorization.
|
|
324
|
+
* Every field is a fixed allowlist projection; this is intentionally not a
|
|
325
|
+
* cleaned or serialized form of either private runtime diagnostics object.
|
|
326
|
+
*/
|
|
327
|
+
export declare interface RuntimeDiagnostics {
|
|
328
|
+
readonly schemaVersion: 1;
|
|
329
|
+
readonly profile: ApplicationRuntimeProfile | 'unknown';
|
|
330
|
+
readonly health: 'healthy' | 'degraded' | 'stopped' | 'unknown';
|
|
331
|
+
readonly schema: {
|
|
332
|
+
readonly status: 'ready' | 'not-ready' | 'unknown';
|
|
333
|
+
readonly migrations: 'current' | 'failed' | 'pending' | 'unknown';
|
|
334
|
+
};
|
|
335
|
+
readonly capabilities: readonly {
|
|
336
|
+
readonly id: CapabilityId;
|
|
337
|
+
readonly status: CapabilityStatus;
|
|
338
|
+
}[];
|
|
339
|
+
readonly tools: {
|
|
340
|
+
readonly names: readonly string[];
|
|
341
|
+
readonly count: number;
|
|
342
|
+
readonly digest: string;
|
|
343
|
+
};
|
|
344
|
+
readonly operationalDifferences: {
|
|
345
|
+
readonly backgroundJobs: 'disabled' | 'enabled';
|
|
346
|
+
readonly workerTopology: WorkerTopology;
|
|
347
|
+
};
|
|
348
|
+
readonly worker: {
|
|
349
|
+
readonly topology: WorkerTopology;
|
|
350
|
+
readonly liveness: 'alive' | 'not-required' | 'stale' | 'unknown';
|
|
351
|
+
readonly heartbeatAt: string | null;
|
|
352
|
+
readonly observedAt: string;
|
|
353
|
+
};
|
|
354
|
+
readonly recentErrors: readonly {
|
|
355
|
+
readonly code: RuntimeDiagnosticErrorCode;
|
|
356
|
+
readonly at: string;
|
|
357
|
+
}[];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export declare interface RuntimeDiagnosticsProjectionInput {
|
|
361
|
+
readonly profile?: unknown;
|
|
362
|
+
readonly health?: unknown;
|
|
363
|
+
readonly schema?: unknown;
|
|
364
|
+
readonly capabilities?: unknown;
|
|
365
|
+
readonly toolNames?: unknown;
|
|
366
|
+
readonly worker?: unknown;
|
|
367
|
+
readonly recentErrors?: unknown;
|
|
368
|
+
/** Required explicit clock seam; the projector never reads the process clock. */
|
|
369
|
+
readonly observedAt?: unknown;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Validate an explicit runtime identity. */
|
|
373
|
+
export declare function validateApplicationId(value: string): string;
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Verify an already-initialized local database root without creating or
|
|
377
|
+
* changing any filesystem/database artifact. Operator read/copy/import paths
|
|
378
|
+
* use this boundary before opening SQLite outside the runtime bootstrap.
|
|
379
|
+
*/
|
|
380
|
+
export declare function validateLocalDatabaseStorage(options: ResolveLocalRuntimePathsOptions): Promise<LocalRuntimePaths>;
|
|
381
|
+
|
|
382
|
+
declare type WorkerTopology = 'embedded' | 'external' | 'inline' | 'scalable';
|
|
383
|
+
|
|
288
384
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -505,7 +505,7 @@ function createDiagnostics(runtime) {
|
|
|
505
505
|
provider: value,
|
|
506
506
|
configured: true
|
|
507
507
|
});
|
|
508
|
-
return deepFreeze({
|
|
508
|
+
return deepFreeze$1({
|
|
509
509
|
schemaVersion: 1,
|
|
510
510
|
runtime,
|
|
511
511
|
providers: {
|
|
@@ -547,7 +547,7 @@ function createStoppedReadiness(profile) {
|
|
|
547
547
|
}, false);
|
|
548
548
|
}
|
|
549
549
|
function freezeReadiness(profile, components, ready) {
|
|
550
|
-
return deepFreeze({
|
|
550
|
+
return deepFreeze$1({
|
|
551
551
|
schemaVersion: 1,
|
|
552
552
|
profile,
|
|
553
553
|
status: ready ? "ready" : "not-ready",
|
|
@@ -581,12 +581,247 @@ async function cleanupAfterStartupFailure(binding, db, startupError) {
|
|
|
581
581
|
}
|
|
582
582
|
throw startupError;
|
|
583
583
|
}
|
|
584
|
-
function deepFreeze(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
584
|
+
function deepFreeze$1(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
585
585
|
if (value && typeof value === "object") {
|
|
586
586
|
if (seen.has(value)) return value;
|
|
587
587
|
seen.add(value);
|
|
588
588
|
if (!Object.isFrozen(value)) Object.freeze(value);
|
|
589
|
-
for (const child of Object.values(value)) deepFreeze(child, seen);
|
|
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);
|
|
590
825
|
}
|
|
591
826
|
return value;
|
|
592
827
|
}
|
|
@@ -645,6 +880,44 @@ var INITIALIZATION_LOCK_WORKER = `
|
|
|
645
880
|
});
|
|
646
881
|
`;
|
|
647
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
|
+
}
|
|
648
921
|
var LocalRuntimeError = class extends Error {
|
|
649
922
|
constructor(code, message) {
|
|
650
923
|
super(message);
|
|
@@ -654,7 +927,7 @@ var LocalRuntimeError = class extends Error {
|
|
|
654
927
|
code;
|
|
655
928
|
};
|
|
656
929
|
function resolveLocalRuntimePaths(options) {
|
|
657
|
-
const appId =
|
|
930
|
+
const appId = validateApplicationId(options.appId);
|
|
658
931
|
const runtimePlatform = options.platform ?? platform();
|
|
659
932
|
const runtimeHome = options.homeDirectory ?? homedir();
|
|
660
933
|
const defaultRoot = defaultApplicationDataRoot(runtimePlatform, runtimeHome, options.env ?? process.env);
|
|
@@ -679,7 +952,7 @@ async function initializeLocalApplicationRuntime(options) {
|
|
|
679
952
|
});
|
|
680
953
|
const paths = resolveLocalRuntimePaths(options);
|
|
681
954
|
const sourceRoot = resolve(options.sourceRoot ?? process.cwd());
|
|
682
|
-
const appId =
|
|
955
|
+
const appId = validateApplicationId(options.appId);
|
|
683
956
|
await preflightLocalApplicationRoot(paths.root, sourceRoot, appId);
|
|
684
957
|
const initializationLock = await acquireInitializationLock(paths.root);
|
|
685
958
|
let db;
|
|
@@ -1075,13 +1348,16 @@ async function acquireLocalDatabase(paths, sourceRoot, appId) {
|
|
|
1075
1348
|
root: paths.root
|
|
1076
1349
|
}
|
|
1077
1350
|
});
|
|
1351
|
+
await enforcePrivateDatabaseMode(paths.database, canonicalSourceRoot);
|
|
1078
1352
|
} catch (error) {
|
|
1353
|
+
if (db) throw await closeDatabaseAfterFailure(db, error);
|
|
1079
1354
|
if (prepared.pendingMarkerCreatedByAttempt) {
|
|
1080
1355
|
await removeMarkerIfPresent(prepared.pendingMarker, canonicalSourceRoot);
|
|
1081
1356
|
await removeCreatedDirectories(createdDirectories);
|
|
1082
1357
|
}
|
|
1083
1358
|
throw error;
|
|
1084
1359
|
}
|
|
1360
|
+
if (!db) throw new Error("Local runtime database acquisition returned no database.");
|
|
1085
1361
|
try {
|
|
1086
1362
|
await publishRootMarker(prepared.finalMarker, canonicalSourceRoot);
|
|
1087
1363
|
await removeMarkerIfPresent(prepared.pendingMarker, canonicalSourceRoot);
|
|
@@ -1093,6 +1369,15 @@ async function acquireLocalDatabase(paths, sourceRoot, appId) {
|
|
|
1093
1369
|
db
|
|
1094
1370
|
};
|
|
1095
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
|
+
}
|
|
1096
1381
|
async function closeDatabaseAfterFailure(db, primaryFailure) {
|
|
1097
1382
|
try {
|
|
1098
1383
|
await db.close?.();
|
|
@@ -1475,11 +1760,17 @@ function normalizeLoopbackHost(value) {
|
|
|
1475
1760
|
if (octets.length === 4 && octets.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255) && Number(octets[0]) === 127) return host;
|
|
1476
1761
|
throw new LocalRuntimeError("unsafe_public_exposure", "Owner bootstrap is restricted to a loopback IP literal. Use ::1 or 127.0.0.0/8.");
|
|
1477
1762
|
}
|
|
1478
|
-
function
|
|
1763
|
+
function validateApplicationId(value) {
|
|
1479
1764
|
const appId = value.trim().toLowerCase();
|
|
1480
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.");
|
|
1481
1766
|
return appId;
|
|
1482
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
|
+
}
|
|
1483
1774
|
function defaultApplicationDataRoot(runtimePlatform, runtimeHome, env) {
|
|
1484
1775
|
if (runtimePlatform === "darwin") return resolve(runtimeHome, "Library", "Application Support");
|
|
1485
1776
|
if (runtimePlatform === "win32") return resolve(env.LOCALAPPDATA ?? env.APPDATA ?? runtimeHome);
|
|
@@ -1511,6 +1802,6 @@ function isDirectoryNotEmpty(error) {
|
|
|
1511
1802
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOTEMPTY";
|
|
1512
1803
|
}
|
|
1513
1804
|
//#endregion
|
|
1514
|
-
export { DeployedRuntimeCleanupError, DeployedRuntimeError, LocalRuntimeError, initializeDeployedApplicationRuntime, 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 };
|
|
1515
1806
|
|
|
1516
1807
|
//# sourceMappingURL=index.js.map
|