@supacloud/elysia 0.9.0 → 0.10.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
@@ -102,6 +102,66 @@ fails partway through.
102
102
 
103
103
  ## API
104
104
 
105
+ ### External SupAuth Identity
106
+
107
+ `createSupAuthRequestContext(options)` supplies the trusted host adapter for an
108
+ external SupAuth user center. It uses `jose` signature verification, requires
109
+ configured HTTPS issuer/JWKS endpoints, audience, subject, expiry and issued-at,
110
+ and accepts only ES256/RS256. It does not implement login, sessions or token issuance.
111
+
112
+ ```ts
113
+ import { createSupAuthRequestContext } from "@supacloud/elysia";
114
+
115
+ const requestContext = createSupAuthRequestContext({
116
+ issuer: "https://identity.example/auth/v1",
117
+ audience: "authenticated",
118
+ clientId: "orders-oauth-client",
119
+ projectId: "orders",
120
+ jwksUrl: "https://identity.example/auth/v1/.well-known/jwks.json",
121
+ resolveAccess: async (identity) =>
122
+ accessRepository.findCurrentAccess(identity.issuer, identity.subject, "orders"),
123
+ });
124
+ ```
125
+
126
+ `accessRepository` is application-owned and must return `{ projectId, tenantId,
127
+ permissions }` or `null` from authoritative local data. The factory rejects
128
+ missing/wrong-project access, ignores forwarded subject/tenant headers and
129
+ returns a frozen identity/access snapshot. Commands must still authorize current
130
+ object relationships within their durable transaction; a permission snapshot is
131
+ not an RLS replacement. The bearer credential is non-enumerable on identity.
132
+ Never log the complete request/context.
133
+
134
+ The adapter protects all routes using that context factory, including health
135
+ routes; mount intentionally public routes separately. Invalid credentials and
136
+ invalid signing keys fail closed with sanitized 401 responses. Tokens must have
137
+ `role: "authenticated"` and a matching `client_id` or `azp`; when both exist they
138
+ must match each other and the configured `clientId`. Verification service failures
139
+ return sanitized 503 `AUTHENTICATION_UNAVAILABLE`, never an identity fallback. Remote
140
+ JWKS uses bounded fetch timeout and the library's key cache; no token-provided key
141
+ URL or local identity fallback is accepted. `keyResolver` is a trusted host
142
+ override for pinned key sets/testing, never request input.
143
+
144
+ ### Execution Inspection
145
+
146
+ Set `createApplication({ onExecution })` for metadata-only events: operation,
147
+ stage, kind, phase, elapsed time and a bounded request correlation ID. Module,
148
+ route and command aspects retain declared order. Standard governance exposes
149
+ authorization, idempotency, transaction, handler and successful audit stages.
150
+ Pass the final optional observer argument to `executeJob` for job traces.
151
+ No request input, token, result or error cause is sent to the observer.
152
+ The boundary and aspect index identify the static declaration; JavaScript
153
+ function names are display hints and may change when consumers minify a bundle.
154
+
155
+ Observer failures are isolated from business results; this is best-effort
156
+ telemetry, not durable audit. Use command governance for mandatory audit.
157
+ An inner successful stage does not prove the enclosing transaction committed.
158
+ The compiler's `context`/`explain` commands show the corresponding static plan.
159
+
160
+ Command transaction/idempotency continuations, custom route executors and job
161
+ handlers reject repeated invocation. This prevents accidental adapter retries
162
+ inside one invocation; cross-request/process deduplication still requires a
163
+ durable idempotency adapter.
164
+
105
165
  ### `validatedJsonResponse(validate, value, init?): Response`
106
166
 
107
167
  Constructs a native JSON response after a synchronous, caller-owned type guard
