@lunora/config 1.0.0-alpha.6 → 1.0.0-alpha.60
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 +6 -0
- package/__assets__/package-og.svg +1 -1
- package/dist/index.d.mts +368 -6
- package/dist/index.d.ts +368 -6
- package/dist/index.mjs +12 -7
- package/dist/packem_shared/AGENT_MODE_ENV-B5R7jpY7.mjs +17 -0
- package/dist/packem_shared/DEV_DAEMON_ENV-FIEtW7Fz.mjs +163 -0
- package/dist/packem_shared/{PACKAGE_SECRETS_REGISTRY-CySy5vR_.mjs → PACKAGE_SECRETS_REGISTRY-BeRSvrl5.mjs} +45 -1
- package/dist/packem_shared/{POLICY_SCAFFOLD_ENDPOINT-CiC2IGKx.mjs → POLICY_SCAFFOLD_ENDPOINT-8bNMHGiu.mjs} +2 -1
- package/dist/packem_shared/{REQUIRED_COMPATIBILITY_DATE-ycqievRl.mjs → REQUIRED_COMPATIBILITY_DATE-DYYxY_If.mjs} +133 -1
- package/dist/packem_shared/assetContentType-ByZjeV3D.mjs +72 -0
- package/dist/packem_shared/{buildPackageSecretsBlock-S74dgmwy.mjs → buildPackageSecretsBlock-BDXX0mKG.mjs} +114 -20
- package/dist/packem_shared/collectWranglerSecretVariables-Red5H-4V.mjs +56 -0
- package/dist/packem_shared/{detectFramework-Br-BcPBq.mjs → detectFramework-U08038Yp.mjs} +3 -3
- package/dist/packem_shared/{discoverSchemaInfo-DWtypqpP.mjs → discoverSchemaInfo-BB-CKlTK.mjs} +1 -1
- package/dist/packem_shared/{inferLunoraBindings-XNJH4pup.mjs → inferLunoraBindings-ByMwG-gH.mjs} +62 -12
- package/dist/packem_shared/{reconcileWranglerBindings-DTHmqTbL.mjs → reconcileWranglerBindings-Cruwwx1M.mjs} +60 -3
- package/dist/packem_shared/reconcileWranglerCompatibilityDate-CYY0rVnJ.mjs +34 -0
- package/dist/packem_shared/streamContainerLogs-BZ4cOZwH.mjs +157 -0
- package/dist/studio-host/index.d.mts +33 -1
- package/dist/studio-host/index.d.ts +33 -1
- package/dist/studio-host/index.mjs +2 -2
- package/package.json +11 -8
- package/dist/packem_shared/loadStudioAssets-Csk5RS4E.mjs +0 -28
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const DEV_STATE_DIR = ".lunora";
|
|
5
|
+
const DEV_STATE_FILE = join(DEV_STATE_DIR, "dev.json");
|
|
6
|
+
const DEV_LOG_FILE = join(DEV_STATE_DIR, "dev.log");
|
|
7
|
+
const DEV_DAEMON_ENV = "LUNORA_DEV_DAEMON";
|
|
8
|
+
const DEV_LOG_FILE_ENV = "LUNORA_DEV_LOG_FILE";
|
|
9
|
+
const DEV_HANDOFF_ENV = "LUNORA_DEV_HANDOFF_PID";
|
|
10
|
+
const isObject = (value) => value !== null && typeof value === "object";
|
|
11
|
+
const stringField = (record, key) => {
|
|
12
|
+
const value = record[key];
|
|
13
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
14
|
+
};
|
|
15
|
+
const isProcessAlive = (pid) => {
|
|
16
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const START_TIME_SKEW_MS = 1e4;
|
|
27
|
+
const LINUX_CLOCK_TICKS_PER_SECOND = 100;
|
|
28
|
+
const processStartTimeMs = (pid) => {
|
|
29
|
+
if (process.platform !== "linux") {
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const stat = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
|
|
34
|
+
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
35
|
+
const startTicks = Number(fields[19]);
|
|
36
|
+
const bootLine = readFileSync("/proc/stat", "utf8").split("\n").find((line) => line.startsWith("btime "));
|
|
37
|
+
const bootSeconds = Number(bootLine?.slice("btime ".length));
|
|
38
|
+
if (!Number.isFinite(startTicks) || !Number.isFinite(bootSeconds)) {
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
return bootSeconds * 1e3 + startTicks / LINUX_CLOCK_TICKS_PER_SECOND * 1e3;
|
|
42
|
+
} catch {
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const isRecordedProcessCurrent = (state) => {
|
|
47
|
+
if (!isProcessAlive(state.pid)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const recordedAtMs = state.startedAt === void 0 ? Number.NaN : Date.parse(state.startedAt);
|
|
51
|
+
if (Number.isNaN(recordedAtMs)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
const actualStartMs = processStartTimeMs(state.pid);
|
|
55
|
+
return actualStartMs === void 0 || actualStartMs <= recordedAtMs + START_TIME_SKEW_MS;
|
|
56
|
+
};
|
|
57
|
+
const readDevServerState = (projectRoot) => {
|
|
58
|
+
const path = join(projectRoot, DEV_STATE_FILE);
|
|
59
|
+
if (!existsSync(path)) {
|
|
60
|
+
return void 0;
|
|
61
|
+
}
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
65
|
+
} catch {
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
if (!isObject(parsed) || typeof parsed["pid"] !== "number") {
|
|
69
|
+
return void 0;
|
|
70
|
+
}
|
|
71
|
+
const url = stringField(parsed, "url");
|
|
72
|
+
if (url === void 0) {
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
background: parsed["background"] === true,
|
|
77
|
+
logFile: stringField(parsed, "logFile"),
|
|
78
|
+
mode: parsed["mode"] === "vite" ? "vite" : "cli",
|
|
79
|
+
pid: parsed["pid"],
|
|
80
|
+
startedAt: stringField(parsed, "startedAt"),
|
|
81
|
+
studioUrl: stringField(parsed, "studioUrl"),
|
|
82
|
+
url
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const writeDevServerState = (projectRoot, state) => {
|
|
86
|
+
try {
|
|
87
|
+
mkdirSync(join(projectRoot, DEV_STATE_DIR), { recursive: true });
|
|
88
|
+
const path = join(projectRoot, DEV_STATE_FILE);
|
|
89
|
+
writeFileSync(path, `${JSON.stringify(state, void 0, 2)}
|
|
90
|
+
`, "utf8");
|
|
91
|
+
return path;
|
|
92
|
+
} catch {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const updateDevServerState = (projectRoot, patch) => {
|
|
97
|
+
const existing = readDevServerState(projectRoot);
|
|
98
|
+
if (existing === void 0) {
|
|
99
|
+
return void 0;
|
|
100
|
+
}
|
|
101
|
+
const merged = { ...existing, ...patch };
|
|
102
|
+
writeDevServerState(projectRoot, merged);
|
|
103
|
+
return merged;
|
|
104
|
+
};
|
|
105
|
+
const clearDevServerState = (projectRoot, expectedPid) => {
|
|
106
|
+
try {
|
|
107
|
+
if (expectedPid !== void 0) {
|
|
108
|
+
const current = readDevServerState(projectRoot);
|
|
109
|
+
if (current !== void 0 && current.pid !== expectedPid) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
rmSync(join(projectRoot, DEV_STATE_FILE), { force: true });
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const readLiveDevServerState = (projectRoot) => {
|
|
118
|
+
const state = readDevServerState(projectRoot);
|
|
119
|
+
if (state === void 0) {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
if (isRecordedProcessCurrent(state)) {
|
|
123
|
+
return state;
|
|
124
|
+
}
|
|
125
|
+
clearDevServerState(projectRoot, state.pid);
|
|
126
|
+
return void 0;
|
|
127
|
+
};
|
|
128
|
+
const resolveClaimConflict = (projectRoot, state, supersedePid) => {
|
|
129
|
+
const existing = readLiveDevServerState(projectRoot);
|
|
130
|
+
if (existing === void 0) {
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
if (supersedePid !== void 0 && existing.pid === supersedePid) {
|
|
134
|
+
return { ok: writeDevServerState(projectRoot, state) !== void 0 };
|
|
135
|
+
}
|
|
136
|
+
if (existing.pid !== state.pid) {
|
|
137
|
+
return { existing, ok: false };
|
|
138
|
+
}
|
|
139
|
+
return { ok: writeDevServerState(projectRoot, state) !== void 0 };
|
|
140
|
+
};
|
|
141
|
+
const claimDevServerState = (projectRoot, state, options) => {
|
|
142
|
+
const path = join(projectRoot, DEV_STATE_FILE);
|
|
143
|
+
const payload = `${JSON.stringify(state, void 0, 2)}
|
|
144
|
+
`;
|
|
145
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
146
|
+
try {
|
|
147
|
+
mkdirSync(join(projectRoot, DEV_STATE_DIR), { recursive: true });
|
|
148
|
+
writeFileSync(path, payload, { encoding: "utf8", flag: "wx" });
|
|
149
|
+
return { ok: true };
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error.code !== "EEXIST") {
|
|
152
|
+
return { ok: writeDevServerState(projectRoot, state) !== void 0 };
|
|
153
|
+
}
|
|
154
|
+
const resolved = resolveClaimConflict(projectRoot, state, options?.supersedePid);
|
|
155
|
+
if (resolved !== void 0) {
|
|
156
|
+
return resolved;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { ok: writeDevServerState(projectRoot, state) !== void 0 };
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export { DEV_DAEMON_ENV, DEV_HANDOFF_ENV, DEV_LOG_FILE, DEV_LOG_FILE_ENV, DEV_STATE_DIR, DEV_STATE_FILE, claimDevServerState, clearDevServerState, isProcessAlive, isRecordedProcessCurrent, readDevServerState, readLiveDevServerState, updateDevServerState, writeDevServerState };
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
const CORE_SECRETS = [
|
|
2
|
+
{
|
|
3
|
+
description: "Bearer token the local Lunora Studio uses to call the worker's admin endpoints (data browser, schema edits) in dev. Generate with: openssl rand -hex 32",
|
|
4
|
+
docsUrl: "https://lunora.sh/docs/packages/studio",
|
|
5
|
+
key: "LUNORA_ADMIN_TOKEN",
|
|
6
|
+
placeholderValue: "replace-with-openssl-rand-hex-32"
|
|
7
|
+
}
|
|
8
|
+
];
|
|
1
9
|
const PACKAGE_SECRETS_REGISTRY = {
|
|
2
10
|
"@lunora/auth": [
|
|
3
11
|
{
|
|
@@ -45,6 +53,42 @@ const PACKAGE_SECRETS_REGISTRY = {
|
|
|
45
53
|
docsUrl: "https://lunora.sh/docs/packages/payment#polar",
|
|
46
54
|
key: "POLAR_WEBHOOK_SECRET",
|
|
47
55
|
placeholderValue: "<your-polar-webhook-secret>"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
description: "Autumn secret key (am_sk_…) for the Autumn payment adapter. Obtain at https://app.useautumn.com/dev",
|
|
59
|
+
docsUrl: "https://lunora.sh/docs/packages/payment#autumn",
|
|
60
|
+
key: "AUTUMN_SECRET_KEY",
|
|
61
|
+
placeholderValue: "<your-autumn-secret-key>"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
description: "Autumn webhook signing secret for verifying Standard Webhooks event payloads. Obtain from your Autumn dashboard webhook settings.",
|
|
65
|
+
docsUrl: "https://lunora.sh/docs/packages/payment#autumn",
|
|
66
|
+
key: "AUTUMN_WEBHOOK_SECRET",
|
|
67
|
+
placeholderValue: "<your-autumn-webhook-secret>"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
description: "Dodo Payments API key (bearer token) for the Dodo Payments adapter. Obtain at https://app.dodopayments.com/developer/api-keys",
|
|
71
|
+
docsUrl: "https://lunora.sh/docs/packages/payment#dodo-payments",
|
|
72
|
+
key: "DODO_PAYMENTS_API_KEY",
|
|
73
|
+
placeholderValue: "<your-dodo-payments-api-key>"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
description: "Dodo Payments webhook signing secret (whsec_…) for verifying event payloads. Obtain at https://app.dodopayments.com/developer/webhooks",
|
|
77
|
+
docsUrl: "https://lunora.sh/docs/packages/payment#dodo-payments",
|
|
78
|
+
key: "DODO_PAYMENTS_WEBHOOK_KEY",
|
|
79
|
+
placeholderValue: "<your-dodo-payments-webhook-secret>"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
description: "Creem API key for the Creem payment adapter. Obtain at https://www.creem.io/dashboard/developers",
|
|
83
|
+
docsUrl: "https://lunora.sh/docs/packages/payment#creem",
|
|
84
|
+
key: "CREEM_API_KEY",
|
|
85
|
+
placeholderValue: "<your-creem-api-key>"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
description: "Creem webhook signing secret for verifying the creem-signature header. Obtain from your Creem dashboard webhook settings.",
|
|
89
|
+
docsUrl: "https://lunora.sh/docs/packages/payment#creem",
|
|
90
|
+
key: "CREEM_WEBHOOK_SECRET",
|
|
91
|
+
placeholderValue: "<your-creem-webhook-secret>"
|
|
48
92
|
}
|
|
49
93
|
]
|
|
50
94
|
};
|
|
@@ -59,4 +103,4 @@ const secretsForPackages = (packageNames) => {
|
|
|
59
103
|
return result;
|
|
60
104
|
};
|
|
61
105
|
|
|
62
|
-
export { PACKAGE_SECRETS_REGISTRY, secretsForPackages };
|
|
106
|
+
export { CORE_SECRETS, PACKAGE_SECRETS_REGISTRY, secretsForPackages };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
2
2
|
import { join, isAbsolute, relative } from 'node:path';
|
|
3
3
|
import { runCodegen, CodegenDiagnosticError } from '@lunora/codegen';
|
|
4
|
+
import { LunoraError } from '@lunora/errors';
|
|
4
5
|
import { classifyPolicyEdit, scaffoldPolicyFile, wireRlsIntoProcedure } from './classifyPolicyEdit-BHeAqF8P.mjs';
|
|
5
6
|
|
|
6
7
|
const POLICY_SCAFFOLD_ENDPOINT = "/__lunora/policy-scaffold";
|
|
@@ -97,7 +98,7 @@ const handlePolicyScaffoldRequest = (request) => {
|
|
|
97
98
|
if (edit.kind === "wireRls") {
|
|
98
99
|
return handleWireRls(request, edit);
|
|
99
100
|
}
|
|
100
|
-
throw new
|
|
101
|
+
throw new LunoraError("INTERNAL", `unhandled additive policy edit kind: ${String(edit.kind)}`);
|
|
101
102
|
};
|
|
102
103
|
|
|
103
104
|
export { POLICY_SCAFFOLD_ENDPOINT, handlePolicyScaffoldRequest };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
-
import { discoverSchemaInfo } from './discoverSchemaInfo-
|
|
3
|
+
import { discoverSchemaInfo } from './discoverSchemaInfo-BB-CKlTK.mjs';
|
|
4
4
|
import { findWranglerFile, readWranglerJsonc } from './WRANGLER_FILES-DwSuC-Kn.mjs';
|
|
5
5
|
|
|
6
6
|
const REQUIRED_COMPATIBILITY_DATE = "2026-04-07";
|
|
@@ -127,6 +127,82 @@ const validateWorkflows = (wrangler, errors) => {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
};
|
|
130
|
+
const validateQueueProducers = (producers, errors) => {
|
|
131
|
+
if (producers === void 0) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (!Array.isArray(producers)) {
|
|
135
|
+
errors.push("queues.producers must be an array of { binding, queue } entries");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const entries = producers;
|
|
139
|
+
for (const [index, entry] of entries.entries()) {
|
|
140
|
+
const label = `queues.producers[${String(index)}]`;
|
|
141
|
+
if (!entry || typeof entry !== "object") {
|
|
142
|
+
errors.push(`${label} must be a { binding, queue } object`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (typeof entry.binding !== "string" || entry.binding.length === 0) {
|
|
146
|
+
errors.push(`${label} must have a non-empty "binding" naming the Queue producer (e.g. QUEUE_EMAIL)`);
|
|
147
|
+
}
|
|
148
|
+
if (typeof entry.queue !== "string" || entry.queue.length === 0) {
|
|
149
|
+
errors.push(`${label} must have a non-empty "queue" naming the deployed queue`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const validateQueueConsumers = (consumers, errors) => {
|
|
154
|
+
if (consumers === void 0) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!Array.isArray(consumers)) {
|
|
158
|
+
errors.push("queues.consumers must be an array of { queue } entries");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const entries = consumers;
|
|
162
|
+
for (const [index, entry] of entries.entries()) {
|
|
163
|
+
const label = `queues.consumers[${String(index)}]`;
|
|
164
|
+
if (!entry || typeof entry !== "object") {
|
|
165
|
+
errors.push(`${label} must be a { queue } object`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (typeof entry.queue !== "string" || entry.queue.length === 0) {
|
|
169
|
+
errors.push(`${label} must have a non-empty "queue" naming the consumed queue`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const validateQueues = (wrangler, errors) => {
|
|
174
|
+
if (wrangler.queues === void 0) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (typeof wrangler.queues !== "object" || Array.isArray(wrangler.queues)) {
|
|
178
|
+
errors.push("queues must be a { producers, consumers } object");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
validateQueueProducers(wrangler.queues.producers, errors);
|
|
182
|
+
validateQueueConsumers(wrangler.queues.consumers, errors);
|
|
183
|
+
};
|
|
184
|
+
const validateSecretsStore = (wrangler, errors) => {
|
|
185
|
+
if (wrangler.secrets_store_secrets === void 0) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (!Array.isArray(wrangler.secrets_store_secrets)) {
|
|
189
|
+
errors.push("secrets_store_secrets must be an array of { binding, store_id, secret_name } entries");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const entries = wrangler.secrets_store_secrets;
|
|
193
|
+
for (const [index, entry] of entries.entries()) {
|
|
194
|
+
const label = `secrets_store_secrets[${String(index)}]`;
|
|
195
|
+
if (!entry || typeof entry !== "object") {
|
|
196
|
+
errors.push(`${label} must be a { binding, store_id, secret_name } object`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
for (const field of ["binding", "store_id", "secret_name"]) {
|
|
200
|
+
if (typeof entry[field] !== "string" || entry[field].length === 0) {
|
|
201
|
+
errors.push(`${label} must have a non-empty "${field}"`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
130
206
|
const isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
131
207
|
const asBindingEntries = (value) => value;
|
|
132
208
|
const HINT_BINDING_RULES = [
|
|
@@ -137,6 +213,13 @@ const HINT_BINDING_RULES = [
|
|
|
137
213
|
hintMessage: (label, binding) => `${label} ("${binding}") has no "id" — run \`wrangler kv namespace create\` and set the namespace id, or the binding can't resolve`,
|
|
138
214
|
key: "kv_namespaces"
|
|
139
215
|
},
|
|
216
|
+
{
|
|
217
|
+
arrayMessage: "flagship must be an array of { binding, app_id } entries",
|
|
218
|
+
bindingMessage: (label) => `${label} must have a non-empty "binding" naming the Flagship binding`,
|
|
219
|
+
hintField: "app_id",
|
|
220
|
+
hintMessage: (label, binding) => `${label} ("${binding}") has no "app_id" — create a Flagship app and set its id, or the binding can't resolve`,
|
|
221
|
+
key: "flagship"
|
|
222
|
+
},
|
|
140
223
|
{
|
|
141
224
|
arrayMessage: "hyperdrive must be an array of { binding, id } entries",
|
|
142
225
|
bindingMessage: (label) => `${label} must have a non-empty "binding" naming the Hyperdrive binding`,
|
|
@@ -303,6 +386,45 @@ const validateObservability = (wrangler, errors) => {
|
|
|
303
386
|
}
|
|
304
387
|
}
|
|
305
388
|
};
|
|
389
|
+
const validateCache = (wrangler, errors) => {
|
|
390
|
+
const { cache } = wrangler;
|
|
391
|
+
if (cache === void 0) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (typeof cache !== "object" || cache === null || Array.isArray(cache)) {
|
|
395
|
+
errors.push('cache must be an object (e.g. { "enabled": true })');
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (cache.enabled !== void 0 && typeof cache.enabled !== "boolean") {
|
|
399
|
+
errors.push("cache.enabled must be a boolean (true or false)");
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
const validateExports = (wrangler, errors) => {
|
|
403
|
+
const { exports } = wrangler;
|
|
404
|
+
if (exports === void 0) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (typeof exports !== "object" || exports === null || Array.isArray(exports)) {
|
|
408
|
+
errors.push("exports must be an object keyed by entrypoint name");
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
for (const [name, entry] of Object.entries(exports)) {
|
|
412
|
+
if (typeof entry !== "object" || entry === null) {
|
|
413
|
+
errors.push(`exports["${name}"] must be an object`);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (entry.type !== void 0 && typeof entry.type !== "string") {
|
|
417
|
+
errors.push(`exports["${name}"].type must be a string`);
|
|
418
|
+
}
|
|
419
|
+
if (entry.cache !== void 0) {
|
|
420
|
+
if (typeof entry.cache !== "object" || entry.cache === null || Array.isArray(entry.cache)) {
|
|
421
|
+
errors.push(`exports["${name}"].cache must be an object`);
|
|
422
|
+
} else if (entry.cache.enabled !== void 0 && typeof entry.cache.enabled !== "boolean") {
|
|
423
|
+
errors.push(`exports["${name}"].cache.enabled must be a boolean`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
};
|
|
306
428
|
const validateAssets = (wrangler, errors) => {
|
|
307
429
|
const { assets } = wrangler;
|
|
308
430
|
if (assets === void 0) {
|
|
@@ -385,6 +507,12 @@ const validateWranglerConfig = (wrangler, schema) => {
|
|
|
385
507
|
} else if (compatibilityDate < REQUIRED_COMPATIBILITY_DATE) {
|
|
386
508
|
errors.push(`compatibility_date must be >= "${REQUIRED_COMPATIBILITY_DATE}" (got "${compatibilityDate || "<missing>"}")`);
|
|
387
509
|
}
|
|
510
|
+
const WORKERS_CACHE_MIN_DATE = "2026-05-01";
|
|
511
|
+
const cacheEnabled = typeof wrangler.cache === "object" && wrangler.cache !== null && wrangler.cache.enabled === true;
|
|
512
|
+
const exportsWithCache = typeof wrangler.exports === "object" && wrangler.exports !== null && Object.values(wrangler.exports).some((entry) => typeof entry === "object" && entry !== null && entry.cache?.enabled === true);
|
|
513
|
+
if ((cacheEnabled || exportsWithCache) && ISO_DATE_PATTERN.test(compatibilityDate) && compatibilityDate < WORKERS_CACHE_MIN_DATE) {
|
|
514
|
+
errors.push(`cache.enabled requires compatibility_date >= "${WORKERS_CACHE_MIN_DATE}" (got "${compatibilityDate || "<missing>"}")`);
|
|
515
|
+
}
|
|
388
516
|
if (schema?.hasGlobalTable) {
|
|
389
517
|
const d1Bindings = wrangler.d1_databases ?? [];
|
|
390
518
|
const databaseBinding = d1Bindings.find((binding) => binding.binding === "DB");
|
|
@@ -398,6 +526,8 @@ const validateWranglerConfig = (wrangler, schema) => {
|
|
|
398
526
|
validateTailConsumers(wrangler, errors);
|
|
399
527
|
validateContainers(wrangler, errors, warnings);
|
|
400
528
|
validateWorkflows(wrangler, errors);
|
|
529
|
+
validateQueues(wrangler, errors);
|
|
530
|
+
validateSecretsStore(wrangler, errors);
|
|
401
531
|
for (const rule of HINT_BINDING_RULES) {
|
|
402
532
|
validateHintBinding(wrangler, rule, errors, warnings);
|
|
403
533
|
}
|
|
@@ -412,6 +542,8 @@ const validateWranglerConfig = (wrangler, schema) => {
|
|
|
412
542
|
validatePlacement(wrangler, errors);
|
|
413
543
|
validateObservability(wrangler, errors);
|
|
414
544
|
validateAssets(wrangler, errors);
|
|
545
|
+
validateCache(wrangler, errors);
|
|
546
|
+
validateExports(wrangler, errors);
|
|
415
547
|
validateCorsVariables(wrangler, errors);
|
|
416
548
|
return { errors, valid: errors.length === 0, warnings };
|
|
417
549
|
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { normalize, join, relative, isAbsolute, dirname } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const loadStudioAssets = (logger, resolveFrom = import.meta.url) => {
|
|
6
|
+
try {
|
|
7
|
+
const require = createRequire(resolveFrom);
|
|
8
|
+
return {
|
|
9
|
+
script: readFileSync(require.resolve("@lunora/studio/standalone/studio.js")),
|
|
10
|
+
styles: readFileSync(require.resolve("@lunora/studio/styles.css"))
|
|
11
|
+
};
|
|
12
|
+
} catch (error) {
|
|
13
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14
|
+
logger?.warnOnce?.(`[lunora] studio assets unavailable (build @lunora/studio?): ${message}`);
|
|
15
|
+
return void 0;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const studioAssetsStamp = (resolveFrom = import.meta.url) => {
|
|
19
|
+
try {
|
|
20
|
+
const require = createRequire(resolveFrom);
|
|
21
|
+
const scriptMtime = statSync(require.resolve("@lunora/studio/standalone/studio.js")).mtimeMs;
|
|
22
|
+
const stylesMtime = statSync(require.resolve("@lunora/studio/styles.css")).mtimeMs;
|
|
23
|
+
return Math.max(scriptMtime, stylesMtime);
|
|
24
|
+
} catch {
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const resolveStandaloneDirectory = (resolveFrom = import.meta.url) => {
|
|
29
|
+
try {
|
|
30
|
+
return dirname(createRequire(resolveFrom).resolve("@lunora/studio/standalone/studio.js"));
|
|
31
|
+
} catch {
|
|
32
|
+
return void 0;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const resolveContainedFile = (directory, fileName) => {
|
|
36
|
+
if (fileName === "" || fileName === "." || fileName === ".." || fileName.includes("/") || fileName.includes("\\") || fileName.includes("\0")) {
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
const resolved = normalize(join(directory, fileName));
|
|
40
|
+
const relativePath = relative(directory, resolved);
|
|
41
|
+
if (relativePath === "" || relativePath.startsWith("..") || isAbsolute(relativePath) || relativePath.includes("/") || relativePath.includes("\\")) {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
return resolved;
|
|
45
|
+
};
|
|
46
|
+
const readStandaloneAsset = (fileName, resolveFrom = import.meta.url) => {
|
|
47
|
+
const directory = resolveStandaloneDirectory(resolveFrom);
|
|
48
|
+
if (directory === void 0) {
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
const resolved = resolveContainedFile(directory, fileName);
|
|
52
|
+
if (resolved === void 0) {
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return readFileSync(resolved);
|
|
57
|
+
} catch {
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const isStandaloneModulePath = (pathname) => pathname.endsWith(".js") || pathname.endsWith(".js.map");
|
|
62
|
+
const assetContentType = (fileName) => {
|
|
63
|
+
if (fileName.endsWith(".css")) {
|
|
64
|
+
return "text/css; charset=utf-8";
|
|
65
|
+
}
|
|
66
|
+
if (fileName.endsWith(".map")) {
|
|
67
|
+
return "application/json; charset=utf-8";
|
|
68
|
+
}
|
|
69
|
+
return "text/javascript; charset=utf-8";
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export { assetContentType, isStandaloneModulePath, loadStudioAssets, readStandaloneAsset, resolveContainedFile, resolveStandaloneDirectory, studioAssetsStamp };
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, renameSync, rmSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, rmSync, statSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { LunoraError } from '@lunora/errors';
|
|
5
|
+
import { DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, splitDevVariableLine, parseDevVariableEntries, DEV_VARS_NEWLINE, unquoteDevVariable } from './DEV_VARS_EXAMPLE_FILE-dJPNTEnK.mjs';
|
|
6
|
+
import { CORE_SECRETS, PACKAGE_SECRETS_REGISTRY, secretsForPackages } from './PACKAGE_SECRETS_REGISTRY-BeRSvrl5.mjs';
|
|
6
7
|
|
|
8
|
+
const requiredSecrets = (packageNames) => [...CORE_SECRETS, ...secretsForPackages(packageNames)];
|
|
7
9
|
const SECRET_BYTES = 32;
|
|
8
10
|
const SECRET_KEY = /(?:KEY|PASSWORD|SECRET|TOKEN)$/u;
|
|
9
11
|
const PLACEHOLDER_MARKERS = [
|
|
@@ -45,6 +47,11 @@ const isPlaceholderValue = (value) => {
|
|
|
45
47
|
const isPlaceholder = (rawValue) => isPlaceholderValue(unquoteDevVariable(rawValue.trim()));
|
|
46
48
|
const defaultRandomHex = (bytes) => randomBytes(bytes).toString("hex");
|
|
47
49
|
const generatedSecretFor = (key, rawValue, randomHex) => SECRET_KEY.test(key) && isPlaceholder(rawValue) ? randomHex(SECRET_BYTES) : void 0;
|
|
50
|
+
const PROVIDER_SECRET_KEYS = new Set(
|
|
51
|
+
[...CORE_SECRETS, ...Object.values(PACKAGE_SECRETS_REGISTRY).flat()].filter((entry) => SECRET_KEY.test(entry.key) && entry.placeholderValue.startsWith("<")).map((entry) => entry.key)
|
|
52
|
+
);
|
|
53
|
+
const isMintableSecretKey = (key) => SECRET_KEY.test(key) && !PROVIDER_SECRET_KEYS.has(key);
|
|
54
|
+
const generateSecretValue = (randomHex = defaultRandomHex) => randomHex(SECRET_BYTES);
|
|
48
55
|
const planDevVariablesScaffold = (input) => {
|
|
49
56
|
if (input.devVarsExists) {
|
|
50
57
|
return { status: "exists" };
|
|
@@ -98,19 +105,48 @@ const atomicCreateDevVariables = (path, content) => {
|
|
|
98
105
|
throw error;
|
|
99
106
|
}
|
|
100
107
|
};
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
104
|
-
const content = `${existing}${separator}${additions.join("\n")}
|
|
105
|
-
`;
|
|
106
|
-
const temporaryPath = `${path}.tmp-${String(process.pid)}`;
|
|
108
|
+
const APPEND_MAX_ATTEMPTS = 5;
|
|
109
|
+
const fingerprintOf = (path) => {
|
|
107
110
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
} catch
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
const stats = statSync(path);
|
|
112
|
+
return { mtimeMs: stats.mtimeMs, size: stats.size };
|
|
113
|
+
} catch {
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const sameFingerprint = (a, b) => {
|
|
118
|
+
if (a === void 0 || b === void 0) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return a.mtimeMs === b.mtimeMs && a.size === b.size;
|
|
122
|
+
};
|
|
123
|
+
const appendDevVariables = (path, buildAdditions) => {
|
|
124
|
+
for (let attempt = 0; attempt < APPEND_MAX_ATTEMPTS; attempt += 1) {
|
|
125
|
+
const beforeFingerprint = fingerprintOf(path);
|
|
126
|
+
const existing = readFileSync(path, "utf8");
|
|
127
|
+
const additions = buildAdditions(existing);
|
|
128
|
+
if (additions.length === 0) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
132
|
+
const content = `${existing}${separator}${additions.join("\n")}
|
|
133
|
+
`;
|
|
134
|
+
const temporaryPath = `${path}.tmp-${String(process.pid)}-${randomBytes(6).toString("hex")}`;
|
|
135
|
+
try {
|
|
136
|
+
writeFileSync(temporaryPath, content, { encoding: "utf8", mode: 384 });
|
|
137
|
+
const beforeRenameFingerprint = fingerprintOf(path);
|
|
138
|
+
if (!sameFingerprint(beforeFingerprint, beforeRenameFingerprint)) {
|
|
139
|
+
rmSync(temporaryPath, { force: true });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
renameSync(temporaryPath, path);
|
|
143
|
+
return additions;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
rmSync(temporaryPath, { force: true });
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
113
148
|
}
|
|
149
|
+
throw new LunoraError("INTERNAL", `Failed to append to ${path} after ${String(APPEND_MAX_ATTEMPTS)} attempts — a concurrent writer kept winning the race.`);
|
|
114
150
|
};
|
|
115
151
|
const ensureDevVariables = async (deps) => {
|
|
116
152
|
const devVariablesPath = join(deps.cwd, DEV_VARS_FILE);
|
|
@@ -146,16 +182,24 @@ const ensureDevVariables = async (deps) => {
|
|
|
146
182
|
deps.info(`Skipped — add ${list} to ${DEV_VARS_FILE} when you're ready.`);
|
|
147
183
|
return { addedKeys: [], generatedKeys: [], status: "declined" };
|
|
148
184
|
}
|
|
149
|
-
appendDevVariables(
|
|
150
|
-
|
|
151
|
-
|
|
185
|
+
const written = appendDevVariables(
|
|
186
|
+
devVariablesPath,
|
|
187
|
+
(currentContent) => planDevVariablesAugment({ existingContent: currentContent, exampleContent, randomHex: deps.randomHex }).additions
|
|
188
|
+
);
|
|
189
|
+
const writtenKeys = written.map((line) => splitDevVariableLine(line)?.key).filter((key) => key !== void 0);
|
|
190
|
+
if (writtenKeys.length === 0) {
|
|
191
|
+
return { addedKeys: [], generatedKeys: [], status: "exists" };
|
|
192
|
+
}
|
|
193
|
+
const writtenGeneratedKeys = augment.generatedKeys.filter((key) => writtenKeys.includes(key));
|
|
194
|
+
deps.info(`Updated ${DEV_VARS_FILE} — added ${writtenKeys.join(", ")}${generatedSuffix(writtenGeneratedKeys)}.`);
|
|
195
|
+
return { addedKeys: writtenKeys, generatedKeys: writtenGeneratedKeys, status: "augmented" };
|
|
152
196
|
};
|
|
153
197
|
const secretEntryBlock = (entry) => {
|
|
154
198
|
const lines = [`# ${entry.description}`, `# Docs: ${entry.docsUrl}`, `${entry.key}="${entry.placeholderValue}"`];
|
|
155
199
|
return lines.join("\n");
|
|
156
200
|
};
|
|
157
201
|
const buildPackageSecretsBlock = (packageNames, existingKeys) => {
|
|
158
|
-
const entries =
|
|
202
|
+
const entries = requiredSecrets(packageNames).filter((entry) => !existingKeys.has(entry.key));
|
|
159
203
|
if (entries.length === 0) {
|
|
160
204
|
return "";
|
|
161
205
|
}
|
|
@@ -181,7 +225,57 @@ ${block}
|
|
|
181
225
|
rmSync(temporaryPath, { force: true });
|
|
182
226
|
throw error;
|
|
183
227
|
}
|
|
184
|
-
return
|
|
228
|
+
return requiredSecrets(packageNames).filter((entry) => !existingKeys.has(entry.key)).map((entry) => entry.key);
|
|
229
|
+
};
|
|
230
|
+
const planDevSecretsFill = (input) => {
|
|
231
|
+
const randomHex = input.randomHex ?? defaultRandomHex;
|
|
232
|
+
const filledKeys = [];
|
|
233
|
+
const lines = input.existingContent.split(DEV_VARS_NEWLINE).map((line) => {
|
|
234
|
+
const parsed = splitDevVariableLine(line);
|
|
235
|
+
const secret = parsed ? generatedSecretFor(parsed.key, parsed.value, randomHex) : void 0;
|
|
236
|
+
if (!parsed || secret === void 0) {
|
|
237
|
+
return line;
|
|
238
|
+
}
|
|
239
|
+
filledKeys.push(parsed.key);
|
|
240
|
+
return `${parsed.key}="${secret}"`;
|
|
241
|
+
});
|
|
242
|
+
const present = new Set(parseDevVariableEntries(input.existingContent).map((entry) => entry.key));
|
|
243
|
+
const addedKeys = [];
|
|
244
|
+
const additions = [];
|
|
245
|
+
for (const entry of CORE_SECRETS) {
|
|
246
|
+
if (present.has(entry.key)) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
addedKeys.push(entry.key);
|
|
250
|
+
additions.push(`# ${entry.description}`, `${entry.key}="${randomHex(SECRET_BYTES)}"`);
|
|
251
|
+
}
|
|
252
|
+
const body = lines.join("\n");
|
|
253
|
+
if (additions.length === 0) {
|
|
254
|
+
return { addedKeys, content: body, filledKeys };
|
|
255
|
+
}
|
|
256
|
+
const separator = body === "" || body.endsWith("\n") ? "" : "\n";
|
|
257
|
+
return { addedKeys, content: `${body}${separator}${additions.join("\n")}
|
|
258
|
+
`, filledKeys };
|
|
259
|
+
};
|
|
260
|
+
const fillDevSecrets = (deps) => {
|
|
261
|
+
const devVariablesPath = join(deps.cwd, DEV_VARS_FILE);
|
|
262
|
+
const exists = existsSync(devVariablesPath);
|
|
263
|
+
const existingContent = exists ? readFileSync(devVariablesPath, "utf8") : "";
|
|
264
|
+
const plan = planDevSecretsFill({ existingContent, randomHex: deps.randomHex });
|
|
265
|
+
if (plan.filledKeys.length === 0 && plan.addedKeys.length === 0) {
|
|
266
|
+
return { addedKeys: [], filledKeys: [], status: "unchanged" };
|
|
267
|
+
}
|
|
268
|
+
const temporaryPath = `${devVariablesPath}.tmp-${String(process.pid)}`;
|
|
269
|
+
try {
|
|
270
|
+
writeFileSync(temporaryPath, plan.content, { encoding: "utf8", mode: 384 });
|
|
271
|
+
renameSync(temporaryPath, devVariablesPath);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
rmSync(temporaryPath, { force: true });
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
const generated = [...plan.filledKeys, ...plan.addedKeys];
|
|
277
|
+
deps.info?.(`Generated ${String(generated.length)} dev secret(s) in ${DEV_VARS_FILE}: ${generated.join(", ")}`);
|
|
278
|
+
return { addedKeys: plan.addedKeys, filledKeys: plan.filledKeys, status: exists ? "filled" : "created" };
|
|
185
279
|
};
|
|
186
280
|
|
|
187
|
-
export { buildPackageSecretsBlock, ensureDevVariables, ensureDevVariablesExample as ensureDevVarsExample, isPlaceholderValue, planDevVariablesAugment, planDevVariablesScaffold };
|
|
281
|
+
export { buildPackageSecretsBlock, ensureDevVariables, ensureDevVariablesExample as ensureDevVarsExample, fillDevSecrets, generateSecretValue, isMintableSecretKey, isPlaceholderValue, planDevSecretsFill, planDevVariablesAugment, planDevVariablesScaffold, requiredSecrets };
|