@meekojs/meeko 0.0.3 → 0.0.4

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.
Files changed (144) hide show
  1. package/README.md +18 -1
  2. package/dist/autoimports-ts.d.ts +26 -0
  3. package/dist/autoimports-ts.js +127 -0
  4. package/dist/autoimports-ts.js.map +1 -0
  5. package/dist/autoimports.d.ts +21 -0
  6. package/dist/autoimports.js +285 -0
  7. package/dist/autoimports.js.map +1 -0
  8. package/dist/check.js +35 -23
  9. package/dist/check.js.map +1 -0
  10. package/dist/cli.js +33 -4
  11. package/dist/cli.js.map +1 -0
  12. package/dist/compiler/import-graph.d.ts +8 -0
  13. package/dist/compiler/import-graph.js +33 -0
  14. package/dist/compiler/import-graph.js.map +1 -0
  15. package/dist/compiler/mapping.js +1 -0
  16. package/dist/compiler/mapping.js.map +1 -0
  17. package/dist/compiler/project-root.d.ts +2 -0
  18. package/dist/compiler/project-root.js +18 -0
  19. package/dist/compiler/project-root.js.map +1 -0
  20. package/dist/compiler/sfc.js +1 -0
  21. package/dist/compiler/sfc.js.map +1 -0
  22. package/dist/compiler/sourcemap.d.ts +3 -0
  23. package/dist/compiler/sourcemap.js +75 -0
  24. package/dist/compiler/sourcemap.js.map +1 -0
  25. package/dist/compiler/template.js +1 -0
  26. package/dist/compiler/template.js.map +1 -0
  27. package/dist/compiler.js +11 -5
  28. package/dist/compiler.js.map +1 -0
  29. package/dist/devtools-ui/index.js +1 -0
  30. package/dist/devtools-ui/index.js.map +1 -0
  31. package/dist/devtools.js +1 -0
  32. package/dist/devtools.js.map +1 -0
  33. package/dist/dom.d.ts +8 -1
  34. package/dist/dom.js +10 -5
  35. package/dist/dom.js.map +1 -0
  36. package/dist/framework/check-app.d.ts +5 -0
  37. package/dist/framework/check-app.js +59 -0
  38. package/dist/framework/check-app.js.map +1 -0
  39. package/dist/framework/client.d.ts +45 -0
  40. package/dist/framework/client.js +146 -0
  41. package/dist/framework/client.js.map +1 -0
  42. package/dist/framework/clients.d.ts +82 -0
  43. package/dist/framework/clients.js +182 -0
  44. package/dist/framework/clients.js.map +1 -0
  45. package/dist/framework/config-type.d.ts +10 -0
  46. package/dist/framework/config-type.js +4 -0
  47. package/dist/framework/config-type.js.map +1 -0
  48. package/dist/framework/config.d.ts +15 -0
  49. package/dist/framework/config.js +138 -0
  50. package/dist/framework/config.js.map +1 -0
  51. package/dist/framework/contracts-generate.d.ts +8 -0
  52. package/dist/framework/contracts-generate.js +227 -0
  53. package/dist/framework/contracts-generate.js.map +1 -0
  54. package/dist/framework/generate.d.ts +7 -0
  55. package/dist/framework/generate.js +67 -0
  56. package/dist/framework/generate.js.map +1 -0
  57. package/dist/framework/init.d.ts +2 -0
  58. package/dist/framework/init.js +362 -0
  59. package/dist/framework/init.js.map +1 -0
  60. package/dist/framework/modules.d.ts +22 -0
  61. package/dist/framework/modules.js +29 -0
  62. package/dist/framework/modules.js.map +1 -0
  63. package/dist/framework/page.d.ts +60 -0
  64. package/dist/framework/page.js +42 -0
  65. package/dist/framework/page.js.map +1 -0
  66. package/dist/framework/prerender.d.ts +14 -0
  67. package/dist/framework/prerender.js +104 -0
  68. package/dist/framework/prerender.js.map +1 -0
  69. package/dist/framework/router.d.ts +39 -0
  70. package/dist/framework/router.js +173 -0
  71. package/dist/framework/router.js.map +1 -0
  72. package/dist/framework/server.d.ts +51 -0
  73. package/dist/framework/server.js +192 -0
  74. package/dist/framework/server.js.map +1 -0
  75. package/dist/framework/socket-contracts-generate.d.ts +5 -0
  76. package/dist/framework/socket-contracts-generate.js +159 -0
  77. package/dist/framework/socket-contracts-generate.js.map +1 -0
  78. package/dist/framework/socket.d.ts +70 -0
  79. package/dist/framework/socket.js +150 -0
  80. package/dist/framework/socket.js.map +1 -0
  81. package/dist/framework/state.d.ts +15 -0
  82. package/dist/framework/state.js +72 -0
  83. package/dist/framework/state.js.map +1 -0
  84. package/dist/framework-server/auto-clients.d.ts +3 -0
  85. package/dist/framework-server/auto-clients.js +27 -0
  86. package/dist/framework-server/auto-clients.js.map +1 -0
  87. package/dist/framework-server/context.d.ts +21 -0
  88. package/dist/framework-server/context.js +22 -0
  89. package/dist/framework-server/context.js.map +1 -0
  90. package/dist/framework-server/contracts.d.ts +64 -0
  91. package/dist/framework-server/contracts.js +129 -0
  92. package/dist/framework-server/contracts.js.map +1 -0
  93. package/dist/framework-server/discovery.d.ts +40 -0
  94. package/dist/framework-server/discovery.js +252 -0
  95. package/dist/framework-server/discovery.js.map +1 -0
  96. package/dist/framework-server/local.d.ts +19 -0
  97. package/dist/framework-server/local.js +98 -0
  98. package/dist/framework-server/local.js.map +1 -0
  99. package/dist/framework-server/node-upgrade.d.ts +23 -0
  100. package/dist/framework-server/node-upgrade.js +78 -0
  101. package/dist/framework-server/node-upgrade.js.map +1 -0
  102. package/dist/framework-server/node-websocket.d.ts +16 -0
  103. package/dist/framework-server/node-websocket.js +125 -0
  104. package/dist/framework-server/node-websocket.js.map +1 -0
  105. package/dist/framework-server/openapi.d.ts +17 -0
  106. package/dist/framework-server/openapi.js +118 -0
  107. package/dist/framework-server/openapi.js.map +1 -0
  108. package/dist/framework-server/session-stores.d.ts +66 -0
  109. package/dist/framework-server/session-stores.js +96 -0
  110. package/dist/framework-server/session-stores.js.map +1 -0
  111. package/dist/framework-server/sessions.d.ts +64 -0
  112. package/dist/framework-server/sessions.js +136 -0
  113. package/dist/framework-server/sessions.js.map +1 -0
  114. package/dist/framework-server/socket.d.ts +83 -0
  115. package/dist/framework-server/socket.js +161 -0
  116. package/dist/framework-server/socket.js.map +1 -0
  117. package/dist/framework-server.d.ts +21 -0
  118. package/dist/framework-server.js +12 -0
  119. package/dist/framework-server.js.map +1 -0
  120. package/dist/framework.d.ts +29 -0
  121. package/dist/framework.js +13 -0
  122. package/dist/framework.js.map +1 -0
  123. package/dist/hmr.js +1 -0
  124. package/dist/hmr.js.map +1 -0
  125. package/dist/index.d.ts +3 -3
  126. package/dist/index.js +2 -1
  127. package/dist/index.js.map +1 -0
  128. package/dist/init.js +4 -2
  129. package/dist/init.js.map +1 -0
  130. package/dist/internal.js +1 -0
  131. package/dist/internal.js.map +1 -0
  132. package/dist/node.d.ts +12 -0
  133. package/dist/node.js +52 -0
  134. package/dist/node.js.map +1 -0
  135. package/dist/reactivity.d.ts +12 -0
  136. package/dist/reactivity.js +40 -3
  137. package/dist/reactivity.js.map +1 -0
  138. package/dist/server.js +1 -0
  139. package/dist/server.js.map +1 -0
  140. package/dist/types.js +23 -8
  141. package/dist/types.js.map +1 -0
  142. package/dist/vite.js +91 -6
  143. package/dist/vite.js.map +1 -0
  144. package/package.json +14 -3
