@supacloud/elysia 0.6.0 → 0.8.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
@@ -15,6 +15,9 @@ Runtime adapter that turns `@supacloud/compiler` output into a production-ready
15
15
  `destroyRequestScope` after the response, including when the handler fails.
16
16
  - **TypeBox schema binding**: attaches compiled parameter, query, body, and
17
17
  response TypeBox schemas directly to Elysia route definitions.
18
+ - **Compiler invoker execution**: uses the compiler-emitted positional invoker
19
+ after Elysia has decoded route input, while retaining the legacy input-object
20
+ handler path for hand-written compiled fixtures.
18
21
  - **Unified Command Pipeline**: runs `@Command`-decorated handlers through a
19
22
  structured `commandGovernance` adapter chain or a custom `composeCommandExecutors`
20
23
  pipeline (fail-closed if command routes lack an executor).
@@ -58,6 +61,31 @@ const app = createApplication({
58
61
  export default app;
59
62
  ```
60
63
 
64
+ For deterministic local verification, use the in-memory sandbox. It supplies
65
+ stable request identity, an isolated key-value database with optimistic
66
+ transaction rollback, and an in-memory object store without requiring
67
+ PostgreSQL, GoTrue, or S3:
68
+
69
+ ```ts
70
+ import { createMemorySandbox } from "@supacloud/elysia";
71
+
72
+ const sandbox = createMemorySandbox({
73
+ modules: [CaseModule],
74
+ identity: { authenticated: true, subject: "test-user" },
75
+ requestId: "test-request",
76
+ });
77
+
78
+ const response = await sandbox.request("/cases/42");
79
+ sandbox.db.set("cases", "42", { state: "draft" });
80
+ sandbox.storage.put("evidence", "42.txt", "fixture");
81
+ sandbox.reset();
82
+ ```
83
+
84
+ Route `body`, `params`, `query`, and `response` schemas are enforced by
85
+ Elysia before and after the handler. Invalid input returns the standard `422`
86
+ validation response; invalid handler output is rejected before it reaches the
87
+ client.
88
+
61
89
  Jobs are executed explicitly with `executeJob(compiledModule, services, job,
62
90
  input, requestContext)`. The asynchronous compiler-generated job scope is
63
91
  destroyed after execution, including when the job throws or scope construction
@@ -88,7 +116,37 @@ once is rejected.
88
116
  Executes a compiler-emitted Job descriptor with its static aspect list and
89
117
  compiler-generated job scope.
90
118
 
119
+ ### `assertFeatureTransition(spec, state, event)`
120
+
121
+ Checks a declared feature transition against an authoritative state and returns
122
+ the destination state. Unknown events, stale/illegal states and inherited object
123
+ members fail with HTTP 409 / `FEATURE_TRANSITION_CONFLICT`; malformed destinations
124
+ fail with `FEATURE_SPEC_INVALID`. The helper is a matrix assertion, not a workflow
125
+ engine, persistence or authorization layer. Call it inside the application's
126
+ transaction and persist with a row lock or expected-version check.
127
+
91
128
  ### `ApplicationError`
92
129
 
93
130
  Lightweight error class carrying HTTP `status`, machine-readable `code`, and
94
131
  optional structured `details`.
132
+
133
+ ### `createMemorySandbox(options): MemorySandbox`
134
+
135
+ Creates an in-process application harness with `request()`, `db`, `storage`,
136
+ `identity`, `policy`, `audit`, and `reset()`. The memory database is a deterministic test adapter,
137
+ not a PostgreSQL emulator; its transaction callback operates on an isolated
138
+ snapshot and detects concurrent commits.
139
+
140
+ The adapter boundary is intentional: production authorization, RLS, PostgreSQL,
141
+ S3 visibility and failure semantics must be supplied by application governance
142
+ adapters. The memory harness is limited to deterministic HTTP, key-value
143
+ transaction and object-storage contract tests.
144
+ `policy` supplies explicit permission grants/revocations and idempotency claims;
145
+ `storage.failNext()` makes storage failure paths deterministic.
146
+
147
+ Set `memoryGovernance: true` to enable test-only authorization, transaction,
148
+ idempotency and audit adapters; permissions still require explicit grants.
149
+ HTTP receipt fingerprints include route, body, params, query and business
150
+ headers, not mutable request scopes/services or identity/tracing transport.
151
+ Authorization runs again on a replay. Use durable application-owned adapters
152
+ for production receipts, transactions and audits.
@@ -0,0 +1,15 @@
1
+ /** Structural contract: the runtime does not import the compiler or discover metadata. */
2
+ export interface FeatureTransitionSpec {
3
+ name: string;
4
+ states: readonly string[];
5
+ transitions: Readonly<Record<string, {
6
+ from: string;
7
+ to: string;
8
+ }>>;
9
+ }
10
+ /**
11
+ * Check a declared transition against an authoritative state read by the caller.
12
+ * This is a matrix assertion, not persistence, authorization, or an FSM engine.
13
+ * Call inside the application's transaction and persist with a version check.
14
+ */
15
+ export declare function assertFeatureTransition<Spec extends FeatureTransitionSpec>(spec: Spec, state: string, event: string): Spec["states"][number];
package/dist/index.d.ts CHANGED
@@ -9,6 +9,14 @@ export interface CompiledRoute {
9
9
  params?: unknown;
10
10
  query?: unknown;
11
11
  response?: unknown;
12
+ /** Compiler-emitted positional invoker; used when available. */
13
+ invoker?: (controller: unknown, request: {
14
+ params?: Record<string, unknown>;
15
+ query?: Record<string, unknown>;
16
+ body?: unknown;
17
+ headers?: Record<string, unknown>;
18
+ context?: unknown;
19
+ }) => Promise<unknown> | unknown;
12
20
  /** Class name of the @Command explicitly bound to this route. */
13
21
  command?: string;
14
22
  /** Statically generated route aspects. */
@@ -73,7 +81,7 @@ export interface CommandInvocation {
73
81
  command: CompiledCommand;
74
82
  input: {
75
83
  body: unknown;
76
- params: Record<string, string>;
84
+ params: Record<string, unknown>;
77
85
  query: Record<string, unknown>;
78
86
  };
79
87
  request: Request;
@@ -82,6 +90,8 @@ export interface CommandInvocation {
82
90
  services: Record<string, unknown>;
83
91
  }
84
92
  export type CommandExecutor = (invocation: CommandInvocation, next: () => unknown | Promise<unknown>) => unknown | Promise<unknown>;
93
+ export { assertFeatureTransition } from "./feature";
94
+ export type { FeatureTransitionSpec } from "./feature";
85
95
  export interface ApplicationAspectContext {
86
96
  kind: "route" | "command" | "job";
87
97
  name: string;
@@ -205,3 +215,7 @@ export declare function defaultErrorResponse(error: unknown, frameworkCode?: str
205
215
  export declare function createApplication(options: ApplicationOptions): Elysia;
206
216
  /** Semantic alias of createApplication for readable tests. */
207
217
  export declare function createTestApp(options: ApplicationOptions): Elysia;
218
+ export { createMemorySandbox, } from "./memory";
219
+ export type { HandleLike as MemoryHandleLike, MemoryDatabase, MemorySandbox, MemorySandboxOptions, MemoryStorage, MemoryStorageObject, } from "./memory";
220
+ export { createMemoryPolicy } from "./memory_policy";
221
+ export type { MemoryPolicy } from "./memory_policy";
package/dist/index.js CHANGED
@@ -1,5 +1,354 @@
1
1
  // src/index.ts
2
2
  import { Elysia } from "elysia";
3
+
4
+ // src/feature.ts
5
+ function assertFeatureTransition(spec, state, event) {
6
+ const transition = Object.hasOwn(spec.transitions, event) ? spec.transitions[event] : undefined;
7
+ if (!transition || !spec.states.includes(state) || transition.from !== state) {
8
+ throw new ApplicationError("Feature transition is not allowed", {
9
+ status: 409,
10
+ code: "FEATURE_TRANSITION_CONFLICT"
11
+ });
12
+ }
13
+ if (!spec.states.includes(transition.to)) {
14
+ throw new ApplicationError("Feature transition references an undeclared state", {
15
+ code: "FEATURE_SPEC_INVALID"
16
+ });
17
+ }
18
+ return transition.to;
19
+ }
20
+ // src/memory.ts
21
+ import { AsyncLocalStorage } from "node:async_hooks";
22
+
23
+ // src/memory_policy.ts
24
+ function createMemoryPolicy() {
25
+ const permissions = new Map;
26
+ const receipts = new Map;
27
+ return {
28
+ grant(subject, permission) {
29
+ const values = permissions.get(subject) ?? new Set;
30
+ values.add(permission);
31
+ permissions.set(subject, values);
32
+ },
33
+ revoke(subject, permission) {
34
+ permissions.get(subject)?.delete(permission);
35
+ },
36
+ can(subject, permission) {
37
+ return permissions.get(subject)?.has(permission) ?? false;
38
+ },
39
+ claimIdempotency(subject, key) {
40
+ const id = JSON.stringify([subject, key]);
41
+ if (receipts.has(id))
42
+ return false;
43
+ receipts.set(id, { input: "", result: Promise.resolve(undefined) });
44
+ return true;
45
+ },
46
+ subject(context) {
47
+ if (typeof context === "object" && context !== null && "identity" in context) {
48
+ const identity = context.identity;
49
+ if (typeof identity === "object" && identity !== null && "authenticated" in identity && identity.authenticated === true && "subject" in identity && typeof identity.subject === "string" && identity.subject.length > 0)
50
+ return identity.subject;
51
+ }
52
+ throw new ApplicationError("Memory authenticated identity required", { status: 401, code: "MEMORY_IDENTITY_REQUIRED" });
53
+ },
54
+ async runOnce(subject, command, key, input, work) {
55
+ if (!subject || !command || !key)
56
+ throw new Error("Memory receipt requires subject, command and key");
57
+ const id = JSON.stringify([subject, command, key]);
58
+ const fingerprint = stableStringify(input);
59
+ const existing = receipts.get(id);
60
+ if (existing) {
61
+ if (existing.input !== fingerprint) {
62
+ throw new ApplicationError("Idempotency input conflict", { status: 409, code: "MEMORY_IDEMPOTENCY_CONFLICT" });
63
+ }
64
+ return structuredClone(await existing.result);
65
+ }
66
+ const result = Promise.resolve().then(work).then((value) => structuredClone(value));
67
+ const entry = { input: fingerprint, result };
68
+ receipts.set(id, entry);
69
+ try {
70
+ return structuredClone(await result);
71
+ } catch (error) {
72
+ if (receipts.get(id) === entry)
73
+ receipts.delete(id);
74
+ throw error;
75
+ }
76
+ },
77
+ clear() {
78
+ permissions.clear();
79
+ receipts.clear();
80
+ }
81
+ };
82
+ }
83
+ function stableStringify(value) {
84
+ if (value === null || typeof value !== "object")
85
+ return JSON.stringify(value);
86
+ if (Array.isArray(value))
87
+ return `[${value.map(stableStringify).join(",")}]`;
88
+ const record = value;
89
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
90
+ }
91
+
92
+ // src/memory.ts
93
+ class DefaultMemoryDatabase {
94
+ tables = new Map;
95
+ version = 0;
96
+ current = new AsyncLocalStorage;
97
+ get(table, key) {
98
+ if (this.current.getStore())
99
+ return this.current.getStore()?.get(table, key);
100
+ const value = this.tables.get(table)?.get(key);
101
+ return value === undefined ? undefined : cloneValue(value);
102
+ }
103
+ set(table, key, value) {
104
+ const current = this.current.getStore();
105
+ if (current)
106
+ return current.set(table, key, value);
107
+ const rows = this.tables.get(table) ?? new Map;
108
+ rows.set(key, cloneValue(value));
109
+ this.tables.set(table, rows);
110
+ this.version += 1;
111
+ }
112
+ delete(table, key) {
113
+ const current = this.current.getStore();
114
+ if (current)
115
+ return current.delete(table, key);
116
+ const deleted = this.tables.get(table)?.delete(key) ?? false;
117
+ if (deleted)
118
+ this.version += 1;
119
+ return deleted;
120
+ }
121
+ list(table) {
122
+ const current = this.current.getStore();
123
+ if (current)
124
+ return current.list(table);
125
+ return [...this.tables.get(table)?.entries() ?? []].map(([key, value]) => ({ key, value: cloneValue(value) })).sort((left, right) => left.key.localeCompare(right.key));
126
+ }
127
+ clear() {
128
+ const current = this.current.getStore();
129
+ if (current)
130
+ return current.clear();
131
+ this.tables.clear();
132
+ this.version += 1;
133
+ }
134
+ async transaction(work) {
135
+ if (this.current.getStore())
136
+ throw new Error("Nested memory transactions are not supported");
137
+ const baseVersion = this.version;
138
+ const snapshot = cloneTables(this.tables);
139
+ const transaction = new SnapshotMemoryDatabase(snapshot);
140
+ try {
141
+ const result = await this.current.run(transaction, () => work(transaction));
142
+ if (this.version !== baseVersion) {
143
+ throw new Error("Memory transaction conflict: state changed during transaction");
144
+ }
145
+ const committed = cloneTables(transaction.tables);
146
+ this.tables.clear();
147
+ for (const [table, rows] of committed)
148
+ this.tables.set(table, rows);
149
+ this.version += 1;
150
+ return result;
151
+ } finally {
152
+ transaction.close();
153
+ }
154
+ }
155
+ }
156
+
157
+ class SnapshotMemoryDatabase {
158
+ tables;
159
+ closed = false;
160
+ constructor(tables) {
161
+ this.tables = tables;
162
+ }
163
+ close() {
164
+ this.closed = true;
165
+ }
166
+ assertOpen() {
167
+ if (this.closed)
168
+ throw new Error("Memory transaction is closed");
169
+ }
170
+ get(table, key) {
171
+ this.assertOpen();
172
+ const value = this.tables.get(table)?.get(key);
173
+ return value === undefined ? undefined : cloneValue(value);
174
+ }
175
+ set(table, key, value) {
176
+ this.assertOpen();
177
+ const rows = this.tables.get(table) ?? new Map;
178
+ rows.set(key, cloneValue(value));
179
+ this.tables.set(table, rows);
180
+ }
181
+ delete(table, key) {
182
+ this.assertOpen();
183
+ return this.tables.get(table)?.delete(key) ?? false;
184
+ }
185
+ list(table) {
186
+ this.assertOpen();
187
+ return [...this.tables.get(table)?.entries() ?? []].map(([key, value]) => ({ key, value: cloneValue(value) })).sort((left, right) => left.key.localeCompare(right.key));
188
+ }
189
+ clear() {
190
+ this.assertOpen();
191
+ this.tables.clear();
192
+ }
193
+ transaction() {
194
+ return Promise.reject(new Error("Nested memory transactions are not supported"));
195
+ }
196
+ }
197
+
198
+ class DefaultMemoryStorage {
199
+ objects = new Map;
200
+ pendingFailure;
201
+ policy;
202
+ put(bucket, key, body, options = {}) {
203
+ this.check("put", bucket, key);
204
+ this.objects.set(storageKey(bucket, key), {
205
+ bucket,
206
+ key,
207
+ body: typeof body === "string" ? new TextEncoder().encode(body) : new Uint8Array(body),
208
+ contentType: options.contentType,
209
+ metadata: { ...options.metadata ?? {} }
210
+ });
211
+ }
212
+ get(bucket, key) {
213
+ this.check("get", bucket, key);
214
+ const object = this.objects.get(storageKey(bucket, key));
215
+ return object ? cloneObject(object) : undefined;
216
+ }
217
+ delete(bucket, key) {
218
+ this.check("delete", bucket, key);
219
+ return this.objects.delete(storageKey(bucket, key));
220
+ }
221
+ list(bucket, prefix = "") {
222
+ this.check("list", bucket, prefix);
223
+ return [...this.objects.values()].filter((object) => object.bucket === bucket && object.key.startsWith(prefix)).filter((object) => !this.policy || this.policy("get", object.bucket, object.key)).sort((left, right) => left.key.localeCompare(right.key)).map(cloneObject);
224
+ }
225
+ clear() {
226
+ this.objects.clear();
227
+ this.pendingFailure = undefined;
228
+ }
229
+ failNext(error = new Error("Memory storage failure")) {
230
+ this.pendingFailure = error;
231
+ }
232
+ setPolicy(policy) {
233
+ this.policy = policy;
234
+ }
235
+ check(operation, bucket, key) {
236
+ if (this.pendingFailure) {
237
+ const error = this.pendingFailure;
238
+ this.pendingFailure = undefined;
239
+ throw error;
240
+ }
241
+ if (this.policy && !this.policy(operation, bucket, key)) {
242
+ throw new ApplicationError("Memory storage access denied", { status: 403, code: "MEMORY_STORAGE_DENIED" });
243
+ }
244
+ }
245
+ }
246
+ function createMemorySandbox(options = {}) {
247
+ const db = new DefaultMemoryDatabase;
248
+ const storage = new DefaultMemoryStorage;
249
+ const policy = createMemoryPolicy();
250
+ const audit = [];
251
+ const governance = {
252
+ authorize(invocation) {
253
+ const subject = policy.subject(invocation.requestContext);
254
+ if (!invocation.command.permission || !policy.can(subject, invocation.command.permission)) {
255
+ throw new ApplicationError("Memory permission denied", { status: 403, code: "MEMORY_PERMISSION_DENIED" });
256
+ }
257
+ },
258
+ idempotency(invocation, next) {
259
+ return policy.runOnce(policy.subject(invocation.requestContext), invocation.command.name, requireIdempotencyKey(invocation), memoryCommandInput(invocation), next);
260
+ },
261
+ transaction(_invocation, next) {
262
+ const count = audit.length;
263
+ return db.transaction(() => next()).catch((error) => {
264
+ audit.splice(count);
265
+ throw error;
266
+ });
267
+ },
268
+ audit: {
269
+ succeeded(invocation) {
270
+ audit.push({ command: invocation.command.name, outcome: "succeeded" });
271
+ },
272
+ failed(invocation) {
273
+ audit.push({ command: invocation.command.name, outcome: "failed" });
274
+ }
275
+ }
276
+ };
277
+ const identity = options.identity ?? { authenticated: false };
278
+ const requestId = options.requestId ?? "memory-request";
279
+ const configuredContext = options.requestContext;
280
+ const app = createApplication({
281
+ ...options,
282
+ commandGovernance: options.commandGovernance ?? (options.memoryGovernance ? governance : undefined),
283
+ deps: {
284
+ ...options.deps ?? {},
285
+ dbClient: db,
286
+ memoryDb: db,
287
+ storage,
288
+ memoryStorage: storage,
289
+ memoryPolicy: policy
290
+ },
291
+ requestContext: configuredContext ?? ((request) => ({
292
+ requestId: request.headers.get("x-request-id") ?? requestId,
293
+ request,
294
+ identity
295
+ }))
296
+ });
297
+ return {
298
+ app,
299
+ db,
300
+ storage,
301
+ policy,
302
+ audit,
303
+ identity,
304
+ request(path, init = {}) {
305
+ const url = path.startsWith("http://") || path.startsWith("https://") ? path : `http://memory.local${path.startsWith("/") ? path : `/${path}`}`;
306
+ return app.handle(new Request(url, init));
307
+ },
308
+ reset() {
309
+ db.clear();
310
+ storage.clear();
311
+ policy.clear();
312
+ audit.length = 0;
313
+ }
314
+ };
315
+ }
316
+ function memoryCommandInput(invocation) {
317
+ const input = invocation.input;
318
+ if (typeof input !== "object" || input === null)
319
+ return input;
320
+ const ignoredHeaders = new Set(["authorization", "idempotency-key", "x-request-id", "traceparent", "tracestate"]);
321
+ const headers = Object.fromEntries([...invocation.request.headers.entries()].filter(([name]) => !ignoredHeaders.has(name)));
322
+ return {
323
+ method: invocation.request.method,
324
+ path: new URL(invocation.request.url).pathname,
325
+ body: "body" in input ? input.body : undefined,
326
+ params: "params" in input ? input.params : undefined,
327
+ query: "query" in input ? input.query : undefined,
328
+ headers
329
+ };
330
+ }
331
+ function storageKey(bucket, key) {
332
+ return JSON.stringify([bucket, key]);
333
+ }
334
+ function cloneObject(object) {
335
+ return {
336
+ ...object,
337
+ body: new Uint8Array(object.body),
338
+ metadata: { ...object.metadata }
339
+ };
340
+ }
341
+ function cloneTables(tables) {
342
+ return new Map([...tables.entries()].map(([table, rows]) => [
343
+ table,
344
+ new Map([...rows.entries()].map(([key, value]) => [key, cloneValue(value)]))
345
+ ]));
346
+ }
347
+ function cloneValue(value) {
348
+ return structuredClone(value);
349
+ }
350
+
351
+ // src/index.ts
3
352
  var VERIFIED_JWT_SUBJECT_HEADER = "x-supacloud-jwt-sub";
