@supacloud/elysia 0.17.0 → 0.19.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/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @supacloud/elysia
2
2
 
3
+ For a cross-module composition shared by HTTP, event/scheduled workers and a
4
+ trusted CLI, see the [fulfillment example](src/examples/fulfillment.ts) and
5
+ [developer guide](../../docs/framework-composition.md). It reuses bound commands,
6
+ explicit aspects and durable receipts, not a new workflow engine. Compensation
7
+ is a separately authorized business command, never an assumed rollback.
8
+
3
9
  ## Compatibility and Acceptance Boundary
4
10
 
5
11
  The dependency range is not a claim that every allowed version has been tested.
@@ -89,6 +95,81 @@ These gates prove the stated scenarios, not a full historical npm upgrade matrix
89
95
  or all business-domain isolation. See [framework acceptance](../../docs/framework-acceptance.md)
90
96
  for the evidence boundaries and upgrade policy.
91
97
 
98
+ ## Native HTTP Context And Static DI
99
+
100
+ Pass a native Elysia plugin as `http` to `createApplication`, `createTestApp`,
101
+ or the options argument of `createModulePlugin`. `decorate` shares existing
102
+ instances; `derive` runs before validation; `resolve` runs after validation.
103
+ Use scoped/global hooks, or finish a context plugin with `.as("scoped")`.
104
+ Local hooks retain native encapsulation and do not extend the consuming routes.
105
+
106
+ ```ts
107
+ import { Elysia, t } from "elysia";
108
+ import { createApplication } from "@supacloud/elysia";
109
+
110
+ const http = new Elysia({ name: "application-context" })
111
+ .decorate("clock", { now: () => Date.now() })
112
+ .derive(({ clock }) => ({ startedAt: clock.now() }))
113
+ .guard({ query: t.Object({ locale: t.Optional(t.String()) }) })
114
+ .resolve(({ query }) => ({ locale: query.locale ?? "en" }))
115
+ .as("scoped");
116
+
117
+ const app = createApplication({
118
+ http,
119
+ modules: compiledModules,
120
+ requestContext: (request, context) => ({
121
+ request,
122
+ startedAt: context.startedAt,
123
+ locale: context.locale,
124
+ }),
125
+ }).get("/locale", ({ locale, clock }) => ({
126
+ locale,
127
+ now: clock.now(),
128
+ }));
129
+ ```
130
+
131
+ The second `requestContext` argument contains the validated HTTP inputs and
132
+ the inferred native plugin extensions. Existing one-argument factories remain
133
+ valid. Its result is passed to generated request-scoped constructors and the
134
+ controller's `context`/`requestContext` input. It is built once per request;
135
+ early resolver responses and validation failures do not construct DI scopes.
136
+ Request scopes are created inside the existing governed handler and released
137
+ after the response, including handler failures. They are not application
138
+ singletons or native `resolve` hooks.
139
+
140
+ Native routes added to the returned app retain the HTTP plugin's decorator,
141
+ derive and resolve types. `createModulePlugin` also preserves the concrete
142
+ `services` type supplied by its caller. Module service bags are attached by
143
+ the module-local resolver, not merged into a root `decorator.services` bag;
144
+ the service instances themselves remain shared. This prevents same-named
145
+ services in sibling modules from overwriting each other's values or types.
146
+
147
+ Fresh compiler output preserves literal module names and inferred application
148
+ service factory results. Narrow a generated module by its `name`, call its
149
+ `createServices`, and pass that result to `createModulePlugin` to retain the
150
+ service types. Regenerate older artifacts to obtain this inference; explicitly
151
+ annotating them as `CompiledModule[]` still intentionally widens the types.
152
+ Compiled route schemas are runtime
153
+ descriptors: their body/params/query/header/cookie fields in the application-wide
154
+ context factory remain `unknown`-based instead of pretending to infer one
155
+ route's schema for every route. Use shared guards for schema-typed native
156
+ resolvers and the existing generated contracts for individual compiled routes.
157
+ Do not store per-request identity or transaction handles in decorated singletons.
158
+
159
+ The `http` plugin is composed into each compiled module and subsequently into
160
+ the root for native routes. Keep it focused on reusable context extensions;
161
+ register unrelated endpoints on the returned app. Hook execution is tested
162
+ for named/anonymous plugins and scoped/global hooks without duplicate work.
163
+ Anonymous extensions receive a stable internal plugin identity for native
164
+ hook deduplication; the caller's plugin configuration is not mutated.
165
+ This is native HTTP composition, not a replacement runtime DI container or
166
+ an arbitrary native-hook passthrough in compiled route descriptors.
167
+
168
+ `src/http-context.test.ts` covers lifecycle ordering, decoded inputs, failure
169
+ short-circuiting, context/service type inference, cross-module composition and
170
+ concurrent request isolation. Run it with `bun run typecheck:test` as well as
171
+ `bun test src/http-context.test.ts`; runtime tests alone do not verify inference.
172
+
92
173
  ## Persistent Command Adapters
