@happyvertical/smrt-template-sveltekit 0.53.3 → 0.54.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.
Files changed (49) hide show
  1. package/AGENTS.md +60 -27
  2. package/README.md +23 -16
  3. package/mcp-apps-template/package.dependencies.json +2 -2
  4. package/mcp-apps-template/src/routes/.well-known/oauth-protected-resource/api/mcp/+server.ts +10 -6
  5. package/mcp-apps-template/src/routes/api/mcp/+server.ts +68 -12
  6. package/package.json +19 -15
  7. package/template/AGENTS.md +20 -5
  8. package/template/Dockerfile +0 -1
  9. package/template/INSTALL_PROMPT.md +2 -2
  10. package/template/README.md +86 -40
  11. package/template/compose.yaml +2 -2
  12. package/template/package.json +28 -27
  13. package/template/smrt.config.ts +16 -34
  14. package/template/src/app.d.ts +4 -11
  15. package/template/src/hooks.server.ts +10 -51
  16. package/template/src/lib/server/smrt.ts +22 -98
  17. package/template/src/routes/+layout.server.ts +4 -12
  18. package/template/src/routes/+layout.svelte +27 -99
  19. package/template/src/routes/+page.server.ts +4 -4
  20. package/template/src/routes/api/_runtime/diagnostics/+server.ts +17 -3
  21. package/template/src/routes/api/_runtime/health/+server.ts +4 -28
  22. package/template/src/routes/settings/+page.svelte +3 -34
  23. package/template/src/routes/setup/+page.server.ts +7 -94
  24. package/template/src/routes/setup/+page.svelte +3 -23
  25. package/template/vite.config.ts +5 -43
  26. package/template.config.js +11 -11
  27. package/mcp-apps-template/src/lib/McpAppsBridge.svelte +0 -37
  28. package/mcp-apps-template/src/lib/server/mcp-hosted-principal.ts +0 -12
  29. package/mcp-apps-template/src/lib/server/mcp-hosted.ts +0 -28
  30. package/mcp-apps-template/src/lib/server/mcp.ts +0 -85
  31. package/template/scripts/smrt-app.mjs +0 -885
  32. package/template/scripts/smrt-mcp-apps.mjs +0 -18
  33. package/template/scripts/smrt-operation-lock.mjs +0 -102
  34. package/template/scripts/smrt-portability-assets.mjs +0 -707
  35. package/template/scripts/smrt-portability.mjs +0 -470
  36. package/template/scripts/smrt-prepare-migration.mjs +0 -93
  37. package/template/scripts/smrt-process.mjs +0 -82
  38. package/template/scripts/smrt-provider-readiness.mjs +0 -33
  39. package/template/scripts/smrt-runtime-identity.mjs +0 -245
  40. package/template/scripts/smrt-vite.mjs +0 -45
  41. package/template/scripts/smrt-web.mjs +0 -10
  42. package/template/scripts/smrt-worker.mjs +0 -100
  43. package/template/scripts/smrt-writer-lease.mjs +0 -199
  44. package/template/src/lib/RuntimeDiagnosticsWebMcp.svelte +0 -11
  45. package/template/src/lib/runtime-diagnostics-webmcp.ts +0 -108
  46. package/template/src/lib/server/application-runtime.ts +0 -262
  47. package/template/src/lib/server/runtime-diagnostics.ts +0 -114
  48. package/template/src/lib/server/tenancy.ts +0 -114
  49. package/template/src/smrt-webmcp.d.ts +0 -6
