@happyvertical/smrt-app-runtime 0.43.9 → 0.44.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.
package/AGENTS.md CHANGED
@@ -5,6 +5,9 @@ Application infrastructure composition for the validated runtime profiles in
5
5
 
6
6
  ## Local profile
7
7
 
8
+ - `validateApplicationId()` is the canonical strict validator for explicit app
9
+ IDs. `encodeApplicationId()` derives a length-bounded, collision-resistant
10
+ ID from package names; generated operational surfaces must share these APIs.
8
11
  - `initializeLocalApplicationRuntime()` owns user data paths, SQLite tuning,
9
12
  local application-secret creation, and the single-use owner bootstrap flow.
10
13
  - SQLite is acquired through `@happyvertical/sql`'s explicit
@@ -12,15 +15,69 @@ Application infrastructure composition for the validated runtime profiles in
12
15
  runtime establishes its user-owned mode-0700 data root.
13
16
  - Application migrations are explicit through `prepareDatabase`; runtime never
14
17
  creates application model tables implicitly.
18
+ - Standalone migration commands must call `prepareLocalDatabaseStorage()`
19
+ before opening SQLite so custody is established without creating schema or
20
+ bootstrap records.
21
+ - Read-only/copy/import operator commands must call
22
+ `validateLocalDatabaseStorage()` before opening SQLite; it verifies the
23
+ app-bound marker, source separation, real path chain, ownership, and mode
24
+ without creating or repairing storage.
15
25
  - Owner bootstrap creates normal `Person`, `User`, `Tenant`, owner `Role` /
16
26
  `Membership`, and `Session` records in one transaction.
17
27
  - Bootstrap is loopback-only. Only an HMAC of the short-lived token is stored.
18
28
  - Background jobs and application-defined paid capabilities are default-off.
19
29
  - The embedded runner reuses `TaskRunner`; it is not a second job contract.
20
30
 
31
+ ## Deployed profiles
32
+
33
+ - `initializeDeployedApplicationRuntime()` composes only `self-hosted` and
34
+ `cloud`; it rejects local initialization and provider/binding mismatches.
35
+ - Provider bindings own credentials, vendor clients, and readiness checks.
36
+ Runtime diagnostics contain selectors and status only.
37
+ - The database binding must expose a provider-owned `close` callback before
38
+ `connect` can run. The runtime owns that cleanup boundary and runs the
39
+ application's explicit, idempotent `prepareDatabase` hook.
40
+ - A failed startup cleanup throws `DeployedRuntimeCleanupError`; callers retain
41
+ and retry its idempotent `retryCleanup()` boundary until it succeeds.
42
+ - Public authentication, asset storage, and secret bindings are mandatory and
43
+ readiness-checked before startup succeeds.
44
+ - `createTaskWorker()` and `createScheduleWorker()` initialize the normal jobs
45
+ package runners against the shared PostgreSQL database. They are intended for
46
+ separate processes; the web process does not start them automatically.
47
+ - `close()` drains in-flight readiness/session/worker initialization and
48
+ serialized runner start/stop operations, stops every runner returned by the
49
+ runtime, and then closes PostgreSQL. Returned runners must not be restarted
50
+ after runtime shutdown begins.
51
+ - A tracked operation may await a re-entrant `close()` request without
52
+ deadlocking; external `close()` callers still await complete cleanup.
53
+ - `health()` is process liveness. `readiness()` probes database/auth/assets/
54
+ secrets; it does not claim that an external worker fleet is running.
55
+ - Database-provider readiness is additive; a PostgreSQL-specific server-version
56
+ probe always gates startup and live readiness.
57
+ - Cloud must keep required tenant context and must never introduce a root or
58
+ unscoped tenant fallback. RLS remains an explicit deployment/migration choice.
59
+
60
+ ## Public runtime diagnostics
61
+
62
+ - `projectRuntimeDiagnostics()` is the only public diagnostic projection. It
63
+ constructs the schema-version-1 allowlist and never serializes the private
64
+ local or deployed diagnostics objects.
65
+ - The allowlist is limited to active profile/coarse health, schema and
66
+ migration readiness, six fixed capability statuses, a sorted/capped public
67
+ tool inventory plus digest, explicit operational topology differences,
68
+ coarse worker heartbeat state, and eight stable code/timestamp errors.
69
+ - Callers supply an explicit clock and worker-heartbeat seam. A missing
70
+ heartbeat is `unknown`; web-process liveness never proves worker liveness.
71
+ - Error ingestion maps unknown codes to `runtime_error`, truncates timestamps
72
+ to the minute, and ignores messages, stacks, logs, records, and nested state.
73
+ - Authentication and diagnostics-read authorization belong before every call
74
+ to the projector or its runtime probes.
75
+
21
76
  ## Invariants
