@happyvertical/smrt-app-runtime 0.55.22 → 0.55.24
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 +19 -1
- package/README.md +46 -5
- package/dist/chunks/{src-vP6GV99v.js → src-DeNO6bG_.js} +217 -19
- package/dist/chunks/src-DeNO6bG_.js.map +1 -0
- package/dist/index.d.ts +33 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/dist/sveltekit/owner-setup.d.ts +4 -2
- package/dist/sveltekit/owner-setup.d.ts.map +1 -1
- package/dist/sveltekit/runtime.d.ts +7 -1
- package/dist/sveltekit/runtime.d.ts.map +1 -1
- package/dist/sveltekit.js +6 -3
- package/dist/sveltekit.js.map +1 -1
- package/package.json +9 -9
- package/dist/chunks/src-vP6GV99v.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -13,6 +13,20 @@ Application infrastructure composition for the validated runtime profiles in
|
|
|
13
13
|
- SQLite is acquired through `@happyvertical/sql`'s explicit
|
|
14
14
|
`{ driver: 'node:sqlite', custody: 'trusted-parent' }` boundary after the
|
|
15
15
|
runtime establishes its user-owned mode-0700 data root.
|
|
16
|
+
- The initialization lease lives beside the data root
|
|
17
|
+
(`<parent>/.smrt-lease-<key>/initialization.sqlite`, #3877), then the legacy
|
|
18
|
+
`/tmp/.smrt-<uid>/<key>` lease, which older releases take alone: secure when
|
|
19
|
+
its ancestry is trusted, else lock-only (owner check on unmapped ancestors
|
|
20
|
+
relaxed, nothing written; skipped only if its directory cannot be created
|
|
21
|
+
and no lock file exists). It is required (secure) when the parent cannot hold
|
|
22
|
+
a lease. Legacy candidates, deduplicated and in this order: the in-namespace
|
|
23
|
+
uid, then that uid mapped through `/proc/self/uid_map` (identity when absent;
|
|
24
|
+
unparseable/non-covering fails closed); ownership checks still use the
|
|
25
|
+
in-namespace uid. The beside-data lease taken first keeps that order
|
|
26
|
+
deadlock-free. Only the in-namespace candidate (when it differs) is skipped
|
|
27
|
+
if its root is an existing real directory owned by another uid. Acquire in that order; never make the
|
|
28
|
+
lease location configurable per process — every process sharing a data root
|
|
29
|
+
must derive the same lease.
|
|
16
30
|
- Application migrations are explicit through `prepareDatabase`; runtime never
|
|
17
31
|
creates application model tables implicitly. A rejected `prepareDatabase` is
|
|
18
32
|
normalized into `LocalRuntimeError` with the stable `migration_failed` code
|
|
@@ -28,7 +42,11 @@ Application infrastructure composition for the validated runtime profiles in
|
|
|
28
42
|
app-bound marker, source separation, real path chain, ownership, and mode
|
|
29
43
|
without creating or repairing storage.
|
|
30
44
|
- Owner bootstrap creates normal `Person`, `User`, `Tenant`, owner `Role` /
|
|
31
|
-
`Membership`, and `Session` records in one transaction.
|
|
45
|
+
`Membership`, and `Session` records in one transaction. `ownerTenant`
|
|
46
|
+
selects the tenant slug (default `default`); because `Tenant` upserts on
|
|
47
|
+
slug, the claim must look the slug up first and fail closed
|
|
48
|
+
(`owner_tenant_conflict`) rather than save over it. `adoptExisting` adopts
|
|
49
|
+
only an active root tenant with zero memberships (#3032).
|
|
32
50
|
- Bootstrap is loopback-only. Only an HMAC of the short-lived token is stored.
|
|
33
51
|
A token that cannot claim fails a read-only preflight before role/catalog
|
|
34
52
|
seeding (#3323); the claim transaction's conditional UPDATE stays the
|
package/README.md
CHANGED
|
@@ -55,9 +55,42 @@ complete ancestor/root chain, including macOS ACLs. Failed custody removes the
|
|
|
55
55
|
pending claim and every directory only when they were created by that attempt;
|
|
56
56
|
an inherited pending claim and its database remain authoritative for retry.
|
|
57
57
|
Initializers for the same data root are serialized across processes by an
|
|
58
|
-
exclusive transaction in a dedicated SQLite lock database under a private
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
exclusive transaction in a dedicated SQLite lock database under a private,
|
|
59
|
+
root-keyed custody directory kept beside the data it protects:
|
|
60
|
+
`<parent of the data root>/.smrt-lease-<key>/initialization.sqlite`. That
|
|
61
|
+
parent has already passed the custody preflight and is the one location every
|
|
62
|
+
process sharing the data directory sees, so the lease also works inside a user
|
|
63
|
+
namespace (agent sandboxes, rootless containers) where `/tmp` reads as owned by
|
|
64
|
+
the overflow uid. The historical per-user `/tmp/.smrt-<uid>/<key>` lease is
|
|
65
|
+
still taken afterwards, so a process running an older release (which takes
|
|
66
|
+
only that lease) stays excluded. When `/tmp` fails the ancestry owner check
|
|
67
|
+
because its owner is unmapped, the legacy lease is taken lock-only: the lock
|
|
68
|
+
file is never written, every ancestor must still be a real directory that is
|
|
69
|
+
not group/world writable unless sticky, and the lock directories and file must
|
|
70
|
+
be current-user-owned and private. A held legacy lease therefore makes the new
|
|
71
|
+
process wait (and fail closed after two minutes). Inside a user namespace that
|
|
72
|
+
remaps the uid (for example `unshare -Ur`, where host uid 1000 reads as 0), the
|
|
73
|
+
legacy lease is taken for both the in-namespace uid (`/tmp/.smrt-0`, what an
|
|
74
|
+
older release inside the namespace locks) and, in that order, the uid's mapping
|
|
75
|
+
in `/proc/self/uid_map` (`/tmp/.smrt-<host uid>`, what an older host process
|
|
76
|
+
locks); identical paths are taken once, so a host process takes the single
|
|
77
|
+
lease it always did. Every new process takes the beside-data lease first, which
|
|
78
|
+
is what keeps the fixed legacy order deadlock-free. When the in-namespace uid
|
|
79
|
+
differs from its mapping and its `/tmp/.smrt-<uid>` root already exists as a
|
|
80
|
+
real directory owned by someone else (under `unshare -Ur`, host root's
|
|
81
|
+
`/tmp/.smrt-0` reads as the overflow uid), that one candidate is skipped: no
|
|
82
|
+
process in the namespace can hold a lease beneath a root it does not own. A
|
|
83
|
+
symlink there, and any problem with the mapped candidate, still fails closed.
|
|
84
|
+
A map that
|
|
85
|
+
cannot be read, parsed, or does not cover the current uid fails closed. Only
|
|
86
|
+
one namespace level is visible, so a doubly nested remap is not translated
|
|
87
|
+
past its parent. The residual rule: when the
|
|
88
|
+
legacy lock directory cannot be created at all (`/tmp` not writable or
|
|
89
|
+
read-only in that view) and no legacy lock file exists, the legacy lease is
|
|
90
|
+
skipped. When the data root's parent is not writable by the current user (or is
|
|
91
|
+
read-only) the secure `/tmp` lease is required instead, as before. The
|
|
92
|
+
released `@happyvertical/sql`
|
|
93
|
+
trusted-parent boundary validates each lease directory, its ancestors, and the lock
|
|
61
94
|
leaf for ownership, write permissions, static links, and macOS ACLs before it
|
|
62
95
|
opens the lock database. Atomic SQLite locking elects one owner without deleting
|
|
63
96
|
or replacing a pathname, so there is no stale-file ABA window and no PID or
|
|
@@ -97,6 +130,14 @@ literals (`127.0.0.0/8` or `::1`), avoiding hostname-resolution ambiguity.
|
|
|
97
130
|
The first valid claim creates a real global `Person`, `User`, default `Tenant`,
|
|
98
131
|
owner `Role` / `Membership`, and server-side `Session` atomically. Startup and
|
|
99
132
|
setup are idempotent; replayed, expired, or concurrent claims fail closed.
|
|
133
|
+
`ownerTenant: { slug, name, adoptExisting }` (also accepted by the SvelteKit
|
|
134
|
+
runtime) selects the bootstrap tenant; the default is a new tenant with slug
|
|
135
|
+
`default`. Because `Tenant` upserts on its slug, the claim checks first and
|
|
136
|
+
never replaces an existing tenant: it fails with `owner_tenant_conflict`, or,
|
|
137
|
+
with `adoptExisting: true`, adds the owner membership to an application-seeded
|
|
138
|
+
tenant only when that tenant is active, has no parent, and has no memberships of
|
|
139
|
+
any status. An adopted tenant keeps its id and name. A failed claim rolls back
|
|
140
|
+
and leaves the invitation claimable.
|
|
100
141
|
Authenticated session TTLs are configured in whole seconds with a minimum of
|
|
101
142
|
one second, and invalid values are rejected before filesystem mutation.
|
|
102
143
|
|
|
@@ -194,8 +235,8 @@ re-checks on every request that both the peer address and the URL host are
|
|
|
194
235
|
loopback; its `default` action reads `token`, `name`, `email`, and optional
|
|
195
236
|
`tenantName`, sets the session cookie, and redirects 303, or returns
|
|
196
237
|
`fail(status, { code, message })` with `setup_disabled` (404),
|
|
197
|
-
`setup_unavailable` (403), `setup_invalid_input` (400),
|
|
198
|
-
(400). Claim error text is never returned. After the claim commits, the
|
|
238
|
+
`setup_unavailable` (403), `setup_invalid_input` (400), `setup_invalid`
|
|
239
|
+
(400), or `setup_conflict` (409, the `ownerTenant` slug already exists). Claim error text is never returned. After the claim commits, the
|
|
199
240
|
runtime removes the `smrt app setup` / `recover` hand-off files
|
|
200
241
|
(`ONBOARDING_HANDOFF_FILES`: `onboarding.json`, `onboarding-launch.html`) from
|
|
201
242
|
`runtime.applicationStateRoot()`, so `pnpm app:open` stops offering the spent
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
3
|
-
import { constants, link, lstat, mkdir, open, readdir, realpath, rmdir, unlink } from "node:fs/promises";
|
|
3
|
+
import { constants, link, lstat, mkdir, open, readFile, readdir, realpath, rmdir, unlink } from "node:fs/promises";
|
|
4
4
|
import { homedir, platform } from "node:os";
|
|
5
5
|
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
@@ -9,7 +9,7 @@ import { resolveApplicationRuntime } from "@happyvertical/smrt-config";
|
|
|
9
9
|
import { SCHEDULED_JOB_QUEUE, ScheduleRunner, TaskRunner } from "@happyvertical/smrt-jobs";
|
|
10
10
|
import { Person, ProfileTypeCollection } from "@happyvertical/smrt-profiles";
|
|
11
11
|
import { withSystemContext } from "@happyvertical/smrt-tenancy";
|
|
12
|
-
import { DEFAULT_ROLE_SLUGS, MembershipCollection, MembershipStatus, PermissionResolver, RoleCollection, SessionCollection, SessionService, TenantService, UserCollection, UserStatus } from "@happyvertical/smrt-users";
|
|
12
|
+
import { DEFAULT_ROLE_SLUGS, MembershipCollection, MembershipStatus, PermissionResolver, RoleCollection, SessionCollection, SessionService, TenantCollection, TenantService, TenantStatus, UserCollection, UserStatus } from "@happyvertical/smrt-users";
|
|
13
13
|
import { getDatabase } from "@happyvertical/sql";
|
|
14
14
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
15
15
|
import { chmodSync, closeSync, constants as constants$1, existsSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -1513,10 +1513,15 @@ var DEFAULT_BIND_HOST = "127.0.0.1";
|
|
|
1513
1513
|
var DEFAULT_BOOTSTRAP_TTL_SECONDS = 600;
|
|
1514
1514
|
var MAX_BOOTSTRAP_TTL_SECONDS = 900;
|
|
1515
1515
|
var DEFAULT_SESSION_TTL_SECONDS = 10080 * 60;
|
|
1516
|
+
var DEFAULT_OWNER_TENANT_SLUG = "default";
|
|
1517
|
+
var DEFAULT_OWNER_TENANT_NAME = "My Workspace";
|
|
1518
|
+
var OWNER_TENANT_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
1516
1519
|
var BOOTSTRAP_TABLE = "_smrt_local_owner_bootstrap";
|
|
1517
1520
|
var SECRET_FILE_NAME = "application.secret";
|
|
1518
1521
|
var DATABASE_FILE_NAME = "application.sqlite";
|
|
1519
1522
|
var ROOT_MARKER_PREFIX = ".smrt-local-runtime-";
|
|
1523
|
+
var LEASE_DIRECTORY_PREFIX = ".smrt-lease-";
|
|
1524
|
+
var UID_MAP_PATH = "/proc/self/uid_map";
|
|
1520
1525
|
var PREFLIGHT_RETRY_LIMIT = 4;
|
|
1521
1526
|
var INITIALIZATION_LOCK_TIMEOUT_MS = 12e4;
|
|
1522
1527
|
var INITIALIZATION_LOCK_WORKER = `
|
|
@@ -1525,6 +1530,19 @@ var INITIALIZATION_LOCK_WORKER = `
|
|
|
1525
1530
|
let transaction;
|
|
1526
1531
|
(async () => {
|
|
1527
1532
|
try {
|
|
1533
|
+
if (workerData.lockOnly) {
|
|
1534
|
+
// Legacy /tmp lease probe (#3877): the same BEGIN IMMEDIATE lock an
|
|
1535
|
+
// older release's secure acquisition takes, on a pre-validated,
|
|
1536
|
+
// already-existing, never-written lock file.
|
|
1537
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
1538
|
+
const lockDatabase = new DatabaseSync(workerData.path, {
|
|
1539
|
+
timeout: workerData.timeout,
|
|
1540
|
+
});
|
|
1541
|
+
database = { close: async () => lockDatabase.close() };
|
|
1542
|
+
lockDatabase.exec('BEGIN IMMEDIATE');
|
|
1543
|
+
transaction = { rollback: async () => lockDatabase.exec('ROLLBACK') };
|
|
1544
|
+
parentPort.postMessage({ type: 'acquired' });
|
|
1545
|
+
} else {
|
|
1528
1546
|
const { getDatabase } = await import(workerData.sqlModuleUrl);
|
|
1529
1547
|
database = await getDatabase({
|
|
1530
1548
|
type: 'sqlite',
|
|
@@ -1542,6 +1560,7 @@ var INITIALIZATION_LOCK_WORKER = `
|
|
|
1542
1560
|
await database.query('PRAGMA busy_timeout = ' + workerData.timeout);
|
|
1543
1561
|
transaction = await database.beginTransaction();
|
|
1544
1562
|
parentPort.postMessage({ type: 'acquired' });
|
|
1563
|
+
}
|
|
1545
1564
|
} catch (error) {
|
|
1546
1565
|
try { await database?.close?.(); } catch {}
|
|
1547
1566
|
parentPort.postMessage({ type: 'error', message: String(error) });
|
|
@@ -1668,6 +1687,7 @@ async function initializeLocalApplicationRuntime(options) {
|
|
|
1668
1687
|
const bindHost = normalizeLoopbackHost(options.bindHost ?? DEFAULT_BIND_HOST);
|
|
1669
1688
|
const bootstrapTtlSeconds = validateBootstrapTtl(options.bootstrapTtlSeconds ?? DEFAULT_BOOTSTRAP_TTL_SECONDS);
|
|
1670
1689
|
const sessionTtlSeconds = validateSessionTtl(options.sessionTtlSeconds ?? DEFAULT_SESSION_TTL_SECONDS);
|
|
1690
|
+
const ownerTenant = resolveOwnerTenant(options.ownerTenant);
|
|
1671
1691
|
const resolvedRuntime = resolveApplicationRuntime({
|
|
1672
1692
|
profile: "local",
|
|
1673
1693
|
providers: options.providers
|
|
@@ -1704,6 +1724,7 @@ async function initializeLocalApplicationRuntime(options) {
|
|
|
1704
1724
|
sessionTtlSeconds,
|
|
1705
1725
|
backgroundJobs: options.backgroundJobs === true,
|
|
1706
1726
|
paidCapabilities: options.paidCapabilities === true,
|
|
1727
|
+
ownerTenant,
|
|
1707
1728
|
now: options.now ?? (() => /* @__PURE__ */ new Date()),
|
|
1708
1729
|
canonicalSourceRoot
|
|
1709
1730
|
});
|
|
@@ -1744,6 +1765,7 @@ var InitializedLocalApplicationRuntime = class {
|
|
|
1744
1765
|
mcpTokens;
|
|
1745
1766
|
bootstrapTtlSeconds;
|
|
1746
1767
|
sessionTtlSeconds;
|
|
1768
|
+
ownerTenant;
|
|
1747
1769
|
now;
|
|
1748
1770
|
canonicalSourceRoot;
|
|
1749
1771
|
constructor(state) {
|
|
@@ -1753,6 +1775,7 @@ var InitializedLocalApplicationRuntime = class {
|
|
|
1753
1775
|
this.resolvedRuntime = state.resolvedRuntime;
|
|
1754
1776
|
this.bootstrapTtlSeconds = state.bootstrapTtlSeconds;
|
|
1755
1777
|
this.sessionTtlSeconds = state.sessionTtlSeconds;
|
|
1778
|
+
this.ownerTenant = state.ownerTenant;
|
|
1756
1779
|
this.backgroundJobsEnabled = state.backgroundJobs;
|
|
1757
1780
|
this.paidCapabilitiesEnabled = state.paidCapabilities;
|
|
1758
1781
|
this.now = state.now;
|
|
@@ -1805,7 +1828,7 @@ var InitializedLocalApplicationRuntime = class {
|
|
|
1805
1828
|
AND expires_at > ?
|
|
1806
1829
|
RETURNING expires_at`, nowIso, tokenHash, nowIso)).rows.length === 0) await throwBootstrapClaimFailure(tx, now);
|
|
1807
1830
|
if (await findExistingOwner(tx)) throw new LocalRuntimeError("bootstrap_claimed", "The local owner has already been claimed.");
|
|
1808
|
-
return createLocalOwnerRecords(tx, input, this.sessionTtlSeconds);
|
|
1831
|
+
return createLocalOwnerRecords(tx, input, this.sessionTtlSeconds, this.ownerTenant);
|
|
1809
1832
|
});
|
|
1810
1833
|
}
|
|
1811
1834
|
/** Restore the normal server-side session created by owner bootstrap. */
|
|
@@ -1895,7 +1918,10 @@ var InitializedLocalApplicationRuntime = class {
|
|
|
1895
1918
|
return createHmac("sha256", (await validateApplicationSecret(this.paths.applicationSecret, this.canonicalSourceRoot)).trim()).update(token).digest("hex");
|
|
1896
1919
|
}
|
|
1897
1920
|
};
|
|
1898
|
-
async function createLocalOwnerRecords(db, input, sessionTtlSeconds) {
|
|
1921
|
+
async function createLocalOwnerRecords(db, input, sessionTtlSeconds, ownerTenant) {
|
|
1922
|
+
const existingTenant = await (await TenantCollection.create({ db })).findBySlug(ownerTenant.slug);
|
|
1923
|
+
const memberships = await MembershipCollection.create({ db });
|
|
1924
|
+
if (existingTenant) await assertAdoptableOwnerTenant(existingTenant, ownerTenant, memberships);
|
|
1899
1925
|
const profileTypes = await ProfileTypeCollection.create({ db });
|
|
1900
1926
|
const person = new Person({
|
|
1901
1927
|
db,
|
|
@@ -1916,15 +1942,32 @@ async function createLocalOwnerRecords(db, input, sessionTtlSeconds) {
|
|
|
1916
1942
|
lastLoginAt: /* @__PURE__ */ new Date()
|
|
1917
1943
|
});
|
|
1918
1944
|
await user.save();
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1945
|
+
const userId = requireId(user.id, "User");
|
|
1946
|
+
let tenant;
|
|
1947
|
+
let membership;
|
|
1948
|
+
if (existingTenant) {
|
|
1949
|
+
const ownerRole = await (await RoleCollection.create({ db })).findSystemRoleBySlug(DEFAULT_ROLE_SLUGS.OWNER);
|
|
1950
|
+
const created = await memberships.create({
|
|
1951
|
+
userId,
|
|
1952
|
+
tenantId: requireId(existingTenant.id, "Tenant"),
|
|
1953
|
+
roleId: requireId(ownerRole?.id, "Owner role"),
|
|
1954
|
+
status: MembershipStatus.ACTIVE
|
|
1955
|
+
});
|
|
1956
|
+
await created.save();
|
|
1957
|
+
tenant = existingTenant;
|
|
1958
|
+
membership = created;
|
|
1959
|
+
} else {
|
|
1960
|
+
const tenantName = input.tenantName?.trim() || ownerTenant.name;
|
|
1961
|
+
const tenantService = new TenantService({ db }, {
|
|
1962
|
+
mode: "required",
|
|
1963
|
+
maxTenants: 1,
|
|
1964
|
+
defaultName: tenantName
|
|
1965
|
+
});
|
|
1966
|
+
await tenantService.initialize();
|
|
1967
|
+
({tenant, membership} = await tenantService.createTenantWithOwnership(userId, tenantName, { slug: ownerTenant.slug }));
|
|
1968
|
+
}
|
|
1926
1969
|
const session = await (await SessionCollection.create({ db })).createSession({
|
|
1927
|
-
userId
|
|
1970
|
+
userId,
|
|
1928
1971
|
tenantId: requireId(tenant.id, "Tenant"),
|
|
1929
1972
|
ttl: sessionTtlSeconds,
|
|
1930
1973
|
userAgent: input.userAgent,
|
|
@@ -1933,12 +1976,27 @@ async function createLocalOwnerRecords(db, input, sessionTtlSeconds) {
|
|
|
1933
1976
|
});
|
|
1934
1977
|
return Object.freeze({
|
|
1935
1978
|
profileId: requireId(person.id, "Person"),
|
|
1936
|
-
userId
|
|
1979
|
+
userId,
|
|
1937
1980
|
tenantId: requireId(tenant.id, "Tenant"),
|
|
1938
1981
|
membershipId: requireId(membership.id, "Membership"),
|
|
1939
1982
|
sessionId: requireId(session.id, "Session")
|
|
1940
1983
|
});
|
|
1941
1984
|
}
|
|
1985
|
+
async function assertAdoptableOwnerTenant(tenant, ownerTenant, memberships) {
|
|
1986
|
+
if (!ownerTenant.adoptExisting) throw new LocalRuntimeError("owner_tenant_conflict", "A tenant with the owner tenant slug already exists; set ownerTenant.adoptExisting to adopt it or choose another slug.");
|
|
1987
|
+
const tenantId = requireId(tenant.id, "Tenant");
|
|
1988
|
+
if (tenant.status !== TenantStatus.ACTIVE || tenant.parentTenantId || (await memberships.findByTenant(tenantId)).length > 0) throw new LocalRuntimeError("owner_tenant_conflict", "The existing owner tenant cannot be adopted: it must be an active root tenant with no memberships.");
|
|
1989
|
+
}
|
|
1990
|
+
function resolveOwnerTenant(options) {
|
|
1991
|
+
const slug = options?.slug ?? DEFAULT_OWNER_TENANT_SLUG;
|
|
1992
|
+
if (typeof slug !== "string" || !OWNER_TENANT_SLUG_PATTERN.test(slug)) throw new LocalRuntimeError("invalid_configuration", "ownerTenant.slug must be 1-64 lowercase letters, digits, or inner hyphens.");
|
|
1993
|
+
const name = typeof options?.name === "string" && options.name.trim() ? options.name.trim() : DEFAULT_OWNER_TENANT_NAME;
|
|
1994
|
+
return Object.freeze({
|
|
1995
|
+
slug,
|
|
1996
|
+
name,
|
|
1997
|
+
adoptExisting: options?.adoptExisting === true
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
1942
2000
|
async function findExistingOwner(db) {
|
|
1943
2001
|
const ownerRole = await (await RoleCollection.create({ db })).findSystemRoleBySlug(DEFAULT_ROLE_SLUGS.OWNER);
|
|
1944
2002
|
if (!ownerRole?.id) return null;
|
|
@@ -2194,15 +2252,97 @@ async function prepareDedicatedRoot(path, canonicalSourceRoot, appId, createdDir
|
|
|
2194
2252
|
async function acquireInitializationLock(root) {
|
|
2195
2253
|
const currentUid = process.getuid?.();
|
|
2196
2254
|
if (currentUid === void 0) throw unsafeFilesystemEntry("Local runtime initialization locks require a numeric current-user identity.");
|
|
2197
|
-
const
|
|
2255
|
+
const lockKey = createHash("sha256").update(normalizeFilesystemIdentity(root)).digest("hex").slice(0, 32);
|
|
2256
|
+
const besideData = await prepareDataParentLeaseRoot(root, lockKey, currentUid);
|
|
2257
|
+
const canonicalTemporaryRoot = await realpath("/tmp");
|
|
2258
|
+
const mappedUid = await legacyLeaseUid(currentUid);
|
|
2259
|
+
const legacyLockRoots = [.../* @__PURE__ */ new Set([currentUid, mappedUid])].map((uid) => resolve(canonicalTemporaryRoot, `.smrt-${uid}`));
|
|
2260
|
+
if (mappedUid !== currentUid && await isForeignOwnedDirectory(legacyLockRoots[0], currentUid)) legacyLockRoots.shift();
|
|
2261
|
+
const held = [];
|
|
2262
|
+
try {
|
|
2263
|
+
if (besideData === null) for (const lockRoot of legacyLockRoots) held.push(await acquireTemporaryInitializationLease(lockRoot, lockKey, currentUid));
|
|
2264
|
+
else {
|
|
2265
|
+
held.push(await acquireInitializationDatabaseLease(resolve(besideData, "initialization.sqlite"), besideData));
|
|
2266
|
+
for (const lockRoot of legacyLockRoots) {
|
|
2267
|
+
const legacy = await hasTrustedLeaseAncestry(lockRoot, currentUid) ? await acquireTemporaryInitializationLease(lockRoot, lockKey, currentUid) : await acquireLegacyLeaseWithoutTrustedAncestry(lockRoot, lockKey, currentUid);
|
|
2268
|
+
if (legacy) held.push(legacy);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
} catch (error) {
|
|
2272
|
+
try {
|
|
2273
|
+
await releaseInitializationLeases(held);
|
|
2274
|
+
} catch (releaseError) {
|
|
2275
|
+
throw preservePrimaryFailure(error, releaseError, "initializationLockReleaseError", "Local runtime initialization-lock acquisition and release both failed.");
|
|
2276
|
+
}
|
|
2277
|
+
throw error;
|
|
2278
|
+
}
|
|
2279
|
+
let released = false;
|
|
2280
|
+
return { release: async () => {
|
|
2281
|
+
if (released) return;
|
|
2282
|
+
released = true;
|
|
2283
|
+
await releaseInitializationLeases(held);
|
|
2284
|
+
} };
|
|
2285
|
+
}
|
|
2286
|
+
async function isForeignOwnedDirectory(path, currentUid) {
|
|
2287
|
+
let details;
|
|
2288
|
+
try {
|
|
2289
|
+
details = await lstat(path);
|
|
2290
|
+
} catch (error) {
|
|
2291
|
+
if (isMissingFile(error)) return false;
|
|
2292
|
+
throw error;
|
|
2293
|
+
}
|
|
2294
|
+
return !details.isSymbolicLink() && details.isDirectory() && details.uid !== currentUid;
|
|
2295
|
+
}
|
|
2296
|
+
async function releaseInitializationLeases(leases) {
|
|
2297
|
+
let failure;
|
|
2298
|
+
for (const lease of leases.toReversed()) try {
|
|
2299
|
+
await lease.release();
|
|
2300
|
+
} catch (error) {
|
|
2301
|
+
failure = failure === void 0 ? error : preservePrimaryFailure(failure, error, "initializationLockReleaseError", "Several local runtime initialization leases failed to release.");
|
|
2302
|
+
}
|
|
2303
|
+
if (failure !== void 0) throw failure;
|
|
2304
|
+
}
|
|
2305
|
+
async function legacyLeaseUid(currentUid) {
|
|
2306
|
+
let map;
|
|
2307
|
+
try {
|
|
2308
|
+
map = await readFile(UID_MAP_PATH, "utf8");
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
if (isMissingFile(error)) return currentUid;
|
|
2311
|
+
throw unsafeFilesystemEntry("Local runtime could not read the user-namespace uid map for its legacy lease.", error);
|
|
2312
|
+
}
|
|
2313
|
+
for (const line of map.split("\n")) {
|
|
2314
|
+
if (!line.trim()) continue;
|
|
2315
|
+
const fields = line.trim().split(/\s+/);
|
|
2316
|
+
const [inside, outside, count] = fields.map((field) => /^\d+$/.test(field) ? Number(field) : NaN);
|
|
2317
|
+
if (fields.length !== 3 || !Number.isSafeInteger(inside) || !Number.isSafeInteger(outside) || !Number.isSafeInteger(count)) throw unsafeFilesystemEntry("Local runtime could not parse the user-namespace uid map for its legacy lease.");
|
|
2318
|
+
if (currentUid >= inside && currentUid - inside < count) return outside + (currentUid - inside);
|
|
2319
|
+
}
|
|
2320
|
+
throw unsafeFilesystemEntry("The user-namespace uid map does not cover the current user; the legacy local runtime lease cannot be located.");
|
|
2321
|
+
}
|
|
2322
|
+
async function prepareDataParentLeaseRoot(root, lockKey, currentUid) {
|
|
2323
|
+
const parent = resolve(root, "..");
|
|
2324
|
+
const leaseRoot = resolve(parent, `${LEASE_DIRECTORY_PREFIX}${lockKey}`);
|
|
2325
|
+
try {
|
|
2326
|
+
await mkdir(parent, {
|
|
2327
|
+
recursive: true,
|
|
2328
|
+
mode: 448
|
|
2329
|
+
});
|
|
2330
|
+
await mkdir(leaseRoot, { mode: 448 });
|
|
2331
|
+
} catch (error) {
|
|
2332
|
+
if (isLeaseLocationUnavailable(error)) return null;
|
|
2333
|
+
if (!isFileAlreadyExists(error)) throw error;
|
|
2334
|
+
}
|
|
2335
|
+
await assertPrivateLeaseDirectory(leaseRoot, currentUid);
|
|
2336
|
+
return leaseRoot;
|
|
2337
|
+
}
|
|
2338
|
+
async function acquireTemporaryInitializationLease(lockRoot, lockKey, currentUid) {
|
|
2198
2339
|
try {
|
|
2199
2340
|
await mkdir(lockRoot, { mode: 448 });
|
|
2200
2341
|
} catch (error) {
|
|
2201
2342
|
if (!isFileAlreadyExists(error)) throw error;
|
|
2202
2343
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
const custodyRoot = resolve(lockRoot, createHash("sha256").update(normalizeFilesystemIdentity(root)).digest("hex").slice(0, 32));
|
|
2344
|
+
await assertPrivateLeaseDirectory(lockRoot, currentUid);
|
|
2345
|
+
const custodyRoot = resolve(lockRoot, lockKey);
|
|
2206
2346
|
try {
|
|
2207
2347
|
await mkdir(custodyRoot, { mode: 448 });
|
|
2208
2348
|
} catch (error) {
|
|
@@ -2210,12 +2350,70 @@ async function acquireInitializationLock(root) {
|
|
|
2210
2350
|
}
|
|
2211
2351
|
return acquireInitializationDatabaseLease(resolve(custodyRoot, "initialization.sqlite"), custodyRoot);
|
|
2212
2352
|
}
|
|
2213
|
-
async function
|
|
2353
|
+
async function assertPrivateLeaseDirectory(path, currentUid) {
|
|
2354
|
+
const details = await lstat(path);
|
|
2355
|
+
if (!details.isDirectory() || details.isSymbolicLink() || details.uid !== currentUid || (details.mode & 511) !== 448) throw unsafeFilesystemEntry(`Local runtime lock directory must be current-user-owned with mode 0700: ${path}`);
|
|
2356
|
+
}
|
|
2357
|
+
async function acquireLegacyLeaseWithoutTrustedAncestry(lockRoot, lockKey, currentUid) {
|
|
2358
|
+
for (const component of absolutePathComponents(resolve(lockRoot, ".."))) {
|
|
2359
|
+
const details = await lstat(component);
|
|
2360
|
+
if (details.isSymbolicLink() || !details.isDirectory() || (details.mode & 18) !== 0 && (details.mode & 512) === 0) throw unsafeFilesystemEntry(`Legacy local runtime lock ancestry permits replacement by another principal: ${component}`);
|
|
2361
|
+
}
|
|
2362
|
+
const custodyRoot = resolve(lockRoot, lockKey);
|
|
2363
|
+
const path = resolve(custodyRoot, "initialization.sqlite");
|
|
2364
|
+
try {
|
|
2365
|
+
await mkdir(lockRoot, { mode: 448 });
|
|
2366
|
+
} catch (error) {
|
|
2367
|
+
if (isLeaseLocationUnavailable(error)) {
|
|
2368
|
+
try {
|
|
2369
|
+
await lstat(path);
|
|
2370
|
+
} catch (probeError) {
|
|
2371
|
+
if (isMissingFile(probeError)) return null;
|
|
2372
|
+
throw probeError;
|
|
2373
|
+
}
|
|
2374
|
+
throw error;
|
|
2375
|
+
}
|
|
2376
|
+
if (!isFileAlreadyExists(error)) throw error;
|
|
2377
|
+
}
|
|
2378
|
+
await assertPrivateLeaseDirectory(lockRoot, currentUid);
|
|
2379
|
+
try {
|
|
2380
|
+
await mkdir(custodyRoot, { mode: 448 });
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
if (!isFileAlreadyExists(error)) throw error;
|
|
2383
|
+
}
|
|
2384
|
+
await assertPrivateLeaseDirectory(custodyRoot, currentUid);
|
|
2385
|
+
let created;
|
|
2386
|
+
try {
|
|
2387
|
+
created = await open(path, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
2388
|
+
} catch (error) {
|
|
2389
|
+
if (!isFileAlreadyExists(error)) throw error;
|
|
2390
|
+
} finally {
|
|
2391
|
+
await created?.close();
|
|
2392
|
+
}
|
|
2393
|
+
const leaf = await lstat(path);
|
|
2394
|
+
if (leaf.isSymbolicLink() || !leaf.isFile() || leaf.uid !== currentUid || (leaf.mode & 18) !== 0) throw unsafeFilesystemEntry(`Legacy local runtime lock file must be a current-user-owned private file: ${path}`);
|
|
2395
|
+
return acquireInitializationDatabaseLease(path, custodyRoot, true);
|
|
2396
|
+
}
|
|
2397
|
+
async function hasTrustedLeaseAncestry(path, currentUid) {
|
|
2398
|
+
for (const component of absolutePathComponents(resolve(path, ".."))) {
|
|
2399
|
+
const details = await lstat(component);
|
|
2400
|
+
if (details.isSymbolicLink() || !details.isDirectory()) return false;
|
|
2401
|
+
if (details.uid !== currentUid && details.uid !== 0) return false;
|
|
2402
|
+
if ((details.mode & 18) !== 0 && (details.mode & 512) === 0) return false;
|
|
2403
|
+
}
|
|
2404
|
+
return true;
|
|
2405
|
+
}
|
|
2406
|
+
function isLeaseLocationUnavailable(error) {
|
|
2407
|
+
const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
2408
|
+
return code === "EACCES" || code === "EPERM" || code === "EROFS";
|
|
2409
|
+
}
|
|
2410
|
+
async function acquireInitializationDatabaseLease(path, custodyRoot, lockOnly = false) {
|
|
2214
2411
|
const worker = new Worker(INITIALIZATION_LOCK_WORKER, {
|
|
2215
2412
|
eval: true,
|
|
2216
2413
|
workerData: {
|
|
2217
2414
|
path,
|
|
2218
2415
|
custodyRoot,
|
|
2416
|
+
lockOnly,
|
|
2219
2417
|
timeout: INITIALIZATION_LOCK_TIMEOUT_MS,
|
|
2220
2418
|
sqlModuleUrl: import.meta.resolve("@happyvertical/sql")
|
|
2221
2419
|
}
|
|
@@ -2552,4 +2750,4 @@ function isDirectoryNotEmpty(error) {
|
|
|
2552
2750
|
//#endregion
|
|
2553
2751
|
export { reclaimStaleRecord as A, deepFrozenPlainCopy as B, findInstalledPackage as C, processExists as D, lockOwnerPid as E, LOCAL_MCP_TOKEN_PRINCIPAL as F, initializeDeployedApplicationRuntime as G, resolveBoundMembershipPermissions as H, LOCAL_MCP_TOKEN_TABLE as I, isSessionTenantAuthorized as K, MAX_LOCAL_MCP_TOKEN_TTL_SECONDS as L, runtimeConfigurationFingerprint as M, DEFAULT_LOCAL_MCP_TOKEN_TTL_SECONDS as N, withOperationLock as O, LOCAL_MCP_TOKEN_PREFIX as P, isLocalMcpTokenPrincipal as R, createProviderReadinessProbe as S, isStaleOperationLock as T, DeployedRuntimeCleanupError as U, ownValue as V, DeployedRuntimeError as W, MAX_RUNTIME_DIAGNOSTIC_TOOLS as _, openLocalMcpTokenStore as a, projectRuntimeDiagnostics as b, validateApplicationId as c, readActiveWriterLease as d, ONBOARDING_HANDOFF_FILES as f, MAX_RUNTIME_DIAGNOSTIC_ERRORS as g, resolveApplicationStateRoot as h, initializeLocalApplicationRuntime as i, resolveApplicationId as j, RECLAIM_MUTEX_FILE as k, validateLocalDatabaseStorage as l, removeOnboardingHandoff as m, MIGRATION_FAILED_MESSAGE as n, prepareLocalDatabaseStorage as o, prepareApplicationStateRoot as p, encodeApplicationId as r, resolveLocalRuntimePaths as s, LocalRuntimeError as t, acquireWriterLease as u, RUNTIME_DIAGNOSTICS_SCHEMA_VERSION as v, resolveReadinessModule as w, PROVIDER_READINESS_SETTINGS as x, RUNTIME_WORKER_FRESHNESS_SECONDS as y, PlainDataError as z };
|
|
2554
2752
|
|
|
2555
|
-
//# sourceMappingURL=src-
|
|
2753
|
+
//# sourceMappingURL=src-DeNO6bG_.js.map
|