@senrijs/senri 0.0.0-stage → 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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +377 -2
  3. package/dist/adapters/node/index.d.ts +14 -0
  4. package/dist/adapters/node/index.js +152 -0
  5. package/dist/adapters/serve/index.d.ts +28 -0
  6. package/dist/adapters/serve/index.js +137 -0
  7. package/dist/adapters/static/index.d.ts +9 -0
  8. package/dist/adapters/static/index.js +120 -0
  9. package/dist/src/app.d.ts +86 -0
  10. package/dist/src/app.js +325 -0
  11. package/dist/src/body.d.ts +3 -0
  12. package/dist/src/body.js +45 -0
  13. package/dist/src/compose.d.ts +22 -0
  14. package/dist/src/compose.js +80 -0
  15. package/dist/src/connection.d.ts +3 -0
  16. package/dist/src/connection.js +49 -0
  17. package/dist/src/context.d.ts +58 -0
  18. package/dist/src/context.js +235 -0
  19. package/dist/src/cookie.d.ts +18 -0
  20. package/dist/src/cookie.js +73 -0
  21. package/dist/src/dev/dashboard/access.d.ts +6 -0
  22. package/dist/src/dev/dashboard/access.js +46 -0
  23. package/dist/src/dev/dashboard/index.d.ts +27 -0
  24. package/dist/src/dev/dashboard/index.js +51 -0
  25. package/dist/src/dev/dashboard/links.d.ts +3 -0
  26. package/dist/src/dev/dashboard/links.js +8 -0
  27. package/dist/src/dev/dashboard/log.d.ts +20 -0
  28. package/dist/src/dev/dashboard/log.js +53 -0
  29. package/dist/src/dev/dashboard/mask.d.ts +4 -0
  30. package/dist/src/dev/dashboard/mask.js +43 -0
  31. package/dist/src/dev/dashboard/record.d.ts +37 -0
  32. package/dist/src/dev/dashboard/record.js +50 -0
  33. package/dist/src/dev/dashboard/routes.d.ts +7 -0
  34. package/dist/src/dev/dashboard/routes.js +121 -0
  35. package/dist/src/dev/dashboard/sse.d.ts +7 -0
  36. package/dist/src/dev/dashboard/sse.js +73 -0
  37. package/dist/src/dev/editor-url.d.ts +14 -0
  38. package/dist/src/dev/editor-url.js +61 -0
  39. package/dist/src/dev/editor.d.ts +6 -0
  40. package/dist/src/dev/editor.js +14 -0
  41. package/dist/src/dev/error-page.d.ts +3 -0
  42. package/dist/src/dev/error-page.js +69 -0
  43. package/dist/src/dev/explain.d.ts +2 -0
  44. package/dist/src/dev/explain.js +37 -0
  45. package/dist/src/dev/fallback.d.ts +3 -0
  46. package/dist/src/dev/fallback.js +40 -0
  47. package/dist/src/dev/hints.d.ts +6 -0
  48. package/dist/src/dev/hints.js +85 -0
  49. package/dist/src/dev/html.d.ts +1 -0
  50. package/dist/src/dev/html.js +14 -0
  51. package/dist/src/dev/index.d.ts +3 -0
  52. package/dist/src/dev/index.js +185 -0
  53. package/dist/src/dev/messages.d.ts +2 -0
  54. package/dist/src/dev/messages.js +6 -0
  55. package/dist/src/dev/not-found-page.d.ts +7 -0
  56. package/dist/src/dev/not-found-page.js +33 -0
  57. package/dist/src/dev/not-found.d.ts +3 -0
  58. package/dist/src/dev/not-found.js +15 -0
  59. package/dist/src/dev/page-data.d.ts +95 -0
  60. package/dist/src/dev/page-data.js +0 -0
  61. package/dist/src/dev/page.generated.d.ts +1 -0
  62. package/dist/src/dev/page.generated.js +3 -0
  63. package/dist/src/dev/pages.d.ts +4 -0
  64. package/dist/src/dev/pages.js +59 -0
  65. package/dist/src/dev/registry.d.ts +3 -0
  66. package/dist/src/dev/registry.js +7 -0
  67. package/dist/src/dev/render.d.ts +5 -0
  68. package/dist/src/dev/render.js +42 -0
  69. package/dist/src/dev/server-timing.d.ts +6 -0
  70. package/dist/src/dev/server-timing.js +45 -0
  71. package/dist/src/dev/source.d.ts +20 -0
  72. package/dist/src/dev/source.js +106 -0
  73. package/dist/src/dev/stack.d.ts +12 -0
  74. package/dist/src/dev/stack.js +93 -0
  75. package/dist/src/dev/suggest.d.ts +12 -0
  76. package/dist/src/dev/suggest.js +95 -0
  77. package/dist/src/dev/validation.d.ts +3 -0
  78. package/dist/src/dev/validation.js +47 -0
  79. package/dist/src/errors.d.ts +25 -0
  80. package/dist/src/errors.js +97 -0
  81. package/dist/src/group.d.ts +37 -0
  82. package/dist/src/group.js +75 -0
  83. package/dist/src/health.d.ts +9 -0
  84. package/dist/src/health.js +40 -0
  85. package/dist/src/index.d.ts +13 -0
  86. package/dist/src/index.js +12 -0
  87. package/dist/src/ip.d.ts +2 -0
  88. package/dist/src/ip.js +12 -0
  89. package/dist/src/logger.d.ts +18 -0
  90. package/dist/src/logger.js +110 -0
  91. package/dist/src/matcher.d.ts +16 -0
  92. package/dist/src/matcher.js +86 -0
  93. package/dist/src/middleware/body-limit.d.ts +6 -0
  94. package/dist/src/middleware/body-limit.js +47 -0
  95. package/dist/src/middleware/cors.d.ts +10 -0
  96. package/dist/src/middleware/cors.js +79 -0
  97. package/dist/src/middleware/response.d.ts +2 -0
  98. package/dist/src/middleware/response.js +28 -0
  99. package/dist/src/middleware/secure-headers.d.ts +19 -0
  100. package/dist/src/middleware/secure-headers.js +46 -0
  101. package/dist/src/router.d.ts +30 -0
  102. package/dist/src/router.js +177 -0
  103. package/dist/src/runtime.d.ts +5 -0
  104. package/dist/src/runtime.js +55 -0
  105. package/dist/src/validate.d.ts +46 -0
  106. package/dist/src/validate.js +55 -0
  107. package/package.json +90 -4
