@edge-base/cli 0.1.1
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/README.md +182 -0
- package/dist/commands/admin.d.ts +10 -0
- package/dist/commands/admin.d.ts.map +1 -0
- package/dist/commands/admin.js +307 -0
- package/dist/commands/admin.js.map +1 -0
- package/dist/commands/backup.d.ts +148 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +1247 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/completion.d.ts +3 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +168 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/create-plugin.d.ts +3 -0
- package/dist/commands/create-plugin.d.ts.map +1 -0
- package/dist/commands/create-plugin.js +208 -0
- package/dist/commands/create-plugin.js.map +1 -0
- package/dist/commands/deploy.d.ts +146 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +1823 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/describe.d.ts +45 -0
- package/dist/commands/describe.d.ts.map +1 -0
- package/dist/commands/describe.js +114 -0
- package/dist/commands/describe.js.map +1 -0
- package/dist/commands/destroy.d.ts +13 -0
- package/dist/commands/destroy.d.ts.map +1 -0
- package/dist/commands/destroy.js +642 -0
- package/dist/commands/destroy.js.map +1 -0
- package/dist/commands/dev.d.ts +80 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +1131 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/docker.d.ts +22 -0
- package/dist/commands/docker.d.ts.map +1 -0
- package/dist/commands/docker.js +373 -0
- package/dist/commands/docker.js.map +1 -0
- package/dist/commands/export.d.ts +15 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +142 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +506 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/keys.d.ts +23 -0
- package/dist/commands/keys.d.ts.map +1 -0
- package/dist/commands/keys.js +347 -0
- package/dist/commands/keys.js.map +1 -0
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +104 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/migrate.d.ts +29 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +302 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/migration.d.ts +18 -0
- package/dist/commands/migration.d.ts.map +1 -0
- package/dist/commands/migration.js +114 -0
- package/dist/commands/migration.js.map +1 -0
- package/dist/commands/neon.d.ts +66 -0
- package/dist/commands/neon.d.ts.map +1 -0
- package/dist/commands/neon.js +600 -0
- package/dist/commands/neon.js.map +1 -0
- package/dist/commands/plugins.d.ts +9 -0
- package/dist/commands/plugins.d.ts.map +1 -0
- package/dist/commands/plugins.js +295 -0
- package/dist/commands/plugins.js.map +1 -0
- package/dist/commands/realtime.d.ts +3 -0
- package/dist/commands/realtime.d.ts.map +1 -0
- package/dist/commands/realtime.js +71 -0
- package/dist/commands/realtime.js.map +1 -0
- package/dist/commands/secret.d.ts +7 -0
- package/dist/commands/secret.d.ts.map +1 -0
- package/dist/commands/secret.js +180 -0
- package/dist/commands/secret.js.map +1 -0
- package/dist/commands/seed.d.ts +21 -0
- package/dist/commands/seed.d.ts.map +1 -0
- package/dist/commands/seed.js +325 -0
- package/dist/commands/seed.js.map +1 -0
- package/dist/commands/telemetry.d.ts +12 -0
- package/dist/commands/telemetry.d.ts.map +1 -0
- package/dist/commands/telemetry.js +57 -0
- package/dist/commands/telemetry.js.map +1 -0
- package/dist/commands/typegen.d.ts +26 -0
- package/dist/commands/typegen.d.ts.map +1 -0
- package/dist/commands/typegen.js +212 -0
- package/dist/commands/typegen.js.map +1 -0
- package/dist/commands/upgrade.d.ts +29 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +265 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/webhook-test.d.ts +3 -0
- package/dist/commands/webhook-test.d.ts.map +1 -0
- package/dist/commands/webhook-test.js +133 -0
- package/dist/commands/webhook-test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +183 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/agent-contract.d.ts +36 -0
- package/dist/lib/agent-contract.d.ts.map +1 -0
- package/dist/lib/agent-contract.js +78 -0
- package/dist/lib/agent-contract.js.map +1 -0
- package/dist/lib/cf-auth.d.ts +76 -0
- package/dist/lib/cf-auth.d.ts.map +1 -0
- package/dist/lib/cf-auth.js +321 -0
- package/dist/lib/cf-auth.js.map +1 -0
- package/dist/lib/cli-context.d.ts +23 -0
- package/dist/lib/cli-context.d.ts.map +1 -0
- package/dist/lib/cli-context.js +40 -0
- package/dist/lib/cli-context.js.map +1 -0
- package/dist/lib/cloudflare-deploy-manifest.d.ts +26 -0
- package/dist/lib/cloudflare-deploy-manifest.d.ts.map +1 -0
- package/dist/lib/cloudflare-deploy-manifest.js +107 -0
- package/dist/lib/cloudflare-deploy-manifest.js.map +1 -0
- package/dist/lib/cloudflare-wrangler-resources.d.ts +32 -0
- package/dist/lib/cloudflare-wrangler-resources.d.ts.map +1 -0
- package/dist/lib/cloudflare-wrangler-resources.js +59 -0
- package/dist/lib/cloudflare-wrangler-resources.js.map +1 -0
- package/dist/lib/config-editor.d.ts +139 -0
- package/dist/lib/config-editor.d.ts.map +1 -0
- package/dist/lib/config-editor.js +1188 -0
- package/dist/lib/config-editor.js.map +1 -0
- package/dist/lib/deploy-shared.d.ts +55 -0
- package/dist/lib/deploy-shared.d.ts.map +1 -0
- package/dist/lib/deploy-shared.js +183 -0
- package/dist/lib/deploy-shared.js.map +1 -0
- package/dist/lib/dev-sidecar.d.ts +31 -0
- package/dist/lib/dev-sidecar.d.ts.map +1 -0
- package/dist/lib/dev-sidecar.js +1058 -0
- package/dist/lib/dev-sidecar.js.map +1 -0
- package/dist/lib/fetch-with-timeout.d.ts +14 -0
- package/dist/lib/fetch-with-timeout.d.ts.map +1 -0
- package/dist/lib/fetch-with-timeout.js +29 -0
- package/dist/lib/fetch-with-timeout.js.map +1 -0
- package/dist/lib/function-registry.d.ts +56 -0
- package/dist/lib/function-registry.d.ts.map +1 -0
- package/dist/lib/function-registry.js +210 -0
- package/dist/lib/function-registry.js.map +1 -0
- package/dist/lib/load-config.d.ts +24 -0
- package/dist/lib/load-config.d.ts.map +1 -0
- package/dist/lib/load-config.js +263 -0
- package/dist/lib/load-config.js.map +1 -0
- package/dist/lib/local-secrets.d.ts +2 -0
- package/dist/lib/local-secrets.d.ts.map +1 -0
- package/dist/lib/local-secrets.js +60 -0
- package/dist/lib/local-secrets.js.map +1 -0
- package/dist/lib/managed-resource-names.d.ts +4 -0
- package/dist/lib/managed-resource-names.d.ts.map +1 -0
- package/dist/lib/managed-resource-names.js +19 -0
- package/dist/lib/managed-resource-names.js.map +1 -0
- package/dist/lib/migrator.d.ts +57 -0
- package/dist/lib/migrator.d.ts.map +1 -0
- package/dist/lib/migrator.js +321 -0
- package/dist/lib/migrator.js.map +1 -0
- package/dist/lib/neon.d.ts +41 -0
- package/dist/lib/neon.d.ts.map +1 -0
- package/dist/lib/neon.js +325 -0
- package/dist/lib/neon.js.map +1 -0
- package/dist/lib/node-tools.d.ts +10 -0
- package/dist/lib/node-tools.d.ts.map +1 -0
- package/dist/lib/node-tools.js +32 -0
- package/dist/lib/node-tools.js.map +1 -0
- package/dist/lib/npm.d.ts +8 -0
- package/dist/lib/npm.d.ts.map +1 -0
- package/dist/lib/npm.js +10 -0
- package/dist/lib/npm.js.map +1 -0
- package/dist/lib/npx.d.ts +9 -0
- package/dist/lib/npx.d.ts.map +1 -0
- package/dist/lib/npx.js +11 -0
- package/dist/lib/npx.js.map +1 -0
- package/dist/lib/project-runtime.d.ts +38 -0
- package/dist/lib/project-runtime.d.ts.map +1 -0
- package/dist/lib/project-runtime.js +122 -0
- package/dist/lib/project-runtime.js.map +1 -0
- package/dist/lib/prompts.d.ts +28 -0
- package/dist/lib/prompts.d.ts.map +1 -0
- package/dist/lib/prompts.js +85 -0
- package/dist/lib/prompts.js.map +1 -0
- package/dist/lib/rate-limit-bindings.d.ts +11 -0
- package/dist/lib/rate-limit-bindings.d.ts.map +1 -0
- package/dist/lib/rate-limit-bindings.js +52 -0
- package/dist/lib/rate-limit-bindings.js.map +1 -0
- package/dist/lib/realtime-provision.d.ts +22 -0
- package/dist/lib/realtime-provision.d.ts.map +1 -0
- package/dist/lib/realtime-provision.js +246 -0
- package/dist/lib/realtime-provision.js.map +1 -0
- package/dist/lib/resolve-options.d.ts +42 -0
- package/dist/lib/resolve-options.d.ts.map +1 -0
- package/dist/lib/resolve-options.js +98 -0
- package/dist/lib/resolve-options.js.map +1 -0
- package/dist/lib/runtime-scaffold.d.ts +17 -0
- package/dist/lib/runtime-scaffold.d.ts.map +1 -0
- package/dist/lib/runtime-scaffold.js +366 -0
- package/dist/lib/runtime-scaffold.js.map +1 -0
- package/dist/lib/schema-check.d.ts +79 -0
- package/dist/lib/schema-check.d.ts.map +1 -0
- package/dist/lib/schema-check.js +347 -0
- package/dist/lib/schema-check.js.map +1 -0
- package/dist/lib/spinner.d.ts +20 -0
- package/dist/lib/spinner.d.ts.map +1 -0
- package/dist/lib/spinner.js +42 -0
- package/dist/lib/spinner.js.map +1 -0
- package/dist/lib/telemetry.d.ts +37 -0
- package/dist/lib/telemetry.d.ts.map +1 -0
- package/dist/lib/telemetry.js +98 -0
- package/dist/lib/telemetry.js.map +1 -0
- package/dist/lib/turnstile-provision.d.ts +27 -0
- package/dist/lib/turnstile-provision.d.ts.map +1 -0
- package/dist/lib/turnstile-provision.js +144 -0
- package/dist/lib/turnstile-provision.js.map +1 -0
- package/dist/lib/update-check.d.ts +13 -0
- package/dist/lib/update-check.d.ts.map +1 -0
- package/dist/lib/update-check.js +110 -0
- package/dist/lib/update-check.js.map +1 -0
- package/dist/lib/wrangler-secrets.d.ts +3 -0
- package/dist/lib/wrangler-secrets.d.ts.map +1 -0
- package/dist/lib/wrangler-secrets.js +32 -0
- package/dist/lib/wrangler-secrets.js.map +1 -0
- package/dist/lib/wrangler.d.ts +9 -0
- package/dist/lib/wrangler.d.ts.map +1 -0
- package/dist/lib/wrangler.js +84 -0
- package/dist/lib/wrangler.js.map +1 -0
- package/dist/templates/plugin/README.md.tmpl +91 -0
- package/dist/templates/plugin/client/js/package.json.tmpl +23 -0
- package/dist/templates/plugin/client/js/src/index.ts.tmpl +68 -0
- package/dist/templates/plugin/client/js/tsconfig.json.tmpl +14 -0
- package/dist/templates/plugin/server/package.json.tmpl +19 -0
- package/dist/templates/plugin/server/src/index.ts.tmpl +59 -0
- package/dist/templates/plugin/server/tsconfig.json.tmpl +14 -0
- package/llms.txt +94 -0
- package/package.json +60 -0
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev Sidecar HTTP Server — Schema editing bridge for admin dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Runs alongside `wrangler dev` on port+1 (default :8788).
|
|
5
|
+
* The dashboard UI calls this directly for schema mutations.
|
|
6
|
+
* After modifying edgebase.config.ts, the existing fs.watch in dev.ts
|
|
7
|
+
* detects the change and auto-restarts wrangler.
|
|
8
|
+
*
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
11
|
+
import { createServer } from 'node:http';
|
|
12
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
13
|
+
import { resolve, join } from 'node:path';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import * as joseLib from 'jose';
|
|
17
|
+
import * as configEditor from './config-editor.js';
|
|
18
|
+
import { loadConfigSafe } from './load-config.js';
|
|
19
|
+
import { getDefaultPostgresEnvKey, removeEnvValue, upsertEnvValue } from './neon.js';
|
|
20
|
+
import { execTsxSync } from './node-tools.js';
|
|
21
|
+
import { buildSnapshot, saveSnapshot } from './schema-check.js';
|
|
22
|
+
import { listAvailableNeonProjects, runNeonSetup, } from '../commands/neon.js';
|
|
23
|
+
const NEON_PROJECT_CACHE_TTL_MS = 60_000;
|
|
24
|
+
let neonProjectsCache = null;
|
|
25
|
+
let pgModulePromise = null;
|
|
26
|
+
let pgSchemaInitModulePromise = null;
|
|
27
|
+
const postgresPoolCache = new Map();
|
|
28
|
+
const LOCAL_ENV_HEADER = '# EdgeBase local development secrets';
|
|
29
|
+
const RELEASE_ENV_HEADER = '# EdgeBase production secrets';
|
|
30
|
+
const MANAGED_AUTH_ENV_KEYS = ['EDGEBASE_AUTH_ALLOWED_OAUTH_PROVIDERS'];
|
|
31
|
+
const MANAGED_AUTH_ENV_PREFIXES = ['EDGEBASE_OAUTH_', 'EDGEBASE_OIDC_'];
|
|
32
|
+
// ─── JWT Verification (HS256, matching server's jose-based signing) ───
|
|
33
|
+
async function verifyAdminJwt(token, secret) {
|
|
34
|
+
try {
|
|
35
|
+
const secretKey = new TextEncoder().encode(secret);
|
|
36
|
+
await joseLib.jwtVerify(token, secretKey, { issuer: 'edgebase:admin' });
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function verifyAuth(req, adminSecret) {
|
|
44
|
+
const internalSecret = req.headers['x-edgebase-internal-secret'] ??
|
|
45
|
+
req.headers['X-EdgeBase-Internal-Secret'];
|
|
46
|
+
const internalValue = Array.isArray(internalSecret) ? internalSecret[0] : internalSecret;
|
|
47
|
+
if (internalValue && internalValue === adminSecret) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
const auth = req.headers.authorization;
|
|
51
|
+
if (!auth?.startsWith('Bearer '))
|
|
52
|
+
return false;
|
|
53
|
+
const token = auth.slice(7);
|
|
54
|
+
return verifyAdminJwt(token, adminSecret);
|
|
55
|
+
}
|
|
56
|
+
// ─── HTTP Helpers ───
|
|
57
|
+
function json(res, status, data) {
|
|
58
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
59
|
+
res.end(JSON.stringify(data));
|
|
60
|
+
}
|
|
61
|
+
function readBody(req) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const chunks = [];
|
|
64
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
65
|
+
req.on('end', () => {
|
|
66
|
+
try {
|
|
67
|
+
const text = Buffer.concat(chunks).toString('utf-8');
|
|
68
|
+
resolve(text ? JSON.parse(text) : {});
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
reject(new Error('Invalid JSON body'));
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
req.on('error', reject);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parse URL path into segments and extract params.
|
|
79
|
+
* E.g., '/schema/tables/posts/columns/title' → ['schema', 'tables', 'posts', 'columns', 'title']
|
|
80
|
+
*/
|
|
81
|
+
function parsePath(urlPath) {
|
|
82
|
+
return urlPath.split('/').filter(Boolean);
|
|
83
|
+
}
|
|
84
|
+
function isDynamicDbBlock(dbBlock) {
|
|
85
|
+
if (!dbBlock)
|
|
86
|
+
return false;
|
|
87
|
+
return !!(dbBlock.instance || dbBlock.access?.canCreate || dbBlock.access?.access);
|
|
88
|
+
}
|
|
89
|
+
function quoteIdentifier(identifier) {
|
|
90
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
91
|
+
}
|
|
92
|
+
function isMissingTableError(message) {
|
|
93
|
+
return /no such table|does not exist|unknown table/i.test(message);
|
|
94
|
+
}
|
|
95
|
+
function parseAuthSettingsTarget(value) {
|
|
96
|
+
return value === 'release' ? 'release' : 'development';
|
|
97
|
+
}
|
|
98
|
+
function isManagedAuthEnvKey(key) {
|
|
99
|
+
return MANAGED_AUTH_ENV_KEYS.includes(key)
|
|
100
|
+
|| MANAGED_AUTH_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
101
|
+
}
|
|
102
|
+
function readAuthEnvValues(projectDir, target) {
|
|
103
|
+
if (target === 'release') {
|
|
104
|
+
return parseEnvFile(join(projectDir, '.env.release'));
|
|
105
|
+
}
|
|
106
|
+
return parseDevVars(projectDir);
|
|
107
|
+
}
|
|
108
|
+
function syncSidecarProcessEnv(projectDir, target = 'development') {
|
|
109
|
+
const envValues = readAuthEnvValues(projectDir, target);
|
|
110
|
+
for (const key of Object.keys(process.env)) {
|
|
111
|
+
if (!isManagedAuthEnvKey(key))
|
|
112
|
+
continue;
|
|
113
|
+
if (!(key in envValues)) {
|
|
114
|
+
delete process.env[key];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const [key, value] of Object.entries(envValues)) {
|
|
118
|
+
process.env[key] = value;
|
|
119
|
+
}
|
|
120
|
+
return envValues;
|
|
121
|
+
}
|
|
122
|
+
function loadSidecarConfig(opts, target = 'development') {
|
|
123
|
+
syncSidecarProcessEnv(opts.projectDir, target);
|
|
124
|
+
return loadConfigSafe(opts.configPath, opts.projectDir, {
|
|
125
|
+
allowRegexFallback: false,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function normalizeEnvSegment(value) {
|
|
129
|
+
return value
|
|
130
|
+
.trim()
|
|
131
|
+
.replace(/[^A-Za-z0-9]+/g, '_')
|
|
132
|
+
.replace(/^_+|_+$/g, '')
|
|
133
|
+
.toUpperCase();
|
|
134
|
+
}
|
|
135
|
+
function getOAuthEnvKeys(provider) {
|
|
136
|
+
if (provider.startsWith('oidc:')) {
|
|
137
|
+
const oidcName = normalizeEnvSegment(provider.slice(5)) || 'CUSTOM';
|
|
138
|
+
return {
|
|
139
|
+
clientId: `EDGEBASE_OIDC_${oidcName}_CLIENT_ID`,
|
|
140
|
+
clientSecret: `EDGEBASE_OIDC_${oidcName}_CLIENT_SECRET`,
|
|
141
|
+
issuer: `EDGEBASE_OIDC_${oidcName}_ISSUER`,
|
|
142
|
+
scopes: `EDGEBASE_OIDC_${oidcName}_SCOPES`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const providerName = normalizeEnvSegment(provider) || 'CUSTOM';
|
|
146
|
+
return {
|
|
147
|
+
clientId: `EDGEBASE_OAUTH_${providerName}_CLIENT_ID`,
|
|
148
|
+
clientSecret: `EDGEBASE_OAUTH_${providerName}_CLIENT_SECRET`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function getEnvTargets(projectDir, target) {
|
|
152
|
+
if (target === 'release') {
|
|
153
|
+
return [{
|
|
154
|
+
filePath: join(projectDir, '.env.release'),
|
|
155
|
+
header: RELEASE_ENV_HEADER,
|
|
156
|
+
}];
|
|
157
|
+
}
|
|
158
|
+
const envDevelopmentPath = join(projectDir, '.env.development');
|
|
159
|
+
const devVarsPath = join(projectDir, '.dev.vars');
|
|
160
|
+
if (existsSync(envDevelopmentPath) || !existsSync(devVarsPath)) {
|
|
161
|
+
return [
|
|
162
|
+
{ filePath: envDevelopmentPath, header: LOCAL_ENV_HEADER },
|
|
163
|
+
{ filePath: devVarsPath, header: LOCAL_ENV_HEADER },
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
return [{ filePath: devVarsPath, header: LOCAL_ENV_HEADER }];
|
|
167
|
+
}
|
|
168
|
+
function updateEnvValue(projectDir, target, key, value) {
|
|
169
|
+
for (const { filePath, header } of getEnvTargets(projectDir, target)) {
|
|
170
|
+
if (value === null || value.trim() === '') {
|
|
171
|
+
removeEnvValue(filePath, key);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
upsertEnvValue(filePath, key, value, header);
|
|
175
|
+
}
|
|
176
|
+
if (value === null || value.trim() === '') {
|
|
177
|
+
delete process.env[key];
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
process.env[key] = value;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function syncOAuthSecretsToLocalEnv(projectDir, target, allowedOAuthProviders, oauth) {
|
|
184
|
+
updateEnvValue(projectDir, target, 'EDGEBASE_AUTH_ALLOWED_OAUTH_PROVIDERS', Array.isArray(allowedOAuthProviders) && allowedOAuthProviders.length > 0
|
|
185
|
+
? allowedOAuthProviders.join(',')
|
|
186
|
+
: null);
|
|
187
|
+
if (!oauth)
|
|
188
|
+
return;
|
|
189
|
+
for (const [provider, config] of Object.entries(oauth)) {
|
|
190
|
+
const envKeys = getOAuthEnvKeys(provider);
|
|
191
|
+
updateEnvValue(projectDir, target, envKeys.clientId, typeof config.clientId === 'string' ? config.clientId : null);
|
|
192
|
+
updateEnvValue(projectDir, target, envKeys.clientSecret, typeof config.clientSecret === 'string' ? config.clientSecret : null);
|
|
193
|
+
if (envKeys.issuer) {
|
|
194
|
+
updateEnvValue(projectDir, target, envKeys.issuer, typeof config.issuer === 'string' ? config.issuer : null);
|
|
195
|
+
}
|
|
196
|
+
if (envKeys.scopes) {
|
|
197
|
+
const scopesValue = Array.isArray(config.scopes)
|
|
198
|
+
? config.scopes.map((scope) => scope.trim()).filter(Boolean).join(',')
|
|
199
|
+
: null;
|
|
200
|
+
updateEnvValue(projectDir, target, envKeys.scopes, scopesValue && scopesValue.length > 0 ? scopesValue : null);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function readAuthSettings(config) {
|
|
205
|
+
const authConfig = config.auth ?? {};
|
|
206
|
+
const oauthEntries = {};
|
|
207
|
+
const oauthConfig = authConfig.oauth ?? {};
|
|
208
|
+
for (const [provider, value] of Object.entries(oauthConfig)) {
|
|
209
|
+
if (!value || typeof value !== 'object')
|
|
210
|
+
continue;
|
|
211
|
+
const providerValue = value;
|
|
212
|
+
if (provider === 'oidc') {
|
|
213
|
+
for (const [oidcName, oidcValue] of Object.entries(providerValue)) {
|
|
214
|
+
if (!oidcValue || typeof oidcValue !== 'object')
|
|
215
|
+
continue;
|
|
216
|
+
const oidcRecord = oidcValue;
|
|
217
|
+
oauthEntries[`oidc:${oidcName}`] = {
|
|
218
|
+
clientId: typeof oidcRecord.clientId === 'string' ? oidcRecord.clientId : null,
|
|
219
|
+
clientSecret: typeof oidcRecord.clientSecret === 'string' ? oidcRecord.clientSecret : null,
|
|
220
|
+
issuer: typeof oidcRecord.issuer === 'string' ? oidcRecord.issuer : null,
|
|
221
|
+
scopes: Array.isArray(oidcRecord.scopes)
|
|
222
|
+
? oidcRecord.scopes.filter((scope) => typeof scope === 'string')
|
|
223
|
+
: [],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
oauthEntries[provider] = {
|
|
229
|
+
clientId: typeof providerValue.clientId === 'string' ? providerValue.clientId : null,
|
|
230
|
+
clientSecret: typeof providerValue.clientSecret === 'string' ? providerValue.clientSecret : null,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
providers: Array.isArray(authConfig.allowedOAuthProviders) ? authConfig.allowedOAuthProviders : [],
|
|
235
|
+
emailAuth: authConfig.emailAuth !== false,
|
|
236
|
+
anonymousAuth: !!authConfig.anonymousAuth,
|
|
237
|
+
allowedRedirectUrls: Array.isArray(authConfig.allowedRedirectUrls) ? authConfig.allowedRedirectUrls : [],
|
|
238
|
+
session: {
|
|
239
|
+
accessTokenTTL: authConfig.session?.accessTokenTTL ?? null,
|
|
240
|
+
refreshTokenTTL: authConfig.session?.refreshTokenTTL ?? null,
|
|
241
|
+
maxActiveSessions: typeof authConfig.session?.maxActiveSessions === 'number'
|
|
242
|
+
? authConfig.session.maxActiveSessions
|
|
243
|
+
: null,
|
|
244
|
+
},
|
|
245
|
+
magicLink: {
|
|
246
|
+
enabled: !!authConfig.magicLink?.enabled,
|
|
247
|
+
autoCreate: authConfig.magicLink?.autoCreate !== false,
|
|
248
|
+
tokenTTL: authConfig.magicLink?.tokenTTL ?? null,
|
|
249
|
+
},
|
|
250
|
+
emailOtp: {
|
|
251
|
+
enabled: !!authConfig.emailOtp?.enabled,
|
|
252
|
+
autoCreate: authConfig.emailOtp?.autoCreate !== false,
|
|
253
|
+
},
|
|
254
|
+
passkeys: {
|
|
255
|
+
enabled: !!authConfig.passkeys?.enabled,
|
|
256
|
+
rpName: authConfig.passkeys?.rpName ?? null,
|
|
257
|
+
rpID: authConfig.passkeys?.rpID ?? null,
|
|
258
|
+
origin: Array.isArray(authConfig.passkeys?.origin)
|
|
259
|
+
? authConfig.passkeys.origin
|
|
260
|
+
: authConfig.passkeys?.origin
|
|
261
|
+
? [authConfig.passkeys.origin]
|
|
262
|
+
: [],
|
|
263
|
+
},
|
|
264
|
+
oauth: oauthEntries,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async function loadPgModule() {
|
|
268
|
+
if (!pgModulePromise) {
|
|
269
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
270
|
+
pgModulePromise = dynamicImport('pg').then((mod) => mod);
|
|
271
|
+
}
|
|
272
|
+
return pgModulePromise;
|
|
273
|
+
}
|
|
274
|
+
async function loadPgSchemaInitModule(opts) {
|
|
275
|
+
if (!pgSchemaInitModulePromise) {
|
|
276
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
277
|
+
const moduleUrl = pathToFileURL(resolve(opts.projectDir, 'packages/server/src/lib/postgres-schema-init.ts')).href;
|
|
278
|
+
pgSchemaInitModulePromise = dynamicImport(moduleUrl).then((mod) => mod);
|
|
279
|
+
}
|
|
280
|
+
return pgSchemaInitModulePromise;
|
|
281
|
+
}
|
|
282
|
+
async function getSidecarPostgresPool(connectionString) {
|
|
283
|
+
let pool = postgresPoolCache.get(connectionString);
|
|
284
|
+
if (!pool) {
|
|
285
|
+
const { Pool } = await loadPgModule();
|
|
286
|
+
pool = new Pool({
|
|
287
|
+
connectionString,
|
|
288
|
+
max: 8,
|
|
289
|
+
});
|
|
290
|
+
postgresPoolCache.set(connectionString, pool);
|
|
291
|
+
}
|
|
292
|
+
return pool;
|
|
293
|
+
}
|
|
294
|
+
async function ensureSidecarPostgresSchema(opts, namespace) {
|
|
295
|
+
const config = loadSidecarConfig(opts);
|
|
296
|
+
const dbBlock = config.databases?.[namespace];
|
|
297
|
+
if (!dbBlock) {
|
|
298
|
+
throw new Error(`Namespace '${namespace}' not found.`);
|
|
299
|
+
}
|
|
300
|
+
if (isDynamicDbBlock(dbBlock)) {
|
|
301
|
+
throw new Error(`Namespace '${namespace}' is dynamic and cannot use PostgreSQL schema ensure.`);
|
|
302
|
+
}
|
|
303
|
+
const connectionString = resolveSidecarPostgresConnectionString(opts, namespace);
|
|
304
|
+
const pool = await getSidecarPostgresPool(connectionString);
|
|
305
|
+
const { ensurePgSchema } = await loadPgSchemaInitModule(opts);
|
|
306
|
+
await ensurePgSchema(connectionString, namespace, dbBlock.tables ?? {}, async (sql, params = []) => {
|
|
307
|
+
const result = await pool.query(sql, params);
|
|
308
|
+
const rows = (result.rows ?? []);
|
|
309
|
+
return {
|
|
310
|
+
columns: (result.fields ?? []).map((field) => field.name),
|
|
311
|
+
rows,
|
|
312
|
+
rowCount: result.rowCount ?? rows.length,
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function resolveSidecarPostgresConnectionString(opts, namespace) {
|
|
317
|
+
const config = loadSidecarConfig(opts);
|
|
318
|
+
const dbBlock = config.databases?.[namespace];
|
|
319
|
+
if (!dbBlock) {
|
|
320
|
+
throw new Error(`Namespace '${namespace}' not found.`);
|
|
321
|
+
}
|
|
322
|
+
if (isDynamicDbBlock(dbBlock)) {
|
|
323
|
+
throw new Error(`Namespace '${namespace}' is dynamic and cannot use the PostgreSQL sidecar.`);
|
|
324
|
+
}
|
|
325
|
+
if (dbBlock.provider !== 'postgres' && dbBlock.provider !== 'neon') {
|
|
326
|
+
throw new Error(`Namespace '${namespace}' is not PostgreSQL-backed.`);
|
|
327
|
+
}
|
|
328
|
+
const envValues = parseDevVars(opts.projectDir);
|
|
329
|
+
const bindingName = `DB_POSTGRES_${namespace.toUpperCase().replace(/-/g, '_')}`;
|
|
330
|
+
const envKey = dbBlock.connectionString ?? `${bindingName}_URL`;
|
|
331
|
+
const connectionString = envValues[envKey];
|
|
332
|
+
if (!connectionString) {
|
|
333
|
+
throw new Error(`PostgreSQL connection '${envKey}' not found for '${namespace}'.`);
|
|
334
|
+
}
|
|
335
|
+
return connectionString;
|
|
336
|
+
}
|
|
337
|
+
async function executeWorkerSql(opts, authorization, namespace, sql, params = []) {
|
|
338
|
+
const response = await fetch(`http://127.0.0.1:${opts.workerPort}/admin/api/data/sql`, {
|
|
339
|
+
method: 'POST',
|
|
340
|
+
headers: {
|
|
341
|
+
'Content-Type': 'application/json',
|
|
342
|
+
Authorization: authorization,
|
|
343
|
+
},
|
|
344
|
+
body: JSON.stringify({ namespace, sql, params }),
|
|
345
|
+
});
|
|
346
|
+
if (response.ok) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
let message = `SQL execution failed with ${response.status}`;
|
|
350
|
+
try {
|
|
351
|
+
const payload = await response.json();
|
|
352
|
+
if (payload.message) {
|
|
353
|
+
message = payload.message;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Ignore non-JSON responses and fall back to the status-derived message.
|
|
358
|
+
}
|
|
359
|
+
throw new Error(message);
|
|
360
|
+
}
|
|
361
|
+
async function callWorkerAdmin(opts, authorization, path, body) {
|
|
362
|
+
const response = await fetch(`http://127.0.0.1:${opts.workerPort}/admin/api/${path}`, {
|
|
363
|
+
method: body === undefined ? 'GET' : 'POST',
|
|
364
|
+
headers: {
|
|
365
|
+
'Content-Type': 'application/json',
|
|
366
|
+
Authorization: authorization,
|
|
367
|
+
},
|
|
368
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
369
|
+
});
|
|
370
|
+
if (!response.ok) {
|
|
371
|
+
let message = `${path} failed with ${response.status}`;
|
|
372
|
+
try {
|
|
373
|
+
const payload = await response.json();
|
|
374
|
+
if (payload.message) {
|
|
375
|
+
message = payload.message;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
// Keep the status-derived fallback.
|
|
380
|
+
}
|
|
381
|
+
throw new Error(message);
|
|
382
|
+
}
|
|
383
|
+
return response.json();
|
|
384
|
+
}
|
|
385
|
+
function getEffectiveSidecarProvider(config, dbKey) {
|
|
386
|
+
const dbBlock = config.databases?.[dbKey];
|
|
387
|
+
if (!dbBlock) {
|
|
388
|
+
throw new Error(`Database block '${dbKey}' not found.`);
|
|
389
|
+
}
|
|
390
|
+
if (isDynamicDbBlock(dbBlock))
|
|
391
|
+
return 'do';
|
|
392
|
+
if (dbBlock.provider === 'postgres' || dbBlock.provider === 'neon')
|
|
393
|
+
return 'postgres';
|
|
394
|
+
if (dbBlock.provider === 'do')
|
|
395
|
+
return 'do';
|
|
396
|
+
return 'd1';
|
|
397
|
+
}
|
|
398
|
+
function saveCurrentSnapshot(opts) {
|
|
399
|
+
const config = loadSidecarConfig(opts);
|
|
400
|
+
const snapshot = buildSnapshot(config.databases ?? {}, config.auth?.provider);
|
|
401
|
+
saveSnapshot(opts.projectDir, snapshot);
|
|
402
|
+
}
|
|
403
|
+
function resolveRequestedPostgresEnvKey(namespace, envKey) {
|
|
404
|
+
const trimmed = envKey?.trim();
|
|
405
|
+
return trimmed || getDefaultPostgresEnvKey(namespace);
|
|
406
|
+
}
|
|
407
|
+
async function waitForNamespaceProvider(opts, authorization, dbKey, expectedProvider, timeoutMs = 45_000) {
|
|
408
|
+
const deadline = Date.now() + timeoutMs;
|
|
409
|
+
let lastError = null;
|
|
410
|
+
while (Date.now() < deadline) {
|
|
411
|
+
try {
|
|
412
|
+
const payload = await callWorkerAdmin(opts, authorization, 'data/schema');
|
|
413
|
+
const provider = payload.namespaces?.[dbKey]?.provider;
|
|
414
|
+
if (provider === expectedProvider) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
lastError = new Error(provider
|
|
418
|
+
? `Namespace '${dbKey}' is still running on '${provider}'.`
|
|
419
|
+
: `Namespace '${dbKey}' is not available yet.`);
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
lastError = err instanceof Error ? err : new Error('Worker is not ready yet.');
|
|
423
|
+
}
|
|
424
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
425
|
+
}
|
|
426
|
+
throw lastError ?? new Error(`Timed out waiting for namespace '${dbKey}' to restart.`);
|
|
427
|
+
}
|
|
428
|
+
export async function renameBackingTable(opts, authorization, dbKey, oldName, newName) {
|
|
429
|
+
const config = loadSidecarConfig(opts);
|
|
430
|
+
const dbBlock = config.databases?.[dbKey];
|
|
431
|
+
if (!dbBlock) {
|
|
432
|
+
throw new Error(`Database block '${dbKey}' not found.`);
|
|
433
|
+
}
|
|
434
|
+
if (isDynamicDbBlock(dbBlock)) {
|
|
435
|
+
throw new Error(`Table rename is not supported for dynamic namespace '${dbKey}' because EdgeBase cannot rename every tenant instance automatically yet.`);
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
await executeWorkerSql(opts, authorization, dbKey, `ALTER TABLE ${quoteIdentifier(oldName)} RENAME TO ${quoteIdentifier(newName)}`);
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
const message = err instanceof Error ? err.message : 'Failed to rename backing table.';
|
|
442
|
+
if (!isMissingTableError(message)) {
|
|
443
|
+
throw err;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// ─── Schema Reader ───
|
|
448
|
+
function readCurrentSchema(configPath) {
|
|
449
|
+
try {
|
|
450
|
+
// Use a quick approach: read config via tsx eval
|
|
451
|
+
const projectDir = resolve(configPath, '..');
|
|
452
|
+
const result = execTsxSync([
|
|
453
|
+
'-e',
|
|
454
|
+
`import c from ${JSON.stringify(configPath)}; const d=c.default??c; const s={}; for (const [ns,b] of Object.entries(d.databases??{})) { for (const [t,tc] of Object.entries((b as any).tables??{})) { s[t]={namespace:ns,fields:(tc as any).schema??{},fts:(tc as any).fts??[]}; } } console.log(JSON.stringify(s));`,
|
|
455
|
+
], { cwd: projectDir, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
456
|
+
return JSON.parse(result);
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
return {};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// ─── Route Handler ───
|
|
463
|
+
async function handleRoute(req, res, opts) {
|
|
464
|
+
const url = new URL(req.url, `http://localhost:${opts.port}`);
|
|
465
|
+
const segments = parsePath(url.pathname);
|
|
466
|
+
const method = req.method;
|
|
467
|
+
const editorOpts = { configPath: opts.configPath };
|
|
468
|
+
// GET /dev/status
|
|
469
|
+
if (method === 'GET' && segments[0] === 'dev' && segments[1] === 'status') {
|
|
470
|
+
json(res, 200, { mode: 'dev', configPath: opts.configPath, port: opts.port });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
// POST /postgres/query — pooled local PostgreSQL query bridge for Worker dev mode
|
|
474
|
+
if (method === 'POST' && segments[0] === 'postgres' && segments[1] === 'query' && segments.length === 2) {
|
|
475
|
+
const body = await readBody(req);
|
|
476
|
+
const namespace = body.namespace;
|
|
477
|
+
const sql = body.sql;
|
|
478
|
+
const params = Array.isArray(body.params) ? body.params : [];
|
|
479
|
+
if (!namespace)
|
|
480
|
+
throw new Error('namespace is required.');
|
|
481
|
+
if (!sql)
|
|
482
|
+
throw new Error('sql is required.');
|
|
483
|
+
const connectionString = resolveSidecarPostgresConnectionString(opts, namespace);
|
|
484
|
+
const pool = await getSidecarPostgresPool(connectionString);
|
|
485
|
+
const result = await pool.query(sql, params);
|
|
486
|
+
const rows = (result.rows ?? []);
|
|
487
|
+
json(res, 200, {
|
|
488
|
+
columns: (result.fields ?? []).map((field) => field.name),
|
|
489
|
+
rows,
|
|
490
|
+
rowCount: result.rowCount ?? rows.length,
|
|
491
|
+
});
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
// POST /postgres/ensure-schema — persistent local PostgreSQL schema warmup/cache
|
|
495
|
+
if (method === 'POST' && segments[0] === 'postgres' && segments[1] === 'ensure-schema' && segments.length === 2) {
|
|
496
|
+
const body = await readBody(req);
|
|
497
|
+
const namespace = body.namespace;
|
|
498
|
+
if (!namespace)
|
|
499
|
+
throw new Error('namespace is required.');
|
|
500
|
+
await ensureSidecarPostgresSchema(opts, namespace);
|
|
501
|
+
json(res, 200, { ok: true });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
// GET /schema — read current schema
|
|
505
|
+
if (method === 'GET' && segments[0] === 'schema' && segments.length === 1) {
|
|
506
|
+
const schema = readCurrentSchema(opts.configPath);
|
|
507
|
+
json(res, 200, { ok: true, schema });
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// POST /schema/tables — create table
|
|
511
|
+
if (method === 'POST' && segments[0] === 'schema' && segments[1] === 'tables' && segments.length === 2) {
|
|
512
|
+
const body = await readBody(req);
|
|
513
|
+
const dbKey = body.dbKey || 'shared';
|
|
514
|
+
const name = body.name;
|
|
515
|
+
const schema = body.schema || {};
|
|
516
|
+
if (!name)
|
|
517
|
+
throw new Error('Table name is required.');
|
|
518
|
+
await configEditor.addTable(editorOpts, dbKey, name, schema);
|
|
519
|
+
json(res, 201, { ok: true, message: `Table '${name}' created in '${dbKey}'.` });
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
// POST /schema/databases — create database block
|
|
523
|
+
if (method === 'POST' && segments[0] === 'schema' && segments[1] === 'databases' && segments.length === 2) {
|
|
524
|
+
const body = await readBody(req);
|
|
525
|
+
const name = body.name;
|
|
526
|
+
const topology = body.topology ?? 'single';
|
|
527
|
+
const provider = body.provider;
|
|
528
|
+
const connectionString = body.connectionString;
|
|
529
|
+
const targetLabel = body.targetLabel;
|
|
530
|
+
const placeholder = body.placeholder;
|
|
531
|
+
const helperText = body.helperText;
|
|
532
|
+
if (!name)
|
|
533
|
+
throw new Error('Database block name is required.');
|
|
534
|
+
await configEditor.addDatabaseBlock(editorOpts, name, {
|
|
535
|
+
topology,
|
|
536
|
+
provider,
|
|
537
|
+
connectionString,
|
|
538
|
+
targetLabel,
|
|
539
|
+
placeholder,
|
|
540
|
+
helperText,
|
|
541
|
+
});
|
|
542
|
+
json(res, 201, { ok: true, message: `Database block '${name}' created.` });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// POST /integrations/neon/databases — create postgres DB block + configure Neon envs
|
|
546
|
+
if (method === 'POST' && segments[0] === 'integrations' && segments[1] === 'neon' && segments[2] === 'databases' && segments.length === 3) {
|
|
547
|
+
const body = await readBody(req);
|
|
548
|
+
const name = body.name;
|
|
549
|
+
const topology = body.topology ?? 'single';
|
|
550
|
+
const envKey = body.envKey;
|
|
551
|
+
const projectName = body.projectName;
|
|
552
|
+
const projectId = body.projectId;
|
|
553
|
+
const mode = (body.mode ?? 'reuse');
|
|
554
|
+
const targetLabel = body.targetLabel;
|
|
555
|
+
const placeholder = body.placeholder;
|
|
556
|
+
const helperText = body.helperText;
|
|
557
|
+
if (!name)
|
|
558
|
+
throw new Error('Database block name is required.');
|
|
559
|
+
if (topology !== 'single')
|
|
560
|
+
throw new Error('Neon helper only supports single-instance database blocks.');
|
|
561
|
+
const effectiveEnvKey = resolveRequestedPostgresEnvKey(name, envKey);
|
|
562
|
+
const neonResult = await runNeonSetup({
|
|
563
|
+
projectDir: opts.projectDir,
|
|
564
|
+
namespace: name,
|
|
565
|
+
envKeyOverride: effectiveEnvKey,
|
|
566
|
+
targetLabelOverride: name,
|
|
567
|
+
projectName,
|
|
568
|
+
projectId,
|
|
569
|
+
projectMode: mode,
|
|
570
|
+
});
|
|
571
|
+
await configEditor.addDatabaseBlock(editorOpts, name, {
|
|
572
|
+
topology: 'single',
|
|
573
|
+
provider: 'postgres',
|
|
574
|
+
connectionString: neonResult.target.envKey,
|
|
575
|
+
targetLabel,
|
|
576
|
+
placeholder,
|
|
577
|
+
helperText,
|
|
578
|
+
});
|
|
579
|
+
neonProjectsCache = null;
|
|
580
|
+
saveCurrentSnapshot(opts);
|
|
581
|
+
json(res, 201, {
|
|
582
|
+
ok: true,
|
|
583
|
+
mode,
|
|
584
|
+
envKey: neonResult.target.envKey,
|
|
585
|
+
projectName: neonResult.projectName,
|
|
586
|
+
message: `Database block '${name}' created and connected to Neon.`,
|
|
587
|
+
});
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
// POST /integrations/neon/connect — configure Neon envs for an existing postgres DB block
|
|
591
|
+
if (method === 'POST' && segments[0] === 'integrations' && segments[1] === 'neon' && segments[2] === 'connect' && segments.length === 3) {
|
|
592
|
+
const body = await readBody(req);
|
|
593
|
+
const namespace = body.namespace;
|
|
594
|
+
const envKey = body.envKey;
|
|
595
|
+
const projectId = body.projectId;
|
|
596
|
+
const mode = (body.mode ?? 'reuse');
|
|
597
|
+
if (!namespace)
|
|
598
|
+
throw new Error('namespace is required.');
|
|
599
|
+
const config = loadSidecarConfig(opts);
|
|
600
|
+
const provider = getEffectiveSidecarProvider(config, namespace);
|
|
601
|
+
if (provider === 'do') {
|
|
602
|
+
throw new Error(`Namespace '${namespace}' is not eligible for Neon because it uses Durable Objects.`);
|
|
603
|
+
}
|
|
604
|
+
if (provider === 'd1') {
|
|
605
|
+
throw new Error(`Namespace '${namespace}' still uses D1. Use the D1 upgrade flow instead of reconnect.`);
|
|
606
|
+
}
|
|
607
|
+
const neonResult = await runNeonSetup({
|
|
608
|
+
projectDir: opts.projectDir,
|
|
609
|
+
namespace,
|
|
610
|
+
envKeyOverride: envKey,
|
|
611
|
+
targetLabelOverride: namespace,
|
|
612
|
+
projectId,
|
|
613
|
+
projectMode: mode,
|
|
614
|
+
});
|
|
615
|
+
await configEditor.updateDatabaseBlock(editorOpts, namespace, {
|
|
616
|
+
provider: 'postgres',
|
|
617
|
+
connectionString: neonResult.target.envKey,
|
|
618
|
+
});
|
|
619
|
+
neonProjectsCache = null;
|
|
620
|
+
saveCurrentSnapshot(opts);
|
|
621
|
+
json(res, 200, {
|
|
622
|
+
ok: true,
|
|
623
|
+
mode,
|
|
624
|
+
envKey: neonResult.target.envKey,
|
|
625
|
+
projectName: neonResult.projectName,
|
|
626
|
+
message: `Neon connection updated for '${namespace}'.`,
|
|
627
|
+
});
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
// POST /integrations/neon/upgrade — migrate a D1 single DB block to Neon-backed postgres
|
|
631
|
+
if (method === 'POST' && segments[0] === 'integrations' && segments[1] === 'neon' && segments[2] === 'upgrade' && segments.length === 3) {
|
|
632
|
+
const body = await readBody(req);
|
|
633
|
+
const namespace = body.namespace;
|
|
634
|
+
const envKey = body.envKey;
|
|
635
|
+
const projectName = body.projectName;
|
|
636
|
+
const projectId = body.projectId;
|
|
637
|
+
const mode = (body.mode ?? 'reuse');
|
|
638
|
+
const authorization = req.headers.authorization;
|
|
639
|
+
if (!namespace)
|
|
640
|
+
throw new Error('namespace is required.');
|
|
641
|
+
if (!authorization)
|
|
642
|
+
throw new Error('Admin authentication required.');
|
|
643
|
+
const config = loadSidecarConfig(opts);
|
|
644
|
+
const provider = getEffectiveSidecarProvider(config, namespace);
|
|
645
|
+
if (provider !== 'd1') {
|
|
646
|
+
throw new Error(`Only D1-backed single database blocks can be upgraded automatically. '${namespace}' is on '${provider}'.`);
|
|
647
|
+
}
|
|
648
|
+
const effectiveEnvKey = resolveRequestedPostgresEnvKey(namespace, envKey);
|
|
649
|
+
console.log();
|
|
650
|
+
console.log(chalk.blue(`📦 Starting D1 -> Postgres migration for database block '${namespace}'...`));
|
|
651
|
+
console.log(chalk.dim(' This migrates every table in the block, not just the current table.'));
|
|
652
|
+
console.log(chalk.dim(' 1/4 Exporting all tables from D1...'));
|
|
653
|
+
const dump = await callWorkerAdmin(opts, authorization, 'data/backup/dump-data', { namespace });
|
|
654
|
+
const dumpedTableCount = Object.keys(dump.tables ?? {}).length;
|
|
655
|
+
console.log(chalk.dim(` 2/4 Connecting Postgres${mode === 'create' ? ' by creating a Neon project' : ' to the selected Neon project'}...`));
|
|
656
|
+
const neonResult = await runNeonSetup({
|
|
657
|
+
projectDir: opts.projectDir,
|
|
658
|
+
namespace,
|
|
659
|
+
envKeyOverride: effectiveEnvKey,
|
|
660
|
+
targetLabelOverride: namespace,
|
|
661
|
+
projectName,
|
|
662
|
+
projectId,
|
|
663
|
+
projectMode: mode,
|
|
664
|
+
});
|
|
665
|
+
await configEditor.updateDatabaseBlock(editorOpts, namespace, {
|
|
666
|
+
provider: 'postgres',
|
|
667
|
+
connectionString: neonResult.target.envKey,
|
|
668
|
+
});
|
|
669
|
+
saveCurrentSnapshot(opts);
|
|
670
|
+
console.log(chalk.dim(' 3/4 Waiting for the dev worker to restart on Postgres...'));
|
|
671
|
+
await waitForNamespaceProvider(opts, authorization, namespace, 'postgres');
|
|
672
|
+
console.log(chalk.dim(` 4/4 Restoring ${dumpedTableCount} table${dumpedTableCount === 1 ? '' : 's'} into Postgres...`));
|
|
673
|
+
await callWorkerAdmin(opts, authorization, 'data/backup/restore-data', {
|
|
674
|
+
namespace,
|
|
675
|
+
tables: dump.tables,
|
|
676
|
+
skipWipe: false,
|
|
677
|
+
});
|
|
678
|
+
console.log(chalk.green(`✓ Database block '${namespace}' is now running on Postgres (${dumpedTableCount} table${dumpedTableCount === 1 ? '' : 's'} restored).`));
|
|
679
|
+
json(res, 200, {
|
|
680
|
+
ok: true,
|
|
681
|
+
mode,
|
|
682
|
+
envKey: neonResult.target.envKey,
|
|
683
|
+
projectName: neonResult.projectName,
|
|
684
|
+
restoredTables: dumpedTableCount,
|
|
685
|
+
message: `Database block '${namespace}' migrated from D1 to Postgres.`,
|
|
686
|
+
});
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
// GET /integrations/neon/projects — list existing Neon projects for dashboard selection
|
|
690
|
+
if (method === 'GET' && segments[0] === 'integrations' && segments[1] === 'neon' && segments[2] === 'projects' && segments.length === 3) {
|
|
691
|
+
const forceRefresh = url.searchParams.get('refresh') === '1';
|
|
692
|
+
const isCacheFresh = !forceRefresh
|
|
693
|
+
&& neonProjectsCache
|
|
694
|
+
&& (Date.now() - neonProjectsCache.loadedAt) < NEON_PROJECT_CACHE_TTL_MS;
|
|
695
|
+
const items = (isCacheFresh && neonProjectsCache)
|
|
696
|
+
? neonProjectsCache.items
|
|
697
|
+
: await listAvailableNeonProjects({
|
|
698
|
+
projectDir: opts.projectDir,
|
|
699
|
+
});
|
|
700
|
+
if (!isCacheFresh) {
|
|
701
|
+
neonProjectsCache = {
|
|
702
|
+
loadedAt: Date.now(),
|
|
703
|
+
items,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
json(res, 200, { ok: true, items });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
// POST /schema/storage/buckets — create storage bucket
|
|
710
|
+
if (method === 'POST' && segments[0] === 'schema' && segments[1] === 'storage' && segments[2] === 'buckets' && segments.length === 3) {
|
|
711
|
+
const body = await readBody(req);
|
|
712
|
+
const name = body.name;
|
|
713
|
+
if (!name)
|
|
714
|
+
throw new Error('Bucket name is required.');
|
|
715
|
+
await configEditor.addStorageBucket(editorOpts, name);
|
|
716
|
+
json(res, 201, { ok: true, message: `Storage bucket '${name}' created.` });
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
// DELETE /schema/tables/:name — delete table
|
|
720
|
+
if (method === 'DELETE' && segments[0] === 'schema' && segments[1] === 'tables' && segments.length === 3) {
|
|
721
|
+
const tableName = segments[2];
|
|
722
|
+
const body = await readBody(req);
|
|
723
|
+
const dbKey = body.dbKey || 'shared';
|
|
724
|
+
await configEditor.removeTable(editorOpts, dbKey, tableName);
|
|
725
|
+
json(res, 200, { ok: true, message: `Table '${tableName}' deleted.` });
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
// PUT /schema/tables/:name/rename — rename table
|
|
729
|
+
if (method === 'PUT' && segments[0] === 'schema' && segments[1] === 'tables' && segments[3] === 'rename' && segments.length === 4) {
|
|
730
|
+
const tableName = segments[2];
|
|
731
|
+
const body = await readBody(req);
|
|
732
|
+
const dbKey = body.dbKey || 'shared';
|
|
733
|
+
const newName = body.newName;
|
|
734
|
+
const authorization = req.headers.authorization;
|
|
735
|
+
if (!newName)
|
|
736
|
+
throw new Error('newName is required.');
|
|
737
|
+
if (!authorization)
|
|
738
|
+
throw new Error('Admin authentication required.');
|
|
739
|
+
await renameBackingTable(opts, authorization, dbKey, tableName, newName);
|
|
740
|
+
try {
|
|
741
|
+
await configEditor.renameTable(editorOpts, dbKey, tableName, newName);
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
try {
|
|
745
|
+
await executeWorkerSql(opts, authorization, dbKey, `ALTER TABLE ${quoteIdentifier(newName)} RENAME TO ${quoteIdentifier(tableName)}`);
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
// Best-effort rollback only; surface the original config write error.
|
|
749
|
+
}
|
|
750
|
+
throw err;
|
|
751
|
+
}
|
|
752
|
+
json(res, 200, { ok: true, message: `Table '${tableName}' renamed to '${newName}'.` });
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
// POST /schema/tables/:name/columns — add column
|
|
756
|
+
if (method === 'POST' && segments[0] === 'schema' && segments[1] === 'tables' && segments[3] === 'columns' && segments.length === 4) {
|
|
757
|
+
const tableName = segments[2];
|
|
758
|
+
const body = await readBody(req);
|
|
759
|
+
const dbKey = body.dbKey || 'shared';
|
|
760
|
+
const columnName = body.columnName;
|
|
761
|
+
const fieldDef = body.fieldDef;
|
|
762
|
+
if (!columnName)
|
|
763
|
+
throw new Error('columnName is required.');
|
|
764
|
+
if (!fieldDef?.type)
|
|
765
|
+
throw new Error('fieldDef with type is required.');
|
|
766
|
+
await configEditor.addColumn(editorOpts, dbKey, tableName, columnName, fieldDef);
|
|
767
|
+
json(res, 201, { ok: true, message: `Column '${columnName}' added to '${tableName}'.` });
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
// PUT /schema/tables/:name/columns/:col — update column
|
|
771
|
+
if (method === 'PUT' && segments[0] === 'schema' && segments[1] === 'tables' && segments[3] === 'columns' && segments.length === 5) {
|
|
772
|
+
const tableName = segments[2];
|
|
773
|
+
const columnName = segments[4];
|
|
774
|
+
const body = await readBody(req);
|
|
775
|
+
const dbKey = body.dbKey || 'shared';
|
|
776
|
+
const fieldDef = body.fieldDef;
|
|
777
|
+
if (!fieldDef)
|
|
778
|
+
throw new Error('fieldDef is required.');
|
|
779
|
+
await configEditor.updateColumn(editorOpts, dbKey, tableName, columnName, fieldDef);
|
|
780
|
+
json(res, 200, { ok: true, message: `Column '${columnName}' updated in '${tableName}'.` });
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
// DELETE /schema/tables/:name/columns/:col — remove column
|
|
784
|
+
if (method === 'DELETE' && segments[0] === 'schema' && segments[1] === 'tables' && segments[3] === 'columns' && segments.length === 5) {
|
|
785
|
+
const tableName = segments[2];
|
|
786
|
+
const columnName = segments[4];
|
|
787
|
+
const body = await readBody(req);
|
|
788
|
+
const dbKey = body.dbKey || 'shared';
|
|
789
|
+
await configEditor.removeColumn(editorOpts, dbKey, tableName, columnName);
|
|
790
|
+
json(res, 200, { ok: true, message: `Column '${columnName}' removed from '${tableName}'.` });
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
// POST /schema/tables/:name/indexes — add index
|
|
794
|
+
if (method === 'POST' && segments[0] === 'schema' && segments[1] === 'tables' && segments[3] === 'indexes' && segments.length === 4) {
|
|
795
|
+
const tableName = segments[2];
|
|
796
|
+
const body = await readBody(req);
|
|
797
|
+
const dbKey = body.dbKey || 'shared';
|
|
798
|
+
const indexDef = body.indexDef;
|
|
799
|
+
if (!indexDef?.fields)
|
|
800
|
+
throw new Error('indexDef with fields is required.');
|
|
801
|
+
await configEditor.addIndex(editorOpts, dbKey, tableName, indexDef);
|
|
802
|
+
json(res, 201, { ok: true, message: `Index added to '${tableName}'.` });
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
// DELETE /schema/tables/:name/indexes/:idx — remove index
|
|
806
|
+
if (method === 'DELETE' && segments[0] === 'schema' && segments[1] === 'tables' && segments[3] === 'indexes' && segments.length === 5) {
|
|
807
|
+
const tableName = segments[2];
|
|
808
|
+
const indexIdx = parseInt(segments[4], 10);
|
|
809
|
+
const body = await readBody(req);
|
|
810
|
+
const dbKey = body.dbKey || 'shared';
|
|
811
|
+
await configEditor.removeIndex(editorOpts, dbKey, tableName, indexIdx);
|
|
812
|
+
json(res, 200, { ok: true, message: `Index ${indexIdx} removed from '${tableName}'.` });
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
// PUT /schema/tables/:name/fts — set FTS fields
|
|
816
|
+
if (method === 'PUT' && segments[0] === 'schema' && segments[1] === 'tables' && segments[3] === 'fts' && segments.length === 4) {
|
|
817
|
+
const tableName = segments[2];
|
|
818
|
+
const body = await readBody(req);
|
|
819
|
+
const dbKey = body.dbKey || 'shared';
|
|
820
|
+
const fields = body.fields || [];
|
|
821
|
+
await configEditor.setFts(editorOpts, dbKey, tableName, fields);
|
|
822
|
+
json(res, 200, { ok: true, message: `FTS fields updated for '${tableName}'.` });
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
// ─── Auth Settings Editing ───
|
|
826
|
+
// GET /auth/settings — read current auth config
|
|
827
|
+
if (method === 'GET' && segments[0] === 'auth' && segments[1] === 'settings' && segments.length === 2) {
|
|
828
|
+
const target = parseAuthSettingsTarget(url.searchParams.get('target'));
|
|
829
|
+
const config = loadSidecarConfig(opts, target);
|
|
830
|
+
json(res, 200, { ok: true, target, ...readAuthSettings(config) });
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
// PUT /auth/settings — save auth config
|
|
834
|
+
if (method === 'PUT' && segments[0] === 'auth' && segments[1] === 'settings' && segments.length === 2) {
|
|
835
|
+
const target = parseAuthSettingsTarget(url.searchParams.get('target'));
|
|
836
|
+
const body = await readBody(req);
|
|
837
|
+
const session = body.session;
|
|
838
|
+
const magicLink = body.magicLink;
|
|
839
|
+
const emailOtp = body.emailOtp;
|
|
840
|
+
const passkeys = body.passkeys;
|
|
841
|
+
const oauth = body.oauth;
|
|
842
|
+
const normalizedOAuth = oauth && typeof oauth === 'object'
|
|
843
|
+
? Object.fromEntries(Object.entries(oauth)
|
|
844
|
+
.filter(([, value]) => value && typeof value === 'object')
|
|
845
|
+
.map(([provider, value]) => [
|
|
846
|
+
provider,
|
|
847
|
+
{
|
|
848
|
+
clientId: typeof value.clientId === 'string' ? value.clientId : null,
|
|
849
|
+
clientSecret: typeof value.clientSecret === 'string' ? value.clientSecret : null,
|
|
850
|
+
issuer: typeof value.issuer === 'string' ? value.issuer : null,
|
|
851
|
+
scopes: Array.isArray(value.scopes)
|
|
852
|
+
? value.scopes.filter((scope) => typeof scope === 'string')
|
|
853
|
+
: [],
|
|
854
|
+
},
|
|
855
|
+
]))
|
|
856
|
+
: undefined;
|
|
857
|
+
syncOAuthSecretsToLocalEnv(opts.projectDir, target, Array.isArray(body.allowedOAuthProviders)
|
|
858
|
+
? body.allowedOAuthProviders.filter((provider) => typeof provider === 'string')
|
|
859
|
+
: undefined, normalizedOAuth);
|
|
860
|
+
await configEditor.setAuthSettings(editorOpts, {
|
|
861
|
+
emailAuth: typeof body.emailAuth === 'boolean' ? body.emailAuth : undefined,
|
|
862
|
+
anonymousAuth: typeof body.anonymousAuth === 'boolean' ? body.anonymousAuth : undefined,
|
|
863
|
+
allowedOAuthProviders: Array.isArray(body.allowedOAuthProviders)
|
|
864
|
+
? body.allowedOAuthProviders.filter((provider) => typeof provider === 'string')
|
|
865
|
+
: undefined,
|
|
866
|
+
allowedRedirectUrls: Array.isArray(body.allowedRedirectUrls)
|
|
867
|
+
? body.allowedRedirectUrls.filter((url) => typeof url === 'string')
|
|
868
|
+
: undefined,
|
|
869
|
+
session: session && typeof session === 'object'
|
|
870
|
+
? {
|
|
871
|
+
accessTokenTTL: typeof session.accessTokenTTL === 'string' ? session.accessTokenTTL : null,
|
|
872
|
+
refreshTokenTTL: typeof session.refreshTokenTTL === 'string' ? session.refreshTokenTTL : null,
|
|
873
|
+
maxActiveSessions: typeof session.maxActiveSessions === 'number' ? session.maxActiveSessions : null,
|
|
874
|
+
}
|
|
875
|
+
: undefined,
|
|
876
|
+
magicLink: magicLink && typeof magicLink === 'object'
|
|
877
|
+
? {
|
|
878
|
+
enabled: typeof magicLink.enabled === 'boolean' ? magicLink.enabled : undefined,
|
|
879
|
+
autoCreate: typeof magicLink.autoCreate === 'boolean' ? magicLink.autoCreate : undefined,
|
|
880
|
+
tokenTTL: typeof magicLink.tokenTTL === 'string' ? magicLink.tokenTTL : null,
|
|
881
|
+
}
|
|
882
|
+
: undefined,
|
|
883
|
+
emailOtp: emailOtp && typeof emailOtp === 'object'
|
|
884
|
+
? {
|
|
885
|
+
enabled: typeof emailOtp.enabled === 'boolean' ? emailOtp.enabled : undefined,
|
|
886
|
+
autoCreate: typeof emailOtp.autoCreate === 'boolean' ? emailOtp.autoCreate : undefined,
|
|
887
|
+
}
|
|
888
|
+
: undefined,
|
|
889
|
+
passkeys: passkeys && typeof passkeys === 'object'
|
|
890
|
+
? {
|
|
891
|
+
enabled: typeof passkeys.enabled === 'boolean' ? passkeys.enabled : undefined,
|
|
892
|
+
rpName: typeof passkeys.rpName === 'string' ? passkeys.rpName : null,
|
|
893
|
+
rpID: typeof passkeys.rpID === 'string' ? passkeys.rpID : null,
|
|
894
|
+
origin: Array.isArray(passkeys.origin)
|
|
895
|
+
? passkeys.origin.filter((origin) => typeof origin === 'string')
|
|
896
|
+
: undefined,
|
|
897
|
+
}
|
|
898
|
+
: undefined,
|
|
899
|
+
oauth: normalizedOAuth,
|
|
900
|
+
});
|
|
901
|
+
json(res, 200, { ok: true, target, message: 'Auth settings updated.' });
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
// ─── Email Template Editing ───
|
|
905
|
+
// PUT /email/templates — save email subject/template override (with optional locale)
|
|
906
|
+
if (method === 'PUT' && segments[0] === 'email' && segments[1] === 'templates' && segments.length === 2) {
|
|
907
|
+
const body = await readBody(req);
|
|
908
|
+
const type = body.type;
|
|
909
|
+
const locale = body.locale ?? 'en';
|
|
910
|
+
const subject = body.subject;
|
|
911
|
+
const template = body.template;
|
|
912
|
+
if (!type)
|
|
913
|
+
throw new Error('Email type is required.');
|
|
914
|
+
// Handle subject
|
|
915
|
+
if (subject !== undefined) {
|
|
916
|
+
if (subject === null || subject === '') {
|
|
917
|
+
if (locale === 'en') {
|
|
918
|
+
await configEditor.removeEmailOverride(editorOpts, type, 'subject');
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
await configEditor.removeEmailOverrideForLocale(editorOpts, type, 'subject', locale);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
if (locale === 'en') {
|
|
926
|
+
await configEditor.setEmailSubject(editorOpts, type, subject);
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
await configEditor.setEmailSubjectForLocale(editorOpts, type, locale, subject);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// Handle template
|
|
934
|
+
if (template !== undefined) {
|
|
935
|
+
if (template === null || template === '') {
|
|
936
|
+
if (locale === 'en') {
|
|
937
|
+
await configEditor.removeEmailOverride(editorOpts, type, 'template');
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
await configEditor.removeEmailOverrideForLocale(editorOpts, type, 'template', locale);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
if (locale === 'en') {
|
|
945
|
+
await configEditor.setEmailTemplate(editorOpts, type, template);
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
await configEditor.setEmailTemplateForLocale(editorOpts, type, locale, template);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
json(res, 200, { ok: true, message: `Email '${type}' (${locale}) config updated.` });
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
// GET /email/templates — read current email config
|
|
956
|
+
if (method === 'GET' && segments[0] === 'email' && segments[1] === 'templates' && segments.length === 2) {
|
|
957
|
+
// Re-read the full config to get email section
|
|
958
|
+
try {
|
|
959
|
+
const projectDir = resolve(opts.configPath, '..');
|
|
960
|
+
const result = execTsxSync([
|
|
961
|
+
'-e',
|
|
962
|
+
`import c from ${JSON.stringify(opts.configPath)}; const d=c.default??c; const e=d.email??{}; console.log(JSON.stringify({appName:e.appName||'EdgeBase',subjects:e.subjects||{},templates:e.templates||{}}));`,
|
|
963
|
+
], { cwd: projectDir, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
964
|
+
const emailConfig = JSON.parse(result);
|
|
965
|
+
json(res, 200, { ok: true, ...emailConfig });
|
|
966
|
+
}
|
|
967
|
+
catch {
|
|
968
|
+
json(res, 200, { ok: true, appName: 'EdgeBase', subjects: {}, templates: {} });
|
|
969
|
+
}
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
json(res, 404, { code: 404, message: 'Not found.' });
|
|
973
|
+
}
|
|
974
|
+
// ─── Server ───
|
|
975
|
+
export function startSidecar(opts) {
|
|
976
|
+
const server = createServer(async (req, res) => {
|
|
977
|
+
// CORS headers (dashboard on :8787, sidecar on :8788)
|
|
978
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
979
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
980
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-EdgeBase-Internal-Secret');
|
|
981
|
+
if (req.method === 'OPTIONS') {
|
|
982
|
+
res.writeHead(204);
|
|
983
|
+
res.end();
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
try {
|
|
987
|
+
// Auth: verify Admin JWT
|
|
988
|
+
const authValid = await verifyAuth(req, opts.adminSecret);
|
|
989
|
+
if (!authValid) {
|
|
990
|
+
json(res, 401, { code: 401, message: 'Admin authentication required.' });
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
await handleRoute(req, res, opts);
|
|
994
|
+
}
|
|
995
|
+
catch (err) {
|
|
996
|
+
const message = err.message || 'Internal error';
|
|
997
|
+
json(res, 400, { code: 400, message });
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
server.on('error', (err) => {
|
|
1001
|
+
if (err.code === 'EADDRINUSE') {
|
|
1002
|
+
console.log(chalk.dim(` 📐 Schema Editor sidecar skipped (:${opts.port} already in use)`));
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
console.log(chalk.dim(' 📐 Schema Editor sidecar skipped:'), err.message);
|
|
1006
|
+
});
|
|
1007
|
+
server.listen(opts.port, () => {
|
|
1008
|
+
console.log(chalk.dim(` 📐 Schema Editor sidecar on :${opts.port}`));
|
|
1009
|
+
});
|
|
1010
|
+
server.on('close', () => {
|
|
1011
|
+
for (const pool of postgresPoolCache.values()) {
|
|
1012
|
+
void pool.end().catch(() => undefined);
|
|
1013
|
+
}
|
|
1014
|
+
postgresPoolCache.clear();
|
|
1015
|
+
});
|
|
1016
|
+
return server;
|
|
1017
|
+
}
|
|
1018
|
+
// ─── Env File Parser ───
|
|
1019
|
+
/**
|
|
1020
|
+
* Parse a KEY=VALUE env file (supports comments, quoted values).
|
|
1021
|
+
* Shared by dev (`.env.development`) and deploy (`.env.release`).
|
|
1022
|
+
*/
|
|
1023
|
+
export function parseEnvFile(filePath) {
|
|
1024
|
+
const vars = {};
|
|
1025
|
+
try {
|
|
1026
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
1027
|
+
for (const line of content.split('\n')) {
|
|
1028
|
+
const trimmed = line.trim();
|
|
1029
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
1030
|
+
continue;
|
|
1031
|
+
const eqIdx = trimmed.indexOf('=');
|
|
1032
|
+
if (eqIdx === -1)
|
|
1033
|
+
continue;
|
|
1034
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
1035
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
1036
|
+
// Strip surrounding quotes
|
|
1037
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
1038
|
+
value = value.slice(1, -1);
|
|
1039
|
+
}
|
|
1040
|
+
vars[key] = value;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
catch {
|
|
1044
|
+
// File not found or unreadable
|
|
1045
|
+
}
|
|
1046
|
+
return vars;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Parse development environment variables.
|
|
1050
|
+
* Priority: `.env.development` → `.dev.vars` (backward compat).
|
|
1051
|
+
*/
|
|
1052
|
+
export function parseDevVars(projectDir) {
|
|
1053
|
+
const envDevPath = join(projectDir, '.env.development');
|
|
1054
|
+
if (existsSync(envDevPath))
|
|
1055
|
+
return parseEnvFile(envDevPath);
|
|
1056
|
+
return parseEnvFile(join(projectDir, '.dev.vars'));
|
|
1057
|
+
}
|
|
1058
|
+
//# sourceMappingURL=dev-sidecar.js.map
|