@supacloud/elysia 0.16.0 → 0.18.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
@@ -125,9 +125,9 @@ Runtime adapter that turns `@supacloud/compiler` output into a production-ready
125
125
  controllers and services.
126
126
  - **Request-scope teardown**: invokes the compiler-generated
127
127
  `destroyRequestScope` after the response, including when the handler fails.
128
- - **Angular-backed async DI**: when `createApplication({ injector })` receives
129
- an `@supacloud/app` root injector, each request gets an isolated child
130
- injector with `REQUEST_CONTEXT`, including across `await` boundaries.
128
+ - **Compile-time DI**: constructor dependencies are directly connected by generated
129
+ factories. Request/job context is passed explicitly without runtime injector
130
+ discovery, registration or token lookup.
131
131
  - **TypeBox schema binding**: attaches compiled parameter, query, body, headers,
132
132
  cookie, single-response and status-map TypeBox schemas directly to Elysia
133
133
  route definitions; Elysia performs request validation and normalization.
@@ -205,10 +205,11 @@ GraphQL endpoint serves a local, role-scoped snapshot only; it does not enable
205
205
  server introspection or create a GraphQL resolver layer. Protect or omit these
206
206
  routes in production when the schema is not public.
207
207
 
208
- The root injector is normally created and owned by `bootstrapBun`. The Elysia
209
- adapter does not take ownership of an injected root injector; stop it from the
210
- same Bun bootstrap that created it. Existing applications may omit `injector`
211
- and continue using compiler-generated request scopes unchanged.
208
+ Use constructor injection and the generated `createServices`,
209
+ `createRequestScope` and `createJobScope` factories. Compiled applications reject
210
+ property `inject()` and runtime injection contexts with `SC2012`. The host provides
211
+ platform dependencies through `deps` and owns their lifecycle; generated factories
212
+ own application scope construction and teardown.
212
213
 
213
214
  For deterministic local verification, use the in-memory sandbox. It supplies
214
215
  stable request identity, an isolated key-value database with optimistic