@@ -0,0 +1,325 @@
1
+ import { Context } from "./context.js";
2
+ import { allowedMethods, checkParamNames, normalizeMethod, routeShape, splitPattern, startsWithSegments, } from "./router.js";
3
+ import { compose } from "./compose.js";
4
+ import { createMatcher } from "./matcher.js";
5
+ import { routeHandler } from "./validate.js";
6
+ import { groupEntries, joinPath } from "./group.js";
7
+ import { defaultNotFound, defaultOnError, checkQueryRequest, handleError, HttpError, markConversionError, methodNotAllowed, senriError, } from "./errors.js";
8
+ // 扱わないメソッド。ルートがあってもなくても 501 にする(app.all() でも受けない)
9
+ const NOT_IMPLEMENTED = new Set(["CONNECT", "TRACE", "TRACK"]);
10
+ // app.route(prefix, app) でつなげるように、アプリに登録されたものを横に置いておく
11
+ const appEntries = new WeakMap();
12
+ // 0.1 の createApp({ dev: true }) は使えなくなった。どう書き換えるかを案内する
13
+ const DEV_TRUE = `createApp({ dev: true }) is no longer supported. Use devtools() from "@senrijs/senri/dev".
14
+ createApp({ dev: true }) は使えなくなりました。@senrijs/senri/dev の devtools() を渡してください。
15
+
16
+ import { createApp } from "@senrijs/senri";
17
+ import { devtools } from "@senrijs/senri/dev";
18
+
19
+ const app = createApp({ dev: devtools() });`;
20
+ // V はアプリの型。c.set / c.get で使う変数の型(createApp<{ user: User }>())か、
21
+ // Workers の env も含めた形(createApp<{ Bindings: Env; Variables: { user: User } }>())で指定する(src/context.ts)
22
+ export function createApp(options = {}) {
23
+ if (options.dev === true)
24
+ throw senriError("dev_true", DEV_TRUE);
25
+ const routes = [];
26
+ const matcher = createMatcher();
27
+ // prefix が null なら全部のリクエストで動く。["admin"] なら /admin とその下だけ
28
+ // anonymous: 無名の関数だった(名前を anonymous#番号 にした)。ダッシュボードで「名前を付けると出る」と案内する
29
+ const middlewares = [];
30
+ // 開発モードの処理。createApp の最後に devtools().install() で入る(渡さなければ undefined のまま)
31
+ let devHooks;
32
+ let onError = defaultOnError;
33
+ let notFound = defaultNotFound;
34
+ // 登録時のエラーに、開発モードでは日本語の説明を付ける
35
+ function guard(fn) {
36
+ return (...args) => {
37
+ try {
38
+ fn(...args);
39
+ }
40
+ catch (err) {
41
+ throw devHooks ? devHooks.explain(err) : err;
42
+ }
43
+ };
44
+ }
45
+ function method(name) {
46
+ return guard((path, a, b) => add(name, path, routeHandler(path, a, b), typeof a === "function" ? undefined : a));
47
+ }
48
+ function add(rawMethod, path, handler, schemas) {
49
+ // app.all() の印の "ALL" だけはそのまま。ほかは名前を確かめて大文字にする
50
+ const method = rawMethod === "ALL" ? rawMethod : normalizeMethod(rawMethod);
51
+ devHooks?.checkRoute(method, path, schemas);
52
+ const segments = splitPattern(path);
53
+ checkParamNames(path, segments);
54
+ // 同じメソッド + 同じ形のパスがあったら、あとから登録したほうは絶対に呼ばれないのでエラーにする
55
+ const shape = routeShape(segments);
56
+ const dup = routes.find((r) => r.method === method && routeShape(r.segments) === shape);
57
+ if (dup)
58
+ throw senriError("route_duplicate", `Duplicate route: ${method} ${path} (already: ${dup.method} ${dup.path})`);
59
+ const paramNames = segments.filter((s) => s.startsWith(":")).map((s) => s.slice(1));
60
+ const route = { path, segments, paramNames, method, handler };
61
+ // routes は一覧(routeList や 404 の「もしかして」)用で、登録順。探すのは matcher(Trie)で行う
62
+ routes.push(route);
63
+ matcher.add(route);
64
+ }
65
+ // マッチしたルートのパラメーターを、ルートごとの名前で組み立てる
66
+ function paramsOf(route, values) {
67
+ const params = {};
68
+ const names = route.paramNames;
69
+ for (let i = 0; i < names.length; i++)
70
+ params[names[i]] = values[i];
71
+ return params;
72
+ }
73
+ function resolve(c) {
74
+ let path;
75
+ try {
76
+ path = c.path;
77
+ }
78
+ catch (error) {
79
+ // エンコードが不正(HttpError 400)。ミドルウェアは動かしてから返す
80
+ return { kind: "badRequest", error };
81
+ }
82
+ const method = c.req.method;
83
+ if (NOT_IMPLEMENTED.has(method))
84
+ return { kind: "notImplemented" };
85
+ // 近道: パラメーターのないルートにぴったり合い、メソッドも合えば、それがいちばん優先される
86
+ // (この場合はパスを分割しなくて済む)
87
+ const exact = matcher.staticLeaf(path);
88
+ if (exact) {
89
+ for (const route of exact) {
90
+ if (route.method === method)
91
+ return { kind: "route", route, params: {} };
92
+ }
93
+ }
94
+ // c.path が読めたので、ここで分割に失敗することはない
95
+ const segments = c.segments;
96
+ const find = (m) => {
97
+ const hit = matcher.first(segments, m);
98
+ return hit ? { kind: "route", route: hit.route, params: paramsOf(hit.route, hit.values) } : undefined;
99
+ };
100
+ // 1. メソッドが合うもの → 2. HEAD なら GET → 3. all の順に、優先順位の高いルートから探す
101
+ const hit = find(method) ?? (method === "HEAD" ? find("GET") : undefined) ?? find("ALL");
102
+ if (hit)
103
+ return hit;
104
+ // どれもなければ、パスが合うルートを全部集めて 404 か 405 かを決める
105
+ const matched = matcher.match(segments);
106
+ if (matched.length === 0)
107
+ return { kind: "notFound" };
108
+ const allow = allowedMethods(matched.flatMap((x) => x.leaf));
109
+ return method === "OPTIONS" ? { kind: "options", allow } : { kind: "methodNotAllowed", allow };
110
+ }
111
+ // ミドルウェアがないときに毎回配列を作らないよう、共有の空配列を返す(中身は変えない)
112
+ const NO_LAYERS = [];
113
+ function applicableMiddlewares(c) {
114
+ if (middlewares.length === 0)
115
+ return NO_LAYERS;
116
+ let segments;
117
+ try {
118
+ segments = c.segments;
119
+ }
120
+ catch {
121
+ // パスが不正なら、パス指定のミドルウェアは動かさない(どのみちルーターが 400 にする)
122
+ segments = null;
123
+ }
124
+ const layers = [];
125
+ for (let i = 0; i < middlewares.length; i++) {
126
+ const { name, mw, prefix } = middlewares[i];
127
+ if (prefix === null || (segments !== null && startsWithSegments(segments, prefix))) {
128
+ // order は登録順(1 から)。同じ関数を2回登録しても区別できるよう、配列の位置から決める
129
+ // (ダッシュボードで、名前ではなく登録順で集計するのに使う)
130
+ layers.push({ name, mw, order: i + 1 });
131
+ }
132
+ }
133
+ return layers;
134
+ }
135
+ const app = {
136
+ use: guard((pathOrMw, mw) => {
137
+ const path = typeof pathOrMw === "string" ? pathOrMw : null;
138
+ const fn = typeof pathOrMw === "string" ? mw : pathOrMw;
139
+ if (!fn)
140
+ throw senriError("no_middleware", `Missing middleware: ${path}`);
141
+ middlewares.push({
142
+ // 関数の名前を使う。無名の関数(その場で書いたアロー関数など)は登録順の番号で呼ぶ
143
+ name: fn.name || `anonymous#${middlewares.length + 1}`,
144
+ anonymous: !fn.name,
145
+ path,
146
+ prefix: path === null ? null : splitPattern(path),
147
+ mw: fn,
148
+ });
149
+ }),
150
+ onError(handler) {
151
+ onError = handler;
152
+ },
153
+ notFound(handler) {
154
+ notFound = handler;
155
+ },
156
+ // どれも app.get(path, handler) か、スキーマを付けた app.get(path, { params, query, body }, handler)(src/validate.ts)
157
+ get: method("GET"),
158
+ post: method("POST"),
159
+ put: method("PUT"),
160
+ patch: method("PATCH"),
161
+ delete: method("DELETE"),
162
+ // 登録しなければ GET を使う(ボディは送らない)。登録すれば GET より優先される
163
+ head: method("HEAD"),
164
+ // 登録しなければ 204 と Allow を自動で返す。登録すればそちらが優先される
165
+ options: method("OPTIONS"),
166
+ // どのメソッドでも動く。ただし、メソッドが合うルートがあればそちらが優先
167
+ all: method("ALL"),
168
+ // 任意のメソッド(QUERY や、PROPFIND などの WebDAV)を登録する。配列でまとめて登録もできる
169
+ // 小文字で書いても大文字にする。CONNECT / TRACE / TRACK は扱わない
170
+ on: guard((methods, path, a, b) => {
171
+ const list = typeof methods === "string" ? [methods] : methods;
172
+ if (list.length === 0)
173
+ throw senriError("no_method", `Missing method: ${path}`);
174
+ const handler = routeHandler(path, a, b);
175
+ for (const m of list) {
176
+ if (m.toUpperCase() === "ALL")
177
+ throw senriError("on_all", "Use app.all() for every method");
178
+ add(m, path, handler, typeof a === "function" ? undefined : a);
179
+ }
180
+ }),
181
+ // K はテーブルのキーから推論される。各キーのパスからパラメーターの型が決まる
182
+ routes: guard((table) => {
183
+ for (const [key, handler] of Object.entries(table)) {
184
+ const [method, path] = key.split(" ");
185
+ if (!method || !path)
186
+ throw senriError("route_key", `Invalid route key: "${key}" (e.g. "GET /hello")`);
187
+ add(method.toUpperCase(), path, handler);
188
+ }
189
+ }),
190
+ // createRouter() で作ったルーターをつなげる。ルーターに登録済みのものが、この時点でアプリに入る
191
+ // app.route(prefix, app) なら、別のアプリをプレフィックス付きでつなげる
192
+ // (つなぐ側のアプリでは、プレフィックスのパラメーターに型は付かない。型が必要なら createRouter を使う)
193
+ // つないだアプリの onError / notFound は使わず、こちらのものを使う
194
+ route: guard((routerOrPrefix, child) => {
195
+ // 先に全部読んでおく(ルーターの作りがおかしいときに、途中まで登録された状態にしない)
196
+ let list;
197
+ if (typeof routerOrPrefix === "string") {
198
+ const read = child ? appEntries.get(child) : undefined;
199
+ if (!read)
200
+ throw senriError("not_app", new TypeError(`Not an app from createApp(): ${routerOrPrefix}`));
201
+ list = read().map((entry) => ({ ...entry, path: joinPath(routerOrPrefix, entry.path) }));
202
+ }
203
+ else {
204
+ list = groupEntries(routerOrPrefix);
205
+ }
206
+ for (const entry of list) {
207
+ if (entry.kind === "use")
208
+ app.use(entry.path, entry.mw);
209
+ else
210
+ add(entry.method, entry.path, entry.handler);
211
+ }
212
+ }),
213
+ middlewareList() {
214
+ return middlewares.map((m) => ({ name: m.name, path: m.path, anonymous: m.anonymous }));
215
+ },
216
+ routeList() {
217
+ return routes.map((r) => ({ method: r.method, path: r.path }));
218
+ },
219
+ // info: ランタイムが渡す2つ目の引数(Bun の server、Deno の info など)。接続元のアドレスを取るのに使う
220
+ // (形で見分けるので、Cloudflare Workers の env のような別のものが来ても壊れない。src/dev/dashboard/access.ts)
221
+ // ctx: Cloudflare Workers の3つ目の引数(c.executionCtx)。export default app で、Workers がそのまま呼ぶ
222
+ fetch(req, info, ctx) {
223
+ // 開発モードは計測・記録をするので別の道を通る(@senrijs/senri/dev)
224
+ if (devHooks)
225
+ return devHooks.fetch(req, info, ctx);
226
+ try {
227
+ const c = new Context(req, info, ctx);
228
+ const resolved = resolve(c);
229
+ if (resolved.kind === "route") {
230
+ c.route = resolved.route.path;
231
+ c.params = resolved.params;
232
+ }
233
+ const fail = (err) => handleError(err, c, onError);
234
+ const layers = applicableMiddlewares(c);
235
+ if (layers.length > 0) {
236
+ return compose(layers, c, () => inner(c, resolved), fail).then((res) => c.finalize(res));
237
+ }
238
+ // 近道: ミドルウェアがなければ compose を通さない。同期のハンドラーなら、待たずにそのまま返す
239
+ let res;
240
+ try {
241
+ res = inner(c, resolved);
242
+ }
243
+ catch (err) {
244
+ res = fail(err);
245
+ }
246
+ if (res instanceof Promise)
247
+ return res.catch(fail).then((r) => c.finalize(r));
248
+ return Promise.resolve(c.finalize(res));
249
+ }
250
+ catch (err) {
251
+ return Promise.reject(err);
252
+ }
253
+ },
254
+ };
255
+ // ミドルウェアを全部通ったあとに動く、いちばん内側の処理
256
+ // 同期で返せるときは Response をそのまま返す(Promise を作らない)
257
+ function inner(c, resolved) {
258
+ switch (resolved.kind) {
259
+ case "badRequest":
260
+ throw resolved.error;
261
+ case "route": {
262
+ const route = resolved.route;
263
+ // QUERY として登録したルートだけ、ボディと Content-Type を確かめる(app.all はハンドラーに任せる)
264
+ if (route.method === "QUERY") {
265
+ return checkQueryRequest(c).then((invalid) => {
266
+ if (invalid)
267
+ throw invalid;
268
+ return callHandler(c, route);
269
+ });
270
+ }
271
+ return callHandler(c, route);
272
+ }
273
+ case "options":
274
+ return new Response(null, { status: 204, headers: { allow: resolved.allow } });
275
+ case "methodNotAllowed":
276
+ throw methodNotAllowed(resolved.allow);
277
+ case "notImplemented":
278
+ throw new HttpError(501, "501 Not Implemented");
279
+ case "notFound":
280
+ c.status(404);
281
+ return Promise.resolve(notFound(c)).then((value) => c.toResponse(value));
282
+ }
283
+ }
284
+ function callHandler(c, route) {
285
+ const value = route.handler(c.params, c);
286
+ // 同期のハンドラーなら待たない
287
+ return value instanceof Promise ? value.then((v) => respond(c, route, v)) : respond(c, route, value);
288
+ }
289
+ function respond(c, route, value) {
290
+ let res;
291
+ try {
292
+ res = c.toResponse(value);
293
+ }
294
+ catch (err) {
295
+ // どこが返した値かは、失敗したときだけ組み立てる
296
+ throw markConversionError(err, { kind: "handler", name: `${route.method} ${route.path}` });
297
+ }
298
+ if (c.req.method !== "HEAD")
299
+ return res;
300
+ // HEAD はヘッダーと status だけ返す(ボディは捨てる)
301
+ return (async () => {
302
+ await res.body?.cancel();
303
+ return new Response(null, { status: res.status, statusText: res.statusText, headers: res.headers });
304
+ })();
305
+ }
306
+ // ミドルウェアを先に、ルートはそのあと(どちらも登録順)
307
+ appEntries.set(app, () => [
308
+ ...middlewares.map((m) => ({ kind: "use", path: m.path ?? "/", mw: m.mw })),
309
+ ...routes.map((r) => ({ kind: "route", method: r.method, path: r.path, handler: r.handler })),
310
+ ]);
311
+ // 開発モード: 中身を渡して、開発用の fetch と既定の onError / notFound を受け取る
312
+ if (options.dev) {
313
+ devHooks = options.dev.install({
314
+ app,
315
+ resolve,
316
+ layers: applicableMiddlewares,
317
+ inner: inner,
318
+ fail: (err, c) => handleError(err, c, onError),
319
+ routes: () => routes,
320
+ });
321
+ onError = devHooks.onError;
322
+ notFound = devHooks.notFound;
323
+ }
324
+ return app;
325
+ }
@@ -0,0 +1,3 @@
1
+ export type FormValue = string | File;
2
+ export type FormBody = Record<string, FormValue | FormValue[]>;
3
+ export declare function readBody(req: Request): Promise<unknown>;
@@ -0,0 +1,45 @@
1
+ // リクエストのボディを、Content-Type に合わせて読む
2
+ import { HttpError } from "./errors.js";
3
+ function mediaType(req) {
4
+ return (req.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
5
+ }
6
+ // フィールド名は送る側が決められるので、プロトタイプのないオブジェクトに入れる
7
+ // (ふつうの {} だと "__proto__" でプロトタイプが差し替わり、"constructor" は継承した関数と混ざる)
8
+ function formToObject(form) {
9
+ const out = Object.create(null);
10
+ for (const [name, value] of form) {
11
+ const prev = out[name];
12
+ if (prev === undefined)
13
+ out[name] = value;
14
+ else
15
+ out[name] = Array.isArray(prev) ? [...prev, value] : [prev, value];
16
+ }
17
+ return out;
18
+ }
19
+ // - application/json と、+json で終わるもの → JSON(壊れていたら 400)
20
+ // - application/x-www-form-urlencoded と multipart/form-data → フォーム
21
+ // - text/* → 文字列
22
+ // - それ以外 → ArrayBuffer
23
+ export async function readBody(req) {
24
+ const type = mediaType(req);
25
+ if (type === "application/json" || type.endsWith("+json")) {
26
+ const text = await req.text();
27
+ try {
28
+ return JSON.parse(text);
29
+ }
30
+ catch (err) {
31
+ throw new HttpError(400, "400 Bad Request: JSON が正しくありません", { cause: err });
32
+ }
33
+ }
34
+ if (type === "application/x-www-form-urlencoded" || type === "multipart/form-data") {
35
+ try {
36
+ return formToObject(await req.formData());
37
+ }
38
+ catch (err) {
39
+ throw new HttpError(400, "400 Bad Request: フォームが正しくありません", { cause: err });
40
+ }
41
+ }
42
+ if (type.startsWith("text/"))
43
+ return req.text();
44
+ return req.arrayBuffer();
45
+ }
@@ -0,0 +1,22 @@
1
+ import type { Context, HandlerResult, Middleware } from "./context.ts";
2
+ import { type ConversionOrigin } from "./errors.ts";
3
+ export declare function toResponseOf(c: Context, value: HandlerResult, origin: ConversionOrigin): Response;
4
+ export type Layer = {
5
+ name: string;
6
+ mw: Middleware;
7
+ order?: number;
8
+ };
9
+ export type Timing = {
10
+ name: string;
11
+ total: number;
12
+ self: number;
13
+ failed: boolean;
14
+ start: number;
15
+ stopped: boolean;
16
+ };
17
+ export type Trace = {
18
+ enter(i: number): void;
19
+ down(i: number): () => void;
20
+ leave(i: number, failed: boolean, stopped: boolean): void;
21
+ };
22
+ export declare function compose(layers: Layer[], c: Context, handle: () => Response | Promise<Response>, onError: (err: unknown) => Promise<Response>, trace?: Trace): Promise<Response>;
@@ -0,0 +1,80 @@
1
+ import { markConversionError, senriError } from "./errors.js";
2
+ // 返した値をレスポンスにする。失敗したら、どこが返した値だったかをエラーに覚えさせてから投げ直す
3
+ export function toResponseOf(c, value, origin) {
4
+ try {
5
+ return c.toResponse(value);
6
+ }
7
+ catch (err) {
8
+ throw markConversionError(err, origin);
9
+ }
10
+ }
11
+ // ミドルウェアを順に通して、最後に handle を呼ぶ
12
+ // 例外は(ミドルウェアでもハンドラーでも)onError に渡してレスポンスにしてもらう
13
+ export function compose(layers, c, handle, onError, trace) {
14
+ // 近道: ミドルウェアがなく、計測もしないなら、いちばん内側の処理を呼ぶだけ(下の dispatch と同じ結果になる)
15
+ if (layers.length === 0 && !trace) {
16
+ try {
17
+ return Promise.resolve(handle()).catch(onError);
18
+ }
19
+ catch (err) {
20
+ return onError(err);
21
+ }
22
+ }
23
+ // どの層で例外が起きたか(層の番号)
24
+ let failedAt = -1;
25
+ const fail = (err, i) => {
26
+ failedAt = i;
27
+ return onError(err);
28
+ };
29
+ const dispatch = async (i) => {
30
+ trace?.enter(i);
31
+ // next() を呼ばずに値を返して終わったか
32
+ let stopped = false;
33
+ try {
34
+ return await step(i, () => (stopped = true));
35
+ }
36
+ finally {
37
+ trace?.leave(i, failedAt === i, stopped);
38
+ }
39
+ };
40
+ const step = async (i, markStopped) => {
41
+ const layer = layers[i];
42
+ if (!layer) {
43
+ try {
44
+ return await handle();
45
+ }
46
+ catch (err) {
47
+ return fail(err, i);
48
+ }
49
+ }
50
+ // next() が返す Promise。呼ばれていなければ undefined
51
+ let downstream;
52
+ let result;
53
+ try {
54
+ result = await layer.mw(c, () => {
55
+ // 2回目を許すと、ハンドラーや後ろのミドルウェアが2回動いてしまう
56
+ if (downstream)
57
+ throw senriError("next_twice", "next() called twice");
58
+ // 下流に入る時刻は、dispatch を始める前に取る(下流が最初の await までに同期で使った時間も、下流の時間にする)
59
+ const up = trace?.down(i);
60
+ const d = dispatch(i + 1);
61
+ downstream = up ? d.finally(up) : d;
62
+ return downstream;
63
+ });
64
+ // toResponse も JSON.stringify で投げることがあるので try の中で
65
+ if (result !== undefined) {
66
+ if (!downstream)
67
+ markStopped();
68
+ return toResponseOf(c, result, { kind: "middleware", name: layer.name });
69
+ }
70
+ // 何も返さず next() 済みなら、下流を使う(await し忘れていても、ここで待つ)
71
+ if (downstream)
72
+ return await downstream;
73
+ }
74
+ catch (err) {
75
+ return fail(err, i);
76
+ }
77
+ return fail(senriError("no_response", "Middleware returned nothing and did not call next()"), i);
78
+ };
79
+ return dispatch(0);
80
+ }
@@ -0,0 +1,3 @@
1
+ export declare function remoteAddress(req: Request, info: unknown): string | null;
2
+ export declare function isConnectionInfo(info: unknown): boolean;
3
+ export declare function clientIp(req: Request, info: unknown): string | null;
@@ -0,0 +1,49 @@
1
+ // 接続元のアドレスを、app.fetch の2つ目の引数から取り出す(c.ip と、ダッシュボードの localhost 制限で使う)
2
+ //
3
+ // Web 標準の Request には接続元が入っていないので、ランタイムが渡すものから取る:
4
+ // - Bun: Bun.serve({ fetch: app.fetch }) は server を渡す → server.requestIP(req)
5
+ // - Deno: Deno.serve(app.fetch) は info を渡す → info.remoteAddr.hostname
6
+ // - Node: senri/node のアダプターが Deno と同じ形 { remoteAddr: { hostname } } で渡す
7
+ // 型ではなく「形」で見分けるので、Cloudflare Workers の fetch(req, env, ctx) の env のような
8
+ // 別のものが来ても壊れず、「接続元不明」(null)になる
9
+ import { isWorkers } from "./runtime.js";
10
+ export function remoteAddress(req, info) {
11
+ try {
12
+ if (typeof info !== "object" || info === null)
13
+ return null;
14
+ const bun = info;
15
+ if (typeof bun.requestIP === "function") {
16
+ const address = bun.requestIP(req)?.address;
17
+ return typeof address === "string" ? address : null;
18
+ }
19
+ const deno = info;
20
+ const hostname = typeof deno.remoteAddr === "object" && deno.remoteAddr !== null ? deno.remoteAddr.hostname : undefined;
21
+ return typeof hostname === "string" ? hostname : null;
22
+ }
23
+ catch {
24
+ // プロパティを読んだだけで例外を投げるような値でも、壊れずに「不明」にする
25
+ return null;
26
+ }
27
+ }
28
+ // Bun の server か、Deno の info(Node のアダプターも同じ形)か。そうでなければ Workers の env などほかのもの
29
+ export function isConnectionInfo(info) {
30
+ try {
31
+ if (typeof info !== "object" || info === null)
32
+ return false;
33
+ const value = info;
34
+ return typeof value.requestIP === "function" || (typeof value.remoteAddr === "object" && value.remoteAddr !== null);
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ // c.ip 用。IPv4 を IPv6 の形で書いたもの(::ffff:192.0.2.1)は IPv4 にそろえる
41
+ // Cloudflare Workers には接続元を渡す引数がないので、Cloudflare が付ける CF-Connecting-IP を読む
42
+ // (Cloudflare を通ったリクエストでは、クライアントが送った同名のヘッダーは Cloudflare が上書きする)
43
+ export function clientIp(req, info) {
44
+ const address = remoteAddress(req, info) ?? (isWorkers() ? req.headers.get("cf-connecting-ip") : null);
45
+ if (address === null)
46
+ return null;
47
+ const lower = address.toLowerCase();
48
+ return lower.startsWith("::ffff:") && lower.includes(".") ? address.slice("::ffff:".length) : address;
49
+ }
@@ -0,0 +1,58 @@
1
+ import type { Timing } from "./compose.ts";
2
+ export type BodyValue = Blob | ArrayBuffer | ArrayBufferView<ArrayBuffer> | ReadableStream | FormData | URLSearchParams;
3
+ export type JsonObject = object & {
4
+ call?: never;
5
+ then?: never;
6
+ };
7
+ export type HandlerResult = Response | string | BodyValue | JsonObject;
8
+ export type ExecutionContext = {
9
+ waitUntil(promise: Promise<unknown>): void;
10
+ passThroughOnException(): void;
11
+ };
12
+ type IsAny<T> = 0 extends 1 & T ? true : false;
13
+ type EnvForm = {
14
+ Bindings: object;
15
+ } | {
16
+ Variables: object;
17
+ };
18
+ export type VariablesOf<E> = IsAny<E> extends true ? any : E extends EnvForm ? (E extends {
19
+ Variables: infer V extends object;
20
+ } ? V : Record<string, unknown>) : E;
21
+ export type BindingsOf<E> = IsAny<E> extends true ? any : E extends {
22
+ Bindings: infer B extends object;
23
+ } ? B : unknown;
24
+ export type Handler<P = Record<string, string>, V extends object = any, I extends object = {}> = (params: P, c: Context<V, I>) => HandlerResult | Promise<HandlerResult>;
25
+ export type Next = () => Promise<Response>;
26
+ export type Middleware<V extends object = any> = (c: Context<V>, next: Next) => HandlerResult | void | Promise<HandlerResult | void>;
27
+ export declare function setValidated(c: Context, target: string, value: unknown): void;
28
+ export declare let connectionOf: (c: Context) => {
29
+ req: Request;
30
+ info: unknown;
31
+ };
32
+ export declare class Context<V extends object = any, I extends object = {}> {
33
+ #private;
34
+ req: Request;
35
+ error: unknown;
36
+ route: string | undefined;
37
+ params: Record<string, string>;
38
+ constructor(req: Request, info?: unknown, ctx?: ExecutionContext);
39
+ get env(): BindingsOf<V>;
40
+ get executionCtx(): ExecutionContext;
41
+ get segments(): readonly string[];
42
+ get path(): string;
43
+ get timings(): Timing[];
44
+ valid<K extends keyof I & string>(target: K): I[K];
45
+ set<K extends keyof VariablesOf<V> & string>(key: K, value: VariablesOf<V>[K]): this;
46
+ get<K extends keyof VariablesOf<V> & string>(key: K): VariablesOf<V>[K] | undefined;
47
+ query(): Record<string, string>;
48
+ query(name: string): string | undefined;
49
+ queries(name: string): string[];
50
+ body<T = unknown>(): Promise<T>;
51
+ status(code: number): this;
52
+ header(name: string, value: string): this;
53
+ json(data: unknown, status?: number): Response;
54
+ text(body: string, status?: number): Response;
55
+ finalize(res: Response): Response;
56
+ toResponse(value: HandlerResult): Response;
57
+ }
58
+ export {};