@happyvertical/smrt-app-runtime 0.60.9 → 0.60.11

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
@@ -73,7 +77,12 @@ Application infrastructure composition for the validated runtime profiles in
73
77
  `tenantBinding: 'direct'` and the
74
78
  `LOCAL_MCP_TOKEN_PRINCIPAL` brand; `runAsPrincipal` rejects a copy that
75
79
  widens the binding, so the two public APIs compose safely.
76
- Never log or return the token outside `issue()`. The operator store opens an
80
+ Return bearer material only to the issuing operator or the authenticated
81
+ desktop credential response. The opt-in `createLocalMcpCredentialHandler`
82
+ requires a direct loopback peer, matching Host and exact Origin, and the sole
83
+ active owner's first-class session. Desktop slots use a disjoint ID namespace,
84
+ atomic hash replacement and terminal revocation. See the desktop protocol in
85
+ [README.md](README.md). The operator store opens an
77
86
  uncached connection and takes no writer lease (revocation is per request).
78
87
  - Background jobs and application-defined paid capabilities are default-off.
79
88
  - The embedded runner reuses `TaskRunner`; it is not a second job contract.
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.
@@ -148,6 +168,73 @@ explicitly enabled. With embedded job topology, `createEmbeddedJobRunner()`
148
168
  returns the normal s-m-r-t `TaskRunner`, so the application keeps one enqueue
149
169
  and execution contract without requiring a separate worker service.
150
170
 
171
+ ### Local desktop MCP credentials
172
+
173
+ A desktop product can mount this opt-in SvelteKit route in
174
+ `src/routes/api/local/mcp-credentials/+server.ts`.
175
+
176
+ ```ts
177
+ import { createLocalMcpCredentialHandler } from '@happyvertical/smrt-app-runtime/sveltekit';
178
+ import { runtime } from '$lib/server/smrt';
179
+
180
+ export const POST = createLocalMcpCredentialHandler(runtime);
181
+ ```
182
+
183
+ Serve it on the desktop app's direct loopback listener, with the adapter's
184
+ socket peer address and request Host. Keep forwarded address, host, and
185
+ protocol configuration unset. Set adapter-node's
186
+ `ORIGIN` to the actual desktop URL, such as `http://127.0.0.1:4678`, because
187
+ adapter-node otherwise assumes HTTPS. The handler checks the socket peer,
188
+ loopback URL host, matching Host and exact Origin. When the browser
189
+ sends `Sec-Fetch-Site`, it must be `same-origin`. Call the endpoint from the
190
+ app's signed-in, same-origin desktop webview using its normal session cookie.
191
+ The session must use local bootstrap, password or passkey authentication,
192
+ have a direct active membership, and match the sole active system Owner
193
+ membership of the installation. Device, layered and permission-capped sessions
194
+ are rejected.
195
+
196
+ Send a JSON POST with an opaque lowercase UUIDv4 `id` generated by the
197
+ desktop app, a nonempty array of permission slugs, and an explicit integer
198
+ lifetime from 60 to 31,536,000 seconds.
199
+
200
+ ```json
201
+ {
202
+ "action": "put",
203
+ "id": "2c70f518-b95a-445b-a75e-8126ae7c032a",
204
+ "scopes": ["notes.read"],
205
+ "expiresInSeconds": 3600
206
+ }
207
+ ```
208
+
209
+ The first PUT creates the slot. A later POST with the same `action: "put"`
210
+ replaces its bearer, scopes and expiry atomically. Scopes must be held by
211
+ both the live owner and the authenticated session. Successful replies carry
212
+ `{ id, token, scopes, expiresAt }` in a private `Cache-Control: no-store`
213
+ response. Pass the bearer directly to native credential handling for the MCP
214
+ bridge. Keep it out of arguments, URLs, logs, browser storage, and generated
215
+ plugin artifacts. The installation secret stays on the server, and the
216
+ database stores only the bearer HMAC.
217
+
218
+ Persist the non-secret slot ID before the first request and serialize actions
219
+ per slot. A failed write preserves the previous credential. If the outcome is
220
+ unknown or the response is lost, repeat PUT with the same ID. Each successful
221
+ write produces a fresh credential, and only the latest committed write
222
+ verifies. A retry therefore recovers the slot without leaving an extra active
223
+ credential.
224
+
225
+ Send `{ "action": "revoke", "id": "<slot-id>" }` with an unrestricted owner
226
+ session to disconnect. Repeating it returns `{ "status": "revoked" }`.
227
+ Revocation is permanent for the slot even if a PUT was delayed or no PUT
228
+ completed, so use a new UUID to reconnect. The operator CLI can list or
229
+ revoke the stored UUIDv8 record. Desktop slots and manual CLI token IDs are
230
+ separate namespaces.
231
+
232
+ Failures carry a fixed `code` and no credentials. A revoked slot returns
233
+ `409 slot_unavailable`; validation failures return `400 invalid_request`
234
+ or `400 invalid_scope`; requests failing the trust checks return
235
+ `403 forbidden`. Deployed profiles return `404 unavailable` to requests
236
+ that reach the profile check.
237
+
151
238
  ## SvelteKit entry
