@voltro/runtime 0.11.4 → 0.13.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/CHANGELOG.md +356 -0
- package/dist/index.d.ts +334 -14
- package/dist/index.js +1620 -1363
- package/package.json +6 -6
package/dist/index.d.ts
CHANGED
|
@@ -673,10 +673,36 @@ 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;
|
|
679
684
|
readonly createdAt: number;
|
|
685
|
+
/**
|
|
686
|
+
* App-owned attributes stored with the key and handed back on resolve.
|
|
687
|
+
*
|
|
688
|
+
* The SECOND OWNERSHIP AXIS. `tenantId` and `onBehalfOf` are the two the
|
|
689
|
+
* framework models, and plenty of apps have a third that authorizes the key —
|
|
690
|
+
* a team, a project, an environment. `ApiKeyRecord` in `@voltro/protocol` has
|
|
691
|
+
* carried this slot all along (its doc comment names `teamId` as the example),
|
|
692
|
+
* but the SERVICE had nowhere to store it and nowhere to return it. So an app
|
|
693
|
+
* with a team axis could authenticate through the built-in strategy and still
|
|
694
|
+
* not authorize, and `apiKeys: true` was unusable for it. The reported
|
|
695
|
+
* workarounds were a second table joined on the hot auth path, or smuggling
|
|
696
|
+
* `team:<id>` into `scopes` — a scope that is not a scope, which `hasScope`
|
|
697
|
+
* would then see. Neither is a good answer to a missing field.
|
|
698
|
+
*
|
|
699
|
+
* Stored as JSON. **It is app data, not identity.** The strategy merges it
|
|
700
|
+
* UNDER the framework's own claims: `provider`, and the acting `userId`, are
|
|
701
|
+
* written afterwards from `onBehalfOf` and always win — including when the
|
|
702
|
+
* answer is "none". A metadata bag that could set `userId` would let whoever
|
|
703
|
+
* minted the key choose who the request is.
|
|
704
|
+
*/
|
|
705
|
+
readonly metadata?: Readonly<Record<string, unknown>> | null;
|
|
680
706
|
}
|
|
681
707
|
|
|
682
708
|
export declare interface ApiKeyServiceShape {
|
|
@@ -692,10 +718,28 @@ export declare interface ApiKeyServiceShape {
|
|
|
692
718
|
readonly resolveByHash: (hash: string, now?: number) => Promise<ResolvedApiKey | null>;
|
|
693
719
|
/** Verify a raw token (hashes then resolves). */
|
|
694
720
|
readonly verify: (token: string, now?: number) => Promise<ResolvedApiKey | null>;
|
|
695
|
-
/**
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
721
|
+
/**
|
|
722
|
+
* Revoke a key (irreversible).
|
|
723
|
+
*
|
|
724
|
+
* `tenantId` is the CALLER's tenant and is REQUIRED, not optional: a key id is
|
|
725
|
+
* the only thing needed to revoke, and ids leak (logs, support tickets, an
|
|
726
|
+
* error message). Without this check an admin of tenant A could revoke tenant
|
|
727
|
+
* B's key by id — the guard on the route asks "is an admin", never "an admin
|
|
728
|
+
* of THIS key's tenant". Returns `false` for a key belonging to anyone else,
|
|
729
|
+
* indistinguishable from "no such key", so it cannot be used to probe.
|
|
730
|
+
*
|
|
731
|
+
* Pass `null` only for the tenant-less system key space. There is deliberately
|
|
732
|
+
* no "skip the check" value: an optional scope on a destructive operation is a
|
|
733
|
+
* scope somebody forgets.
|
|
734
|
+
*/
|
|
735
|
+
readonly revoke: (id: string, tenantId: string | null, now?: number) => Promise<boolean>;
|
|
736
|
+
/**
|
|
737
|
+
* Rotate: revoke the old key + issue a replacement with the same
|
|
738
|
+
* name/scopes/tenant. Same required tenant scope as `revoke`, and here it
|
|
739
|
+
* matters more — rotate RETURNS A USABLE TOKEN, so an unscoped version would
|
|
740
|
+
* hand the caller a working credential for another tenant.
|
|
741
|
+
*/
|
|
742
|
+
readonly rotate: (id: string, tenantId: string | null, now?: number) => Promise<IssuedApiKey | null>;
|
|
699
743
|
/** List a tenant's keys (no secrets). */
|
|
700
744
|
readonly list: (tenantId: string | null) => Promise<ReadonlyArray<PublicApiKey>>;
|
|
701
745
|
}
|
|
@@ -708,6 +752,38 @@ export declare interface ApiKeyStore {
|
|
|
708
752
|
readonly patch: (id: string, fields: Partial<ApiKeyRow>) => Promise<void>;
|
|
709
753
|
}
|
|
710
754
|
|
|
755
|
+
export declare interface ApiKeyUsageBuffer {
|
|
756
|
+
/** Record one use. Synchronous, no I/O. */
|
|
757
|
+
readonly touch: (id: string, at?: number) => void;
|
|
758
|
+
/** Write everything pending now. */
|
|
759
|
+
readonly flushNow: () => Promise<void>;
|
|
760
|
+
/** Flush and stop the timer. Idempotent. */
|
|
761
|
+
readonly shutdown: () => Promise<void>;
|
|
762
|
+
/** Pending key count — for tests and the inspect surface. */
|
|
763
|
+
readonly pending: () => number;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export declare interface ApiKeyUsageBufferOptions {
|
|
767
|
+
/** Apply one key's accumulated window. Must ADD `requests` to the stored
|
|
768
|
+
* count and move `lastUsedAt` forward — never assign either, or a second
|
|
769
|
+
* replica's window overwrites this one. */
|
|
770
|
+
readonly flush: (id: string, delta: ApiKeyUsageDelta) => Promise<void>;
|
|
771
|
+
/** Flush cadence. Default 30s. */
|
|
772
|
+
readonly intervalMs?: number;
|
|
773
|
+
/** Flush early once this many distinct keys are pending, so a burst across
|
|
774
|
+
* many keys doesn't sit unbounded in memory for a whole window. Default 500. */
|
|
775
|
+
readonly maxPending?: number;
|
|
776
|
+
/** Injectable for tests. */
|
|
777
|
+
readonly now?: () => number;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export declare interface ApiKeyUsageDelta {
|
|
781
|
+
/** Requests counted for this key in the flushed window. Always ≥ 1. */
|
|
782
|
+
readonly requests: number;
|
|
783
|
+
/** The latest use observed in the window (epoch ms). */
|
|
784
|
+
readonly lastUsedAt: number;
|
|
785
|
+
}
|
|
786
|
+
|
|
711
787
|
/**
|
|
712
788
|
* Typed authorization slice on `ctx.access` — the ergonomic, cast-free face of
|
|
713
789
|
* the caller's EFFECTIVE scope set (raw subject scopes ∪ rbac role-derived
|
|
@@ -731,6 +807,28 @@ export declare interface AppAccess {
|
|
|
731
807
|
|
|
732
808
|
export declare interface AppContext {
|
|
733
809
|
readonly store: FluentStore;
|
|
810
|
+
/**
|
|
811
|
+
* The same store, scoped to ONE tenant.
|
|
812
|
+
*
|
|
813
|
+
* For non-request work — a schedule, a subscriber, a workflow step — whose
|
|
814
|
+
* subject is `system` with `tenantId: null`. Reads there see every tenant and
|
|
815
|
+
* a write to a `tenant()` table fails with `TenantScopeViolation`, so a
|
|
816
|
+
* per-tenant cron has to say which tenant it means. Every fan-out cron is
|
|
817
|
+
* literally a loop doing this by hand, and doing it by hand means each
|
|
818
|
+
* `.where('tenantId', t.id)` is one forgotten call away from reading the whole
|
|
819
|
+
* table:
|
|
820
|
+
*
|
|
821
|
+
* for (const t of await ctx.store.select('tenants').all()) {
|
|
822
|
+
* const scoped = ctx.storeForTenant(t.id)
|
|
823
|
+
* await scoped.insert('digests', { … }) // tenantId stamped, not passed
|
|
824
|
+
* }
|
|
825
|
+
*
|
|
826
|
+
* Inside a REQUEST this is almost always the wrong tool: the subject already
|
|
827
|
+
* carries a tenant, and reaching for another one is a cross-tenant read with
|
|
828
|
+
* extra steps. It exists because the system subject has no tenant to infer,
|
|
829
|
+
* not to let a request pick a different one.
|
|
830
|
+
*/
|
|
831
|
+
readonly storeForTenant: (tenantId: string) => FluentStore;
|
|
734
832
|
readonly request: RuntimeContext;
|
|
735
833
|
/** Typed authorization slice — the caller's effective scopes. Use
|
|
736
834
|
* `ctx.access.has(scope)` / `yield* ctx.access.require(scope)` instead of
|
|
@@ -1560,7 +1658,7 @@ export declare const crud: {
|
|
|
1560
1658
|
*
|
|
1561
1659
|
* export default crud.count('absenceRequests', { filter: (i) => ({ status: i.status }) })
|
|
1562
1660
|
*/
|
|
1563
|
-
count: (table: string, options?: Pick<CrudListOptions, "filter">) => (input: unknown, ctx: AppContext) => Promise<number>;
|
|
1661
|
+
count: (table: string, options?: Pick<CrudListOptions, "filter" | "scope">) => (input: unknown, ctx: AppContext) => Promise<number>;
|
|
1564
1662
|
/** Delete a row by id; returns `{ deleted }`. Guard the DESCRIPTOR. */
|
|
1565
1663
|
remove: (table: string) => (input: {
|
|
1566
1664
|
readonly id: string;
|
|
@@ -1586,8 +1684,33 @@ export declare interface CrudListOptions extends CrudReadOptions {
|
|
|
1586
1684
|
* is simply ignored (`{ employeeId: input.employeeId, status: input.status }`).
|
|
1587
1685
|
* The descriptor's `input` schema declares those fields; this maps them to a
|
|
1588
1686
|
* scoped `.where(column, value)` on the store query.
|
|
1687
|
+
*
|
|
1688
|
+
* Request input ONLY, on purpose — see `scope` for the caller-derived half.
|
|
1589
1689
|
*/
|
|
1590
1690
|
readonly filter?: (input: Readonly<Record<string, unknown>>) => Readonly<Record<string, unknown>>;
|
|
1691
|
+
/**
|
|
1692
|
+
* The caller-derived WHERE — what this subject may SEE, as opposed to what it
|
|
1693
|
+
* asked for. Merged into the same query as `filter`, and it WINS on a key
|
|
1694
|
+
* collision, so a caller cannot widen its own scope by sending that field.
|
|
1695
|
+
*
|
|
1696
|
+
* scope: (ctx) => ({ ownerId: ctx.request.subject.id })
|
|
1697
|
+
*
|
|
1698
|
+
* Why this is a SEPARATE option rather than a second argument to `filter`:
|
|
1699
|
+
* the two answer different questions, and only one of them is a security
|
|
1700
|
+
* boundary. Kept apart, "does this list declare a `scope`?" is a question a
|
|
1701
|
+
* reviewer — or a future boot audit — can actually ask. Folded into `filter`,
|
|
1702
|
+
* it becomes "does this filter happen to read ctx somewhere in its body?",
|
|
1703
|
+
* which nothing can check.
|
|
1704
|
+
*
|
|
1705
|
+
* The gap this closes: tenant scope is applied automatically, but anything
|
|
1706
|
+
* narrower — owner, team, role — was not expressible at all. Replacing a
|
|
1707
|
+
* hand-written handler that carried such a narrowing with `crud.list`
|
|
1708
|
+
* therefore WIDENED the result set, silently and with no error. One app lost
|
|
1709
|
+
* exactly that across eight list views.
|
|
1710
|
+
*
|
|
1711
|
+
* Pass the same `scope` to `crud.count`, or the total contradicts the pages.
|
|
1712
|
+
*/
|
|
1713
|
+
readonly scope?: (ctx: AppContext) => Readonly<Record<string, unknown>>;
|
|
1591
1714
|
/**
|
|
1592
1715
|
* Page the result from the request. BOTH styles are accepted, so a caller uses
|
|
1593
1716
|
* whichever its UI thinks in:
|
|
@@ -1683,7 +1806,6 @@ export declare interface DataLoader {
|
|
|
1683
1806
|
|
|
1684
1807
|
export { DataStore }
|
|
1685
1808
|
|
|
1686
|
-
/** DataStore-backed store over `_voltro_api_keys`. */
|
|
1687
1809
|
export declare const dataStoreApiKeyStore: (store: DataStore) => ApiKeyStore;
|
|
1688
1810
|
|
|
1689
1811
|
export declare const dataStoreIdempotencyStore: (store: DataStore) => IdempotencyStore;
|
|
@@ -1761,7 +1883,7 @@ export declare const defineEventTrigger: <EventPayload = unknown, WorkflowPayloa
|
|
|
1761
1883
|
* Runtime identity — returns `fn` unchanged; the whole value is the compile-time
|
|
1762
1884
|
* check. See the module header for the failure it prevents.
|
|
1763
1885
|
*/
|
|
1764
|
-
export declare const defineExecutor: <D extends ExecutorDescriptor,
|
|
1886
|
+
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
1887
|
|
|
1766
1888
|
/**
|
|
1767
1889
|
* Declare who delivers an effect. One per `*.outbox.ts` file.
|
|
@@ -2026,6 +2148,10 @@ export declare interface EffectStoreOps {
|
|
|
2026
2148
|
* discovered tables. */
|
|
2027
2149
|
export declare const emptySchemaRegistry: SchemaRegistry;
|
|
2028
2150
|
|
|
2151
|
+
/** Turn recording on from code. Idempotent. Must run BEFORE the stores that
|
|
2152
|
+
* should be observed are constructed — `observeStore` decides once, per store. */
|
|
2153
|
+
export declare const enableGraphObservation: () => void;
|
|
2154
|
+
|
|
2029
2155
|
/** Encrypt a value with the registered field cipher (same one `.encrypted()`
|
|
2030
2156
|
* columns use). Throws if no cipher is registered. */
|
|
2031
2157
|
export declare const encryptField: (plaintext: string) => string;
|
|
@@ -2545,6 +2671,8 @@ export declare const isIdleNow: (opts: IdleCheckOptions) => Promise<boolean>;
|
|
|
2545
2671
|
|
|
2546
2672
|
export declare const isInfraError: (e: unknown) => boolean;
|
|
2547
2673
|
|
|
2674
|
+
export declare const isObservingGraph: () => boolean;
|
|
2675
|
+
|
|
2548
2676
|
/**
|
|
2549
2677
|
* A subject that AUTHENTICATED but resolved with NO active org — a `user` (not
|
|
2550
2678
|
* `anonymous`/`system`) carrying the empty-string tenant sentinel. Every
|
|
@@ -2585,7 +2713,16 @@ export declare interface IssueInput {
|
|
|
2585
2713
|
readonly name: string;
|
|
2586
2714
|
readonly scopes?: ReadonlyArray<string>;
|
|
2587
2715
|
readonly expiresAt?: Date | number | null;
|
|
2716
|
+
/** WHO MINTED IT. Provenance — set it always, including for an org key. */
|
|
2588
2717
|
readonly createdBy?: string | null;
|
|
2718
|
+
/** WHO IT ACTS AS. Omit for an ORG key (acts as no person); set it for a
|
|
2719
|
+
* personal key, which may be the minter or — when an admin mints for a
|
|
2720
|
+
* colleague — someone else entirely. */
|
|
2721
|
+
readonly onBehalfOf?: string | null;
|
|
2722
|
+
/** App-owned attributes to store with the key — see `ApiKeyRow.metadata`.
|
|
2723
|
+
* Round-trips verbatim to `ResolvedApiKey.metadata`; it is app data, and the
|
|
2724
|
+
* strategy never lets it decide who the request is. */
|
|
2725
|
+
readonly metadata?: Readonly<Record<string, unknown>> | null;
|
|
2589
2726
|
}
|
|
2590
2727
|
|
|
2591
2728
|
export declare const isWorkflowEventTriggerDefinition: (value: unknown) => value is WorkflowEventTriggerDefinition;
|
|
@@ -2808,8 +2945,30 @@ export declare const makeAesCipher: (key: Buffer) => FieldCipher;
|
|
|
2808
2945
|
|
|
2809
2946
|
export declare const makeApiKeyService: (store: ApiKeyStore, opts?: {
|
|
2810
2947
|
prefix?: string;
|
|
2948
|
+
/** Buffers `lastUsedAt` + `requestCount` instead of writing a row per auth
|
|
2949
|
+
* check. Omit to keep the synchronous stamp (see `resolveByHash`). */
|
|
2950
|
+
usage?: ApiKeyUsageBuffer;
|
|
2811
2951
|
}) => ApiKeyServiceShape;
|
|
2812
2952
|
|
|
2953
|
+
export declare const makeApiKeyUsageBuffer: (options: ApiKeyUsageBufferOptions) => ApiKeyUsageBuffer;
|
|
2954
|
+
|
|
2955
|
+
/**
|
|
2956
|
+
* The flusher a buffered `ApiKeyUsageBuffer` is built with: read-modify-write of
|
|
2957
|
+
* `requestCount` + `lastUsedAt` for one key, once per window.
|
|
2958
|
+
*
|
|
2959
|
+
* Not atomic across replicas, and that is a deliberate limit rather than an
|
|
2960
|
+
* oversight. Two pods flushing the same key in the same instant can lose one
|
|
2961
|
+
* window's delta. Making it exact would need a dialect-specific
|
|
2962
|
+
* `SET requestCount = requestCount + ?`, which is real complexity in service of
|
|
2963
|
+
* a field whose stated purpose is "is this key still in use, and roughly how
|
|
2964
|
+
* much". If this number ever needs to be exact, that is the signal it is being
|
|
2965
|
+
* used for something it was not built for (billing), not a signal to add locking.
|
|
2966
|
+
*/
|
|
2967
|
+
export declare const makeApiKeyUsageFlusher: (store: ApiKeyStore) => (id: string, delta: {
|
|
2968
|
+
readonly requests: number;
|
|
2969
|
+
readonly lastUsedAt: number;
|
|
2970
|
+
}) => Promise<void>;
|
|
2971
|
+
|
|
2813
2972
|
/** Build the `ctx.access` slice for a subject. Reads the effective-scope seam
|
|
2814
2973
|
* LIVE (via getters/closures) so a scope rbac resolves after context
|
|
2815
2974
|
* construction is still reflected. Used by the shared AppContext builder AND
|
|
@@ -3371,6 +3530,50 @@ export declare interface OAuth2ConnectionDefinition extends ConnectionDefinition
|
|
|
3371
3530
|
readonly identify?: (tokens: ConnectionTokens) => Promise<ConnectionAccount>;
|
|
3372
3531
|
}
|
|
3373
3532
|
|
|
3533
|
+
/** One recorded `(procedure → table, op)` edge. */
|
|
3534
|
+
export declare interface ObservedEdge {
|
|
3535
|
+
readonly tag: string;
|
|
3536
|
+
readonly kind: ObservedProcedure['kind'];
|
|
3537
|
+
readonly table: string;
|
|
3538
|
+
readonly op: ObservedOp;
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
export declare interface ObservedGraph {
|
|
3542
|
+
/** Schema version of this file's shape, so `check` can refuse an old one
|
|
3543
|
+
* loudly instead of silently diffing against a different meaning. */
|
|
3544
|
+
readonly version: 1;
|
|
3545
|
+
readonly edges: ReadonlyArray<ObservedEdge>;
|
|
3546
|
+
/** Tags seen entering the recorder at all — the difference between "this
|
|
3547
|
+
* procedure touched no table" and "this procedure never ran", which is the
|
|
3548
|
+
* distinction the whole feature stands on. */
|
|
3549
|
+
readonly exercised: ReadonlyArray<string>;
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
/** Snapshot everything recorded so far. */
|
|
3553
|
+
export declare const observedGraph: () => ObservedGraph;
|
|
3554
|
+
|
|
3555
|
+
/** What a procedure did to a table. `read` covers every query path. */
|
|
3556
|
+
export declare type ObservedOp = 'read' | 'insert' | 'update' | 'delete' | 'upsert';
|
|
3557
|
+
|
|
3558
|
+
export declare interface ObservedProcedure {
|
|
3559
|
+
/** The procedure's effective rpc tag (`notes.create`). */
|
|
3560
|
+
readonly tag: string;
|
|
3561
|
+
readonly kind: 'query' | 'mutation' | 'action' | 'stream' | 'schedule' | 'workflow';
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
/**
|
|
3565
|
+
* Wrap a `DataStore` so every operation is recorded. Returns the store unchanged
|
|
3566
|
+
* when recording is off, so the production path keeps the exact same object
|
|
3567
|
+
* (no added indirection, no added allocation).
|
|
3568
|
+
*
|
|
3569
|
+
* Wrapping the store BENEATH the mixin middleware rather than instrumenting the
|
|
3570
|
+
* middleware's ~20 public methods is deliberate: those methods delegate to each
|
|
3571
|
+
* other (`one`/`first`/`maybeOne` all go through `query`), so instrumenting them
|
|
3572
|
+
* individually would double-count some paths and miss any new one. Underneath,
|
|
3573
|
+
* each operation passes exactly once.
|
|
3574
|
+
*/
|
|
3575
|
+
export declare const observeStore: <S extends object>(store: S) => S;
|
|
3576
|
+
|
|
3374
3577
|
/**
|
|
3375
3578
|
* Listen for re-bind events. Returns an unsubscribe function. Used by
|
|
3376
3579
|
* the dispatcher to re-scope active subscriptions when a connection's
|
|
@@ -3565,6 +3768,24 @@ export declare interface PatConnectionDefinition extends ConnectionDefinitionCom
|
|
|
3565
3768
|
readonly validate?: (token: string) => Promise<ConnectionAccount>;
|
|
3566
3769
|
}
|
|
3567
3770
|
|
|
3771
|
+
/**
|
|
3772
|
+
* TOP-LEVEL property names of a struct-ish schema, split by optionality.
|
|
3773
|
+
*
|
|
3774
|
+
* Deliberately not `schemaPropertyNames` (`./serverOnlyAudit`), which answers a
|
|
3775
|
+
* DIFFERENT question: that one walks the whole schema recursively and flattens
|
|
3776
|
+
* every name at any depth, because a leak check cares whether a column name
|
|
3777
|
+
* appears anywhere in an output. This one asks "what does this payload require
|
|
3778
|
+
* at its top level", so depth would be wrong and optionality is the point.
|
|
3779
|
+
*
|
|
3780
|
+
* Best effort — at runtime it only makes an error readable and never decides
|
|
3781
|
+
* validity — but `voltro doctor`'s static `workflows.start` audit calls the same
|
|
3782
|
+
* function, so the two cannot disagree about what a payload requires.
|
|
3783
|
+
*/
|
|
3784
|
+
export declare const payloadPropertyNames: (schema: Schema.Schema.Any) => {
|
|
3785
|
+
readonly required: ReadonlyArray<string>;
|
|
3786
|
+
readonly optional: ReadonlyArray<string>;
|
|
3787
|
+
};
|
|
3788
|
+
|
|
3568
3789
|
/** The 3-arg signature a plugin sees on its bind-ctx. */
|
|
3569
3790
|
export declare type PluginScheduleCoordinated = (name: string, intervalMs: number, effect: () => void | Promise<void>) => CoordinatedScheduleHandle;
|
|
3570
3791
|
|
|
@@ -3671,6 +3892,18 @@ export declare interface PublicApiKey {
|
|
|
3671
3892
|
readonly expiresAt: number | null;
|
|
3672
3893
|
readonly revokedAt: number | null;
|
|
3673
3894
|
readonly createdAt: number;
|
|
3895
|
+
/**
|
|
3896
|
+
* WHO MINTED IT and WHO IT ACTS AS — both omitted before, which made
|
|
3897
|
+
* `service.list` unable to answer the two questions an admin actually asks
|
|
3898
|
+
* about a shared credential. Neither is a secret: they are the accountability
|
|
3899
|
+
* record, and hiding them from the management view only hides them from the
|
|
3900
|
+
* person responsible for the key.
|
|
3901
|
+
*/
|
|
3902
|
+
readonly createdBy: string | null;
|
|
3903
|
+
readonly onBehalfOf: string | null;
|
|
3904
|
+
/** App-owned attributes — see `ApiKeyRow.metadata`. Surfaced so a management
|
|
3905
|
+
* UI can group by the app's own ownership axis (team, project, …). */
|
|
3906
|
+
readonly metadata?: Readonly<Record<string, unknown>> | null;
|
|
3674
3907
|
}
|
|
3675
3908
|
|
|
3676
3909
|
/** A typed builder or its descriptor — the single-row terminals accept either,
|
|
@@ -3855,6 +4088,14 @@ export declare const recordSubscriptionActive: (label: string, delta: number) =>
|
|
|
3855
4088
|
* latency + a delivery counter. Rides the framework's existing per-delivery tap. */
|
|
3856
4089
|
export declare const recordSubscriptionDelivery: (d: SubscriptionDeliveryRecord) => void;
|
|
3857
4090
|
|
|
4091
|
+
/**
|
|
4092
|
+
* Record one table access against the procedure on the current fiber. Silently
|
|
4093
|
+
* ignored outside a procedure scope — background work (migrations, the
|
|
4094
|
+
* scheduler's own bookkeeping, plugin boot) is not a procedure and must not
|
|
4095
|
+
* invent edges for one.
|
|
4096
|
+
*/
|
|
4097
|
+
export declare const recordTableAccess: (table: string, op: ObservedOp) => void;
|
|
4098
|
+
|
|
3858
4099
|
/** Feed a committed change into the process recorder (gated + loop-safe). The
|
|
3859
4100
|
* serve paths call this from their `store.onChange` tap. */
|
|
3860
4101
|
export declare const recordTimelineEvent: (change: CdcChange & {
|
|
@@ -4222,9 +4463,15 @@ export declare interface ResendOptions {
|
|
|
4222
4463
|
readonly attempts?: number;
|
|
4223
4464
|
}
|
|
4224
4465
|
|
|
4466
|
+
/** Exported for tests — the warn-once set is process-global by design. */
|
|
4467
|
+
export declare const resetComputedQueryCacheWarnings: () => void;
|
|
4468
|
+
|
|
4225
4469
|
/** Test/dev-only — clear ALL overrides. Don't call from app code. */
|
|
4226
4470
|
export declare const _resetConnectionSubjectsForTest: () => void;
|
|
4227
4471
|
|
|
4472
|
+
/** Drop everything recorded. For tests; not called by the runtime. */
|
|
4473
|
+
export declare const resetObservedGraph: () => void;
|
|
4474
|
+
|
|
4228
4475
|
/** Reset to the env default (tests). */
|
|
4229
4476
|
export declare const resetSecretsBackend: () => void;
|
|
4230
4477
|
|
|
@@ -4276,10 +4523,23 @@ export declare const resolveCurrentRegion: (env?: NodeJS.ProcessEnv) => string |
|
|
|
4276
4523
|
export declare interface ResolvedApiKey {
|
|
4277
4524
|
readonly id: string;
|
|
4278
4525
|
readonly tenantId: string;
|
|
4279
|
-
/**
|
|
4280
|
-
* outside a user session (bootstrap / admin tooling). */
|
|
4526
|
+
/** WHO MINTED the key. Provenance — present for org keys too. */
|
|
4281
4527
|
readonly createdBy: string | null;
|
|
4528
|
+
/** WHO THE KEY ACTS AS. `null` = an ORG key. This is the identity attribution
|
|
4529
|
+
* follows and the one the Subject carries as `metadata.userId`. */
|
|
4530
|
+
readonly onBehalfOf: string | null;
|
|
4282
4531
|
readonly scopes: ReadonlyArray<string>;
|
|
4532
|
+
/**
|
|
4533
|
+
* App-owned attributes as stored — see `ApiKeyRow.metadata`. This is the
|
|
4534
|
+
* return trip that was missing: a store could read the app's ownership axis
|
|
4535
|
+
* and it was then dropped before the strategy could see it.
|
|
4536
|
+
*
|
|
4537
|
+
* Optional-absent rather than nullable, deliberately: this shape has to be
|
|
4538
|
+
* assignable to `ApiKeyRecord` (what `apiKeyStrategy` consumes), whose slot is
|
|
4539
|
+
* `?: Readonly<Record<string, unknown>>`. A DB `null` normalises to absent on
|
|
4540
|
+
* the way out, so the two never disagree.
|
|
4541
|
+
*/
|
|
4542
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
4283
4543
|
}
|
|
4284
4544
|
|
|
4285
4545
|
/** A usable credential, handed to a plugin / handler. */
|
|
@@ -4755,6 +5015,16 @@ export declare const runWithDeadlockRetry: <T>(work: () => Promise<T>, options?:
|
|
|
4755
5015
|
readonly delay?: (attempt: number) => Promise<void>;
|
|
4756
5016
|
}) => Promise<T>;
|
|
4757
5017
|
|
|
5018
|
+
/**
|
|
5019
|
+
* Run `work` attributed to `procedure`. Every store operation performed inside
|
|
5020
|
+
* (or in its async children) is recorded against that tag.
|
|
5021
|
+
*
|
|
5022
|
+
* A no-op when recording is off, including the ALS entry itself — an
|
|
5023
|
+
* AsyncLocalStorage `run` on every rpc call is not free, and a disabled feature
|
|
5024
|
+
* should cost nothing.
|
|
5025
|
+
*/
|
|
5026
|
+
export declare const runWithObservedProcedure: <T>(procedure: ObservedProcedure, work: () => T) => T;
|
|
5027
|
+
|
|
4758
5028
|
/**
|
|
4759
5029
|
* Run `work` inside a routing scope. Every `query()` call against a
|
|
4760
5030
|
* `ReplicatedDataStore` from inside `work` (or its async children)
|
|
@@ -5381,6 +5651,51 @@ export declare const snapshotMetrics: Effect.Effect<ReadonlyArray<MetricSample>>
|
|
|
5381
5651
|
/** Eager snapshot for sync callers (the inspect endpoint). */
|
|
5382
5652
|
export declare const snapshotMetricsSync: () => ReadonlyArray<MetricSample>;
|
|
5383
5653
|
|
|
5654
|
+
/** Thrown (as a typed defect) when a request targets a blocked address. */
|
|
5655
|
+
export declare class SsrfBlockedError extends Error {
|
|
5656
|
+
readonly url: string;
|
|
5657
|
+
readonly reason: string;
|
|
5658
|
+
readonly _tag = "SsrfBlockedError";
|
|
5659
|
+
constructor(url: string, reason: string);
|
|
5660
|
+
}
|
|
5661
|
+
|
|
5662
|
+
/**
|
|
5663
|
+
* Decorator layer: consumes a base `HttpClient` and re-provides one that blocks
|
|
5664
|
+
* SSRF targets on the initial request AND on every redirect hop.
|
|
5665
|
+
*
|
|
5666
|
+
* Compose it BELOW the tracing layer and above the base fetch client:
|
|
5667
|
+
*
|
|
5668
|
+
* ssrfGuardHttpClientLayer(policy).pipe(Layer.provide(FetchHttpClient.layer))
|
|
5669
|
+
*/
|
|
5670
|
+
export declare const ssrfGuardHttpClientLayer: (policy?: SsrfPolicy) => Layer.Layer<HttpClient.HttpClient, never, HttpClient.HttpClient>;
|
|
5671
|
+
|
|
5672
|
+
export declare interface SsrfPolicy {
|
|
5673
|
+
/**
|
|
5674
|
+
* Hosts that bypass the guard. An entry may be an exact hostname
|
|
5675
|
+
* (`billing.internal`), a suffix wildcard (`*.svc.cluster.local`), or a
|
|
5676
|
+
* host:port (`127.0.0.1:8787`) when only one port should be reachable.
|
|
5677
|
+
*
|
|
5678
|
+
* This is the one escape hatch, and it is deliberately per-HOST rather than a
|
|
5679
|
+
* boolean: "we call one internal service" and "we do not check URLs" are very
|
|
5680
|
+
* different postures, and a boolean cannot tell them apart six months later.
|
|
5681
|
+
*/
|
|
5682
|
+
readonly allowHosts?: ReadonlyArray<string>;
|
|
5683
|
+
/** Max redirect hops to follow. Each one is revalidated. Default 5. */
|
|
5684
|
+
readonly maxRedirects?: number;
|
|
5685
|
+
}
|
|
5686
|
+
|
|
5687
|
+
/**
|
|
5688
|
+
* Decide whether a URL may be fetched. Returns `null` when allowed, else the
|
|
5689
|
+
* reason. Fails CLOSED: a URL that will not parse is blocked.
|
|
5690
|
+
*
|
|
5691
|
+
* DNS is deliberately NOT resolved — this is a synchronous check on the literal
|
|
5692
|
+
* target, so a hostname that RESOLVES to a private address (DNS rebinding) is not
|
|
5693
|
+
* caught here. Blocking the direct-IP vector is the high-severity, cheap half;
|
|
5694
|
+
* rebinding needs network-layer egress control and is called out in the docs
|
|
5695
|
+
* rather than silently implied.
|
|
5696
|
+
*/
|
|
5697
|
+
export declare const ssrfReason: (rawUrl: string, policy?: SsrfPolicy) => string | null;
|
|
5698
|
+
|
|
5384
5699
|
/**
|
|
5385
5700
|
* Start the rpc-based runtime server on Node. Composes:
|
|
5386
5701
|
* - NodeHttpServer + NodeContext at the platform layer
|
|
@@ -5388,10 +5703,15 @@ export declare const snapshotMetricsSync: () => ReadonlyArray<MetricSample>;
|
|
|
5388
5703
|
* - RpcServer.layerHttpRouter (websocket protocol) consuming
|
|
5389
5704
|
* RpcSerialization.layerJson and the user's handlers layer
|
|
5390
5705
|
*
|
|
5391
|
-
*
|
|
5392
|
-
*
|
|
5393
|
-
|
|
5394
|
-
|
|
5706
|
+
* Forks `Layer.launch` so the Node event loop stays alive for the lifetime of the
|
|
5707
|
+
* layer's scope, and returns that scope's `shutdown` alongside the node server —
|
|
5708
|
+
* so a programmatic close releases the layer, not just the socket. SIGINT/SIGTERM
|
|
5709
|
+
* are wired once per process (not once per call) and drain every live launch.
|
|
5710
|
+
*/
|
|
5711
|
+
export declare const startRpcServer: <Rpcs extends Rpc.Any>(options: RpcServerOptions<Rpcs>) => {
|
|
5712
|
+
server: ReturnType<typeof createServer>;
|
|
5713
|
+
shutdown: () => Promise<void>;
|
|
5714
|
+
};
|
|
5395
5715
|
|
|
5396
5716
|
export declare const startScheduler: (schedules: ReadonlyArray<BrandedScheduleDefinition>, deps: SchedulerDeps) => Promise<SchedulerHandle>;
|
|
5397
5717
|
|
|
@@ -6555,6 +6875,6 @@ export declare interface WorkflowWaitOptions {
|
|
|
6555
6875
|
*/
|
|
6556
6876
|
export declare const wrapCaptureStore: (tx: DataStore, collect: (c: ForwardChange) => void) => DataStore;
|
|
6557
6877
|
|
|
6558
|
-
export declare const wrapStoreWithMixinBehaviour: (
|
|
6878
|
+
export declare const wrapStoreWithMixinBehaviour: (rawUnderlying: DataStore, ctx: StoreMiddlewareContext) => FluentStore;
|
|
6559
6879
|
|
|
6560
6880
|
export { }
|