@supacloud/elysia 0.17.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/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
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
4
  import { type ExecutionObserver } from "./execution";
4
5
  import { type ApplicationDocumentationOptions } from "./documentation";
@@ -116,19 +117,8 @@ export interface SupaCloudRequestContext {
116
117
  identity: TrustedRequestIdentity;
117
118
  idempotencyKey?: string;
118
119
  }
119
- export interface CommandInvocation {
120
- command: CompiledCommand;
121
- input: {
122
- body: unknown;
123
- params: Record<string, unknown>;
124
- query: Record<string, unknown>;
125
- };
126
- request: Request;
127
- requestContext: unknown;
128
- scope?: Record<string, unknown>;
129
- services: Record<string, unknown>;
130
- }
131
- export type CommandExecutor = (invocation: CommandInvocation, next: () => unknown | Promise<unknown>) => unknown | Promise<unknown>;
120
+ export type CommandInvocation = CommandRuntimeInvocation<CompiledCommand>;
121
+ export type CommandExecutor = CommandRuntimeMiddleware<CommandInvocation>;
132
122
  export { assertFeatureTransition } from "./feature";
133
123
  export type { FeatureTransitionSpec } from "./feature";
134
124
  export interface ApplicationAspectContext {
@@ -149,28 +139,48 @@ export type ApplicationAspect = (context: ApplicationAspectContext, next: () =>
149
139
  */
150
140
  export declare function composeAspects(...aspects: (ApplicationAspect | undefined | null)[]): ApplicationAspect;
151
141
  export type ApplicationAspectPipeline = (context: ApplicationAspectContext, next: () => unknown | Promise<unknown>, observe?: (stage: string, run: () => unknown | Promise<unknown>) => unknown | Promise<unknown>) => unknown | Promise<unknown>;
152
- export type CommandAuthorizer = (invocation: CommandInvocation) => void | Promise<void>;
153
- export type CommandMiddleware = (invocation: CommandInvocation, next: () => unknown | Promise<unknown>) => unknown | Promise<unknown>;
154
- export interface CommandAudit {
155
- succeeded(invocation: CommandInvocation, result: unknown): void | Promise<void>;
156
- failed(invocation: CommandInvocation, error: unknown): void | Promise<void>;
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;
157
167
  }
158
- export interface CommandGovernance {
159
- /** Application-owned adapters: a single RPC owns all declared persistence. */
160
- rpc?: Record<string, {
161
- capabilities: {
162
- audit?: boolean;
163
- transaction?: boolean;
164
- idempotency?: boolean;
165
- boundary?: "database" | "external";
166
- };
167
- execute: CommandMiddleware;
168
- }>;
169
- authorize: CommandAuthorizer;
170
- idempotency?: CommandMiddleware;
171
- transaction?: CommandMiddleware;
172
- 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;
173
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;
174
184
  /**
175
185
  * Compose multiple CommandExecutors into a single onion-style pipeline.
176
186
  * Outer executors run first before calling `next()`, and complete last.
package/dist/index.js CHANGED
@@ -1291,6 +1291,42 @@ function descriptorPipeline(descriptor, boundary, observer) {
1291
1291
  ...executionTrace(context.requestContext)
1292
1292
  }, run) : undefined);
1293
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
+ }
1294
1330
  function composeCommandExecutors(...executors) {
1295
1331
  const active = executors.filter((e) => typeof e === "function");
1296
1332
  if (active.length === 0)
@@ -1585,6 +1621,67 @@ function requireIdempotencyKey(invocation) {
1585
1621
  }
1586
1622
  return key;
1587
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
+ }
1588
1685
  function joinPaths(prefix, path) {
1589
1686
  const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
1590
1687
  return joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
@@ -1654,6 +1751,7 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
1654
1751
  if (hasCommandRoutes && !options.commandGovernance && !options.commandExecutor) {
1655
1752
  throw new ApplicationError(`Module "${compiled.name}" has command routes but no commandGovernance`, { code: "COMMAND_GOVERNANCE_UNCONFIGURED" });
1656
1753
  }
1754
+ assertCommandGovernanceReady(compiled, options.commandGovernance, options.commandExecutor);
1657
1755
  const commandsByClassName = new Map((compiled.commands ?? []).map((command) => [command.className, command]));
1658
1756
  for (const controller of compiled.controllers) {
1659
1757
  for (const route of controller.routes) {
@@ -1953,6 +2051,7 @@ export {
1953
2051
  composeAspects,
1954
2052
  composeCommandExecutors,
1955
2053
  createApplication,
2054
+ createCommandAuthorizationAdapter,
1956
2055
  createCommandExecutor,
1957
2056
  createDocumentationPlugin,
1958
2057
  createMemoryPolicy,
@@ -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.17.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,7 +46,7 @@
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
  },
@@ -58,7 +58,7 @@
58
58
  "elysia": "^1.4.30"
59
59
  },
60
60
  "devDependencies": {
61
- "@supacloud/compiler": "0.22.0",
61
+ "@supacloud/compiler": "0.23.0",
62
62
  "@supacloud/commands": "0.5.0",
63
63
  "@supacloud/db": "0.8.0",
64
64
  "@types/bun": "^1.4.2",