@voltro/runtime 0.11.4 → 0.12.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
@@ -673,6 +673,11 @@ export declare interface ApiKeyRow {
673
673
  readonly keyPrefix: string;
674
674
  readonly scopes: ReadonlyArray<string> | null;
675
675
  readonly lastUsedAt: number | null;
676
+ /** WHO THE KEY ACTS AS — null for an org key. Distinct from `createdBy`. */
677
+ readonly onBehalfOf?: string | null;
678
+ /** Coarse usage counter, buffered — see `apiKeyUsage.ts`. Optional on the type
679
+ * because a row written before the column existed reads back without it. */
680
+ readonly requestCount?: number;
676
681
  readonly expiresAt: number | null;
677
682
  readonly revokedAt: number | null;
678
683
  readonly createdBy: string | null;
@@ -692,10 +697,28 @@ export declare interface ApiKeyServiceShape {
692
697
  readonly resolveByHash: (hash: string, now?: number) => Promise<ResolvedApiKey | null>;
693
698
  /** Verify a raw token (hashes then resolves). */
694
699
  readonly verify: (token: string, now?: number) => Promise<ResolvedApiKey | null>;
695
- /** Revoke a key (irreversible). */
696
- readonly revoke: (id: string, now?: number) => Promise<boolean>;
697
- /** Rotate: revoke the old key + issue a replacement with the same name/scopes/tenant. */
698
- readonly rotate: (id: string, now?: number) => Promise<IssuedApiKey | null>;
700
+ /**
701
+ * Revoke a key (irreversible).
702
+ *
703
+ * `tenantId` is the CALLER's tenant and is REQUIRED, not optional: a key id is
704
+ * the only thing needed to revoke, and ids leak (logs, support tickets, an
705
+ * error message). Without this check an admin of tenant A could revoke tenant
706
+ * B's key by id — the guard on the route asks "is an admin", never "an admin
707
+ * of THIS key's tenant". Returns `false` for a key belonging to anyone else,
708
+ * indistinguishable from "no such key", so it cannot be used to probe.
709
+ *
710
+ * Pass `null` only for the tenant-less system key space. There is deliberately
711
+ * no "skip the check" value: an optional scope on a destructive operation is a
712
+ * scope somebody forgets.
713
+ */
714
+ readonly revoke: (id: string, tenantId: string | null, now?: number) => Promise<boolean>;
715
+ /**
716
+ * Rotate: revoke the old key + issue a replacement with the same
717
+ * name/scopes/tenant. Same required tenant scope as `revoke`, and here it
718
+ * matters more — rotate RETURNS A USABLE TOKEN, so an unscoped version would
719
+ * hand the caller a working credential for another tenant.
720
+ */
721
+ readonly rotate: (id: string, tenantId: string | null, now?: number) => Promise<IssuedApiKey | null>;
699
722
  /** List a tenant's keys (no secrets). */
700
723
  readonly list: (tenantId: string | null) => Promise<ReadonlyArray<PublicApiKey>>;
701
724
  }
@@ -708,6 +731,38 @@ export declare interface ApiKeyStore {
708
731
  readonly patch: (id: string, fields: Partial<ApiKeyRow>) => Promise<void>;
709
732
  }
710
733
 
734
+ export declare interface ApiKeyUsageBuffer {
735
+ /** Record one use. Synchronous, no I/O. */
736
+ readonly touch: (id: string, at?: number) => void;
737
+ /** Write everything pending now. */
738
+ readonly flushNow: () => Promise<void>;
739
+ /** Flush and stop the timer. Idempotent. */
740
+ readonly shutdown: () => Promise<void>;
741
+ /** Pending key count — for tests and the inspect surface. */
742
+ readonly pending: () => number;
743
+ }
744
+
745
+ export declare interface ApiKeyUsageBufferOptions {
746
+ /** Apply one key's accumulated window. Must ADD `requests` to the stored
747
+ * count and move `lastUsedAt` forward — never assign either, or a second
748
+ * replica's window overwrites this one. */
749
+ readonly flush: (id: string, delta: ApiKeyUsageDelta) => Promise<void>;
750
+ /** Flush cadence. Default 30s. */
751
+ readonly intervalMs?: number;
752
+ /** Flush early once this many distinct keys are pending, so a burst across
753
+ * many keys doesn't sit unbounded in memory for a whole window. Default 500. */
754
+ readonly maxPending?: number;
755
+ /** Injectable for tests. */
756
+ readonly now?: () => number;
757
+ }
758
+
759
+ export declare interface ApiKeyUsageDelta {
760
+ /** Requests counted for this key in the flushed window. Always ≥ 1. */
761
+ readonly requests: number;
762
+ /** The latest use observed in the window (epoch ms). */
763
+ readonly lastUsedAt: number;
764
+ }
765
+
711
766
  /**
712
767
  * Typed authorization slice on `ctx.access` — the ergonomic, cast-free face of
713
768
  * the caller's EFFECTIVE scope set (raw subject scopes ∪ rbac role-derived
@@ -731,6 +786,28 @@ export declare interface AppAccess {
731
786
 
732
787
  export declare interface AppContext {
733
788
  readonly store: FluentStore;
789
+ /**
790
+ * The same store, scoped to ONE tenant.
791
+ *
792
+ * For non-request work — a schedule, a subscriber, a workflow step — whose
793
+ * subject is `system` with `tenantId: null`. Reads there see every tenant and
794
+ * a write to a `tenant()` table fails with `TenantScopeViolation`, so a
795
+ * per-tenant cron has to say which tenant it means. Every fan-out cron is
796
+ * literally a loop doing this by hand, and doing it by hand means each
797
+ * `.where('tenantId', t.id)` is one forgotten call away from reading the whole
798
+ * table:
799
+ *
800
+ * for (const t of await ctx.store.select('tenants').all()) {
801
+ * const scoped = ctx.storeForTenant(t.id)
802
+ * await scoped.insert('digests', { … }) // tenantId stamped, not passed
803
+ * }
804
+ *
805
+ * Inside a REQUEST this is almost always the wrong tool: the subject already
806
+ * carries a tenant, and reaching for another one is a cross-tenant read with
807
+ * extra steps. It exists because the system subject has no tenant to infer,
808
+ * not to let a request pick a different one.
809
+ */
810
+ readonly storeForTenant: (tenantId: string) => FluentStore;
734
811
  readonly request: RuntimeContext;
735
812
  /** Typed authorization slice — the caller's effective scopes. Use
736
813
  * `ctx.access.has(scope)` / `yield* ctx.access.require(scope)` instead of
@@ -1560,7 +1637,7 @@ export declare const crud: {
1560
1637
  *
1561
1638
  * export default crud.count('absenceRequests', { filter: (i) => ({ status: i.status }) })
1562
1639
  */
1563
- count: (table: string, options?: Pick<CrudListOptions, "filter">) => (input: unknown, ctx: AppContext) => Promise<number>;
1640
+ count: (table: string, options?: Pick<CrudListOptions, "filter" | "scope">) => (input: unknown, ctx: AppContext) => Promise<number>;
1564
1641
  /** Delete a row by id; returns `{ deleted }`. Guard the DESCRIPTOR. */
1565
1642
  remove: (table: string) => (input: {
1566
1643
  readonly id: string;
@@ -1586,8 +1663,33 @@ export declare interface CrudListOptions extends CrudReadOptions {
1586
1663
  * is simply ignored (`{ employeeId: input.employeeId, status: input.status }`).
1587
1664
  * The descriptor's `input` schema declares those fields; this maps them to a
1588
1665
  * scoped `.where(column, value)` on the store query.
1666
+ *
1667
+ * Request input ONLY, on purpose — see `scope` for the caller-derived half.
1589
1668
  */
1590
1669
  readonly filter?: (input: Readonly<Record<string, unknown>>) => Readonly<Record<string, unknown>>;
1670
+ /**
1671
+ * The caller-derived WHERE — what this subject may SEE, as opposed to what it
1672
+ * asked for. Merged into the same query as `filter`, and it WINS on a key
1673
+ * collision, so a caller cannot widen its own scope by sending that field.
1674
+ *
1675
+ * scope: (ctx) => ({ ownerId: ctx.request.subject.id })
1676
+ *
1677
+ * Why this is a SEPARATE option rather than a second argument to `filter`:
1678
+ * the two answer different questions, and only one of them is a security
1679
+ * boundary. Kept apart, "does this list declare a `scope`?" is a question a
1680
+ * reviewer — or a future boot audit — can actually ask. Folded into `filter`,
1681
+ * it becomes "does this filter happen to read ctx somewhere in its body?",
1682
+ * which nothing can check.
1683
+ *
1684
+ * The gap this closes: tenant scope is applied automatically, but anything
1685
+ * narrower — owner, team, role — was not expressible at all. Replacing a
1686
+ * hand-written handler that carried such a narrowing with `crud.list`
1687
+ * therefore WIDENED the result set, silently and with no error. One app lost
1688
+ * exactly that across eight list views.
1689
+ *
1690
+ * Pass the same `scope` to `crud.count`, or the total contradicts the pages.
1691
+ */
1692
+ readonly scope?: (ctx: AppContext) => Readonly<Record<string, unknown>>;
1591
1693
  /**
1592
1694
  * Page the result from the request. BOTH styles are accepted, so a caller uses
1593
1695
  * whichever its UI thinks in:
@@ -1683,7 +1785,6 @@ export declare interface DataLoader {
1683
1785
 
1684
1786
  export { DataStore }
1685
1787
 
1686
- /** DataStore-backed store over `_voltro_api_keys`. */
1687
1788
  export declare const dataStoreApiKeyStore: (store: DataStore) => ApiKeyStore;
1688
1789
 
1689
1790
  export declare const dataStoreIdempotencyStore: (store: DataStore) => IdempotencyStore;
@@ -1761,7 +1862,7 @@ export declare const defineEventTrigger: <EventPayload = unknown, WorkflowPayloa
1761
1862
  * Runtime identity — returns `fn` unchanged; the whole value is the compile-time
1762
1863
  * check. See the module header for the failure it prevents.
1763
1864
  */
1764
- export declare const defineExecutor: <D extends ExecutorDescriptor, E = never, R = never>(_descriptor: D, fn: (input: ExecutorInput<D>, ctx: AppContext) => ExecutorReturn<D, E, R>) => ((input: ExecutorInput<D>, ctx: AppContext) => ExecutorReturn<D, E, R>);
1865
+ export declare const defineExecutor: <D extends ExecutorDescriptor, F extends (input: ExecutorInput<D>, ctx: AppContext) => ExecutorReturn<D, unknown, unknown>>(_descriptor: D, fn: F) => ((input: ExecutorInput<D>, ctx: AppContext) => ReturnType<F>);
1765
1866
 
1766
1867
  /**
1767
1868
  * Declare who delivers an effect. One per `*.outbox.ts` file.
@@ -2026,6 +2127,10 @@ export declare interface EffectStoreOps {
2026
2127
  * discovered tables. */
2027
2128
  export declare const emptySchemaRegistry: SchemaRegistry;
2028
2129
 
2130
+ /** Turn recording on from code. Idempotent. Must run BEFORE the stores that
2131
+ * should be observed are constructed — `observeStore` decides once, per store. */
2132
+ export declare const enableGraphObservation: () => void;
2133
+
2029
2134
  /** Encrypt a value with the registered field cipher (same one `.encrypted()`
2030
2135
  * columns use). Throws if no cipher is registered. */
2031
2136
  export declare const encryptField: (plaintext: string) => string;
@@ -2545,6 +2650,8 @@ export declare const isIdleNow: (opts: IdleCheckOptions) => Promise<boolean>;
2545
2650
 
2546
2651
  export declare const isInfraError: (e: unknown) => boolean;
2547
2652
 
2653
+ export declare const isObservingGraph: () => boolean;
2654
+
2548
2655
  /**
2549
2656
  * A subject that AUTHENTICATED but resolved with NO active org — a `user` (not
2550
2657
  * `anonymous`/`system`) carrying the empty-string tenant sentinel. Every
@@ -2585,7 +2692,12 @@ export declare interface IssueInput {
2585
2692
  readonly name: string;
2586
2693
  readonly scopes?: ReadonlyArray<string>;
2587
2694
  readonly expiresAt?: Date | number | null;
2695
+ /** WHO MINTED IT. Provenance — set it always, including for an org key. */
2588
2696
  readonly createdBy?: string | null;
2697
+ /** WHO IT ACTS AS. Omit for an ORG key (acts as no person); set it for a
2698
+ * personal key, which may be the minter or — when an admin mints for a
2699
+ * colleague — someone else entirely. */
2700
+ readonly onBehalfOf?: string | null;
2589
2701
  }
2590
2702
 
2591
2703
  export declare const isWorkflowEventTriggerDefinition: (value: unknown) => value is WorkflowEventTriggerDefinition;
@@ -2808,8 +2920,30 @@ export declare const makeAesCipher: (key: Buffer) => FieldCipher;
2808
2920
 
2809
2921
  export declare const makeApiKeyService: (store: ApiKeyStore, opts?: {
2810
2922
  prefix?: string;
2923
+ /** Buffers `lastUsedAt` + `requestCount` instead of writing a row per auth
2924
+ * check. Omit to keep the synchronous stamp (see `resolveByHash`). */
2925
+ usage?: ApiKeyUsageBuffer;
2811
2926
  }) => ApiKeyServiceShape;
2812
2927
 
2928
+ export declare const makeApiKeyUsageBuffer: (options: ApiKeyUsageBufferOptions) => ApiKeyUsageBuffer;
2929
+
2930
+ /**
2931
+ * The flusher a buffered `ApiKeyUsageBuffer` is built with: read-modify-write of
2932
+ * `requestCount` + `lastUsedAt` for one key, once per window.
2933
+ *
2934
+ * Not atomic across replicas, and that is a deliberate limit rather than an
2935
+ * oversight. Two pods flushing the same key in the same instant can lose one
2936
+ * window's delta. Making it exact would need a dialect-specific
2937
+ * `SET requestCount = requestCount + ?`, which is real complexity in service of
2938
+ * a field whose stated purpose is "is this key still in use, and roughly how
2939
+ * much". If this number ever needs to be exact, that is the signal it is being
2940
+ * used for something it was not built for (billing), not a signal to add locking.
2941
+ */
2942
+ export declare const makeApiKeyUsageFlusher: (store: ApiKeyStore) => (id: string, delta: {
2943
+ readonly requests: number;
2944
+ readonly lastUsedAt: number;
2945
+ }) => Promise<void>;
2946
+
2813
2947
  /** Build the `ctx.access` slice for a subject. Reads the effective-scope seam
2814
2948
  * LIVE (via getters/closures) so a scope rbac resolves after context
2815
2949
  * construction is still reflected. Used by the shared AppContext builder AND
@@ -3371,6 +3505,50 @@ export declare interface OAuth2ConnectionDefinition extends ConnectionDefinition
3371
3505
  readonly identify?: (tokens: ConnectionTokens) => Promise<ConnectionAccount>;
3372
3506
  }
3373
3507
 
3508
+ /** One recorded `(procedure → table, op)` edge. */
3509
+ export declare interface ObservedEdge {
3510
+ readonly tag: string;
3511
+ readonly kind: ObservedProcedure['kind'];
3512
+ readonly table: string;
3513
+ readonly op: ObservedOp;
3514
+ }
3515
+
3516
+ export declare interface ObservedGraph {
3517
+ /** Schema version of this file's shape, so `check` can refuse an old one
3518
+ * loudly instead of silently diffing against a different meaning. */
3519
+ readonly version: 1;
3520
+ readonly edges: ReadonlyArray<ObservedEdge>;
3521
+ /** Tags seen entering the recorder at all — the difference between "this
3522
+ * procedure touched no table" and "this procedure never ran", which is the
3523
+ * distinction the whole feature stands on. */
3524
+ readonly exercised: ReadonlyArray<string>;
3525
+ }
3526
+
3527
+ /** Snapshot everything recorded so far. */
3528
+ export declare const observedGraph: () => ObservedGraph;
3529
+
3530
+ /** What a procedure did to a table. `read` covers every query path. */
3531
+ export declare type ObservedOp = 'read' | 'insert' | 'update' | 'delete' | 'upsert';
3532
+
3533
+ export declare interface ObservedProcedure {
3534
+ /** The procedure's effective rpc tag (`notes.create`). */
3535
+ readonly tag: string;
3536
+ readonly kind: 'query' | 'mutation' | 'action' | 'stream' | 'schedule' | 'workflow';
3537
+ }
3538
+
3539
+ /**
3540
+ * Wrap a `DataStore` so every operation is recorded. Returns the store unchanged
3541
+ * when recording is off, so the production path keeps the exact same object
3542
+ * (no added indirection, no added allocation).
3543
+ *
3544
+ * Wrapping the store BENEATH the mixin middleware rather than instrumenting the
3545
+ * middleware's ~20 public methods is deliberate: those methods delegate to each
3546
+ * other (`one`/`first`/`maybeOne` all go through `query`), so instrumenting them
3547
+ * individually would double-count some paths and miss any new one. Underneath,
3548
+ * each operation passes exactly once.
3549
+ */
3550
+ export declare const observeStore: <S extends object>(store: S) => S;
3551
+
3374
3552
  /**
3375
3553
  * Listen for re-bind events. Returns an unsubscribe function. Used by
3376
3554
  * the dispatcher to re-scope active subscriptions when a connection's
@@ -3565,6 +3743,24 @@ export declare interface PatConnectionDefinition extends ConnectionDefinitionCom
3565
3743
  readonly validate?: (token: string) => Promise<ConnectionAccount>;
3566
3744
  }
3567
3745
 
3746
+ /**
3747
+ * TOP-LEVEL property names of a struct-ish schema, split by optionality.
3748
+ *
3749
+ * Deliberately not `schemaPropertyNames` (`./serverOnlyAudit`), which answers a
3750
+ * DIFFERENT question: that one walks the whole schema recursively and flattens
3751
+ * every name at any depth, because a leak check cares whether a column name
3752
+ * appears anywhere in an output. This one asks "what does this payload require
3753
+ * at its top level", so depth would be wrong and optionality is the point.
3754
+ *
3755
+ * Best effort — at runtime it only makes an error readable and never decides
3756
+ * validity — but `voltro doctor`'s static `workflows.start` audit calls the same
3757
+ * function, so the two cannot disagree about what a payload requires.
3758
+ */
3759
+ export declare const payloadPropertyNames: (schema: Schema.Schema.Any) => {
3760
+ readonly required: ReadonlyArray<string>;
3761
+ readonly optional: ReadonlyArray<string>;
3762
+ };
3763
+
3568
3764
  /** The 3-arg signature a plugin sees on its bind-ctx. */
3569
3765
  export declare type PluginScheduleCoordinated = (name: string, intervalMs: number, effect: () => void | Promise<void>) => CoordinatedScheduleHandle;
3570
3766
 
@@ -3855,6 +4051,14 @@ export declare const recordSubscriptionActive: (label: string, delta: number) =>
3855
4051
  * latency + a delivery counter. Rides the framework's existing per-delivery tap. */
3856
4052
  export declare const recordSubscriptionDelivery: (d: SubscriptionDeliveryRecord) => void;
3857
4053
 
4054
+ /**
4055
+ * Record one table access against the procedure on the current fiber. Silently
4056
+ * ignored outside a procedure scope — background work (migrations, the
4057
+ * scheduler's own bookkeeping, plugin boot) is not a procedure and must not
4058
+ * invent edges for one.
4059
+ */
4060
+ export declare const recordTableAccess: (table: string, op: ObservedOp) => void;
4061
+
3858
4062
  /** Feed a committed change into the process recorder (gated + loop-safe). The
3859
4063
  * serve paths call this from their `store.onChange` tap. */
3860
4064
  export declare const recordTimelineEvent: (change: CdcChange & {
@@ -4225,6 +4429,9 @@ export declare interface ResendOptions {
4225
4429
  /** Test/dev-only — clear ALL overrides. Don't call from app code. */
4226
4430
  export declare const _resetConnectionSubjectsForTest: () => void;
4227
4431
 
4432
+ /** Drop everything recorded. For tests; not called by the runtime. */
4433
+ export declare const resetObservedGraph: () => void;
4434
+
4228
4435
  /** Reset to the env default (tests). */
4229
4436
  export declare const resetSecretsBackend: () => void;
4230
4437
 
@@ -4276,9 +4483,11 @@ export declare const resolveCurrentRegion: (env?: NodeJS.ProcessEnv) => string |
4276
4483
  export declare interface ResolvedApiKey {
4277
4484
  readonly id: string;
4278
4485
  readonly tenantId: string;
4279
- /** The user who created the key, when one is recorded. Null for a key minted
4280
- * outside a user session (bootstrap / admin tooling). */
4486
+ /** WHO MINTED the key. Provenance present for org keys too. */
4281
4487
  readonly createdBy: string | null;
4488
+ /** WHO THE KEY ACTS AS. `null` = an ORG key. This is the identity attribution
4489
+ * follows and the one the Subject carries as `metadata.userId`. */
4490
+ readonly onBehalfOf: string | null;
4282
4491
  readonly scopes: ReadonlyArray<string>;
4283
4492
  }
4284
4493
 
@@ -4755,6 +4964,16 @@ export declare const runWithDeadlockRetry: <T>(work: () => Promise<T>, options?:
4755
4964
  readonly delay?: (attempt: number) => Promise<void>;
4756
4965
  }) => Promise<T>;
4757
4966
 
4967
+ /**
4968
+ * Run `work` attributed to `procedure`. Every store operation performed inside
4969
+ * (or in its async children) is recorded against that tag.
4970
+ *
4971
+ * A no-op when recording is off, including the ALS entry itself — an
4972
+ * AsyncLocalStorage `run` on every rpc call is not free, and a disabled feature
4973
+ * should cost nothing.
4974
+ */
4975
+ export declare const runWithObservedProcedure: <T>(procedure: ObservedProcedure, work: () => T) => T;
4976
+
4758
4977
  /**
4759
4978
  * Run `work` inside a routing scope. Every `query()` call against a
4760
4979
  * `ReplicatedDataStore` from inside `work` (or its async children)
@@ -5381,6 +5600,51 @@ export declare const snapshotMetrics: Effect.Effect<ReadonlyArray<MetricSample>>
5381
5600
  /** Eager snapshot for sync callers (the inspect endpoint). */
5382
5601
  export declare const snapshotMetricsSync: () => ReadonlyArray<MetricSample>;
5383
5602
 
5603
+ /** Thrown (as a typed defect) when a request targets a blocked address. */
5604
+ export declare class SsrfBlockedError extends Error {
5605
+ readonly url: string;
5606
+ readonly reason: string;
5607
+ readonly _tag = "SsrfBlockedError";
5608
+ constructor(url: string, reason: string);
5609
+ }
5610
+
5611
+ /**
5612
+ * Decorator layer: consumes a base `HttpClient` and re-provides one that blocks
5613
+ * SSRF targets on the initial request AND on every redirect hop.
5614
+ *
5615
+ * Compose it BELOW the tracing layer and above the base fetch client:
5616
+ *
5617
+ * ssrfGuardHttpClientLayer(policy).pipe(Layer.provide(FetchHttpClient.layer))
5618
+ */
5619
+ export declare const ssrfGuardHttpClientLayer: (policy?: SsrfPolicy) => Layer.Layer<HttpClient.HttpClient, never, HttpClient.HttpClient>;
5620
+
5621
+ export declare interface SsrfPolicy {
5622
+ /**
5623
+ * Hosts that bypass the guard. An entry may be an exact hostname
5624
+ * (`billing.internal`), a suffix wildcard (`*.svc.cluster.local`), or a
5625
+ * host:port (`127.0.0.1:8787`) when only one port should be reachable.
5626
+ *
5627
+ * This is the one escape hatch, and it is deliberately per-HOST rather than a
5628
+ * boolean: "we call one internal service" and "we do not check URLs" are very
5629
+ * different postures, and a boolean cannot tell them apart six months later.
5630
+ */
5631
+ readonly allowHosts?: ReadonlyArray<string>;
5632
+ /** Max redirect hops to follow. Each one is revalidated. Default 5. */
5633
+ readonly maxRedirects?: number;
5634
+ }
5635
+
5636
+ /**
5637
+ * Decide whether a URL may be fetched. Returns `null` when allowed, else the
5638
+ * reason. Fails CLOSED: a URL that will not parse is blocked.
5639
+ *
5640
+ * DNS is deliberately NOT resolved — this is a synchronous check on the literal
5641
+ * target, so a hostname that RESOLVES to a private address (DNS rebinding) is not
5642
+ * caught here. Blocking the direct-IP vector is the high-severity, cheap half;
5643
+ * rebinding needs network-layer egress control and is called out in the docs
5644
+ * rather than silently implied.
5645
+ */
5646
+ export declare const ssrfReason: (rawUrl: string, policy?: SsrfPolicy) => string | null;
5647
+
5384
5648
  /**
5385
5649
  * Start the rpc-based runtime server on Node. Composes:
5386
5650
  * - NodeHttpServer + NodeContext at the platform layer
@@ -5388,10 +5652,15 @@ export declare const snapshotMetricsSync: () => ReadonlyArray<MetricSample>;
5388
5652
  * - RpcServer.layerHttpRouter (websocket protocol) consuming
5389
5653
  * RpcSerialization.layerJson and the user's handlers layer
5390
5654
  *
5391
- * Hands the result to NodeRuntime.runMain via Layer.launch so the Node event
5392
- * loop stays alive for the lifetime of the layer's scope.
5393
- */
5394
- export declare const startRpcServer: <Rpcs extends Rpc.Any>(options: RpcServerOptions<Rpcs>) => ReturnType<typeof createServer>;
5655
+ * Forks `Layer.launch` so the Node event loop stays alive for the lifetime of the
5656
+ * layer's scope, and returns that scope's `shutdown` alongside the node server —
5657
+ * so a programmatic close releases the layer, not just the socket. SIGINT/SIGTERM
5658
+ * are wired once per process (not once per call) and drain every live launch.
5659
+ */
5660
+ export declare const startRpcServer: <Rpcs extends Rpc.Any>(options: RpcServerOptions<Rpcs>) => {
5661
+ server: ReturnType<typeof createServer>;
5662
+ shutdown: () => Promise<void>;
5663
+ };
5395
5664
 
5396
5665
  export declare const startScheduler: (schedules: ReadonlyArray<BrandedScheduleDefinition>, deps: SchedulerDeps) => Promise<SchedulerHandle>;
5397
5666
 
@@ -6555,6 +6824,6 @@ export declare interface WorkflowWaitOptions {
6555
6824
  */
6556
6825
  export declare const wrapCaptureStore: (tx: DataStore, collect: (c: ForwardChange) => void) => DataStore;
6557
6826
 
6558
- export declare const wrapStoreWithMixinBehaviour: (underlying: DataStore, ctx: StoreMiddlewareContext) => FluentStore;
6827
+ export declare const wrapStoreWithMixinBehaviour: (rawUnderlying: DataStore, ctx: StoreMiddlewareContext) => FluentStore;
6559
6828
 
6560
6829
  export { }