@happyvertical/smrt-app-runtime 0.60.9 → 0.60.10

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
@@ -46,7 +46,11 @@ Application infrastructure composition for the validated runtime profiles in
46
46
  selects the tenant slug (default `default`); because `Tenant` upserts on
47
47
  slug, the claim must look the slug up first and fail closed
48
48
  (`owner_tenant_conflict`) rather than save over it. `adoptExisting` adopts
49
- only an active root tenant with zero memberships (#3032).
49
+ only an active root tenant with zero memberships (#3032). Fresh tenant
50
+ creation uses `TenantService` with the local ownership policy and a trusted
51
+ reserved identity. Default tenant and owner graph IDs derive from the
52
+ protected per-install application secret. Preserve that file when restoring
53
+ the same installation.
50
54
  - Bootstrap is loopback-only. Only an HMAC of the short-lived token is stored.
51
55
  A token that cannot claim fails a read-only preflight before role/catalog
52
56
  seeding (#3323); the claim transaction's conditional UPDATE stays the
package/README.md CHANGED
@@ -134,11 +134,31 @@ remains global. Startup and setup are idempotent; replayed, expired, or
134
134
  concurrent claims fail closed.
135
135
  `ownerTenant: { slug, name, adoptExisting }` (also accepted by the SvelteKit
136
136
  runtime) selects the bootstrap tenant; the default is a new tenant with slug
137
- `default`. Because `Tenant` upserts on its slug, the claim checks first and
138
- never replaces an existing tenant: it fails with `owner_tenant_conflict`, or,
139
- with `adoptExisting: true`, adds the owner membership to an application-seeded
140
- tenant only when that tenant is active, has no parent, and has no memberships of
141
- any status. An adopted tenant keeps its id and name. A failed claim rolls back
137
+ `default`. The default tenant, owner User, owner Person, and owner Membership
138
+ have separate UUIDv8 identifiers derived with domain-separated HMAC-SHA256
139
+ from the installation's private `secrets/application.secret`. Independent
140
+ installs of the same app get distinct identities and share the global built-in
141
+ role IDs. Restarts reuse the same installation identities. Treat
142
+ `secrets/application.secret` as installation identity material. Preserve it
143
+ with the database in protected backups and during a restore or move of the
144
+ same installation. Deleting, replacing, or rotating the file changes the
145
+ derived identities and invalidates existing bootstrap and local MCP tokens.
146
+ Keep only one active installation for a given secret. A copied data root
147
+ retains the same owner, tenant, and writer IDs, and the runtime currently has
148
+ no way to detect that copy. Start independent installations with a fresh data
149
+ root.
150
+
151
+ The invitation and active owner membership authorize the owner. The same
152
+ seed derives a separate writer UUID. The local runtime binds it to the
153
+ database so local edits to tracked rows identify this installation, including
154
+ after a restart.
155
+
156
+ A tenant with the selected slug causes `owner_tenant_conflict`, unless
157
+ `adoptExisting: true` explicitly allows adopting an active root tenant with
158
+ zero memberships of any status. An adopted tenant keeps its id and name.
159
+ The claim rejects an unrelated row using the reserved tenant UUID and an
160
+ account using the owner UUID or the claimed email. The owner Person and
161
+ Membership also use strict inserts. A failed claim rolls back
142
162
  and leaves the invitation claimable.
143
163
  Authenticated session TTLs are configured in whole seconds with a minimum of
144
164
  one second, and invalid values are rejected before filesystem mutation.
@@ -6,6 +6,7 @@ import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:p
6
6
  import { promisify } from "node:util";
7
7
  import { Worker } from "node:worker_threads";
8
8
  import { resolveApplicationRuntime } from "@happyvertical/smrt-config";
9
+ import { setDatabaseSyncDeviceId } from "@happyvertical/smrt-core";
9
10
  import { SCHEDULED_JOB_QUEUE, ScheduleRunner, TaskRunner } from "@happyvertical/smrt-jobs";
10
11
  import { Person, ProfileTypeCollection } from "@happyvertical/smrt-profiles";
11
12
  import { withSystemContext } from "@happyvertical/smrt-tenancy";
@@ -16,6 +17,25 @@ import { chmodSync, closeSync, constants as constants$1, existsSync, lstatSync,
16
17
  import { DatabaseSync } from "node:sqlite";
17
18
  import { registerHooks } from "node:module";
18
19
  import { fileURLToPath, pathToFileURL } from "node:url";
20
+ //#region src/local-installation-identity.ts
21
+ function installationUuid(key, purpose) {
22
+ const bytes = createHmac("sha256", key).update(`smrt/local-installation/v1/${purpose}`).digest();
23
+ bytes[6] = bytes[6] & 15 | 128;
24
+ bytes[8] = bytes[8] & 63 | 128;
25
+ const hex = bytes.toString("hex", 0, 16);
26
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
27
+ }
28
+ function deriveLocalInstallationIdentity(applicationSecret) {
29
+ const key = Buffer.from(applicationSecret.trim(), "base64url");
30
+ return {
31
+ deviceId: installationUuid(key, "device"),
32
+ tenantId: installationUuid(key, "default-tenant"),
33
+ ownerId: installationUuid(key, "owner"),
34
+ profileId: installationUuid(key, "owner-profile"),
35
+ membershipId: installationUuid(key, "owner-membership")
36
+ };
37
+ }
38
+ //#endregion
19
39
  //#region src/deployed-runtime.ts
20
40
  var DeployedRuntimeError = class extends Error {
21
41
  constructor(code, message, component) {
@@ -1707,7 +1727,8 @@ async function initializeLocalApplicationRuntime(options) {
1707
1727
  const acquired = await acquireLocalDatabase(paths, sourceRoot, appId);
1708
1728
  db = acquired.db;
1709
1729
  const { canonicalSourceRoot } = acquired;
1710
- await prepareLocalFilesystem(paths, canonicalSourceRoot);
1730
+ const identity = await prepareLocalFilesystem(paths, canonicalSourceRoot);
1731
+ setDatabaseSyncDeviceId(db, identity.deviceId);
1711
1732
  await tuneLocalSqlite(db);
1712
1733
  try {
1713
1734
  await options.prepareDatabase?.(db);
@@ -1727,6 +1748,7 @@ async function initializeLocalApplicationRuntime(options) {
1727
1748
  paidCapabilities: options.paidCapabilities === true,
1728
1749
  ownerTenant,
1729
1750
  passwordPolicy: options.passwordPolicy,
1751
+ identity,
1730
1752
  now: options.now ?? (() => /* @__PURE__ */ new Date()),
1731
1753
  canonicalSourceRoot
1732
1754
  });
@@ -1771,6 +1793,7 @@ var InitializedLocalApplicationRuntime = class {
1771
1793
  passwordPolicy;
1772
1794
  now;
1773
1795
  canonicalSourceRoot;
1796
+ identity;
1774
1797
  constructor(state) {
1775
1798
  this.db = state.db;
1776
1799
  this.paths = state.paths;
@@ -1784,6 +1807,7 @@ var InitializedLocalApplicationRuntime = class {
1784
1807
  this.paidCapabilitiesEnabled = state.paidCapabilities;
1785
1808
  this.now = state.now;
1786
1809
  this.canonicalSourceRoot = state.canonicalSourceRoot;
1810
+ this.identity = state.identity;
1787
1811
  this.mcpTokens = localMcpTokenStore(state.db, state.paths, state.canonicalSourceRoot, state.now);
1788
1812
  }
1789
1813
  /**
@@ -1833,7 +1857,7 @@ var InitializedLocalApplicationRuntime = class {
1833
1857
  AND expires_at > ?
1834
1858
  RETURNING expires_at`, nowIso, tokenHash, nowIso)).rows.length === 0) await throwBootstrapClaimFailure(tx, now);
1835
1859
  if (await findExistingOwner(tx)) throw new LocalRuntimeError("bootstrap_claimed", "The local owner has already been claimed.");
1836
- return createLocalOwnerRecords(tx, input, this.sessionTtlSeconds, this.ownerTenant, this.passwordPolicy);
1860
+ return createLocalOwnerRecords(tx, input, this.sessionTtlSeconds, this.ownerTenant, this.identity, this.passwordPolicy);
1837
1861
  });
1838
1862
  }
1839
1863
  /** Restore the normal server-side session created by owner bootstrap. */
@@ -1923,54 +1947,71 @@ var InitializedLocalApplicationRuntime = class {
1923
1947
  return createHmac("sha256", (await validateApplicationSecret(this.paths.applicationSecret, this.canonicalSourceRoot)).trim()).update(token).digest("hex");
1924
1948
  }
1925
1949
  };
1926
- async function createLocalOwnerRecords(db, input, sessionTtlSeconds, ownerTenant, passwordPolicy) {
1927
- const existingTenant = await (await TenantCollection.create({ db })).findBySlug(ownerTenant.slug);
1950
+ async function createLocalOwnerRecords(db, input, sessionTtlSeconds, ownerTenant, identity, passwordPolicy) {
1951
+ const tenants = await TenantCollection.create({ db });
1952
+ const existingTenant = await tenants.findBySlug(ownerTenant.slug);
1953
+ const defaultTenant = ownerTenant.slug === DEFAULT_OWNER_TENANT_SLUG;
1954
+ const reservedTenant = defaultTenant ? await tenants.get(identity.tenantId) : null;
1955
+ if (reservedTenant && reservedTenant.id !== existingTenant?.id) throw new LocalRuntimeError("owner_tenant_conflict", "The reserved local tenant identity is already in use.");
1956
+ const users = await UserCollection.create({ db });
1957
+ if (await users.get(identity.ownerId) || await users.findByEmail(input.email.trim())) throw new LocalRuntimeError("owner_identity_conflict", "The local owner identity is already in use.");
1928
1958
  const memberships = await MembershipCollection.create({ db });
1929
1959
  if (existingTenant) await assertAdoptableOwnerTenant(existingTenant, ownerTenant, memberships);
1930
1960
  const profileTypes = await ProfileTypeCollection.create({ db });
1961
+ const personType = await withSystemContext(() => profileTypes.getOrCreateGlobalBySlug("person", {
1962
+ name: "Person",
1963
+ description: "An individual person"
1964
+ }));
1931
1965
  const person = new Person({
1932
1966
  db,
1933
- typeId: requireId((await withSystemContext(() => profileTypes.getOrCreateGlobalBySlug("person", {
1934
- name: "Person",
1935
- description: "An individual person"
1936
- }))).id, "Person profile type"),
1967
+ id: identity.profileId,
1968
+ _skipLoad: true,
1969
+ typeId: requireId(personType.id, "Person profile type"),
1937
1970
  tenantId: null,
1938
1971
  name: input.name.trim(),
1939
1972
  email: input.email.trim()
1940
1973
  });
1941
1974
  await person.initialize();
1975
+ person.requireInsertOnSave({ preserveDerivedSlug: true });
1942
1976
  await person.save();
1943
- const user = await (await UserCollection.create({ db })).create({
1977
+ const user = await users.create({
1978
+ id: identity.ownerId,
1979
+ _insertOnly: true,
1944
1980
  profileId: requireId(person.id, "Person"),
1945
1981
  email: input.email.trim(),
1946
1982
  status: UserStatus.ACTIVE,
1947
1983
  lastLoginAt: /* @__PURE__ */ new Date()
1948
1984
  });
1949
- await user.save();
1950
1985
  const userId = requireId(user.id, "User");
1951
- let tenant;
1952
- let membership;
1986
+ let ownership;
1953
1987
  if (existingTenant) {
1954
1988
  const ownerRole = await (await RoleCollection.create({ db })).findSystemRoleBySlug(DEFAULT_ROLE_SLUGS.OWNER);
1955
- const created = await memberships.create({
1956
- userId,
1957
- tenantId: requireId(existingTenant.id, "Tenant"),
1958
- roleId: requireId(ownerRole?.id, "Owner role"),
1959
- status: MembershipStatus.ACTIVE
1960
- });
1961
- await created.save();
1962
- tenant = existingTenant;
1963
- membership = created;
1989
+ ownership = {
1990
+ tenant: existingTenant,
1991
+ membership: await memberships.create({
1992
+ id: identity.membershipId,
1993
+ _insertOnly: true,
1994
+ userId,
1995
+ tenantId: requireId(existingTenant.id, "Tenant"),
1996
+ roleId: requireId(ownerRole?.id, "Owner role"),
1997
+ status: MembershipStatus.ACTIVE
1998
+ })
1999
+ };
1964
2000
  } else {
1965
2001
  const tenantName = input.tenantName?.trim() || ownerTenant.name;
1966
- const tenantService = new TenantService({ db }, {
2002
+ ownership = await (await TenantService.create({ db }, {
1967
2003
  mode: "required",
1968
2004
  maxTenants: 1,
1969
2005
  defaultName: tenantName
2006
+ })).createTenantWithOwnership(userId, tenantName, {
2007
+ slug: ownerTenant.slug,
2008
+ reservedIdentity: {
2009
+ tenantId: defaultTenant ? identity.tenantId : void 0,
2010
+ membershipId: identity.membershipId
2011
+ }
1970
2012
  });
1971
- await tenantService.initialize();
1972
- ({tenant, membership} = await tenantService.createTenantWithOwnership(userId, tenantName, { slug: ownerTenant.slug }));
1973
2013
  }
2014
+ const { tenant, membership } = ownership;
1974
2015
  person.tenantId = requireId(tenant.id, "Tenant");
1975
2016
  await person.save();
1976
2017
  const session = await (await SessionCollection.create({ db })).createSession({
@@ -2233,6 +2274,7 @@ async function prepareLocalFilesystem(paths, canonicalSourceRoot) {
2233
2274
  await ensureSafeDirectoryTree(paths.assets, canonicalSourceRoot);
2234
2275
  await ensureSafeDirectoryTree(paths.secrets, canonicalSourceRoot);
2235
2276
  await ensureApplicationSecret(paths.applicationSecret, canonicalSourceRoot);
2277
+ return deriveLocalInstallationIdentity(await validateApplicationSecret(paths.applicationSecret, canonicalSourceRoot));
2236
2278
  }
2237
2279
  async function prepareDedicatedRoot(path, canonicalSourceRoot, appId, createdDirectories) {
2238
2280
  const finalMarker = resolve(path, `${ROOT_MARKER_PREFIX}${appId}`);
@@ -2775,4 +2817,4 @@ function isDirectoryNotEmpty(error) {
2775
2817
  //#endregion
2776
2818
  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 };
2777
2819
 
2778
- //# sourceMappingURL=src-DP7SxcSY.js.map
2820
+ //# sourceMappingURL=src-DOepXMDO.js.map