@objectstack/runtime 4.0.5 → 4.1.1
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/README.md +62 -0
- package/dist/index.cjs +2786 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +930 -53
- package/dist/index.d.ts +930 -53
- package/dist/index.js +2758 -121
- package/dist/index.js.map +1 -1
- package/package.json +16 -13
package/dist/index.d.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { ObjectKernel, IHttpServer, ObjectKernelConfig, Plugin, PluginContext, R
|
|
|
2
2
|
export * from '@objectstack/core';
|
|
3
3
|
export { ObjectKernel } from '@objectstack/core';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import * as Contracts from '@objectstack/spec/contracts';
|
|
5
6
|
import { ISeedLoaderService, IDataEngine, IMetadataService } from '@objectstack/spec/contracts';
|
|
6
7
|
import { SeedLoaderRequest, SeedLoaderResult, ObjectDependencyGraph, Dataset, SeedLoaderConfigInput, ExpressionBody, ScriptBody, HookBody, Hook } from '@objectstack/spec/data';
|
|
7
8
|
import { ExecutionContext } from '@objectstack/spec/kernel';
|
|
8
9
|
import { MiddlewareConfig, MiddlewareType } from '@objectstack/spec/system';
|
|
10
|
+
import { ProjectArtifact } from '@objectstack/spec/cloud';
|
|
9
11
|
export { RestApiPluginConfig, RestServer, RouteEntry, RouteGroupBuilder, RouteManager, createRestApiPlugin } from '@objectstack/rest';
|
|
10
12
|
|
|
11
13
|
interface RuntimeConfig {
|
|
@@ -73,9 +75,55 @@ interface StandaloneStackResult {
|
|
|
73
75
|
enableProjectScoping: false;
|
|
74
76
|
projectResolution: 'none';
|
|
75
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;
|
|
76
89
|
}
|
|
77
90
|
declare function createStandaloneStack(config?: StandaloneStackConfig): Promise<StandaloneStackResult>;
|
|
78
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
|
+
|
|
79
127
|
/**
|
|
80
128
|
* Driver Plugin
|
|
81
129
|
*
|
|
@@ -247,6 +295,337 @@ declare class SeedLoaderService implements ISeedLoaderService {
|
|
|
247
295
|
private buildResult;
|
|
248
296
|
}
|
|
249
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
|
+
|
|
250
629
|
interface DispatcherPluginConfig {
|
|
251
630
|
/**
|
|
252
631
|
* API path prefix for all endpoints.
|
|
@@ -278,6 +657,46 @@ interface DispatcherPluginConfig {
|
|
|
278
657
|
* where membership has not been seeded.
|
|
279
658
|
*/
|
|
280
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
|
+
};
|
|
281
700
|
}
|
|
282
701
|
/**
|
|
283
702
|
* Dispatcher Plugin
|
|
@@ -423,58 +842,6 @@ declare class HttpServer implements IHttpServer {
|
|
|
423
842
|
getMiddlewares(): Middleware[];
|
|
424
843
|
}
|
|
425
844
|
|
|
426
|
-
/**
|
|
427
|
-
* ProjectScopeManager
|
|
428
|
-
*
|
|
429
|
-
* Replaces KernelManager in shared-kernel mode. Instead of managing full
|
|
430
|
-
* ObjectKernel instances per project, it manages TTL/LRU eviction of
|
|
431
|
-
* SCOPED service instances inside a single shared kernel.
|
|
432
|
-
*
|
|
433
|
-
* The kernel's PluginLoader already stores scoped instances in:
|
|
434
|
-
* scopedServices: Map<scopeId, Map<serviceName, instance>>
|
|
435
|
-
*
|
|
436
|
-
* This class tracks last-access timestamps and calls kernel.clearScope()
|
|
437
|
-
* to release driver connections and metadata caches for idle projects.
|
|
438
|
-
*/
|
|
439
|
-
|
|
440
|
-
interface ProjectScopeManagerConfig {
|
|
441
|
-
/** Shared kernel whose scoped services this manager evicts. */
|
|
442
|
-
kernel: ObjectKernel;
|
|
443
|
-
/** Idle TTL in ms. Scopes not accessed within this window are evicted. Default: 15 min. */
|
|
444
|
-
ttlMs?: number;
|
|
445
|
-
/** Max number of active scopes. LRU eviction when exceeded. Default: 200. */
|
|
446
|
-
maxSize?: number;
|
|
447
|
-
/** Eviction check interval in ms. Default: 5 min. */
|
|
448
|
-
checkIntervalMs?: number;
|
|
449
|
-
}
|
|
450
|
-
declare class ProjectScopeManager {
|
|
451
|
-
private readonly kernel;
|
|
452
|
-
private readonly ttlMs;
|
|
453
|
-
private readonly maxSize;
|
|
454
|
-
private readonly lastAccess;
|
|
455
|
-
private timer?;
|
|
456
|
-
constructor(config: ProjectScopeManagerConfig);
|
|
457
|
-
/**
|
|
458
|
-
* Touch a scope to reset its idle TTL. Call this on every request.
|
|
459
|
-
*/
|
|
460
|
-
touch(scopeId: string): void;
|
|
461
|
-
/**
|
|
462
|
-
* Evict all scopes not accessed within ttlMs.
|
|
463
|
-
*/
|
|
464
|
-
evictIdle(): void;
|
|
465
|
-
/**
|
|
466
|
-
* Evict a specific scope immediately.
|
|
467
|
-
*/
|
|
468
|
-
evict(scopeId: string): void;
|
|
469
|
-
/**
|
|
470
|
-
* Evict all scopes (e.g. on shutdown).
|
|
471
|
-
*/
|
|
472
|
-
evictAll(): void;
|
|
473
|
-
destroy(): void;
|
|
474
|
-
get activeCount(): number;
|
|
475
|
-
private evictLRU;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
845
|
/**
|
|
479
846
|
* Factory contract for instantiating a per-project {@link ObjectKernel}.
|
|
480
847
|
*
|
|
@@ -543,6 +910,10 @@ declare class KernelManager {
|
|
|
543
910
|
private enforceMaxSize;
|
|
544
911
|
}
|
|
545
912
|
|
|
913
|
+
/** Minimal local interface — full ProjectScopeManager was removed in Phase R. */
|
|
914
|
+
interface ProjectScopeManager {
|
|
915
|
+
touch(projectId: string): void;
|
|
916
|
+
}
|
|
546
917
|
interface HttpProtocolContext {
|
|
547
918
|
request: any;
|
|
548
919
|
response?: any;
|
|
@@ -1037,6 +1408,12 @@ declare class HttpDispatcher {
|
|
|
1037
1408
|
* Physical database addressing (database_url, database_driver, etc.)
|
|
1038
1409
|
* is stored directly on the sys_project row.
|
|
1039
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;
|
|
1040
1417
|
handleCloud(path: string, method: string, body: any, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
1041
1418
|
/**
|
|
1042
1419
|
* Cascade-delete a project: cred / member / package_installation rows,
|
|
@@ -1254,6 +1631,506 @@ declare function readArtifactSource(pathOrUrl: string, opts?: {
|
|
|
1254
1631
|
declare function loadArtifactBundle(absArtifactPath: string, opts?: LoadArtifactBundleOptions): Promise<any | null>;
|
|
1255
1632
|
declare function mergeRuntimeModule(bundle: any, artifactAbsPath: string, tag?: string): Promise<void>;
|
|
1256
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
|
+
|
|
1257
2134
|
/**
|
|
1258
2135
|
* # Hook & Action Body Sandbox
|
|
1259
2136
|
*
|
|
@@ -1484,4 +2361,4 @@ declare function actionBodyRunnerFactory(runner: ScriptRunner, opts: FactoryOpti
|
|
|
1484
2361
|
timeoutMs?: number;
|
|
1485
2362
|
}) => ((actionCtx: any) => Promise<unknown>) | undefined;
|
|
1486
2363
|
|
|
1487
|
-
export { AppPlugin, type DispatcherPluginConfig, DriverPlugin, HttpDispatcher, type HttpDispatcherResult, type HttpProtocolContext, HttpServer, type LoadArtifactBundleOptions, MiddlewareManager, QuickJSScriptRunner, type QuickJSScriptRunnerOptions, Runtime, type RuntimeConfig, SYSTEM_PROJECT_ID, SandboxError, type ScriptContext, type ScriptOrigin, type ScriptResult, type ScriptRunOptions, type ScriptRunner, SeedLoaderService, type StandaloneStackConfig, type StandaloneStackResult, type SystemProjectPluginConfig, UnimplementedScriptRunner, actionBodyRunnerFactory, collectBundleActions, collectBundleFunctions, collectBundleHooks, createDispatcherPlugin, createStandaloneStack, createSystemProjectPlugin, hookBodyRunnerFactory, isHttpUrl, loadArtifactBundle, mergeRuntimeModule, readArtifactSource };
|
|
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 };
|