93
174
 
94
175
  `createPersistentCommandAdapter(command, { identity, input })` binds a
@@ -112,6 +193,67 @@ authorization infrastructure failure is 503, and redacted-input lookup is 410.
112
193
  See [the migration plan](../../docs/command-migration.md) for compiler policy,
113
194
  authentication replay changes, deployment order and rollback limitations.
114
195
 
196
+ ## Bind A Command Once
197
+
198
+ `bindCompiledCommand` is an optional convenience layer over
199
+ `executeCompiledCommand` and `previewCompiledCommand`. Register static wiring
200
+ once and supply fresh trusted host context for each invocation:
201
+
202
+ ```ts
203
+ import { bindCompiledCommand } from "@supacloud/elysia";
204
+
205
+ const approve = bindCompiledCommand({
206
+ module: generatedApprovalModule,
207
+ command: "ApproveCommand", // compiled class name
208
+ governance,
209
+ handler: (input: ApproveInput, call) =>
210
+ approvalService.execute(input, call.requestContext),
211
+ decode: decodeApprovalResult,
212
+ preview: (input, call) =>
213
+ approvalService.preview(input, call.requestContext),
214
+ });
215
+
216
+ // In a trusted HTTP controller, Worker job, or server-side CLI adapter:
217
+ const result = await approve.execute(input, {
218
+ request,
219
+ requestContext: verifiedContext,
220
+ services,
221
+ scope,
222
+ });
223
+ ```
224
+
225
+ The generated module, domain types, service, decoder and governance above are
226
+ supplied by the application. The wrapper does not implement a second business
227
+ model, permission system, transaction mechanism or identity provider.
228
+
229
+ - A binding does not execute business code or capture a request identity.
230
+ Calls still resolve the descriptor and authorize each execution, including
231
+ idempotent replays. Static dependencies should be application-scoped; resolve
232
+ request/job-scoped services from the current `call.scope` instead of capturing
233
+ them in the binding.
234
+ - `preview` is present only when a domain preview function was supplied.
235
+ An explicit callback makes it callable without a presence check; dynamically
236
+ optional configuration still requires checking `approve.preview`. It reuses the existing read-only
237
+ preview API: authorization and domain preview only, no command execution,
238
+ aspects, transaction, idempotency, RPC or audit.
239
+ - Keep decoding/validating untrusted input at the existing ingress/domain
240
+ boundary. A TypeScript input type alone is not runtime validation.
241
+ - An HTTP handler calling `approve.execute` must not also bind that same
242
+ command through route-level `command:` metadata. Choose one execution
243
+ boundary to avoid duplicate authorization or aspect execution. Likewise,
244
+ place command aspects on the business module, not a duplicate entry module.
245
+ - Workers must resolve trusted identity in the host and explicitly construct
246
+ the call's `Request`, cancellation signal and idempotency context where
247
+ applicable. Do not infer identity from queue payloads. The binding adds no
248
+ retry, acknowledgement or scope-cleanup policy.
249
+ - The result decoder still runs after execution or receipt replay. A decoding
250
+ failure does not prove rollback and must not trigger a blind retry.
251
+
252
+ Existing direct APIs, route bindings, custom executors, RPC adapters and Worker
253
+ transports remain available without adopting the binding. Local parity tests
254
+ cover HTTP and Worker ingress with fake governance adapters; real database
255
+ atomicity remains covered by the separate PostgreSQL acceptance gates.
256
+
115
257
  Runtime adapter that turns `@supacloud/compiler` output into a production-ready
