@nylorun/runtime 0.9.0-beta → 0.11.0-beta
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/CHANGELOG.md +81 -0
- package/README.md +110 -37
- package/dist/adapters/execution/restate.d.ts +130 -0
- package/dist/adapters/execution/restate.js +442 -0
- package/dist/adapters/media.d.ts +2 -2
- package/dist/adapters/media.js +2 -2
- package/dist/adapters/observe.d.ts +2 -1
- package/dist/adapters/observe.js +2 -3
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/adapters/streams/s2.d.ts +18 -0
- package/dist/adapters/streams/s2.js +361 -0
- package/dist/assets.d.ts +2 -2
- package/dist/assets.js +5 -3
- package/dist/core/executors.d.ts +15 -3
- package/dist/core/executors.js +40 -5
- package/dist/core/flow-host.d.ts +225 -0
- package/dist/core/flow-host.js +518 -0
- package/dist/core/limits.d.ts +40 -0
- package/dist/core/limits.js +76 -0
- package/dist/core/runtime.d.ts +5 -90
- package/dist/core/runtime.js +3 -1447
- package/dist/core/sandbox-routes.d.ts +40 -0
- package/dist/core/sandbox-routes.js +106 -0
- package/dist/core/turn-manifest.d.ts +50 -0
- package/dist/core/turn-manifest.js +66 -0
- package/dist/execution/memory.d.ts +54 -0
- package/dist/execution/memory.js +217 -0
- package/dist/execution/types.d.ts +125 -0
- package/dist/execution/types.js +52 -0
- package/dist/host/config-for.d.ts +17 -0
- package/dist/host/config-for.js +27 -0
- package/dist/host/config.d.ts +16 -0
- package/dist/host/create-host.d.ts +56 -0
- package/dist/host/create-host.js +420 -0
- package/dist/host/environment.d.ts +13 -0
- package/dist/host/environment.js +53 -0
- package/dist/host/execution.d.ts +61 -0
- package/dist/host/execution.js +69 -0
- package/dist/host/http.d.ts +39 -0
- package/dist/host/http.js +142 -0
- package/dist/host/index.d.ts +8 -0
- package/dist/host/index.js +7 -0
- package/dist/host/logger.d.ts +3 -0
- package/dist/host/logger.js +18 -0
- package/dist/host/main.d.ts +3 -0
- package/dist/host/main.js +201 -0
- package/dist/host/stack-config.d.ts +72 -0
- package/dist/host/stack-config.js +193 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +4 -2
- package/dist/infra/database.d.ts +16 -0
- package/dist/infra/database.js +31 -0
- package/dist/infra/execution.d.ts +39 -0
- package/dist/infra/execution.js +68 -0
- package/dist/infra/index.d.ts +42 -0
- package/dist/infra/index.js +56 -0
- package/dist/infra/readiness.d.ts +33 -0
- package/dist/infra/readiness.js +53 -0
- package/dist/infra/streams.d.ts +17 -0
- package/dist/infra/streams.js +33 -0
- package/dist/infra/workers.d.ts +39 -0
- package/dist/infra/workers.js +31 -0
- package/dist/mcp/connect.d.ts +1 -0
- package/dist/mcp/connect.js +2 -0
- package/dist/mcp/pool.d.ts +3 -1
- package/dist/mcp/pool.js +2 -1
- package/dist/mcp/stdio.d.ts +2 -0
- package/dist/mcp/stdio.js +3 -1
- package/dist/model/auth-store.d.ts +1 -1
- package/dist/model/auth-store.js +2 -2
- package/dist/model/models.d.ts +4 -0
- package/dist/model/models.js +22 -26
- package/dist/model/pi-model.d.ts +2 -2
- package/dist/model/pi-model.js +6 -6
- package/dist/model/settings.d.ts +5 -1
- package/dist/model/settings.js +6 -2
- package/dist/sandbox/manager.d.ts +26 -8
- package/dist/sandbox/manager.js +95 -47
- package/dist/sandbox/select.d.ts +5 -4
- package/dist/sandbox/select.js +18 -13
- package/dist/sandbox/share.d.ts +23 -0
- package/dist/sandbox/share.js +58 -0
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/store/canonical.d.ts +5 -0
- package/dist/store/canonical.js +17 -0
- package/dist/store/cursor.d.ts +9 -0
- package/dist/store/cursor.js +18 -0
- package/dist/store/memory.d.ts +57 -0
- package/dist/store/memory.js +643 -0
- package/dist/store/ownership.d.ts +19 -0
- package/dist/store/ownership.js +33 -0
- package/dist/store/postgres/connect.d.ts +25 -0
- package/dist/store/postgres/connect.js +25 -0
- package/dist/store/postgres/locking.d.ts +16 -0
- package/dist/store/postgres/locking.js +22 -0
- package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
- package/dist/store/postgres/migrations/001_initial.js +196 -0
- package/dist/store/postgres/migrations/index.d.ts +49 -0
- package/dist/store/postgres/migrations/index.js +75 -0
- package/dist/store/postgres/names.d.ts +11 -0
- package/dist/store/postgres/names.js +42 -0
- package/dist/store/postgres/store.d.ts +11 -0
- package/dist/store/postgres/store.js +790 -0
- package/dist/store/postgres/tenants.d.ts +90 -0
- package/dist/store/postgres/tenants.js +207 -0
- package/dist/store/types.d.ts +440 -0
- package/dist/store/types.js +10 -0
- package/dist/streams/basin.d.ts +23 -0
- package/dist/streams/basin.js +51 -0
- package/dist/streams/memory.d.ts +25 -0
- package/dist/streams/memory.js +124 -0
- package/dist/streams/relay.d.ts +37 -0
- package/dist/streams/relay.js +254 -0
- package/dist/streams/types.d.ts +115 -0
- package/dist/streams/types.js +86 -0
- package/dist/tenant/actions.d.ts +153 -0
- package/dist/tenant/actions.js +191 -0
- package/dist/tenant/advance.d.ts +15 -0
- package/dist/tenant/advance.js +439 -0
- package/dist/tenant/auth.d.ts +11 -0
- package/dist/tenant/auth.js +34 -0
- package/dist/tenant/commands.d.ts +3 -0
- package/dist/tenant/commands.js +384 -0
- package/dist/tenant/context.d.ts +140 -0
- package/dist/tenant/context.js +40 -0
- package/dist/tenant/effects.d.ts +20 -0
- package/dist/tenant/effects.js +539 -0
- package/dist/tenant/envelope.d.ts +7 -0
- package/dist/tenant/envelope.js +22 -0
- package/dist/tenant/ephemeral.d.ts +42 -0
- package/dist/tenant/ephemeral.js +224 -0
- package/dist/tenant/http.d.ts +32 -0
- package/dist/tenant/http.js +51 -0
- package/dist/tenant/live.d.ts +92 -0
- package/dist/tenant/live.js +290 -0
- package/dist/tenant/logger.d.ts +9 -0
- package/dist/tenant/logger.js +25 -0
- package/dist/tenant/model-setting.d.ts +16 -0
- package/dist/tenant/model-setting.js +12 -0
- package/dist/tenant/module.d.ts +38 -0
- package/dist/tenant/module.js +319 -0
- package/dist/tenant/paths.d.ts +28 -0
- package/dist/tenant/paths.js +91 -0
- package/dist/tenant/pool.d.ts +10 -0
- package/dist/tenant/pool.js +54 -0
- package/dist/tenant/principals.d.ts +13 -0
- package/dist/tenant/principals.js +37 -0
- package/dist/tenant/quarantine-error.d.ts +6 -0
- package/dist/tenant/quarantine-error.js +9 -0
- package/dist/tenant/quarantine.d.ts +27 -0
- package/dist/tenant/quarantine.js +72 -0
- package/dist/tenant/reset.d.ts +22 -0
- package/dist/tenant/reset.js +81 -0
- package/dist/tenant/routes-tenant.d.ts +12 -0
- package/dist/tenant/routes-tenant.js +126 -0
- package/dist/tenant/routes.d.ts +9 -0
- package/dist/tenant/routes.js +142 -0
- package/dist/tenant/runtime.d.ts +81 -0
- package/dist/tenant/runtime.js +298 -0
- package/dist/tenant/scheduler.d.ts +25 -0
- package/dist/tenant/scheduler.js +84 -0
- package/dist/tenant/session.d.ts +60 -0
- package/dist/tenant/session.js +94 -0
- package/dist/tenant/sessions.d.ts +34 -0
- package/dist/tenant/sessions.js +149 -0
- package/dist/tenant/status.d.ts +37 -0
- package/dist/tenant/status.js +130 -0
- package/dist/tenant/store-memory.d.ts +16 -0
- package/dist/tenant/store-memory.js +111 -0
- package/dist/tenant/store-pg.d.ts +15 -0
- package/dist/tenant/store-pg.js +140 -0
- package/dist/tenant/streams.d.ts +126 -0
- package/dist/tenant/streams.js +419 -0
- package/dist/tenant/sweep.d.ts +15 -0
- package/dist/tenant/sweep.js +122 -0
- package/dist/tenant/types.d.ts +205 -0
- package/dist/tenant/types.js +23 -0
- package/dist/tenant/worker.d.ts +84 -0
- package/dist/tenant/worker.js +189 -0
- package/dist/vault/kek.d.ts +5 -2
- package/dist/vault/kek.js +7 -10
- package/dist/vault/service.d.ts +37 -31
- package/dist/vault/service.js +439 -359
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -11
- package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
- package/dist/adapters/sandbox/microsandbox.js +0 -238
- package/dist/core/main.js +0 -35
- package/dist/core/store.d.ts +0 -30
- package/dist/core/store.js +0 -202
- /package/dist/{core/main.d.ts → host/config.js} +0 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Logger } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tenant logger (D12): JSON lines to `logs/tenant.log` with `tenantId` on every record.
|
|
4
|
+
* Never writes to the process stdout/stderr APIs.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createTenantLogger(options: {
|
|
7
|
+
tenantId: string;
|
|
8
|
+
logPath: string;
|
|
9
|
+
}): Logger;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Tenant logger (D12): JSON lines to `logs/tenant.log` with `tenantId` on every record.
|
|
5
|
+
* Never writes to the process stdout/stderr APIs.
|
|
6
|
+
*/
|
|
7
|
+
export function createTenantLogger(options) {
|
|
8
|
+
const { tenantId, logPath } = options;
|
|
9
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
10
|
+
const write = (level, message, fields) => {
|
|
11
|
+
const record = {
|
|
12
|
+
ts: new Date().toISOString(),
|
|
13
|
+
level,
|
|
14
|
+
tenantId,
|
|
15
|
+
message,
|
|
16
|
+
...(fields ?? {}),
|
|
17
|
+
};
|
|
18
|
+
appendFileSync(logPath, `${JSON.stringify(record)}\n`);
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
info: (message, fields) => write("info", message, fields),
|
|
22
|
+
warn: (message, fields) => write("warn", message, fields),
|
|
23
|
+
error: (message, fields) => write("error", message, fields),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tenant-level fixture model: Tenant setting `model.fixture`, seeded with
|
|
3
|
+
* `fixtureModel: true` (`PUT /v1/tenant/config/seed`, Host feature `tenant-fixture-model`).
|
|
4
|
+
* A Tenant with it answers every model call with the Runtime's deterministic fixture model
|
|
5
|
+
* (`core/provider.ts` `toolFixtureModel`) instead of its configured model, whatever the Host
|
|
6
|
+
* uses for other Tenants. Temporary test Tenants seed it (scripts/lib/temporary-tenant.mjs).
|
|
7
|
+
*
|
|
8
|
+
* The setting is read once per advance, in a transaction the advance makes anyway, so a Tenant
|
|
9
|
+
* seeded on another node takes it at its next advance.
|
|
10
|
+
*/
|
|
11
|
+
import type { Tx } from "../store/types.js";
|
|
12
|
+
export declare const FIXTURE_MODEL_SETTING = "model.fixture";
|
|
13
|
+
/** Whether the Tenant uses the fixture model. */
|
|
14
|
+
export declare function usesFixtureModel(t: Tx): Promise<boolean>;
|
|
15
|
+
/** Seeds the setting if absent. Returns whether it was inserted. */
|
|
16
|
+
export declare function seedFixtureModel(t: Tx): Promise<boolean>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const FIXTURE_MODEL_SETTING = "model.fixture";
|
|
2
|
+
/** Whether the Tenant uses the fixture model. */
|
|
3
|
+
export async function usesFixtureModel(t) {
|
|
4
|
+
return (await t.getSetting(FIXTURE_MODEL_SETTING)) === "true";
|
|
5
|
+
}
|
|
6
|
+
/** Seeds the setting if absent. Returns whether it was inserted. */
|
|
7
|
+
export async function seedFixtureModel(t) {
|
|
8
|
+
if ((await t.getSetting(FIXTURE_MODEL_SETTING)) !== undefined)
|
|
9
|
+
return false;
|
|
10
|
+
await t.putSetting(FIXTURE_MODEL_SETTING, "true");
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isQuarantineError, quarantine, TenantBusyError, TenantConflictError } from "./quarantine.js";
|
|
2
|
+
import { TenantNotFoundError, TenantUnavailableError, type Logger, type OpenTenantRuntime, type TenantConfig, type TenantModule, type TenantStore } from "./types.js";
|
|
3
|
+
export interface CreateTenantModuleOptions {
|
|
4
|
+
store: TenantStore;
|
|
5
|
+
/** Unused: the store derives paths. Accepted so callers can pass the store's options. */
|
|
6
|
+
hostRoot?: string;
|
|
7
|
+
/** Unused: the store opens Tenants. Accepted so callers can pass the store's options. */
|
|
8
|
+
openRuntime?: OpenTenantRuntime;
|
|
9
|
+
/** Unused; see `openRuntime`. */
|
|
10
|
+
configFor?: (tenantId: string) => TenantConfig;
|
|
11
|
+
logger: Logger;
|
|
12
|
+
/** Bound on one Tenant open; a slower open quarantines it with `open-timeout`. */
|
|
13
|
+
openTimeoutMs?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Runs after a Tenant is created in the store, before it opens: creates what lives outside
|
|
16
|
+
* it (its Durable Streams basin). A failure is logged; opening the Tenant repairs it.
|
|
17
|
+
*/
|
|
18
|
+
onCreated?: (tenantId: string) => Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Runs after a Tenant is removed from the store: removes what lives outside it (its
|
|
21
|
+
* Durable Streams basin, its armed sweep). A failure is logged; the Tenant stays deleted.
|
|
22
|
+
*/
|
|
23
|
+
onDeleted?: (tenantId: string) => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Deep Tenant module (§8): discovery, create, resolve, list, delete and summarize over an
|
|
27
|
+
* injected `TenantStore`.
|
|
28
|
+
*
|
|
29
|
+
* Handles open on demand and are cached (architecture §8.2). The first call that needs a
|
|
30
|
+
* Tenant (`resolve`, `status`, `worker`, `delete`) opens it once, even when several ask at
|
|
31
|
+
* the same time; a failed open is cached as a quarantine, and a missing Tenant is not
|
|
32
|
+
* cached. A failure outside the Tenant (`TenantUnavailableError`: the database is down, its
|
|
33
|
+
* sweep could not be armed) is thrown to the caller and not cached either, so the next use
|
|
34
|
+
* tries again. A Tenant being deleted resolves as not found, so a late request or Worker
|
|
35
|
+
* call cannot reopen it while its storage is removed.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createTenantModule(options: CreateTenantModuleOptions): TenantModule;
|
|
38
|
+
export { TenantBusyError, TenantConflictError, TenantNotFoundError, TenantUnavailableError, isQuarantineError, quarantine, };
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { isTenantId } from "@nylorun/core/compatibility";
|
|
2
|
+
import { POSTGRES_SCHEMA_VERSION } from "../store/postgres/migrations/index.js";
|
|
3
|
+
import { envelopeNow } from "./envelope.js";
|
|
4
|
+
import { TimeoutError, withTimeout } from "./pool.js";
|
|
5
|
+
import { asQuarantine, isQuarantineError, quarantine, TenantBusyError, TenantConflictError, } from "./quarantine.js";
|
|
6
|
+
import { TenantNotFoundError, TenantUnavailableError, } from "./types.js";
|
|
7
|
+
const OPEN_TIMEOUT_MS = 30_000;
|
|
8
|
+
function hasLiveWork(summary) {
|
|
9
|
+
return (summary.runningSessions > 0 ||
|
|
10
|
+
summary.connectedExecutors > 0 ||
|
|
11
|
+
summary.pendingActions > 0);
|
|
12
|
+
}
|
|
13
|
+
function toAdmin(id, entry, envelope) {
|
|
14
|
+
if (entry?.kind === "open") {
|
|
15
|
+
return {
|
|
16
|
+
id,
|
|
17
|
+
name: entry.handle.envelope.name,
|
|
18
|
+
state: "open",
|
|
19
|
+
envelope: entry.handle.envelope,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
name: envelope?.name ?? null,
|
|
25
|
+
state: entry ? "quarantined" : "open",
|
|
26
|
+
envelope,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** The quarantine an open failure carries, from either quarantine error type. */
|
|
30
|
+
function quarantineOf(error) {
|
|
31
|
+
const carried = error?.quarantine;
|
|
32
|
+
if (carried && typeof carried === "object" && "code" in carried)
|
|
33
|
+
return carried;
|
|
34
|
+
return asQuarantine(error);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Deep Tenant module (§8): discovery, create, resolve, list, delete and summarize over an
|
|
38
|
+
* injected `TenantStore`.
|
|
39
|
+
*
|
|
40
|
+
* Handles open on demand and are cached (architecture §8.2). The first call that needs a
|
|
41
|
+
* Tenant (`resolve`, `status`, `worker`, `delete`) opens it once, even when several ask at
|
|
42
|
+
* the same time; a failed open is cached as a quarantine, and a missing Tenant is not
|
|
43
|
+
* cached. A failure outside the Tenant (`TenantUnavailableError`: the database is down, its
|
|
44
|
+
* sweep could not be armed) is thrown to the caller and not cached either, so the next use
|
|
45
|
+
* tries again. A Tenant being deleted resolves as not found, so a late request or Worker
|
|
46
|
+
* call cannot reopen it while its storage is removed.
|
|
47
|
+
*/
|
|
48
|
+
export function createTenantModule(options) {
|
|
49
|
+
const { store, logger, openTimeoutMs = OPEN_TIMEOUT_MS } = options;
|
|
50
|
+
const entries = new Map();
|
|
51
|
+
const opening = new Map();
|
|
52
|
+
const deleting = new Set();
|
|
53
|
+
let started = false;
|
|
54
|
+
let closed = false;
|
|
55
|
+
function rememberQuarantine(id, q) {
|
|
56
|
+
const entry = { kind: "quarantined", quarantine: q };
|
|
57
|
+
entries.set(id, entry);
|
|
58
|
+
logger.warn("tenant quarantined", {
|
|
59
|
+
tenantId: id,
|
|
60
|
+
code: q.code,
|
|
61
|
+
repair: q.repair,
|
|
62
|
+
});
|
|
63
|
+
return entry;
|
|
64
|
+
}
|
|
65
|
+
async function openEntry(id) {
|
|
66
|
+
let handle;
|
|
67
|
+
try {
|
|
68
|
+
handle = await withTimeout(store.open(id), openTimeoutMs, (late) => {
|
|
69
|
+
void late.then((h) => h.close()).catch(() => undefined);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error instanceof TenantNotFoundError)
|
|
74
|
+
return undefined;
|
|
75
|
+
if (closed || deleting.has(id))
|
|
76
|
+
return undefined;
|
|
77
|
+
if (error instanceof TenantUnavailableError) {
|
|
78
|
+
// Not the Tenant's fault: not cached, so the next use opens it again.
|
|
79
|
+
logger.warn("tenant unavailable", {
|
|
80
|
+
tenantId: id,
|
|
81
|
+
message: error.cause instanceof Error ? error.cause.message : String(error.cause),
|
|
82
|
+
});
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
if (error instanceof TimeoutError)
|
|
86
|
+
return rememberQuarantine(id, quarantine("open-timeout", `open timed out after ${openTimeoutMs}ms`, {
|
|
87
|
+
tenantId: id,
|
|
88
|
+
}).toQuarantine());
|
|
89
|
+
return rememberQuarantine(id, quarantineOf(error) ??
|
|
90
|
+
quarantine("open-failed", error instanceof Error ? error.message : "open failed", { tenantId: id }).toQuarantine());
|
|
91
|
+
}
|
|
92
|
+
if (closed || deleting.has(id)) {
|
|
93
|
+
await handle.close().catch(() => undefined);
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const entry = { kind: "open", handle };
|
|
97
|
+
entries.set(id, entry);
|
|
98
|
+
return entry;
|
|
99
|
+
}
|
|
100
|
+
/** The cached entry, or the result of opening the Tenant once. */
|
|
101
|
+
async function load(id) {
|
|
102
|
+
if (deleting.has(id))
|
|
103
|
+
return undefined;
|
|
104
|
+
const cached = entries.get(id);
|
|
105
|
+
if (cached)
|
|
106
|
+
return cached;
|
|
107
|
+
if (closed)
|
|
108
|
+
return undefined;
|
|
109
|
+
let pending = opening.get(id);
|
|
110
|
+
if (!pending) {
|
|
111
|
+
pending = openEntry(id).finally(() => opening.delete(id));
|
|
112
|
+
opening.set(id, pending);
|
|
113
|
+
}
|
|
114
|
+
return pending;
|
|
115
|
+
}
|
|
116
|
+
const openEntries = () => [...entries.values()].filter((e) => e.kind === "open");
|
|
117
|
+
const module = {
|
|
118
|
+
get started() {
|
|
119
|
+
return started;
|
|
120
|
+
},
|
|
121
|
+
async start() {
|
|
122
|
+
if (closed)
|
|
123
|
+
throw new Error("Tenant module is closed");
|
|
124
|
+
const ids = await store.enumerate();
|
|
125
|
+
started = true;
|
|
126
|
+
logger.info("tenant module started", { tenants: ids.length });
|
|
127
|
+
},
|
|
128
|
+
async resolve(id) {
|
|
129
|
+
if (!isTenantId(id))
|
|
130
|
+
return { kind: "not-found" };
|
|
131
|
+
const entry = await load(id);
|
|
132
|
+
if (!entry)
|
|
133
|
+
return { kind: "not-found" };
|
|
134
|
+
if (entry.kind === "open")
|
|
135
|
+
return { kind: "open", handle: entry.handle };
|
|
136
|
+
return { kind: "quarantined", quarantine: entry.quarantine };
|
|
137
|
+
},
|
|
138
|
+
async worker(id) {
|
|
139
|
+
if (!isTenantId(id))
|
|
140
|
+
return undefined;
|
|
141
|
+
const entry = await load(id);
|
|
142
|
+
return entry?.kind === "open" ? entry.handle.worker : undefined;
|
|
143
|
+
},
|
|
144
|
+
async create(input) {
|
|
145
|
+
if (!isTenantId(input.tenantId)) {
|
|
146
|
+
throw new Error(`Invalid tenant id: ${input.tenantId}`);
|
|
147
|
+
}
|
|
148
|
+
const bootstrap = {
|
|
149
|
+
principalId: input.principalId,
|
|
150
|
+
credentialHash: input.credentialHash,
|
|
151
|
+
idempotencyKey: input.idempotencyKey,
|
|
152
|
+
...(input.studioCredentialHash
|
|
153
|
+
? { studioCredentialHash: input.studioCredentialHash }
|
|
154
|
+
: {}),
|
|
155
|
+
};
|
|
156
|
+
// The store records its own schema version.
|
|
157
|
+
const envelope = envelopeNow({
|
|
158
|
+
id: input.tenantId,
|
|
159
|
+
name: input.name,
|
|
160
|
+
schemaVersion: POSTGRES_SCHEMA_VERSION,
|
|
161
|
+
});
|
|
162
|
+
let outcome;
|
|
163
|
+
try {
|
|
164
|
+
outcome = await store.create(envelope, bootstrap);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
await store.removePartial(input.tenantId).catch(() => undefined);
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
if (outcome === "exists") {
|
|
171
|
+
const matches = await store.bootstrapMatches(input.tenantId, bootstrap);
|
|
172
|
+
if (!matches)
|
|
173
|
+
throw new TenantConflictError();
|
|
174
|
+
const existing = await store.readEnvelope(input.tenantId);
|
|
175
|
+
await load(input.tenantId).catch(() => undefined);
|
|
176
|
+
return { envelope: existing, created: false };
|
|
177
|
+
}
|
|
178
|
+
await options.onCreated?.(input.tenantId).catch((error) => logger.warn("tenant create step failed; open repairs it", {
|
|
179
|
+
tenantId: input.tenantId,
|
|
180
|
+
message: error instanceof Error ? error.message : String(error),
|
|
181
|
+
}));
|
|
182
|
+
const opened = await load(input.tenantId).catch(() => undefined);
|
|
183
|
+
if (opened?.kind === "open") {
|
|
184
|
+
return { envelope: opened.handle.envelope, created: true };
|
|
185
|
+
}
|
|
186
|
+
// Created but failed to open: report the stored envelope.
|
|
187
|
+
return {
|
|
188
|
+
envelope: await store
|
|
189
|
+
.readEnvelope(input.tenantId)
|
|
190
|
+
.catch(() => envelope),
|
|
191
|
+
created: true,
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
async list() {
|
|
195
|
+
const ids = new Set(await store.enumerate());
|
|
196
|
+
for (const id of entries.keys())
|
|
197
|
+
ids.add(id);
|
|
198
|
+
const result = [];
|
|
199
|
+
for (const id of [...ids].sort()) {
|
|
200
|
+
if (!isTenantId(id) || deleting.has(id))
|
|
201
|
+
continue;
|
|
202
|
+
let entry = entries.get(id);
|
|
203
|
+
if (entry?.kind === "open") {
|
|
204
|
+
result.push(toAdmin(id, entry, null));
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
let envelope = null;
|
|
208
|
+
try {
|
|
209
|
+
envelope = await store.readEnvelope(id);
|
|
210
|
+
if (envelope.id !== id) {
|
|
211
|
+
entry = {
|
|
212
|
+
kind: "quarantined",
|
|
213
|
+
quarantine: quarantine("envelope-invalid", `Tenant envelope id ${envelope.id} does not match ${id}`, { tenantId: id }).toQuarantine(),
|
|
214
|
+
};
|
|
215
|
+
envelope = null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
// Deleted since it was enumerated.
|
|
220
|
+
if (error instanceof TenantNotFoundError)
|
|
221
|
+
continue;
|
|
222
|
+
if (error instanceof TenantUnavailableError)
|
|
223
|
+
throw error;
|
|
224
|
+
// Listed as quarantined; only opening it caches a quarantine.
|
|
225
|
+
entry ??= {
|
|
226
|
+
kind: "quarantined",
|
|
227
|
+
quarantine: quarantineOf(error) ??
|
|
228
|
+
quarantine("envelope-invalid", "Tenant envelope is unreadable", {
|
|
229
|
+
tenantId: id,
|
|
230
|
+
}).toQuarantine(),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
result.push(toAdmin(id, entry, envelope));
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
},
|
|
237
|
+
async status(id) {
|
|
238
|
+
if (!isTenantId(id))
|
|
239
|
+
return undefined;
|
|
240
|
+
const entry = await load(id);
|
|
241
|
+
if (!entry)
|
|
242
|
+
return undefined;
|
|
243
|
+
if (entry.kind === "open")
|
|
244
|
+
return toAdmin(id, entry, null);
|
|
245
|
+
const envelope = await store.readEnvelope(id).catch(() => null);
|
|
246
|
+
return { ...toAdmin(id, entry, envelope), quarantine: entry.quarantine };
|
|
247
|
+
},
|
|
248
|
+
async delete(id, activeWork) {
|
|
249
|
+
if (!isTenantId(id))
|
|
250
|
+
throw new Error(`Invalid tenant id: ${id}`);
|
|
251
|
+
if (deleting.has(id))
|
|
252
|
+
throw new TenantBusyError("Tenant is being deleted");
|
|
253
|
+
const entry = await load(id);
|
|
254
|
+
if (!entry)
|
|
255
|
+
throw new TenantNotFoundError(id);
|
|
256
|
+
if (entry.kind === "open") {
|
|
257
|
+
const summary = await entry.handle.summary();
|
|
258
|
+
if (activeWork === "refuse" && hasLiveWork(summary)) {
|
|
259
|
+
throw new TenantBusyError();
|
|
260
|
+
}
|
|
261
|
+
if (activeWork === "drain" || activeWork === "cancel") {
|
|
262
|
+
await entry.handle.drain(activeWork);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
deleting.add(id);
|
|
266
|
+
try {
|
|
267
|
+
entries.delete(id);
|
|
268
|
+
// Sandbox prefix cleanup is owned by TenantHandle.close().
|
|
269
|
+
if (entry.kind === "open")
|
|
270
|
+
await entry.handle.close();
|
|
271
|
+
await store.trash(id, new Date());
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
deleting.delete(id);
|
|
275
|
+
}
|
|
276
|
+
logger.info("tenant deleted", { tenantId: id, activeWork });
|
|
277
|
+
await options.onDeleted?.(id).catch((error) => logger.warn("tenant delete cleanup failed", {
|
|
278
|
+
tenantId: id,
|
|
279
|
+
message: error instanceof Error ? error.message : String(error),
|
|
280
|
+
}));
|
|
281
|
+
},
|
|
282
|
+
async summarize() {
|
|
283
|
+
let runningSessions = 0;
|
|
284
|
+
let connectedExecutors = 0;
|
|
285
|
+
let pendingActions = 0;
|
|
286
|
+
let uncertainEffects = 0;
|
|
287
|
+
let outboxDepth;
|
|
288
|
+
let relayLagMs;
|
|
289
|
+
for (const s of await Promise.all(openEntries().map((e) => e.handle.summary()))) {
|
|
290
|
+
runningSessions += s.runningSessions;
|
|
291
|
+
connectedExecutors += s.connectedExecutors;
|
|
292
|
+
pendingActions += s.pendingActions;
|
|
293
|
+
uncertainEffects += s.uncertainEffects;
|
|
294
|
+
if (s.outboxDepth !== undefined)
|
|
295
|
+
outboxDepth = (outboxDepth ?? 0) + s.outboxDepth;
|
|
296
|
+
if (s.relayLagMs !== undefined)
|
|
297
|
+
relayLagMs = Math.max(relayLagMs ?? 0, s.relayLagMs);
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
runningSessions,
|
|
301
|
+
connectedExecutors,
|
|
302
|
+
pendingActions,
|
|
303
|
+
uncertainEffects,
|
|
304
|
+
...(outboxDepth !== undefined ? { outboxDepth } : {}),
|
|
305
|
+
...(relayLagMs !== undefined ? { relayLagMs } : {}),
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
async close() {
|
|
309
|
+
closed = true;
|
|
310
|
+
started = false;
|
|
311
|
+
await Promise.allSettled([...opening.values()]);
|
|
312
|
+
const open = openEntries();
|
|
313
|
+
entries.clear();
|
|
314
|
+
await Promise.all(open.map((e) => e.handle.close().catch(() => undefined)));
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
return module;
|
|
318
|
+
}
|
|
319
|
+
export { TenantBusyError, TenantConflictError, TenantNotFoundError, TenantUnavailableError, isQuarantineError, quarantine, };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Logger, TenantPaths } from "./types.js";
|
|
2
|
+
export interface HostPaths {
|
|
3
|
+
root: string;
|
|
4
|
+
config: string;
|
|
5
|
+
credentials: string;
|
|
6
|
+
home: string;
|
|
7
|
+
tmp: string;
|
|
8
|
+
tenants: string;
|
|
9
|
+
trash: string;
|
|
10
|
+
}
|
|
11
|
+
/** Host layout under NYLORUN_HOME / ~/.nylorun. */
|
|
12
|
+
export declare function hostPaths(hostRoot: string): HostPaths;
|
|
13
|
+
/**
|
|
14
|
+
* Absolute Tenant directory paths under `<hostRoot>/tenants/<tenantId>/`.
|
|
15
|
+
* Validates the id and asserts containment after realpath.
|
|
16
|
+
*
|
|
17
|
+
* The directory holds what stays on the Host (the Tenant's data is its Postgres schema):
|
|
18
|
+
* `vault-kek`, `home/`, `tmp/`, `sandboxes/`, `plugin-data/`, `logs/tenant.log`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function tenantPaths(hostRoot: string, tenantId: string): TenantPaths;
|
|
21
|
+
/**
|
|
22
|
+
* Moves every Tenant directory that holds a `tenant.sqlite` to `trash/`, logging each.
|
|
23
|
+
*
|
|
24
|
+
* Those are beta Tenants from the SQLite Runtime. They are not migrated (recreate them); a
|
|
25
|
+
* Postgres Tenant's directory never holds that file. Moving them at Host start keeps a new
|
|
26
|
+
* Postgres Tenant with the same id from sharing the old directory. Returns the moved ids.
|
|
27
|
+
*/
|
|
28
|
+
export declare function trashSqliteTenants(hostRoot: string, logger: Logger): string[];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, realpathSync, renameSync, } from "node:fs";
|
|
2
|
+
import { join, resolve, sep } from "node:path";
|
|
3
|
+
import { isTenantId } from "@nylorun/core/compatibility";
|
|
4
|
+
function ensureDir(path) {
|
|
5
|
+
mkdirSync(path, { recursive: true });
|
|
6
|
+
return realpathSync(path);
|
|
7
|
+
}
|
|
8
|
+
function assertContained(parent, child) {
|
|
9
|
+
const root = parent.endsWith(sep) ? parent : parent + sep;
|
|
10
|
+
if (child !== parent && !child.startsWith(root)) {
|
|
11
|
+
throw new Error(`Path ${child} escapes host tenants root ${parent}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Host layout under NYLORUN_HOME / ~/.nylorun. */
|
|
15
|
+
export function hostPaths(hostRoot) {
|
|
16
|
+
const root = resolve(hostRoot);
|
|
17
|
+
return {
|
|
18
|
+
root,
|
|
19
|
+
config: join(root, "host.json"),
|
|
20
|
+
credentials: join(root, "host-credentials.json"),
|
|
21
|
+
home: join(root, "home"),
|
|
22
|
+
tmp: join(root, "tmp"),
|
|
23
|
+
tenants: join(root, "tenants"),
|
|
24
|
+
trash: join(root, "trash"),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Absolute Tenant directory paths under `<hostRoot>/tenants/<tenantId>/`.
|
|
29
|
+
* Validates the id and asserts containment after realpath.
|
|
30
|
+
*
|
|
31
|
+
* The directory holds what stays on the Host (the Tenant's data is its Postgres schema):
|
|
32
|
+
* `vault-kek`, `home/`, `tmp/`, `sandboxes/`, `plugin-data/`, `logs/tenant.log`.
|
|
33
|
+
*/
|
|
34
|
+
export function tenantPaths(hostRoot, tenantId) {
|
|
35
|
+
if (!isTenantId(tenantId)) {
|
|
36
|
+
throw new Error(`Invalid tenant id: ${String(tenantId)}`);
|
|
37
|
+
}
|
|
38
|
+
const host = hostPaths(hostRoot);
|
|
39
|
+
const tenantsRoot = ensureDir(host.tenants);
|
|
40
|
+
const root = join(tenantsRoot, tenantId);
|
|
41
|
+
// Containment before the Tenant directory exists: resolve under real tenants root.
|
|
42
|
+
const resolvedRoot = resolve(tenantsRoot, tenantId);
|
|
43
|
+
assertContained(tenantsRoot, resolvedRoot);
|
|
44
|
+
if (existsSync(root)) {
|
|
45
|
+
assertContained(tenantsRoot, realpathSync(root));
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
root: resolvedRoot,
|
|
49
|
+
kek: join(resolvedRoot, "vault-kek"),
|
|
50
|
+
home: join(resolvedRoot, "home"),
|
|
51
|
+
tmp: join(resolvedRoot, "tmp"),
|
|
52
|
+
sandboxes: join(resolvedRoot, "sandboxes"),
|
|
53
|
+
pluginData: join(resolvedRoot, "plugin-data"),
|
|
54
|
+
logs: join(resolvedRoot, "logs"),
|
|
55
|
+
log: join(resolvedRoot, "logs", "tenant.log"),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** The database file of a Tenant from before the Postgres Session Store. */
|
|
59
|
+
const SQLITE_DATABASE = "tenant.sqlite";
|
|
60
|
+
/**
|
|
61
|
+
* Moves every Tenant directory that holds a `tenant.sqlite` to `trash/`, logging each.
|
|
62
|
+
*
|
|
63
|
+
* Those are beta Tenants from the SQLite Runtime. They are not migrated (recreate them); a
|
|
64
|
+
* Postgres Tenant's directory never holds that file. Moving them at Host start keeps a new
|
|
65
|
+
* Postgres Tenant with the same id from sharing the old directory. Returns the moved ids.
|
|
66
|
+
*/
|
|
67
|
+
export function trashSqliteTenants(hostRoot, logger) {
|
|
68
|
+
const host = hostPaths(hostRoot);
|
|
69
|
+
if (!existsSync(host.tenants))
|
|
70
|
+
return [];
|
|
71
|
+
const moved = [];
|
|
72
|
+
const stamp = new Date().toISOString().replaceAll(":", "-");
|
|
73
|
+
for (const entry of readdirSync(host.tenants, { withFileTypes: true })) {
|
|
74
|
+
if (!entry.isDirectory())
|
|
75
|
+
continue;
|
|
76
|
+
const directory = join(host.tenants, entry.name);
|
|
77
|
+
if (!existsSync(join(directory, SQLITE_DATABASE)))
|
|
78
|
+
continue;
|
|
79
|
+
mkdirSync(host.trash, { recursive: true });
|
|
80
|
+
const destination = join(host.trash, `${entry.name}-sqlite-${stamp}`);
|
|
81
|
+
renameSync(directory, destination);
|
|
82
|
+
moved.push(entry.name);
|
|
83
|
+
logger.warn("sqlite_tenant_moved_to_trash", {
|
|
84
|
+
tenant: entry.name,
|
|
85
|
+
from: directory,
|
|
86
|
+
to: destination,
|
|
87
|
+
reason: "SQLite Tenants are not migrated to the Postgres Session Store; recreate the Tenant",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return moved;
|
|
91
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Run async work over items with a fixed concurrency limit. */
|
|
2
|
+
export declare function mapPool<T, R>(items: readonly T[], concurrency: number, worker: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
3
|
+
export declare class TimeoutError extends Error {
|
|
4
|
+
constructor(message?: string);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Race a promise against a timeout. On timeout the late result is handed to
|
|
8
|
+
* `onLate` (Risk R6: mark quarantined and close a late handle).
|
|
9
|
+
*/
|
|
10
|
+
export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, onLate?: (late: Promise<T>) => void): Promise<T>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Run async work over items with a fixed concurrency limit. */
|
|
2
|
+
export async function mapPool(items, concurrency, worker) {
|
|
3
|
+
if (items.length === 0)
|
|
4
|
+
return [];
|
|
5
|
+
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
6
|
+
const results = new Array(items.length);
|
|
7
|
+
let next = 0;
|
|
8
|
+
async function run() {
|
|
9
|
+
for (;;) {
|
|
10
|
+
const index = next++;
|
|
11
|
+
if (index >= items.length)
|
|
12
|
+
return;
|
|
13
|
+
results[index] = await worker(items[index], index);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
await Promise.all(Array.from({ length: limit }, () => run()));
|
|
17
|
+
return results;
|
|
18
|
+
}
|
|
19
|
+
export class TimeoutError extends Error {
|
|
20
|
+
constructor(message = "operation timed out") {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "TimeoutError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Race a promise against a timeout. On timeout the late result is handed to
|
|
27
|
+
* `onLate` (Risk R6: mark quarantined and close a late handle).
|
|
28
|
+
*/
|
|
29
|
+
export async function withTimeout(promise, timeoutMs, onLate) {
|
|
30
|
+
let timer;
|
|
31
|
+
let timedOut = false;
|
|
32
|
+
const timeout = new Promise((_, reject) => {
|
|
33
|
+
timer = setTimeout(() => {
|
|
34
|
+
timedOut = true;
|
|
35
|
+
reject(new TimeoutError(`timed out after ${timeoutMs}ms`));
|
|
36
|
+
}, timeoutMs);
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
const result = await Promise.race([promise, timeout]);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (timedOut && onLate) {
|
|
44
|
+
onLate(promise.then((value) => value, (lateError) => {
|
|
45
|
+
throw lateError;
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
if (timer !== undefined)
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Tx } from "../store/types.js";
|
|
2
|
+
import type { BootstrapPrincipal } from "./types.js";
|
|
3
|
+
export type { PrincipalRow } from "../store/types.js";
|
|
4
|
+
/** Principal id of the Studio key derived from the admin key. */
|
|
5
|
+
export declare const STUDIO_PRINCIPAL_ID = "studio";
|
|
6
|
+
/**
|
|
7
|
+
* Writes the application principal and idempotency key in the caller's transaction, plus
|
|
8
|
+
* principal `studio` when the bootstrap carries a Studio credential hash.
|
|
9
|
+
* Exported for WS-B's `TenantStore.create` (A3).
|
|
10
|
+
*/
|
|
11
|
+
export declare function bootstrapPrincipal(t: Tx, bootstrap: BootstrapPrincipal, now?: Date): Promise<void>;
|
|
12
|
+
/** Whether the stored bootstrap (and Studio) principals equal `bootstrap`. */
|
|
13
|
+
export declare function bootstrapPrincipalMatches(t: Tx, bootstrap: BootstrapPrincipal): Promise<boolean>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Principal id of the Studio key derived from the admin key. */
|
|
2
|
+
export const STUDIO_PRINCIPAL_ID = "studio";
|
|
3
|
+
/**
|
|
4
|
+
* Writes the application principal and idempotency key in the caller's transaction, plus
|
|
5
|
+
* principal `studio` when the bootstrap carries a Studio credential hash.
|
|
6
|
+
* Exported for WS-B's `TenantStore.create` (A3).
|
|
7
|
+
*/
|
|
8
|
+
export async function bootstrapPrincipal(t, bootstrap, now = new Date()) {
|
|
9
|
+
const createdAt = now.toISOString();
|
|
10
|
+
await t.insertPrincipal({
|
|
11
|
+
id: bootstrap.principalId,
|
|
12
|
+
role: "application",
|
|
13
|
+
tokenHash: bootstrap.credentialHash,
|
|
14
|
+
idempotencyKey: bootstrap.idempotencyKey,
|
|
15
|
+
createdAt,
|
|
16
|
+
});
|
|
17
|
+
if (bootstrap.studioCredentialHash)
|
|
18
|
+
await t.insertPrincipal({
|
|
19
|
+
id: STUDIO_PRINCIPAL_ID,
|
|
20
|
+
role: "application",
|
|
21
|
+
tokenHash: bootstrap.studioCredentialHash,
|
|
22
|
+
idempotencyKey: null,
|
|
23
|
+
createdAt,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Whether the stored bootstrap (and Studio) principals equal `bootstrap`. */
|
|
27
|
+
export async function bootstrapPrincipalMatches(t, bootstrap) {
|
|
28
|
+
const row = await t.principalById(bootstrap.principalId);
|
|
29
|
+
if (!row ||
|
|
30
|
+
row.role !== "application" ||
|
|
31
|
+
row.tokenHash !== bootstrap.credentialHash ||
|
|
32
|
+
row.idempotencyKey !== bootstrap.idempotencyKey)
|
|
33
|
+
return false;
|
|
34
|
+
const studio = await t.principalById(STUDIO_PRINCIPAL_ID);
|
|
35
|
+
return ((studio?.role === "application" ? studio.tokenHash : undefined) ===
|
|
36
|
+
bootstrap.studioCredentialHash);
|
|
37
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Quarantine } from "./types.js";
|
|
2
|
+
/** Typed quarantine raised while opening a Tenant Runtime (A7). */
|
|
3
|
+
export declare class QuarantineError extends Error {
|
|
4
|
+
readonly quarantine: Quarantine;
|
|
5
|
+
constructor(code: Quarantine["code"], message: string, repair?: string);
|
|
6
|
+
}
|