4
353
  var EXECUTION_ID_HEADER = "x-sb-execution-id";
5
354
  var IDEMPOTENCY_KEY_HEADER = "idempotency-key";
@@ -250,11 +599,13 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
250
599
  body: ctx.body,
251
600
  params: ctx.params,
252
601
  query: ctx.query,
602
+ headers: Object.fromEntries(ctx.request.headers.entries()),
603
+ context: requestContext,
253
604
  request: ctx.request,
254
605
  scope: requestScope,
255
606
  requestContext
256
607
  };
257
- const invoke = () => Reflect.apply(method, instance, [input]);
608
+ const invoke = () => route.invoker ? route.invoker(instance, input) : Reflect.apply(method, instance, [input]);
258
609
  const routeContext = {
259
610
  kind: "route",
260
611
  name: `${route.method} ${path}`,
@@ -436,10 +787,13 @@ export {
436
787
  EXECUTION_ID_HEADER,
437
788
  IDEMPOTENCY_KEY_HEADER,
438
789
  VERIFIED_JWT_SUBJECT_HEADER,
790
+ assertFeatureTransition,
439
791
  composeAspects,
440
792
  composeCommandExecutors,
441
793
  createApplication,
442
794
  createCommandExecutor,
795
+ createMemoryPolicy,
796
+ createMemorySandbox,
443
797
  createModulePlugin,
444
798
  createSupaCloudRequestContext,
445
799
  createTestApp,
@@ -0,0 +1,68 @@
1
+ import { type ApplicationOptions, type TrustedRequestIdentity } from "./index";
2
+ import { type MemoryPolicy } from "./memory_policy";
3
+ export interface HandleLike {
4
+ handle(request: Request): Promise<Response> | Response;
5
+ }
6
+ export interface MemoryDatabase {
7
+ get(table: string, key: string): unknown;
8
+ set(table: string, key: string, value: unknown): void;
9
+ delete(table: string, key: string): boolean;
10
+ list(table: string): Array<{
11
+ key: string;
12
+ value: unknown;
13
+ }>;
14
+ clear(): void;
15
+ transaction<T>(work: (tx: MemoryDatabase) => T | Promise<T>): Promise<T>;
16
+ }
17
+ export interface MemoryStorageObject {
18
+ bucket: string;
19
+ key: string;
20
+ body: Uint8Array;
21
+ contentType?: string;
22
+ metadata: Record<string, string>;
23
+ }
24
+ export interface MemoryStorage {
25
+ put(bucket: string, key: string, body: Uint8Array | string, options?: {
26
+ contentType?: string;
27
+ metadata?: Record<string, string>;
28
+ }): void;
29
+ get(bucket: string, key: string): MemoryStorageObject | undefined;
30
+ delete(bucket: string, key: string): boolean;
31
+ list(bucket: string, prefix?: string): MemoryStorageObject[];
32
+ clear(): void;
33
+ failNext(error?: Error): void;
34
+ setPolicy(policy?: (operation: "put" | "get" | "delete" | "list", bucket: string, key: string) => boolean): void;
35
+ }
36
+ export interface MemorySandboxOptions extends Omit<ApplicationOptions, "deps" | "requestContext"> {
37
+ /** Stable identity used by every request unless overridden by requestContext. */
38
+ identity?: TrustedRequestIdentity;
39
+ /** Stable request id used when the request does not provide x-request-id. */
40
+ requestId?: string;
41
+ /** Extend the platform dependency bag without replacing memory dependencies. */
42
+ deps?: Record<string, unknown>;
43
+ /** Replace the default deterministic request context factory. */
44
+ requestContext?: ApplicationOptions["requestContext"];
45
+ /** Enable the test-only permission, receipt, transaction and audit adapters. */
46
+ memoryGovernance?: boolean;
47
+ }
48
+ export interface MemorySandbox {
49
+ app: HandleLike;
50
+ db: MemoryDatabase;
51
+ storage: MemoryStorage;
52
+ policy: MemoryPolicy;
53
+ audit: Array<{
54
+ command: string;
55
+ outcome: "succeeded" | "failed";
56
+ }>;
57
+ identity: TrustedRequestIdentity;
58
+ request(path: string, init?: RequestInit): Promise<Response>;
59
+ reset(): void;
60
+ }
61
+ /**
62
+ * Creates a deterministic in-process application harness.
63
+ *
64
+ * The database and storage are intentionally small adapter contracts, not
65
+ * PostgreSQL or S3 emulators. They are sufficient for fast command and HTTP
66
+ * behavior tests while keeping production adapters replaceable.
67
+ */
68
+ export declare function createMemorySandbox(options?: MemorySandboxOptions): MemorySandbox;
@@ -0,0 +1,11 @@
1
+ export interface MemoryPolicy {
2
+ grant(subject: string, permission: string): void;
3
+ revoke(subject: string, permission: string): void;
4
+ can(subject: string, permission: string): boolean;
5
+ claimIdempotency(subject: string, key: string): boolean;
6
+ subject(context: unknown): string;
7
+ runOnce<T>(subject: string, command: string, key: string, input: unknown, work: () => T | Promise<T>): Promise<T>;
8
+ clear(): void;
9
+ }
10
+ /** Test-only grants and successful-result receipts, scoped by subject and command. */
11
+ export declare function createMemoryPolicy(): MemoryPolicy;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/elysia",
3
- "version": "0.6.0",
3
+ "version": "0.8.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",