@lunora/runtime 0.0.0 → 1.0.0-alpha.2

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.
@@ -0,0 +1,233 @@
1
+ const DEFAULT_CSP = "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'";
2
+ const DEFAULT_PERMISSIONS_POLICY = "accelerometer=(), autoplay=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()";
3
+ const DEFAULT_CORS_HEADERS = ["Authorization", "Content-Type", "X-D1-Bookmark", "X-Lunora-Mutation-Id"];
4
+ const DEFAULT_CORS_METHODS = ["DELETE", "GET", "HEAD", "PATCH", "POST", "PUT"];
5
+ const ONE_YEAR_SECONDS = 31536e3;
6
+ const SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
7
+ const resolveHstsHeader = (hsts) => {
8
+ if (hsts === false) {
9
+ return void 0;
10
+ }
11
+ const config = hsts === void 0 || hsts === true ? {} : hsts;
12
+ const maxAge = config.maxAge ?? ONE_YEAR_SECONDS;
13
+ const includeSubDomains = config.includeSubDomains ?? true;
14
+ return `max-age=${String(maxAge)}${includeSubDomains ? "; includeSubDomains" : ""}${config.preload ? "; preload" : ""}`;
15
+ };
16
+ const resolveCspHeader = (csp) => {
17
+ if (csp === false) {
18
+ return void 0;
19
+ }
20
+ if (typeof csp === "string") {
21
+ return { htmlToo: true, value: csp };
22
+ }
23
+ return { htmlToo: false, value: DEFAULT_CSP };
24
+ };
25
+ const resolveHeaders = (input) => {
26
+ if (input === false) {
27
+ return {
28
+ coop: void 0,
29
+ csp: void 0,
30
+ enabled: false,
31
+ frameOptions: void 0,
32
+ hsts: void 0,
33
+ permissionsPolicy: void 0,
34
+ referrerPolicy: void 0
35
+ };
36
+ }
37
+ const options = input === void 0 || input === true ? {} : input;
38
+ return {
39
+ coop: "same-origin",
40
+ csp: resolveCspHeader(options.csp),
41
+ enabled: true,
42
+ frameOptions: options.frameOptions === false ? void 0 : options.frameOptions ?? "SAMEORIGIN",
43
+ hsts: resolveHstsHeader(options.hsts),
44
+ permissionsPolicy: options.permissionsPolicy === false ? void 0 : options.permissionsPolicy ?? DEFAULT_PERMISSIONS_POLICY,
45
+ referrerPolicy: options.referrerPolicy === false ? void 0 : options.referrerPolicy ?? "strict-origin-when-cross-origin"
46
+ };
47
+ };
48
+ const resolveCors = (input) => {
49
+ const disabled = {
50
+ allowCredentials: false,
51
+ allowedHeaders: DEFAULT_CORS_HEADERS,
52
+ allowedMethods: DEFAULT_CORS_METHODS,
53
+ enabled: false,
54
+ isAllowed: () => false,
55
+ isExplicitlyAllowed: () => false,
56
+ maxAge: 600
57
+ };
58
+ if (input === void 0 || input === false) {
59
+ return disabled;
60
+ }
61
+ const allowCredentials = input.allowCredentials ?? false;
62
+ const origins = input.allowedOrigins;
63
+ let isAllowed;
64
+ let isExplicitlyAllowed;
65
+ if (typeof origins === "function") {
66
+ isAllowed = origins;
67
+ isExplicitlyAllowed = origins;
68
+ } else {
69
+ const originsList = origins;
70
+ if (originsList.includes("*") && allowCredentials) {
71
+ throw new Error(
72
+ '@lunora/runtime: security.cors cannot combine a wildcard origin ("*") with allowCredentials: true — browsers reject it and it defeats the allowlist.'
73
+ );
74
+ }
75
+ isAllowed = (origin) => originsList.includes("*") || originsList.includes(origin);
76
+ isExplicitlyAllowed = (origin) => originsList.includes(origin);
77
+ }
78
+ return {
79
+ allowCredentials,
80
+ allowedHeaders: input.allowedHeaders ?? DEFAULT_CORS_HEADERS,
81
+ allowedMethods: input.allowedMethods ?? DEFAULT_CORS_METHODS,
82
+ enabled: true,
83
+ isAllowed,
84
+ isExplicitlyAllowed,
85
+ maxAge: input.maxAge ?? 600
86
+ };
87
+ };
88
+ const resolveCsrf = (input) => {
89
+ if (input === false) {
90
+ return { enabled: false, trustedOrigins: [] };
91
+ }
92
+ const options = input === void 0 || input === true ? {} : input;
93
+ return { enabled: true, trustedOrigins: options.trustedOrigins ?? [] };
94
+ };
95
+ const DISABLED_ENV_VALUES = /* @__PURE__ */ new Set(["0", "disabled", "false", "no", "off"]);
96
+ const ENABLED_ENV_VALUES = /* @__PURE__ */ new Set(["1", "enabled", "on", "true", "yes"]);
97
+ const isEnvDisabled = (value) => typeof value === "string" && DISABLED_ENV_VALUES.has(value.trim().toLowerCase());
98
+ const isEnvEnabled = (value) => typeof value === "string" && ENABLED_ENV_VALUES.has(value.trim().toLowerCase());
99
+ const parseEnvCors = (env) => {
100
+ const raw = env?.["LUNORA_ALLOWED_ORIGINS"];
101
+ if (typeof raw !== "string") {
102
+ return void 0;
103
+ }
104
+ const allowedOrigins = raw.split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
105
+ if (allowedOrigins.length === 0) {
106
+ return void 0;
107
+ }
108
+ const wildcard = allowedOrigins.includes("*");
109
+ const allowCredentials = !wildcard && isEnvEnabled(env?.["LUNORA_CORS_ALLOW_CREDENTIALS"]);
110
+ return { allowCredentials, allowedOrigins };
111
+ };
112
+ const resolveSecurity = (security, env) => {
113
+ const headers = security?.headers ?? (isEnvDisabled(env?.["LUNORA_SECURITY_HEADERS"]) ? false : void 0);
114
+ const csrf = security?.csrf ?? (isEnvDisabled(env?.["LUNORA_SECURITY_CSRF"]) ? false : void 0);
115
+ const cors = security?.cors ?? parseEnvCors(env);
116
+ return {
117
+ cors: resolveCors(cors),
118
+ csrf: resolveCsrf(csrf),
119
+ headers: resolveHeaders(headers)
120
+ };
121
+ };
122
+ const originOf = (value) => {
123
+ if (!value) {
124
+ return void 0;
125
+ }
126
+ try {
127
+ return new URL(value).origin;
128
+ } catch {
129
+ return void 0;
130
+ }
131
+ };
132
+ const isTrustedOrigin = (origin, selfOrigin, resolved) => {
133
+ if (origin === selfOrigin) {
134
+ return true;
135
+ }
136
+ if (resolved.csrf.trustedOrigins.includes(origin)) {
137
+ return true;
138
+ }
139
+ return resolved.cors.enabled && resolved.cors.isExplicitlyAllowed(origin);
140
+ };
141
+ const enforceOrigin = (request, resolved) => {
142
+ if (!resolved.csrf.enabled || SAFE_METHODS.has(request.method) || !request.headers.get("cookie")) {
143
+ return void 0;
144
+ }
145
+ const selfOrigin = new URL(request.url).origin;
146
+ const source = originOf(request.headers.get("origin")) ?? originOf(request.headers.get("referer"));
147
+ if (source !== void 0 && isTrustedOrigin(source, selfOrigin, resolved)) {
148
+ return void 0;
149
+ }
150
+ return Response.json(
151
+ { error: { code: "FORBIDDEN_ORIGIN", message: "cross-origin state-changing request rejected" } },
152
+ { headers: { "content-type": "application/json" }, status: 403 }
153
+ );
154
+ };
155
+ const corsResponseHeaders = (origin, cors) => {
156
+ const headers = new Headers();
157
+ headers.set("access-control-allow-origin", origin);
158
+ headers.append("vary", "Origin");
159
+ if (cors.allowCredentials) {
160
+ headers.set("access-control-allow-credentials", "true");
161
+ }
162
+ return headers;
163
+ };
164
+ const handleCorsPreflight = (request, resolved) => {
165
+ if (!resolved.cors.enabled || request.method !== "OPTIONS") {
166
+ return void 0;
167
+ }
168
+ const origin = request.headers.get("origin");
169
+ if (!origin || !request.headers.get("access-control-request-method") || !resolved.cors.isAllowed(origin)) {
170
+ return void 0;
171
+ }
172
+ const headers = corsResponseHeaders(origin, resolved.cors);
173
+ const requested = request.headers.get("access-control-request-headers");
174
+ headers.set("access-control-allow-methods", resolved.cors.allowedMethods.join(", "));
175
+ headers.set("access-control-allow-headers", requested ?? resolved.cors.allowedHeaders.join(", "));
176
+ headers.set("access-control-max-age", String(resolved.cors.maxAge));
177
+ return new Response(null, { headers, status: 204 });
178
+ };
179
+ const isHtmlResponse = (response) => (response.headers.get("content-type") ?? "").toLowerCase().includes("text/html");
180
+ const setIfAbsent = (headers, name, value) => {
181
+ if (!headers.has(name)) {
182
+ headers.set(name, value);
183
+ }
184
+ };
185
+ const applyBaselineHeaders = (headers, request, response, config) => {
186
+ if (config.hsts !== void 0 && new URL(request.url).protocol === "https:") {
187
+ setIfAbsent(headers, "strict-transport-security", config.hsts);
188
+ }
189
+ setIfAbsent(headers, "x-content-type-options", "nosniff");
190
+ if (config.frameOptions !== void 0) {
191
+ setIfAbsent(headers, "x-frame-options", config.frameOptions);
192
+ }
193
+ if (config.referrerPolicy !== void 0) {
194
+ setIfAbsent(headers, "referrer-policy", config.referrerPolicy);
195
+ }
196
+ if (config.permissionsPolicy !== void 0) {
197
+ setIfAbsent(headers, "permissions-policy", config.permissionsPolicy);
198
+ }
199
+ if (config.coop !== void 0) {
200
+ setIfAbsent(headers, "cross-origin-opener-policy", config.coop);
201
+ }
202
+ if (config.csp !== void 0 && (config.csp.htmlToo || !isHtmlResponse(response))) {
203
+ setIfAbsent(headers, "content-security-policy", config.csp.value);
204
+ }
205
+ };
206
+ const applyCorsHeaders = (headers, request, cors) => {
207
+ const origin = request.headers.get("origin");
208
+ if (!origin || !cors.isAllowed(origin)) {
209
+ return;
210
+ }
211
+ for (const [name, value] of corsResponseHeaders(origin, cors).entries()) {
212
+ if (name === "vary") {
213
+ headers.append("vary", value);
214
+ } else {
215
+ setIfAbsent(headers, name, value);
216
+ }
217
+ }
218
+ };
219
+ const decorateResponse = (response, request, resolved) => {
220
+ if (response.status === 101 || response.webSocket) {
221
+ return response;
222
+ }
223
+ const headers = new Headers(response.headers);
224
+ if (resolved.headers.enabled) {
225
+ applyBaselineHeaders(headers, request, response, resolved.headers);
226
+ }
227
+ if (resolved.cors.enabled) {
228
+ applyCorsHeaders(headers, request, resolved.cors);
229
+ }
230
+ return new Response(response.body, { headers, status: response.status, statusText: response.statusText });
231
+ };
232
+
233
+ export { decorateResponse, enforceOrigin, handleCorsPreflight, resolveSecurity };
@@ -0,0 +1,20 @@
1
+ const emitRpcEvent = (sink, event, context) => {
2
+ if (!sink?.onRpc) {
3
+ return;
4
+ }
5
+ try {
6
+ sink.onRpc(event, context);
7
+ } catch {
8
+ }
9
+ };
10
+ const emitLogEvent = (sink, event, context) => {
11
+ if (!sink?.onLog) {
12
+ return;
13
+ }
14
+ try {
15
+ sink.onLog(event, context);
16
+ } catch {
17
+ }
18
+ };
19
+
20
+ export { emitLogEvent, emitRpcEvent };
@@ -0,0 +1,9 @@
1
+ const resolveShard = (namespace, shardKey) => {
2
+ if (typeof namespace.getByName === "function") {
3
+ return namespace.getByName(shardKey);
4
+ }
5
+ const id = namespace.idFromName(shardKey);
6
+ return namespace.get(id);
7
+ };
8
+
9
+ export { resolveShard };
@@ -0,0 +1,39 @@
1
+ const DEFAULT_PRIMARY_KEY = "_id";
2
+ const toFivetranResponse = (page, primaryKey = DEFAULT_PRIMARY_KEY) => {
3
+ const insert = {};
4
+ const update = {};
5
+ const remove = {};
6
+ const schema = {};
7
+ const pkFor = (table) => typeof primaryKey === "string" ? primaryKey : primaryKey[table] ?? DEFAULT_PRIMARY_KEY;
8
+ const bucketFor = (target, table) => {
9
+ const existing = target[table];
10
+ if (existing) {
11
+ return existing;
12
+ }
13
+ const created = [];
14
+ target[table] = created;
15
+ return created;
16
+ };
17
+ for (const change of page.changes) {
18
+ schema[change.table] ??= { primary_key: [pkFor(change.table)] };
19
+ if (change.op === "delete") {
20
+ bucketFor(remove, change.table).push(change.doc);
21
+ } else if (change.op === "update") {
22
+ bucketFor(update, change.table).push(change.doc);
23
+ } else {
24
+ bucketFor(insert, change.table).push(change.doc);
25
+ }
26
+ }
27
+ return { delete: remove, hasMore: page.hasMore, insert, schema, state: { cursor: page.nextCursor }, update };
28
+ };
29
+ const toAirbyteMessages = (page, emittedAt = Date.now()) => {
30
+ const messages = [];
31
+ for (const change of page.changes) {
32
+ const data = change.op === "delete" ? { ...change.doc, _lunora_deleted: true } : change.doc;
33
+ messages.push({ record: { data, emitted_at: emittedAt, stream: change.table }, type: "RECORD" });
34
+ }
35
+ messages.push({ state: { data: { cursor: page.nextCursor } }, type: "STATE" });
36
+ return messages;
37
+ };
38
+
39
+ export { toAirbyteMessages, toFivetranResponse };
package/package.json CHANGED
@@ -1,31 +1,51 @@
1
1
  {
2
2
  "name": "@lunora/runtime",
3
- "version": "0.0.0",
3
+ "version": "1.0.0-alpha.2",
4
4
  "description": "Lunora Worker runtime: the RPC router, shard resolver, and query coordinator",
5
- "license": "FSL-1.1-Apache-2.0",
5
+ "keywords": [
6
+ "cloudflare",
7
+ "durable-objects",
8
+ "lunora",
9
+ "query-coordinator",
10
+ "rpc",
11
+ "runtime",
12
+ "sharding",
13
+ "workers"
14
+ ],
6
15
  "homepage": "https://lunora.sh",
16
+ "bugs": "https://github.com/anolilab/lunora/issues",
17
+ "license": "FSL-1.1-Apache-2.0",
18
+ "author": {
19
+ "name": "Daniel Bannert",
20
+ "email": "d.bannert@anolilab.de"
21
+ },
7
22
  "repository": {
8
23
  "type": "git",
9
24
  "url": "git+https://github.com/anolilab/lunora.git",
10
25
  "directory": "packages/runtime"
11
26
  },
12
- "bugs": {
13
- "url": "https://github.com/anolilab/lunora/issues"
14
- },
15
- "keywords": [
16
- "lunora",
17
- "cloudflare",
18
- "workers",
19
- "durable-objects",
20
- "rpc",
21
- "sharding",
22
- "query-coordinator",
23
- "runtime"
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE.md",
31
+ "__assets__"
24
32
  ],
33
+ "type": "module",
34
+ "sideEffects": false,
35
+ "main": "./dist/index.mjs",
36
+ "module": "./dist/index.mjs",
37
+ "types": "./dist/index.d.ts",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.ts",
41
+ "import": "./dist/index.mjs"
42
+ },
43
+ "./package.json": "./package.json"
44
+ },
25
45
  "publishConfig": {
26
46
  "access": "public"
27
47
  },
28
- "files": [
29
- "README.md"
30
- ]
48
+ "engines": {
49
+ "node": "^22.15.0 || >=24.11.0"
50
+ }
31
51
  }