@lunora/config 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.
- package/LICENSE.md +105 -0
- package/README.md +115 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/index.d.mts +1075 -0
- package/dist/index.d.ts +1075 -0
- package/dist/index.mjs +20 -0
- package/dist/packem_shared/AGENT_RULES_DIR-lcgC08aE.mjs +40 -0
- package/dist/packem_shared/DEV_VARS_EXAMPLE_FILE-dJPNTEnK.mjs +37 -0
- package/dist/packem_shared/LINKED_PROJECT_DIR-CXwXzV_C.mjs +52 -0
- package/dist/packem_shared/PACKAGE_SECRETS_REGISTRY-CySy5vR_.mjs +62 -0
- package/dist/packem_shared/REQUIRED_COMPATIBILITY_DATE-Dd1suoit.mjs +476 -0
- package/dist/packem_shared/applyAdditiveEdit-C-snTFEV.mjs +228 -0
- package/dist/packem_shared/buildPackageSecretsBlock-S74dgmwy.mjs +187 -0
- package/dist/packem_shared/classifyPolicyEdit-BHeAqF8P.mjs +99 -0
- package/dist/packem_shared/createConfirm-fvpdgJ9s.mjs +100 -0
- package/dist/packem_shared/detectFramework-Br-BcPBq.mjs +41 -0
- package/dist/packem_shared/discoverContainerInfo-BXFs6Wav.mjs +19 -0
- package/dist/packem_shared/discoverSchemaInfo-DWtypqpP.mjs +25 -0
- package/dist/packem_shared/discoverWorkflowInfo-CedvR0mn.mjs +19 -0
- package/dist/packem_shared/findWranglerFile-DwSuC-Kn.mjs +25 -0
- package/dist/packem_shared/formatLunoraEvent-D2fDeGB6.mjs +86 -0
- package/dist/packem_shared/handlePolicyScaffoldRequest-CiC2IGKx.mjs +103 -0
- package/dist/packem_shared/handleSchemaEditRequest-Df-Wrix-.mjs +99 -0
- package/dist/packem_shared/handleSeedRequest-DVCjaGO-.mjs +61 -0
- package/dist/packem_shared/inferLunoraBindings-0W3eRdIP.mjs +302 -0
- package/dist/packem_shared/injectRemoteFlags-C-WZAKLY.mjs +105 -0
- package/dist/packem_shared/interpretRemote-CtcIcB5-.mjs +34 -0
- package/dist/packem_shared/parseDevVariable-CJiq2IwE.mjs +30 -0
- package/dist/packem_shared/parseSchema-DSeyktvG.mjs +107 -0
- package/dist/packem_shared/policy-scaffold.d-DCmwn7zQ.d.mts +74 -0
- package/dist/packem_shared/policy-scaffold.d-DCmwn7zQ.d.ts +74 -0
- package/dist/packem_shared/reconcileWranglerBindings-ByJk3yLU.mjs +277 -0
- package/dist/packem_shared/renderStudioHtml-449Ysn75.mjs +37 -0
- package/dist/packem_shared/serveJsonHandler-B4OLTGLS.mjs +86 -0
- package/dist/packem_shared/studioAssetsStamp-Csk5RS4E.mjs +28 -0
- package/dist/studio-host/index.d.mts +227 -0
- package/dist/studio-host/index.d.ts +227 -0
- package/dist/studio-host/index.mjs +7 -0
- package/package.json +57 -17
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { existsSync, statSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { init, parse } from 'es-module-lexer';
|
|
3
|
+
import { discoverContainerInfo } from './discoverContainerInfo-BXFs6Wav.mjs';
|
|
4
|
+
import { discoverSchemaInfo } from './discoverSchemaInfo-DWtypqpP.mjs';
|
|
5
|
+
import { discoverWorkflowInfo } from './discoverWorkflowInfo-CedvR0mn.mjs';
|
|
6
|
+
import { WRANGLER_FILES, readWranglerJsonc } from './findWranglerFile-DwSuC-Kn.mjs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".cjs", ".cts", ".js", ".jsx", ".mjs", ".mts", ".ts", ".tsx"]);
|
|
10
|
+
const IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([".git", ".lunora-cache", ".wrangler", "_generated", "dist", "node_modules"]);
|
|
11
|
+
const DEFAULT_SCAN_DIRECTORIES = ["lunora", "src"];
|
|
12
|
+
const WORKER_ENTRY_FALLBACKS = ["src/server/index.ts", "src/server/index.tsx", "src/index.ts", "src/worker.ts"];
|
|
13
|
+
const DURABLE_OBJECT_BINDINGS = {
|
|
14
|
+
SchedulerDO: "SCHEDULER",
|
|
15
|
+
SessionDO: "SESSION",
|
|
16
|
+
ShardDO: "SHARD"
|
|
17
|
+
};
|
|
18
|
+
const DURABLE_OBJECT_CLASSES = Object.keys(DURABLE_OBJECT_BINDINGS);
|
|
19
|
+
const TYPE_ONLY_EXPORT_PATTERNS = {
|
|
20
|
+
SchedulerDO: /\btype\s+SchedulerDO\b/,
|
|
21
|
+
SessionDO: /\btype\s+SessionDO\b/,
|
|
22
|
+
ShardDO: /\btype\s+ShardDO\b/
|
|
23
|
+
};
|
|
24
|
+
const ENV_DB_PATTERN = /\benv\s*\.\s*DB\b/;
|
|
25
|
+
const ENV_AI_PATTERN = /\benv\s*\.\s*AI\b/;
|
|
26
|
+
const TYPE_ONLY_IMPORT_PATTERN = /^\s*import\s+type\b/;
|
|
27
|
+
const CAPABILITY_SOURCES = {
|
|
28
|
+
usesAi: { pattern: /\bfrom\s+["']@lunora\/ai["']/, source: "@lunora/ai" },
|
|
29
|
+
usesAnalytics: { pattern: /\bfrom\s+["']@lunora\/analytics["']/, source: "@lunora/analytics" },
|
|
30
|
+
usesAuth: { pattern: /\bfrom\s+["']@lunora\/auth["']/, source: "@lunora/auth" },
|
|
31
|
+
usesBrowser: { pattern: /\bfrom\s+["']@lunora\/browser["']/, source: "@lunora/browser" },
|
|
32
|
+
usesHyperdrive: { pattern: /\bfrom\s+["']@lunora\/hyperdrive["']/, source: "@lunora/hyperdrive" },
|
|
33
|
+
usesImages: { pattern: /\bfrom\s+["']@lunora\/images["']/, source: "@lunora/images" },
|
|
34
|
+
usesKv: { pattern: /\bfrom\s+["']@lunora\/kv["']/, source: "@lunora/kv" },
|
|
35
|
+
usesMail: { pattern: /\bfrom\s+["']@lunora\/mail["']/, source: "@lunora/mail" },
|
|
36
|
+
usesPayment: { pattern: /\bfrom\s+["']@lunora\/payment["']/, source: "@lunora/payment" },
|
|
37
|
+
usesPipelines: { pattern: /\bfrom\s+["']@lunora\/pipelines["']/, source: "@lunora/pipelines" },
|
|
38
|
+
usesScheduler: { pattern: /\bfrom\s+["']@lunora\/scheduler["']/, source: "@lunora/scheduler" },
|
|
39
|
+
usesStorage: { pattern: /\bfrom\s+["']@lunora\/storage["']/, source: "@lunora/storage" }
|
|
40
|
+
};
|
|
41
|
+
const CAPABILITY_FLAGS = Object.keys(CAPABILITY_SOURCES);
|
|
42
|
+
const PAYMENT_PROVIDER_SECRETS = "STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET (Stripe) or POLAR_ACCESS_TOKEN + POLAR_WEBHOOK_SECRET (Polar)";
|
|
43
|
+
const ALL_CAPABILITY_KEYS = [...CAPABILITY_FLAGS, "needsD1"];
|
|
44
|
+
const emptyCapabilities = () => {
|
|
45
|
+
const base = {};
|
|
46
|
+
for (const key of ALL_CAPABILITY_KEYS) {
|
|
47
|
+
base[key] = false;
|
|
48
|
+
}
|
|
49
|
+
return base;
|
|
50
|
+
};
|
|
51
|
+
const NO_CAPABILITIES = Object.freeze(emptyCapabilities());
|
|
52
|
+
const mergeCapabilities = (a, b) => {
|
|
53
|
+
const merged = {};
|
|
54
|
+
for (const key of ALL_CAPABILITY_KEYS) {
|
|
55
|
+
merged[key] = a[key] || b[key];
|
|
56
|
+
}
|
|
57
|
+
return merged;
|
|
58
|
+
};
|
|
59
|
+
const capabilityForImportSource = (source) => {
|
|
60
|
+
for (const flag of CAPABILITY_FLAGS) {
|
|
61
|
+
if (CAPABILITY_SOURCES[flag].source === source) {
|
|
62
|
+
return { ...NO_CAPABILITIES, [flag]: true };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return NO_CAPABILITIES;
|
|
66
|
+
};
|
|
67
|
+
const lexCapabilities = (code) => {
|
|
68
|
+
const [imports] = parse(code);
|
|
69
|
+
let capabilities = NO_CAPABILITIES;
|
|
70
|
+
for (const entry of imports) {
|
|
71
|
+
const source = entry.n;
|
|
72
|
+
if (!source || TYPE_ONLY_IMPORT_PATTERN.test(code.slice(entry.ss, entry.se))) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
capabilities = mergeCapabilities(capabilities, capabilityForImportSource(source));
|
|
76
|
+
}
|
|
77
|
+
return capabilities;
|
|
78
|
+
};
|
|
79
|
+
const regexCapabilities = (code) => {
|
|
80
|
+
const capabilities = { ...NO_CAPABILITIES };
|
|
81
|
+
for (const flag of CAPABILITY_FLAGS) {
|
|
82
|
+
capabilities[flag] = CAPABILITY_SOURCES[flag].pattern.test(code);
|
|
83
|
+
}
|
|
84
|
+
return capabilities;
|
|
85
|
+
};
|
|
86
|
+
const capabilitiesFromSource = (code) => {
|
|
87
|
+
let capabilities;
|
|
88
|
+
try {
|
|
89
|
+
capabilities = lexCapabilities(code);
|
|
90
|
+
} catch {
|
|
91
|
+
capabilities = regexCapabilities(code);
|
|
92
|
+
}
|
|
93
|
+
return mergeCapabilities(capabilities, { ...NO_CAPABILITIES, needsD1: ENV_DB_PATTERN.test(code), usesAi: ENV_AI_PATTERN.test(code) });
|
|
94
|
+
};
|
|
95
|
+
const collectSourceFiles = (directory, accumulator) => {
|
|
96
|
+
let entries;
|
|
97
|
+
try {
|
|
98
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
99
|
+
} catch {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (entry.isDirectory()) {
|
|
104
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
105
|
+
collectSourceFiles(join(directory, entry.name), accumulator);
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const dotIndex = entry.name.lastIndexOf(".");
|
|
110
|
+
if (dotIndex !== -1 && SOURCE_EXTENSIONS.has(entry.name.slice(dotIndex))) {
|
|
111
|
+
accumulator.push(join(directory, entry.name));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const resolveWorkerEntry = (projectRoot) => {
|
|
116
|
+
for (const candidate of WRANGLER_FILES) {
|
|
117
|
+
const wranglerPath = join(projectRoot, candidate);
|
|
118
|
+
if (!existsSync(wranglerPath)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const { parsed } = readWranglerJsonc(wranglerPath);
|
|
122
|
+
const main = parsed?.main;
|
|
123
|
+
if (typeof main === "string" && existsSync(join(projectRoot, main))) {
|
|
124
|
+
return join(projectRoot, main);
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
for (const fallback of WORKER_ENTRY_FALLBACKS) {
|
|
129
|
+
const fullPath = join(projectRoot, fallback);
|
|
130
|
+
if (existsSync(fullPath)) {
|
|
131
|
+
return fullPath;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return void 0;
|
|
135
|
+
};
|
|
136
|
+
const detectExportedDurableObjects = (entryPath) => {
|
|
137
|
+
const code = readFileSync(entryPath, "utf8");
|
|
138
|
+
let exportedNames;
|
|
139
|
+
try {
|
|
140
|
+
const [, exports$1] = parse(code);
|
|
141
|
+
exportedNames = new Set(exports$1.map((entry) => entry.n));
|
|
142
|
+
} catch {
|
|
143
|
+
exportedNames = new Set(DURABLE_OBJECT_CLASSES.filter((className) => new RegExp(String.raw`\bexport\b[^\n;]*\b${className}\b`).test(code)));
|
|
144
|
+
}
|
|
145
|
+
return DURABLE_OBJECT_CLASSES.filter((className) => exportedNames.has(className) && !TYPE_ONLY_EXPORT_PATTERNS[className].test(code)).map((className) => {
|
|
146
|
+
return {
|
|
147
|
+
binding: DURABLE_OBJECT_BINDINGS[className],
|
|
148
|
+
className
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
const CONTAINERS_STAR_REEXPORT_PATTERN = /\bexport\s*\*\s*from\s*["'][^"']*_generated\/containers(?:\.js)?["']/;
|
|
153
|
+
const detectContainerExports = (entryPath, containers) => {
|
|
154
|
+
if (containers.length === 0) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
if (entryPath === void 0) {
|
|
158
|
+
return containers.map((container) => {
|
|
159
|
+
return { ...container, exported: false };
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const code = readFileSync(entryPath, "utf8");
|
|
163
|
+
const starReexport = CONTAINERS_STAR_REEXPORT_PATTERN.test(code);
|
|
164
|
+
let exportedNames;
|
|
165
|
+
try {
|
|
166
|
+
const [, exports$1] = parse(code);
|
|
167
|
+
exportedNames = new Set(exports$1.map((entry) => entry.n));
|
|
168
|
+
} catch {
|
|
169
|
+
exportedNames = new Set(
|
|
170
|
+
containers.map((container) => container.className).filter((className) => new RegExp(String.raw`\bexport\b[^\n;]*\b${className}\b`).test(code))
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return containers.map((container) => {
|
|
174
|
+
return { ...container, exported: starReexport || exportedNames.has(container.className) };
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
const WORKFLOWS_STAR_REEXPORT_PATTERN = /\bexport\s*\*\s*from\s*["'][^"']*_generated\/workflows(?:\.js)?["']/;
|
|
178
|
+
const detectWorkflowExports = (entryPath, workflows) => {
|
|
179
|
+
if (workflows.length === 0) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
if (entryPath === void 0) {
|
|
183
|
+
return workflows.map((workflow) => {
|
|
184
|
+
return { ...workflow, exported: false };
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const code = readFileSync(entryPath, "utf8");
|
|
188
|
+
const starReexport = WORKFLOWS_STAR_REEXPORT_PATTERN.test(code);
|
|
189
|
+
let exportedNames;
|
|
190
|
+
try {
|
|
191
|
+
const [, exports$1] = parse(code);
|
|
192
|
+
exportedNames = new Set(exports$1.map((entry) => entry.n));
|
|
193
|
+
} catch {
|
|
194
|
+
exportedNames = new Set(
|
|
195
|
+
workflows.map((workflow) => workflow.className).filter((className) => new RegExp(String.raw`\bexport\b[^\n;]*\b${className}\b`).test(code))
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return workflows.map((workflow) => {
|
|
199
|
+
return { ...workflow, exported: starReexport || exportedNames.has(workflow.className) };
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
const schemaNeedsD1 = (projectRoot, schemaDirectory) => discoverSchemaInfo(projectRoot, schemaDirectory).info?.hasGlobalTable ?? false;
|
|
203
|
+
const scanCapabilities = (projectRoot, scanDirectories) => {
|
|
204
|
+
let merged = NO_CAPABILITIES;
|
|
205
|
+
for (const relativeDirectory of scanDirectories) {
|
|
206
|
+
const absolute = join(projectRoot, relativeDirectory);
|
|
207
|
+
if (!existsSync(absolute) || !statSync(absolute).isDirectory()) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const files = [];
|
|
211
|
+
collectSourceFiles(absolute, files);
|
|
212
|
+
for (const file of files) {
|
|
213
|
+
merged = mergeCapabilities(merged, capabilitiesFromSource(readFileSync(file, "utf8")));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return merged;
|
|
217
|
+
};
|
|
218
|
+
const describeDeclaredExports = (containers, workflows) => [
|
|
219
|
+
...containers.map(
|
|
220
|
+
(container) => container.exported ? `${container.bindingName}/${container.className} (container "${container.exportName}" declared and exported)` : `hint: container "${container.exportName}" is declared but ${container.className} is not exported by the worker entry — add \`export * from "./lunora/_generated/containers"\``
|
|
221
|
+
),
|
|
222
|
+
...workflows.map(
|
|
223
|
+
(workflow) => workflow.exported ? `${workflow.bindingName}/${workflow.className} (workflow "${workflow.exportName}" declared and exported)` : `hint: workflow "${workflow.exportName}" is declared but ${workflow.className} is not exported by the worker entry — add \`export * from "./lunora/_generated/workflows"\``
|
|
224
|
+
)
|
|
225
|
+
];
|
|
226
|
+
const describeCapabilitySignals = (capabilities, exported) => {
|
|
227
|
+
const rules = [
|
|
228
|
+
[capabilities.usesAi, "AI (@lunora/ai imported or env.AI used)"],
|
|
229
|
+
[
|
|
230
|
+
capabilities.usesAuth && !exported.has("SessionDO"),
|
|
231
|
+
"hint: @lunora/auth is imported but no SessionDO is exported (sessions are D1-backed, or export SessionDO for DO-backed sessions)"
|
|
232
|
+
],
|
|
233
|
+
[capabilities.usesScheduler && !exported.has("SchedulerDO"), "hint: @lunora/scheduler is imported but no SchedulerDO is exported by the worker entry"],
|
|
234
|
+
[capabilities.usesStorage, "hint: @lunora/storage is imported; add an r2_buckets binding (bucket binding names are user-defined)"],
|
|
235
|
+
[capabilities.usesMail, "hint: @lunora/mail is imported; set RESEND_API_KEY in .dev.vars (obtain at https://resend.com/api-keys)"],
|
|
236
|
+
[capabilities.usesPayment, `hint: @lunora/payment is imported; set the provider secrets in .dev.vars — ${PAYMENT_PROVIDER_SECRETS}`],
|
|
237
|
+
// Self-describing bindings: the binding name is the whole config (no remote
|
|
238
|
+
// id to mint), so reconcile auto-writes them like the DO/D1 bindings.
|
|
239
|
+
[capabilities.usesBrowser, "browser (@lunora/browser imported) — self-describing { binding: BROWSER }"],
|
|
240
|
+
[capabilities.usesImages, "images (@lunora/images imported) — self-describing { binding: IMAGES }"],
|
|
241
|
+
[capabilities.usesAnalytics, "analytics_engine_datasets (@lunora/analytics imported) — self-describing { binding: ANALYTICS, dataset }"],
|
|
242
|
+
// Hint bindings: each needs a remote resource Lunora can't fabricate (a KV
|
|
243
|
+
// namespace id, a Hyperdrive id, a Pipelines pipeline name), so they surface
|
|
244
|
+
// as hints — never an auto-write — exactly like R2's user-defined bucket name.
|
|
245
|
+
[
|
|
246
|
+
capabilities.usesKv,
|
|
247
|
+
"hint: @lunora/kv is imported; add a kv_namespaces binding ({ binding, id }) and pass env.<BINDING> to createKv() — the namespace id can't be auto-provisioned"
|
|
248
|
+
],
|
|
249
|
+
[
|
|
250
|
+
capabilities.usesHyperdrive,
|
|
251
|
+
"hint: @lunora/hyperdrive is imported; run 'wrangler hyperdrive create' and add a 'hyperdrive' binding ({ binding, id }) — the id can't be auto-provisioned"
|
|
252
|
+
],
|
|
253
|
+
[
|
|
254
|
+
capabilities.usesPipelines,
|
|
255
|
+
"hint: @lunora/pipelines is imported; run 'wrangler pipelines create <name>' and add a 'pipelines' binding ({ binding, pipeline }) — the pipeline resource can't be auto-provisioned"
|
|
256
|
+
]
|
|
257
|
+
];
|
|
258
|
+
return rules.filter(([active]) => active).map(([, signal]) => signal);
|
|
259
|
+
};
|
|
260
|
+
const describeSignals = (durableObjects, needsD1, capabilities, containers = [], workflows = []) => {
|
|
261
|
+
const exported = new Set(durableObjects.map((object) => object.className));
|
|
262
|
+
const signals = durableObjects.map((object) => `${object.binding}/${object.className} (exported by worker entry)`);
|
|
263
|
+
if (needsD1) {
|
|
264
|
+
signals.push("DB (.global() table declared)");
|
|
265
|
+
}
|
|
266
|
+
signals.push(...describeDeclaredExports(containers, workflows), ...describeCapabilitySignals(capabilities, exported));
|
|
267
|
+
return signals;
|
|
268
|
+
};
|
|
269
|
+
const inferLunoraBindings = async (options) => {
|
|
270
|
+
await init;
|
|
271
|
+
const schemaDirectory = options.schemaDir ?? "lunora";
|
|
272
|
+
const scanDirectories = options.scanDirs ?? DEFAULT_SCAN_DIRECTORIES;
|
|
273
|
+
const capabilities = scanCapabilities(options.projectRoot, scanDirectories);
|
|
274
|
+
const entryPath = resolveWorkerEntry(options.projectRoot);
|
|
275
|
+
const durableObjects = entryPath ? detectExportedDurableObjects(entryPath) : [];
|
|
276
|
+
const needsD1 = capabilities.needsD1 || schemaNeedsD1(options.projectRoot, schemaDirectory);
|
|
277
|
+
const containers = detectContainerExports(entryPath, discoverContainerInfo(options.projectRoot, schemaDirectory).containers);
|
|
278
|
+
const workflows = detectWorkflowExports(entryPath, discoverWorkflowInfo(options.projectRoot, schemaDirectory).workflows);
|
|
279
|
+
const capabilityFlags = {};
|
|
280
|
+
for (const flag of CAPABILITY_FLAGS) {
|
|
281
|
+
capabilityFlags[flag] = capabilities[flag];
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
containers,
|
|
285
|
+
durableObjects,
|
|
286
|
+
needsD1,
|
|
287
|
+
signals: describeSignals(durableObjects, needsD1, capabilities, containers, workflows),
|
|
288
|
+
workflows,
|
|
289
|
+
...capabilityFlags
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
const packageNamesFromBindings = (bindings) => {
|
|
293
|
+
const names = [];
|
|
294
|
+
for (const flag of CAPABILITY_FLAGS) {
|
|
295
|
+
if (bindings[flag]) {
|
|
296
|
+
names.push(CAPABILITY_SOURCES[flag].source);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return names;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export { inferLunoraBindings, packageNamesFromBindings };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { writeFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { modify, applyEdits } from 'jsonc-parser';
|
|
3
|
+
import { findWranglerFile, readWranglerJsonc } from './findWranglerFile-DwSuC-Kn.mjs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const FORMATTING = { formattingOptions: { insertSpaces: true, tabSize: 4 } };
|
|
7
|
+
const REMOTE_ELIGIBLE_KEYS = {
|
|
8
|
+
ai: { label: "AI", shape: "object" },
|
|
9
|
+
d1_databases: { label: "D1", shape: "array" },
|
|
10
|
+
kv_namespaces: { label: "KV", shape: "array" },
|
|
11
|
+
queues: { label: "Queue", shape: "producers" },
|
|
12
|
+
r2_buckets: { label: "R2", shape: "array" },
|
|
13
|
+
services: { label: "Service", shape: "array" },
|
|
14
|
+
vectorize: { label: "Vectorize", shape: "array" }
|
|
15
|
+
};
|
|
16
|
+
const REMOTE_ELIGIBLE_KEY_LIST = Object.keys(REMOTE_ELIGIBLE_KEYS);
|
|
17
|
+
const entryName = (entry, fallback) => typeof entry.binding === "string" ? entry.binding : fallback;
|
|
18
|
+
const planArrayEntries = (section, entries, kind, pathPrefix) => {
|
|
19
|
+
const plans = [];
|
|
20
|
+
for (const [index, entry] of entries.entries()) {
|
|
21
|
+
if (entry === null || entry === void 0) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
plans.push({ binding: entryName(entry, `#${String(index)}`), kind, path: [...pathPrefix, index], section });
|
|
25
|
+
}
|
|
26
|
+
return plans;
|
|
27
|
+
};
|
|
28
|
+
const planSection = (section, parsed) => {
|
|
29
|
+
const { label, shape } = REMOTE_ELIGIBLE_KEYS[section];
|
|
30
|
+
if (shape === "array") {
|
|
31
|
+
const entries = parsed[section] ?? [];
|
|
32
|
+
return planArrayEntries(section, entries, label, []);
|
|
33
|
+
}
|
|
34
|
+
if (shape === "producers") {
|
|
35
|
+
return planArrayEntries(section, parsed.queues?.producers ?? [], label, ["producers"]);
|
|
36
|
+
}
|
|
37
|
+
const entry = parsed.ai;
|
|
38
|
+
return entry === null || entry === void 0 ? [] : [{ binding: entryName(entry, section), kind: label, path: [], section }];
|
|
39
|
+
};
|
|
40
|
+
const planRemoteBindings = (parsed) => REMOTE_ELIGIBLE_KEY_LIST.flatMap((section) => planSection(section, parsed));
|
|
41
|
+
const applyModify = (text, path, value) => {
|
|
42
|
+
const edits = modify(text, [...path], value, FORMATTING);
|
|
43
|
+
return edits.length > 0 ? applyEdits(text, edits) : text;
|
|
44
|
+
};
|
|
45
|
+
const injectRemoteFlags = (text, plans) => {
|
|
46
|
+
let next = text;
|
|
47
|
+
for (const plan of plans) {
|
|
48
|
+
next = applyModify(next, [plan.section, ...plan.path, "remote"], true);
|
|
49
|
+
}
|
|
50
|
+
return next;
|
|
51
|
+
};
|
|
52
|
+
const noopCleanup = () => {
|
|
53
|
+
};
|
|
54
|
+
const createCleanup = (path) => {
|
|
55
|
+
let done = false;
|
|
56
|
+
return () => {
|
|
57
|
+
if (done) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
done = true;
|
|
61
|
+
try {
|
|
62
|
+
rmSync(path, { force: true, recursive: true });
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
const remoteConfigBasename = () => `.wrangler.lunora-remote.${String(process.pid)}.jsonc`;
|
|
68
|
+
const materializeRemoteWranglerConfig = (options) => {
|
|
69
|
+
if (!options.enabled) {
|
|
70
|
+
return { cleanup: noopCleanup, enabled: false, remoteBindings: [] };
|
|
71
|
+
}
|
|
72
|
+
const wranglerPath = findWranglerFile(options.projectRoot);
|
|
73
|
+
if (!wranglerPath) {
|
|
74
|
+
return { cleanup: noopCleanup, enabled: true, reason: "wrangler.jsonc not found", remoteBindings: [] };
|
|
75
|
+
}
|
|
76
|
+
const { parsed, text } = readWranglerJsonc(wranglerPath);
|
|
77
|
+
if (parsed === void 0) {
|
|
78
|
+
return { cleanup: noopCleanup, enabled: true, reason: `failed to parse ${wranglerPath} as JSONC`, remoteBindings: [] };
|
|
79
|
+
}
|
|
80
|
+
const plans = planRemoteBindings(parsed);
|
|
81
|
+
if (plans.length === 0) {
|
|
82
|
+
return { cleanup: noopCleanup, enabled: true, reason: "no remote-eligible bindings to proxy", remoteBindings: [] };
|
|
83
|
+
}
|
|
84
|
+
const configPath = join(options.projectRoot, remoteConfigBasename());
|
|
85
|
+
writeFileSync(configPath, injectRemoteFlags(text, plans), "utf8");
|
|
86
|
+
return { cleanup: createCleanup(configPath), configPath, enabled: true, remoteBindings: plans };
|
|
87
|
+
};
|
|
88
|
+
const isRemoteEnvEnabled = (value) => {
|
|
89
|
+
if (value === void 0) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const normalized = value.trim().toLowerCase();
|
|
93
|
+
return normalized === "1" || normalized === "true";
|
|
94
|
+
};
|
|
95
|
+
const resolveRemoteEnabled = (inputs) => {
|
|
96
|
+
if (inputs.flag === true) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (isRemoteEnvEnabled(inputs.envValue)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return inputs.configPreference ?? false;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export { REMOTE_ELIGIBLE_KEYS, injectRemoteFlags, isRemoteEnvEnabled, materializeRemoteWranglerConfig, planRemoteBindings, resolveRemoteEnabled };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { parse } from 'jsonc-parser';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const LUNORA_CONFIG_FILE = "lunora.json";
|
|
6
|
+
const interpretRemote = (value) => {
|
|
7
|
+
if (typeof value === "boolean") {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
if (value !== null && typeof value === "object") {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
return void 0;
|
|
14
|
+
};
|
|
15
|
+
const readProjectRemotePreference = (projectRoot) => {
|
|
16
|
+
const configPath = join(projectRoot, LUNORA_CONFIG_FILE);
|
|
17
|
+
if (!existsSync(configPath)) {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
let text;
|
|
21
|
+
try {
|
|
22
|
+
text = readFileSync(configPath, "utf8");
|
|
23
|
+
} catch {
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
const parseErrors = [];
|
|
27
|
+
const parsed = parse(text, parseErrors, { allowTrailingComma: true });
|
|
28
|
+
if (parseErrors.length > 0 || parsed === null || typeof parsed !== "object") {
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
return interpretRemote(parsed.remote);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export { LUNORA_CONFIG_FILE, interpretRemote, readProjectRemotePreference };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const LINE_BREAK = /\r?\n/u;
|
|
5
|
+
const parseDevVariable = (contents, key) => {
|
|
6
|
+
for (const raw of contents.split(LINE_BREAK)) {
|
|
7
|
+
const line = raw.trim();
|
|
8
|
+
const eq = line.indexOf("=");
|
|
9
|
+
if (line === "" || line.startsWith("#") || eq === -1 || line.slice(0, eq).trim() !== key) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const value = line.slice(eq + 1).trim();
|
|
13
|
+
const unquoted = value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'") ? value.slice(1, -1) : value;
|
|
14
|
+
return unquoted === "" ? void 0 : unquoted;
|
|
15
|
+
}
|
|
16
|
+
return void 0;
|
|
17
|
+
};
|
|
18
|
+
const resolveAdminToken = (root) => {
|
|
19
|
+
const fromEnv = process.env["LUNORA_ADMIN_TOKEN"];
|
|
20
|
+
if (typeof fromEnv === "string" && fromEnv !== "") {
|
|
21
|
+
return fromEnv;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return parseDevVariable(readFileSync(join(root, ".dev.vars"), "utf8"), "LUNORA_ADMIN_TOKEN");
|
|
25
|
+
} catch {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export { parseDevVariable, resolveAdminToken };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Project, SyntaxKind } from 'ts-morph';
|
|
2
|
+
|
|
3
|
+
const OPTIONAL_VALIDATOR_PATTERN = /^v\s*\.\s*optional\s*\(/u;
|
|
4
|
+
const SHARD_BY_PATTERN = /\.shardBy\(\s*["']([^"']+)["']\s*\)/u;
|
|
5
|
+
const GLOBAL_PATTERN = /\.global\(\s*\)/u;
|
|
6
|
+
const QUOTE_PATTERN = /["']/gu;
|
|
7
|
+
const collectCalls = (node) => {
|
|
8
|
+
const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
9
|
+
if (node.getKind() === SyntaxKind.CallExpression) {
|
|
10
|
+
calls.push(node.asKindOrThrow(SyntaxKind.CallExpression));
|
|
11
|
+
}
|
|
12
|
+
return calls;
|
|
13
|
+
};
|
|
14
|
+
const isOptionalValidator = (validatorText) => OPTIONAL_VALIDATOR_PATTERN.test(validatorText.trim());
|
|
15
|
+
const readStringArray = (node) => {
|
|
16
|
+
if (node?.getKind() !== SyntaxKind.ArrayLiteralExpression) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
return node.asKindOrThrow(SyntaxKind.ArrayLiteralExpression).getElements().filter((element) => element.getKind() === SyntaxKind.StringLiteral).map((element) => element.asKindOrThrow(SyntaxKind.StringLiteral).getLiteralText());
|
|
20
|
+
};
|
|
21
|
+
const readUnique = (node) => {
|
|
22
|
+
if (node?.getKind() !== SyntaxKind.ObjectLiteralExpression) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const property = node.asKindOrThrow(SyntaxKind.ObjectLiteralExpression).getProperty("unique");
|
|
26
|
+
return property?.getKind() === SyntaxKind.PropertyAssignment && property.getInitializer()?.getText() === "true";
|
|
27
|
+
};
|
|
28
|
+
const parseIndexCall = (call) => {
|
|
29
|
+
const expression = call.getExpression();
|
|
30
|
+
if (expression.getKind() !== SyntaxKind.PropertyAccessExpression || expression.asKindOrThrow(SyntaxKind.PropertyAccessExpression).getName() !== "index") {
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
const [nameArgument, fieldsArgument, optionsArgument] = call.getArguments();
|
|
34
|
+
if (nameArgument?.getKind() !== SyntaxKind.StringLiteral) {
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
fields: readStringArray(fieldsArgument),
|
|
39
|
+
name: nameArgument.asKindOrThrow(SyntaxKind.StringLiteral).getLiteralText(),
|
|
40
|
+
unique: readUnique(optionsArgument)
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const parseIndexes = (initializer) => {
|
|
44
|
+
const initializerNode = initializer.getInitializer();
|
|
45
|
+
if (initializerNode === void 0) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const indexes = [];
|
|
49
|
+
for (const call of collectCalls(initializerNode)) {
|
|
50
|
+
const index = parseIndexCall(call);
|
|
51
|
+
if (index !== void 0) {
|
|
52
|
+
indexes.push(index);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return indexes;
|
|
56
|
+
};
|
|
57
|
+
const parseColumns = (initializer) => {
|
|
58
|
+
const columns = [];
|
|
59
|
+
const defineTableCall = initializer.getInitializer()?.getDescendantsOfKind(SyntaxKind.CallExpression).find((call) => call.getExpression().getText() === "defineTable");
|
|
60
|
+
const tableShape = defineTableCall?.getArguments()[0];
|
|
61
|
+
if (tableShape?.getKind() !== SyntaxKind.ObjectLiteralExpression) {
|
|
62
|
+
return columns;
|
|
63
|
+
}
|
|
64
|
+
for (const property of tableShape.getProperties()) {
|
|
65
|
+
if (property.getKind() !== SyntaxKind.PropertyAssignment) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const assignment = property;
|
|
69
|
+
const name = assignment.getName().replaceAll(QUOTE_PATTERN, "");
|
|
70
|
+
const validator = assignment.getInitializer()?.getText() ?? "";
|
|
71
|
+
columns.push({ name, optional: isOptionalValidator(validator), validator });
|
|
72
|
+
}
|
|
73
|
+
return columns;
|
|
74
|
+
};
|
|
75
|
+
const parseSchema = (source) => {
|
|
76
|
+
const project = new Project({ compilerOptions: { allowJs: true }, useInMemoryFileSystem: true });
|
|
77
|
+
const sourceFile = project.createSourceFile("schema.ts", source, { overwrite: true });
|
|
78
|
+
const defineSchemaCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((call) => call.getExpression().getText() === "defineSchema");
|
|
79
|
+
if (defineSchemaCall === void 0) {
|
|
80
|
+
const aliased = sourceFile.getImportDeclarations().some((declaration) => declaration.getNamedImports().some((named) => named.getName() === "defineSchema" && named.getAliasNode() !== void 0));
|
|
81
|
+
return { ok: false, reason: aliased ? "aliased-define-schema" : "no-define-schema" };
|
|
82
|
+
}
|
|
83
|
+
const tablesArgument = defineSchemaCall.getArguments()[0];
|
|
84
|
+
if (tablesArgument?.getKind() !== SyntaxKind.ObjectLiteralExpression) {
|
|
85
|
+
return { ok: false, reason: "non-object-argument" };
|
|
86
|
+
}
|
|
87
|
+
const tables = [];
|
|
88
|
+
for (const property of tablesArgument.getProperties()) {
|
|
89
|
+
if (property.getKind() !== SyntaxKind.PropertyAssignment) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const assignment = property;
|
|
93
|
+
const name = assignment.getName().replaceAll(QUOTE_PATTERN, "");
|
|
94
|
+
const initializerText = assignment.getInitializer()?.getText() ?? "";
|
|
95
|
+
const shardMatch = SHARD_BY_PATTERN.exec(initializerText);
|
|
96
|
+
tables.push({
|
|
97
|
+
columns: parseColumns(assignment),
|
|
98
|
+
global: GLOBAL_PATTERN.test(initializerText),
|
|
99
|
+
indexes: parseIndexes(assignment),
|
|
100
|
+
name,
|
|
101
|
+
...shardMatch?.[1] === void 0 ? {} : { shardBy: shardMatch[1] }
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return { ok: true, tables };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export { collectCalls, parseSchema };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Scaffold a new policy/role/permission stub file under `lunora/`. */
|
|
2
|
+
interface ScaffoldPolicyEdit {
|
|
3
|
+
readonly kind: "scaffoldPolicy";
|
|
4
|
+
/**
|
|
5
|
+
* Base name for the generated file and its exported policy-set identifier,
|
|
6
|
+
* e.g. `invoices` → `lunora/invoices.policies.ts` exporting `invoicesPolicies`.
|
|
7
|
+
*/
|
|
8
|
+
readonly name: string;
|
|
9
|
+
/** Logical table the scaffolded policy guards (used in the stub body). */
|
|
10
|
+
readonly table: string;
|
|
11
|
+
}
|
|
12
|
+
/** Append `.use(rls(<policies>))` to an existing procedure's builder chain. */
|
|
13
|
+
interface WireRlsEdit {
|
|
14
|
+
/** Exported procedure name to wire, e.g. `listInvoices`. */
|
|
15
|
+
readonly exportName: string;
|
|
16
|
+
readonly kind: "wireRls";
|
|
17
|
+
/** Identifier of the policy set passed to `rls(...)`, e.g. `invoicesPolicies`. */
|
|
18
|
+
readonly policies: string;
|
|
19
|
+
}
|
|
20
|
+
/** Additive scaffolder edits — the only requests the scaffolder applies. */
|
|
21
|
+
type AdditivePolicyEdit = ScaffoldPolicyEdit | WireRlsEdit;
|
|
22
|
+
/**
|
|
23
|
+
* Destructive scaffolder requests — never applied. Rewriting an existing `when`
|
|
24
|
+
* body changes evaluation semantics silently, so it is refused (STOP condition
|
|
25
|
+
* in plan 025); carried as data so the editor can describe the request.
|
|
26
|
+
*/
|
|
27
|
+
interface DestructivePolicyEdit {
|
|
28
|
+
readonly exportName?: string;
|
|
29
|
+
readonly kind: "rewritePolicyWhen";
|
|
30
|
+
readonly table?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Any request the scaffolder can receive. */
|
|
33
|
+
type PolicyEdit = AdditivePolicyEdit | DestructivePolicyEdit;
|
|
34
|
+
/**
|
|
35
|
+
* Classify a scaffolder request. Additive requests ({@link AdditivePolicyEdit})
|
|
36
|
+
* apply directly; everything else (rewriting an existing predicate) changes
|
|
37
|
+
* evaluation semantics and is destructive.
|
|
38
|
+
*/
|
|
39
|
+
declare const classifyPolicyEdit: (edit: PolicyEdit) => "additive" | "destructive";
|
|
40
|
+
/** Failure reasons a scaffolder request can report. */
|
|
41
|
+
type PolicyScaffoldFailureReason = "already-wired" | "destructive" | "invalid-identifier" | "unknown-procedure" | "unsupported-procedure-shape";
|
|
42
|
+
/** Tagged result of generating a stub file. */
|
|
43
|
+
type ScaffoldFileResult = {
|
|
44
|
+
fileName: string;
|
|
45
|
+
ok: true;
|
|
46
|
+
source: string;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
reason: PolicyScaffoldFailureReason;
|
|
50
|
+
};
|
|
51
|
+
/** Tagged result of wiring a procedure. */
|
|
52
|
+
type WireResult = {
|
|
53
|
+
ok: false;
|
|
54
|
+
reason: PolicyScaffoldFailureReason;
|
|
55
|
+
} | {
|
|
56
|
+
ok: true;
|
|
57
|
+
text: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Generate the source of a new policy/role/permission stub file. The `when`
|
|
61
|
+
* predicate is a `() => false` skeleton with a TODO — the scaffolder never
|
|
62
|
+
* authors real logic, the developer fills it in. Pure (no I/O); the handler
|
|
63
|
+
* writes the returned source and refuses to overwrite an existing file.
|
|
64
|
+
*/
|
|
65
|
+
declare const scaffoldPolicyFile: (edit: ScaffoldPolicyEdit) => ScaffoldFileResult;
|
|
66
|
+
/**
|
|
67
|
+
* Append `.use(rls(<policies>))` to a procedure's builder chain, preserving the
|
|
68
|
+
* terminal `.query(handler)` (and its handler body) byte-for-byte. Only the
|
|
69
|
+
* **builder** form can be wired; the bare-factory form (`query({ handler })`)
|
|
70
|
+
* has no chain and is reported `unsupported-procedure-shape` so the editor can
|
|
71
|
+
* tell the developer to convert it rather than silently rewriting their code.
|
|
72
|
+
*/
|
|
73
|
+
declare const wireRlsIntoProcedure: (source: string, edit: WireRlsEdit) => WireResult;
|
|
74
|
+
export { AdditivePolicyEdit as A, DestructivePolicyEdit as D, PolicyEdit as P, ScaffoldPolicyEdit as S, WireRlsEdit as W, PolicyScaffoldFailureReason as a, ScaffoldFileResult as b, WireResult as c, classifyPolicyEdit as d, scaffoldPolicyFile as s, wireRlsIntoProcedure as w };
|