152
239
 
153
240
  `@happyvertical/smrt-app-runtime/sveltekit` composes the profile runtime,
@@ -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) {
@@ -688,6 +708,8 @@ var MAX_SCOPE_LENGTH = 128;
688
708
  var MAX_LABEL_LENGTH = 80;
689
709
  var SCOPE_PATTERN = /^[\x21\x23-\x5b\x5d-\x7e]+$/u;
690
710
  var TOKEN_PATTERN = /^smrt_mcp_[A-Za-z0-9_-]{43}$/u;
711
+ var DESKTOP_SLOT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
712
+ var DESKTOP_SLOT_LABEL = "Desktop integration";
691
713
  var TOKEN_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u;
692
714
  var HMAC_DOMAIN = "smrt.local-mcp-token.v1\0";
693
715
  var LOCAL_MCP_TOKEN_PRINCIPAL = /* @__PURE__ */ Symbol.for("@happyvertical/smrt-app-runtime/local-mcp-token-principal");
@@ -744,7 +766,7 @@ function createLocalMcpTokenStore(deps) {
744
766
  if (fields === null || typeof fields !== "object") throw fail("invalid_configuration", "MCP token input must be an object.");
745
767
  const scopes = normalizeScopes(ownValue(fields, "scopes"), fail);
746
768
  const ttl = ownValue(fields, "expiresInSeconds") ?? 2592e3;
747
- if (!Number.isSafeInteger(ttl) || ttl < 60 || ttl > 31536e3) throw fail("invalid_configuration", "MCP token lifetime must be between one minute and 365 days.");
769
+ validateTtl(ttl, fail);
748
770
  const label = normalizeLabel(ownValue(fields, "label"), fail);
749
771
  const owner = await deps.findOwner(db);
750
772
  if (!owner) throw fail("owner_unavailable", "No local owner exists yet; finish owner setup before issuing MCP tokens.");
@@ -769,6 +791,58 @@ function createLocalMcpTokenStore(deps) {
769
791
  expiresAt
770
792
  });
771
793
  },