@@ -33,6 +33,7 @@ export interface CompiledRoute {
33
33
  title?: string;
34
34
  data?: Record<string, unknown>;
35
35
  aspects?: CompiledAspect[];
36
+ aspectPipeline?: CompiledAspectPipeline;
36
37
  invoker?: (controller: unknown, request: {
37
38
  params?: Record<string, unknown>;
38
39
  query?: Record<string, unknown>;
@@ -52,6 +53,7 @@ export interface CompiledCommand {
52
53
  idempotency: "required" | "none";
53
54
  standalone?: boolean;
54
55
  aspects?: CompiledAspect[];
56
+ aspectPipeline?: CompiledAspectPipeline;
55
57
  }
56
58
  export interface CompiledJob {
57
59
  className: string;
@@ -65,6 +67,7 @@ export interface CompiledJob {
65
67
  maxAttempts?: number;
66
68
  idempotency?: "required" | "none";
67
69
  aspects?: CompiledAspect[];
70
+ aspectPipeline?: CompiledAspectPipeline;
68
71
  }
69
72
  export interface CompiledAspectContext {
70
73
  kind: "route" | "command" | "job";
@@ -94,7 +97,11 @@ export interface CompiledModule {
94
97
  commands: CompiledCommand[];
95
98
  jobs: CompiledJob[];
96
99
  aspects?: CompiledAspect[];
100
+ aspectPipeline?: CompiledAspectPipeline;
97
101
  }
102
+ type CompiledAspectObserver = (stage: string, run: () => unknown | Promise<unknown>) => unknown | Promise<unknown>;
103
+ type CompiledAspectPipeline = (context: CompiledAspectContext, next: () => unknown | Promise<unknown>, observe?: CompiledAspectObserver) => unknown | Promise<unknown>;
98
104
  export declare function createCompiledModules(): CompiledModule[];
99
105
  export declare function initializeApplication(services: Record<string, unknown>): Promise<void>;
100
106
  export declare function destroyApplication(services: Record<string, unknown>): Promise<void>;
107
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Elysia } from "elysia";
2
+ import type { CommandRuntimeAudit, CommandRuntimeAuthorizer, CommandRuntimeGovernance, CommandRuntimeInvocation, CommandRuntimeMiddleware } from "@supacloud/app";
2
3
  import { type CommandPreview } from "@supacloud/contracts";
3
- import { type EnvironmentInjector } from "@supacloud/app";
4
4
  import { type ExecutionObserver } from "./execution";
5
5
  import { type ApplicationDocumentationOptions } from "./documentation";
6
6
  export type { ExecutionEvent, ExecutionObserver } from "./execution";
@@ -46,6 +46,7 @@ export interface CompiledRoute {
46
46
  command?: string;
47
47
  /** Statically generated route aspects. */
48
48
  aspects?: ApplicationAspect[];
49
+ aspectPipeline?: ApplicationAspectPipeline;
49
50
  }
50
51
  export interface CompiledCommand {
51
52
  rpc?: string;
@@ -57,6 +58,7 @@ export interface CompiledCommand {
57
58
  idempotency?: "required" | "none" | string;
58
59
  /** Statically generated command aspects. */
59
60
  aspects?: ApplicationAspect[];
61
+ aspectPipeline?: ApplicationAspectPipeline;
60
62
  }
61
63
  export interface CompiledJob {
62
64
  className: string;
@@ -72,6 +74,7 @@ export interface CompiledJob {
72
74
  maxAttempts?: number;
73
75
  idempotency?: "required" | "none";
74
76
  aspects?: ApplicationAspect[];
77
+ aspectPipeline?: ApplicationAspectPipeline;
75
78
  }
76
79
  export interface CompiledController {
77
80
  /** Controller path prefix, e.g. "/cases". */
@@ -93,6 +96,7 @@ export interface CompiledModule {
93
96
  jobs?: CompiledJob[];
94
97
  /** Statically generated module aspects. */
95
98
  aspects?: ApplicationAspect[];
99
+ aspectPipeline?: ApplicationAspectPipeline;
96
100
  }
97
101
  export type RequestContextFactory = (request: Request) => unknown | Promise<unknown>;
98
102
  export { createSupAuthRequestContext } from "./identity";
@@ -113,19 +117,8 @@ export interface SupaCloudRequestContext {
113
117
  identity: TrustedRequestIdentity;
114
118
  idempotencyKey?: string;
115
119
  }
116
- export interface CommandInvocation {
117
- command: CompiledCommand;
118
- input: {
119
- body: unknown;
120
- params: Record<string, unknown>;
121
- query: Record<string, unknown>;
122
- };
123
- request: Request;
124
- requestContext: unknown;
125
- scope?: Record<string, unknown>;
126
- services: Record<string, unknown>;
127
- }
128
- export type CommandExecutor = (invocation: CommandInvocation, next: () => unknown | Promise<unknown>) => unknown | Promise<unknown>;
120
+ export type CommandInvocation = CommandRuntimeInvocation<CompiledCommand>;
121
+ export type CommandExecutor = CommandRuntimeMiddleware<CommandInvocation>;
129
122
  export { assertFeatureTransition } from "./feature";
130
123
  export type { FeatureTransitionSpec } from "./feature";
131
124
  export interface ApplicationAspectContext {
@@ -145,28 +138,49 @@ export type ApplicationAspect = (context: ApplicationAspectContext, next: () =>
145
138
  * registers aspects.
146
139
  */
147
140
  export declare function composeAspects(...aspects: (ApplicationAspect | undefined | null)[]): ApplicationAspect;
148
- export type CommandAuthorizer = (invocation: CommandInvocation) => void | Promise<void>;
149
- export type CommandMiddleware = (invocation: CommandInvocation, next: () => unknown | Promise<unknown>) => unknown | Promise<unknown>;
150
- export interface CommandAudit {
151
- succeeded(invocation: CommandInvocation, result: unknown): void | Promise<void>;
152
- failed(invocation: CommandInvocation, error: unknown): void | Promise<void>;
141
+ export type ApplicationAspectPipeline = (context: ApplicationAspectContext, next: () => unknown | Promise<unknown>, observe?: (stage: string, run: () => unknown | Promise<unknown>) => unknown | Promise<unknown>) => unknown | Promise<unknown>;
142
+ export type CommandAuthorizer = CommandRuntimeAuthorizer<CommandInvocation>;
143
+ export type CommandMiddleware = CommandRuntimeMiddleware<CommandInvocation>;
144
+ export type CommandAudit = CommandRuntimeAudit<CommandInvocation>;
145
+ export type CommandGovernance = CommandRuntimeGovernance<CommandInvocation>;
146
+ export interface CommandAuthorizationRequest {
147
+ readonly principal: {
148
+ readonly kind: "user" | "service";
149
+ readonly issuer: string;
150
+ readonly subject: string;
151
+ };
152
+ readonly applicationId: string;
153
+ readonly domain: {
154
+ readonly type: string;
155
+ readonly id: string;
156
+ };
157
+ }
158
+ export interface CommandAuthorizationContext {
159
+ readonly applicationId: string;
160
+ readonly permissions: readonly string[];
161
+ readonly permissionCatalogVersion?: string;
162
+ readonly permissionCatalogDigest?: string;
163
+ }
164
+ export interface CommandAuthorizationCatalog {
165
+ readonly version: string;
166
+ readonly digest?: string;
153
167
  }
154
- export interface CommandGovernance {
155
- /** Application-owned adapters: a single RPC owns all declared persistence. */
156
- rpc?: Record<string, {
157
- capabilities: {
158
- audit?: boolean;
159
- transaction?: boolean;
160
- idempotency?: boolean;
161
- boundary?: "database" | "external";
162
- };
163
- execute: CommandMiddleware;
164
- }>;
165
- authorize: CommandAuthorizer;
166
- idempotency?: CommandMiddleware;
167
- transaction?: CommandMiddleware;
168
- audit?: CommandAudit;
168
+ export interface CommandAuthorizationAdapterOptions {
169
+ readonly applicationId: string;
170
+ readonly issuer: string;
171
+ readonly domain: (invocation: CommandInvocation) => {
172
+ readonly type: string;
173
+ readonly id: string;
174
+ };
175
+ readonly resolve: (request: CommandAuthorizationRequest) => Promise<CommandAuthorizationContext>;
176
+ readonly catalog?: CommandAuthorizationCatalog;
169
177
  }
178
+ /**
179
+ * Adapt a SupAuth-style resolver to the standard command governance port.
180
+ * The resolver remains the authorization source of truth; this adapter only
181
+ * maps trusted request identity and normalizes public failure statuses.
182
+ */
183
+ export declare function createCommandAuthorizationAdapter(options: CommandAuthorizationAdapterOptions): CommandAuthorizer;
170
184
  /**
171
185
  * Compose multiple CommandExecutors into a single onion-style pipeline.
172
186
  * Outer executors run first before calling `next()`, and complete last.
@@ -211,8 +225,6 @@ export interface ApplicationOptions {
211
225
  modules?: CompiledModule[];
212
226
  /** Platform-level dependencies (db client etc.), passed to createServices. */
213
227
  deps?: Record<string, unknown>;
214
- /** Optional Angular-backed root injector used for async request contexts. */
215
- injector?: EnvironmentInjector;
216
228
  /** Builds the per-request context object. Defaults to { requestId, request }. */
217
229
  requestContext?: RequestContextFactory;
218
230
  /** Enforces permission/audit/idempotency policy for command-bound routes. */
@@ -248,7 +260,7 @@ export declare const createSupaCloudRequestContext: (request: Request) => SupaCl
248
260
  export declare function createCommandExecutor(governance: CommandGovernance, observer?: ExecutionObserver): CommandExecutor;
249
261
  /** Execute at the business-selected boundary without binding or re-entering an HTTP route. */
250
262
  export declare function executeCompiledCommand<Input, Result>(options: {
251
- module: Pick<CompiledModule, "name" | "commands" | "aspects">;
263
+ module: Pick<CompiledModule, "name" | "commands" | "aspects" | "aspectPipeline">;
252
264
  command: string;
253
265
  input: Input;
254
266
  request: Request;
@@ -282,7 +294,7 @@ export declare function requireIdempotencyKey(invocation: CommandInvocation): st
282
294
  * exposed on the `scope` context key. Request-scoped controller instances are
283
295
  * looked up on `scope`, everything else on `services`.
284
296
  */
285
- export declare function createModulePlugin(compiled: CompiledModule, services: Record<string, unknown>, ctxFactory?: RequestContextFactory, options?: Pick<ApplicationOptions, "commandGovernance" | "commandExecutor" | "errorMapper" | "onExecution" | "normalize" | "injector">, imported?: Record<string, Record<string, unknown>>): Elysia;
297
+ export declare function createModulePlugin(compiled: CompiledModule, services: Record<string, unknown>, ctxFactory?: RequestContextFactory, options?: Pick<ApplicationOptions, "commandGovernance" | "commandExecutor" | "errorMapper" | "onExecution" | "normalize">, imported?: Record<string, Record<string, unknown>>): Elysia;
286
298
  /**
287
299
  * Execute one compiler-emitted Job descriptor.
288
300
  *
package/dist/index.js CHANGED
@@ -1,11 +1,6 @@
1
1
  // src/index.ts
2
2
  import { Elysia as Elysia2 } from "elysia";
3
3
  import { decodeCommandPreview } from "@supacloud/contracts";
4
- import {
5
- provideToken,
6
- runInRequestContext
7
- } from "@supacloud/app";
8
- import { REQUEST_CONTEXT } from "@supacloud/app";
9
4
 
10
5
  // src/command-errors.ts
11
6
  function commandErrorCode(error) {
@@ -429,7 +424,7 @@ function createPersistentCommandAdapter(command, options) {
429
424
  async execute(invocation) {
430
425
  try {
431
426
  const identity = decodeCommandIdentity(await options.identity(invocation));
432
- return await command.execute(identity, requireIdempotencyKey(invocation), options.input(invocation));
427
+ return await command.execute(identity, requireIdempotencyKey(invocation), options.input(invocation), invocation.request.signal);
433
428
  } catch (error) {
434
429
  if (!(error instanceof CommandError))
435
430
  throw error;
@@ -1285,6 +1280,53 @@ function observedAspects(aspects, boundary, observer) {
1285
1280
  ...executionTrace(context.requestContext)
1286
1281
  }, () => aspect(context, next))));
1287
1282
  }
1283
+ function descriptorPipeline(descriptor, boundary, observer) {
1284
+ const pipeline = descriptor.aspectPipeline;
1285
+ if (!pipeline)
1286
+ return observedAspects(descriptor.aspects ?? [], boundary, observer);
1287
+ return (context, next) => pipeline(context, next, observer ? (stage, run) => observeExecution(observer, {
1288
+ kind: context.kind,
1289
+ operation: context.name,
1290
+ stage: `${boundary}.${stage}`,
1291
+ ...executionTrace(context.requestContext)
1292
+ }, run) : undefined);
1293
+ }
1294
+ function createCommandAuthorizationAdapter(options) {
1295
+ return async (invocation) => {
1296
+ const permission = invocation.command.permission;
1297
+ if (!permission || !/^[a-z][a-z0-9._-]*:[a-z][a-z0-9._-]*$/.test(permission)) {
1298
+ throw new ApplicationError("Command permission must use resource:action syntax", {
1299
+ status: 500,
1300
+ code: "COMMAND_PERMISSION_INVALID"
1301
+ });
1302
+ }
1303
+ const context = invocation.requestContext;
1304
+ const identity = isRecord(context) && isTrustedIdentity(context.identity) ? context.identity : undefined;
1305
+ if (!isAuthenticatedTrustedIdentity(identity)) {
1306
+ throw new ApplicationError("Authentication required", { status: 401, code: "AUTHENTICATION_REQUIRED" });
1307
+ }
1308
+ const request = {
1309
+ principal: { kind: "user", issuer: options.issuer, subject: identity.subject },
1310
+ applicationId: options.applicationId,
1311
+ domain: options.domain(invocation)
1312
+ };
1313
+ let resolved;
1314
+ try {
1315
+ resolved = await options.resolve(request);
1316
+ } catch {
1317
+ throw new ApplicationError("Authorization is unavailable", { status: 503, code: "AUTHORIZATION_UNAVAILABLE" });
1318
+ }
1319
+ if (!isRecord(resolved) || !Array.isArray(resolved.permissions) || !resolved.permissions.every((value) => typeof value === "string") || resolved.permissionCatalogVersion !== undefined && typeof resolved.permissionCatalogVersion !== "string" || resolved.permissionCatalogDigest !== undefined && typeof resolved.permissionCatalogDigest !== "string" || resolved.applicationId !== options.applicationId || options.catalog !== undefined && (resolved.permissionCatalogVersion !== options.catalog.version || options.catalog.digest !== undefined && resolved.permissionCatalogDigest !== options.catalog.digest)) {
1320
+ throw new ApplicationError("Authorization context is not bound to this application", {
1321
+ status: 503,
1322
+ code: "AUTHORIZATION_CONTEXT_INVALID"
1323
+ });
1324
+ }
1325
+ if (!resolved.permissions.includes(permission)) {
1326
+ throw new ApplicationError("Permission denied", { status: 403, code: "PERMISSION_DENIED" });
1327
+ }
1328
+ };
1329
+ }
1288
1330
  function composeCommandExecutors(...executors) {
1289
1331
  const active = executors.filter((e) => typeof e === "function");
1290
1332
  if (active.length === 0)
@@ -1499,7 +1541,7 @@ async function executeCompiledCommand(options) {
1499
1541
  services: options.services ?? {},
1500
1542
  ...options.scope ? { scope: options.scope } : {}
1501
1543
  };
1502
- const aspects = composeAspects(observedAspects(options.module.aspects ?? [], `module:${options.module.name}`, options.observer), observedAspects(command.aspects ?? [], "command", options.observer));
1544
+ const aspects = composeAspects(descriptorPipeline(options.module, `module:${options.module.name}`, options.observer), descriptorPipeline(command, "command", options.observer));
1503
1545
  const value = await createCommandExecutor(options.governance, options.observer)(invocation, once(() => aspects({
1504
1546
  kind: "command",
1505
1547
  name: command.name,
@@ -1579,6 +1621,67 @@ function requireIdempotencyKey(invocation) {
1579
1621
  }
1580
1622
  return key;
1581
1623
  }
1624
+ function assertCommandGovernanceReady(compiled, governance, executor) {
1625
+ if ((compiled.commands ?? []).length === 0)
1626
+ return;
1627
+ if (executor !== undefined && typeof executor !== "function") {
1628
+ throw new ApplicationError(`Module "${compiled.name}" has no callable command executor`, {
1629
+ code: "COMMAND_EXECUTOR_UNCONFIGURED"
1630
+ });
1631
+ }
1632
+ if (!governance && !executor) {
1633
+ throw new ApplicationError(`Module "${compiled.name}" has commands but no command governance`, {
1634
+ code: "COMMAND_GOVERNANCE_UNCONFIGURED"
1635
+ });
1636
+ }
1637
+ if (!governance)
1638
+ return;
1639
+ if (typeof governance.authorize !== "function") {
1640
+ throw new ApplicationError(`Module "${compiled.name}" has no authorization adapter`, {
1641
+ code: "COMMAND_AUTHORIZATION_UNCONFIGURED"
1642
+ });
1643
+ }
1644
+ for (const command of compiled.commands ?? []) {
1645
+ if (command.audit && command.rpc === undefined && (typeof governance.audit?.succeeded !== "function" || typeof governance.audit?.failed !== "function")) {
1646
+ throw new ApplicationError(`Command "${command.name}" has no audit adapter`, {
1647
+ code: "COMMAND_AUDIT_UNCONFIGURED"
1648
+ });
1649
+ }
1650
+ if (command.idempotency === "required" && typeof governance.idempotency !== "function" && command.rpc === undefined) {
1651
+ throw new ApplicationError(`Command "${command.name}" has no idempotency adapter`, {
1652
+ code: "COMMAND_IDEMPOTENCY_UNCONFIGURED"
1653
+ });
1654
+ }
1655
+ if (command.transaction === "required" && typeof governance.transaction !== "function" && command.rpc === undefined) {
1656
+ throw new ApplicationError(`Command "${command.name}" has no transaction adapter`, {
1657
+ code: "COMMAND_TRANSACTION_UNCONFIGURED"
1658
+ });
1659
+ }
1660
+ if (command.rpc !== undefined) {
1661
+ const adapter = Object.hasOwn(governance.rpc ?? {}, command.rpc) ? governance.rpc?.[command.rpc] : undefined;
1662
+ if (!adapter || typeof adapter.execute !== "function" || !isRecord(adapter.capabilities)) {
1663
+ throw new ApplicationError(`Command "${command.name}" has no RPC governance adapter`, {
1664
+ code: "COMMAND_RPC_UNCONFIGURED"
1665
+ });
1666
+ }
1667
+ if (command.audit && adapter.capabilities.audit !== true) {
1668
+ throw new ApplicationError(`Command "${command.name}" RPC adapter has no audit capability`, {
1669
+ code: "COMMAND_AUDIT_UNCONFIGURED"
1670
+ });
1671
+ }
1672
+ if (command.idempotency === "required" && adapter.capabilities.idempotency !== true) {
1673
+ throw new ApplicationError(`Command "${command.name}" RPC adapter has no idempotency capability`, {
1674
+ code: "COMMAND_IDEMPOTENCY_UNCONFIGURED"
1675
+ });
1676
+ }
1677
+ if (command.transaction === "required" && adapter.capabilities.transaction !== true) {
1678
+ throw new ApplicationError(`Command "${command.name}" RPC adapter has no transaction capability`, {
1679
+ code: "COMMAND_TRANSACTION_UNCONFIGURED"
1680
+ });
1681
+ }
1682
+ }
1683
+ }
1684
+ }
1582
1685
  function joinPaths(prefix, path) {
1583
1686
  const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
1584
1687
  return joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
@@ -1625,6 +1728,7 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1625
1728
  "invoker",
1626
1729
  "command",
1627
1730
  "aspects",
1731
+ "aspectPipeline",
1628
1732
  "paramTransforms",
1629
1733
  "paramDefaults",
1630
1734
  "queryTransforms",
@@ -1647,6 +1751,7 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1647
1751
  if (hasCommandRoutes && !options.commandGovernance && !options.commandExecutor) {
1648
1752
  throw new ApplicationError(`Module "${compiled.name}" has command routes but no commandGovernance`, { code: "COMMAND_GOVERNANCE_UNCONFIGURED" });
1649
1753
  }
1754
+ assertCommandGovernanceReady(compiled, options.commandGovernance, options.commandExecutor);
1650
1755
  const commandsByClassName = new Map((compiled.commands ?? []).map((command) => [command.className, command]));
1651
1756
  for (const controller of compiled.controllers) {
1652
1757
  for (const route of controller.routes) {
@@ -1747,14 +1852,13 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1747
1852
  services,
1748
1853
  metadata: command ?? route
1749
1854
  };
1750
- const commandAspects = route.command ? commandsByClassName.get(route.command)?.aspects ?? [] : [];
1751
- const routePipeline = observedAspects(route.aspects ?? [], "route", options.onExecution);
1752
- const commandPipeline = observedAspects(commandAspects, "command", options.onExecution);
1753
- const modulePipeline = observedAspects(compiled.aspects ?? [], `module:${compiled.name}`, options.onExecution);
1754
- const invokeRoute = () => routePipeline(routeContext, () => route.command ? commandPipeline(commandContext, () => invokeCommand()) : invoke());
1855
+ const routePipeline = descriptorPipeline(route, "route", options.onExecution);
1856
+ const commandPipeline = descriptorPipeline(command ?? {}, "command", options.onExecution);
1857
+ const modulePipeline = descriptorPipeline(compiled, `module:${compiled.name}`, options.onExecution);
1858
+ const invokeRoute = () => modulePipeline(route.command ? commandContext : routeContext, once(() => routePipeline(routeContext, once(() => route.command ? commandPipeline(commandContext, once(invoke)) : invoke()))));
1755
1859
  const invokeCommand = () => {
1756
1860
  if (!route.command)
1757
- return invoke();
1861
+ return invokeRoute();
1758
1862
  if (!command) {
1759
1863
  throw new ApplicationError(`Command "${route.command}" is not registered`, {
1760
1864
  code: "COMMAND_NOT_REGISTERED"
@@ -1779,11 +1883,11 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1779
1883
  operation: command.name,
1780
1884
  stage: "commandExecutor",
1781
1885
  ...executionTrace(requestContext)
1782
- }, () => commandExecutor(invocation, invoke));
1886
+ }, () => commandExecutor(invocation, once(invokeRoute)));
1783
1887
  };
1784
- return modulePipeline(route.command ? commandContext : routeContext, invokeRoute);
1888
+ return invokeCommand();
1785
1889
  };
1786
- const result = options.injector ? await runInRequestContext(options.injector, [provideToken(REQUEST_CONTEXT, requestContext)], execute) : await execute();
1890
+ const result = await execute();
1787
1891
  assertDeclaredResponseStatus(route, result, ctx.set.status);
1788
1892
  return result;
1789
1893
  };
@@ -1846,8 +1950,8 @@ async function executeJob(compiled, services, job, input, requestContext, import
1846
1950
  stage: "handler",
1847
1951
  ...executionTrace(requestContext)
1848
1952
  }, () => Reflect.apply(method, instance, [decodedInput])));
1849
- const pipeline = observedAspects(compiled.aspects ?? [], `module:${compiled.name}`, observer);
1850
- const jobPipeline = observedAspects(job.aspects ?? [], "job", observer);
1953
+ const pipeline = descriptorPipeline(compiled, `module:${compiled.name}`, observer);
1954
+ const jobPipeline = descriptorPipeline(job, "job", observer);
1851
1955
  const result = await pipeline(context, () => jobPipeline(context, () => observeExecution(observer, {
1852
1956
  kind: "job",
1853
1957
  operation: job.name,
@@ -1947,6 +2051,7 @@ export {
1947
2051
  composeAspects,
1948
2052
  composeCommandExecutors,
1949
2053
  createApplication,
2054
+ createCommandAuthorizationAdapter,
1950
2055
  createCommandExecutor,
1951
2056
  createDocumentationPlugin,
1952
2057
  createMemoryPolicy,
@@ -2,7 +2,7 @@ import { type CommandIdentity, type DurableCommandReceipt } from "@supacloud/con
2
2
  import { type CommandInvocation } from "./index";
3
3
  export interface PersistentCommandHandler<Result> {
4
4
  readonly kind: "transactional" | "external";
5
- execute(identity: CommandIdentity, key: string, value: unknown): Promise<DurableCommandReceipt<Result>>;
5
+ execute(identity: CommandIdentity, key: string, value: unknown, signal?: AbortSignal): Promise<DurableCommandReceipt<Result>>;
6
6
  }
7
7
  /** Registered persistence owns the operation; a second route handler is never invoked. */
8
8
  export declare function createPersistentCommandAdapter<Result>(command: PersistentCommandHandler<Result>, options: {
@@ -1,7 +1,16 @@
1
- import type { CommandAuthorization, CommandIdentity } from "@supacloud/contracts";
1
+ import { type CommandAuthorization, type CommandIdentity } from "@supacloud/contracts";
2
2
  import { type CommandDatabase, type CommandTransaction } from "@supacloud/db";
3
+ import { type CommandExecutor } from "./index";
3
4
  export { decodeWebhookInput } from "./fixtures/webhook/contracts";
4
5
  export declare const WEBHOOK_EXAMPLE_SQL = "CREATE TABLE IF NOT EXISTS public.webhook_module_example (\n tenant_id text NOT NULL, id text NOT NULL, enabled boolean NOT NULL,\n writes integer NOT NULL DEFAULT 0, PRIMARY KEY (tenant_id,id)\n);";
6
+ /**
7
+ * Register the existing native command boundary, not a pass-through executor.
8
+ * Controllers still call their generated UpdateWebhook service directly. Any
9
+ * explicit command invocation delegates to that same service, which owns
10
+ * authorization, durable receipts, transactions and audit. Never run a second
11
+ * route continuation or construct another command runtime here.
12
+ */
13
+ export declare const executeWebhookCommand: CommandExecutor;
5
14
  /** Composition only; the compiler generates factories and routes from the real module. */
6
15
  export declare function createWebhookMigrationExample(options: {
7
16
  database: CommandDatabase;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/elysia",
3
- "version": "0.16.0",
3
+ "version": "0.18.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",
@@ -46,21 +46,21 @@
46
46
  "directory": "packages/elysia"
47
47
  },
48
48
  "dependencies": {
49
- "@supacloud/app": "0.14.0",
49
+ "@supacloud/app": "0.15.0",
50
50
  "@supacloud/contracts": "0.4.0",
51
51
  "jose": "^6.2.11"
52
52
  },
53
53
  "overrides": {
54
54
  "@supacloud/contracts": "0.4.0",
55
- "@supacloud/commands": "0.4.0"
55
+ "@supacloud/commands": "0.5.0"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "elysia": "^1.4.30"
59
59
  },
60
60
  "devDependencies": {
61
- "@supacloud/compiler": "0.21.0",
62
- "@supacloud/commands": "0.4.0",
63
- "@supacloud/db": "0.7.1",
61
+ "@supacloud/compiler": "0.23.0",
62
+ "@supacloud/commands": "0.5.0",
63
+ "@supacloud/db": "0.8.0",
64
64
  "@types/bun": "^1.4.2",
65
65
  "elysia": "^1.4.30",
66
66
  "typescript": "^7.0.2"