@@ -0,0 +1,27 @@
1
+ import { currentRequest } from "./context.js";
2
+ /** Dynamic options prevent a module-level client from retaining any user's credentials. */
3
+ export function createServerClientOptions() {
4
+ return {
5
+ baseUrl: () => currentRequest().context.request.url,
6
+ headers: () => {
7
+ const request = currentRequest().context.request;
8
+ const headers = new Headers();
9
+ for (const name of ["cookie", "authorization"]) {
10
+ const value = request.headers.get(name);
11
+ if (value !== null)
12
+ headers.set(name, value);
13
+ }
14
+ headers.set("origin", new URL(request.url).origin);
15
+ return headers;
16
+ },
17
+ csrfToken: () => currentRequest().context.session.current?.csrfToken,
18
+ fetch: async (input, init) => {
19
+ const active = currentRequest();
20
+ const request = new Request(input, init);
21
+ if (active.serverFetch)
22
+ return active.serverFetch(request, active.context);
23
+ return fetch(request);
24
+ },
25
+ };
26
+ }
27
+ //# sourceMappingURL=auto-clients.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-clients.js","sourceRoot":"","sources":["../../src/framework-server/auto-clients.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG9C,2FAA2F;AAC3F,MAAM,UAAU,yBAAyB;IACvC,OAAO;QACL,OAAO,EAAE,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG;QACnD,OAAO,EAAE,GAAG,EAAE;YACZ,MAAM,OAAO,GAAG,cAAc,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;YACjD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;YAC9B,KAAK,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,EAAE,CAAC;gBAC/C,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACxC,IAAI,KAAK,KAAK,IAAI;oBAAE,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC/C,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;YACnD,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,SAAS,EAAE,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,SAAS;QACpE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YAC3B,MAAM,MAAM,GAAG,cAAc,EAAE,CAAC;YAChC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACzC,IAAI,MAAM,CAAC,WAAW;gBAAE,OAAO,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;YAC3E,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { currentRequest } from \"./context.js\";\nimport type { ClientOptions } from \"../framework/clients.js\";\n\n/** Dynamic options prevent a module-level client from retaining any user's credentials. */\nexport function createServerClientOptions(): ClientOptions {\n return {\n baseUrl: () => currentRequest().context.request.url,\n headers: () => {\n const request = currentRequest().context.request;\n const headers = new Headers();\n for (const name of [\"cookie\", \"authorization\"]) {\n const value = request.headers.get(name);\n if (value !== null) headers.set(name, value);\n }\n headers.set(\"origin\", new URL(request.url).origin);\n return headers;\n },\n csrfToken: () => currentRequest().context.session.current?.csrfToken,\n fetch: async (input, init) => {\n const active = currentRequest();\n const request = new Request(input, init);\n if (active.serverFetch) return active.serverFetch(request, active.context);\n return fetch(request);\n },\n };\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import type { SessionHandle } from "./sessions.js";
2
+ /** State belongs to one HTTP request and is never shared between users. */
3
+ export interface RequestContext<TServices = unknown> {
4
+ readonly request: Request;
5
+ readonly requestId: string;
6
+ readonly signal: AbortSignal;
7
+ readonly services: TServices;
8
+ readonly session: SessionHandle;
9
+ readonly state: Map<string, unknown>;
10
+ }
11
+ export type ServerFetch = (request: Request, context: RequestContext) => Promise<Response>;
12
+ interface ActiveRequest {
13
+ readonly context: RequestContext;
14
+ readonly serverFetch?: ServerFetch;
15
+ }
16
+ /** Binds automatic server clients to the current request across async work. */
17
+ export declare function withRequestContext<T>(context: RequestContext, serverFetch: ServerFetch | undefined, run: () => T): T;
18
+ export declare function currentRequest(): ActiveRequest;
19
+ export declare function createRequestContext<TServices>(request: Request, services: TServices, session: SessionHandle): RequestContext<TServices>;
20
+ export declare function useRequestState<T>(context: RequestContext, key: string, factory: () => T): T;
21
+ export {};
@@ -0,0 +1,22 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ const activeRequest = new AsyncLocalStorage();
4
+ /** Binds automatic server clients to the current request across async work. */
5
+ export function withRequestContext(context, serverFetch, run) {
6
+ return activeRequest.run({ context, ...(serverFetch ? { serverFetch } : {}) }, run);
7
+ }
8
+ export function currentRequest() {
9
+ const active = activeRequest.getStore();
10
+ if (!active)
11
+ throw new Error("Automatic server clients require an active Meeko request context");
12
+ return active;
13
+ }
14
+ export function createRequestContext(request, services, session) {
15
+ return { request, requestId: randomUUID(), signal: request.signal, services, session, state: new Map() };
16
+ }
17
+ export function useRequestState(context, key, factory) {
18
+ if (!context.state.has(key))
19
+ context.state.set(key, factory());
20
+ return context.state.get(key);
21
+ }
22
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/framework-server/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAerD,MAAM,aAAa,GAAG,IAAI,iBAAiB,EAAiB,CAAC;AAE7D,+EAA+E;AAC/E,MAAM,UAAU,kBAAkB,CAAI,OAAuB,EAAE,WAAoC,EAAE,GAAY;IAC/G,OAAO,aAAa,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;AACtF,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,EAAE,CAAC;IACxC,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;IACjG,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAY,OAAgB,EAAE,QAAmB,EAAE,OAAsB;IAC3G,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;AAC3G,CAAC;AAED,MAAM,UAAU,eAAe,CAAI,OAAuB,EAAE,GAAW,EAAE,OAAgB;IACvF,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;IAC/D,OAAO,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAM,CAAC;AACrC,CAAC","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { SessionHandle } from \"./sessions.js\";\n\n/** State belongs to one HTTP request and is never shared between users. */\nexport interface RequestContext<TServices = unknown> {\n readonly request: Request;\n readonly requestId: string;\n readonly signal: AbortSignal;\n readonly services: TServices;\n readonly session: SessionHandle;\n readonly state: Map<string, unknown>;\n}\n\nexport type ServerFetch = (request: Request, context: RequestContext) => Promise<Response>;\ninterface ActiveRequest { readonly context: RequestContext; readonly serverFetch?: ServerFetch }\nconst activeRequest = new AsyncLocalStorage<ActiveRequest>();\n\n/** Binds automatic server clients to the current request across async work. */\nexport function withRequestContext<T>(context: RequestContext, serverFetch: ServerFetch | undefined, run: () => T): T {\n return activeRequest.run({ context, ...(serverFetch ? { serverFetch } : {}) }, run);\n}\n\nexport function currentRequest(): ActiveRequest {\n const active = activeRequest.getStore();\n if (!active) throw new Error(\"Automatic server clients require an active Meeko request context\");\n return active;\n}\n\nexport function createRequestContext<TServices>(request: Request, services: TServices, session: SessionHandle): RequestContext<TServices> {\n return { request, requestId: randomUUID(), signal: request.signal, services, session, state: new Map() };\n}\n\nexport function useRequestState<T>(context: RequestContext, key: string, factory: () => T): T {\n if (!context.state.has(key)) context.state.set(key, factory());\n return context.state.get(key) as T;\n}\n"]}
@@ -0,0 +1,64 @@
1
+ import type { RequestContext } from "./context.js";
2
+ import { type SessionManager } from "./sessions.js";
3
+ /** Valibot schemas implement this public Standard Schema contract. */
4
+ export interface Schema<I = unknown, O = I> {
5
+ readonly "~standard": {
6
+ readonly version: 1;
7
+ readonly validate: (value: unknown) => {
8
+ value: O;
9
+ issues?: undefined;
10
+ } | {
11
+ issues: readonly unknown[];
12
+ } | Promise<{
13
+ value: O;
14
+ issues?: undefined;
15
+ } | {
16
+ issues: readonly unknown[];
17
+ }>;
18
+ readonly types?: {
19
+ readonly input: I;
20
+ readonly output: O;
21
+ } | undefined;
22
+ };
23
+ }
24
+ type Input<S> = S extends Schema<infer I, unknown> ? I : never;
25
+ type Output<S> = S extends Schema<unknown, infer O> ? O : never;
26
+ type OptionalOutput<S> = S extends Schema ? Output<S> : undefined;
27
+ export declare class ApiError extends Error {
28
+ readonly code: string;
29
+ readonly status: number;
30
+ constructor(code: string, status: number, message?: string);
31
+ }
32
+ export interface ApiRouteDefinition<P extends Schema | undefined, Q extends Schema | undefined, B extends Schema | undefined, R extends Schema | undefined, S = unknown> {
33
+ params?: P;
34
+ query?: Q;
35
+ body?: B;
36
+ response?: R;
37
+ auth?: "required" | "optional";
38
+ contentType?: "json" | "response";
39
+ authorize?: (context: RequestContext<S>) => boolean | Promise<boolean>;
40
+ handler: (args: {
41
+ params: OptionalOutput<P>;
42
+ query: OptionalOutput<Q>;
43
+ body: OptionalOutput<B>;
44
+ context: RequestContext<S>;
45
+ }) => Promise<R extends Schema ? Output<R> : Response> | (R extends Schema ? Output<R> : Response);
46
+ }
47
+ export declare function defineApiRoute<P extends Schema | undefined = undefined, Q extends Schema | undefined = undefined, B extends Schema | undefined = undefined, R extends Schema | undefined = undefined, S = unknown>(definition: ApiRouteDefinition<P, Q, B, R, S>): ApiRouteDefinition<P, Q, B, R, S>;
48
+ export interface RpcDefinition<I extends Schema, O extends Schema, S = unknown> {
49
+ input: I;
50
+ output: O;
51
+ auth?: "required" | "optional";
52
+ authorize?: (context: RequestContext<S>) => boolean | Promise<boolean>;
53
+ handler: (args: {
54
+ input: Output<I>;
55
+ context: RequestContext<S>;
56
+ }) => Promise<Input<O> | Output<O>> | Input<O> | Output<O>;
57
+ }
58
+ export declare function defineRpc<I extends Schema, O extends Schema, S = unknown>(definition: RpcDefinition<I, O, S>): RpcDefinition<I, O, S>;
59
+ export declare function executeApiRoute<P extends Schema | undefined, Q extends Schema | undefined, B extends Schema | undefined, R extends Schema | undefined, S>(definition: ApiRouteDefinition<P, Q, B, R, S>, context: RequestContext<S>, sessions: SessionManager, rawParams?: unknown): Promise<Response>;
60
+ export declare function executeRpc<I extends Schema, O extends Schema, S>(definition: RpcDefinition<I, O, S>, rawInput: unknown, context: RequestContext<S>, sessions: SessionManager): Promise<Output<O>>;
61
+ export declare function errorResponse(error: unknown, requestId: string): Response;
62
+ export declare function dispatchApiRoute<P extends Schema | undefined, Q extends Schema | undefined, B extends Schema | undefined, R extends Schema | undefined, S>(definition: ApiRouteDefinition<P, Q, B, R, S>, context: RequestContext<S>, sessions: SessionManager, params?: unknown): Promise<Response>;
63
+ export declare function dispatchRpc<I extends Schema, O extends Schema, S>(definition: RpcDefinition<I, O, S>, context: RequestContext<S>, sessions: SessionManager): Promise<Response>;
64
+ export {};
@@ -0,0 +1,129 @@
1
+ import { serializeJson } from "../server.js";
2
+ import { SessionSecurityError } from "./sessions.js";
3
+ export class ApiError extends Error {
4
+ code;
5
+ status;
6
+ constructor(code, status, message = code) {
7
+ super(message);
8
+ this.code = code;
9
+ this.status = status;
10
+ }
11
+ }
12
+ export function defineApiRoute(definition) {
13
+ return definition;
14
+ }
15
+ export function defineRpc(definition) {
16
+ return definition;
17
+ }
18
+ async function validate(schema, value, label) {
19
+ const result = await schema["~standard"].validate(value);
20
+ if ("issues" in result && result.issues)
21
+ throw new ApiError(`INVALID_${label.toUpperCase()}`, label === "response" || label === "output" ? 500 : 400);
22
+ return result.value;
23
+ }
24
+ async function readJson(request, maxBytes = 1024 * 1024) {
25
+ if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json"))
26
+ throw new ApiError("UNSUPPORTED_MEDIA_TYPE", 415);
27
+ const reader = request.body?.getReader();
28
+ if (!reader)
29
+ throw new ApiError("INVALID_JSON", 400);
30
+ let length = 0;
31
+ const chunks = [];
32
+ try {
33
+ while (true) {
34
+ const next = await reader.read();
35
+ if (next.done)
36
+ break;
37
+ length += next.value.byteLength;
38
+ if (length > maxBytes)
39
+ throw new ApiError("BODY_TOO_LARGE", 413);
40
+ chunks.push(next.value);
41
+ }
42
+ }
43
+ finally {
44
+ reader.releaseLock();
45
+ }
46
+ const bytes = new Uint8Array(length);
47
+ let position = 0;
48
+ for (const chunk of chunks) {
49
+ bytes.set(chunk, position);
50
+ position += chunk.byteLength;
51
+ }
52
+ try {
53
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
54
+ }
55
+ catch {
56
+ throw new ApiError("INVALID_JSON", 400);
57
+ }
58
+ }
59
+ function queryObject(url) {
60
+ const result = Object.create(null);
61
+ for (const [key, value] of url.searchParams) {
62
+ const previous = result[key];
63
+ result[key] = previous === undefined ? value : Array.isArray(previous) ? [...previous, value] : [previous, value];
64
+ }
65
+ return result;
66
+ }
67
+ async function guard(definition, context, sessions, mutating) {
68
+ sessions.assertMutationAllowed(context.request, context.session, mutating);
69
+ if (definition.auth === "required" && !context.session.current)
70
+ throw new ApiError("UNAUTHENTICATED", 401);
71
+ if (definition.authorize && !(await definition.authorize(context)))
72
+ throw new ApiError("FORBIDDEN", 403);
73
+ }
74
+ export async function executeApiRoute(definition, context, sessions, rawParams = {}) {
75
+ await guard(definition, context, sessions);
76
+ const params = definition.params ? await validate(definition.params, rawParams, "params") : undefined;
77
+ const query = definition.query ? await validate(definition.query, queryObject(new URL(context.request.url)), "query") : undefined;
78
+ const body = definition.body ? await validate(definition.body, await readJson(context.request), "body") : undefined;
79
+ const result = await definition.handler({ params: params, query: query, body: body, context });
80
+ if (definition.contentType === "response") {
81
+ if (!(result instanceof Response))
82
+ throw new ApiError("INVALID_RESPONSE", 500);
83
+ return result;
84
+ }
85
+ if (!definition.response)
86
+ throw new ApiError("MISSING_RESPONSE_SCHEMA", 500);
87
+ const output = await validate(definition.response, result, "response");
88
+ return new Response(serializeJson(output), { headers: { "content-type": "application/json; charset=utf-8" } });
89
+ }
90
+ export async function executeRpc(definition, rawInput, context, sessions) {
91
+ await guard(definition, context, sessions, true);
92
+ const input = await validate(definition.input, rawInput, "input");
93
+ const result = await definition.handler({ input, context });
94
+ const output = await validate(definition.output, result, "output");
95
+ serializeJson(output);
96
+ return output;
97
+ }
98
+ export function errorResponse(error, requestId) {
99
+ const known = error instanceof ApiError ? error : error instanceof SessionSecurityError ? new ApiError(error.code, 403) : new ApiError("INTERNAL_ERROR", 500);
100
+ return new Response(serializeJson({ code: known.code, status: known.status, requestId }), { status: known.status, headers: { "content-type": "application/json; charset=utf-8", "x-request-id": requestId } });
101
+ }
102
+ function finish(response, context) {
103
+ response.headers.set("x-request-id", context.requestId);
104
+ if (context.session.cookieHeader)
105
+ response.headers.append("set-cookie", context.session.cookieHeader);
106
+ if (context.session.current)
107
+ response.headers.set("x-meeko-csrf-token", context.session.current.csrfToken);
108
+ return response;
109
+ }
110
+ export async function dispatchApiRoute(definition, context, sessions, params = {}) {
111
+ try {
112
+ return finish(await executeApiRoute(definition, context, sessions, params), context);
113
+ }
114
+ catch (error) {
115
+ return finish(errorResponse(error, context.requestId), context);
116
+ }
117
+ }
118
+ export async function dispatchRpc(definition, context, sessions) {
119
+ try {
120
+ if (context.request.method !== "POST")
121
+ throw new ApiError("METHOD_NOT_ALLOWED", 405);
122
+ const output = await executeRpc(definition, await readJson(context.request), context, sessions);
123
+ return finish(new Response(serializeJson(output), { headers: { "content-type": "application/json; charset=utf-8" } }), context);
124
+ }
125
+ catch (error) {
126
+ return finish(errorResponse(error, context.requestId), context);
127
+ }
128
+ }
129
+ //# sourceMappingURL=contracts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contracts.js","sourceRoot":"","sources":["../../src/framework-server/contracts.ts"],"names":[],"mappings":"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","sourcesContent":["import { serializeJson } from \"../server.js\";\nimport type { RequestContext } from \"./context.js\";\nimport { SessionSecurityError, type SessionManager } from \"./sessions.js\";\n\n/** Valibot schemas implement this public Standard Schema contract. */\nexport interface Schema<I = unknown, O = I> {\n readonly \"~standard\": {\n readonly version: 1;\n readonly validate: (value: unknown) => { value: O; issues?: undefined } | { issues: readonly unknown[] } | Promise<{ value: O; issues?: undefined } | { issues: readonly unknown[] }>;\n readonly types?: { readonly input: I; readonly output: O } | undefined;\n };\n}\n\ntype Input<S> = S extends Schema<infer I, unknown> ? I : never;\ntype Output<S> = S extends Schema<unknown, infer O> ? O : never;\ntype OptionalOutput<S> = S extends Schema ? Output<S> : undefined;\n\nexport class ApiError extends Error {\n constructor(public readonly code: string, public readonly status: number, message = code) {\n super(message);\n }\n}\n\nexport interface ApiRouteDefinition<P extends Schema | undefined, Q extends Schema | undefined, B extends Schema | undefined, R extends Schema | undefined, S = unknown> {\n params?: P;\n query?: Q;\n body?: B;\n response?: R;\n auth?: \"required\" | \"optional\";\n contentType?: \"json\" | \"response\";\n authorize?: (context: RequestContext<S>) => boolean | Promise<boolean>;\n handler: (args: { params: OptionalOutput<P>; query: OptionalOutput<Q>; body: OptionalOutput<B>; context: RequestContext<S> }) => Promise<R extends Schema ? Output<R> : Response> | (R extends Schema ? Output<R> : Response);\n}\n\nexport function defineApiRoute<P extends Schema | undefined = undefined, Q extends Schema | undefined = undefined, B extends Schema | undefined = undefined, R extends Schema | undefined = undefined, S = unknown>(definition: ApiRouteDefinition<P, Q, B, R, S>): ApiRouteDefinition<P, Q, B, R, S> {\n return definition;\n}\n\nexport interface RpcDefinition<I extends Schema, O extends Schema, S = unknown> {\n input: I;\n output: O;\n auth?: \"required\" | \"optional\";\n authorize?: (context: RequestContext<S>) => boolean | Promise<boolean>;\n handler: (args: { input: Output<I>; context: RequestContext<S> }) => Promise<Input<O> | Output<O>> | Input<O> | Output<O>;\n}\n\nexport function defineRpc<I extends Schema, O extends Schema, S = unknown>(definition: RpcDefinition<I, O, S>): RpcDefinition<I, O, S> {\n return definition;\n}\n\nasync function validate<S extends Schema>(schema: S, value: unknown, label: string): Promise<Output<S>> {\n const result = await schema[\"~standard\"].validate(value);\n if (\"issues\" in result && result.issues) throw new ApiError(`INVALID_${label.toUpperCase()}`, label === \"response\" || label === \"output\" ? 500 : 400);\n return (result as { value: Output<S> }).value;\n}\n\nasync function readJson(request: Request, maxBytes = 1024 * 1024): Promise<unknown> {\n if (!request.headers.get(\"content-type\")?.toLowerCase().startsWith(\"application/json\")) throw new ApiError(\"UNSUPPORTED_MEDIA_TYPE\", 415);\n const reader = request.body?.getReader();\n if (!reader) throw new ApiError(\"INVALID_JSON\", 400);\n let length = 0;\n const chunks: Uint8Array[] = [];\n try {\n while (true) {\n const next = await reader.read();\n if (next.done) break;\n length += next.value.byteLength;\n if (length > maxBytes) throw new ApiError(\"BODY_TOO_LARGE\", 413);\n chunks.push(next.value);\n }\n } finally { reader.releaseLock(); }\n const bytes = new Uint8Array(length);\n let position = 0;\n for (const chunk of chunks) { bytes.set(chunk, position); position += chunk.byteLength; }\n try { return JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes)) as unknown; }\n catch { throw new ApiError(\"INVALID_JSON\", 400); }\n}\n\nfunction queryObject(url: URL): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = Object.create(null);\n for (const [key, value] of url.searchParams) {\n const previous = result[key];\n result[key] = previous === undefined ? value : Array.isArray(previous) ? [...previous, value] : [previous, value];\n }\n return result;\n}\n\nasync function guard<S>(definition: { auth?: \"required\" | \"optional\"; authorize?: (context: RequestContext<S>) => boolean | Promise<boolean> }, context: RequestContext<S>, sessions: SessionManager, mutating?: boolean): Promise<void> {\n sessions.assertMutationAllowed(context.request, context.session, mutating);\n if (definition.auth === \"required\" && !context.session.current) throw new ApiError(\"UNAUTHENTICATED\", 401);\n if (definition.authorize && !(await definition.authorize(context))) throw new ApiError(\"FORBIDDEN\", 403);\n}\n\nexport async function executeApiRoute<P extends Schema | undefined, Q extends Schema | undefined, B extends Schema | undefined, R extends Schema | undefined, S>(definition: ApiRouteDefinition<P, Q, B, R, S>, context: RequestContext<S>, sessions: SessionManager, rawParams: unknown = {}): Promise<Response> {\n await guard(definition, context, sessions);\n const params = definition.params ? await validate(definition.params, rawParams, \"params\") : undefined;\n const query = definition.query ? await validate(definition.query, queryObject(new URL(context.request.url)), \"query\") : undefined;\n const body = definition.body ? await validate(definition.body, await readJson(context.request), \"body\") : undefined;\n const result = await definition.handler({ params: params as OptionalOutput<P>, query: query as OptionalOutput<Q>, body: body as OptionalOutput<B>, context });\n if (definition.contentType === \"response\") {\n if (!(result instanceof Response)) throw new ApiError(\"INVALID_RESPONSE\", 500);\n return result;\n }\n if (!definition.response) throw new ApiError(\"MISSING_RESPONSE_SCHEMA\", 500);\n const output = await validate(definition.response, result, \"response\");\n return new Response(serializeJson(output), { headers: { \"content-type\": \"application/json; charset=utf-8\" } });\n}\n\nexport async function executeRpc<I extends Schema, O extends Schema, S>(definition: RpcDefinition<I, O, S>, rawInput: unknown, context: RequestContext<S>, sessions: SessionManager): Promise<Output<O>> {\n await guard(definition, context, sessions, true);\n const input = await validate(definition.input, rawInput, \"input\");\n const result = await definition.handler({ input, context });\n const output = await validate(definition.output, result, \"output\");\n serializeJson(output);\n return output;\n}\n\nexport function errorResponse(error: unknown, requestId: string): Response {\n const known = error instanceof ApiError ? error : error instanceof SessionSecurityError ? new ApiError(error.code, 403) : new ApiError(\"INTERNAL_ERROR\", 500);\n return new Response(serializeJson({ code: known.code, status: known.status, requestId }), { status: known.status, headers: { \"content-type\": \"application/json; charset=utf-8\", \"x-request-id\": requestId } });\n}\n\nfunction finish(response: Response, context: RequestContext): Response {\n response.headers.set(\"x-request-id\", context.requestId);\n if (context.session.cookieHeader) response.headers.append(\"set-cookie\", context.session.cookieHeader);\n if (context.session.current) response.headers.set(\"x-meeko-csrf-token\", context.session.current.csrfToken);\n return response;\n}\n\nexport async function dispatchApiRoute<P extends Schema | undefined, Q extends Schema | undefined, B extends Schema | undefined, R extends Schema | undefined, S>(definition: ApiRouteDefinition<P, Q, B, R, S>, context: RequestContext<S>, sessions: SessionManager, params: unknown = {}): Promise<Response> {\n try { return finish(await executeApiRoute(definition, context, sessions, params), context); }\n catch (error) { return finish(errorResponse(error, context.requestId), context); }\n}\n\nexport async function dispatchRpc<I extends Schema, O extends Schema, S>(definition: RpcDefinition<I, O, S>, context: RequestContext<S>, sessions: SessionManager): Promise<Response> {\n try {\n if (context.request.method !== \"POST\") throw new ApiError(\"METHOD_NOT_ALLOWED\", 405);\n const output = await executeRpc(definition, await readJson(context.request), context, sessions);\n return finish(new Response(serializeJson(output), { headers: { \"content-type\": \"application/json; charset=utf-8\" } }), context);\n } catch (error) { return finish(errorResponse(error, context.requestId), context); }\n}\n"]}
@@ -0,0 +1,40 @@
1
+ import type { ServerFetch } from "./context.js";
2
+ import type { SessionManager } from "./sessions.js";
3
+ export type ApiMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
4
+ export interface ApiEndpoint {
5
+ readonly kind: "api";
6
+ readonly file: string;
7
+ readonly pathname: string;
8
+ readonly method: ApiMethod;
9
+ readonly clientKey: string;
10
+ }
11
+ export interface RpcEndpoint {
12
+ readonly kind: "rpc";
13
+ readonly file: string;
14
+ readonly pathname: string;
15
+ readonly id: string;
16
+ }
17
+ export interface SocketEndpoint {
18
+ readonly kind: "socket";
19
+ readonly file: string;
20
+ readonly name: string;
21
+ readonly pathname: string;
22
+ }
23
+ export interface ServerManifest {
24
+ readonly api: readonly ApiEndpoint[];
25
+ readonly rpc: readonly RpcEndpoint[];
26
+ readonly socket?: readonly SocketEndpoint[];
27
+ }
28
+ /** Static discovery is shared by development and production; no handler is executed here. */
29
+ export declare function discoverServerManifest(projectRoot: string): Promise<ServerManifest>;
30
+ /** Loads server-only channel definitions; callers pass the result to createSocketHub. */
31
+ export declare function loadSocketChannels(manifest: Pick<ServerManifest, "socket">, load?: (file: string) => Promise<unknown>): Promise<Readonly<Record<string, unknown>>>;
32
+ export interface ServerDispatcherOptions<S = unknown> {
33
+ manifest: ServerManifest;
34
+ sessions: SessionManager;
35
+ services: S | ((request: Request) => S | Promise<S>);
36
+ load?: (file: string) => Promise<unknown>;
37
+ }
38
+ export declare function createServerDispatcher<S>(options: ServerDispatcherOptions<S>): (request: Request) => Promise<Response>;
39
+ /** SSR dispatch reuses the original session, services, state and request ID. */
40
+ export declare function createLocalServerFetch<S>(options: Pick<ServerDispatcherOptions<S>, "manifest" | "sessions" | "load">): ServerFetch;
@@ -0,0 +1,252 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
3
+ import { join, relative, resolve, sep } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { createRequestContext } from "./context.js";
6
+ import { ApiError, dispatchApiRoute, dispatchRpc, errorResponse } from "./contracts.js";
7
+ function parts(pathname) {
8
+ return pathname.split("/").filter(Boolean).map((part, index, all) => {
9
+ if (/^\[\.\.\.[A-Za-z_$][\w$]*\]$/.test(part)) {
10
+ if (index !== all.length - 1)
11
+ throw new Error(`Catch-all must be last: ${pathname}`);
12
+ return { kind: "rest", value: part.slice(4, -1) };
13
+ }
14
+ if (/^\[[A-Za-z_$][\w$]*\]$/.test(part))
15
+ return { kind: "param", value: part.slice(1, -1) };
16
+ if (!/^[A-Za-z0-9_-]+$/.test(part))
17
+ throw new Error(`Invalid server route segment: ${part}`);
18
+ return { kind: "static", value: part };
19
+ });
20
+ }
21
+ function collisionKey(pathname) {
22
+ return parts(pathname).map((part) => part.kind === "static" ? part.value : part.kind === "param" ? "[]" : "[...]").join("/");
23
+ }
24
+ function clientKey(pathname) {
25
+ return parts(pathname).map((part) => part.kind === "static" ? part.value : part.kind === "param" ? `by${part.value[0].toUpperCase()}${part.value.slice(1)}` : `by${part.value[0].toUpperCase()}${part.value.slice(1)}`).join(".");
26
+ }
27
+ async function files(directory) {
28
+ let entries;
29
+ try {
30
+ entries = await readdir(directory, { withFileTypes: true });
31
+ }
32
+ catch (error) {
33
+ if (error.code === "ENOENT")
34
+ return [];
35
+ throw error;
36
+ }
37
+ const found = [];
38
+ for (const entry of entries) {
39
+ if (entry.name.startsWith(".") || entry.name === "node_modules")
40
+ continue;
41
+ const file = join(directory, entry.name);
42
+ if (entry.isDirectory())
43
+ found.push(...await files(file));
44
+ else if (entry.isFile() && /\.(?:ts|js|mts|mjs)$/.test(entry.name) && !/\.(?:test|spec|d)\.(?:ts|js|mts|mjs)$/.test(entry.name))
45
+ found.push(file);
46
+ }
47
+ return found.sort();
48
+ }
49
+ function rel(root, file) {
50
+ return relative(root, file).split(sep).join("/").replace(/\.(?:ts|js|mts|mjs)$/, "");
51
+ }
52
+ function apiEndpoint(directory, file) {
53
+ const path = rel(directory, file);
54
+ const match = /^(.*)\.(get|post|put|patch|delete)$/.exec(path);
55
+ if (!match)
56
+ throw new Error(`API handler requires a method suffix: ${file}`);
57
+ const route = match[1].split("/");
58
+ if (route.at(-1) === "index")
59
+ route.pop();
60
+ const pathname = `/api${route.length ? `/${route.join("/")}` : ""}`;
61
+ parts(pathname);
62
+ return { kind: "api", file, pathname, method: match[2].toUpperCase(), clientKey: clientKey(pathname.slice(4)) };
63
+ }
64
+ function rpcEndpoint(directory, file) {
65
+ const path = rel(directory, file);
66
+ const route = path.split("/");
67
+ if (route.some((part) => !/^[A-Za-z_$][\w$]*$/.test(part)))
68
+ throw new Error(`Invalid RPC procedure name: ${file}`);
69
+ const id = route.join(".");
70
+ return { kind: "rpc", file, id, pathname: `/_meeko/rpc/${route.join("/")}` };
71
+ }
72
+ /** Static discovery is shared by development and production; no handler is executed here. */
73
+ export async function discoverServerManifest(projectRoot) {
74
+ const root = resolve(projectRoot);
75
+ const apiRoot = join(root, "server", "api");
76
+ const rpcRoot = join(root, "server", "rpc");
77
+ const socketRoot = join(root, "server", "socket");
78
+ const api = (await files(apiRoot)).map((file) => apiEndpoint(apiRoot, file));
79
+ const rpc = (await files(rpcRoot)).map((file) => rpcEndpoint(rpcRoot, file));
80
+ const socket = (await files(socketRoot)).map((file) => {
81
+ const name = rel(socketRoot, file);
82
+ if (!/^[A-Za-z_$][\w$-]*$/.test(name))
83
+ throw new Error(`Invalid Socket channel name in ${file}; use a single named file`);
84
+ return { kind: "socket", file, name, pathname: `/_meeko/socket/${name}` };
85
+ });
86
+ const socketNames = new Map();
87
+ for (const endpoint of socket) {
88
+ const previous = socketNames.get(endpoint.name);
89
+ if (previous)
90
+ throw new Error(`Socket channel collision: ${previous} and ${endpoint.file} both resolve to ${endpoint.name}`);
91
+ socketNames.set(endpoint.name, endpoint.file);
92
+ }
93
+ const paths = new Map();
94
+ const keys = new Map();
95
+ for (const endpoint of api) {
96
+ const pathKey = `${endpoint.method} ${collisionKey(endpoint.pathname)}`;
97
+ const previous = paths.get(pathKey);
98
+ if (previous)
99
+ throw new Error(`API route collision: ${previous} and ${endpoint.file} both resolve to ${pathKey}`);
100
+ paths.set(pathKey, endpoint.file);
101
+ const key = `${endpoint.method} ${endpoint.clientKey}`;
102
+ const keyPrevious = keys.get(key);
103
+ if (keyPrevious)
104
+ throw new Error(`API client name collision: ${keyPrevious} and ${endpoint.file} both generate ${key}`);
105
+ keys.set(key, endpoint.file);
106
+ }
107
+ const ids = new Map();
108
+ for (const endpoint of rpc) {
109
+ const previous = ids.get(endpoint.id);
110
+ if (previous)
111
+ throw new Error(`RPC collision: ${previous} and ${endpoint.file} both resolve to ${endpoint.id}`);
112
+ ids.set(endpoint.id, endpoint.file);
113
+ }
114
+ return Object.freeze({ api: Object.freeze(api), rpc: Object.freeze(rpc), socket: Object.freeze(socket) });
115
+ }
116
+ /** Loads server-only channel definitions; callers pass the result to createSocketHub. */
117
+ export async function loadSocketChannels(manifest, load = async (file) => import(pathToFileURL(file).href)) {
118
+ const channels = Object.create(null);
119
+ for (const endpoint of manifest.socket ?? []) {
120
+ const module = await load(endpoint.file);
121
+ if (!module || typeof module !== "object" || !("default" in module))
122
+ throw new Error(`${endpoint.file}: Socket channel requires a default export`);
123
+ channels[endpoint.name] = module.default;
124
+ }
125
+ return channels;
126
+ }
127
+ function decode(part) {
128
+ try {
129
+ const value = decodeURIComponent(part);
130
+ return value && value !== "." && value !== ".." && !value.includes("/") && !value.includes("\\") ? value : undefined;
131
+ }
132
+ catch {
133
+ return undefined;
134
+ }
135
+ }
136
+ function match(pathname, pattern) {
137
+ const raw = pathname.split("/").filter(Boolean);
138
+ const path = raw.map(decode);
139
+ if (path.some((part) => part === undefined))
140
+ return undefined;
141
+ const patternParts = parts(pattern);
142
+ const params = Object.create(null);
143
+ let index = 0;
144
+ for (const part of patternParts) {
145
+ if (part.kind === "rest") {
146
+ if (index >= path.length)
147
+ return undefined;
148
+ params[part.value] = path.slice(index).join("/");
149
+ index = path.length;
150
+ break;
151
+ }
152
+ const value = path[index++];
153
+ if (value === undefined || (part.kind === "static" && part.value !== value))
154
+ return undefined;
155
+ if (part.kind === "param")
156
+ params[part.value] = value;
157
+ }
158
+ return index === path.length ? params : undefined;
159
+ }
160
+ function rank(endpoint) {
161
+ return parts(endpoint.pathname).map((part) => part.kind === "static" ? 3 : part.kind === "param" ? 2 : 1);
162
+ }
163
+ function compareApi(a, b) {
164
+ const left = rank(a);
165
+ const right = rank(b);
166
+ for (let index = 0; index < Math.max(left.length, right.length); index++) {
167
+ const score = (right[index] ?? 0) - (left[index] ?? 0);
168
+ if (score)
169
+ return score;
170
+ }
171
+ return a.pathname.localeCompare(b.pathname);
172
+ }
173
+ function exportedDefinition(module) {
174
+ if (module && typeof module === "object" && "default" in module)
175
+ return module.default;
176
+ throw new ApiError("INVALID_HANDLER", 500);
177
+ }
178
+ /** Dispatches API and RPC through the same request context and security pipeline. */
179
+ function requestDispatcher(options) {
180
+ const api = [...options.manifest.api].sort(compareApi);
181
+ const load = options.load ?? (async (file) => import(pathToFileURL(file).href));
182
+ return async (context) => {
183
+ const request = context.request;
184
+ try {
185
+ const pathname = new URL(request.url).pathname;
186
+ if (pathname.startsWith("/_meeko/rpc/")) {
187
+ const endpoint = options.manifest.rpc.find((record) => record.pathname === pathname);
188
+ if (!endpoint)
189
+ throw new ApiError("NOT_FOUND", 404);
190
+ if (request.method !== "POST")
191
+ throw new ApiError("METHOD_NOT_ALLOWED", 405);
192
+ const definition = exportedDefinition(await load(endpoint.file));
193
+ return dispatchRpc(definition, context, options.sessions);
194
+ }
195
+ let allowed = [];
196
+ for (const endpoint of api) {
197
+ const params = match(pathname, endpoint.pathname);
198
+ if (!params)
199
+ continue;
200
+ if (request.method !== endpoint.method) {
201
+ allowed.push(endpoint.method);
202
+ continue;
203
+ }
204
+ const definition = exportedDefinition(await load(endpoint.file));
205
+ return dispatchApiRoute(definition, context, options.sessions, params);
206
+ }
207
+ if (allowed.length) {
208
+ const response = errorResponse(new ApiError("METHOD_NOT_ALLOWED", 405), context.requestId);
209
+ response.headers.set("allow", [...new Set(allowed)].join(", "));
210
+ return response;
211
+ }
212
+ throw new ApiError("NOT_FOUND", 404);
213
+ }
214
+ catch (error) {
215
+ return errorResponse(error, context.requestId);
216
+ }
217
+ };
218
+ }
219
+ export function createServerDispatcher(options) {
220
+ const dispatch = requestDispatcher(options);
221
+ return async (request) => {
222
+ let requestId = randomUUID();
223
+ try {
224
+ const session = await options.sessions.load(request);
225
+ const services = typeof options.services === "function" ? await options.services(request) : options.services;
226
+ const context = createRequestContext(request, services, session);
227
+ requestId = context.requestId;
228
+ return dispatch(context);
229
+ }
230
+ catch (error) {
231
+ return errorResponse(error, requestId);
232
+ }
233
+ };
234
+ }
235
+ /** SSR dispatch reuses the original session, services, state and request ID. */
236
+ export function createLocalServerFetch(options) {
237
+ const dispatch = requestDispatcher(options);
238
+ return async (request, active) => {
239
+ const source = active.request;
240
+ if (new URL(request.url).origin !== new URL(source.url).origin) {
241
+ return errorResponse(new ApiError("INVALID_ORIGIN", 403), active.requestId);
242
+ }
243
+ for (const name of ["cookie", "authorization"]) {
244
+ if (request.headers.get(name) !== source.headers.get(name)) {
245
+ return errorResponse(new ApiError("INVALID_CREDENTIALS", 403), active.requestId);
246
+ }
247
+ }
248
+ const context = { ...active, request, signal: request.signal };
249
+ return dispatch(context);
250
+ };
251
+ }
252
+ //# sourceMappingURL=discovery.js.map