116
258
  [Elysia](https://elysiajs.com/) application.
117
259
 
@@ -526,3 +668,180 @@ registration instead of silently disabling response validation.
526
668
 
527
669
  See [type safety and migration](../../docs/type-safety.md) and [command migration](../../docs/command-migration.md) for examples and
528
670
  the distinction between contract declarations and runtime verification.
671
+
672
+ ## Declarative HTTP Policies
673
+
674
+ Business dependency wiring remains generated constructors and factories. HTTP
675
+ policies are selected using existing compiler-preserved route metadata:
676
+
677
+ ```ts
678
+ @Get("/:id", {
679
+ data: { httpPolicies: [{ name: "authenticated" }] },
680
+ })
681
+ getItem() { /* domain handler */ }
682
+ ```
683
+
684
+ Register implementations at the HTTP composition root:
685
+
686
+ ```ts
687
+ const app = createApplication({
688
+ modules: createCompiledModules(),
689
+ http: identityPlugin,
690
+ httpPolicies: {
691
+ authenticated: (options, route) => {
692
+ // Validate options here. This factory runs once per declared route policy.
693
+ return async ({ http }) => {
694
+ if (!http.identity) {
695
+ return new Response("Unauthorized", { status: 401 });
696
+ }
697
+ };
698
+ },
699
+ },
700
+ });
701
+ ```
702
+
703
+ `identityPlugin` is an application-owned scoped/global Elysia plugin that verifies
704
+ credentials and resolves `identity`; the adapter does not trust a raw user/tenant
705
+ header as identity. Policy callbacks preserve its native context types.
706
+
707
+ Policies become native route-local `beforeHandle` hooks. They execute sequentially
708
+ after schema validation, native resolvers and the application request-context
709
+ factory, but before compiled request-scope construction and command execution.
710
+ Return `undefined` to continue or a `Response` to stop; thrown errors use the
711
+ application error mapper. Native earlier hooks can still short-circuit the request.
712
+ Unknown policies, malformed declarations and invalid factories reject startup.
713
+ Routes without declarations do not install a policy hook.
714
+
715
+ Use the registry for custom resource access or application-owned HTTP policies.
716
+ For bundled security, rate limiting, caching and tracing, use the suite below. Shared factories
717
+ must not retain mutable per-request state; use the callback's request/context.
718
+ Cleanup belongs to native lifecycle hooks. Transactions, durable audit, idempotency
719
+ and recovery remain in command governance, not HTTP policies.
720
+
721
+ Policy metadata can appear in generated clients: never put secrets in options.
722
+ Changes to declarations or registry configuration require creating a new application.
723
+
724
+ ### Built-In Security And Governance
725
+
726
+ ```ts
727
+ import {
728
+ createApplication, createHttpPolicySuite, createHttpTelemetry,
729
+ createMemoryHttpRateLimitStore, createMemoryHttpCacheStore,
730
+ } from "@supacloud/elysia";
731
+
732
+ const suite = createHttpPolicySuite({
733
+ auth: supAuthOptions,
734
+ cacheNamespace: "release-2026-09-25",
735
+ rateLimitStore: createMemoryHttpRateLimitStore({ maxEntries: 10_000 }),
736
+ cacheStore: createMemoryHttpCacheStore({ maxEntries: 1_000, maxBytes: 8 * 1024 * 1024 }),
737
+ });
738
+ const app = createApplication({
739
+ ...suite,
740
+ modules: createCompiledModules(),
741
+ http: createHttpTelemetry((event) => logger.info(event)),
742
+ });
743
+ ```
744
+
745
+ `supAuthOptions` uses `createSupAuthRequestContext`'s existing configuration:
746
+ trusted issuer, audience, application client ID, project ID, HTTPS JWKS endpoint
747
+ and `resolveAccess` for current server-side tenant membership/permissions.
748
+ JWT verification and membership resolution run on every credentialed request,
749
+ including cache hits. They are not replaced by a tenant or user header.
750
+ Requests without Authorization get an anonymous identity; public routes may
751
+ remain anonymous. Invalid supplied credentials are rejected even on public routes.
752
+ Use the suite's paired `requestContext`; the registry cannot trust fabricated
753
+ context objects or forwarded subjects. Additional native context plugins may be
754
+ composed with the telemetry plugin using ordinary Elysia `.use(...)`.
755
+
756
+ Routes select built-ins through metadata (use `BuiltinHttpPolicyDeclaration`
757
+ with TypeScript `satisfies` for author-time option checking):
758
+
759
+ ```ts
760
+ @Get("/tenants/:tenant/items", {
761
+ data: {
762
+ httpPolicies: [
763
+ { name: "authenticated" },
764
+ { name: "tenant", options: { param: "tenant" } },
765
+ { name: "permission", options: { allOf: ["items.read"] } },
766
+ { name: "rateLimit", options: { limit: 120, windowMs: 60_000 } },
767
+ { name: "cache", options: { ttlMs: 5_000, maxBodyBytes: 262_144 } },
768
+ ],
769
+ },
770
+ })
771
+ listItems() { /* use the verified tenant in repository queries */ }
772
+ ```
773
+
774
+ - `authenticated`: requires successful JWT verification and active application access.
775
+ - `tenant`: matches a validated route parameter to that access record's tenant.
776
+ It is an HTTP boundary check, not automatic repository filtering or database RLS.
777
+ - `permission`: requires every exact permission in `allOf`; no wildcard inference.
778
+ - `rateLimit`: fixed-window quota scoped to route, issuer, application, actor and
779
+ tenant. It does not trust forwarded IP headers. Denials return 429 with
780
+ `Retry-After`; unavailable storage returns a sanitized 503. Anonymous/IP abuse
781
+ protection belongs at the trusted proxy or an explicitly configured native hook.
782
+ - `cache`: authenticated, private GET query caching only; commands are rejected.
783
+ It must be last, so cache hits cannot skip declared permission or quota checks.
784
+ Keys hash trusted identity, permissions, complete URL and request headers
785
+ except the correlation ID. Credentials and query contents are not stored as keys.
786
+ Only successful plain JSON object/array results are stored. Native responses,
787
+ streams, errors, oversized output, cookies, Vary, custom response headers and
788
+ cache-control prohibitions are conservatively excluded. Conditional/range
789
+ requests and request no-cache/no-store bypass caching. Cache-read outages return
790
+ 503; post-response write failures notify `onCacheWriteError` (sanitized warning
791
+ by default) without changing a completed response.
792
+ - `createHttpTelemetry`: emits immutable request ID, method, static route template,
793
+ final status and duration after responses, including denied and invalid requests.
794
+ The response header and command/request context share the same correlation ID.
795
+ It never emits raw paths, query parameters, tokens, bodies or errors. Observer
796
+ failures cannot change business results. Connect the observer to your logger or
797
+ telemetry exporter; this is request tracing, not an OpenTelemetry backend.
798
+
799
+ The memory stores are explicitly **single-process**. Quota capacity exhaustion
800
+ fails closed rather than evicting active quotas; local cache storage is bounded
801
+ by entry count and byte budget. They do not coordinate replicas.
802
+
803
+ ### Shared PostgreSQL Stores
804
+
805
+ Apply `HTTP_POLICY_STORE_SQL` through normal migrations, then use
806
+ `createPostgresHttpPolicyStores(database)`. Its database port takes a parameterized
807
+ `query(text, parameters)` function; it does not own a pool or transaction scope:
808
+
809
+ ```ts
810
+ const stores = createPostgresHttpPolicyStores({
811
+ query: async (text, parameters) => Array.from(await sql.unsafe(text, [...parameters])),
812
+ });
813
+ const suite = createHttpPolicySuite({ auth: supAuthOptions, cacheNamespace: deploymentId, ...stores });
814
+ ```
815
+
816
+ Concurrent replicas share an atomic row-locked quota and persistent
817
+ cache entries. Tables live in `supacloud_http` with no PUBLIC privileges. Grant
818
+ only the server runtime's database role access; never expose store credentials to
819
+ clients. Different applications/issuers/tenants/users have distinct keys.
820
+
821
+ Schedule `stores.prune()` to remove expired rows and monitor database size.
822
+ `cacheNamespace` is mandatory when a cache policy is declared. Use the same
823
+ namespace across replicas of one release and a different namespace for every
824
+ representation/schema/security-rule revision. This prevents old in-flight requests
825
+ from repopulating the current release's cache; reusing a namespace opts into reuse.
826
+ Use TTLs appropriate for stale-read tolerance and invoke `cacheStore.clear()` only
827
+ after a confirmed write when explicit broad invalidation is desired. Clear advances
828
+ a shared generation atomically; fills from older generations are rejected. Already
829
+ in-flight HTTP responses are not cancelled. Custom stores must implement the same
830
+ generation/check-and-write contract. Do not cache responses
831
+ containing per-request IDs, nonces or time-sensitive authorization decisions.
832
+ These stores do not provide business transactions or durable audit.
833
+
834
+ ### Reproducible Performance Checks
835
+
836
+ Run `bun run bench:http-policy [output.json]`. Optional environment settings:
837
+ `BENCH_REQUESTS`, `BENCH_ROUNDS`, `BENCH_CONCURRENCY`. It compares native static
838
+ Elysia, compiled static DI, one no-op policy, and a full verified policy pipeline.
839
+ The harness warms each case, rotates case order, validates every response and
840
+ records throughput, P50/P95/P99, live heap deltas and RSS. Cache hits and misses
841
+ are separate scenarios with asserted handler/hit/fill counts; sorting is outside
842
+ the throughput timer.
843
+
844
+ Loopback results include the same-process fetch client; heap deltas are affected
845
+ by GC and are **not total allocation counts**. Full-policy results include local
846
+ ES256 verification, not remote identity/database latency. See
847
+ [acceptance evidence](../../docs/http-policy-acceptance.md) for the measured scope.
@@ -0,0 +1,33 @@
1
+ import type { CommandPreview } from "@supacloud/contracts";
2
+ import { type CommandGovernance, type CompiledModule, type ExecutionObserver } from "./index";
3
+ /** Supplied by the trusted host for each call, never captured as binding configuration. */
4
+ export interface CompiledCommandCallContext {
5
+ request: Request;
6
+ requestContext: unknown;
7
+ services?: Record<string, unknown>;
8
+ scope?: Record<string, unknown>;
9
+ }
10
+ export interface CompiledCommandBindingOptions<Input, Result> {
11
+ module: Pick<CompiledModule, "name" | "commands" | "aspects" | "aspectPipeline">;
12
+ /** Compiled command class name, as required by executeCompiledCommand. */
13
+ command: string;
14
+ governance: CommandGovernance;
15
+ observer?: ExecutionObserver;
16
+ handler(input: Input, context: CompiledCommandCallContext): Result | Promise<Result>;
17
+ decode(value: unknown): Result;
18
+ preview?(input: Input, context: CompiledCommandCallContext): unknown;
19
+ }
20
+ export interface CompiledCommandBinding<Input, Result> {
21
+ execute(input: Input, context: CompiledCommandCallContext): Promise<Result>;
22
+ preview?(input: Input, context: CompiledCommandCallContext): Promise<CommandPreview>;
23
+ }
24
+ /**
25
+ * Binds static wiring, not an identity or an execution outcome.
26
+ * All policy, replay, aspects and decoding remain in the existing command APIs.
27
+ */
28
+ export declare function bindCompiledCommand<Input, Result>(options: CompiledCommandBindingOptions<Input, Result> & {
29
+ preview: NonNullable<CompiledCommandBindingOptions<Input, Result>["preview"]>;
30
+ }): CompiledCommandBinding<Input, Result> & {
31
+ preview(input: Input, context: CompiledCommandCallContext): Promise<CommandPreview>;
32
+ };
33
+ export declare function bindCompiledCommand<Input, Result>(options: CompiledCommandBindingOptions<Input, Result>): CompiledCommandBinding<Input, Result>;
@@ -0,0 +1,38 @@
1
+ import type { DurableCommandReceipt } from "@supacloud/contracts";
2
+ import type { CompiledCommandBinding, CompiledCommandCallContext } from "../command-binding";
3
+ export interface OrderInput {
4
+ orderId: string;
5
+ }
6
+ export interface ReservationInput extends OrderInput {
7
+ reservationId: string;
8
+ }
9
+ export interface ReservationResult {
10
+ reservationId: string;
11
+ }
12
+ export interface PaymentResult {
13
+ outcome: "paid" | "declined";
14
+ }
15
+ export interface OrderResult {
16
+ orderId: string;
17
+ }
18
+ export interface FulfillmentSteps {
19
+ reserve: CompiledCommandBinding<OrderInput, DurableCommandReceipt<ReservationResult>>;
20
+ charge: CompiledCommandBinding<ReservationInput, DurableCommandReceipt<PaymentResult>>;
21
+ confirm: CompiledCommandBinding<ReservationInput, DurableCommandReceipt<OrderResult>>;
22
+ release: CompiledCommandBinding<ReservationInput, DurableCommandReceipt<OrderResult>>;
23
+ }
24
+ export type FulfillmentOutcome = {
25
+ status: "completed" | "declined";
26
+ orderId: string;
27
+ } | {
28
+ status: "pending";
29
+ step: keyof FulfillmentSteps;
30
+ operationId: string;
31
+ };
32
+ /**
33
+ * Application-owned composition, not a scheduler or a distributed transaction.
34
+ * Each bound step owns authorization, durable receipts and its local transaction.
35
+ */
36
+ export declare function createFulfillment(steps: FulfillmentSteps): {
37
+ execute(input: OrderInput, context: CompiledCommandCallContext): Promise<FulfillmentOutcome>;
38
+ };
@@ -1,4 +1,7 @@
1
+ import { UpdateWebhook } from "../webhook/update.command";
1
2
  export interface CompiledRoute {
3
+ parse?: "none";
4
+ allowDeleteBody?: true;
2
5
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
3
6
  path: string;
4
7
  handler: string;
@@ -101,7 +104,14 @@ export interface CompiledModule {
101
104
  }
102
105
  type CompiledAspectObserver = (stage: string, run: () => unknown | Promise<unknown>) => unknown | Promise<unknown>;
103
106
  type CompiledAspectPipeline = (context: CompiledAspectContext, next: () => unknown | Promise<unknown>, observe?: CompiledAspectObserver) => unknown | Promise<unknown>;
104
- export declare function createCompiledModules(): CompiledModule[];
107
+ export type CompiledApplicationModule = Omit<CompiledModule, "name" | "createServices"> & ({
108
+ name: "webhook";
109
+ createServices: typeof createWebhookServices;
110
+ });
111
+ export declare function createCompiledModules(): CompiledApplicationModule[];
105
112
  export declare function initializeApplication(services: Record<string, unknown>): Promise<void>;
106
113
  export declare function destroyApplication(services: Record<string, unknown>): Promise<void>;
114
+ declare function createWebhookServices(deps: Record<string, unknown>, imported: Record<string, Record<string, unknown>>): {
115
+ updateWebhook: UpdateWebhook;
116
+ };
107
117
  export {};
@@ -0,0 +1,12 @@
1
+ import type { HttpCacheStore, HttpRateLimitStore } from "./http-policy-stores";
2
+ export interface HttpPolicyDatabase {
3
+ query(text: string, parameters: readonly unknown[]): Promise<readonly Record<string, unknown>[]>;
4
+ }
5
+ /** Apply through migrations, never from a request. Credentials must be server-only. */
6
+ export declare const HTTP_POLICY_STORE_SQL = "\nCREATE SCHEMA IF NOT EXISTS supacloud_http;\nREVOKE ALL ON SCHEMA supacloud_http FROM PUBLIC;\nCREATE TABLE IF NOT EXISTS supacloud_http.rate_limits (\n key text PRIMARY KEY CHECK (key ~ '^[a-f0-9]{64}$'),\n used bigint NOT NULL CHECK (used > 0),\n reset_at timestamptz NOT NULL\n);\nCREATE INDEX IF NOT EXISTS http_rate_limit_expiry ON supacloud_http.rate_limits(reset_at);\nCREATE TABLE IF NOT EXISTS supacloud_http.response_cache (\n key text PRIMARY KEY CHECK (key ~ '^[a-f0-9]{64}$'),\n body text NOT NULL CHECK (octet_length(body) <= 16777216),\n expires_at timestamptz NOT NULL,\n generation uuid NOT NULL\n);\nALTER TABLE supacloud_http.response_cache ADD COLUMN IF NOT EXISTS generation uuid\n NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000';\nCREATE TABLE IF NOT EXISTS supacloud_http.cache_generation (\n singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),\n generation uuid NOT NULL DEFAULT gen_random_uuid()\n);\nINSERT INTO supacloud_http.cache_generation(singleton) VALUES (true) ON CONFLICT DO NOTHING;\nCREATE INDEX IF NOT EXISTS http_response_cache_expiry ON supacloud_http.response_cache(expires_at);\nREVOKE ALL ON ALL TABLES IN SCHEMA supacloud_http FROM PUBLIC;\nCREATE OR REPLACE FUNCTION supacloud_http.consume_rate_limit(p_key text, p_limit bigint, p_window_ms integer)\nRETURNS TABLE(allowed boolean, remaining bigint, reset_at_ms numeric)\nLANGUAGE plpgsql SET search_path = pg_catalog, supacloud_http AS $rate$\nDECLARE\n entry supacloud_http.rate_limits%ROWTYPE;\n observed_at timestamptz;\nBEGIN\n IF p_limit < 1 OR p_limit > 1000000000 OR p_window_ms < 1 OR p_window_ms > 86400000 THEN\n RAISE EXCEPTION 'Invalid rate limit window';\n END IF;\n LOOP\n INSERT INTO supacloud_http.rate_limits(key, used, reset_at)\n VALUES (p_key, 1, '-infinity') ON CONFLICT DO NOTHING;\n SELECT * INTO entry FROM supacloud_http.rate_limits WHERE key=p_key FOR UPDATE;\n EXIT WHEN FOUND;\n -- Expiry pruning may remove a conflicting row before we acquire its lock.\n END LOOP;\n observed_at := clock_timestamp();\n IF entry.reset_at <= observed_at THEN\n entry.used := 1;\n entry.reset_at := observed_at + p_window_ms * interval '1 millisecond';\n ELSE\n entry.used := least(entry.used + 1, p_limit + 1);\n END IF;\n UPDATE supacloud_http.rate_limits SET used=entry.used, reset_at=entry.reset_at WHERE key=p_key;\n RETURN QUERY SELECT entry.used <= p_limit, greatest(p_limit-entry.used,0),\n extract(epoch FROM entry.reset_at)*1000;\nEND\n$rate$;\nREVOKE ALL ON FUNCTION supacloud_http.consume_rate_limit(text,bigint,integer) FROM PUBLIC;\n";
7
+ /** Independent instances sharing a database share atomic quotas and private cache entries. */
8
+ export declare function createPostgresHttpPolicyStores(database: HttpPolicyDatabase): {
9
+ rateLimitStore: HttpRateLimitStore;
10
+ cacheStore: HttpCacheStore;
11
+ prune(): Promise<void>;
12
+ };
@@ -0,0 +1,33 @@
1
+ export interface HttpRateLimitResult {
2
+ allowed: boolean;
3
+ remaining: number;
4
+ resetAt: number;
5
+ }
6
+ export interface HttpRateLimitStore {
7
+ /** Must atomically consume one request across every process sharing this store. */
8
+ consume(key: string, limit: number, windowMs: number): HttpRateLimitResult | Promise<HttpRateLimitResult>;
9
+ }
10
+ export interface HttpCacheEntry {
11
+ body: string;
12
+ contentType: string;
13
+ expiresAt: number;
14
+ }
15
+ export interface HttpCacheStore {
16
+ generation(): string | Promise<string>;
17
+ get(key: string, generation: string): HttpCacheEntry | undefined | Promise<HttpCacheEntry | undefined>;
18
+ /** Atomically refuse fills from an invalidated generation. */
19
+ set(key: string, value: HttpCacheEntry, generation: string): void | Promise<void>;
20
+ /** Invalidation is explicit, never inferred from a write that may have failed. */
21
+ clear(): void | Promise<void>;
22
+ }
23
+ /** Single-process fixed windows. Capacity exhaustion fails closed, not quota eviction. */
24
+ export declare function createMemoryHttpRateLimitStore(options?: {
25
+ maxEntries?: number;
26
+ now?: () => number;
27
+ }): HttpRateLimitStore;
28
+ /** Bounded local LRU; use a shared store for cross-process caching/invalidation. */
29
+ export declare function createMemoryHttpCacheStore(options?: {
30
+ maxEntries?: number;
31
+ maxBytes?: number;
32
+ now?: () => number;
33
+ }): HttpCacheStore;
@@ -0,0 +1,75 @@
1
+ import { type SupaCloudRequestContext } from "./index";
2
+ import { type SupAuthContextOptions, type SupAuthRequestContext } from "./identity";
3
+ import type { HttpPolicy } from "./http-policy";
4
+ import type { HttpCacheStore, HttpRateLimitStore } from "./http-policy-stores";
5
+ export type BuiltinHttpPolicyDeclaration = {
6
+ name: "authenticated";
7
+ options?: never;
8
+ } | {
9
+ name: "tenant";
10
+ options: {
11
+ param: string;
12
+ };
13
+ } | {
14
+ name: "permission";
15
+ options: {
16
+ allOf: readonly string[];
17
+ };
18
+ } | {
19
+ name: "rateLimit";
20
+ options: {
21
+ limit: number;
22
+ windowMs: number;
23
+ };
24
+ } | {
25
+ name: "cache";
26
+ options: {
27
+ ttlMs: number;
28
+ maxBodyBytes?: number;
29
+ };
30
+ };
31
+ export interface HttpPolicySuiteOptions {
32
+ auth: SupAuthContextOptions;
33
+ rateLimitStore?: HttpRateLimitStore;
34
+ cacheStore?: HttpCacheStore;
35
+ /** Required for cache policies. Change for each representation/deployment version. */
36
+ cacheNamespace?: string;
37
+ /** Sanitized operational notification; no exception, key, user or payload. */
38
+ onCacheWriteError?: () => void;
39
+ }
40
+ export declare function policyOptions(value: unknown, allowed: readonly string[]): Record<string, unknown>;
41
+ export declare function positiveInteger(value: unknown, max: number): number;
42
+ export declare function policyKey(parts: unknown[]): Promise<string>;
43
+ export declare function principalKey(context: SupAuthRequestContext): unknown[];
44
+ /** Pair the returned factory and registry; only this verifier can populate policy identity. */
45
+ export declare function createHttpPolicySuite(options: HttpPolicySuiteOptions): {
46
+ requestContext: (request: Request) => Promise<SupaCloudRequestContext | SupAuthRequestContext>;
47
+ httpPolicies: Readonly<Record<string, (options: unknown, route: Readonly<Pick<import("./index").CompiledRoute, "method" | "path" | "command">>) => HttpPolicy<import("elysia").default<"", {
48
+ decorator: {};
49
+ store: {};
50
+ derive: {};
51
+ resolve: {};
52
+ }, {
53
+ typebox: {};
54
+ error: {};
55
+ }, {
56
+ schema: {};
57
+ standaloneSchema: {};
58
+ macro: {};
59
+ macroFn: {};
60
+ parser: {};
61
+ response: {};
62
+ }, {}, {
63
+ derive: {};
64
+ resolve: {};
65
+ schema: {};
66
+ standaloneSchema: {};
67
+ response: {};
68
+ }, {
69
+ derive: {};
70
+ resolve: {};
71
+ schema: {};
72
+ standaloneSchema: {};
73
+ response: {};
74
+ }>>>>;
75
+ };
@@ -0,0 +1,25 @@
1
+ import type { AnyElysia, Elysia } from "elysia";
2
+ import type { ApplicationHttpContext, CompiledRoute } from "./index";
3
+ export interface HttpPolicyDeclaration {
4
+ name: string;
5
+ options?: unknown;
6
+ }
7
+ export interface HttpPolicyContext<Http extends AnyElysia = Elysia> {
8
+ http: ApplicationHttpContext<Http>;
9
+ requestContext: unknown;
10
+ }
11
+ export interface HttpPolicyResponseContext<Http extends AnyElysia = Elysia> extends HttpPolicyContext<Http> {
12
+ response: unknown;
13
+ }
14
+ export type HttpPolicy<Http extends AnyElysia = Elysia> = ((context: HttpPolicyContext<Http>) => void | Response | Promise<void | Response>) & {
15
+ /** Terminal policies (cache hits) must not skip later security checks. */
16
+ terminal?: boolean;
17
+ mapResponse?: (context: HttpPolicyResponseContext<Http>) => void | Response | Promise<void | Response>;
18
+ afterResponse?: (context: HttpPolicyResponseContext<Http>) => void | Promise<void>;
19
+ };
20
+ /** Factories validate configuration once at startup, never per request. */
21
+ export type HttpPolicyRegistry<Http extends AnyElysia = Elysia> = Readonly<Record<string, (options: unknown, route: Readonly<Pick<CompiledRoute, "method" | "path" | "command">>) => HttpPolicy<Http>>>;
22
+ export declare class HttpPolicyConfigurationError extends Error {
23
+ readonly code = "HTTP_POLICY_CONFIGURATION_INVALID";
24
+ }
25
+ export declare function compileHttpPolicies<Http extends AnyElysia>(route: CompiledRoute, path: string, registry?: HttpPolicyRegistry<Http>): HttpPolicy<Http>[];
@@ -0,0 +1,14 @@
1
+ import type { SupAuthRequestContext } from "./identity";
2
+ import type { HttpPolicy } from "./http-policy";
3
+ import type { HttpCacheStore } from "./http-policy-stores";
4
+ interface CachePolicyOptions {
5
+ store: HttpCacheStore;
6
+ requireAccess(request: Request): SupAuthRequestContext;
7
+ ttlMs: number;
8
+ maxBodyBytes: number;
9
+ route: string;
10
+ namespace: string;
11
+ onWriteError(): void;
12
+ }
13
+ export declare function createCachePolicy(options: CachePolicyOptions): HttpPolicy;
14
+ export {};
@@ -0,0 +1,40 @@
1
+ import { Elysia } from "elysia";
2
+ export declare function httpRequestId(request: Request): string | undefined;
3
+ export interface HttpTelemetryEvent {
4
+ requestId: string;
5
+ method: string;
6
+ /** Static route template only. Query strings and concrete paths are excluded. */
7
+ route: string;
8
+ status: number;
9
+ durationMs: number;
10
+ }
11
+ export type HttpTelemetryObserver = (event: Readonly<HttpTelemetryEvent>) => void | Promise<void>;
12
+ /** Request tracing is best effort, not a durable business audit. */
13
+ export declare function createHttpTelemetry(observe: HttpTelemetryObserver): Elysia<"", {
14
+ decorator: {};
15
+ store: {};
16
+ derive: {};
17
+ resolve: {};
18
+ }, {
19
+ typebox: {};
20
+ error: {};
21
+ }, {
22
+ schema: {};
23
+ standaloneSchema: {};
24
+ macro: {};
25
+ macroFn: {};
26
+ parser: {};
27
+ response: {};
28
+ }, {}, {
29
+ derive: {};
30
+ resolve: {};
31
+ schema: {};
32
+ standaloneSchema: {};
33
+ response: {};
34
+ }, {
35
+ derive: {};
36
+ resolve: {};
37
+ schema: {};
38
+ standaloneSchema: {};
39
+ response: {};
40
+ }>;