@surfjs/next 0.2.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 ADDED
@@ -0,0 +1,85 @@
1
+ # @surfjs/next
2
+
3
+ Next.js adapter for [Surf.js](https://surf.codes) — supports both **App Router** (route handlers) and **Pages Router** (API routes).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @surfjs/next @surfjs/core
9
+ ```
10
+
11
+ ## App Router (Recommended)
12
+
13
+ Create a catch-all route handler at `app/api/surf/[...slug]/route.ts`:
14
+
15
+ ```ts
16
+ import { createSurf } from '@surfjs/core';
17
+ import { createSurfRouteHandler } from '@surfjs/next';
18
+
19
+ const surf = createSurf({
20
+ name: 'my-app',
21
+ commands: {
22
+ hello: {
23
+ description: 'Say hello',
24
+ params: { name: { type: 'string', required: true } },
25
+ run: ({ name }) => ({ message: `Hello, ${name}!` }),
26
+ },
27
+ },
28
+ });
29
+
30
+ export const { GET, POST } = createSurfRouteHandler(surf);
31
+ ```
32
+
33
+ The handler serves:
34
+ - `GET /api/surf/.well-known/surf.json` — Surf manifest
35
+ - `POST /api/surf/surf/execute` — Execute commands
36
+ - `POST /api/surf/surf/pipeline` — Execute pipelines
37
+ - `POST /api/surf/surf/session/start` — Start sessions
38
+ - `POST /api/surf/surf/session/end` — End sessions
39
+
40
+ ### Edge Runtime
41
+
42
+ The App Router handler is fully edge-compatible — no Node.js APIs are used. Add the edge runtime directive if desired:
43
+
44
+ ```ts
45
+ export const runtime = 'edge';
46
+ ```
47
+
48
+ ## Pages Router
49
+
50
+ Create a catch-all API route at `pages/api/surf/[...slug].ts`:
51
+
52
+ ```ts
53
+ import { createSurf } from '@surfjs/core';
54
+ import { createSurfApiHandler } from '@surfjs/next/pages';
55
+
56
+ const surf = createSurf({
57
+ name: 'my-app',
58
+ commands: {
59
+ hello: {
60
+ description: 'Say hello',
61
+ params: { name: { type: 'string', required: true } },
62
+ run: ({ name }) => ({ message: `Hello, ${name}!` }),
63
+ },
64
+ },
65
+ });
66
+
67
+ export default createSurfApiHandler(surf);
68
+ ```
69
+
70
+ ## Features
71
+
72
+ - ✅ App Router (route handlers) with edge runtime support
73
+ - ✅ Pages Router (API routes) with Node.js runtime
74
+ - ✅ SSE streaming for both routers
75
+ - ✅ Session management
76
+ - ✅ Pipeline execution
77
+ - ✅ Bearer token auth extraction
78
+ - ✅ Client IP extraction (`x-forwarded-for`, `x-real-ip`)
79
+ - ✅ ETag caching for manifest
80
+ - ✅ CORS headers
81
+ - ✅ Consistent error code → HTTP status mapping
82
+
83
+ ## License
84
+
85
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,243 @@
1
+ 'use strict';
2
+
3
+ var core = require('@surfjs/core');
4
+
5
+ // src/index.ts
6
+
7
+ // src/shared.ts
8
+ function getErrorStatus(code) {
9
+ switch (code) {
10
+ case "UNKNOWN_COMMAND":
11
+ return 404;
12
+ case "INVALID_PARAMS":
13
+ return 400;
14
+ case "AUTH_REQUIRED":
15
+ return 401;
16
+ case "AUTH_FAILED":
17
+ return 403;
18
+ case "SESSION_EXPIRED":
19
+ return 410;
20
+ case "RATE_LIMITED":
21
+ return 429;
22
+ case "NOT_SUPPORTED":
23
+ return 501;
24
+ default:
25
+ return 500;
26
+ }
27
+ }
28
+ function extractAuth(authHeader) {
29
+ if (!authHeader) return void 0;
30
+ return authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
31
+ }
32
+ function extractIp(forwardedFor, realIp) {
33
+ if (forwardedFor) return forwardedFor.split(",")[0]?.trim();
34
+ return realIp ?? void 0;
35
+ }
36
+ var CORS_HEADERS = {
37
+ "Access-Control-Allow-Origin": "*"
38
+ };
39
+
40
+ // src/index.ts
41
+ function createSurfRouteHandler(surf, options = {}) {
42
+ const registry = surf.commands;
43
+ const sessions = surf.sessions;
44
+ const basePath = options.basePath ?? "/api/surf";
45
+ function getPathname(request) {
46
+ const req = request;
47
+ if (req.nextUrl) return req.nextUrl.pathname;
48
+ return new URL(request.url).pathname;
49
+ }
50
+ function resolveRoute(request, slug) {
51
+ if (slug && slug.length > 0) {
52
+ return "/" + slug.join("/");
53
+ }
54
+ const pathname = getPathname(request);
55
+ const stripped = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
56
+ return stripped || "/";
57
+ }
58
+ function jsonResponse(body, status, extraHeaders) {
59
+ return new Response(JSON.stringify(body), {
60
+ status,
61
+ headers: {
62
+ "Content-Type": "application/json",
63
+ ...CORS_HEADERS,
64
+ ...extraHeaders
65
+ }
66
+ });
67
+ }
68
+ async function GET(request, context) {
69
+ const { slug } = await context.params;
70
+ const route = resolveRoute(request, slug);
71
+ if (route === "/.well-known/surf.json" || route === "/") {
72
+ const manifestData = surf.manifest();
73
+ const etag = `"${manifestData.checksum}"`;
74
+ if (request.headers.get("if-none-match") === etag) {
75
+ return new Response(null, {
76
+ status: 304,
77
+ headers: { "ETag": etag, "Cache-Control": "public, max-age=300", ...CORS_HEADERS }
78
+ });
79
+ }
80
+ return jsonResponse(manifestData, 200, {
81
+ "ETag": etag,
82
+ "Cache-Control": "public, max-age=300"
83
+ });
84
+ }
85
+ return jsonResponse(
86
+ { ok: false, error: { code: "NOT_FOUND", message: `Unknown route: GET ${route}` } },
87
+ 404
88
+ );
89
+ }
90
+ async function POST(request, context) {
91
+ const { slug } = await context.params;
92
+ const route = resolveRoute(request, slug);
93
+ const auth = extractAuth(request.headers.get("authorization"));
94
+ const ip = extractIp(
95
+ request.headers.get("x-forwarded-for"),
96
+ request.headers.get("x-real-ip")
97
+ );
98
+ if (route === "/surf/execute" || route === "/execute") {
99
+ let body;
100
+ try {
101
+ body = await request.json();
102
+ } catch {
103
+ return jsonResponse(
104
+ { ok: false, error: { code: "INVALID_PARAMS", message: "Invalid JSON body" } },
105
+ 400
106
+ );
107
+ }
108
+ if (!body?.command || typeof body.command !== "string") {
109
+ return jsonResponse(
110
+ { ok: false, error: { code: "INVALID_PARAMS", message: "Missing command field" } },
111
+ 400
112
+ );
113
+ }
114
+ let sessionState;
115
+ if (body.sessionId) {
116
+ const session = await sessions.get(body.sessionId);
117
+ if (session) sessionState = session.state;
118
+ }
119
+ const command = registry.get(body.command);
120
+ const wantsStream = body.stream === true && command?.stream === true;
121
+ if (wantsStream) {
122
+ const stream = new ReadableStream({
123
+ async start(controller) {
124
+ const encoder = new TextEncoder();
125
+ const write = (data) => {
126
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
127
+
128
+ `));
129
+ };
130
+ const sseContext = {
131
+ sessionId: body.sessionId,
132
+ auth,
133
+ ip,
134
+ state: sessionState,
135
+ requestId: body.requestId,
136
+ emit: (data) => {
137
+ write({ type: "chunk", data });
138
+ }
139
+ };
140
+ try {
141
+ const response2 = await registry.execute(body.command, body.params, sseContext);
142
+ if (response2.ok) {
143
+ write({ type: "done", result: response2.result });
144
+ } else {
145
+ write({ type: "error", error: { code: response2.error.code, message: response2.error.message } });
146
+ }
147
+ } catch (e) {
148
+ write({
149
+ type: "error",
150
+ error: {
151
+ code: "INTERNAL_ERROR",
152
+ message: e instanceof Error ? e.message : "Unknown error"
153
+ }
154
+ });
155
+ } finally {
156
+ controller.close();
157
+ }
158
+ }
159
+ });
160
+ return new Response(stream, {
161
+ status: 200,
162
+ headers: {
163
+ "Content-Type": "text/event-stream",
164
+ "Cache-Control": "no-cache",
165
+ "Connection": "keep-alive",
166
+ ...CORS_HEADERS
167
+ }
168
+ });
169
+ }
170
+ const response = await registry.execute(body.command, body.params, {
171
+ sessionId: body.sessionId,
172
+ auth,
173
+ ip,
174
+ state: sessionState,
175
+ requestId: body.requestId
176
+ });
177
+ if (body.sessionId && response.ok && response.state) {
178
+ await sessions.update(body.sessionId, response.state);
179
+ }
180
+ const statusCode = response.ok ? 200 : getErrorStatus(response.error.code);
181
+ const headers = {};
182
+ if (!response.ok && response.error.code === "RATE_LIMITED") {
183
+ const retryMs = response.error.details?.["retryAfterMs"] ?? 0;
184
+ headers["Retry-After"] = String(Math.ceil(retryMs / 1e3));
185
+ }
186
+ return jsonResponse(response, statusCode, headers);
187
+ }
188
+ if (route === "/surf/pipeline" || route === "/pipeline") {
189
+ let body;
190
+ try {
191
+ body = await request.json();
192
+ } catch {
193
+ return jsonResponse(
194
+ { ok: false, error: { code: "INVALID_PARAMS", message: "Invalid JSON body" } },
195
+ 400
196
+ );
197
+ }
198
+ try {
199
+ const result = await core.executePipeline(
200
+ body,
201
+ registry,
202
+ sessions,
203
+ auth
204
+ );
205
+ return jsonResponse(result, 200);
206
+ } catch (e) {
207
+ return jsonResponse(
208
+ {
209
+ ok: false,
210
+ error: { code: "INTERNAL_ERROR", message: e instanceof Error ? e.message : "Unknown error" }
211
+ },
212
+ 500
213
+ );
214
+ }
215
+ }
216
+ if (route === "/surf/session/start" || route === "/session/start") {
217
+ const session = await sessions.create();
218
+ return jsonResponse({ ok: true, sessionId: session.id }, 200);
219
+ }
220
+ if (route === "/surf/session/end" || route === "/session/end") {
221
+ try {
222
+ const body = await request.json();
223
+ if (body?.sessionId) {
224
+ await sessions.destroy(body.sessionId);
225
+ }
226
+ } catch {
227
+ }
228
+ return jsonResponse({ ok: true }, 200);
229
+ }
230
+ return jsonResponse(
231
+ { ok: false, error: { code: "NOT_FOUND", message: `Unknown route: POST ${route}` } },
232
+ 404
233
+ );
234
+ }
235
+ return { GET, POST };
236
+ }
237
+
238
+ exports.createSurfRouteHandler = createSurfRouteHandler;
239
+ exports.extractAuth = extractAuth;
240
+ exports.extractIp = extractIp;
241
+ exports.getErrorStatus = getErrorStatus;
242
+ //# sourceMappingURL=index.cjs.map
243
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared.ts","../src/index.ts"],"names":["response","executePipeline"],"mappings":";;;;;;;AAMO,SAAS,eAAe,IAAA,EAAsB;AACnD,EAAA,QAAQ,IAAA;AAAM,IACZ,KAAK,iBAAA;AAAmB,MAAA,OAAO,GAAA;AAAA,IAC/B,KAAK,gBAAA;AAAkB,MAAA,OAAO,GAAA;AAAA,IAC9B,KAAK,eAAA;AAAiB,MAAA,OAAO,GAAA;AAAA,IAC7B,KAAK,aAAA;AAAe,MAAA,OAAO,GAAA;AAAA,IAC3B,KAAK,iBAAA;AAAmB,MAAA,OAAO,GAAA;AAAA,IAC/B,KAAK,cAAA;AAAgB,MAAA,OAAO,GAAA;AAAA,IAC5B,KAAK,eAAA;AAAiB,MAAA,OAAO,GAAA;AAAA,IAC7B;AAAS,MAAA,OAAO,GAAA;AAAA;AAEpB;AAQO,SAAS,YAAY,UAAA,EAA2D;AACrF,EAAA,IAAI,CAAC,YAAY,OAAO,MAAA;AACxB,EAAA,OAAO,WAAW,UAAA,CAAW,SAAS,IAAI,UAAA,CAAW,KAAA,CAAM,CAAC,CAAA,GAAI,UAAA;AAClE;AASO,SAAS,SAAA,CACd,cACA,MAAA,EACoB;AACpB,EAAA,IAAI,YAAA,SAAqB,YAAA,CAAa,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,GAAG,IAAA,EAAK;AAC1D,EAAA,OAAO,MAAA,IAAU,MAAA;AACnB;AAKO,IAAM,YAAA,GAAuC;AAAA,EAClD,6BAAA,EAA+B;AACjC,CAAA;;;ACDO,SAAS,sBAAA,CACd,IAAA,EACA,OAAA,GAAiC,EAAC,EAIlC;AACA,EAAA,MAAM,WAAW,IAAA,CAAK,QAAA;AACtB,EAAA,MAAM,WAAW,IAAA,CAAK,QAAA;AACtB,EAAA,MAAM,QAAA,GAAW,QAAQ,QAAA,IAAY,WAAA;AAErC,EAAA,SAAS,YAAY,OAAA,EAA0B;AAC7C,IAAA,MAAM,GAAA,GAAM,OAAA;AACZ,IAAA,IAAI,GAAA,CAAI,OAAA,EAAS,OAAO,GAAA,CAAI,OAAA,CAAQ,QAAA;AACpC,IAAA,OAAO,IAAI,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA,CAAE,QAAA;AAAA,EAC9B;AAEA,EAAA,SAAS,YAAA,CAAa,SAAkB,IAAA,EAAoC;AAC1E,IAAA,IAAI,IAAA,IAAQ,IAAA,CAAK,MAAA,GAAS,CAAA,EAAG;AAC3B,MAAA,OAAO,GAAA,GAAM,IAAA,CAAK,IAAA,CAAK,GAAG,CAAA;AAAA,IAC5B;AAEA,IAAA,MAAM,QAAA,GAAW,YAAY,OAAO,CAAA;AACpC,IAAA,MAAM,QAAA,GAAW,SAAS,UAAA,CAAW,QAAQ,IACzC,QAAA,CAAS,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,GAC9B,QAAA;AACJ,IAAA,OAAO,QAAA,IAAY,GAAA;AAAA,EACrB;AAEA,EAAA,SAAS,YAAA,CAAa,IAAA,EAAe,MAAA,EAAgB,YAAA,EAAiD;AACpG,IAAA,OAAO,IAAI,QAAA,CAAS,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA,EAAG;AAAA,MACxC,MAAA;AAAA,MACA,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,kBAAA;AAAA,QAChB,GAAG,YAAA;AAAA,QACH,GAAG;AAAA;AACL,KACD,CAAA;AAAA,EACH;AAGA,EAAA,eAAe,GAAA,CACb,SACA,OAAA,EACmB;AACnB,IAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,OAAA,CAAQ,MAAA;AAC/B,IAAA,MAAM,KAAA,GAAQ,YAAA,CAAa,OAAA,EAAS,IAAI,CAAA;AAGxC,IAAA,IAAI,KAAA,KAAU,wBAAA,IAA4B,KAAA,KAAU,GAAA,EAAK;AACvD,MAAA,MAAM,YAAA,GAAe,KAAK,QAAA,EAAS;AACnC,MAAA,MAAM,IAAA,GAAO,CAAA,CAAA,EAAI,YAAA,CAAa,QAAQ,CAAA,CAAA,CAAA;AAEtC,MAAA,IAAI,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,eAAe,MAAM,IAAA,EAAM;AACjD,QAAA,OAAO,IAAI,SAAS,IAAA,EAAM;AAAA,UACxB,MAAA,EAAQ,GAAA;AAAA,UACR,SAAS,EAAE,MAAA,EAAQ,MAAM,eAAA,EAAiB,qBAAA,EAAuB,GAAG,YAAA;AAAa,SAClF,CAAA;AAAA,MACH;AAEA,MAAA,OAAO,YAAA,CAAa,cAAc,GAAA,EAAK;AAAA,QACrC,MAAA,EAAQ,IAAA;AAAA,QACR,eAAA,EAAiB;AAAA,OAClB,CAAA;AAAA,IACH;AAEA,IAAA,OAAO,YAAA;AAAA,MACL,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,EAAE,IAAA,EAAM,WAAA,EAAa,OAAA,EAAS,CAAA,mBAAA,EAAsB,KAAK,CAAA,CAAA,EAAG,EAAE;AAAA,MAClF;AAAA,KACF;AAAA,EACF;AAGA,EAAA,eAAe,IAAA,CACb,SACA,OAAA,EACmB;AACnB,IAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,OAAA,CAAQ,MAAA;AAC/B,IAAA,MAAM,KAAA,GAAQ,YAAA,CAAa,OAAA,EAAS,IAAI,CAAA;AACxC,IAAA,MAAM,OAAO,WAAA,CAAY,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,eAAe,CAAC,CAAA;AAC7D,IAAA,MAAM,EAAA,GAAK,SAAA;AAAA,MACT,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,iBAAiB,CAAA;AAAA,MACrC,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,WAAW;AAAA,KACjC;AAGA,IAAA,IAAI,KAAA,KAAU,eAAA,IAAmB,KAAA,KAAU,UAAA,EAAY;AACrD,MAAA,IAAI,IAAA;AACJ,MAAA,IAAI;AACF,QAAA,IAAA,GAAO,MAAM,QAAQ,IAAA,EAAK;AAAA,MAC5B,CAAA,CAAA,MAAQ;AACN,QAAA,OAAO,YAAA;AAAA,UACL,EAAE,IAAI,KAAA,EAAO,KAAA,EAAO,EAAE,IAAA,EAAM,gBAAA,EAAkB,OAAA,EAAS,mBAAA,EAAoB,EAAE;AAAA,UAC7E;AAAA,SACF;AAAA,MACF;AAEA,MAAA,IAAI,CAAC,IAAA,EAAM,OAAA,IAAW,OAAO,IAAA,CAAK,YAAY,QAAA,EAAU;AACtD,QAAA,OAAO,YAAA;AAAA,UACL,EAAE,IAAI,KAAA,EAAO,KAAA,EAAO,EAAE,IAAA,EAAM,gBAAA,EAAkB,OAAA,EAAS,uBAAA,EAAwB,EAAE;AAAA,UACjF;AAAA,SACF;AAAA,MACF;AAEA,MAAA,IAAI,YAAA;AACJ,MAAA,IAAI,KAAK,SAAA,EAAW;AAClB,QAAA,MAAM,OAAA,GAAU,MAAM,QAAA,CAAS,GAAA,CAAI,KAAK,SAAS,CAAA;AACjD,QAAA,IAAI,OAAA,iBAAwB,OAAA,CAAQ,KAAA;AAAA,MACtC;AAEA,MAAA,MAAM,OAAA,GAAU,QAAA,CAAS,GAAA,CAAI,IAAA,CAAK,OAAO,CAAA;AACzC,MAAA,MAAM,WAAA,GAAc,IAAA,CAAK,MAAA,KAAW,IAAA,IAAQ,SAAS,MAAA,KAAW,IAAA;AAEhE,MAAA,IAAI,WAAA,EAAa;AAEf,QAAA,MAAM,MAAA,GAAS,IAAI,cAAA,CAAe;AAAA,UAChC,MAAM,MAAM,UAAA,EAAY;AACtB,YAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAChC,YAAA,MAAM,KAAA,GAAQ,CAAC,IAAA,KAAkB;AAC/B,cAAA,UAAA,CAAW,QAAQ,OAAA,CAAQ,MAAA,CAAO,SAAS,IAAA,CAAK,SAAA,CAAU,IAAI,CAAC;;AAAA,CAAM,CAAC,CAAA;AAAA,YACxE,CAAA;AAEA,YAAA,MAAM,UAAA,GAAa;AAAA,cACjB,WAAW,IAAA,CAAK,SAAA;AAAA,cAChB,IAAA;AAAA,cACA,EAAA;AAAA,cACA,KAAA,EAAO,YAAA;AAAA,cACP,WAAW,IAAA,CAAK,SAAA;AAAA,cAChB,IAAA,EAAM,CAAC,IAAA,KAAkB;AACvB,gBAAA,KAAA,CAAM,EAAE,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,CAAA;AAAA,cAC/B;AAAA,aACF;AAEA,YAAA,IAAI;AACF,cAAA,MAAMA,SAAAA,GAAyB,MAAM,QAAA,CAAS,OAAA,CAAQ,KAAK,OAAA,EAAS,IAAA,CAAK,QAAQ,UAAU,CAAA;AAC3F,cAAA,IAAIA,UAAS,EAAA,EAAI;AACf,gBAAA,KAAA,CAAM,EAAE,IAAA,EAAM,MAAA,EAAQ,MAAA,EAAQA,SAAAA,CAAS,QAAQ,CAAA;AAAA,cACjD,CAAA,MAAO;AACL,gBAAA,KAAA,CAAM,EAAE,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,EAAE,IAAA,EAAMA,SAAAA,CAAS,KAAA,CAAM,IAAA,EAAM,OAAA,EAASA,SAAAA,CAAS,KAAA,CAAM,OAAA,IAAW,CAAA;AAAA,cAChG;AAAA,YACF,SAAS,CAAA,EAAG;AACV,cAAA,KAAA,CAAM;AAAA,gBACJ,IAAA,EAAM,OAAA;AAAA,gBACN,KAAA,EAAO;AAAA,kBACL,IAAA,EAAM,gBAAA;AAAA,kBACN,OAAA,EAAS,CAAA,YAAa,KAAA,GAAQ,CAAA,CAAE,OAAA,GAAU;AAAA;AAC5C,eACD,CAAA;AAAA,YACH,CAAA,SAAE;AACA,cAAA,UAAA,CAAW,KAAA,EAAM;AAAA,YACnB;AAAA,UACF;AAAA,SACD,CAAA;AAED,QAAA,OAAO,IAAI,SAAS,MAAA,EAAQ;AAAA,UAC1B,MAAA,EAAQ,GAAA;AAAA,UACR,OAAA,EAAS;AAAA,YACP,cAAA,EAAgB,mBAAA;AAAA,YAChB,eAAA,EAAiB,UAAA;AAAA,YACjB,YAAA,EAAc,YAAA;AAAA,YACd,GAAG;AAAA;AACL,SACD,CAAA;AAAA,MACH;AAGA,MAAA,MAAM,WAAyB,MAAM,QAAA,CAAS,QAAQ,IAAA,CAAK,OAAA,EAAS,KAAK,MAAA,EAAQ;AAAA,QAC/E,WAAW,IAAA,CAAK,SAAA;AAAA,QAChB,IAAA;AAAA,QACA,EAAA;AAAA,QACA,KAAA,EAAO,YAAA;AAAA,QACP,WAAW,IAAA,CAAK;AAAA,OACjB,CAAA;AAED,MAAA,IAAI,IAAA,CAAK,SAAA,IAAa,QAAA,CAAS,EAAA,IAAM,SAAS,KAAA,EAAO;AACnD,QAAA,MAAM,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,SAAA,EAAW,SAAS,KAAK,CAAA;AAAA,MACtD;AAEA,MAAA,MAAM,aAAa,QAAA,CAAS,EAAA,GAAK,MAAM,cAAA,CAAe,QAAA,CAAS,MAAM,IAAI,CAAA;AACzE,MAAA,MAAM,UAAkC,EAAC;AAEzC,MAAA,IAAI,CAAC,QAAA,CAAS,EAAA,IAAM,QAAA,CAAS,KAAA,CAAM,SAAS,cAAA,EAAgB;AAC1D,QAAA,MAAM,OAAA,GAAW,QAAA,CAAS,KAAA,CAAM,OAAA,GAAU,cAAc,CAAA,IAA4B,CAAA;AACpF,QAAA,OAAA,CAAQ,aAAa,CAAA,GAAI,MAAA,CAAO,KAAK,IAAA,CAAK,OAAA,GAAU,GAAI,CAAC,CAAA;AAAA,MAC3D;AAEA,MAAA,OAAO,YAAA,CAAa,QAAA,EAAU,UAAA,EAAY,OAAO,CAAA;AAAA,IACnD;AAGA,IAAA,IAAI,KAAA,KAAU,gBAAA,IAAoB,KAAA,KAAU,WAAA,EAAa;AACvD,MAAA,IAAI,IAAA;AACJ,MAAA,IAAI;AACF,QAAA,IAAA,GAAO,MAAM,QAAQ,IAAA,EAAK;AAAA,MAC5B,CAAA,CAAA,MAAQ;AACN,QAAA,OAAO,YAAA;AAAA,UACL,EAAE,IAAI,KAAA,EAAO,KAAA,EAAO,EAAE,IAAA,EAAM,gBAAA,EAAkB,OAAA,EAAS,mBAAA,EAAoB,EAAE;AAAA,UAC7E;AAAA,SACF;AAAA,MACF;AAEA,MAAA,IAAI;AACF,QAAA,MAAM,SAAS,MAAMC,oBAAA;AAAA,UACnB,IAAA;AAAA,UACA,QAAA;AAAA,UACA,QAAA;AAAA,UACA;AAAA,SACF;AACA,QAAA,OAAO,YAAA,CAAa,QAAQ,GAAG,CAAA;AAAA,MACjC,SAAS,CAAA,EAAG;AACV,QAAA,OAAO,YAAA;AAAA,UACL;AAAA,YACE,EAAA,EAAI,KAAA;AAAA,YACJ,KAAA,EAAO,EAAE,IAAA,EAAM,gBAAA,EAAkB,SAAS,CAAA,YAAa,KAAA,GAAQ,CAAA,CAAE,OAAA,GAAU,eAAA;AAAgB,WAC7F;AAAA,UACA;AAAA,SACF;AAAA,MACF;AAAA,IACF;AAGA,IAAA,IAAI,KAAA,KAAU,qBAAA,IAAyB,KAAA,KAAU,gBAAA,EAAkB;AACjE,MAAA,MAAM,OAAA,GAAU,MAAM,QAAA,CAAS,MAAA,EAAO;AACtC,MAAA,OAAO,YAAA,CAAa,EAAE,EAAA,EAAI,IAAA,EAAM,WAAW,OAAA,CAAQ,EAAA,IAAM,GAAG,CAAA;AAAA,IAC9D;AAGA,IAAA,IAAI,KAAA,KAAU,mBAAA,IAAuB,KAAA,KAAU,cAAA,EAAgB;AAC7D,MAAA,IAAI;AACF,QAAA,MAAM,IAAA,GAAO,MAAM,OAAA,CAAQ,IAAA,EAAK;AAChC,QAAA,IAAI,MAAM,SAAA,EAAW;AACnB,UAAA,MAAM,QAAA,CAAS,OAAA,CAAQ,IAAA,CAAK,SAAS,CAAA;AAAA,QACvC;AAAA,MACF,CAAA,CAAA,MAAQ;AAAA,MAER;AACA,MAAA,OAAO,YAAA,CAAa,EAAE,EAAA,EAAI,IAAA,IAAQ,GAAG,CAAA;AAAA,IACvC;AAEA,IAAA,OAAO,YAAA;AAAA,MACL,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,EAAE,IAAA,EAAM,WAAA,EAAa,OAAA,EAAS,CAAA,oBAAA,EAAuB,KAAK,CAAA,CAAA,EAAG,EAAE;AAAA,MACnF;AAAA,KACF;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,KAAK,IAAA,EAAK;AACrB","file":"index.cjs","sourcesContent":["import type { SurfInstance } from '@surfjs/core';\n\n/**\n * Surf error code to HTTP status code mapping.\n * Consistent with fastify and hono adapters.\n */\nexport function getErrorStatus(code: string): number {\n switch (code) {\n case 'UNKNOWN_COMMAND': return 404;\n case 'INVALID_PARAMS': return 400;\n case 'AUTH_REQUIRED': return 401;\n case 'AUTH_FAILED': return 403;\n case 'SESSION_EXPIRED': return 410;\n case 'RATE_LIMITED': return 429;\n case 'NOT_SUPPORTED': return 501;\n default: return 500;\n }\n}\n\n/**\n * Extract Bearer token from an Authorization header value.\n *\n * @param authHeader - Raw Authorization header value\n * @returns The token string, or `undefined` if not present\n */\nexport function extractAuth(authHeader: string | null | undefined): string | undefined {\n if (!authHeader) return undefined;\n return authHeader.startsWith('Bearer ') ? authHeader.slice(7) : authHeader;\n}\n\n/**\n * Extract client IP from forwarding headers.\n *\n * @param forwardedFor - `x-forwarded-for` header value\n * @param realIp - `x-real-ip` header value\n * @returns The client IP string, or `undefined` if not determinable\n */\nexport function extractIp(\n forwardedFor: string | null | undefined,\n realIp: string | null | undefined,\n): string | undefined {\n if (forwardedFor) return forwardedFor.split(',')[0]?.trim();\n return realIp ?? undefined;\n}\n\n/**\n * CORS headers applied to all Surf responses.\n */\nexport const CORS_HEADERS: Record<string, string> = {\n 'Access-Control-Allow-Origin': '*',\n};\n\n/**\n * Common types re-exported for internal use.\n */\nexport type { SurfInstance };\n","import type { SurfInstance } from '@surfjs/core';\nimport type {\n ExecuteRequest,\n PipelineRequest,\n SurfResponse,\n} from '@surfjs/core';\nimport { executePipeline } from '@surfjs/core';\nimport {\n getErrorStatus,\n extractAuth,\n extractIp,\n CORS_HEADERS,\n} from './shared.js';\n\n/**\n * Next.js App Router request type (web-standard Request with nextUrl).\n * We use the global `Request` type for edge compatibility\n * and avoid importing Next.js types at runtime.\n */\ntype NextRequest = Request & { nextUrl?: URL };\n\n/**\n * Creates App Router route handlers for Surf.js.\n *\n * Returns `GET` and `POST` handlers for use in a Next.js catch-all route file:\n * `app/api/surf/[...slug]/route.ts`\n *\n * @example\n * ```ts\n * // app/api/surf/[...slug]/route.ts\n * import { createSurf } from '@surfjs/core';\n * import { createSurfRouteHandler } from '@surfjs/next';\n *\n * const surf = createSurf({ name: 'my-app', commands: { ... } });\n * export const { GET, POST } = createSurfRouteHandler(surf);\n * ```\n *\n * @example\n * ```ts\n * // With a custom base path\n * export const { GET, POST } = createSurfRouteHandler(surf, {\n * basePath: '/api/surf',\n * });\n * ```\n *\n * @param surf - A `SurfInstance` created via `createSurf()`\n * @param options - Optional configuration\n * @returns An object with `GET` and `POST` handlers\n */\nexport function createSurfRouteHandler(\n surf: SurfInstance,\n options: { basePath?: string } = {},\n): {\n GET: (request: Request, context: { params: Promise<{ slug?: string[] }> }) => Promise<Response>;\n POST: (request: Request, context: { params: Promise<{ slug?: string[] }> }) => Promise<Response>;\n} {\n const registry = surf.commands;\n const sessions = surf.sessions;\n const basePath = options.basePath ?? '/api/surf';\n\n function getPathname(request: Request): string {\n const req = request as NextRequest;\n if (req.nextUrl) return req.nextUrl.pathname;\n return new URL(request.url).pathname;\n }\n\n function resolveRoute(request: Request, slug: string[] | undefined): string {\n if (slug && slug.length > 0) {\n return '/' + slug.join('/');\n }\n // Fallback: extract from pathname\n const pathname = getPathname(request);\n const stripped = pathname.startsWith(basePath)\n ? pathname.slice(basePath.length)\n : pathname;\n return stripped || '/';\n }\n\n function jsonResponse(body: unknown, status: number, extraHeaders?: Record<string, string>): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: {\n 'Content-Type': 'application/json',\n ...CORS_HEADERS,\n ...extraHeaders,\n },\n });\n }\n\n // ─── GET handler ─────────────────────────────────────────────────────\n async function GET(\n request: Request,\n context: { params: Promise<{ slug?: string[] }> },\n ): Promise<Response> {\n const { slug } = await context.params;\n const route = resolveRoute(request, slug);\n\n // /.well-known/surf.json\n if (route === '/.well-known/surf.json' || route === '/') {\n const manifestData = surf.manifest();\n const etag = `\"${manifestData.checksum}\"`;\n\n if (request.headers.get('if-none-match') === etag) {\n return new Response(null, {\n status: 304,\n headers: { 'ETag': etag, 'Cache-Control': 'public, max-age=300', ...CORS_HEADERS },\n });\n }\n\n return jsonResponse(manifestData, 200, {\n 'ETag': etag,\n 'Cache-Control': 'public, max-age=300',\n });\n }\n\n return jsonResponse(\n { ok: false, error: { code: 'NOT_FOUND', message: `Unknown route: GET ${route}` } },\n 404,\n );\n }\n\n // ─── POST handler ────────────────────────────────────────────────────\n async function POST(\n request: Request,\n context: { params: Promise<{ slug?: string[] }> },\n ): Promise<Response> {\n const { slug } = await context.params;\n const route = resolveRoute(request, slug);\n const auth = extractAuth(request.headers.get('authorization'));\n const ip = extractIp(\n request.headers.get('x-forwarded-for'),\n request.headers.get('x-real-ip'),\n );\n\n // ─── POST /surf/execute ──────────────────────────────────────────\n if (route === '/surf/execute' || route === '/execute') {\n let body: ExecuteRequest;\n try {\n body = await request.json() as ExecuteRequest;\n } catch {\n return jsonResponse(\n { ok: false, error: { code: 'INVALID_PARAMS', message: 'Invalid JSON body' } },\n 400,\n );\n }\n\n if (!body?.command || typeof body.command !== 'string') {\n return jsonResponse(\n { ok: false, error: { code: 'INVALID_PARAMS', message: 'Missing command field' } },\n 400,\n );\n }\n\n let sessionState: Record<string, unknown> | undefined;\n if (body.sessionId) {\n const session = await sessions.get(body.sessionId);\n if (session) sessionState = session.state;\n }\n\n const command = registry.get(body.command);\n const wantsStream = body.stream === true && command?.stream === true;\n\n if (wantsStream) {\n // SSE streaming via ReadableStream (edge-compatible)\n const stream = new ReadableStream({\n async start(controller) {\n const encoder = new TextEncoder();\n const write = (data: unknown) => {\n controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\\n\\n`));\n };\n\n const sseContext = {\n sessionId: body.sessionId,\n auth,\n ip,\n state: sessionState,\n requestId: body.requestId,\n emit: (data: unknown) => {\n write({ type: 'chunk', data });\n },\n };\n\n try {\n const response: SurfResponse = await registry.execute(body.command, body.params, sseContext);\n if (response.ok) {\n write({ type: 'done', result: response.result });\n } else {\n write({ type: 'error', error: { code: response.error.code, message: response.error.message } });\n }\n } catch (e) {\n write({\n type: 'error',\n error: {\n code: 'INTERNAL_ERROR',\n message: e instanceof Error ? e.message : 'Unknown error',\n },\n });\n } finally {\n controller.close();\n }\n },\n });\n\n return new Response(stream, {\n status: 200,\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n ...CORS_HEADERS,\n },\n });\n }\n\n // Standard JSON response\n const response: SurfResponse = await registry.execute(body.command, body.params, {\n sessionId: body.sessionId,\n auth,\n ip,\n state: sessionState,\n requestId: body.requestId,\n });\n\n if (body.sessionId && response.ok && response.state) {\n await sessions.update(body.sessionId, response.state);\n }\n\n const statusCode = response.ok ? 200 : getErrorStatus(response.error.code);\n const headers: Record<string, string> = {};\n\n if (!response.ok && response.error.code === 'RATE_LIMITED') {\n const retryMs = (response.error.details?.['retryAfterMs'] as number | undefined) ?? 0;\n headers['Retry-After'] = String(Math.ceil(retryMs / 1000));\n }\n\n return jsonResponse(response, statusCode, headers);\n }\n\n // ─── POST /surf/pipeline ─────────────────────────────────────────\n if (route === '/surf/pipeline' || route === '/pipeline') {\n let body: PipelineRequest;\n try {\n body = await request.json() as PipelineRequest;\n } catch {\n return jsonResponse(\n { ok: false, error: { code: 'INVALID_PARAMS', message: 'Invalid JSON body' } },\n 400,\n );\n }\n\n try {\n const result = await executePipeline(\n body,\n registry as Parameters<typeof executePipeline>[1],\n sessions as Parameters<typeof executePipeline>[2],\n auth,\n );\n return jsonResponse(result, 200);\n } catch (e) {\n return jsonResponse(\n {\n ok: false,\n error: { code: 'INTERNAL_ERROR', message: e instanceof Error ? e.message : 'Unknown error' },\n },\n 500,\n );\n }\n }\n\n // ─── POST /surf/session/start ────────────────────────────────────\n if (route === '/surf/session/start' || route === '/session/start') {\n const session = await sessions.create();\n return jsonResponse({ ok: true, sessionId: session.id }, 200);\n }\n\n // ─── POST /surf/session/end ──────────────────────────────────────\n if (route === '/surf/session/end' || route === '/session/end') {\n try {\n const body = await request.json() as { sessionId?: string };\n if (body?.sessionId) {\n await sessions.destroy(body.sessionId);\n }\n } catch {\n // Ignore parse errors for session end\n }\n return jsonResponse({ ok: true }, 200);\n }\n\n return jsonResponse(\n { ok: false, error: { code: 'NOT_FOUND', message: `Unknown route: POST ${route}` } },\n 404,\n );\n }\n\n return { GET, POST };\n}\n\nexport { getErrorStatus, extractAuth, extractIp } from './shared.js';\n"]}
@@ -0,0 +1,47 @@
1
+ import { SurfInstance } from '@surfjs/core';
2
+ export { e as extractAuth, a as extractIp, g as getErrorStatus } from './shared-CckJeURD.cjs';
3
+
4
+ /**
5
+ * Creates App Router route handlers for Surf.js.
6
+ *
7
+ * Returns `GET` and `POST` handlers for use in a Next.js catch-all route file:
8
+ * `app/api/surf/[...slug]/route.ts`
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // app/api/surf/[...slug]/route.ts
13
+ * import { createSurf } from '@surfjs/core';
14
+ * import { createSurfRouteHandler } from '@surfjs/next';
15
+ *
16
+ * const surf = createSurf({ name: 'my-app', commands: { ... } });
17
+ * export const { GET, POST } = createSurfRouteHandler(surf);
18
+ * ```
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * // With a custom base path
23
+ * export const { GET, POST } = createSurfRouteHandler(surf, {
24
+ * basePath: '/api/surf',
25
+ * });
26
+ * ```
27
+ *
28
+ * @param surf - A `SurfInstance` created via `createSurf()`
29
+ * @param options - Optional configuration
30
+ * @returns An object with `GET` and `POST` handlers
31
+ */
32
+ declare function createSurfRouteHandler(surf: SurfInstance, options?: {
33
+ basePath?: string;
34
+ }): {
35
+ GET: (request: Request, context: {
36
+ params: Promise<{
37
+ slug?: string[];
38
+ }>;
39
+ }) => Promise<Response>;
40
+ POST: (request: Request, context: {
41
+ params: Promise<{
42
+ slug?: string[];
43
+ }>;
44
+ }) => Promise<Response>;
45
+ };
46
+
47
+ export { createSurfRouteHandler };
@@ -0,0 +1,47 @@
1
+ import { SurfInstance } from '@surfjs/core';
2
+ export { e as extractAuth, a as extractIp, g as getErrorStatus } from './shared-CckJeURD.js';
3
+
4
+ /**
5
+ * Creates App Router route handlers for Surf.js.
6
+ *
7
+ * Returns `GET` and `POST` handlers for use in a Next.js catch-all route file:
8
+ * `app/api/surf/[...slug]/route.ts`
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // app/api/surf/[...slug]/route.ts
13
+ * import { createSurf } from '@surfjs/core';
14
+ * import { createSurfRouteHandler } from '@surfjs/next';
15
+ *
16
+ * const surf = createSurf({ name: 'my-app', commands: { ... } });
17
+ * export const { GET, POST } = createSurfRouteHandler(surf);
18
+ * ```
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * // With a custom base path
23
+ * export const { GET, POST } = createSurfRouteHandler(surf, {
24
+ * basePath: '/api/surf',
25
+ * });
26
+ * ```
27
+ *
28
+ * @param surf - A `SurfInstance` created via `createSurf()`
29
+ * @param options - Optional configuration
30
+ * @returns An object with `GET` and `POST` handlers
31
+ */
32
+ declare function createSurfRouteHandler(surf: SurfInstance, options?: {
33
+ basePath?: string;
34
+ }): {
35
+ GET: (request: Request, context: {
36
+ params: Promise<{
37
+ slug?: string[];
38
+ }>;
39
+ }) => Promise<Response>;
40
+ POST: (request: Request, context: {
41
+ params: Promise<{
42
+ slug?: string[];
43
+ }>;
44
+ }) => Promise<Response>;
45
+ };
46
+
47
+ export { createSurfRouteHandler };