@@ -1,108 +0,0 @@
1
- export const RUNTIME_DIAGNOSTICS_WEBMCP_TOOL_NAME =
2
- 'smrt.runtime.diagnostics.read';
3
-
4
- interface RuntimeDiagnosticsTool {
5
- readonly name: string;
6
- readonly description: string;
7
- readonly inputSchema: Record<string, unknown>;
8
- readonly annotations: {
9
- readonly readOnlyHint: true;
10
- readonly destructiveHint: false;
11
- readonly idempotentHint: true;
12
- readonly openWorldHint: false;
13
- };
14
- readonly execute: (args: Record<string, unknown>) => Promise<string>;
15
- }
16
-
17
- interface RuntimeDiagnosticsModelContext {
18
- registerTool(
19
- tool: RuntimeDiagnosticsTool,
20
- options: { signal: AbortSignal },
21
- ): void | Promise<void>;
22
- }
23
-
24
- export interface RuntimeDiagnosticsWebMcpOwner {
25
- readonly dispose: () => void;
26
- }
27
-
28
- export interface RegisterRuntimeDiagnosticsWebMcpOptions {
29
- readonly fetchFn?: typeof fetch;
30
- readonly modelContext?: RuntimeDiagnosticsModelContext;
31
- }
32
-
33
- /** Register exactly one page-user, read-only diagnostics tool for its owner. */
34
- export function registerRuntimeDiagnosticsWebMcp(
35
- options: RegisterRuntimeDiagnosticsWebMcpOptions = {},
36
- ): RuntimeDiagnosticsWebMcpOwner | null {
37
- const modelContext =
38
- options.modelContext ??
39
- (globalThis as {
40
- document?: { modelContext?: RuntimeDiagnosticsModelContext };
41
- }).document?.modelContext;
42
- if (!modelContext || typeof modelContext.registerTool !== 'function') {
43
- return null;
44
- }
45
-
46
- const controller = new AbortController();
47
- const tool = createRuntimeDiagnosticsWebMcpTool(options.fetchFn ?? fetch);
48
- try {
49
- const registration = modelContext.registerTool(tool, {
50
- signal: controller.signal,
51
- });
52
- void Promise.resolve(registration).catch(() => controller.abort());
53
- } catch {
54
- controller.abort();
55
- }
56
- return Object.freeze({ dispose: () => controller.abort() });
57
- }
58
-
59
- export function createRuntimeDiagnosticsWebMcpTool(
60
- fetchFn: typeof fetch,
61
- ): RuntimeDiagnosticsTool {
62
- return Object.freeze({
63
- name: RUNTIME_DIAGNOSTICS_WEBMCP_TOOL_NAME,
64
- description:
65
- 'Read the authenticated, redacted application runtime diagnostics.',
66
- inputSchema: Object.freeze({
67
- type: 'object',
68
- properties: Object.freeze({}),
69
- additionalProperties: false,
70
- }),
71
- annotations: Object.freeze({
72
- readOnlyHint: true as const,
73
- destructiveHint: false as const,
74
- idempotentHint: true as const,
75
- openWorldHint: false as const,
76
- }),
77
- async execute() {
78
- let response: Response;
79
- try {
80
- response = await fetchFn('/api/_runtime/diagnostics', {
81
- method: 'GET',
82
- credentials: 'same-origin',
83
- headers: { accept: 'application/json' },
84
- });
85
- } catch {
86
- return stableToolError('diagnostics_unavailable');
87
- }
88
- if (!response.ok) {
89
- return stableToolError(
90
- response.status === 401
91
- ? 'authentication_required'
92
- : response.status === 403
93
- ? 'authorization_denied'
94
- : 'diagnostics_unavailable',
95
- );
96
- }
97
- try {
98
- return JSON.stringify(await response.json());
99
- } catch {
100
- return stableToolError('diagnostics_unavailable');
101
- }
102
- },
103
- });
104
- }
105
-
106
- function stableToolError(code: string): string {
107
- return JSON.stringify({ ok: false, error: { code } });
108
- }
@@ -1,262 +0,0 @@
1
- import {
2
- initializeDeployedApplicationRuntime,
3
- initializeLocalApplicationRuntime,
4
- projectRuntimeDiagnostics,
5
- resolveLocalRuntimePaths,
6
- type LocalApplicationRuntime,
7
- type RuntimeDiagnostics,
8
- type RuntimeDiagnosticsProjectionInput,
9
- } from '@happyvertical/smrt-app-runtime';
10
- import {
11
- loadConfig,
12
- resolveApplicationRuntime,
13
- resolveConfiguredApplicationRuntime,
14
- } from '@happyvertical/smrt-config';
15
- import type { SmrtClassOptions } from '@happyvertical/smrt-core';
16
- import { getDatabase } from '@happyvertical/sql';
17
- import {
18
- prepareApplicationStateRoot,
19
- resolveApplicationId,
20
- runtimeConfigurationFingerprint,
21
- } from '../../../scripts/smrt-runtime-identity.mjs';
22
- import { acquireWriterLease } from '../../../scripts/smrt-writer-lease.mjs';
23
- import { createProviderReadinessProbe } from '../../../scripts/smrt-provider-readiness.mjs';
24
-
25
- const loadedConfig = await loadConfig();
26
-
27
- export const applicationRuntime = loadedConfig.runtime
28
- ? resolveConfiguredApplicationRuntime()
29
- : resolveApplicationRuntime({ profile: 'local' });
30
- const appId = resolveApplicationId({
31
- sourceRoot: process.cwd(),
32
- explicitId: process.env.SMRT_APP_ID,
33
- });
34
-
35
- export const applicationRuntimeConfiguration = runtimeConfigurationFingerprint(
36
- applicationRuntime,
37
- process.env,
38
- );
39
-
40
- export function getApplicationDatabaseConfig(): SmrtClassOptions['db'] {
41
- if (applicationRuntime.profile === 'local') {
42
- const paths = resolveLocalRuntimePaths({
43
- appId,
44
- dataDirectory: process.env.SMRT_DATA_DIR,
45
- sourceRoot: process.cwd(),
46
- });
47
- return { type: 'sqlite', url: paths.database };
48
- }
49
- const databaseUrl = process.env.DATABASE_URL;
50
- if (!databaseUrl) {
51
- throw new Error(`${applicationRuntime.profile} requires DATABASE_URL.`);
52
- }
53
- return { type: 'postgres', url: databaseUrl };
54
- }
55
-
56
- let localRuntimePromise: Promise<LocalApplicationRuntime> | undefined;
57
- let localWriterLease: { release(): void } | undefined;
58
- let deployedRuntimePromise: ReturnType<
59
- typeof initializeDeployedApplicationRuntime
60
- > | undefined;
61
-
62
- /** Fail-closed startup gate for every deployed web process. */
63
- export async function ensureApplicationRuntimeReady(): Promise<void> {
64
- if (applicationRuntime.profile === 'local') {
65
- await getLocalApplicationRuntime();
66
- return;
67
- }
68
- const databaseUrl = process.env.DATABASE_URL;
69
- if (!databaseUrl) {
70
- throw new Error(`${applicationRuntime.profile} requires DATABASE_URL.`);
71
- }
72
- const authenticationProvider =
73
- applicationRuntime.providers.authentication.provider;
74
- if (authenticationProvider === 'owner-bootstrap') {
75
- throw new Error('Deployed profiles require public authentication.');
76
- }
77
- deployedRuntimePromise ??= initializeDeployedApplicationRuntime({
78
- profile: applicationRuntime.profile,
79
- providers: {
80
- database: applicationRuntime.providers.database,
81
- authentication: applicationRuntime.providers.authentication,
82
- tenancy: applicationRuntime.providers.tenancy,
83
- assets: applicationRuntime.providers.assets,
84
- secrets: applicationRuntime.providers.secrets,
85
- jobs: applicationRuntime.providers.jobs,
86
- network: applicationRuntime.providers.network,
87
- },
88
- database: {
89
- engine: 'postgres',
90
- connect: () => getDatabase({ type: 'postgres', url: databaseUrl }),
91
- close: async (db) => db.close?.(),
92
- },
93
- authentication: {
94
- provider: authenticationProvider,
95
- readiness: createProviderReadinessProbe('authentication', {
96
- profile: applicationRuntime.profile,
97
- provider: authenticationProvider,
98
- }),
99
- },
100
- assets: {
101
- provider: applicationRuntime.providers.assets.provider,
102
- readiness: createProviderReadinessProbe('assets', {
103
- profile: applicationRuntime.profile,
104
- provider: applicationRuntime.providers.assets.provider,
105
- }),
106
- },
107
- secrets: {
108
- provider: applicationRuntime.providers.secrets.provider,
109
- readiness: createProviderReadinessProbe('secrets', {
110
- profile: applicationRuntime.profile,
111
- provider: applicationRuntime.providers.secrets.provider,
112
- }),
113
- },
114
- });
115
- await deployedRuntimePromise;
116
- }
117
-
118
- /** Local onboarding runtime. Deployed authentication belongs to its provider. */
119
- export function getLocalApplicationRuntime(): Promise<LocalApplicationRuntime> {
120
- if (applicationRuntime.profile !== 'local') {
121
- throw new Error('Owner bootstrap is available only in the local profile.');
122
- }
123
- const bindHost =
124
- process.env.HOST ||
125
- (process.env.NODE_ENV === 'development' ? '127.0.0.1' : null);
126
- if (!bindHost) {
127
- throw new Error(
128
- 'Local production startup requires an explicit loopback HOST; use pnpm app:start.',
129
- );
130
- }
131
- localWriterLease ??= acquireWriterLease(
132
- prepareApplicationStateRoot({
133
- appId,
134
- dataDirectory: process.env.SMRT_DATA_DIR,
135
- sourceRoot: process.cwd(),
136
- }),
137
- { operationInstance: process.env.SMRT_OPERATION_INSTANCE },
138
- );
139
- localRuntimePromise ??= initializeLocalApplicationRuntime({
140
- appId,
141
- dataDirectory: process.env.SMRT_DATA_DIR,
142
- sourceRoot: process.cwd(),
143
- bindHost,
144
- providers: {
145
- database: applicationRuntime.providers.database,
146
- authentication: applicationRuntime.providers.authentication,
147
- tenancy: applicationRuntime.providers.tenancy,
148
- assets: applicationRuntime.providers.assets,
149
- secrets: applicationRuntime.providers.secrets,
150
- jobs: applicationRuntime.providers.jobs,
151
- network: applicationRuntime.providers.network,
152
- },
153
- backgroundJobs: process.env.SMRT_BACKGROUND_JOBS === 'true',
154
- })
155
- .then(({ runtime }) => runtime)
156
- .catch((error) => {
157
- localWriterLease?.release();
158
- localWriterLease = undefined;
159
- localRuntimePromise = undefined;
160
- throw error;
161
- });
162
- return localRuntimePromise;
163
- }
164
-
165
- export interface ApplicationRuntimeDiagnosticsOptions {
166
- readonly toolNames: readonly string[];
167
- readonly observedAt: Date;
168
- /** Application-owned schema verification seam; runtime liveness is not proof. */
169
- readonly schemaStatus?: 'not-ready' | 'ready' | 'unknown';
170
- /** Application-owned migration tracker seam; database liveness is not proof. */
171
- readonly migrationStatus?: 'current' | 'failed' | 'pending' | 'unknown';
172
- /** Bounded worker seam supplied by the deployment's lease/heartbeat adapter. */
173
- readonly workerHeartbeatAt?: Date | string | null;
174
- /** Stable code/timestamp pairs only; raw errors never cross this seam. */
175
- readonly recentErrors?: readonly {
176
- readonly code?: unknown;
177
- readonly at?: unknown;
178
- }[];
179
- }
180
-
181
- /**
182
- * Read the private runtime only after the route has authenticated and
183
- * authorized its caller, then immediately project onto the public allowlist.
184
- */
185
- export async function readApplicationRuntimeDiagnostics(
186
- options: ApplicationRuntimeDiagnosticsOptions,
187
- ): Promise<RuntimeDiagnostics> {
188
- await ensureApplicationRuntimeReady();
189
-
190
- if (applicationRuntime.profile === 'local') {
191
- const runtime = await getLocalApplicationRuntime();
192
- const diagnostics = await runtime.diagnostics();
193
- return projectRuntimeDiagnostics({
194
- profile: 'local',
195
- health: 'healthy',
196
- schema: {
197
- status: options.schemaStatus ?? 'unknown',
198
- migrations: options.migrationStatus ?? 'unknown',
199
- },
200
- capabilities: {
201
- 'asset-storage': 'available',
202
- authentication: 'available',
203
- 'background-jobs': diagnostics.jobs.backgroundEnabled
204
- ? 'available'
205
- : 'disabled',
206
- database: 'available',
207
- 'paid-capabilities': diagnostics.paidCapabilitiesEnabled
208
- ? 'available'
209
- : 'disabled',
210
- 'secret-storage': 'available',
211
- },
212
- toolNames: options.toolNames,
213
- worker: {
214
- topology: diagnostics.jobs.topology,
215
- required: diagnostics.jobs.backgroundEnabled,
216
- heartbeatAt: options.workerHeartbeatAt,
217
- },
218
- recentErrors: options.recentErrors,
219
- observedAt: options.observedAt,
220
- });
221
- }
222
-
223
- const runtime = await deployedRuntimePromise;
224
- if (!runtime) throw new Error('deployed_runtime_unavailable');
225
- const readiness = await runtime.readiness();
226
- const componentStatus = (
227
- component: keyof typeof readiness.components,
228
- ): 'available' | 'unavailable' =>
229
- readiness.components[component].status === 'ready'
230
- ? 'available'
231
- : 'unavailable';
232
- const input: RuntimeDiagnosticsProjectionInput = {
233
- profile: applicationRuntime.profile,
234
- health:
235
- runtime.health().status === 'healthy'
236
- ? readiness.status === 'ready'
237
- ? 'healthy'
238
- : 'degraded'
239
- : 'stopped',
240
- schema: {
241
- status: options.schemaStatus ?? 'unknown',
242
- migrations: options.migrationStatus ?? 'unknown',
243
- },
244
- capabilities: {
245
- 'asset-storage': componentStatus('assets'),
246
- authentication: componentStatus('authentication'),
247
- 'background-jobs': 'available',
248
- database: componentStatus('database'),
249
- 'paid-capabilities': 'unknown',
250
- 'secret-storage': componentStatus('secrets'),
251
- },
252
- toolNames: options.toolNames,
253
- worker: {
254
- topology: runtime.resolvedRuntime.providers.jobs.topology,
255
- required: true,
256
- heartbeatAt: options.workerHeartbeatAt,
257
- },
258
- recentErrors: options.recentErrors,
259
- observedAt: options.observedAt,
260
- };
261
- return projectRuntimeDiagnostics(input);
262
- }
@@ -1,114 +0,0 @@
1
- import type { RuntimeDiagnostics } from '@happyvertical/smrt-app-runtime';
2
- import type { Role } from '@happyvertical/smrt-users';
3
- import { webMcpToolDefinitions } from '@happyvertical/smrt-virt-web';
4
- import { json } from '@sveltejs/kit';
5
-
6
- import { getCollection } from './smrt.js';
7
- import { readApplicationRuntimeDiagnostics } from './application-runtime.js';
8
- import { RUNTIME_DIAGNOSTICS_WEBMCP_TOOL_NAME } from '../runtime-diagnostics-webmcp.js';
9
-
10
- export const RUNTIME_DIAGNOSTICS_READ_PERMISSION = 'runtime_diagnostics.read';
11
-
12
- interface DiagnosticsLocals {
13
- readonly user?: { readonly id?: unknown } | null;
14
- readonly membership?: {
15
- readonly userId?: unknown;
16
- readonly tenantId?: unknown;
17
- readonly roleId?: unknown;
18
- isActive?: () => boolean;
19
- } | null;
20
- readonly permissions?: readonly string[];
21
- readonly tenantId?: unknown;
22
- readonly sessionId?: unknown;
23
- }
24
-
25
- interface RuntimeDiagnosticsHandlerOptions {
26
- readonly resolveRoleSlug: (roleId: string) => Promise<string | null>;
27
- readonly readDiagnostics: () => Promise<RuntimeDiagnostics>;
28
- }
29
-
30
- /** Testable route boundary: authorization always completes before projection. */
31
- export function createRuntimeDiagnosticsGet(
32
- options: RuntimeDiagnosticsHandlerOptions,
33
- ) {
34
- return async ({ locals }: { locals: DiagnosticsLocals }) => {
35
- const principal = authenticatedPrincipal(locals);
36
- if (!principal) {
37
- return stableError(401, 'authentication_required');
38
- }
39
-
40
- let authorized =
41
- locals.permissions?.includes(RUNTIME_DIAGNOSTICS_READ_PERMISSION) === true;
42
- if (!authorized) {
43
- try {
44
- authorized = (await options.resolveRoleSlug(principal.roleId)) === 'owner';
45
- } catch {
46
- authorized = false;
47
- }
48
- }
49
- if (!authorized) return stableError(403, 'authorization_denied');
50
-
51
- try {
52
- return json(await options.readDiagnostics());
53
- } catch {
54
- return stableError(503, 'diagnostics_unavailable');
55
- }
56
- };
57
- }
58
-
59
- export const runtimeDiagnosticsGet = createRuntimeDiagnosticsGet({
60
- async resolveRoleSlug(roleId) {
61
- const roles = await getCollection<Role>('Role');
62
- return (await roles.findById(roleId))?.slug ?? null;
63
- },
64
- readDiagnostics: () =>
65
- readApplicationRuntimeDiagnostics({
66
- toolNames: [
67
- ...webMcpToolDefinitions.map((definition) => definition.name),
68
- RUNTIME_DIAGNOSTICS_WEBMCP_TOOL_NAME,
69
- ],
70
- observedAt: new Date(),
71
- // Install application migration/schema verifiers here. Process/database
72
- // liveness alone intentionally leaves both values `unknown`.
73
- schemaStatus: 'unknown',
74
- migrationStatus: 'unknown',
75
- // External deployments install their lease-backed heartbeat adapter here.
76
- // Missing data remains `unknown`; web-process liveness is never substituted.
77
- workerHeartbeatAt: null,
78
- recentErrors: [],
79
- }),
80
- });
81
-
82
- function authenticatedPrincipal(locals: DiagnosticsLocals): {
83
- roleId: string;
84
- } | null {
85
- const userId = locals.user?.id;
86
- const tenantId = locals.tenantId;
87
- const sessionId = locals.sessionId;
88
- const membership = locals.membership;
89
- if (
90
- typeof userId !== 'string' ||
91
- !userId ||
92
- typeof tenantId !== 'string' ||
93
- !tenantId ||
94
- typeof sessionId !== 'string' ||
95
- !sessionId ||
96
- !membership ||
97
- membership.userId !== userId ||
98
- membership.tenantId !== tenantId ||
99
- typeof membership.roleId !== 'string' ||
100
- !membership.roleId
101
- ) {
102
- return null;
103
- }
104
- try {
105
- if (membership.isActive?.() !== true) return null;
106
- } catch {
107
- return null;
108
- }
109
- return { roleId: membership.roleId };
110
- }
111
-
112
- function stableError(status: number, code: string) {
113
- return json({ schemaVersion: 1, error: { code } }, { status });
114
- }
@@ -1,114 +0,0 @@
1
- /**
2
- * Tenant selection for SvelteKit requests.
3
- *
4
- * Selection and authorization are deliberately separate:
5
- *
6
- * - this module maps a trusted URL shape (the leading subdomain) to an active
7
- * tenant record;
8
- * - `createSessionHandler({ enterTenantContext: true })` authorizes the signed-
9
- * in user's active membership and establishes the request tenant context.
10
- *
11
- * A selected tenant is useful for login and membership-gated switch flows, but
12
- * it never becomes ambient query authority by itself. In particular, this
13
- * resolver ignores client-provided tenant headers.
14
- */
15
-
16
- import {
17
- TenantCollection,
18
- TenantStatus,
19
- } from '@happyvertical/smrt-users';
20
-
21
- import { getSmrtConfig } from './smrt.js';
22
-
23
- export interface TenantResolverEvent {
24
- url: URL;
25
- request: { headers: Headers };
26
- }
27
-
28
- export interface TenantSelection {
29
- /** Database UUID for an active tenant, or null when none resolves. */
30
- tenantId: string | null;
31
- /** URL-derived slug. Informational until membership authorizes a switch. */
32
- tenantSlug: string | null;
33
- }
34
-
35
- const ROOT_LIKE_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
36
- const RESERVED_SUBDOMAINS = new Set(['www', 'api', 'app', 'admin']);
37
-
38
- /**
39
- * Pure URL parser used by the default resolver.
40
- *
41
- * Set `TENANT_BASE_DOMAIN` in production so multi-label public suffixes are
42
- * never guessed. Without it, the fallback is intended only for local domains
43
- * such as `acme.demo.local`.
44
- */
45
- export function selectTenantSlug(
46
- url: URL,
47
- baseDomain = process.env.TENANT_BASE_DOMAIN,
48
- ): string | null {
49
- const hostname = url.hostname.toLowerCase();
50
- if (
51
- ROOT_LIKE_HOSTS.has(hostname) ||
52
- /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)
53
- ) {
54
- return null;
55
- }
56
-
57
- const normalizedBase = baseDomain
58
- ?.trim()
59
- .toLowerCase()
60
- .replace(/^\.+|\.+$/g, '');
61
-
62
- let candidate: string | undefined;
63
- if (normalizedBase) {
64
- if (hostname === normalizedBase) return null;
65
- const suffix = `.${normalizedBase}`;
66
- if (!hostname.endsWith(suffix)) return null;
67
- candidate = hostname.slice(0, -suffix.length).split('.')[0];
68
- } else {
69
- const labels = hostname.split('.');
70
- if (labels.length < 3) return null;
71
- candidate = labels[0];
72
- }
73
-
74
- return candidate && !RESERVED_SUBDOMAINS.has(candidate) ? candidate : null;
75
- }
76
-
77
- function isMissingTenantTable(error: unknown): boolean {
78
- if (!(error instanceof Error)) return false;
79
- const code = (error as Error & { code?: string }).code;
80
- return (
81
- code === '42P01' ||
82
- error.message.includes('relation "tenants" does not exist') ||
83
- error.message.includes('no such table: tenants')
84
- );
85
- }
86
-
87
- async function findActiveTenantId(slug: string): Promise<string | null> {
88
- try {
89
- const tenants = await TenantCollection.create(getSmrtConfig('Tenant'));
90
- const tenant = await tenants.findBySlug(slug);
91
- return tenant?.status === TenantStatus.ACTIVE ? (tenant.id ?? null) : null;
92
- } catch (error) {
93
- // A fresh project can render before its first `pnpm db:migrate`.
94
- if (isMissingTenantTable(error)) return null;
95
- throw error;
96
- }
97
- }
98
-
99
- /**
100
- * Default selection extension point. Replace this function when your tenant
101
- * key comes from a path, signed cookie, or trusted gateway assertion. Preserve
102
- * the rule that selection alone does not establish authorization context.
103
- */
104
- export async function resolveTenant(
105
- event: TenantResolverEvent,
106
- ): Promise<TenantSelection> {
107
- const tenantSlug = selectTenantSlug(event.url);
108
- if (!tenantSlug) return { tenantId: null, tenantSlug: null };
109
-
110
- return {
111
- tenantId: await findActiveTenantId(tenantSlug),
112
- tenantSlug,
113
- };
114
- }
@@ -1,6 +0,0 @@
1
- // `smrtPlugin` supplies the runtime virtual module during Vite builds. Keep
2
- // source-first `svelte-check` useful before that build has emitted its ambient
3
- // declaration, while preserving the generated shape when the plugin runs.
4
- declare module '@happyvertical/smrt-virt-web' {
5
- export const webMcpToolDefinitions: readonly import('@happyvertical/smrt-web').WebMcpToolDefinition[];
6
- }