22
77
 
23
78
  - Never expose application-secret bytes or bootstrap token hashes in diagnostics.
79
+ - Never include provider error text, URLs, tokens, or credentials in deployed
80
+ errors, diagnostics, health, or readiness payloads.
24
81
  - Keep local data, assets, database, and secrets outside the source tree.
25
82
  - Do not reach into collection/database private fields. Add an upstream public
26
83
  API if composition cannot be expressed through exported surfaces.
package/README.md CHANGED
@@ -90,3 +90,99 @@ Background jobs and application-defined paid capabilities remain disabled until
90
90
  explicitly enabled. With embedded job topology, `createEmbeddedJobRunner()`
91
91
  returns the normal s-m-r-t `TaskRunner`, so the application keeps one enqueue
92
92
  and execution contract without requiring a separate worker service.
93
+
94
+ ## Self-hosted and cloud applications
95
+
96
+ The deployed initializer validates the selected profile against concrete,
97
+ provider-owned bindings. Database URLs, OIDC credentials, storage keys, and
98
+ secret-manager identifiers stay inside those adapters and are never copied into
99
+ the runtime snapshot.
100
+
101
+ ```ts
102
+ import {
103
+ initializeDeployedApplicationRuntime,
104
+ } from '@happyvertical/smrt-app-runtime';
105
+ import { getDatabase } from '@happyvertical/sql';
106
+
107
+ const initialized = await initializeDeployedApplicationRuntime({
108
+ profile: 'self-hosted',
109
+ database: {
110
+ engine: 'postgres',
111
+ connect: () => getDatabase({
112
+ type: 'postgres',
113
+ url: requirePrivateSetting('DATABASE_URL'),
114
+ }),
115
+ close: async (db) => {
116
+ await db.close?.();
117
+ },
118
+ },
119
+ authentication: {
120
+ provider: 'oidc',
121
+ readiness: () => oidcProvider.assertReady(),
122
+ },
123
+ assets: {
124
+ provider: 's3-compatible',
125
+ readiness: () => assetProvider.assertReady(),
126
+ },
127
+ secrets: {
128
+ provider: 'environment',
129
+ readiness: () => secretProvider.assertReady(),
130
+ },
131
+ prepareDatabase: runApplicationMigrations,
132
+ });
133
+ ```
134
+
135
+ Startup validates every binding, including the provider-owned database cleanup
136
+ boundary, before opening a connection. A missing public-auth or secret binding,
137
+ a selector mismatch, an unavailable provider, a failed
138
+ PostgreSQL probe, or a failed migration rejects startup. Provider failures are
139
+ reported with stable component codes and omit the underlying provider message
140
+ so credentials cannot leak into HTTP or orchestration payloads.
141
+ A custom database readiness callback is additive; the runtime always runs its
142
+ own PostgreSQL-specific server-version probe before startup or readiness can
143
+ succeed.
144
+ If cleanup after a startup failure also fails,
145
+ `DeployedRuntimeCleanupError.retryCleanup()` retains the redacted, idempotent
146
+ ownership path until the provider closes successfully.
147
+
148
+ `health()` reports process liveness. `readiness()` rechecks PostgreSQL,
149
+ authentication, assets, and secrets and returns only `ready` / `not-ready`
150
+ component states. `diagnostics()` reports the resolved profile, explicit
151
+ provider selectors, tenancy posture, and worker topology without secret values.
152
+
153
+ Job producers keep using `SmrtObject.bg()` / `background().enqueue()` in every
154
+ profile. Deployed worker entry points initialize the ordinary runners against
155
+ the same PostgreSQL database:
156
+
157
+ ```ts
158
+ const taskWorker = await initialized.createTaskWorker({ concurrency: 8 });
159
+ await taskWorker.start();
160
+
161
+ // Run this in a separate schedule-worker process, not beside the web server.
162
+ const scheduleWorker = await initialized.createScheduleWorker();
163
+ await scheduleWorker.start();
164
+ ```
165
+
166
+ `initialized.close()` drains in-flight readiness/session/worker initialization
167
+ and serialized runner start/stop operations, stops every runner returned by
168
+ this runtime, and only then closes PostgreSQL.
169
+ Callers may stop a runner earlier, but must not restart a returned runner after
170
+ its runtime has begun shutdown; lifecycle-gated `start()` calls then reject.
171
+ An awaited `close()` request made from inside a tracked provider or worker
172
+ operation acknowledges shutdown so that operation can unwind; callers outside
173
+ the operation continue to await complete worker and database cleanup.
174
+
175
+ Self-hosted deployments may select OIDC or magic-link authentication, explicit
176
+ single- or multi-tenancy, local or S3-compatible assets, and environment,
177
+ local-file, or external secrets. Cloud requires hosted identity, PostgreSQL,
178
+ multi-tenancy with required tenant context, managed/external secrets,
179
+ managed/S3-compatible object storage, public TLS, and scalable workers. Cloud
180
+ may select application isolation instead of PostgreSQL RLS, but it can never
181
+ enable an unscoped/root-tenant fallback.
182
+
183
+ The application runtime does not provision databases, buckets, identity
184
+ providers, secret managers, worker fleets, TLS, billing, or a hosted control
185
+ plane. Those remain operator/managed-platform responsibilities. Enabling the
186
+ `database-rls` selector also requires the deployment migration to apply the
187
+ documented s-m-r-t PostgreSQL policies; the selector does not grant or mutate
188
+ database privileges at runtime.
package/dist/index.d.ts CHANGED
@@ -1,10 +1,22 @@
1
+ import { ApplicationRuntimeProfile } from '@happyvertical/smrt-config';
2
+ import { AssetStorageProvider } from '@happyvertical/smrt-config';
3
+ import { AuthenticationProvider } from '@happyvertical/smrt-config';
1
4
  import { DatabaseInterface } from '@happyvertical/sql';
