@happyvertical/smrt-app-runtime 0.55.23 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md 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
- per-user, root-keyed custody directory. The released `@happyvertical/sql`
60
- trusted-parent boundary validates that directory, its ancestors, and the lock
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), or `setup_invalid`
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 tenantService = new TenantService({ db }, {
1920
- mode: "required",
1921
- maxTenants: 1,
1922
- defaultName: input.tenantName?.trim() || "My Workspace"
1923
- });
1924
- await tenantService.initialize();
1925
- const { tenant, membership } = await tenantService.createTenantWithOwnership(requireId(user.id, "User"), input.tenantName?.trim() || "My Workspace", { slug: "default" });
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: requireId(user.id, "User"),
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: requireId(user.id, "User"),
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 lockRoot = resolve(await realpath("/tmp"), `.smrt-${currentUid}`);
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
- const lockRootDetails = await lstat(lockRoot);
2204
- if (!lockRootDetails.isDirectory() || lockRootDetails.isSymbolicLink() || lockRootDetails.uid !== currentUid || (lockRootDetails.mode & 511) !== 448) throw unsafeFilesystemEntry(`Local runtime lock directory must be current-user-owned with mode 0700: ${lockRoot}`);
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 acquireInitializationDatabaseLease(path, custodyRoot) {
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-vP6GV99v.js.map
2753
+ //# sourceMappingURL=src-DeNO6bG_.js.map