@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 +5 -1
- package/README.md +25 -5
- package/dist/chunks/{src-DP7SxcSY.js → src-DOepXMDO.js} +67 -25
- package/dist/chunks/src-DOepXMDO.js.map +1 -0
- package/dist/index.d.ts +1 -1
- 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/manifest.json +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/dist/smrt-knowledge.okf.md +52 -0
- package/dist/sveltekit/owner-setup.d.ts.map +1 -1
- package/dist/sveltekit-runtime.js +2 -2
- package/dist/sveltekit.js +1 -1
- 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
|
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.
|
|
@@ -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
|
|
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
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
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
|
-
|
|
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-
|
|
2820
|
+
//# sourceMappingURL=src-DOepXMDO.js.map
|