@objectstack/runtime 4.0.4 → 4.1.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/dist/index.d.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  import { ObjectKernel, IHttpServer, ObjectKernelConfig, Plugin, PluginContext, RouteHandler, Middleware } from '@objectstack/core';
2
2
  export * from '@objectstack/core';
3
3
  export { ObjectKernel } from '@objectstack/core';
4
+ import { z } from 'zod';
5
+ import * as Contracts from '@objectstack/spec/contracts';
4
6
  import { ISeedLoaderService, IDataEngine, IMetadataService } from '@objectstack/spec/contracts';
5
- import { SeedLoaderRequest, SeedLoaderResult, ObjectDependencyGraph, Dataset, SeedLoaderConfigInput } from '@objectstack/spec/data';
7
+ import { SeedLoaderRequest, SeedLoaderResult, ObjectDependencyGraph, Dataset, SeedLoaderConfigInput, ExpressionBody, ScriptBody, HookBody, Hook } from '@objectstack/spec/data';
8
+ import { ExecutionContext } from '@objectstack/spec/kernel';
6
9
  import { MiddlewareConfig, MiddlewareType } from '@objectstack/spec/system';
10
+ import { ProjectArtifact } from '@objectstack/spec/cloud';
7
11
  export { RestApiPluginConfig, RestServer, RouteEntry, RouteGroupBuilder, RouteManager, createRestApiPlugin } from '@objectstack/rest';
8
12
 
9
13
  interface RuntimeConfig {
@@ -51,6 +55,75 @@ declare class Runtime {
51
55
  getKernel(): ObjectKernel;
52
56
  }
53
57
 
58
+ declare const StandaloneStackConfigSchema: z.ZodObject<{
59
+ databaseUrl: z.ZodOptional<z.ZodString>;
60
+ databaseAuthToken: z.ZodOptional<z.ZodString>;
61
+ databaseDriver: z.ZodOptional<z.ZodEnum<{
62
+ sqlite: "sqlite";
63
+ turso: "turso";
64
+ memory: "memory";
65
+ postgres: "postgres";
66
+ mongodb: "mongodb";
67
+ }>>;
68
+ projectId: z.ZodOptional<z.ZodString>;
69
+ artifactPath: z.ZodOptional<z.ZodString>;
70
+ }, z.core.$strip>;
71
+ type StandaloneStackConfig = z.input<typeof StandaloneStackConfigSchema>;
72
+ interface StandaloneStackResult {
73
+ plugins: any[];
74
+ api: {
75
+ enableProjectScoping: false;
76
+ projectResolution: 'none';
77
+ };
78
+ /**
79
+ * Top-level metadata copied from the loaded artifact bundle (when an
80
+ * artifact was successfully loaded). These are surfaced so callers
81
+ * that wrap this result as a `defineStack()`-shaped config (e.g. the
82
+ * CLI's `serve` command without a host `objectstack.config.ts`) can
83
+ * still drive tier resolution, capability detection and driver
84
+ * auto-registration off the artifact's declarations.
85
+ */
86
+ requires?: string[];
87
+ objects?: any[];
88
+ manifest?: any;
89
+ }
90
+ declare function createStandaloneStack(config?: StandaloneStackConfig): Promise<StandaloneStackResult>;
91
+
92
+ interface DefaultHostConfigOptions extends StandaloneStackConfig {
93
+ /**
94
+ * When true (the default), throws if no artifact source can be
95
+ * resolved (no explicit `artifactPath`, no `OS_ARTIFACT_PATH` env,
96
+ * and `<cwd>/dist/objectstack.json` does not exist).
97
+ *
98
+ * Set to false to allow booting an empty kernel — useful for tests
99
+ * that want to assemble plugins manually after the stack is built.
100
+ */
101
+ requireArtifact?: boolean;
102
+ }
103
+ type DefaultHostConfigResult = StandaloneStackResult;
104
+ /**
105
+ * Resolve the artifact source for a default-host boot.
106
+ *
107
+ * Returns the explicit override, then `OS_ARTIFACT_PATH`, then the
108
+ * canonical `<cwd>/dist/objectstack.json` if it exists on disk.
109
+ * Returns `undefined` if none of these are available.
110
+ *
111
+ * URLs (`http(s)://`) are returned as-is — they are validated lazily by
112
+ * the loader, since we cannot stat a remote resource cheaply.
113
+ */
114
+ declare function resolveDefaultArtifactPath(explicitPath?: string, cwd?: string): string | undefined;
115
+ /**
116
+ * Build a `defineStack()`-shaped config from an `objectstack build`
117
+ * artifact, with no `objectstack.config.ts` required.
118
+ *
119
+ * @example
120
+ * // packages/cli/src/commands/serve.ts
121
+ * if (!fs.existsSync(absolutePath)) {
122
+ * config = await createDefaultHostConfig();
123
+ * }
124
+ */
125
+ declare function createDefaultHostConfig(options?: DefaultHostConfigOptions): Promise<DefaultHostConfigResult>;
126
+
54
127
  /**
55
128
  * Driver Plugin
56
129
  *
@@ -65,16 +138,45 @@ declare class Runtime {
65
138
  * const driverPlugin = new DriverPlugin(memoryDriver, 'memory');
66
139
  * kernel.use(driverPlugin);
67
140
  */
141
+ interface DriverPluginOptions {
142
+ /**
143
+ * If set, registers a named datasource so packages declaring
144
+ * `defaultDatasource: '<name>'` resolve to this driver.
145
+ */
146
+ datasourceName?: string;
147
+ /**
148
+ * If `true` (default), registers this driver as the `default` datasource
149
+ * when none exists. Set to `false` for proxy drivers (e.g. cloud proxy)
150
+ * that should never become the default.
151
+ */
152
+ registerAsDefault?: boolean;
153
+ }
68
154
  declare class DriverPlugin implements Plugin {
69
155
  name: string;
70
156
  type: string;
71
157
  version: string;
72
158
  private driver;
73
- constructor(driver: any, driverName?: string);
159
+ private options;
160
+ constructor(driver: any, driverNameOrOptions?: string | DriverPluginOptions, options?: DriverPluginOptions);
74
161
  init: (ctx: PluginContext) => Promise<void>;
75
162
  start: (ctx: PluginContext) => Promise<void>;
76
163
  }
77
164
 
165
+ /**
166
+ * Optional per-project context attached when AppPlugin is instantiated by the
167
+ * project kernel factory. Required for the `app:registered` / `app:unregistered`
168
+ * hooks that drive the org-scoped `sys_app` catalog. Standalone (single-tenant)
169
+ * usages may omit this — no catalog hooks are emitted in that case.
170
+ */
171
+ interface AppPluginProjectContext {
172
+ projectId: string;
173
+ organizationId: string;
174
+ projectName?: string;
175
+ /** When the app comes from a package installation, the source package id. */
176
+ packageId?: string;
177
+ /** Defaults to 'package' when packageId is set, otherwise 'user'. */
178
+ source?: 'package' | 'user';
179
+ }
78
180
  /**
79
181
  * AppPlugin
80
182
  *
@@ -90,9 +192,18 @@ declare class AppPlugin implements Plugin {
90
192
  type: string;
91
193
  version?: string;
92
194
  private bundle;
93
- constructor(bundle: any);
195
+ private projectContext?;
196
+ constructor(bundle: any, projectContext?: AppPluginProjectContext);
94
197
  init: (ctx: PluginContext) => Promise<void>;
95
198
  start: (ctx: PluginContext) => Promise<void>;
199
+ stop: (ctx: PluginContext) => Promise<void>;
200
+ /**
201
+ * Emit a kernel hook so the control-plane `AppCatalogService` can
202
+ * upsert / delete the corresponding `sys_app` row. Silently no-ops
203
+ * when no project context is attached (standalone single-tenant mode)
204
+ * or when the kernel has no `trigger` API available.
205
+ */
206
+ private emitCatalogEvent;
96
207
  /**
97
208
  * Auto-load i18n translation bundles from the app config into the
98
209
  * kernel's i18n service. Handles both `translations` (array of
@@ -103,6 +214,34 @@ declare class AppPlugin implements Plugin {
103
214
  */
104
215
  private loadTranslations;
105
216
  }
217
+ /** Collect declarative `Hook` definitions from a bundle (top-level + manifest). */
218
+ declare function collectBundleHooks(bundle: any): any[];
219
+ /**
220
+ * Collect declarative actions from the bundle. Walks both root-level
221
+ * `actions[]` and per-object `objects[*].actions[]`, attaching the parent
222
+ * object name where applicable so `engine.registerAction(object, name, ...)`
223
+ * sees the correct routing key.
224
+ *
225
+ * Each returned record is a shallow copy with `object` set when the action
226
+ * originated under an object (and not already present on the action itself).
227
+ */
228
+ declare function collectBundleActions(bundle: any): Array<{
229
+ name: string;
230
+ object?: string;
231
+ body?: unknown;
232
+ type?: string;
233
+ [k: string]: unknown;
234
+ }>;
235
+ /**
236
+ * Collect a name → handler map from `bundle.functions`. Accepted shapes:
237
+ *
238
+ * - `{ functions: { foo: fn, bar: fn } }` ← preferred map form
239
+ * - `{ functions: [{ name: 'foo', handler: fn }] }` ← array of records
240
+ *
241
+ * String-named hook handlers (`Hook.handler: 'foo'`) are resolved against
242
+ * this map (and the engine's persistent function registry).
243
+ */
244
+ declare function collectBundleFunctions(bundle: any): Record<string, (ctx: any) => any>;
106
245
 