794
+ async manageDesktopCredential(session, input) {
795
+ let request;
796
+ try {
797
+ request = deepFrozenPlainCopy(input);
798
+ } catch (error) {
799
+ if (!(error instanceof PlainDataError)) throw error;
800
+ throw fail("invalid_configuration", "Invalid desktop MCP input.");
801
+ }
802
+ if (typeof request?.id !== "string" || !DESKTOP_SLOT_ID_PATTERN.test(request.id)) throw fail("invalid_configuration", "Invalid desktop MCP slot id.");
803
+ const id = `${request.id.slice(0, 14)}8${request.id.slice(15)}`;
804
+ const { userId, tenantId, permissions } = session;
805
+ const allowed = new Set(permissions);
806
+ const owner = await deps.findOwner(db, true);
807
+ if (!owner || owner.userId !== userId || owner.tenantId !== tenantId) throw fail("owner_unavailable", "The local owner session is required.");
808
+ const user = await (await UserCollection.create({ db })).get({ id: owner.userId });
809
+ const held = await livePermissions(owner.userId, owner.tenantId);
810
+ if (!user?.isActive() || !held) throw fail("owner_unavailable", "The local owner session is required.");
811
+ const created = now();
812
+ const createdAt = created.toISOString();
813
+ if (request.action === "revoke") return (await db.query(`INSERT INTO _smrt_local_mcp_tokens
814
+ (id, token_hash, user_id, tenant_id, scopes, label, created_at, expires_at, revoked_at)
815
+ VALUES (?, ?, ?, ?, '[]', ?, ?, ?, ?)
816
+ ON CONFLICT(id) DO UPDATE SET revoked_at =
817
+ coalesce(_smrt_local_mcp_tokens.revoked_at, excluded.revoked_at)
818
+ WHERE _smrt_local_mcp_tokens.user_id = excluded.user_id
819
+ AND _smrt_local_mcp_tokens.tenant_id = excluded.tenant_id
820
+ RETURNING id`, id, randomBytes(32).toString("hex"), owner.userId, owner.tenantId, DESKTOP_SLOT_LABEL, createdAt, createdAt, createdAt)).rows.length > 0 ? "revoked" : "slot_unavailable";
821
+ if (request.action !== "put") throw fail("invalid_configuration", "Invalid desktop MCP action.");
822
+ const scopes = normalizeScopes(request.scopes, fail);
823
+ validateTtl(request.expiresInSeconds, fail);
824
+ if (scopes.some((scope) => !held.has(scope) || !allowed.has(scope))) throw fail("invalid_scope", "Desktop MCP scopes are not authorized.");
825
+ const expiresAt = new Date(created.getTime() + request.expiresInSeconds * 1e3).toISOString();
826
+ const token = `${LOCAL_MCP_TOKEN_PREFIX}${randomBytes(32).toString("base64url")}`;
827
+ if ((await db.query(`INSERT INTO _smrt_local_mcp_tokens
828
+ (id, token_hash, user_id, tenant_id, scopes, label, created_at, expires_at)
829
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
830
+ ON CONFLICT(id) DO UPDATE SET
831
+ token_hash = excluded.token_hash, scopes = excluded.scopes,
832
+ created_at = excluded.created_at, expires_at = excluded.expires_at
833
+ WHERE _smrt_local_mcp_tokens.revoked_at IS NULL
834
+ AND _smrt_local_mcp_tokens.user_id = excluded.user_id
835
+ AND _smrt_local_mcp_tokens.tenant_id = excluded.tenant_id
836
+ RETURNING id`, id, await hashToken(token), owner.userId, owner.tenantId, JSON.stringify(scopes), DESKTOP_SLOT_LABEL, createdAt, expiresAt)).rows.length === 0) return "slot_unavailable";
837
+ return Object.freeze({
838
+ id,
839
+ token,
840
+ scopes: Object.freeze(scopes),
841
+ label: DESKTOP_SLOT_LABEL,
842
+ createdAt,
843
+ expiresAt
844
+ });
845
+ },
772
846
  async list() {
773
847
  await ensureLocalMcpTokenTable(db);
774
848
  const result = await db.query(`SELECT id, scopes, label, created_at, expires_at, revoked_at
@@ -828,6 +902,9 @@ function createLocalMcpTokenStore(deps) {
828
902
  }
829
903
  });
830
904
  }
905
+ function validateTtl(ttl, fail) {
906
+ if (!Number.isSafeInteger(ttl) || ttl < 60 || ttl > 31536e3) throw fail("invalid_configuration", "MCP token lifetime must be between one minute and 365 days.");
907
+ }
831
908
  function normalizeScopes(value, fail) {
832
909
  if (!Array.isArray(value) || value.length === 0) throw fail("invalid_scope", "An MCP token needs at least one scope.");
833
910
  if (value.length > MAX_SCOPES) throw fail("invalid_scope", "An MCP token may carry at most 64 scopes.");
@@ -1707,7 +1784,8 @@ async function initializeLocalApplicationRuntime(options) {
1707
1784
  const acquired = await acquireLocalDatabase(paths, sourceRoot, appId);
1708
1785
  db = acquired.db;
1709
1786
  const { canonicalSourceRoot } = acquired;
1710
- await prepareLocalFilesystem(paths, canonicalSourceRoot);
1787
+ const identity = await prepareLocalFilesystem(paths, canonicalSourceRoot);
1788
+ setDatabaseSyncDeviceId(db, identity.deviceId);
1711
1789
  await tuneLocalSqlite(db);
1712
1790
  try {
1713
1791
  await options.prepareDatabase?.(db);
@@ -1727,6 +1805,7 @@ async function initializeLocalApplicationRuntime(options) {
1727
1805
  paidCapabilities: options.paidCapabilities === true,
1728
1806
  ownerTenant,
1729
1807
  passwordPolicy: options.passwordPolicy,
1808
+ identity,
1730
1809
  now: options.now ?? (() => /* @__PURE__ */ new Date()),
1731
1810
  canonicalSourceRoot
1732
1811
  });
@@ -1771,6 +1850,7 @@ var InitializedLocalApplicationRuntime = class {
1771
1850
  passwordPolicy;
1772
1851
  now;
1773
1852
  canonicalSourceRoot;
1853
+ identity;
1774
1854
  constructor(state) {
1775
1855
  this.db = state.db;
1776
1856
  this.paths = state.paths;
@@ -1784,6 +1864,7 @@ var InitializedLocalApplicationRuntime = class {
1784
1864
  this.paidCapabilitiesEnabled = state.paidCapabilities;
1785
1865
  this.now = state.now;
1786
1866
  this.canonicalSourceRoot = state.canonicalSourceRoot;
1867
+ this.identity = state.identity;
1787
1868
  this.mcpTokens = localMcpTokenStore(state.db, state.paths, state.canonicalSourceRoot, state.now);
1788
1869
  }
1789
1870
  /**
@@ -1833,7 +1914,7 @@ var InitializedLocalApplicationRuntime = class {
1833
1914
  AND expires_at > ?
1834
1915
  RETURNING expires_at`, nowIso, tokenHash, nowIso)).rows.length === 0) await throwBootstrapClaimFailure(tx, now);
