@lunora/server 1.0.0-alpha.4 → 1.0.0-alpha.41

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 (68) hide show
  1. package/LICENSE.md +219 -0
  2. package/README.md +50 -0
  3. package/dist/data-model.d.mts +231 -156
  4. package/dist/data-model.d.ts +231 -156
  5. package/dist/data-model.mjs +0 -1
  6. package/dist/drizzle.mjs +1 -1
  7. package/dist/index.d.mts +1304 -831
  8. package/dist/index.d.ts +1304 -831
  9. package/dist/index.mjs +1 -24
  10. package/dist/otel.d.mts +543 -0
  11. package/dist/otel.d.ts +543 -0
  12. package/dist/otel.mjs +1 -0
  13. package/dist/packem_shared/LunoraEnvError-CgpI2Mm_.mjs +3 -0
  14. package/dist/packem_shared/LunoraError-LVhdU0Lo.mjs +1 -0
  15. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-CF_9uiu1.mjs +1 -0
  16. package/dist/packem_shared/allowAll-BnyNbJZT.mjs +1 -0
  17. package/dist/packem_shared/asBucketStorage-1pFfH-Tn.mjs +1 -0
  18. package/dist/packem_shared/bindOrm-Bp9hsM2q.mjs +1 -0
  19. package/dist/packem_shared/buildRlsReadRegistry-WdiqSj87.mjs +1 -0
  20. package/dist/packem_shared/composePluginMiddleware-COr09CXA.mjs +1 -0
  21. package/dist/packem_shared/context-identity-GunnA6La.mjs +1 -0
  22. package/dist/packem_shared/createPolicyDsl-sV1swpkD.mjs +1 -0
  23. package/dist/packem_shared/createSecrets-CgVPiW2C.mjs +1 -0
  24. package/dist/packem_shared/defineAggregateIndex-CYuH6IbJ.mjs +1 -0
  25. package/dist/packem_shared/defineIdentity-B7gfAgxx.mjs +1 -0
  26. package/dist/packem_shared/defineMigration-Bfpwxv2f.mjs +1 -0
  27. package/dist/packem_shared/defineMutator-BgpQ-xUo.mjs +1 -0
  28. package/dist/packem_shared/defineShape-Ds8uNqzX.mjs +1 -0
  29. package/dist/packem_shared/defineStorageRule-BDu01PUn.mjs +1 -0
  30. package/dist/packem_shared/functions-CDC08CWY.mjs +1 -0
  31. package/dist/packem_shared/httpAction-C14NuF3V.mjs +4 -0
  32. package/dist/packem_shared/initLunora-CHh1EmR4.mjs +1 -0
  33. package/dist/packem_shared/mask-C6Bi78qj.mjs +1 -0
  34. package/dist/packem_shared/onConnect-CEtRmUpJ.mjs +1 -0
  35. package/dist/packem_shared/policy-tag-Dprt9JWo.mjs +1 -0
  36. package/dist/packem_shared/protectPublic-BhKewPqm.mjs +1 -0
  37. package/dist/packem_shared/rls-_iVsPvhX.mjs +1 -0
  38. package/dist/packem_shared/run-middleware-BeEEqmdE.mjs +1 -0
  39. package/dist/packem_shared/storageRules-BptPZbi8.mjs +1 -0
  40. package/dist/packem_shared/types.d-C4CMJK8x.d.mts +141 -0
  41. package/dist/packem_shared/types.d-DdYF8E18.d.ts +141 -0
  42. package/dist/rls/testing.d.mts +31 -31
  43. package/dist/rls/testing.d.ts +31 -31
  44. package/dist/rls/testing.mjs +1 -49
  45. package/dist/types.d.mts +1163 -440
  46. package/dist/types.d.ts +1163 -440
  47. package/dist/types.mjs +1 -31
  48. package/package.json +18 -5
  49. package/dist/packem_shared/LunoraEnvError-DjFkpkSP.mjs +0 -187
  50. package/dist/packem_shared/LunoraError-DhggBJZF.mjs +0 -51
  51. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-BgBQsqQ-.mjs +0 -114
  52. package/dist/packem_shared/asBucketStorage-Cnxd9y2q.mjs +0 -11
  53. package/dist/packem_shared/bindOrm-Ce57S3N9.mjs +0 -128
  54. package/dist/packem_shared/composePluginMiddleware-Ck5_TUO8.mjs +0 -100
  55. package/dist/packem_shared/createPolicyDsl-De67zPDS.mjs +0 -29
  56. package/dist/packem_shared/defineAggregateIndex-C2gT1GzM.mjs +0 -252
  57. package/dist/packem_shared/defineMigration-CAJLr6fx.mjs +0 -8
  58. package/dist/packem_shared/defineStorageRule-qu0mpilX.mjs +0 -20
  59. package/dist/packem_shared/httpAction-B7FYUEgr.mjs +0 -340
  60. package/dist/packem_shared/initLunora-CATvPsVt.mjs +0 -86
  61. package/dist/packem_shared/mask-eCUYOwhd.mjs +0 -211
  62. package/dist/packem_shared/onConnect-CIPXKPyw.mjs +0 -13
  63. package/dist/packem_shared/protectPublic-BjFkQ_Or.mjs +0 -15
  64. package/dist/packem_shared/rls-Bi9HiyDC.mjs +0 -567
  65. package/dist/packem_shared/run-middleware-CYQOuoV6.mjs +0 -18
  66. package/dist/packem_shared/storageRules-4a30FSpI.mjs +0 -88
  67. package/dist/packem_shared/types.d-BDY0FYHK.d.ts +0 -135
  68. package/dist/packem_shared/types.d-DmvyEMD6.d.mts +0 -135