@@ -0,0 +1,12 @@
1
+ export interface ExecutionEvent {
2
+ kind: "route" | "command" | "job";
3
+ operation: string;
4
+ stage: string;
5
+ phase: "started" | "succeeded" | "failed";
6
+ requestId?: string;
7
+ durationMs?: number;
8
+ }
9
+ /** Metadata only: never receives request bodies, credentials, results or errors. */
10
+ export type ExecutionObserver = (event: Readonly<ExecutionEvent>) => void | Promise<void>;
11
+ export declare function observeExecution<T>(observer: ExecutionObserver | undefined, event: Pick<ExecutionEvent, "kind" | "operation" | "stage" | "requestId">, next: () => T | Promise<T>): Promise<T>;
12
+ export declare function executionRequestId(context: unknown): string | undefined;
@@ -0,0 +1,37 @@
1
+ import { type JWTVerifyGetKey } from "jose";
2
+ import { type SupaCloudRequestContext, type TrustedRequestIdentity } from "./index";
3
+ export interface SupAuthIdentity extends TrustedRequestIdentity {
4
+ authenticated: true;
5
+ subject: string;
6
+ issuer: string;
7
+ clientId: string;
8
+ }
9
+ export interface SupAuthAccess {
10
+ projectId: string;
11
+ tenantId: string;
12
+ permissions: readonly string[];
13
+ }
14
+ export interface SupAuthRequestContext extends SupaCloudRequestContext {
15
+ identity: SupAuthIdentity;
16
+ access: Readonly<SupAuthAccess>;
17
+ }
18
+ export interface SupAuthContextOptions {
19
+ issuer: string;
20
+ audience: string;
21
+ /** SupAuth OAuth application binding, independent of audience/project membership. */
22
+ clientId: string;
23
+ projectId: string;
24
+ /** Explicit trusted JWKS endpoint; never read from a token's jku/x5u header. */
25
+ jwksUrl: string;
26
+ /** Defaults to ES256 and RS256; symmetric algorithms are not supported. */
27
+ algorithms?: readonly ("ES256" | "RS256")[];
28
+ /** Trusted host override for pinned/local keys and deterministic tests. */
29
+ keyResolver?: JWTVerifyGetKey;
30
+ /** Read current application-local access, not user-supplied tenant headers. */
31
+ resolveAccess(identity: Readonly<SupAuthIdentity>, request: Request): Promise<SupAuthAccess | null>;
32
+ }
33
+ /**
34
+ * External user-center verification only. SupAuth/GoTrue still owns login,
35
+ * passwords, sessions and token issuance; applications own access decisions.
36
+ */
37
+ export declare function createSupAuthRequestContext(options: SupAuthContextOptions): (request: Request) => Promise<SupAuthRequestContext>;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { Elysia } from "elysia";
2
+ import { type ExecutionObserver } from "./execution";
3
+ export type { ExecutionEvent, ExecutionObserver } from "./execution";
2
4
  export interface CompiledRoute {
3
5
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
4
6
  path: string;
@@ -23,6 +25,7 @@ export interface CompiledRoute {
23
25
  aspects?: ApplicationAspect[];
24
26
  }
25
27
  export interface CompiledCommand {
28
+ rpc?: string;
26
29
  className: string;
27
30
  name: string;
28
31
  permission?: string;
@@ -61,6 +64,8 @@ export interface CompiledModule {
61
64
  aspects?: ApplicationAspect[];
62
65
  }
63
66
  export type RequestContextFactory = (request: Request) => unknown | Promise<unknown>;
67
+ export { createSupAuthRequestContext } from "./identity";
68
+ export type { SupAuthIdentity, SupAuthAccess, SupAuthRequestContext, SupAuthContextOptions } from "./identity";
64
69
  export declare const VERIFIED_JWT_SUBJECT_HEADER = "x-supacloud-jwt-sub";
65
70
  export declare const EXECUTION_ID_HEADER = "x-sb-execution-id";
66
71
  export declare const IDEMPOTENCY_KEY_HEADER = "idempotency-key";
@@ -116,6 +121,15 @@ export interface CommandAudit {
116
121
  failed(invocation: CommandInvocation, error: unknown): void | Promise<void>;
117
122
  }
118
123
  export interface CommandGovernance {
124
+ /** Application-owned adapters: a single RPC owns all declared persistence. */
125
+ rpc?: Record<string, {
126
+ capabilities: {
127
+ audit?: boolean;
128
+ transaction?: boolean;
129
+ idempotency?: boolean;
130
+ };
131
+ execute: CommandMiddleware;
132
+ }>;
119
133
  authorize: CommandAuthorizer;
120
134
  idempotency?: CommandMiddleware;
121
135
  transaction?: CommandMiddleware;
@@ -171,6 +185,8 @@ export interface ApplicationOptions {
171
185
  commandExecutor?: CommandExecutor;
172
186
  /** Maps framework or application failures to the public HTTP contract. */
173
187
  errorMapper?: ErrorMapper;
188
+ /** Best-effort execution metadata only; durable audit belongs to governance. */
189
+ onExecution?: ExecutionObserver;
174
190
  }
175
191
  export interface JobInvocation {
176
192
  job: CompiledJob;
@@ -185,13 +201,27 @@ export type JobExecutor = (invocation: JobInvocation, next: () => unknown | Prom
185
201
  * Runtime. The runtime strips incoming x-supacloud-jwt-sub values and writes
186
202
  * the header only after JWT verification.
187
203
  */
188
- export declare const createSupaCloudRequestContext: RequestContextFactory;
204
+ export declare const createSupaCloudRequestContext: (request: Request) => SupaCloudRequestContext;
189
205
  /**
190
206
  * Compose the standard command governance order:
191
207
  * authorization -> idempotency -> transaction -> audit -> handler.
192
208
  * Declared governance metadata fails closed when its adapter is absent.
193
209
  */
194
- export declare function createCommandExecutor(governance: CommandGovernance): CommandExecutor;
210
+ export declare function createCommandExecutor(governance: CommandGovernance, observer?: ExecutionObserver): CommandExecutor;
211
+ /** Execute at the business-selected boundary without binding or re-entering an HTTP route. */
212
+ export declare function executeCompiledCommand<Input, Result>(options: {
213
+ module: Pick<CompiledModule, "name" | "commands" | "aspects">;
214
+ command: string;
215
+ input: Input;
216
+ request: Request;
217
+ requestContext: unknown;
218
+ services?: Record<string, unknown>;
219
+ scope?: Record<string, unknown>;
220
+ governance: CommandGovernance;
221
+ observer?: ExecutionObserver;
222
+ handler: (input: Input) => Result | Promise<Result>;
223
+ decode: (value: unknown) => Result;
224
+ }): Promise<Result>;
195
225
  export declare function requireTrustedIdentity(requestContext: unknown): Required<Pick<TrustedRequestIdentity, "subject" | "accessToken">> & TrustedRequestIdentity;
196
226
  export declare function requireIdempotencyKey(invocation: CommandInvocation): string;
197
227
  /**
@@ -202,7 +232,7 @@ export declare function requireIdempotencyKey(invocation: CommandInvocation): st
202
232
  * exposed on the `scope` context key. Request-scoped controller instances are
203
233
  * looked up on `scope`, everything else on `services`.
204
234
  */
205
- export declare function createModulePlugin(compiled: CompiledModule, services: Record<string, unknown>, ctxFactory?: RequestContextFactory, options?: Pick<ApplicationOptions, "commandGovernance" | "commandExecutor" | "errorMapper">, imported?: Record<string, Record<string, unknown>>): Elysia;
235
+ export declare function createModulePlugin(compiled: CompiledModule, services: Record<string, unknown>, ctxFactory?: RequestContextFactory, options?: Pick<ApplicationOptions, "commandGovernance" | "commandExecutor" | "errorMapper" | "onExecution">, imported?: Record<string, Record<string, unknown>>): Elysia;
206
236
  /**
207
237
  * Execute one compiler-emitted Job descriptor.
208
238
  *
@@ -210,7 +240,7 @@ export declare function createModulePlugin(compiled: CompiledModule, services: R
210
240
  * accepted for command-like job implementations. The scope factory and
211
241
  * destruction hooks are generated statically by the compiler.
212
242
  */
213
- export declare function executeJob(compiled: CompiledModule, services: Record<string, unknown>, job: CompiledJob, input: unknown, requestContext: unknown, imported?: Record<string, Record<string, unknown>>, executor?: JobExecutor): Promise<unknown>;
243
+ export declare function executeJob(compiled: CompiledModule, services: Record<string, unknown>, job: CompiledJob, input: unknown, requestContext: unknown, imported?: Record<string, Record<string, unknown>>, executor?: JobExecutor, observer?: ExecutionObserver): Promise<unknown>;
214
244
  export declare function defaultErrorResponse(error: unknown, frameworkCode?: string | number): Response;
215
245
  /**
216
246
  * Create the root Elysia application from compiled modules.
package/dist/index.js CHANGED
@@ -1,6 +1,108 @@
1
1
  // src/index.ts
2
2
  import { Elysia } from "elysia";
3
3
 
4
+ // src/execution.ts
5
+ async function observeExecution(observer, event, next) {
6
+ const emit = (phase, durationMs) => {
7
+ if (!observer)
8
+ return;
9
+ try {
10
+ Promise.resolve(observer(Object.freeze({ ...event, phase, durationMs }))).catch(() => {});
11
+ } catch {}
12
+ };
13
+ const started = performance.now();
14
+ emit("started");
15
+ try {
16
+ const result = await next();
17
+ emit("succeeded", performance.now() - started);
18
+ return result;
19
+ } catch (error) {
20
+ emit("failed", performance.now() - started);
21
+ throw error;
22
+ }
23
+ }
24
+ function executionRequestId(context) {
25
+ if (!context || typeof context !== "object" || !("requestId" in context))
26
+ return;
27
+ const value = context.requestId;
28
+ return typeof value === "string" && /^[A-Za-z0-9._:-]{1,256}$/.test(value) ? value : undefined;
29
+ }
30
+
31
+ // src/identity.ts
32
+ import { createRemoteJWKSet, jwtVerify, errors } from "jose";
33
+ function httpsUrl(value) {
34
+ const url = new URL(value);
35
+ if (url.protocol !== "https:" || url.username || url.password || url.hash) {
36
+ throw new TypeError("Identity endpoints must be HTTPS URLs without credentials or fragments");
37
+ }
38
+ return url;
39
+ }
40
+ function createSupAuthRequestContext(options) {
41
+ httpsUrl(options.issuer);
42
+ const jwksUrl = httpsUrl(options.jwksUrl);
43
+ if (!options.audience.trim() || !options.projectId.trim() || !options.clientId?.trim()) {
44
+ throw new TypeError("SupAuth audience, clientId and projectId are required");
45
+ }
46
+ const algorithms = [...options.algorithms ?? ["ES256", "RS256"]];
47
+ if (!algorithms.length || algorithms.some((value) => value !== "ES256" && value !== "RS256")) {
48
+ throw new TypeError("SupAuth requires ES256 or RS256");
49
+ }
50
+ const { issuer, audience, clientId, projectId, resolveAccess } = options;
51
+ const keyResolver = options.keyResolver ?? createRemoteJWKSet(jwksUrl, { timeoutDuration: 5000 });
52
+ return async (request) => {
53
+ const match = request.headers.get("authorization")?.match(/^Bearer ([^\s]+)$/i);
54
+ const token = match?.[1];
55
+ if (!token || token.length > 16384)
56
+ throw unauthenticated();
57
+ let subject;
58
+ try {
59
+ const { payload } = await jwtVerify(token, keyResolver, {
60
+ issuer,
61
+ audience,
62
+ algorithms,
63
+ requiredClaims: ["sub", "exp", "iat"]
64
+ });
65
+ if (typeof payload.sub !== "string" || !payload.sub.trim() || payload.sub.length > 1024 || /[\u0000-\u001f\u007f]/.test(payload.sub)) {
66
+ throw unauthenticated();
67
+ }
68
+ const application = payload.client_id ?? payload.azp;
69
+ if (application !== clientId || payload.role !== "authenticated" || payload.client_id !== undefined && payload.client_id !== clientId || payload.azp !== undefined && payload.azp !== clientId) {
70
+ throw unauthenticated();
71
+ }
72
+ subject = payload.sub;
73
+ } catch (error) {
74
+ if (error instanceof ApplicationError || error instanceof errors.JWTClaimValidationFailed || error instanceof errors.JWTExpired || error instanceof errors.JWTInvalid || error instanceof errors.JWSInvalid || error instanceof errors.JWSSignatureVerificationFailed || error instanceof errors.JOSEAlgNotAllowed || error instanceof errors.JOSENotSupported || error instanceof errors.JWKSNoMatchingKey)
75
+ throw unauthenticated();
76
+ throw new ApplicationError("Identity verification service unavailable", {
77
+ status: 503,
78
+ code: "AUTHENTICATION_UNAVAILABLE"
79
+ });
80
+ }
81
+ const identity = { authenticated: true, subject, issuer, clientId };
82
+ Object.defineProperty(identity, "accessToken", { value: token, enumerable: false });
83
+ Object.freeze(identity);
84
+ const access = await resolveAccess(identity, request);
85
+ if (!access || access.projectId !== projectId || typeof access.tenantId !== "string" || !access.tenantId.trim() || !Array.isArray(access.permissions) || access.permissions.some((permission) => typeof permission !== "string" || !permission.trim())) {
86
+ throw new ApplicationError("Application access denied", { status: 403, code: "APPLICATION_ACCESS_DENIED" });
87
+ }
88
+ const context = createSupaCloudRequestContext(request);
89
+ return {
90
+ ...context,
91
+ identity,
92
+ access: Object.freeze({
93
+ projectId,
94
+ tenantId: access.tenantId,
95
+ permissions: Object.freeze([...new Set(access.permissions)])
96
+ })
97
+ };
98
+ };
99
+ }
100
+ function unauthenticated() {
101
+ return new ApplicationError("Authenticated user context is required", {
102
+ status: 401,
103
+ code: "AUTHENTICATION_REQUIRED"
104
+ });
105
+ }
4
106
  // src/feature.ts
5
107
  function assertFeatureTransition(spec, state, event) {
6
108
  const transition = Object.hasOwn(spec.transitions, event) ? spec.transitions[event] : undefined;
@@ -368,6 +470,14 @@ function composeAspects(...aspects) {
368
470
  return dispatch(0);
369
471
  };
370
472
  }
473
+ function observedAspects(aspects, boundary, observer) {
474
+ return composeAspects(...aspects.map((aspect, index) => (context, next) => observeExecution(observer, {
475
+ kind: context.kind,
476
+ operation: context.name,
477
+ stage: `${boundary}.aspect[${index}]:${aspect.name || "anonymous"}`,
478
+ requestId: executionRequestId(context.requestContext)
479
+ }, () => aspect(context, next))));
480
+ }
371
481
  function composeCommandExecutors(...executors) {
372
482
  const active = executors.filter((e) => typeof e === "function");
373
483
  if (active.length === 0)
@@ -482,9 +592,31 @@ function missingGovernanceAdapter(command, adapter) {
482
592
  code: codes[adapter]
483
593
  });
484
594
  }
485
- function createCommandExecutor(governance) {
595
+ function createCommandExecutor(governance, observer) {
486
596
  return async (invocation, next) => {
487
597
  const { command } = invocation;
598
+ for (const mode of [command.transaction, command.idempotency]) {
599
+ if (mode !== undefined && mode !== "required" && mode !== "none") {
600
+ throw new ApplicationError("Invalid command governance mode", { code: "COMMAND_MODE_INVALID" });
601
+ }
602
+ }
603
+ if (command.rpc !== undefined) {
604
+ const adapter = Object.hasOwn(governance.rpc ?? {}, command.rpc) ? governance.rpc?.[command.rpc] : undefined;
605
+ if (!adapter)
606
+ throw new ApplicationError("RPC governance adapter unavailable", { code: "COMMAND_RPC_UNAVAILABLE", status: 501 });
607
+ for (const capability of ["audit", "transaction", "idempotency"]) {
608
+ const required = capability === "audit" ? !!command.audit : command[capability] === "required";
609
+ if (required && adapter.capabilities[capability] !== true)
610
+ throw missingGovernanceAdapter(command, capability);
611
+ }
612
+ const event = {
613
+ kind: "command",
614
+ operation: command.name,
615
+ requestId: executionRequestId(invocation.requestContext)
616
+ };
617
+ await observeExecution(observer, { ...event, stage: "authorize" }, () => governance.authorize(invocation));
618
+ return observeExecution(observer, { ...event, stage: `rpc:${command.rpc}` }, () => adapter.execute(invocation, once(next)));
619
+ }
488
620
  const audit = command.audit ? governance.audit : undefined;
489
621
  const transaction = command.transaction === "required" ? governance.transaction : undefined;
490
622
  const idempotency = command.idempotency === "required" ? governance.idempotency : undefined;
@@ -498,24 +630,31 @@ function createCommandExecutor(governance) {
498
630
  throw missingGovernanceAdapter(command, "idempotency");
499
631
  }
500
632
  try {
501
- await governance.authorize(invocation);
633
+ const observe = (stage, next2) => observeExecution(observer, {
634
+ kind: "command",
635
+ operation: command.name,
636
+ stage,
637
+ requestId: executionRequestId(invocation.requestContext)
638
+ }, next2);
639
+ await observe("authorize", () => governance.authorize(invocation));
502
640
  let execute = async () => {
503
- const result = await next();
641
+ const result = await observe("handler", invokeOnce);
504
642
  if (audit)
505
- await audit.succeeded.call(audit, invocation, result);
643
+ await observe("audit", () => audit.succeeded.call(audit, invocation, result));
506
644
  return result;
507
645
  };
646
+ const invokeOnce = once(next);
508
647
  if (command.transaction === "required") {
509
648
  if (!transaction)
510
649
  throw missingGovernanceAdapter(command, "transaction");
511
650
  const inner = execute;
512
- execute = () => Promise.resolve(transaction.call(governance, invocation, inner));
651
+ execute = () => observe("transaction", () => transaction.call(governance, invocation, once(inner)));
513
652
  }
514
653
  if (command.idempotency === "required") {
515
654
  if (!idempotency)
516
655
  throw missingGovernanceAdapter(command, "idempotency");
517
656
  const inner = execute;
518
- execute = () => Promise.resolve(idempotency.call(governance, invocation, inner));
657
+ execute = () => observe("idempotency", () => idempotency.call(governance, invocation, once(inner)));
519
658
  }
520
659
  return await execute();
521
660
  } catch (error) {
@@ -525,6 +664,44 @@ function createCommandExecutor(governance) {
525
664
  }
526
665
  };
527
666
  }
667
+ async function executeCompiledCommand(options) {
668
+ const matches = options.module.commands?.filter((item) => item.className === options.command) ?? [];
669
+ if (matches.length !== 1)
670
+ throw new ApplicationError("Command descriptor missing or ambiguous", { code: "COMMAND_NOT_REGISTERED" });
671
+ const command = matches[0];
672
+ const invocation = {
673
+ command,
674
+ input: { body: options.input, params: {}, query: {} },
675
+ request: options.request,
676
+ requestContext: options.requestContext,
677
+ services: options.services ?? {},
678
+ scope: options.scope
679
+ };
680
+ const aspects = composeAspects(observedAspects(options.module.aspects ?? [], `module:${options.module.name}`, options.observer), observedAspects(command.aspects ?? [], "command", options.observer));
681
+ const value = await createCommandExecutor(options.governance, options.observer)(invocation, once(() => aspects({
682
+ kind: "command",
683
+ name: command.name,
684
+ input: options.input,
685
+ request: options.request,
686
+ requestContext: options.requestContext,
687
+ services: invocation.services,
688
+ scope: invocation.scope,
689
+ metadata: command
690
+ }, once(() => options.handler(options.input)))));
691
+ return options.decode(value);
692
+ }
693
+ function once(next) {
694
+ let called = false;
695
+ return async () => {
696
+ if (called) {
697
+ throw new ApplicationError("Command continuation called multiple times", {
698
+ code: "COMMAND_CONTINUATION_REUSED"
699
+ });
700
+ }
701
+ called = true;
702
+ return await next();
703
+ };
704
+ }
528
705
  function requireTrustedIdentity(requestContext) {
529
706
  const context = isRecord(requestContext) ? requestContext : {};
530
707
  const identity = isTrustedIdentity(context.identity) ? context.identity : undefined;
@@ -570,7 +747,7 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
570
747
  }
571
748
  }
572
749
  const requestContexts = new WeakMap;
573
- const governanceExecutor = options.commandGovernance ? createCommandExecutor(options.commandGovernance) : undefined;
750
+ const governanceExecutor = options.commandGovernance ? createCommandExecutor(options.commandGovernance, options.onExecution) : undefined;
574
751
  const commandExecutor = options.commandExecutor && governanceExecutor ? composeCommandExecutors(options.commandExecutor, governanceExecutor) : options.commandExecutor ?? governanceExecutor;
575
752
  const plugin = new Elysia({ name: `supacloud:${compiled.name}` }).decorate("services", services);
576
753
  const createRequestScope = compiled.createRequestScope;
@@ -636,7 +813,13 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
636
813
  scope: requestScope,
637
814
  requestContext
638
815
  };
639
- const invoke = () => route.invoker ? route.invoker(instance, input) : Reflect.apply(method, instance, [input]);
816
+ const handlerCall = () => route.invoker ? route.invoker(instance, input) : Reflect.apply(method, instance, [input]);
817
+ const invoke = once(() => route.command && options.commandGovernance ? handlerCall() : observeExecution(options.onExecution, {
818
+ kind: route.command ? "command" : "route",
819
+ operation: route.command ?? `${route.method} ${path}`,
820
+ stage: "handler",
821
+ requestId: executionRequestId(requestContext)
822
+ }, handlerCall));
640
823
  const routeContext = {
641
824
  kind: "route",
642
825
  name: `${route.method} ${path}`,
@@ -659,9 +842,9 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
659
842
  metadata: command ?? route
660
843
  };
661
844
  const commandAspects = route.command ? commandsByClassName.get(route.command)?.aspects ?? [] : [];
662
- const routePipeline = composeAspects(...route.aspects ?? []);
663
- const commandPipeline = composeAspects(...commandAspects);
664
- const modulePipeline = composeAspects(...compiled.aspects ?? []);
845
+ const routePipeline = observedAspects(route.aspects ?? [], "route", options.onExecution);
846
+ const commandPipeline = observedAspects(commandAspects, "command", options.onExecution);
847
+ const modulePipeline = observedAspects(compiled.aspects ?? [], `module:${compiled.name}`, options.onExecution);
665
848
  const invokeRoute = () => routePipeline(routeContext, () => route.command ? commandPipeline(commandContext, () => invokeCommand()) : invoke());
666
849
  const invokeCommand = () => {
667
850
  if (!route.command)
@@ -685,7 +868,12 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
685
868
  scope: requestScope,
686
869
  services
687
870
  };
688
- return commandExecutor(invocation, invoke);
871
+ return observeExecution(options.onExecution, {
872
+ kind: "command",
873
+ operation: command.name,
874
+ stage: "commandExecutor",
875
+ requestId: executionRequestId(requestContext)
876
+ }, () => commandExecutor(invocation, invoke));
689
877
  };
690
878
  return modulePipeline(route.command ? commandContext : routeContext, invokeRoute);
691
879
  };
@@ -716,7 +904,7 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
716
904
  }
717
905
  return plugin;
718
906
  }
719
- async function executeJob(compiled, services, job, input, requestContext, imported = {}, executor) {
907
+ async function executeJob(compiled, services, job, input, requestContext, imported = {}, executor, observer) {
720
908
  const jobScope = job.scope === "job" && compiled.createJobScope ? await compiled.createJobScope(services, requestContext, imported) : undefined;
721
909
  try {
722
910
  const source = job.scope === "job" ? jobScope : services;
@@ -741,9 +929,20 @@ async function executeJob(compiled, services, job, input, requestContext, import
741
929
  services,
742
930
  metadata: job
743
931
  };
744
- const invoke = () => Reflect.apply(method, instance, [input]);
745
- const pipeline = composeAspects(...compiled.aspects ?? [], ...job.aspects ?? []);
746
- return await pipeline(context, executor ? () => executor(invocation, invoke) : invoke);
932
+ const invoke = once(() => observeExecution(observer, {
933
+ kind: "job",
934
+ operation: job.name,
935
+ stage: "handler",
936
+ requestId: executionRequestId(requestContext)
937
+ }, () => Reflect.apply(method, instance, [input])));
938
+ const pipeline = observedAspects(compiled.aspects ?? [], `module:${compiled.name}`, observer);
939
+ const jobPipeline = observedAspects(job.aspects ?? [], "job", observer);
940
+ return await pipeline(context, () => jobPipeline(context, () => observeExecution(observer, {
941
+ kind: "job",
942
+ operation: job.name,
943
+ stage: "jobExecutor",
944
+ requestId: executionRequestId(requestContext)
945
+ }, executor ? () => executor(invocation, invoke) : invoke)));
747
946
  } finally {
748
947
  if (jobScope && compiled.destroyJobScope) {
749
948
  await compiled.destroyJobScope(jobScope);
@@ -803,7 +1002,8 @@ function createApplication(options) {
803
1002
  app.use(createModulePlugin(module, services, ctxFactory, {
804
1003
  commandGovernance: options.commandGovernance,
805
1004
  commandExecutor: options.commandExecutor,
806
- errorMapper: options.errorMapper
1005
+ errorMapper: options.errorMapper,
1006
+ onExecution: options.onExecution
807
1007
  }, imported));
808
1008
  }
809
1009
  return app;
@@ -824,9 +1024,11 @@ export {
824
1024
  createMemoryPolicy,
825
1025
  createMemorySandbox,
826
1026
  createModulePlugin,
1027
+ createSupAuthRequestContext,
827
1028
  createSupaCloudRequestContext,
828
1029
  createTestApp,
829
1030
  defaultErrorResponse,
1031
+ executeCompiledCommand,
830
1032
  executeJob,
831
1033
  requireIdempotencyKey,
832
1034
  requireTrustedIdentity,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/elysia",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Elysia runtime adapter for SupaCloud compiled modules: application/request scopes, route registration and validation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "scripts": {
21
21
  "build": "bun run clean && bun run build:js && bun run build:types",
22
- "build:js": "bun build src/index.ts --outdir dist --target node --external elysia",
22
+ "build:js": "bun build src/index.ts --outdir dist --target node --external elysia --external jose",
23
23
  "build:types": "tsc -p tsconfig.json --emitDeclarationOnly",
24
24
  "clean": "rm -rf dist",
25
25
  "prepublishOnly": "bun run build",
@@ -40,6 +40,9 @@
40
40
  "url": "https://github.com/vibeunion/supacloud.git",
41
41
  "directory": "packages/elysia"
42
42
  },
43
+ "dependencies": {
44
+ "jose": "^6.2.11"
45
+ },
43
46
  "peerDependencies": {
44
47
  "elysia": "^1.4.30"
45
48
  },