@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 +11 -2
- package/README.md +92 -5
- package/dist/chunks/{src-DP7SxcSY.js → src-DG72tyzB.js} +129 -29
- package/dist/chunks/src-DG72tyzB.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/local-installation-identity.d.ts +10 -0
- package/dist/local-installation-identity.d.ts.map +1 -0
- package/dist/local-mcp-tokens.d.ts +19 -1
- package/dist/local-mcp-tokens.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/smrt-knowledge.json +13 -5
- package/dist/smrt-knowledge.okf.md +56 -0
- package/dist/sveltekit/index.d.ts +1 -0
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit/local-mcp-credentials.d.ts +10 -0
- package/dist/sveltekit/local-mcp-credentials.d.ts.map +1 -0
- package/dist/sveltekit/owner-setup.d.ts.map +1 -1
- package/dist/sveltekit-runtime.js +2 -2
- package/dist/sveltekit.js +97 -2
- package/dist/sveltekit.js.map +1 -1
- package/package.json +9 -9
- package/dist/chunks/src-DP7SxcSY.js.map +0 -1
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
|
-
|
|
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`.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
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
|
-
|
|
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
|
|
2029
|
-
|
|
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-
|
|
2878
|
+
//# sourceMappingURL=src-DG72tyzB.js.map
|