1835
1916
  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);
1917
+ return createLocalOwnerRecords(tx, input, this.sessionTtlSeconds, this.ownerTenant, this.identity, this.passwordPolicy);
1837
1918
  });
1838
1919
  }
1839
1920
  /** Restore the normal server-side session created by owner bootstrap. */
@@ -1923,54 +2004,71 @@ var InitializedLocalApplicationRuntime = class {
1923
2004
  return createHmac("sha256", (await validateApplicationSecret(this.paths.applicationSecret, this.canonicalSourceRoot)).trim()).update(token).digest("hex");
1924
2005
  }
1925
2006
  };
1926
- async function createLocalOwnerRecords(db, input, sessionTtlSeconds, ownerTenant, passwordPolicy) {
1927
- const existingTenant = await (await TenantCollection.create({ db })).findBySlug(ownerTenant.slug);
2007
+ async function createLocalOwnerRecords(db, input, sessionTtlSeconds, ownerTenant, identity, passwordPolicy) {
2008
+ const tenants = await TenantCollection.create({ db });
2009
+ const existingTenant = await tenants.findBySlug(ownerTenant.slug);
2010
+ const defaultTenant = ownerTenant.slug === DEFAULT_OWNER_TENANT_SLUG;
2011
+ const reservedTenant = defaultTenant ? await tenants.get(identity.tenantId) : null;
2012
+ if (reservedTenant && reservedTenant.id !== existingTenant?.id) throw new LocalRuntimeError("owner_tenant_conflict", "The reserved local tenant identity is already in use.");
2013
+ const users = await UserCollection.create({ db });
2014
+ 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
2015
  const memberships = await MembershipCollection.create({ db });
1929
2016
  if (existingTenant) await assertAdoptableOwnerTenant(existingTenant, ownerTenant, memberships);
1930
2017
  const profileTypes = await ProfileTypeCollection.create({ db });
2018
+ const personType = await withSystemContext(() => profileTypes.getOrCreateGlobalBySlug("person", {
2019
+ name: "Person",
2020
+ description: "An individual person"
2021
+ }));
1931
2022
  const person = new Person({
1932
2023
  db,
1933
- typeId: requireId((await withSystemContext(() => profileTypes.getOrCreateGlobalBySlug("person", {
1934
- name: "Person",
1935
- description: "An individual person"
1936
- }))).id, "Person profile type"),
2024
+ id: identity.profileId,
2025
+ _skipLoad: true,
2026
+ typeId: requireId(personType.id, "Person profile type"),
1937
2027
  tenantId: null,
1938
2028
  name: input.name.trim(),
1939
2029
  email: input.email.trim()
1940
2030
  });
1941
2031
  await person.initialize();
2032
+ person.requireInsertOnSave({ preserveDerivedSlug: true });
1942
2033
  await person.save();
1943
- const user = await (await UserCollection.create({ db })).create({
2034
+ const user = await users.create({
2035
+ id: identity.ownerId,
2036
+ _insertOnly: true,
1944
2037
  profileId: requireId(person.id, "Person"),
1945
2038
  email: input.email.trim(),
1946
2039
  status: UserStatus.ACTIVE,
1947
2040
  lastLoginAt: /* @__PURE__ */ new Date()
1948
2041
  });
1949
- await user.save();
1950
2042
  const userId = requireId(user.id, "User");
1951
- let tenant;
1952
- let membership;
2043
+ let ownership;
1953
2044
  if (existingTenant) {
1954
2045
  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;
2046
+ ownership = {
2047
+ tenant: existingTenant,
2048
+ membership: await memberships.create({
2049
+ id: identity.membershipId,
2050
+ _insertOnly: true,
2051
+ userId,
2052
+ tenantId: requireId(existingTenant.id, "Tenant"),
2053
+ roleId: requireId(ownerRole?.id, "Owner role"),
2054
+ status: MembershipStatus.ACTIVE
2055
+ })
2056
+ };
1964
2057
  } else {
1965
2058
  const tenantName = input.tenantName?.trim() || ownerTenant.name;
1966
- const tenantService = new TenantService({ db }, {
2059
+ ownership = await (await TenantService.create({ db }, {
1967
2060
  mode: "required",
1968
2061
  maxTenants: 1,
1969
2062
  defaultName: tenantName
2063
+ })).createTenantWithOwnership(userId, tenantName, {
2064
+ slug: ownerTenant.slug,
2065
+ reservedIdentity: {
2066
+ tenantId: defaultTenant ? identity.tenantId : void 0,
2067
+ membershipId: identity.membershipId
2068
+ }
1970
2069
  });
1971
- await tenantService.initialize();
1972
- ({tenant, membership} = await tenantService.createTenantWithOwnership(userId, tenantName, { slug: ownerTenant.slug }));
1973
2070
  }
2071
+ const { tenant, membership } = ownership;
1974
2072
  person.tenantId = requireId(tenant.id, "Tenant");
1975
2073
  await person.save();
1976
2074
  const session = await (await SessionCollection.create({ db })).createSession({
@@ -2022,11 +2120,12 @@ function resolveOwnerTenant(options) {
2022
2120
  adoptExisting: options?.adoptExisting === true
2023
2121
  });
2024
2122
  }
2025
- async function findExistingOwner(db) {
2123
+ async function findExistingOwner(db, requireUnique = false) {
2026
2124
  const ownerRole = await (await RoleCollection.create({ db })).findSystemRoleBySlug(DEFAULT_ROLE_SLUGS.OWNER);
2027
2125
  if (!ownerRole?.id) return null;
2028
- const active = (await (await MembershipCollection.create({ db })).findByRole(ownerRole.id)).find((membership) => membership.status === MembershipStatus.ACTIVE && typeof membership.userId === "string" && typeof membership.tenantId === "string");
2029
- return active ? {
2126
+ const activeOwners = (await (await MembershipCollection.create({ db })).findByRole(ownerRole.id)).filter((membership) => membership.status === MembershipStatus.ACTIVE && typeof membership.userId === "string" && typeof membership.tenantId === "string");
2127
+ const active = activeOwners[0];
2128
+ return active && (!requireUnique || activeOwners.length === 1) ? {
2030
2129
  userId: active.userId,
2031
2130
  tenantId: active.tenantId
2032
2131
  } : null;
@@ -2233,6 +2332,7 @@ async function prepareLocalFilesystem(paths, canonicalSourceRoot) {
2233
2332
  await ensureSafeDirectoryTree(paths.assets, canonicalSourceRoot);
2234
2333
  await ensureSafeDirectoryTree(paths.secrets, canonicalSourceRoot);
2235
2334
  await ensureApplicationSecret(paths.applicationSecret, canonicalSourceRoot);
2335
+ return deriveLocalInstallationIdentity(await validateApplicationSecret(paths.applicationSecret, canonicalSourceRoot));
2236
2336
  }
2237
2337
  async function prepareDedicatedRoot(path, canonicalSourceRoot, appId, createdDirectories) {
2238
2338
  const finalMarker = resolve(path, `${ROOT_MARKER_PREFIX}${appId}`);
@@ -2775,4 +2875,4 @@ function isDirectoryNotEmpty(error) {
2775
2875
  //#endregion
2776
2876
  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
2877
 
2778
- //# sourceMappingURL=src-DP7SxcSY.js.map
2878
+ //# sourceMappingURL=src-DG72tyzB.js.map