@@ -1,29 +0,0 @@
1
- const definePolicy = (input) => {
2
- return { on: input.on, table: input.table, when: input.when };
3
- };
4
- const createPolicyDsl = () => (input) => {
5
- return { on: input.on, table: input.table, when: input.when };
6
- };
7
- const definePermission = (name, options = {}) => {
8
- return { name, ...options };
9
- };
10
- const definePolicies = (policies) => {
11
- const seenWhenByKey = /* @__PURE__ */ new Map();
12
- for (const policy of policies) {
13
- const key = JSON.stringify([policy.table, policy.on]);
14
- const whens = seenWhenByKey.get(key) ?? /* @__PURE__ */ new Set();
15
- if (whens.has(policy.when)) {
16
- throw new Error(
17
- `definePolicies: duplicate policy for (table "${policy.table}", on "${policy.on}") — the same decision function is registered more than once. Multiple distinct policies per (table, on) are allowed (reads OR, writes AND); remove the duplicate.`
18
- );
19
- }
20
- whens.add(policy.when);
21
- seenWhenByKey.set(key, whens);
22
- }
23
- return policies;
24
- };
25
- const defineRole = (name, options = {}) => {
26
- return { name, ...options };
27
- };
28
-
29
- export { createPolicyDsl, definePermission, definePolicies, definePolicy, defineRole };
@@ -1,252 +0,0 @@
1
- import { isOrWrapsFromValidator, v } from '@lunora/values';
2
- import { mergeSchemaExtension } from './composePluginMiddleware-Ck5_TUO8.mjs';
3
-
4
- const relationBuilder = {
5
- many: (table, options) => {
6
- return { field: options.field, kind: "many", references: options.references ?? "_id", table };
7
- },
8
- one: (table, options) => {
9
- return { field: options.field, kind: "one", onDelete: options.onDelete, references: options.references ?? "_id", table };
10
- }
11
- };
12
- const makeTrigger = (timing, op, handler) => {
13
- return { handler, op, timing };
14
- };
15
- const createTriggerBuilder = () => {
16
- return {
17
- afterDelete: (handler) => makeTrigger("after", "delete", handler),
18
- afterInsert: (handler) => makeTrigger("after", "insert", handler),
19
- afterUpdate: (handler) => makeTrigger("after", "update", handler),
20
- beforeDelete: (handler) => makeTrigger("before", "delete", handler),
21
- beforeInsert: (handler) => makeTrigger("before", "insert", handler),
22
- beforeUpdate: (handler) => makeTrigger("before", "update", handler)
23
- };
24
- };
25
- const defineTable = (inputShape) => {
26
- const shape = { ...inputShape };
27
- for (const [columnName, validator] of Object.entries(shape)) {
28
- if (isOrWrapsFromValidator(validator)) {
29
- throw new Error(`defineTable: column "${columnName}" uses v.from() which is args-only — table columns need a concrete v.* type`);
30
- }
31
- }
32
- const aggregateIndexes = [];
33
- const indexes = [];
34
- const rankIndexes = [];
35
- const relations = {};
36
- const searchIndexes = [];
37
- const triggers = {};
38
- const triggerBuilder = createTriggerBuilder();
39
- const vectorIndexes = [];
40
- let shardMode = { kind: "root" };
41
- let isExternallyManaged = false;
42
- let isPublic = false;
43
- let softDelete;
44
- const builder = {
45
- aggregateIndex(name, options) {
46
- const op = options?.op ?? "count";
47
- if (op !== "count" && !options?.field) {
48
- throw new Error(`aggregateIndex "${name}": op "${op}" requires a "field"`);
49
- }
50
- aggregateIndexes.push({
51
- by: options?.by,
52
- field: options?.field,
53
- name,
54
- // `on` is filled in by `defineSchema` once the table is keyed; we
55
- // stash the placeholder so the AggregateIndexDefinition shape stays
56
- // straightforward for D1/DO consumers (who read `on`).
57
- on: "",
58
- op,
59
- where: options?.where
60
- });
61
- return builder;
62
- },
63
- get aggregateIndexes() {
64
- return aggregateIndexes;
65
- },
66
- externallyManaged() {
67
- isExternallyManaged = true;
68
- return builder;
69
- },
70
- global(options) {
71
- shardMode = { backend: options?.backend ?? "d1", kind: "global" };
72
- return builder;
73
- },
74
- get isExternallyManaged() {
75
- return isExternallyManaged;
76
- },
77
- get isPublic() {
78
- return isPublic;
79
- },
80
- index(name, fields, options) {
81
- indexes.push({ fields, name, unique: options?.unique ?? false });
82
- return builder;
83
- },
84
- get indexes() {
85
- return indexes;
86
- },
87
- public() {
88
- isPublic = true;
89
- return builder;
90
- },
91
- rankIndex(name, options) {
92
- if (!options.sortBy || options.sortBy.length === 0) {
93
- throw new Error(`rankIndex "${name}": "sortBy" is required and must list at least one key`);
94
- }
95
- const sortBy = options.sortBy.map((key) => {
96
- return {
97
- direction: key.direction ?? "asc",
98
- field: key.field
99
- };
100
- });
101
- rankIndexes.push({
102
- name,
103
- // `on` is filled in by `defineSchema` once the table is keyed —
104
- // same pattern as `aggregateIndex`.
105
- on: "",
106
- partitionBy: options.partitionBy,
107
- sortBy,
108
- where: options.where
109
- });
110
- return builder;
111
- },
112
- get rankIndexes() {
113
- return rankIndexes;
114
- },
115
- get relationMap() {
116
- return relations;
117
- },
118
- relations(build) {
119
- Object.assign(relations, build(relationBuilder));
120
- return builder;
121
- },
122
- searchIndex(name, options) {
123
- searchIndexes.push({ field: options.field, filterFields: options.filterFields, name });
124
- return builder;
125
- },
126
- get searchIndexes() {
127
- return searchIndexes;
128
- },
129
- shape,
130
- shardBy(field) {
131
- shardMode = { field, kind: "shardBy" };
132
- return builder;
133
- },
134
- get shardMode() {
135
- return shardMode;
136
- },
137
- get softDeleteMode() {
138
- return softDelete;
139
- },
140
- softDelete(options) {
141
- const field = options?.field ?? "deletedAt";
142
- softDelete = { field };
143
- if (!(field in shape)) {
144
- shape[field] = v.optional(v.number().nullable());
145
- }
146
- return builder;
147
- },
148
- get triggerMap() {
149
- return triggers;
150
- },
151
- triggers(build) {
152
- Object.assign(triggers, build(triggerBuilder));
153
- return builder;
154
- },
155
- get vectorIndexes() {
156
- return vectorIndexes;
157
- },
158
- vectorize(field, options) {
159
- vectorIndexes.push({
160
- dimensions: options.dimensions,
161
- embed: options.embed,
162
- field,
163
- metadata: options.metadata,
164
- metric: options.metric,
165
- name: options.index
166
- });
167
- return builder;
168
- }
169
- };
170
- return builder;
171
- };
172
- const defineVectorIndex = (options) => {
173
- return {
174
- dimensions: options.dimensions,
175
- embed: options.embed,
176
- kind: "vectorIndex",
177
- metadata: options.metadata,
178
- metric: options.metric,
179
- select: options.source.select,
180
- table: options.source.table
181
- };
182
- };
183
- const defineAggregateIndex = (name, options) => {
184
- const op = options.op ?? "count";
185
- if (op !== "count" && !options.field) {
186
- throw new Error(`aggregateIndex "${name}": op "${op}" requires a "field"`);
187
- }
188
- return { by: options.by, field: options.field, name, on: options.on, op, where: options.where };
189
- };
190
- const defineRankIndex = (name, options) => {
191
- if (!options.sortBy || options.sortBy.length === 0) {
192
- throw new Error(`rankIndex "${name}": "sortBy" is required and must list at least one key`);
193
- }
194
- const sortBy = options.sortBy.map((key) => {
195
- return {
196
- direction: key.direction ?? "asc",
197
- field: key.field
198
- };
199
- });
200
- return { name, on: options.table, partitionBy: options.partitionBy, sortBy, where: options.where };
201
- };
202
- const withExtend = (schema) => {
203
- return {
204
- ...schema,
205
- extend(extension) {
206
- return withExtend(mergeSchemaExtension(schema, extension));
207
- },
208
- jurisdiction(_jurisdiction) {
209
- return withExtend(schema);
210
- },
211
- rls(mode) {
212
- return withExtend({ ...schema, rlsMode: mode });
213
- }
214
- };
215
- };
216
- const fillIndexTableNames = (tables) => {
217
- for (const [tableName, table] of Object.entries(tables)) {
218
- for (const index of table.aggregateIndexes) {
219
- if (index.on === "") {
220
- index.on = tableName;
221
- }
222
- }
223
- for (const index of table.rankIndexes) {
224
- if (index.on === "") {
225
- index.on = tableName;
226
- }
227
- }
228
- }
229
- };
230
- const attachStandaloneIndexes = (tables, aggregateIndexes, rankIndexes) => {
231
- for (const index of Object.values(aggregateIndexes)) {
232
- const table = tables[index.on];
233
- if (!table) {
234
- throw new Error(`defineAggregateIndex "${index.name}": unknown table "${index.on}"`);
235
- }
236
- table.aggregateIndexes.push(index);
237
- }
238
- for (const index of Object.values(rankIndexes)) {
239
- const table = tables[index.on];
240
- if (!table) {
241
- throw new Error(`defineRankIndex "${index.name}": unknown table "${index.on}"`);
242
- }
243
- table.rankIndexes.push(index);
244
- }
245
- };
246
- const defineSchema = (tables, vectorIndexes = {}, aggregateIndexes = {}, rankIndexes = {}) => {
247
- fillIndexTableNames(tables);
248
- attachStandaloneIndexes(tables, aggregateIndexes, rankIndexes);
249
- return withExtend({ tables, vectorIndexes });
250
- };
251
-
252
- export { defineAggregateIndex, defineRankIndex, defineSchema, defineTable, defineVectorIndex };
@@ -1,8 +0,0 @@
1
- const defineMigration = (definition) => {
2
- if (definition.id.trim() === "") {
3
- throw new Error("defineMigration: `id` must be a non-empty string");
4
- }
5
- return { __lunoraMigration: true, ...definition };
6
- };
7
-
8
- export { defineMigration };
@@ -1,20 +0,0 @@
1
- const defineStorageRule = (input) => {
2
- return { bucket: input.bucket, on: input.on, prefix: input.prefix, when: input.when };
3
- };
4
- const defineStorageRules = (rules) => {
5
- const seenWhenByKey = /* @__PURE__ */ new Map();
6
- for (const rule of rules) {
7
- const key = JSON.stringify([rule.bucket, rule.on, rule.prefix]);
8
- const whens = seenWhenByKey.get(key) ?? /* @__PURE__ */ new Set();
9
- if (whens.has(rule.when)) {
10
- throw new Error(
11
- `defineStorageRules: duplicate rule for (bucket "${rule.bucket}", on "${rule.on}"${rule.prefix === void 0 ? "" : `, prefix "${rule.prefix}"`}) — the same decision function is registered more than once. Multiple distinct rules per (bucket, on) are allowed (they OR); remove the duplicate.`
12
- );
13
- }
14
- whens.add(rule.when);
15
- seenWhenByKey.set(key, whens);
16
- }
17
- return rules;
18
- };
19
-
20
- export { defineStorageRule, defineStorageRules };
@@ -1,340 +0,0 @@
1
- import { parseValidatorMap, ValidationError } from '@lunora/values';
2
- import { Hono } from 'hono';
3
- import { LunoraError } from './LunoraError-DhggBJZF.mjs';
4
-
5
- const httpAction = (handler) => async (c) => handler(c.get("lunora"), c.req.raw);
6
- const httpRouter = () => {
7
- const app = new Hono();
8
- app.use("*", async (c, next) => {
9
- const injected = c.env.__lunoraCtx;
10
- if (!injected) {
11
- throw new LunoraError(
12
- "INTERNAL_SERVER_ERROR",
13
- "HttpActionCtx was not injected — mount httpRouter() on createWorker(), which supplies it per request."
14
- );
15
- }
16
- c.set("lunora", injected);
17
- await next();
18
- });
19
- return app;
20
- };
21
- const unwrapOptional = (validator) => validator.kind === "optional" ? validator._meta?.inner ?? validator : validator;
22
- const coerceScalar = (kind, raw) => {
23
- switch (kind) {
24
- case "bigint": {
25
- try {
26
- return BigInt(raw);
27
- } catch {
28
- return raw;
29
- }
30
- }
31
- case "boolean": {
32
- if (raw === "true" || raw === "1") {
33
- return true;
34
- }
35
- if (raw === "false" || raw === "0") {
36
- return false;
37
- }
38
- return raw;
39
- }
40
- case "number": {
41
- return raw === "" ? Number.NaN : Number(raw);
42
- }
43
- default: {
44
- return raw;
45
- }
46
- }
47
- };
48
- const coerceSearchParameter = (validator, c, key) => {
49
- const effective = unwrapOptional(validator);
50
- if (effective.kind === "array") {
51
- const values = c.req.queries(key);
52
- if (values === void 0) {
53
- return void 0;
54
- }
55
- const element = effective._meta?.inner;
56
- return values.map((raw2) => coerceScalar(element?.kind ?? "string", raw2));
57
- }
58
- const raw = c.req.query(key);
59
- return raw === void 0 ? void 0 : coerceScalar(effective.kind, raw);
60
- };
61
- const parseSearchParams = (validators, c) => {
62
- const raw = {};
63
- for (const key of Object.keys(validators)) {
64
- const validator = validators[key];
65
- if (!validator) {
66
- continue;
67
- }
68
- raw[key] = coerceSearchParameter(validator, c, key);
69
- }
70
- return parseValidatorMap(validators, raw, "searchParams");
71
- };
72
- const parseParams = (validators, c) => {
73
- const provided = c.req.param();
74
- const raw = {};
75
- for (const key of Object.keys(validators)) {
76
- const validator = validators[key];
77
- if (!validator) {
78
- continue;
79
- }
80
- const value = provided[key];
81
- raw[key] = value === void 0 ? void 0 : coerceScalar(unwrapOptional(validator).kind, value);
82
- }
83
- return parseValidatorMap(validators, raw, "params");
84
- };
85
- const parseBody = async (validators, c) => {
86
- let json;
87
- try {
88
- json = await c.req.json();
89
- } catch {
90
- throw new LunoraError("BAD_REQUEST", "Invalid JSON body");
91
- }
92
- if (typeof json !== "object" || json === null || Array.isArray(json)) {
93
- throw new LunoraError("BAD_REQUEST", "Expected a JSON object body");
94
- }
95
- return parseValidatorMap(validators, json, "body");
96
- };
97
- const applyOutput = (output, result) => {
98
- try {
99
- return output.parse(result);
100
- } catch (error) {
101
- if (error instanceof ValidationError) {
102
- throw new LunoraError("INTERNAL_SERVER_ERROR", `Response did not match the declared output schema: ${error.message}`);
103
- }
104
- throw error;
105
- }
106
- };
107
- const errorResponse = (error) => {
108
- if (error instanceof ValidationError) {
109
- return Response.json({ code: "BAD_REQUEST", error: error.message }, { status: 400 });
110
- }
111
- if (error instanceof LunoraError) {
112
- return Response.json({ code: error.code, error: error.message }, { status: error.status });
113
- }
114
- throw error;
115
- };
116
- const buildRouteHandler = (state, userHandler) => async (c) => {
117
- try {
118
- const context = c.get("lunora");
119
- const searchParams = Object.keys(state.searchParams).length > 0 ? parseSearchParams(state.searchParams, c) : {};
120
- const params = Object.keys(state.params).length > 0 ? parseParams(state.params, c) : {};
121
- const body = Object.keys(state.body).length > 0 ? await parseBody(state.body, c) : {};
122
- const result = await userHandler({ body, ctx: context, params, searchParams });
123
- const payload = state.output ? applyOutput(state.output, result) : result;
124
- return payload === void 0 ? new Response(null, { status: 204 }) : Response.json(payload);
125
- } catch (error) {
126
- return errorResponse(error);
127
- }
128
- };
129
- const isLunoraErrorLike = (error) => {
130
- if (!error || typeof error !== "object") {
131
- return false;
132
- }
133
- const candidate = error;
134
- return candidate.name === "LunoraError" && typeof candidate.code === "string" && typeof candidate.message === "string";
135
- };
136
- const sseFrame = (chunk, event) => {
137
- const data = JSON.stringify(chunk);
138
- const prefix = event ? `event: ${event}
139
- ` : "";
140
- return `${prefix}data: ${data}
141
-
142
- `;
143
- };
144
- const buildStreamHandler = (state, userHandler) => (
145
- // eslint-disable-next-line @typescript-eslint/require-await -- LunoraRouteHandler is contractually `(c) => Promise<Response>`; this handler returns synchronously (all awaits live inside the ReadableStream pump), so `async` is required by the type, not the body.
146
- async (c) => {
147
- let searchParams;
148
- let params;
149
- try {
150
- searchParams = Object.keys(state.searchParams).length > 0 ? parseSearchParams(state.searchParams, c) : {};
151
- params = Object.keys(state.params).length > 0 ? parseParams(state.params, c) : {};
152
- } catch (error) {
153
- return errorResponse(error);
154
- }
155
- const context = c.get("lunora");
156
- const request = c.req.raw;
157
- const encoder = new TextEncoder();
158
- const ac = new AbortController();
159
- if (request.signal.aborted) {
160
- ac.abort();
161
- return new Response(
162
- new ReadableStream({
163
- start: (controller) => {
164
- controller.close();
165
- }
166
- }),
167
- {
168
- headers: {
169
- "cache-control": "no-cache, no-transform",
170
- "content-type": "text/event-stream; charset=utf-8",
171
- "x-accel-buffering": "no"
172
- }
173
- }
174
- );
175
- }
176
- const onAbort = () => {
177
- ac.abort();
178
- };
179
- request.signal.addEventListener("abort", onAbort, { once: true });
180
- const stream = new ReadableStream({
181
- cancel() {
182
- request.signal.removeEventListener("abort", onAbort);
183
- ac.abort();
184
- },
185
- async start(controller) {
186
- try {
187
- const iterator = userHandler({ ctx: context, params, request, searchParams, signal: ac.signal });
188
- for await (const chunk of iterator) {
189
- if (ac.signal.aborted) {
190
- break;
191
- }
192
- controller.enqueue(encoder.encode(sseFrame(chunk)));
193
- }
194
- controller.enqueue(encoder.encode(sseFrame({}, "complete")));
195
- } catch (error) {
196
- let payload;
197
- if (isLunoraErrorLike(error)) {
198
- payload = { code: error.code, message: error.message };
199
- } else {
200
- console.error("[lunora] unhandled stream handler error:", error);
201
- payload = { code: "INTERNAL_SERVER_ERROR", message: "Internal error" };
202
- }
203
- controller.enqueue(encoder.encode(sseFrame(payload, "error")));
204
- } finally {
205
- request.signal.removeEventListener("abort", onAbort);
206
- controller.close();
207
- }
208
- }
209
- });
210
- return new Response(stream, {
211
- headers: {
212
- "cache-control": "no-cache, no-transform",
213
- "content-type": "text/event-stream; charset=utf-8",
214
- // Hint to proxies (including Cloudflare's own buffering layer)
215
- // that this response must not be coalesced.
216
- "x-accel-buffering": "no"
217
- }
218
- });
219
- }
220
- );
221
- const makeRouteBuilder = (state) => {
222
- return {
223
- body: (validators) => makeRouteBuilder({ ...state, body: { ...state.body, ...validators } }),
224
- handler: (userHandler) => buildRouteHandler(state, userHandler),
225
- output: (validator) => makeRouteBuilder({ ...state, output: validator }),
226
- params: (validators) => makeRouteBuilder({ ...state, params: { ...state.params, ...validators } }),
227
- searchParams: (validators) => makeRouteBuilder({ ...state, searchParams: { ...state.searchParams, ...validators } }),
228
- stream: (userHandler) => buildStreamHandler(state, userHandler)
229
- };
230
- };
231
- const makeRouteFactory = (method) => (path) => makeRouteBuilder({ body: {}, method, params: {}, path, searchParams: {} });
232
- const httpRoute = {
233
- delete: makeRouteFactory("DELETE"),
234
- get: makeRouteFactory("GET"),
235
- head: makeRouteFactory("HEAD"),
236
- options: makeRouteFactory("OPTIONS"),
237
- patch: makeRouteFactory("PATCH"),
238
- post: makeRouteFactory("POST"),
239
- put: makeRouteFactory("PUT")
240
- };
241
- const SINGLE_BYTE_RANGE_RE = /^bytes=(\d*)-(\d*)$/;
242
- const toHttpEtag = (etag) => {
243
- if (etag.startsWith('"') || etag.startsWith('W/"')) {
244
- return etag;
245
- }
246
- return `"${etag}"`;
247
- };
248
- const isSafeHeaderValue = (value) => {
249
- for (let index = 0; index < value.length; index += 1) {
250
- const code = value.codePointAt(index);
251
- if (code === 13 || code === 10 || code === 0) {
252
- return false;
253
- }
254
- }
255
- return true;
256
- };
257
- const parseRange = (header, size) => {
258
- if (header === null) {
259
- return { kind: "full" };
260
- }
261
- const match = SINGLE_BYTE_RANGE_RE.exec(header.trim());
262
- if (!match) {
263
- return { kind: "full" };
264
- }
265
- const startRaw = match[1] ?? "";
266
- const endRaw = match[2] ?? "";
267
- if (startRaw === "" && endRaw === "") {
268
- return { kind: "full" };
269
- }
270
- let start;
271
- let end;
272
- if (startRaw === "") {
273
- const suffix = Number(endRaw);
274
- if (suffix === 0) {
275
- return { kind: "unsatisfiable" };
276
- }
277
- start = Math.max(0, size - suffix);
278
- end = size - 1;
279
- } else {
280
- start = Number(startRaw);
281
- end = endRaw === "" ? size - 1 : Math.min(Number(endRaw), size - 1);
282
- }
283
- if (start > end || start >= size) {
284
- return { kind: "unsatisfiable" };
285
- }
286
- return { end, kind: "partial", start };
287
- };
288
- const serveStorageObject = async (context, key, request) => {
289
- const object = await context.storage.download(key);
290
- if (!object) {
291
- return new Response("Not Found", { status: 404 });
292
- }
293
- const rawContentType = object.httpMetadata?.contentType;
294
- const contentType = rawContentType !== void 0 && isSafeHeaderValue(rawContentType) ? rawContentType : "application/octet-stream";
295
- const baseHeaders = {
296
- "accept-ranges": "bytes",
297
- "content-type": contentType,
298
- etag: toHttpEtag(object.etag)
299
- };
300
- if (object.sha256Base64 !== void 0) {
301
- baseHeaders["repr-digest"] = `sha-256=:${object.sha256Base64}:`;
302
- }
303
- const range = parseRange(request.headers.get("range"), object.size);
304
- if (range.kind === "unsatisfiable") {
305
- object.body?.cancel().catch(() => {
306
- });
307
- return new Response("Range Not Satisfiable", {
308
- headers: {
309
- "accept-ranges": "bytes",
310
- "content-range": `bytes */${String(object.size)}`,
311
- "content-type": "text/plain; charset=utf-8",
312
- etag: toHttpEtag(object.etag)
313
- },
314
- status: 416
315
- });
316
- }
317
- if (range.kind === "full") {
318
- return new Response(object.body, {
319
- headers: { ...baseHeaders, "content-length": String(object.size) },
320
- status: 200
321
- });
322
- }
323
- object.body?.cancel().catch(() => {
324
- });
325
- const length = range.end - range.start + 1;
326
- const slice = await context.storage.download(key, { range: { length, offset: range.start } });
327
- if (!slice) {
328
- return new Response("Not Found", { status: 404 });
329
- }
330
- return new Response(slice.body, {
331
- headers: {
332
- ...baseHeaders,
333
- "content-length": String(length),
334
- "content-range": `bytes ${String(range.start)}-${String(range.end)}/${String(object.size)}`
335
- },
336
- status: 206
337
- });
338
- };
339
-
340
- export { httpAction, httpRoute, httpRouter, serveStorageObject };