2
5
  import { ResolvedApplicationRuntime } from '@happyvertical/smrt-config';
3
6
  import { RuntimeProviderOverrides } from '@happyvertical/smrt-config';
7
+ import { ScheduleRunner } from '@happyvertical/smrt-jobs';
8
+ import { ScheduleRunnerConfig } from '@happyvertical/smrt-jobs';
9
+ import { SecretProvider } from '@happyvertical/smrt-config';
4
10
  import { SessionService } from '@happyvertical/smrt-users';
5
11
  import { TaskRunner } from '@happyvertical/smrt-jobs';
6
12
  import { TaskRunnerConfig } from '@happyvertical/smrt-jobs';
7
13
 
14
+ declare const CAPABILITY_IDS: readonly ["asset-storage", "authentication", "background-jobs", "database", "paid-capabilities", "secret-storage"];
15
+
16
+ declare type CapabilityId = (typeof CAPABILITY_IDS)[number];
17
+
18
+ declare type CapabilityStatus = 'available' | 'disabled' | 'unavailable' | 'unknown';
19
+
8
20
  export declare interface ClaimLocalOwnerInput {
9
21
  token: string;
10
22
  name: string;
@@ -14,6 +26,136 @@ export declare interface ClaimLocalOwnerInput {
14
26
  ipAddress?: string;
15
27
  }
16
28
 
29
+ export declare interface DeployedApplicationRuntime {
30
+ readonly db: DatabaseInterface;
31
+ readonly resolvedRuntime: ResolvedApplicationRuntime;
32
+ restoreSession(sessionId: string): ReturnType<SessionService['loadSessionContext']>;
33
+ createTaskWorker(config?: TaskRunnerConfig): Promise<TaskRunner>;
34
+ createScheduleWorker(config?: ScheduleRunnerConfig): Promise<ScheduleRunner>;
35
+ diagnostics(): DeployedRuntimeDiagnostics;
36
+ health(): DeployedRuntimeHealth;
37
+ readiness(): Promise<DeployedRuntimeReadiness>;
38
+ close(): Promise<void>;
39
+ }
40
+
41
+ export declare interface DeployedApplicationRuntimeOptions {
42
+ readonly profile: DeployedApplicationRuntimeProfile;
43
+ /** Validated selector overrides; never put credentials in this object. */
44
+ readonly providers?: RuntimeProviderOverrides;
45
+ readonly database: DeployedDatabaseBinding;
46
+ readonly authentication: DeployedProviderBinding<PublicAuthenticationProvider>;
47
+ readonly assets: DeployedProviderBinding<AssetStorageProvider>;
48
+ readonly secrets: DeployedProviderBinding<SecretProvider>;
49
+ /** Explicit, idempotent application migration hook. */
50
+ readonly prepareDatabase?: (db: DatabaseInterface) => Promise<void>;
51
+ }
52
+
53
+ export declare type DeployedApplicationRuntimeProfile = Exclude<ApplicationRuntimeProfile, 'local'>;
54
+
55
+ export declare interface DeployedComponentReadiness {
56
+ readonly status: 'ready' | 'not-ready';
57
+ }
58
+
59
+ /** PostgreSQL connection factory. URLs and credentials stay in this adapter. */
60
+ export declare interface DeployedDatabaseBinding {
61
+ readonly engine: 'postgres';
62
+ readonly connect: () => Promise<DatabaseInterface>;
63
+ readonly readiness?: (db: DatabaseInterface) => Promise<void>;
64
+ /**
65
+ * Provider-owned cleanup boundary, required before a connection is opened.
66
+ * Keeping cleanup on the binding also covers malformed connection handles.
67
+ */
68
+ readonly close: (db: DatabaseInterface) => Promise<void>;
69
+ }
70
+
71
+ /**
72
+ * A provider-owned readiness boundary.
73
+ *
74
+ * The callback should prove that required configuration is present and that
75
+ * the provider is usable. It must not return credentials or secret values.
76
+ */
77
+ export declare interface DeployedProviderBinding<Provider extends string> {
78
+ readonly provider: Provider;
79
+ readonly readiness: () => Promise<void>;
80
+ }
81
+
82
+ export declare interface DeployedProviderDiagnostic {
83
+ readonly provider: string;
84
+ readonly configured: true;
85
+ }
86
+
87
+ /** Startup failure that retains the only safe retry path for database cleanup. */
88
+ export declare class DeployedRuntimeCleanupError extends DeployedRuntimeError {
89
+ private readonly cleanup;
90
+ private cleaned;
91
+ private cleanupAttempt?;
92
+ constructor(cleanup: () => Promise<void>);
93
+ retryCleanup(): Promise<void>;
94
+ }
95
+
96
+ export declare type DeployedRuntimeComponent = 'database' | 'authentication' | 'assets' | 'secrets';
97
+
98
+ export declare interface DeployedRuntimeDiagnostics {
99
+ readonly schemaVersion: 1;
100
+ readonly runtime: ResolvedApplicationRuntime;
101
+ readonly providers: {
102
+ readonly database: DeployedProviderDiagnostic;
103
+ readonly authentication: DeployedProviderDiagnostic;
104
+ readonly assets: DeployedProviderDiagnostic;
105
+ readonly secrets: DeployedProviderDiagnostic;
106
+ };
107
+ readonly tenancy: {
108
+ readonly mode: 'single-tenant' | 'multi-tenant';
109
+ readonly context: 'defaulted' | 'required';
110
+ readonly isolation: 'application' | 'database-rls';
111
+ readonly rootTenantFallback: 'disabled';
112
+ };
113
+ readonly workers: {
114
+ readonly topology: 'external' | 'scalable';
115
+ readonly taskProcess: 'separate';
116
+ readonly scheduleProcess: 'separate';
117
+ readonly replicas: 'operator-managed' | 'horizontal';
118
+ };
119
+ readonly secretValuesIncluded: false;
120
+ }
121
+
122
+ /** Stable failure that never includes a provider's underlying error text. */
123
+ export declare class DeployedRuntimeError extends Error {
124
+ readonly code: DeployedRuntimeErrorCode;
125
+ readonly component?: DeployedRuntimeComponent | undefined;
126
+ constructor(code: DeployedRuntimeErrorCode, message: string, component?: DeployedRuntimeComponent | undefined);
127
+ }
128
+
129
+ export declare type DeployedRuntimeErrorCode = 'invalid_configuration' | 'provider_mismatch' | 'provider_unavailable' | 'tenant_context_required' | 'tenant_context_unauthorized' | 'runtime_stopped';
130
+
131
+ export declare interface DeployedRuntimeHealth {
132
+ readonly schemaVersion: 1;
133
+ readonly profile: DeployedApplicationRuntimeProfile;
134
+ readonly status: 'healthy' | 'stopped';
135
+ }
136
+
137
+ export declare interface DeployedRuntimeReadiness {
138
+ readonly schemaVersion: 1;
139
+ readonly profile: DeployedApplicationRuntimeProfile;
140
+ readonly status: 'ready' | 'not-ready';
141
+ readonly components: Readonly<Record<DeployedRuntimeComponent, DeployedComponentReadiness>>;
142
+ readonly secretValuesIncluded: false;
143
+ }
144
+
145
+ /** Encode a package/project identity into a collision-resistant runtime ID. */
146
+ export declare function encodeApplicationId(value: string): string;
147
+
148
+ declare const ERROR_CODES: readonly ["authentication_unavailable", "database_unavailable", "migration_required", "provider_unavailable", "runtime_stopped", "worker_stale"];
149
+
150
+ /**
151
+ * Initialize a self-hosted or cloud application composition.
152
+ *
153
+ * Configuration and provider identity are validated before a connection is
154
+ * opened. Provider readiness and database migrations complete before the
155
+ * runtime is returned. A failed initialization closes the acquired connection.
156
+ */
157
+ export declare function initializeDeployedApplicationRuntime(options: DeployedApplicationRuntimeOptions): Promise<DeployedApplicationRuntime>;
158
+
17
159
  /**
18
160
  * Initialize or reopen a local application runtime.
19
161
  *
@@ -132,6 +274,25 @@ export declare interface LocalRuntimePaths {
132
274
  readonly applicationSecret: string;
133
275
  }
134
276
 
277
+ export declare const MAX_RUNTIME_DIAGNOSTIC_ERRORS = 8;
278
+
279
+ export declare const MAX_RUNTIME_DIAGNOSTIC_TOOLS = 64;
280
+
281
+ /**
282
+ * Claim the secure local database root before a standalone migration command
283
+ * opens SQLite. This does not create application schema or bootstrap records.
284
+ */
285
+ export declare function prepareLocalDatabaseStorage(options: ResolveLocalRuntimePathsOptions): Promise<LocalRuntimePaths>;
286
+
287
+ /**
288
+ * Project private local/deployed state into the deterministic public contract.
289
+ * Unknown, oversized, nested, accessor-backed, or prohibited values are never
290
+ * copied. Invalid error codes become `runtime_error` at ingestion.
291
+ */
292
+ export declare function projectRuntimeDiagnostics(input: RuntimeDiagnosticsProjectionInput): RuntimeDiagnostics;
293
+
294
+ export declare type PublicAuthenticationProvider = Exclude<AuthenticationProvider, 'owner-bootstrap'>;
295
+
135
296
  /**
136
297
  * Resolve deterministic user-owned paths and reject source-tree placement.
137
298
  */
@@ -152,4 +313,72 @@ export declare interface ResolveLocalRuntimePathsOptions {
152
313
  env?: NodeJS.ProcessEnv;
153
314
  }
154
315
 
316
+ export declare const RUNTIME_DIAGNOSTICS_SCHEMA_VERSION: 1;
317
+
318
+ export declare const RUNTIME_WORKER_FRESHNESS_SECONDS = 120;
319
+
320
+ declare type RuntimeDiagnosticErrorCode = (typeof ERROR_CODES)[number] | 'runtime_error';
321
+
322
+ /**
323
+ * Versioned public diagnostics returned only after application authorization.
324
+ * Every field is a fixed allowlist projection; this is intentionally not a
325
+ * cleaned or serialized form of either private runtime diagnostics object.
326
+ */
327
+ export declare interface RuntimeDiagnostics {
328
+ readonly schemaVersion: 1;
329
+ readonly profile: ApplicationRuntimeProfile | 'unknown';
330
+ readonly health: 'healthy' | 'degraded' | 'stopped' | 'unknown';
331
+ readonly schema: {
332
+ readonly status: 'ready' | 'not-ready' | 'unknown';
333
+ readonly migrations: 'current' | 'failed' | 'pending' | 'unknown';
334
+ };
335
+ readonly capabilities: readonly {
336
+ readonly id: CapabilityId;
337
+ readonly status: CapabilityStatus;
338
+ }[];
339
+ readonly tools: {
340
+ readonly names: readonly string[];
341
+ readonly count: number;
342
+ readonly digest: string;
343
+ };
344
+ readonly operationalDifferences: {
345
+ readonly backgroundJobs: 'disabled' | 'enabled';
346
+ readonly workerTopology: WorkerTopology;
347
+ };
348
+ readonly worker: {
349
+ readonly topology: WorkerTopology;
350
+ readonly liveness: 'alive' | 'not-required' | 'stale' | 'unknown';
351
+ readonly heartbeatAt: string | null;
352
+ readonly observedAt: string;
353
+ };
354
+ readonly recentErrors: readonly {
355
+ readonly code: RuntimeDiagnosticErrorCode;
356
+ readonly at: string;
357
+ }[];
358
+ }
359
+
360
+ export declare interface RuntimeDiagnosticsProjectionInput {
361
+ readonly profile?: unknown;
362
+ readonly health?: unknown;
363
+ readonly schema?: unknown;
364
+ readonly capabilities?: unknown;
365
+ readonly toolNames?: unknown;
366
+ readonly worker?: unknown;
367
+ readonly recentErrors?: unknown;
368
+ /** Required explicit clock seam; the projector never reads the process clock. */
369
+ readonly observedAt?: unknown;
370
+ }
371
+
372
+ /** Validate an explicit runtime identity. */
373
+ export declare function validateApplicationId(value: string): string;
374
+
375
+ /**
376
+ * Verify an already-initialized local database root without creating or
377
+ * changing any filesystem/database artifact. Operator read/copy/import paths
378
+ * use this boundary before opening SQLite outside the runtime bootstrap.
379
+ */
380
+ export declare function validateLocalDatabaseStorage(options: ResolveLocalRuntimePathsOptions): Promise<LocalRuntimePaths>;
381
+
382
+ declare type WorkerTopology = 'embedded' | 'external' | 'inline' | 'scalable';
383
+
155
384
  export { }