107
246
  interface Logger {
108
247
  info(message: string, meta?: Record<string, any>): void;
@@ -132,6 +271,14 @@ declare class SeedLoaderService implements ISeedLoaderService {
132
271
  private loadDataset;
133
272
  private resolveFromDatabase;
134
273
  private resolveDeferredUpdates;
274
+ /**
275
+ * Seed writes always run as a privileged system context. This bypasses
276
+ * RBAC checks (so seeds can target system tables like `sys_*`) and
277
+ * disables the SecurityPlugin's auto-injection of `organization_id` /
278
+ * `owner_id` — seeds either declare those fields explicitly per
279
+ * record, or are intentionally cross-tenant / global.
280
+ */
281
+ private static readonly SEED_OPTIONS;
135
282
  private writeRecord;
136
283
  /**
137
284
  * Kahn's algorithm for topological sort with cycle detection.
@@ -148,12 +295,408 @@ declare class SeedLoaderService implements ISeedLoaderService {
148
295
  private buildResult;
149
296
  }
150
297
 
298
+ /**
299
+ * Security response headers builder.
300
+ *
301
+ * Returns the conservative defaults every production API server should
302
+ * send on every response. Designed to be merged with route-specific
303
+ * headers by the dispatcher (`sendResult`) so all adapters (Hono,
304
+ * Fastify, Express, Next.js, …) get them uniformly without each one
305
+ * re-implementing helmet.
306
+ *
307
+ * What we DO opinionate:
308
+ * - Content-Security-Policy (api-default: deny everything but self)
309
+ * - Strict-Transport-Security (HSTS, prod-only — TLS is the caller's
310
+ * responsibility; we just emit the header)
311
+ * - X-Content-Type-Options: nosniff
312
+ * - X-Frame-Options: DENY (anti clickjacking)
313
+ * - Referrer-Policy: no-referrer
314
+ * - Permissions-Policy: geolocation=(), camera=(), microphone=()
315
+ * - Cross-Origin-Resource-Policy: same-origin
316
+ *
317
+ * What we DON'T opinionate:
318
+ * - X-XSS-Protection (deprecated)
319
+ * - CORS — that's an app concern, configure separately
320
+ * - CSP for HTML pages — set a different CSP at the SPA host
321
+ *
322
+ * Every header can be overridden or disabled by config.
323
+ */
324
+ interface SecurityHeadersOptions {
325
+ /**
326
+ * Enable HSTS. Set to `true` in production behind TLS. When `false`
327
+ * the Strict-Transport-Security header is omitted.
328
+ * @default false
329
+ */
330
+ hsts?: boolean | {
331
+ /** Max-age in seconds. @default 15552000 (180 days) */
332
+ maxAge?: number;
333
+ includeSubDomains?: boolean;
334
+ preload?: boolean;
335
+ };
336
+ /**
337
+ * Override the Content-Security-Policy header. Pass `false` to omit.
338
+ * @default "default-src 'none'; frame-ancestors 'none'"
339
+ */
340
+ contentSecurityPolicy?: string | false;
341
+ /**
342
+ * Override X-Frame-Options. @default 'DENY'
343
+ */
344
+ frameOptions?: 'DENY' | 'SAMEORIGIN' | false;
345
+ /**
346
+ * Override Referrer-Policy. @default 'no-referrer'
347
+ */
348
+ referrerPolicy?: string | false;
349
+ /**
350
+ * Override Permissions-Policy. Pass `false` to omit.
351
+ * @default 'geolocation=(), camera=(), microphone=(), payment=()'
352
+ */
353
+ permissionsPolicy?: string | false;
354
+ /**
355
+ * Override Cross-Origin-Resource-Policy. @default 'same-origin'
356
+ */
357
+ corp?: 'same-origin' | 'same-site' | 'cross-origin' | false;
358
+ /**
359
+ * Free-form extra headers merged last.
360
+ */
361
+ extra?: Record<string, string>;
362
+ }
363
+ /**
364
+ * Build a header map ready to be `Object.assign`'d into a response.
365
+ * Idempotent and synchronous — safe to call per-request.
366
+ */
367
+ declare function buildSecurityHeaders(opts?: SecurityHeadersOptions): Record<string, string>;
368
+
369
+ /**
370
+ * In-memory token-bucket rate limiter.
371
+ *
372
+ * Designed to be adapter-agnostic — the dispatcher calls `consume(key)`
373
+ * with a request fingerprint (IP, IP+route bucket, or user id) and
374
+ * short-circuits with 429 if the bucket is empty.
375
+ *
376
+ * For production multi-instance deploys, swap the in-memory store via
377
+ * `RateLimitStore`. The shape is intentionally narrow so a Redis-backed
378
+ * implementation is straightforward.
379
+ */
380
+ interface RateLimitDecision {
381
+ allowed: boolean;
382
+ /** Remaining tokens in the bucket after this consume. */
383
+ remaining: number;
384
+ /** Wall-clock ms until next token is available (when not allowed). */
385
+ retryAfterMs: number;
386
+ /** UNIX ms when the limit window resets. */
387
+ resetAt: number;
388
+ }
389
+ interface RateLimitBucketConfig {
390
+ /** Max tokens (bucket capacity). */
391
+ capacity: number;
392
+ /** Tokens added per second. */
393
+ refillPerSec: number;
394
+ /** Optional cost override for the consume operation. @default 1 */
395
+ defaultCost?: number;
396
+ }
397
+ interface BucketState {
398
+ tokens: number;
399
+ /** Last refill timestamp (ms). */
400
+ lastRefill: number;
401
+ }
402
+ /**
403
+ * Storage interface — swap for Redis/Memcached in clustered deploys.
404
+ * Implementations MUST be safe under concurrent access.
405
+ */
406
+ interface RateLimitStore {
407
+ get(key: string): BucketState | undefined;
408
+ set(key: string, state: BucketState): void;
409
+ /** Cleanup hint — implementations may evict idle entries. */
410
+ prune?(olderThanMs: number): void;
411
+ }
412
+ declare class RateLimiter {
413
+ private config;
414
+ private store;
415
+ private now;
416
+ constructor(config: RateLimitBucketConfig, opts?: {
417
+ store?: RateLimitStore;
418
+ now?: () => number;
419
+ });
420
+ /**
421
+ * Attempt to consume `cost` tokens for `key`. Returns a decision
422
+ * describing whether the request should proceed and, if not, how
423
+ * long the caller should wait before retrying.
424
+ */
425
+ consume(key: string, cost?: number): RateLimitDecision;
426
+ /** Force-reset a key (e.g. after a successful auth flow). */
427
+ reset(key: string): void;
428
+ }
429
+ /**
430
+ * Curated default buckets for the three traffic classes ObjectStack
431
+ * dispatches. Conservative — tune via `DispatcherPluginConfig.rateLimit`
432
+ * for your deployment.
433
+ *
434
+ * - auth: 10 req / minute / IP — guards /auth/* against credential
435
+ * stuffing and password-spray.
436
+ * - write: 60 req / minute / IP — POST/PUT/PATCH/DELETE.
437
+ * - read: 600 req / minute / IP — GET, including discovery and
438
+ * metadata.
439
+ *
440
+ * "Per-IP" is just the suggested key shape; the dispatcher constructs
441
+ * the key from `${ip}:${bucket}` so a single noisy IP can saturate
442
+ * one bucket without blocking the others.
443
+ */
444
+ interface RateLimitDefaults {
445
+ auth: RateLimitBucketConfig;
446
+ write: RateLimitBucketConfig;
447
+ read: RateLimitBucketConfig;
448
+ }
449
+ declare const DEFAULT_RATE_LIMITS: RateLimitDefaults;
450
+
451
+ /**
452
+ * Extract a request id from incoming headers, validating shape. If
453
+ * the header is missing or malformed, returns `undefined` and the
454
+ * caller should mint one via {@link generateRequestId}.
455
+ *
456
+ * Header lookup is case-insensitive — adapters normalize differently.
457
+ */
458
+ declare function extractRequestId(headers: unknown): string | undefined;
459
+ /**
460
+ * Mint a fresh request id. Uses `crypto.randomUUID()` when available
461
+ * (Node 16+, modern browsers, edge runtimes); falls back to a
462
+ * timestamp+random suffix otherwise so the function is universally
463
+ * callable.
464
+ *
465
+ * Format is `req_<hex>`; the prefix makes it obvious in logs that the
466
+ * id was minted by this layer (vs. propagated from a client).
467
+ */
468
+ declare function generateRequestId(): string;
469
+ /**
470
+ * Return the incoming request id if valid, otherwise mint one.
471
+ */
472
+ declare function resolveRequestId(headers: unknown, generate?: () => string): string;
473
+ /**
474
+ * Parsed W3C Trace Context. `sampled` reflects the lowest flag bit.
475
+ */
476
+ interface TraceContext {
477
+ traceId: string;
478
+ spanId: string;
479
+ sampled: boolean;
480
+ }
481
+ /**
482
+ * Parse a `traceparent` header value into its W3C fields. Returns
483
+ * `undefined` for malformed input, the all-zero trace/span ids
484
+ * (spec-mandated invalid), or unknown versions.
485
+ */
486
+ declare function parseTraceparent(value: unknown): TraceContext | undefined;
487
+ /**
488
+ * Build the response header equivalent so downstream services
489
+ * continue the trace.
490
+ */
491
+ declare function formatTraceparent(ctx: TraceContext): string;
492
+
493
+ /**
494
+ * Metrics registry contract.
495
+ *
496
+ * The runtime emits metrics via this interface so the host application
497
+ * can plug in whatever metrics backend it wants (Prometheus via
498
+ * `prom-client`, OTel via `@opentelemetry/api-metrics`, StatsD,
499
+ * CloudWatch, etc.) without the framework taking a hard dep on any of
500
+ * them.
501
+ *
502
+ * Naming follows Prometheus conventions:
503
+ * - snake_case names
504
+ * - unit suffix (`_ms`, `_seconds`, `_bytes`, `_total` for counters)
505
+ *
506
+ * Labels are arbitrary string maps; backends should map them to their
507
+ * native label/tag concept. Keep cardinality low — never label by raw
508
+ * url path or user id.
509
+ *
510
+ * All methods are fire-and-forget; implementations MUST NOT throw on
511
+ * the hot path. Use {@link NoopMetricsRegistry} when metrics are
512
+ * disabled.
513
+ */
514
+ interface MetricsRegistry {
515
+ /** Monotonic counter. `value` defaults to 1. */
516
+ counter(name: string, labels?: Record<string, string>, value?: number): void;
517
+ /** Histogram / timing in arbitrary units (typically ms). */
518
+ histogram(name: string, value: number, labels?: Record<string, string>): void;
519
+ /** Point-in-time gauge. */
520
+ gauge(name: string, value: number, labels?: Record<string, string>): void;
521
+ }
522
+ /**
523
+ * No-op metrics registry — the default. Discards every observation.
524
+ * Production deployments should swap this for a real registry; tests
525
+ * can use {@link InMemoryMetricsRegistry} to assert emissions.
526
+ */
527
+ declare class NoopMetricsRegistry implements MetricsRegistry {
528
+ counter(): void;
529
+ histogram(): void;
530
+ gauge(): void;
531
+ }
532
+ /** Recorded metric sample (in-memory registry). */
533
+ interface MetricSample {
534
+ name: string;
535
+ kind: 'counter' | 'histogram' | 'gauge';
536
+ value: number;
537
+ labels: Record<string, string>;
538
+ /** Wall-clock timestamp; useful for ordering assertions in tests. */
539
+ at: number;
540
+ }
541
+ /**
542
+ * In-memory registry used for tests and local inspection. Stores
543
+ * every observation in insertion order; query via the helpers below
544
+ * or read {@link samples} directly.
545
+ *
546
+ * Not intended for production — unbounded growth.
547
+ */
548
+ declare class InMemoryMetricsRegistry implements MetricsRegistry {
549
+ readonly samples: MetricSample[];
550
+ counter(name: string, labels?: Record<string, string>, value?: number): void;
551
+ histogram(name: string, value: number, labels?: Record<string, string>): void;
552
+ gauge(name: string, value: number, labels?: Record<string, string>): void;
553
+ /**
554
+ * Sum of all counter increments matching `name` (and optionally a
555
+ * label subset). Useful in tests: `metrics.totalCounter('http_requests_total', { status: '500' })`.
556
+ */
557
+ totalCounter(name: string, labelMatch?: Record<string, string>): number;
558
+ /**
559
+ * All histogram observations matching `name` (and optionally a
560
+ * label subset), as raw values.
561
+ */
562
+ histogramValues(name: string, labelMatch?: Record<string, string>): number[];
563
+ /** Clear all recorded samples. */
564
+ reset(): void;
565
+ }
566
+ /**
567
+ * Canonical metric names emitted by the runtime. Hosts may rely on
568
+ * these (e.g., to wire alerts) so they are listed here for reference
569
+ * rather than being string literals scattered through call sites.
570
+ */
571
+ declare const RUNTIME_METRICS: {
572
+ /** Counter, labels: method, route, status. */
573
+ readonly httpRequestsTotal: "http_requests_total";
574
+ /** Histogram (ms), labels: method, route. */
575
+ readonly httpRequestDurationMs: "http_request_duration_ms";
576
+ /** Counter, labels: method, route. Incremented when an in-flight handler throws (after the response is sent). */
577
+ readonly httpRequestErrorsTotal: "http_request_errors_total";
578
+ };
579
+
580
+ /**
581
+ * Error reporter contract.
582
+ *
583
+ * Production deployments wire this to Sentry, Datadog APM, Rollbar,
584
+ * etc. The runtime calls {@link ErrorReporter.captureException} when a
585
+ * route handler results in a 5xx response so the host's APM gets the
586
+ * stack trace without each plugin/route needing to import the SDK.
587
+ *
588
+ * Implementations MUST NOT throw — error reporting failures should be
589
+ * swallowed (or at most logged) so the original error reaches the
590
+ * client unmolested.
591
+ *
592
+ * 4xx responses are intentionally NOT captured here. Client errors
593
+ * (validation failures, auth, not-found) flood APM systems with noise
594
+ * and obscure real bugs. If a deployment wants to track them, do it
595
+ * via the metrics counter (`http_requests_total{status="4xx"}`),
596
+ * not error reporting.
597
+ */
598
+ interface ErrorReporter {
599
+ /**
600
+ * Capture a thrown error with optional context. Context typically
601
+ * includes `requestId`, `method`, `route`, `userId`, `orgId`.
602
+ *
603
+ * The reporter is responsible for redacting sensitive fields from
604
+ * `context` (the runtime does not know what is sensitive in the
605
+ * caller's deployment).
606
+ */
607
+ captureException(error: unknown, context?: Record<string, unknown>): void;
608
+ }
609
+ /** No-op reporter — the default. */
610
+ declare class NoopErrorReporter implements ErrorReporter {
611
+ captureException(): void;
612
+ }
613
+ /** Recorded report (in-memory reporter). */
614
+ interface CapturedError {
615
+ error: unknown;
616
+ context: Record<string, unknown>;
617
+ at: number;
618
+ }
619
+ /**
620
+ * In-memory reporter used in tests to assert that error capture was
621
+ * (or was not) invoked for a given request.
622
+ */
623
+ declare class InMemoryErrorReporter implements ErrorReporter {
624
+ readonly captured: CapturedError[];
625
+ captureException(error: unknown, context?: Record<string, unknown>): void;
626
+ reset(): void;
627
+ }
628
+
151
629
  interface DispatcherPluginConfig {
152
630
  /**
153
631
  * API path prefix for all endpoints.
154
632
  * @default '/api/v1'
155
633
  */
156
634
  prefix?: string;
635
+ /**
636
+ * Project-scoping configuration. Must match the REST API
637
+ * `enableProjectScoping` / `projectResolution` fields so AI / automation
638
+ * routes stay in lockstep with /data and /meta.
639
+ *
640
+ * When `enableProjectScoping` is true and `projectResolution` is:
641
+ * - `required` — only `/projects/:projectId/...` variants are registered.
642
+ * - `optional` / `auto` — both unscoped and scoped variants are registered
643
+ * (the scoped handler forwards `req.params.projectId` into context).
644
+ */
645
+ scoping?: {
646
+ enableProjectScoping?: boolean;
647
+ projectResolution?: 'required' | 'optional' | 'auto';
648
+ };
649
+ /**
650
+ * Enforce per-project membership (`sys_project_member`) on scoped
651
+ * data-plane routes. Returns 403 for non-members unless they are
652
+ * staff (platform org) or the project is the well-known system
653
+ * project.
654
+ *
655
+ * Defaults to `true` when `scoping.enableProjectScoping` is enabled;
656
+ * explicitly set to `false` for tests and single-tenant deployments
657
+ * where membership has not been seeded.
658
+ */
659
+ enforceProjectMembership?: boolean;
660
+ /**
661
+ * Security response headers. When provided, every response routed
662
+ * through this plugin gets the headers merged in (route-specific
663
+ * headers still win on conflict).
664
+ *
665
+ * Pass `false` to disable. Pass `true` (or omit) to enable with
666
+ * conservative API-server defaults (CSP=deny-all, XCTO=nosniff,
667
+ * X-Frame-Options=DENY, etc.). Pass an object to customize — see
668
+ * {@link SecurityHeadersOptions}.
669
+ *
670
+ * @default true
671
+ */
672
+ securityHeaders?: boolean | SecurityHeadersOptions;
673
+ /**
674
+ * Observability wiring. All fields optional; defaults are noop
675
+ * (zero overhead, no behavior change).
676
+ *
677
+ * - `metrics`: registry receiving `http_requests_total`,
678
+ * `http_request_duration_ms`, `http_request_errors_total` for
679
+ * every route this plugin mounts. Plug in `prom-client` /
680
+ * `@opentelemetry/api-metrics` / your own adapter.
681
+ *
682
+ * - `errorReporter`: invoked on 5xx responses with the thrown
683
+ * error and `{ requestId, method, route }`. Plug in Sentry /
684
+ * Datadog / Rollbar.
685
+ *
686
+ * - `generateRequestId`: customize the format of minted request
687
+ * ids (default: `req_<uuid>` via `crypto.randomUUID`). The
688
+ * incoming `X-Request-Id` header is honored when present and
689
+ * well-formed, regardless of this setting.
690
+ *
691
+ * - `requestIdHeader`: response header name to echo the id back
692
+ * on. Defaults to `X-Request-Id`.
693
+ */
694
+ observability?: {
695
+ metrics?: MetricsRegistry;
696
+ errorReporter?: ErrorReporter;
697
+ generateRequestId?: () => string;
698
+ requestIdHeader?: string;
699
+ };
157
700
  }
158
701
  /**
159
702
  * Dispatcher Plugin
@@ -177,6 +720,44 @@ interface DispatcherPluginConfig {
177
720
  */
178
721
  declare function createDispatcherPlugin(config?: DispatcherPluginConfig): Plugin;
179
722
 
723
+ /**
724
+ * The well-known UUID for the built-in system project.
725
+ * Kept in lockstep with `ProjectProvisioningService.provisionSystemProject`.
726
+ */
727
+ declare const SYSTEM_PROJECT_ID = "00000000-0000-0000-0000-000000000001";
728
+ interface SystemProjectPluginConfig {
729
+ /**
730
+ * Service name that resolves to a `ProjectProvisioningService`-shaped
731
+ * object. Defaults to `tenant.provisioning` (convention used by
732
+ * `@objectstack/service-tenant`).
733
+ */
734
+ serviceName?: string;
735
+ /**
736
+ * When true, plugin treats a missing provisioning service as an error.
737
+ * Defaults to false — bootstrap is opt-in and must no-op gracefully when
738
+ * the tenant package is not part of the stack.
739
+ */
740
+ strict?: boolean;
741
+ }
742
+ /**
743
+ * System Project Bootstrap Plugin
744
+ *
745
+ * Ensures the built-in system project (well-known UUID
746
+ * {@link SYSTEM_PROJECT_ID}) exists on the control plane the first time the
747
+ * runtime starts. Calls are idempotent — `provisionSystemProject()` returns
748
+ * the existing row when the project has already been created.
749
+ *
750
+ * Register AFTER the tenant service is available so the provisioning service
751
+ * can be resolved from the kernel.
752
+ *
753
+ * @example
754
+ * ```ts
755
+ * kernel.use(tenantPlugin);
756
+ * kernel.use(createSystemProjectPlugin());
757
+ * ```
758
+ */
759
+ declare function createSystemProjectPlugin(config?: SystemProjectPluginConfig): Plugin;
760
+
180
761
  /**
181
762
  * HttpServer - Unified HTTP Server Abstraction
182
763
  *
@@ -261,9 +842,89 @@ declare class HttpServer implements IHttpServer {
261
842
  getMiddlewares(): Middleware[];
262
843
  }
263
844
 
845
+ /**
846
+ * Factory contract for instantiating a per-project {@link ObjectKernel}.
847
+ *
848
+ * Given a `projectId`, the factory is expected to:
849
+ * 1. Read control-plane metadata (`sys_project` + credentials + subscribed packages).
850
+ * 2. Construct a fresh `ObjectKernel` with project-scoped driver + plugins + Apps.
851
+ * 3. Return a **bootstrapped** kernel ready to serve requests.
852
+ */
853
+ interface ProjectKernelFactory {
854
+ create(projectId: string): Promise<ObjectKernel>;
855
+ }
856
+ interface KernelManagerConfig {
857
+ factory: ProjectKernelFactory;
858
+ /** Maximum number of kernels to keep resident. Defaults to 32. */
859
+ maxSize?: number;
860
+ /**
861
+ * Time-to-live (ms). Kernels idle longer than this are evicted on next
862
+ * access. `0` disables TTL expiry. Defaults to 15 minutes.
863
+ */
864
+ ttlMs?: number;
865
+ /**
866
+ * Optional logger (duck-typed). Falls back to `console` when omitted.
867
+ */
868
+ logger?: {
869
+ info?: (...a: any[]) => void;
870
+ warn?: (...a: any[]) => void;
871
+ error?: (...a: any[]) => void;
872
+ };
873
+ }
874
+ /**
875
+ * LRU + TTL cache of per-project {@link ObjectKernel} instances.
876
+ *
877
+ * Implements ADR-0003 multi-kernel scheduling: each project gets an
878
+ * isolated kernel (App/plugin/metadata namespaces) that is lazily built
879
+ * on first request and evicted under memory / idle pressure. Concurrent
880
+ * `getOrCreate()` calls for the same projectId share a single in-flight
881
+ * factory invocation (singleflight).
882
+ */
883
+ declare class KernelManager {
884
+ private readonly factory;
885
+ private readonly maxSize;
886
+ private readonly ttlMs;
887
+ private readonly logger;
888
+ private readonly cache;
889
+ private readonly pending;
890
+ constructor(config: KernelManagerConfig);
891
+ /** Returns the currently cached projectIds (ordered by insertion). */
892
+ keys(): string[];
893
+ /** Cache size for diagnostics. */
894
+ get size(): number;
895
+ /**
896
+ * Resolve or construct the kernel for `projectId`.
897
+ *
898
+ * - Cache hit (fresh): bumps `lastAccess` and returns immediately.
899
+ * - Cache hit (TTL expired): evicts then falls through to factory.
900
+ * - Cache miss: dedupes concurrent callers through `pending`.
901
+ */
902
+ getOrCreate(projectId: string): Promise<ObjectKernel>;
903
+ /**
904
+ * Evict the kernel for `projectId` and invoke `kernel.shutdown()`.
905
+ * No-op when the entry is absent.
906
+ */
907
+ evict(projectId: string): Promise<void>;
908
+ /** Evict all resident kernels. Used on runtime shutdown. */
909
+ evictAll(): Promise<void>;
910
+ private enforceMaxSize;
911
+ }
912
+
913
+ /** Minimal local interface — full ProjectScopeManager was removed in Phase R. */
914
+ interface ProjectScopeManager {
915
+ touch(projectId: string): void;
916
+ }
264
917
  interface HttpProtocolContext {
265
918
  request: any;
266
919
  response?: any;
920
+ projectId?: string;
921
+ dataDriver?: any;
922
+ /**
923
+ * Identity envelope resolved by `resolveExecutionContext` and threaded
924
+ * into every ObjectQL call so the SecurityPlugin middleware can apply
925
+ * RBAC/RLS/FLS. Optional — anonymous requests carry an empty context.
926
+ */
927
+ executionContext?: ExecutionContext;
267
928
  }
268
929
  interface HttpDispatcherResult {
269
930
  handled: boolean;
@@ -274,6 +935,27 @@ interface HttpDispatcherResult {
274
935
  };
275
936
  result?: any;
276
937
  }
938
+ /**
939
+ * Optional configuration passed to the dispatcher constructor. Supports the
940
+ * legacy `enforceProjectMembership` toggle plus the new multi-kernel
941
+ * scheduling hook required by ADR-0003's cloud runtime mode.
942
+ */
943
+ interface HttpDispatcherOptions {
944
+ enforceProjectMembership?: boolean;
945
+ /**
946
+ * Optional {@link KernelManager}. When present, the dispatcher resolves
947
+ * `context.projectId` first and then routes the request against the
948
+ * project's dedicated kernel via `kernelManager.getOrCreate(projectId)`.
949
+ * Requests that fail to resolve a projectId fall through to the
950
+ * constructor-supplied kernel (self-hosted / legacy behavior).
951
+ */
952
+ kernelManager?: KernelManager;
953
+ /**
954
+ * Optional {@link ProjectScopeManager}. When present, `touch(projectId)` is
955
+ * called on every scoped request so idle projects are evicted after TTL.
956
+ */
957
+ scopeManager?: ProjectScopeManager;
958
+ }
277
959
  /**
278
960
  * @deprecated Use `createDispatcherPlugin()` from `@objectstack/runtime` instead.
279
961
  * This class will be removed in v2. Prefer the plugin-based approach:
@@ -284,7 +966,33 @@ interface HttpDispatcherResult {
284
966
  */
285
967
  declare class HttpDispatcher {
286
968
  private kernel;
287
- constructor(kernel: ObjectKernel);
969
+ private defaultKernel;
970
+ private envRegistry?;
971
+ private defaultProject?;
972
+ private kernelManager?;
973
+ private scopeManager?;
974
+ /**
975
+ * When `true`, scoped data-plane routes enforce a
976
+ * `sys_project_member` lookup and return 403 for non-members.
977
+ * Defaults to `true` when a projectId is resolvable — legacy callers
978
+ * can opt out via the third constructor argument (see
979
+ * `DispatcherConfig.enforceProjectMembership`).
980
+ */
981
+ private enforceMembership;
982
+ /**
983
+ * In-memory cache of positive membership checks, keyed by
984
+ * `${projectId}:${userId}`. Entries expire 60 seconds after insertion
985
+ * — a short TTL is acceptable because a user whose access was just
986
+ * revoked sees stale access for at most one minute.
987
+ */
988
+ private membershipCache;
989
+ private static readonly MEMBERSHIP_CACHE_TTL_MS;
990
+ /** Well-known system project id — bypassed for any authenticated user. */
991
+ private static readonly SYSTEM_PROJECT_ID;
992
+ /** Well-known platform org id — members bypass project membership. */
993
+ private static readonly PLATFORM_ORG_ID;
994
+ constructor(kernel: ObjectKernel, envRegistry?: any, options?: HttpDispatcherOptions);
995
+ private resolveDefaultProject;
288
996
  private success;
289
997
  private error;
290
998
  /**
@@ -294,8 +1002,55 @@ declare class HttpDispatcher {
294
1002
  /**
295
1003
  * Direct data service dispatch — replaces broker.call('data.*').
296
1004
  * Tries protocol service first (supports expand/populate), falls back to ObjectQL.
1005
+ *
1006
+ * @param dataDriver - Optional environment-scoped driver to use instead of kernel default
1007
+ * @param scopeId - Optional project ID for scoped service resolution (SharedProjectPlugin mode)
297
1008
  */
298
1009
  private callData;
1010
+ /**
1011
+ * Parse a project UUID out of a scoped URL path such as
1012
+ * `/api/v1/projects/abc-123/data/task` or `/projects/abc-123/meta`.
1013
+ * Returns `undefined` when the path does not match the scoped pattern.
1014
+ */
1015
+ private extractProjectIdFromPath;
1016
+ /**
1017
+ * Resolve environment context for incoming request.
1018
+ *
1019
+ * Precedence:
1020
+ * 0. URL path matches `/projects/:projectId/...` OR request.params.projectId set by router
1021
+ * → envRegistry.resolveById(id)
1022
+ * 1. request.headers.host → envRegistry.resolveByHostname(host)
1023
+ * 2. request.headers['x-project-id'] → envRegistry.resolveById(id)
1024
+ * 3. session.activeEnvironmentId → envRegistry.resolveById(id)
1025
+ * 4. session.activeOrganizationId → find default project → envRegistry.resolveById(id)
1026
+ * 5. single-project default (registered by `createSingleProjectPlugin`)
1027
+ * → envRegistry.resolveById(defaultProject.projectId). Lets bare
1028
+ * `/api/v1/data/...` URLs resolve to the lone project in
1029
+ * `cloudUrl: 'local'` deployments.
1030
+ *
1031
+ * Skip for paths: /auth, /cloud, /health, /discovery (NOT /meta when scoped,
1032
+ * so project-scoped meta routes can resolve their project).
1033
+ */
1034
+ private resolveEnvironmentContext;
1035
+ /**
1036
+ * Check whether the authenticated user is a member of
1037
+ * `context.projectId`. Runs after {@link resolveEnvironmentContext}
1038
+ * and is a no-op when:
1039
+ *
1040
+ * - Membership enforcement is disabled via the constructor.
1041
+ * - The route is control-plane (`/auth/*`, `/cloud/*`, `/health`,
1042
+ * `/discovery`) — already skipped upstream.
1043
+ * - No `projectId` was resolved (e.g. unscoped legacy routes).
1044
+ * - The project is the well-known system project (bypassed so any
1045
+ * authenticated user can read platform metadata).
1046
+ * - The user's active organization is the platform org (staff).
1047
+ *
1048
+ * Positive results are cached for 60 seconds to avoid hitting the
1049
+ * control-plane on every request. A failed check returns a 403
1050
+ * response object that callers should surface directly — no further
1051
+ * dispatch happens.
1052
+ */
1053
+ private enforceProjectMembership;
299
1054
  /**
300
1055
  * Generates the discovery JSON response for the API root.
301
1056
  *
@@ -615,6 +1370,62 @@ declare class HttpDispatcher {
615
1370
  * Uses ObjectQL SchemaRegistry directly (via the 'objectql' service).
616
1371
  */
617
1372
  handlePackages(path: string, method: string, body: any, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1373
+ /**
1374
+ * Cloud / Environment Control-Plane routes.
1375
+ *
1376
+ * - GET /cloud/drivers → list registered ObjectQL drivers (for env provisioning)
1377
+ * - GET /cloud/projects → list
1378
+ * - POST /cloud/projects → provision (driver: memory | turso | <any registered driver>)
1379
+ * - GET /cloud/projects/:id → detail (+ db, credential, membership)
1380
+ * - PATCH /cloud/projects/:id → update displayName / plan / status / isDefault / metadata
1381
+ * - DELETE /cloud/projects/:id[?force=1] → cascade-delete the project (cred/member/package install rows + physical DB)
1382
+ * - DELETE /cloud/organizations/:id → cascade-delete every project (and its DB) for the org, then drop the org
1383
+ * - POST /cloud/projects/:id/retry → re-run provisioning for a failed environment
1384
+ * - POST /cloud/projects/:id/activate → mark as active for session (stub)
1385
+ * - POST /cloud/projects/:id/credentials/rotate → rotate credential
1386
+ * - GET /cloud/projects/:id/members → list members
1387
+ * - GET /cloud/projects/:id/packages → list installed packages
1388
+ * - POST /cloud/projects/:id/packages → install package into env
1389
+ * - GET /cloud/projects/:id/packages/:pkgId → get installation detail
1390
+ * - PATCH /cloud/projects/:id/packages/:pkgId/enable → enable package
1391
+ * - PATCH /cloud/projects/:id/packages/:pkgId/disable → disable package
1392
+ * - DELETE /cloud/projects/:id/packages/:pkgId → uninstall (scope=platform forbidden)
1393
+ * - POST /cloud/projects/:id/packages/:pkgId/upgrade → upgrade to newer version
1394
+ *
1395
+ * Driver binding
1396
+ * --------------
1397
+ * Environments are not tied to any specific driver. At provisioning time the
1398
+ * caller passes `driver` (a short name such as `memory`, `turso`, or any
1399
+ * future `sql` / `postgres` driver). The dispatcher validates the name
1400
+ * against the kernel's registered driver services (`driver.<name>`) and
1401
+ * derives an appropriate placeholder `database_url` for the chosen driver.
1402
+ * If `driver` is omitted, the dispatcher auto-selects the first available
1403
+ * in preference order: turso → memory → any other registered driver.
1404
+ *
1405
+ * Backed by ObjectQL sys_project / sys_project_credential /
1406
+ * sys_project_member tables (registered by
1407
+ * `@objectstack/service-tenant`'s `createTenantPlugin`).
1408
+ * Physical database addressing (database_url, database_driver, etc.)
1409
+ * is stored directly on the sys_project row.
1410
+ */
1411
+ /**
1412
+ * Resolve the calling user id from the request session, if any.
1413
+ * Returns `undefined` for anonymous calls or when auth is not wired up.
1414
+ */
1415
+ private resolveActiveOrganizationId;
1416
+ private resolveCallerUserId;
1417
+ handleCloud(path: string, method: string, body: any, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1418
+ /**
1419
+ * Cascade-delete a project: cred / member / package_installation rows,
1420
+ * then the physical database via the provisioning adapter, then the
1421
+ * `sys_project` row itself. Used by both `DELETE /cloud/projects/:id`
1422
+ * and the org-cascade in `DELETE /cloud/organizations/:id`.
1423
+ *
1424
+ * Idempotent and best-effort: missing rows / unreachable adapters
1425
+ * become warnings rather than hard failures, so a half-provisioned
1426
+ * project can still be cleaned out.
1427
+ */
1428
+ private deleteProjectCascade;
618
1429
  /**
619
1430
  * Handles Storage requests
620
1431
  * path: sub-path after /storage/
@@ -645,8 +1456,11 @@ declare class HttpDispatcher {
645
1456
  private getService;
646
1457
  /**
647
1458
  * Resolve any service by name, supporting async factories.
648
- * Fallback chain: getServiceAsync → getService (sync) → context.getService → services map.
1459
+ * Fallback chain: getServiceAsync(scopeId)getServiceAsync → getService (sync) → context.getService → services map.
649
1460
  * Only returns when a non-null service is found; otherwise falls through to the next step.
1461
+ *
1462
+ * When `scopeId` is provided, tries the SCOPED factory on `defaultKernel` first (SharedProjectPlugin
1463
+ * mode). Falls back to the current `kernel` for singleton / legacy services.
650
1464
  */
651
1465
  private resolveService;
652
1466
  /**
@@ -654,6 +1468,24 @@ declare class HttpDispatcher {
654
1468
  * Tries multiple access patterns since kernel structure varies.
655
1469
  */
656
1470
  private getObjectQLService;
1471
+ /**
1472
+ * Handle action invocation routes (`/actions/...`).
1473
+ *
1474
+ * Dispatches a named, server-registered action handler (registered via
1475
+ * `engine.registerAction(objectName, actionName, handler)`) over HTTP.
1476
+ * Three URL shapes are accepted to keep the client contract flexible:
1477
+ *
1478
+ * - `POST /actions/:object/:action` — record-scoped action
1479
+ * - `POST /actions/:object/:action/:recordId` — record-scoped action with id in URL
1480
+ * - `POST /actions/global/:action` — wildcard ("*") action
1481
+ *
1482
+ * Body shape: `{ recordId?: string, params?: Record<string, unknown> }`.
1483
+ * The handler is invoked with an `ActionContext` of:
1484
+ * `{ record, user, engine, params }`
1485
+ * where `engine` exposes the slimmed CRUD surface used by CRM handlers
1486
+ * (`insert`, `update`, `delete`, `find`).
1487
+ */
1488
+ handleActions(path: string, method: string, body: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
657
1489
  /**
658
1490
  * Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
659
1491
  * Resolves the AI service and its built-in route handlers, then dispatches.
@@ -779,4 +1611,754 @@ declare class MiddlewareManager {
779
1611
  createCompositeMiddleware(): Middleware;
780
1612
  }
781
1613
 
782
- export { AppPlugin, type DispatcherPluginConfig, DriverPlugin, HttpDispatcher, type HttpDispatcherResult, type HttpProtocolContext, HttpServer, MiddlewareManager, Runtime, type RuntimeConfig, SeedLoaderService, createDispatcherPlugin };
1614
+ interface LoadArtifactBundleOptions {
1615
+ /** Optional log tag for warnings (defaults to `[loadArtifactBundle]`). */
1616
+ tag?: string;
1617
+ /** When true, an unwrapped `{ schemaVersion, metadata }` envelope is unwrapped. */
1618
+ unwrapEnvelope?: boolean;
1619
+ /** Optional fetch timeout in ms for `http(s)://` sources (default 15000). */
1620
+ fetchTimeoutMs?: number;
1621
+ }
1622
+ /** Returns true when `pathOrUrl` looks like an `http://` or `https://` URL. */
1623
+ declare function isHttpUrl(pathOrUrl: string): boolean;
1624
+ /**
1625
+ * Read a JSON artifact from either a local file path or an `http(s)://` URL.
1626
+ * Returns the raw text body. Throws on network or filesystem failure.
1627
+ */
1628
+ declare function readArtifactSource(pathOrUrl: string, opts?: {
1629
+ fetchTimeoutMs?: number;
1630
+ }): Promise<string>;
1631
+ declare function loadArtifactBundle(absArtifactPath: string, opts?: LoadArtifactBundleOptions): Promise<any | null>;
1632
+ declare function mergeRuntimeModule(bundle: any, artifactAbsPath: string, tag?: string): Promise<void>;
1633
+
1634
+ /**
1635
+ * Artifact API client.
1636
+ *
1637
+ * HTTP client that talks to the ObjectStack control plane (e.g.
1638
+ * `apps/cloud`) to resolve hostnames to projects and to download a
1639
+ * project's compiled artifact.
1640
+ *
1641
+ * The control plane is expected to expose two endpoints:
1642
+ *
1643
+ * GET {controlPlaneUrl}/api/v1/cloud/resolve-hostname?host={hostname}
1644
+ * → { projectId: string, organizationId?: string, runtime?: ProjectRuntimeConfig }
1645
+ *
1646
+ * GET {controlPlaneUrl}/api/v1/cloud/projects/:projectId/artifact
1647
+ * → ProjectArtifactResponse (ProjectArtifact + optional `runtime` block)
1648
+ *
1649
+ * Both endpoints accept an optional `Authorization: Bearer <apiKey>`.
1650
+ *
1651
+ * Responses are cached in-memory with a TTL so each kernel-manager
1652
+ * miss does not produce an extra HTTP round trip. Concurrent callers
1653
+ * for the same key share a single in-flight promise (singleflight).
1654
+ */
1655
+
1656
+ /**
1657
+ * Per-project runtime config injected by the control plane alongside
1658
+ * the artifact. Carries the physical database URL the runtime should
1659
+ * connect to (this is *not* part of the developer-authored compiled
1660
+ * artifact — the control plane mints it when serving the API).
1661
+ */
1662
+ interface ProjectRuntimeConfig {
1663
+ organizationId?: string;
1664
+ hostname?: string;
1665
+ /** Driver type — e.g. `sqlite`, `postgres`, `turso`, `memory`. */
1666
+ databaseDriver: string;
1667
+ /** Driver-specific connection URL. */
1668
+ databaseUrl: string;
1669
+ /** Optional auth token (e.g. for libSQL/Turso). */
1670
+ databaseAuthToken?: string;
1671
+ /**
1672
+ * Project-level metadata captured by the control plane at create time
1673
+ * (e.g. `ownerSeed`, `orgSeed`). Forwarded to the runtime so cold-boot
1674
+ * seed replay can mirror the cloud org + owner into the project DB
1675
+ * before the user's first SSO callback arrives.
1676
+ */
1677
+ metadata?: Record<string, unknown>;
1678
+ }
1679
+ /**
1680
+ * Hostname resolution response.
1681
+ */
1682
+ interface ResolvedHostname {
1683
+ projectId: string;
1684
+ organizationId?: string;
1685
+ /** Optional runtime config — when present, callers can skip the artifact fetch's runtime block. */
1686
+ runtime?: ProjectRuntimeConfig;
1687
+ }
1688
+ /**
1689
+ * Artifact response wrapping the spec's `ProjectArtifact` envelope plus
1690
+ * an optional `runtime` block carrying the project's database
1691
+ * connection details.
1692
+ */
1693
+ interface ProjectArtifactResponse extends ProjectArtifact {
1694
+ runtime?: ProjectRuntimeConfig;
1695
+ }
1696
+ interface ArtifactApiClientConfig {
1697
+ /** Control-plane base URL (no trailing slash). */
1698
+ controlPlaneUrl: string;
1699
+ /** Optional bearer token. */
1700
+ apiKey?: string;
1701
+ /** Cache TTL in ms. Default: 5 min. */
1702
+ cacheTtlMs?: number;
1703
+ /** Timeout for control-plane HTTP calls in ms. Default: 10s. */
1704
+ requestTimeoutMs?: number;
1705
+ /** Optional fetch override (testing). */
1706
+ fetch?: typeof fetch;
1707
+ /** Optional logger. */
1708
+ logger?: {
1709
+ info?: (...a: any[]) => void;
1710
+ warn?: (...a: any[]) => void;
1711
+ error?: (...a: any[]) => void;
1712
+ };
1713
+ }
1714
+ declare class ArtifactApiClient {
1715
+ private readonly base;
1716
+ private readonly apiKey?;
1717
+ private readonly cacheTtlMs;
1718
+ private readonly requestTimeoutMs;
1719
+ private readonly fetchImpl;
1720
+ private readonly logger;
1721
+ private readonly hostnameCache;
1722
+ private readonly artifactCache;
1723
+ private readonly pendingHostname;
1724
+ private readonly pendingArtifact;
1725
+ constructor(config: ArtifactApiClientConfig);
1726
+ /**
1727
+ * Resolve a hostname to its project. Returns `null` on 404 or
1728
+ * malformed responses. Errors (network / 5xx) are thrown so
1729
+ * upstream callers can retry.
1730
+ */
1731
+ resolveHostname(host: string): Promise<ResolvedHostname | null>;
1732
+ /**
1733
+ * Fetch the compiled artifact for a project.
1734
+ *
1735
+ * When `opts.commit` is set, requests that specific revision via the
1736
+ * existing `?commit=` query param. Different commits are cached
1737
+ * independently (the cache key includes the commit id) so the preview
1738
+ * runtime can hold multiple versions in memory simultaneously.
1739
+ */
1740
+ fetchArtifact(projectId: string, opts?: {
1741
+ commit?: string;
1742
+ }): Promise<ProjectArtifactResponse | null>;
1743
+ /**
1744
+ * Resolve an 8-hex project short id (first 8 hex chars of the UUID,
1745
+ * dashes stripped) to the full projectId. Used by the preview
1746
+ * runtime, which encodes project ids in subdomains.
1747
+ *
1748
+ * Returns `null` on 404 or ambiguity (the control plane returns 409
1749
+ * if the prefix matches more than one project).
1750
+ */
1751
+ lookupProjectByShortId(shortId: string): Promise<{
1752
+ projectId: string;
1753
+ organizationId?: string;
1754
+ } | null>;
1755
+ /**
1756
+ * Fetch the head commit of a branch. Returns the commit id (and the
1757
+ * matching revision row's `published_at` for cache-validity checks).
1758
+ * Reuses the existing `GET /cloud/projects/:id/branches` endpoint.
1759
+ */
1760
+ fetchBranchHead(projectId: string, branchName: string): Promise<{
1761
+ commitId: string;
1762
+ publishedAt?: string | null;
1763
+ } | null>;
1764
+ /** Drop cached entries for a project (and any matching hostname). */
1765
+ invalidate(projectId: string): void;
1766
+ /** Drop everything. Used on shutdown / hot-reload. */
1767
+ clear(): void;
1768
+ private request;
1769
+ private buildHeaders;
1770
+ }
1771
+
1772
+ interface FileArtifactApiClientConfig {
1773
+ /**
1774
+ * Path to a compiled artifact JSON file (`dist/objectstack.json`).
1775
+ * Resolved against `process.cwd()` when relative. Defaults to
1776
+ * `<cwd>/dist/objectstack.json`.
1777
+ */
1778
+ artifactPath?: string;
1779
+ /**
1780
+ * Project id every hostname maps to. Defaults to
1781
+ * `process.env.OS_PROJECT_ID` or `'proj_local'`.
1782
+ */
1783
+ projectId?: string;
1784
+ /**
1785
+ * Organization id surfaced alongside the project. Defaults to
1786
+ * `process.env.OS_ORGANIZATION_ID` or `'org_local'`.
1787
+ */
1788
+ organizationId?: string;
1789
+ /**
1790
+ * Override runtime config. When unset, the client tries to derive
1791
+ * one from the artifact's `datasources` array; if that fails it
1792
+ * falls back to a local-file SQLite DB at
1793
+ * `<cwd>/.objectstack/data/<projectId>.db`.
1794
+ */
1795
+ runtime?: ProjectRuntimeConfig;
1796
+ /**
1797
+ * Reload the artifact on every fetch instead of caching the first
1798
+ * read. Useful when iterating on a project's metadata without
1799
+ * restarting objectos. Defaults to `true` for dev ergonomics.
1800
+ */
1801
+ watch?: boolean;
1802
+ /** Optional logger. */
1803
+ logger?: {
1804
+ info?: (...a: any[]) => void;
1805
+ warn?: (...a: any[]) => void;
1806
+ error?: (...a: any[]) => void;
1807
+ };
1808
+ }
1809
+ declare class FileArtifactApiClient {
1810
+ private readonly artifactPath;
1811
+ private readonly projectId;
1812
+ private readonly organizationId;
1813
+ private readonly overrideRuntime?;
1814
+ private readonly watch;
1815
+ private readonly logger;
1816
+ private cached?;
1817
+ constructor(config?: FileArtifactApiClientConfig);
1818
+ resolveHostname(_host: string): Promise<ResolvedHostname | null>;
1819
+ fetchArtifact(_projectId: string, _opts?: {
1820
+ commit?: string;
1821
+ }): Promise<ProjectArtifactResponse | null>;
1822
+ lookupProjectByShortId(_shortId: string): Promise<{
1823
+ projectId: string;
1824
+ organizationId?: string;
1825
+ } | null>;
1826
+ fetchBranchHead(_projectId: string, _branchName: string): Promise<{
1827
+ commitId: string;
1828
+ publishedAt?: string | null;
1829
+ } | null>;
1830
+ invalidate(_projectId: string): void;
1831
+ clear(): void;
1832
+ private loadArtifact;
1833
+ private readRuntimeFromArtifact;
1834
+ private deriveRuntimeFromMetadata;
1835
+ private defaultLocalSqliteRuntime;
1836
+ }
1837
+
1838
+ interface ObjectOSStackConfig {
1839
+ /**
1840
+ * Control-plane base URL (HTTP) or a sentinel of `'file'` for the
1841
+ * local file-backed dev mode. Required unless `client` is supplied.
1842
+ *
1843
+ * - `http(s)://…` — talk to a real ObjectStack Cloud control plane
1844
+ * over HTTP and resolve hostnames via its `/cloud/*` API.
1845
+ * - `'file'` — load a single project from a local
1846
+ * `dist/objectstack.json` (or `fileConfig.artifactPath`). Every
1847
+ * request, regardless of hostname, resolves to the same project.
1848
+ * Intended for `pnpm dev` / smoke tests where standing up a
1849
+ * separate control plane is overkill.
1850
+ */
1851
+ controlPlaneUrl?: string;
1852
+ /** Optional bearer token for the control-plane API. */
1853
+ controlPlaneApiKey?: string;
1854
+ /**
1855
+ * Override the artifact client entirely. When supplied,
1856
+ * `controlPlaneUrl` is ignored — useful for tests or custom transports.
1857
+ */
1858
+ client?: ArtifactApiClient | FileArtifactApiClient;
1859
+ /** Config for the file-backed mode (used when `controlPlaneUrl === 'file'`). */
1860
+ fileConfig?: FileArtifactApiClientConfig;
1861
+ /** KernelManager LRU size. Default: 32. */
1862
+ kernelCacheSize?: number;
1863
+ /** KernelManager idle TTL (ms). Default: 15 min. */
1864
+ kernelTtlMs?: number;
1865
+ /** EnvironmentDriverRegistry cache TTL (ms). Default: 5 min. */
1866
+ envCacheTtlMs?: number;
1867
+ /** Artifact / hostname response cache TTL (ms). Default: 5 min. */
1868
+ artifactCacheTtlMs?: number;
1869
+ /** API prefix (carried for parity with cloud-stack). Default: /api/v1. */
1870
+ apiPrefix?: string;
1871
+ }
1872
+ interface ObjectOSStackResult {
1873
+ plugins: any[];
1874
+ api: {
1875
+ enableProjectScoping: true;
1876
+ projectResolution: 'auto';
1877
+ requireAuth: true;
1878
+ };
1879
+ }
1880
+ declare function createObjectOSStack(config: ObjectOSStackConfig): Promise<ObjectOSStackResult>;
1881
+
1882
+ /**
1883
+ * Per-project driver registry contract.
1884
+ *
1885
+ * Resolves a project (by hostname or ID) and produces an instantiated
1886
+ * `IDataDriver` bound to that project's physical database. Concrete
1887
+ * implementations sit on top of either:
1888
+ * - the ObjectStack Cloud HTTP API (see {@link ArtifactEnvironmentRegistry})
1889
+ * - a local file artifact (see {@link FileArtifactApiClient} +
1890
+ * {@link ArtifactEnvironmentRegistry})
1891
+ *
1892
+ * The contract was extracted from `@objectstack/service-cloud` in Phase R
1893
+ * so the runtime can express "fetch artifact + boot per-project kernel"
1894
+ * without taking a dependency on the cloud control plane.
1895
+ */
1896
+
1897
+ type IDataDriver$1 = Contracts.IDataDriver;
1898
+ interface EnvironmentDriverRegistry {
1899
+ /** Resolve a project by hostname. Returns `null` when unknown. */
1900
+ resolveByHostname(host: string): Promise<{
1901
+ projectId: string;
1902
+ driver: IDataDriver$1;
1903
+ } | null>;
1904
+ /** Resolve a project's driver by ID. Returns `null` when unknown. */
1905
+ resolveById(projectId: string): Promise<IDataDriver$1 | null>;
1906
+ /**
1907
+ * Look up the cached project row + driver by ID without triggering a
1908
+ * remote/file fetch. Returns the full cached entry when fresh.
1909
+ */
1910
+ peekById(projectId: string): {
1911
+ projectId: string;
1912
+ driver: IDataDriver$1;
1913
+ project: any;
1914
+ } | null;
1915
+ /** Drop cached entries for the given project. */
1916
+ invalidate(projectId: string): void;
1917
+ }
1918
+
1919
+ /**
1920
+ * EnvironmentDriverRegistry implementation that talks to the control plane
1921
+ * over HTTP via {@link ArtifactApiClient}.
1922
+ *
1923
+ * Mirrors {@link DefaultEnvironmentDriverRegistry} from `environment-registry.ts`
1924
+ * but does **not** read from a local control-plane database. Hostname →
1925
+ * projectId resolution and per-project runtime config (database URL /
1926
+ * driver) come from the control plane API.
1927
+ *
1928
+ * The cached `project` payload exposed by `peekById()` is shaped to look
1929
+ * like a `sys_project` row so callers downstream (notably
1930
+ * `ArtifactKernelFactory`) can read `id`, `organization_id`,
1931
+ * `database_url` and `database_driver` without branching.
1932
+ */
1933
+
1934
+ type IDataDriver = Contracts.IDataDriver;
1935
+ interface ArtifactEnvironmentRegistryConfig {
1936
+ client: ArtifactApiClient;
1937
+ /** Cache TTL for resolved drivers in ms. Default: 5 min. */
1938
+ cacheTtlMs?: number;
1939
+ /** Optional logger. */
1940
+ logger?: {
1941
+ info?: (...a: any[]) => void;
1942
+ warn?: (...a: any[]) => void;
1943
+ error?: (...a: any[]) => void;
1944
+ };
1945
+ }
1946
+ declare class ArtifactEnvironmentRegistry implements EnvironmentDriverRegistry {
1947
+ private readonly client;
1948
+ private readonly cacheTTL;
1949
+ private readonly logger;
1950
+ private readonly hostnameCache;
1951
+ private readonly idCache;
1952
+ private readonly pending;
1953
+ constructor(config: ArtifactEnvironmentRegistryConfig);
1954
+ resolveByHostname(host: string): Promise<{
1955
+ projectId: string;
1956
+ driver: IDataDriver;
1957
+ } | null>;
1958
+ resolveById(projectId: string): Promise<IDataDriver | null>;
1959
+ peekById(projectId: string): {
1960
+ projectId: string;
1961
+ driver: IDataDriver;
1962
+ project: any;
1963
+ } | null;
1964
+ invalidate(projectId: string): void;
1965
+ private buildCacheEntry;
1966
+ }
1967
+
1968
+ interface ArtifactKernelFactoryConfig {
1969
+ client: ArtifactApiClient;
1970
+ envRegistry: EnvironmentDriverRegistry;
1971
+ /** Optional logger. */
1972
+ logger?: {
1973
+ info?: (...a: any[]) => void;
1974
+ warn?: (...a: any[]) => void;
1975
+ error?: (...a: any[]) => void;
1976
+ };
1977
+ /** Optional kernel constructor config. */
1978
+ kernelConfig?: ConstructorParameters<typeof ObjectKernel>[0];
1979
+ /**
1980
+ * Base secret used to derive per-project AuthPlugin secrets via
1981
+ * HKDF-style HMAC-SHA256(baseSecret, projectId). Falls back to
1982
+ * `process.env.OS_AUTH_SECRET` / `AUTH_SECRET` at construction time.
1983
+ */
1984
+ authBaseSecret?: string;
1985
+ }
1986
+ declare class ArtifactKernelFactory implements ProjectKernelFactory {
1987
+ private readonly client;
1988
+ private readonly envRegistry;
1989
+ private readonly logger;
1990
+ private readonly kernelConfig?;
1991
+ private readonly authBaseSecret;
1992
+ constructor(config: ArtifactKernelFactoryConfig);
1993
+ create(projectId: string): Promise<ObjectKernel>;
1994
+ }
1995
+
1996
+ /**
1997
+ * AuthProxyPlugin
1998
+ *
1999
+ * Mounts a single `/api/v1/auth/*` wildcard route on the host's Hono server
2000
+ * that forwards every request to the per-project `AuthManager` registered
2001
+ * by `ArtifactKernelFactory`.
2002
+ *
2003
+ * Why a dedicated plugin: AuthPlugin (better-auth) registers its routes by
2004
+ * grabbing the host's `http-server` service from its own `PluginContext`.
2005
+ * In objectos runtime mode AuthPlugin lives on a per-project kernel — it
2006
+ * has no access to the host's HTTP server, so its `kernel:ready` route
2007
+ * registration is a no-op. The dispatcher plugin's wildcard registration
2008
+ * via `IHttpServer.post('/auth/*', …)` proved unreliable in practice,
2009
+ * so this plugin uses Hono's raw app directly (same path AuthPlugin took
2010
+ * historically) which is rock solid.
2011
+ *
2012
+ * Routing:
2013
+ * 1. Resolve the project from the request hostname via `env-registry`.
2014
+ * 2. Acquire the project's kernel via `kernel-manager`.
2015
+ * 3. Look up the `auth` service on that kernel — this is the better-auth
2016
+ * handler injected by `ArtifactKernelFactory`.
2017
+ * 4. Build a Web `Request` using the project's canonical baseUrl and
2018
+ * hand it to the better-auth handler.
2019
+ * 5. Stream the response back through Hono.
2020
+ */
2021
+
2022
+ declare class AuthProxyPlugin implements Plugin {
2023
+ readonly name = "com.objectstack.runtime.auth-proxy";
2024
+ readonly version = "1.0.0";
2025
+ init: (_ctx: PluginContext) => Promise<void>;
2026
+ start: (ctx: PluginContext) => Promise<void>;
2027
+ }
2028
+
2029
+ /**
2030
+ * Provider id used in better-auth's `genericOAuth` and as part of the
2031
+ * callback URL: `/api/v1/auth/oauth2/callback/<PROVIDER_ID>`. Keep stable —
2032
+ * changing it invalidates every registered redirect_uri.
2033
+ */
2034
+ declare const PLATFORM_SSO_PROVIDER_ID = "objectstack-cloud";
2035
+ /**
2036
+ * Derive the per-project OAuth client_id used in `sys_oauth_application`
2037
+ * (cloud side) and {@link genericOAuth} config (project side).
2038
+ */
2039
+ declare function derivePlatformSsoClientId(projectId: string): string;
2040
+ /**
2041
+ * Derive the per-project OAuth client_secret deterministically from the
2042
+ * shared master secret. HMAC-SHA256(baseSecret, 'oauth-client:' + projectId)
2043
+ * yields a 64-char hex string that is:
2044
+ * - stable across container cold-starts (no DB lookup needed)
2045
+ * - independent per project (compromising one does not compromise others)
2046
+ * - rotatable via OS_AUTH_SECRET rotation (invalidates all SSO clients)
2047
+ *
2048
+ * This is the **plaintext** value the RP must present at the token endpoint.
2049
+ * The cloud-side `sys_oauth_application.client_secret` column instead stores
2050
+ * {@link hashPlatformSsoClientSecret}(plaintext) — better-auth's oauth-provider
2051
+ * defaults to `storeClientSecret: 'hashed'` (SHA-256 + base64url) when the JWT
2052
+ * plugin is enabled, and looks up the row by hashing the presented secret.
2053
+ */
2054
+ declare function derivePlatformSsoClientSecret(baseSecret: string, projectId: string): string;
2055
+ /**
2056
+ * Build the redirect_uri better-auth's `genericOAuth` plugin will use
2057
+ * when the project kernel mounts the provider with id
2058
+ * {@link PLATFORM_SSO_PROVIDER_ID}. MUST be one of the URIs registered
2059
+ * on the cloud-side oauth client or the authorization server will reject
2060
+ * the callback with `invalid_request`.
2061
+ */
2062
+ declare function buildPlatformSsoRedirectUri(hostname: string, basePath?: string): string;
2063
+ interface SeedPlatformSsoClientOptions {
2064
+ /**
2065
+ * Cloud control-plane ObjectQL engine. Must expose `find(object, query)`,
2066
+ * `insert(object, data)`, and `update(object, data, {where})`. Both the
2067
+ * `apps/cloud` boot kernel (via `kernel.getService('objectql')`) and the
2068
+ * dispatcher's local `ql` reference satisfy this shape.
2069
+ */
2070
+ ql: {
2071
+ find: (object: string, query: any, opts?: any) => Promise<any>;
2072
+ insert: (object: string, data: any, opts?: any) => Promise<any>;
2073
+ update: (object: string, data: any, where: any, opts?: any) => Promise<any>;
2074
+ };
2075
+ /** Project id (also used to derive client_id + client_secret). */
2076
+ projectId: string;
2077
+ /**
2078
+ * Project hostname (e.g. `acme-crm.objectos.app`). Optional — projects
2079
+ * may be created before a hostname is assigned, in which case no
2080
+ * redirect_uri is registered yet and the row is upserted with an
2081
+ * empty `redirect_uris` array. Calling this function again once the
2082
+ * hostname is known will merge the new URI in.
2083
+ */
2084
+ hostname?: string | null;
2085
+ /** Master secret shared between cloud and project containers. */
2086
+ baseSecret: string;
2087
+ /** Optional logger for diagnostics. */
2088
+ logger?: {
2089
+ info?: (...a: any[]) => void;
2090
+ warn?: (...a: any[]) => void;
2091
+ error?: (...a: any[]) => void;
2092
+ };
2093
+ /** When true, rethrow insert/update errors instead of swallowing them.
2094
+ * Backfill uses this to surface real failures via the admin endpoint. */
2095
+ throwOnError?: boolean;
2096
+ }
2097
+ /**
2098
+ * Idempotently upsert a `sys_oauth_application` row for the given project.
2099
+ * Re-running with the same `projectId` is a no-op (the deterministic
2100
+ * `client_id` is uniquely indexed and the secret derivation is stable).
2101
+ * Re-running with a new `hostname` adds the new redirect_uri to the
2102
+ * existing row's JSON array.
2103
+ */
2104
+ declare function seedPlatformSsoClient(opts: SeedPlatformSsoClientOptions): Promise<void>;
2105
+ interface BackfillPlatformSsoClientsOptions {
2106
+ ql: SeedPlatformSsoClientOptions['ql'];
2107
+ baseSecret: string;
2108
+ logger?: {
2109
+ info?: (...a: any[]) => void;
2110
+ warn?: (...a: any[]) => void;
2111
+ error?: (...a: any[]) => void;
2112
+ };
2113
+ /** Hard cap on rows scanned (default: 1000). */
2114
+ limit?: number;
2115
+ }
2116
+ /**
2117
+ * Scan `sys_project` and ensure every active project has a corresponding
2118
+ * `sys_oauth_application` row. Intended to run once at cloud boot — the
2119
+ * happy path is dominated by the project-create hook
2120
+ * ({@link seedPlatformSsoClient}); the backfill exists so projects
2121
+ * created before this feature shipped also get SSO support without an
2122
+ * out-of-band migration.
2123
+ */
2124
+ declare function backfillPlatformSsoClients(opts: BackfillPlatformSsoClientsOptions): Promise<{
2125
+ scanned: number;
2126
+ seeded: number;
2127
+ alreadyExisted: number;
2128
+ failures: Array<{
2129
+ projectId: string;
2130
+ error: string;
2131
+ }>;
2132
+ }>;
2133
+
2134
+ /**
2135
+ * # Hook & Action Body Sandbox
2136
+ *
2137
+ * Pluggable execution engine for L2 `ScriptBody` payloads coming from
2138
+ * `@objectstack/spec/data` `HookBodySchema`.
2139
+ *
2140
+ * ## Engine choice — quickjs-emscripten
2141
+ *
2142
+ * Two candidates were evaluated:
2143
+ *
2144
+ * | Property | isolated-vm | quickjs-emscripten |
2145
+ * |-------------------------|----------------------------|---------------------------|
2146
+ * | True isolation | ✅ V8 isolate | ✅ separate JS heap |
2147
+ * | Memory limit enforced | ✅ hard cap | ⚠️ soft (engine-level) |
2148
+ * | CPU timeout enforced | ✅ hard kill | ✅ interrupt handler |
2149
+ * | Native dependency | ❌ requires N-API build | ✅ pure WASM |
2150
+ * | Edge runtime support | ❌ Cloudflare/Vercel ban | ✅ runs on every JS host |
2151
+ * | Cold-start cost | ~50ms (native init) | ~100ms (WASM init) |
2152
+ * | Per-invocation overhead | very low | low–medium |
2153
+ *
2154
+ * **Decision:** `quickjs-emscripten`.
2155
+ *
2156
+ * The single biggest constraint for ObjectStack is that `objectos` ships as a
2157
+ * pure-JS runtime so it can run on serverless edges, Cloudflare Workers,
2158
+ * Vercel Edge, Deno Deploy, plus traditional Node servers. `isolated-vm`
2159
+ * disqualifies us from every edge target because of its N-API dependency.
2160
+ * The performance penalty of QuickJS for short hook/action bodies (typically
2161
+ * <1 ms of script logic) is dominated by `ctx.api` round-trips anyway, so the
2162
+ * trade is favorable.
2163
+ *
2164
+ * The engine sits behind the `ScriptRunner` interface — if a host environment
2165
+ * can guarantee node-only deployment we can plug `isolated-vm` later without
2166
+ * touching call sites.
2167
+ */
2168
+
2169
+ /**
2170
+ * Identity / origin information used by the sandbox for diagnostics, capability
2171
+ * gating, and audit logs.
2172
+ */
2173
+ interface ScriptOrigin {
2174
+ /** Whether the body is attached to a Hook or an Action. */
2175
+ kind: 'hook' | 'action';
2176
+ /** Object the hook/action targets, when applicable. */
2177
+ object?: string;
2178
+ /** Hook/Action name, used in error messages and traces. */
2179
+ name: string;
2180
+ }
2181
+ /**
2182
+ * Context object exposed to the script. The shape mirrors `HookContext` /
2183
+ * `ActionContext` from `@objectstack/spec`. The sandbox copies a subset of
2184
+ * these into the isolated heap; capability checks gate which methods are
2185
+ * actually wired up.
2186
+ */
2187
+ interface ScriptContext {
2188
+ input: unknown;
2189
+ previous?: unknown;
2190
+ user?: unknown;
2191
+ session?: unknown;
2192
+ /**
2193
+ * The lifecycle event name the hook is firing for (e.g. `beforeInsert`,
2194
+ * `afterUpdate`). Required for hooks that subscribe to multiple events
2195
+ * and dispatch on event name.
2196
+ */
2197
+ event?: string;
2198
+ /** The object the hook/action targets — surfaces from `HookContext.object`. */
2199
+ object?: string;
2200
+ /**
2201
+ * Action only: the record id passed in the action invocation URL
2202
+ * (`POST /api/v1/actions/:object/:action/:recordId`). Hooks always have
2203
+ * the record on `input` so this stays undefined for them.
2204
+ */
2205
+ recordId?: string;
2206
+ /**
2207
+ * Action only: the record loaded by the dispatcher before the action ran
2208
+ * (when the dispatcher pre-fetches it). May be undefined for actions
2209
+ * declared with `requiresRecord: false` or when no `recordId` was supplied.
2210
+ */
2211
+ record?: unknown;
2212
+ /** Engine-side `result` (only set for after* hooks). */
2213
+ result?: unknown;
2214
+ api?: unknown;
2215
+ log?: {
2216
+ info: (msg: string, data?: unknown) => void;
2217
+ warn: (msg: string, data?: unknown) => void;
2218
+ error: (msg: string, data?: unknown) => void;
2219
+ };
2220
+ crypto?: {
2221
+ randomUUID?: () => string;
2222
+ hash?: (algo: string, data: string | Uint8Array) => Promise<string>;
2223
+ };
2224
+ }
2225
+ /**
2226
+ * Result returned to the caller after script execution.
2227
+ * - For hooks the `value` is typically `undefined` (mutations happen on `ctx`).
2228
+ * - For actions the `value` is the script's return value.
2229
+ */
2230
+ interface ScriptResult {
2231
+ value: unknown;
2232
+ /** Total wall-clock time inside the sandbox, milliseconds. */
2233
+ durationMs: number;
2234
+ /**
2235
+ * Snapshot of `ctx.input` *as observed inside the VM after the script settled*.
2236
+ *
2237
+ * Hooks frequently mutate `ctx.input.x = y` directly without returning a value.
2238
+ * The runner dumps the post-execution `ctx.input` so the host body-runner can
2239
+ * write the mutations back through to the engine's `hookContext.input` (which
2240
+ * is itself usually a flat-record Proxy).
2241
+ *
2242
+ * `undefined` if the dump failed or the script context did not expose `input`.
2243
+ */
2244
+ mutatedInput?: Record<string, unknown>;
2245
+ }
2246
+ interface ScriptRunOptions {
2247
+ origin: ScriptOrigin;
2248
+ /** Hard timeout for this invocation. The smaller of body.timeoutMs and this wins. */
2249
+ timeoutMs?: number;
2250
+ /** Optional abort signal from the surrounding kernel. */
2251
+ signal?: AbortSignal;
2252
+ }
2253
+ /**
2254
+ * The sandbox engine contract. Implementations live under
2255
+ * `packages/runtime/src/sandbox/engines/`.
2256
+ */
2257
+ interface ScriptRunner {
2258
+ /** Execute an L1 expression. Pure, side-effect-free. */
2259
+ evalExpression(body: ExpressionBody, ctx: ScriptContext, opts: ScriptRunOptions): Promise<ScriptResult>;
2260
+ /** Execute an L2 sandboxed JS script body. */
2261
+ runScript(body: ScriptBody, ctx: ScriptContext, opts: ScriptRunOptions): Promise<ScriptResult>;
2262
+ /** Convenience dispatch on the discriminated union. */
2263
+ run(body: HookBody, ctx: ScriptContext, opts: ScriptRunOptions): Promise<ScriptResult>;
2264
+ /** Release any underlying VM resources. */
2265
+ dispose(): Promise<void>;
2266
+ }
2267
+ /**
2268
+ * Default no-op runner — throws on every call. The real engine is injected
2269
+ * during runtime bootstrap once `quickjs-emscripten` is wired in. This stub
2270
+ * lets the rest of the pipeline (loader, dispatcher, type plumbing) compile
2271
+ * and be unit-tested ahead of the engine landing.
2272
+ */
2273
+ declare class UnimplementedScriptRunner implements ScriptRunner {
2274
+ evalExpression(_body: ExpressionBody, _ctx: ScriptContext, _opts: ScriptRunOptions): Promise<ScriptResult>;
2275
+ runScript(_body: ScriptBody, _ctx: ScriptContext, _opts: ScriptRunOptions): Promise<ScriptResult>;
2276
+ run(body: HookBody, ctx: ScriptContext, opts: ScriptRunOptions): Promise<ScriptResult>;
2277
+ dispose(): Promise<void>;
2278
+ }
2279
+
2280
+ interface QuickJSScriptRunnerOptions {
2281
+ /** Default per-invocation timeout for hooks (ms). */
2282
+ hookTimeoutMs?: number;
2283
+ /** Default per-invocation timeout for actions (ms). */
2284
+ actionTimeoutMs?: number;
2285
+ /** Default memory cap in MB. */
2286
+ memoryMb?: number;
2287
+ }
2288
+ declare class QuickJSScriptRunner implements ScriptRunner {
2289
+ private opts;
2290
+ constructor(opts?: QuickJSScriptRunnerOptions);
2291
+ evalExpression(body: ExpressionBody, ctx: ScriptContext, opts: ScriptRunOptions): Promise<ScriptResult>;
2292
+ runScript(body: ScriptBody, ctx: ScriptContext, opts: ScriptRunOptions): Promise<ScriptResult>;
2293
+ run(body: HookBody, ctx: ScriptContext, opts: ScriptRunOptions): Promise<ScriptResult>;
2294
+ dispose(): Promise<void>;
2295
+ /** Pick the smallest of body / opts / engine-default. */
2296
+ private resolveTimeout;
2297
+ private execute;
2298
+ /**
2299
+ * Install ctx onto the VM's globalThis. Each capability is wired in only if
2300
+ * the body declared it; missing methods throw at call-time inside the VM
2301
+ * with a clear diagnostic.
2302
+ *
2303
+ * Host API methods are installed via {@link QuickJSAsyncContext.newAsyncifiedFunction}
2304
+ * so they may return Promises (real ObjectQL `find/count/insert/...` are async).
2305
+ */
2306
+ private installCtx;
2307
+ }
2308
+ declare class SandboxError extends Error {
2309
+ constructor(message: string);
2310
+ }
2311
+
2312
+ /**
2313
+ * Hook & Action Body Runner Factory
2314
+ *
2315
+ * Bridges the metadata-only `Hook.body` / `Action.body` discriminated union
2316
+ * (defined in `@objectstack/spec/data/hook-body.zod`) into an executable
2317
+ * handler registered on the ObjectQL engine.
2318
+ *
2319
+ * The runtime owns this bridge — `objectql` itself never imports the
2320
+ * sandbox engine, so it can stay light enough to embed in tooling and
2321
+ * tests. `AppPlugin` constructs one factory per bundle bind and passes it
2322
+ * through `bindHooksToEngine({ bodyRunner })` for hooks, and walks the
2323
+ * bundle actions to register them via `engine.registerAction`.
2324
+ *
2325
+ * Per-invocation flow when a triggered hook fires:
2326
+ * 1. ObjectQL calls the wrapped handler with its native `(ctx)` arg.
2327
+ * 2. We adapt that engine-context into the sandbox `ScriptContext`
2328
+ * shape and proxy `ctx.api.object(...)` to the running ObjectQL
2329
+ * proxy bound to the current organization/user.
2330
+ * 3. `ScriptRunner.runScript` evaluates the body inside QuickJS with
2331
+ * the declared capabilities + timeout.
2332
+ * 4. After settle, we write back two kinds of mutations to the host
2333
+ * `ctx.input`:
2334
+ * a. `result.mutatedInput` — a snapshot of `ctx.input` taken inside
2335
+ * the VM, used to propagate direct property writes such as
2336
+ * `ctx.input.account_number = 'ABC'`.
2337
+ * b. `result.value` — if the script returned an object, it is
2338
+ * shallow-merged on top of `mutatedInput` as an explicit patch.
2339
+ * Writes go through `Object.assign`, which means the host engine's
2340
+ * flat-record Proxy (installed by `wrapDeclarativeHook`) sees them
2341
+ * via its set trap.
2342
+ */
2343
+
2344
+ interface FactoryOptions {
2345
+ ql: any;
2346
+ appId: string;
2347
+ logger?: any;
2348
+ }
2349
+ declare function hookBodyRunnerFactory(runner: ScriptRunner, opts: FactoryOptions): (hook: Hook) => ((engineCtx: any) => Promise<void>) | undefined;
2350
+ /**
2351
+ * Action body runner factory.
2352
+ *
2353
+ * Returns a handler with the shape ObjectQL's `executeAction` expects:
2354
+ * `(actionCtx) => Promise<unknown>`. The action's return value bubbles up
2355
+ * to the HTTP dispatcher which JSON-serialises it back to the caller.
2356
+ */
2357
+ declare function actionBodyRunnerFactory(runner: ScriptRunner, opts: FactoryOptions): (action: {
2358
+ name: string;
2359
+ body?: unknown;
2360
+ object?: string;
2361
+ timeoutMs?: number;
2362
+ }) => ((actionCtx: any) => Promise<unknown>) | undefined;
2363
+
2364
+ export { AppPlugin, ArtifactApiClient, type ArtifactApiClientConfig, ArtifactEnvironmentRegistry, type ArtifactEnvironmentRegistryConfig, ArtifactKernelFactory, type ArtifactKernelFactoryConfig, AuthProxyPlugin, type BackfillPlatformSsoClientsOptions, type CapturedError, DEFAULT_RATE_LIMITS, type DefaultHostConfigOptions, type DefaultHostConfigResult, type DispatcherPluginConfig, DriverPlugin, type EnvironmentDriverRegistry, type ErrorReporter, FileArtifactApiClient, type FileArtifactApiClientConfig, HttpDispatcher, type HttpDispatcherResult, type HttpProtocolContext, HttpServer, InMemoryErrorReporter, InMemoryMetricsRegistry, KernelManager, type KernelManagerConfig, type LoadArtifactBundleOptions, type MetricSample, type MetricsRegistry, MiddlewareManager, NoopErrorReporter, NoopMetricsRegistry, type ObjectOSStackConfig, type ObjectOSStackResult, PLATFORM_SSO_PROVIDER_ID, type ProjectArtifactResponse, type ProjectKernelFactory, type ProjectRuntimeConfig, QuickJSScriptRunner, type QuickJSScriptRunnerOptions, RUNTIME_METRICS, type RateLimitBucketConfig, type RateLimitDecision, type RateLimitDefaults, type RateLimitStore, RateLimiter, type ResolvedHostname, Runtime, type RuntimeConfig, SYSTEM_PROJECT_ID, SandboxError, type ScriptContext, type ScriptOrigin, type ScriptResult, type ScriptRunOptions, type ScriptRunner, type SecurityHeadersOptions, SeedLoaderService, type SeedPlatformSsoClientOptions, type StandaloneStackConfig, type StandaloneStackResult, type SystemProjectPluginConfig, type TraceContext, UnimplementedScriptRunner, actionBodyRunnerFactory, backfillPlatformSsoClients, buildPlatformSsoRedirectUri, buildSecurityHeaders, collectBundleActions, collectBundleFunctions, collectBundleHooks, createDefaultHostConfig, createDispatcherPlugin, createObjectOSStack, createStandaloneStack, createSystemProjectPlugin, derivePlatformSsoClientId, derivePlatformSsoClientSecret, extractRequestId, formatTraceparent, generateRequestId, hookBodyRunnerFactory, isHttpUrl, loadArtifactBundle, mergeRuntimeModule, parseTraceparent, readArtifactSource, resolveDefaultArtifactPath, resolveRequestId, seedPlatformSsoClient };