@meetploy/cli 1.11.4 → 1.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dashboard-dist/assets/main-BnI0iIIW.css +1 -0
- package/dist/dashboard-dist/assets/main-DnrX4BgS.js +319 -0
- package/dist/dashboard-dist/index.html +2 -2
- package/dist/dev.js +579 -14
- package/dist/index.js +680 -51
- package/package.json +1 -1
- package/dist/dashboard-dist/assets/main-B1TTAu_I.js +0 -166
- package/dist/dashboard-dist/assets/main-BXQv-ae0.css +0 -1
package/dist/dev.js
CHANGED
|
@@ -9,14 +9,15 @@ import { promisify } from 'util';
|
|
|
9
9
|
import { parse } from 'yaml';
|
|
10
10
|
import { serve } from '@hono/node-server';
|
|
11
11
|
import { Hono } from 'hono';
|
|
12
|
-
import { randomUUID } from 'crypto';
|
|
12
|
+
import { randomUUID, createHmac, pbkdf2Sync, timingSafeEqual, randomBytes } from 'crypto';
|
|
13
|
+
import { getCookie, deleteCookie, setCookie } from 'hono/cookie';
|
|
13
14
|
import 'os';
|
|
14
15
|
import Database from 'better-sqlite3';
|
|
15
16
|
|
|
16
17
|
createRequire(import.meta.url);
|
|
17
18
|
promisify(readFile);
|
|
18
19
|
function readPloyConfigSync(projectDir, configPath) {
|
|
19
|
-
const configFile = configPath;
|
|
20
|
+
const configFile = configPath || "ploy.yaml";
|
|
20
21
|
const fullPath = join(projectDir, configFile);
|
|
21
22
|
if (!existsSync(fullPath)) {
|
|
22
23
|
throw new Error(`Config file not found: ${fullPath}`);
|
|
@@ -29,13 +30,257 @@ function readPloyConfigSync(projectDir, configPath) {
|
|
|
29
30
|
function readPloyConfig(projectDir, configPath) {
|
|
30
31
|
const config = readPloyConfigSync(projectDir, configPath);
|
|
31
32
|
if (!config.kind) {
|
|
32
|
-
throw new Error(`Missing required field 'kind' in ${configPath}`);
|
|
33
|
+
throw new Error(`Missing required field 'kind' in ${configPath || "ploy.yaml"}`);
|
|
33
34
|
}
|
|
34
35
|
if (config.kind !== "dynamic" && config.kind !== "worker") {
|
|
35
|
-
throw new Error(`Invalid kind '${config.kind}' in ${configPath}. Must be 'dynamic' or 'worker'`);
|
|
36
|
+
throw new Error(`Invalid kind '${config.kind}' in ${configPath || "ploy.yaml"}. Must be 'dynamic' or 'worker'`);
|
|
36
37
|
}
|
|
37
38
|
return config;
|
|
38
39
|
}
|
|
40
|
+
function generateId() {
|
|
41
|
+
return randomBytes(16).toString("hex");
|
|
42
|
+
}
|
|
43
|
+
function hashPassword(password) {
|
|
44
|
+
const salt = randomBytes(32).toString("hex");
|
|
45
|
+
const hash = pbkdf2Sync(password, salt, 1e5, 64, "sha512").toString("hex");
|
|
46
|
+
return `${salt}:${hash}`;
|
|
47
|
+
}
|
|
48
|
+
function verifyPassword(password, storedHash) {
|
|
49
|
+
const [salt, hash] = storedHash.split(":");
|
|
50
|
+
const derivedHash = pbkdf2Sync(password, salt, 1e5, 64, "sha512").toString("hex");
|
|
51
|
+
return timingSafeEqual(Buffer.from(hash, "hex"), Buffer.from(derivedHash, "hex"));
|
|
52
|
+
}
|
|
53
|
+
function hashToken(token) {
|
|
54
|
+
return createHmac("sha256", "emulator-secret").update(token).digest("hex");
|
|
55
|
+
}
|
|
56
|
+
var JWT_SECRET = "ploy-emulator-dev-secret";
|
|
57
|
+
var SESSION_TOKEN_EXPIRY = 7 * 24 * 60 * 60;
|
|
58
|
+
function base64UrlEncode(str) {
|
|
59
|
+
return Buffer.from(str).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
60
|
+
}
|
|
61
|
+
function base64UrlDecode(str) {
|
|
62
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
63
|
+
while (base64.length % 4) {
|
|
64
|
+
base64 += "=";
|
|
65
|
+
}
|
|
66
|
+
return Buffer.from(base64, "base64").toString();
|
|
67
|
+
}
|
|
68
|
+
function createJWT(payload) {
|
|
69
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
70
|
+
const headerB64 = base64UrlEncode(JSON.stringify(header));
|
|
71
|
+
const payloadB64 = base64UrlEncode(JSON.stringify(payload));
|
|
72
|
+
const signature = createHmac("sha256", JWT_SECRET).update(`${headerB64}.${payloadB64}`).digest("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
73
|
+
return `${headerB64}.${payloadB64}.${signature}`;
|
|
74
|
+
}
|
|
75
|
+
function verifyJWT(token) {
|
|
76
|
+
try {
|
|
77
|
+
const parts = token.split(".");
|
|
78
|
+
if (parts.length !== 3) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const [headerB64, payloadB64, signature] = parts;
|
|
82
|
+
const expectedSig = createHmac("sha256", JWT_SECRET).update(`${headerB64}.${payloadB64}`).digest("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
83
|
+
if (signature !== expectedSig) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const payload = JSON.parse(base64UrlDecode(payloadB64));
|
|
87
|
+
if (payload.exp < Math.floor(Date.now() / 1e3)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return payload;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function createSessionToken(userId, email) {
|
|
96
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
97
|
+
const sessionId = generateId();
|
|
98
|
+
const token = createJWT({
|
|
99
|
+
sub: userId,
|
|
100
|
+
email,
|
|
101
|
+
iat: now,
|
|
102
|
+
exp: now + SESSION_TOKEN_EXPIRY,
|
|
103
|
+
jti: sessionId
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
token,
|
|
107
|
+
sessionId,
|
|
108
|
+
expiresAt: new Date((now + SESSION_TOKEN_EXPIRY) * 1e3)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function validateEmail(email) {
|
|
112
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
113
|
+
if (!emailRegex.test(email)) {
|
|
114
|
+
return "Invalid email format";
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function validatePassword(password) {
|
|
119
|
+
if (password.length < 8) {
|
|
120
|
+
return "Password must be at least 8 characters";
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
function setSessionCookie(c, sessionToken) {
|
|
125
|
+
setCookie(c, "ploy_session", sessionToken, {
|
|
126
|
+
httpOnly: true,
|
|
127
|
+
secure: false,
|
|
128
|
+
sameSite: "Lax",
|
|
129
|
+
path: "/",
|
|
130
|
+
maxAge: SESSION_TOKEN_EXPIRY
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function clearSessionCookie(c) {
|
|
134
|
+
deleteCookie(c, "ploy_session", { path: "/" });
|
|
135
|
+
}
|
|
136
|
+
function createAuthHandlers(db) {
|
|
137
|
+
const signupHandler = async (c) => {
|
|
138
|
+
try {
|
|
139
|
+
const body = await c.req.json();
|
|
140
|
+
const { email, password, metadata } = body;
|
|
141
|
+
const emailError = validateEmail(email);
|
|
142
|
+
if (emailError) {
|
|
143
|
+
return c.json({ error: emailError }, 400);
|
|
144
|
+
}
|
|
145
|
+
const passwordError = validatePassword(password);
|
|
146
|
+
if (passwordError) {
|
|
147
|
+
return c.json({ error: passwordError }, 400);
|
|
148
|
+
}
|
|
149
|
+
const existingUser = db.prepare("SELECT id FROM auth_users WHERE email = ?").get(email.toLowerCase());
|
|
150
|
+
if (existingUser) {
|
|
151
|
+
return c.json({ error: "User already exists" }, 409);
|
|
152
|
+
}
|
|
153
|
+
const userId = generateId();
|
|
154
|
+
const passwordHash = hashPassword(password);
|
|
155
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
156
|
+
db.prepare(`INSERT INTO auth_users (id, email, password_hash, created_at, updated_at, metadata)
|
|
157
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(userId, email.toLowerCase(), passwordHash, now, now, metadata ? JSON.stringify(metadata) : null);
|
|
158
|
+
const { token: sessionToken, sessionId, expiresAt } = createSessionToken(userId, email.toLowerCase());
|
|
159
|
+
const sessionTokenHash = hashToken(sessionToken);
|
|
160
|
+
db.prepare(`INSERT INTO auth_sessions (id, user_id, token_hash, expires_at, created_at)
|
|
161
|
+
VALUES (?, ?, ?, ?, ?)`).run(sessionId, userId, sessionTokenHash, expiresAt.toISOString(), now);
|
|
162
|
+
setSessionCookie(c, sessionToken);
|
|
163
|
+
return c.json({
|
|
164
|
+
user: {
|
|
165
|
+
id: userId,
|
|
166
|
+
email: email.toLowerCase(),
|
|
167
|
+
emailVerified: false,
|
|
168
|
+
createdAt: now,
|
|
169
|
+
metadata: metadata ?? null
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
} catch (err) {
|
|
173
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
174
|
+
return c.json({ error: message }, 500);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const signinHandler = async (c) => {
|
|
178
|
+
try {
|
|
179
|
+
const body = await c.req.json();
|
|
180
|
+
const { email, password } = body;
|
|
181
|
+
const user = db.prepare("SELECT * FROM auth_users WHERE email = ?").get(email.toLowerCase());
|
|
182
|
+
if (!user) {
|
|
183
|
+
return c.json({ error: "Invalid credentials" }, 401);
|
|
184
|
+
}
|
|
185
|
+
if (!verifyPassword(password, user.password_hash)) {
|
|
186
|
+
return c.json({ error: "Invalid credentials" }, 401);
|
|
187
|
+
}
|
|
188
|
+
const { token: sessionToken, sessionId, expiresAt } = createSessionToken(user.id, user.email);
|
|
189
|
+
const sessionTokenHash = hashToken(sessionToken);
|
|
190
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
191
|
+
db.prepare(`INSERT INTO auth_sessions (id, user_id, token_hash, expires_at, created_at)
|
|
192
|
+
VALUES (?, ?, ?, ?, ?)`).run(sessionId, user.id, sessionTokenHash, expiresAt.toISOString(), now);
|
|
193
|
+
let metadata = null;
|
|
194
|
+
if (user.metadata) {
|
|
195
|
+
try {
|
|
196
|
+
metadata = JSON.parse(user.metadata);
|
|
197
|
+
} catch {
|
|
198
|
+
metadata = null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
setSessionCookie(c, sessionToken);
|
|
202
|
+
return c.json({
|
|
203
|
+
user: {
|
|
204
|
+
id: user.id,
|
|
205
|
+
email: user.email,
|
|
206
|
+
emailVerified: user.email_verified === 1,
|
|
207
|
+
createdAt: user.created_at,
|
|
208
|
+
metadata
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
} catch (err) {
|
|
212
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
213
|
+
return c.json({ error: message }, 500);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const meHandler = async (c) => {
|
|
217
|
+
try {
|
|
218
|
+
const cookieToken = getCookie(c, "ploy_session");
|
|
219
|
+
const authHeader = c.req.header("Authorization");
|
|
220
|
+
let token;
|
|
221
|
+
if (cookieToken) {
|
|
222
|
+
token = cookieToken;
|
|
223
|
+
} else if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
224
|
+
token = authHeader.slice(7);
|
|
225
|
+
}
|
|
226
|
+
if (!token) {
|
|
227
|
+
return c.json({ error: "Missing authentication" }, 401);
|
|
228
|
+
}
|
|
229
|
+
const payload = verifyJWT(token);
|
|
230
|
+
if (!payload) {
|
|
231
|
+
return c.json({ error: "Invalid or expired session" }, 401);
|
|
232
|
+
}
|
|
233
|
+
const user = db.prepare("SELECT id, email, email_verified, created_at, updated_at, metadata FROM auth_users WHERE id = ?").get(payload.sub);
|
|
234
|
+
if (!user) {
|
|
235
|
+
return c.json({ error: "User not found" }, 401);
|
|
236
|
+
}
|
|
237
|
+
let metadata = null;
|
|
238
|
+
if (user.metadata) {
|
|
239
|
+
try {
|
|
240
|
+
metadata = JSON.parse(user.metadata);
|
|
241
|
+
} catch {
|
|
242
|
+
metadata = null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return c.json({
|
|
246
|
+
user: {
|
|
247
|
+
id: user.id,
|
|
248
|
+
email: user.email,
|
|
249
|
+
emailVerified: user.email_verified === 1,
|
|
250
|
+
createdAt: user.created_at,
|
|
251
|
+
updatedAt: user.updated_at,
|
|
252
|
+
metadata
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
} catch (err) {
|
|
256
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
257
|
+
return c.json({ error: message }, 500);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const signoutHandler = async (c) => {
|
|
261
|
+
try {
|
|
262
|
+
const sessionToken = getCookie(c, "ploy_session");
|
|
263
|
+
if (sessionToken) {
|
|
264
|
+
const payload = verifyJWT(sessionToken);
|
|
265
|
+
if (payload) {
|
|
266
|
+
const tokenHash = hashToken(sessionToken);
|
|
267
|
+
db.prepare("UPDATE auth_sessions SET revoked = 1 WHERE token_hash = ?").run(tokenHash);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
clearSessionCookie(c);
|
|
271
|
+
return c.json({ success: true });
|
|
272
|
+
} catch (err) {
|
|
273
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
274
|
+
return c.json({ error: message }, 500);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
return {
|
|
278
|
+
signupHandler,
|
|
279
|
+
signinHandler,
|
|
280
|
+
meHandler,
|
|
281
|
+
signoutHandler
|
|
282
|
+
};
|
|
283
|
+
}
|
|
39
284
|
var __filename = fileURLToPath(import.meta.url);
|
|
40
285
|
var __dirname = dirname(__filename);
|
|
41
286
|
function findDashboardDistPath() {
|
|
@@ -77,9 +322,174 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
77
322
|
return c.json({
|
|
78
323
|
db: config.db,
|
|
79
324
|
queue: config.queue,
|
|
80
|
-
workflow: config.workflow
|
|
325
|
+
workflow: config.workflow,
|
|
326
|
+
auth: config.auth
|
|
81
327
|
});
|
|
82
328
|
});
|
|
329
|
+
if (config.auth) {
|
|
330
|
+
app.get("/api/auth/tables", (c) => {
|
|
331
|
+
try {
|
|
332
|
+
const db = dbManager2.emulatorDb;
|
|
333
|
+
const tables = db.prepare(`SELECT name FROM sqlite_master
|
|
334
|
+
WHERE type='table' AND (name = 'auth_users' OR name = 'auth_sessions')
|
|
335
|
+
ORDER BY name`).all();
|
|
336
|
+
return c.json({ tables });
|
|
337
|
+
} catch (err) {
|
|
338
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
app.get("/api/auth/tables/:tableName", (c) => {
|
|
342
|
+
const tableName = c.req.param("tableName");
|
|
343
|
+
if (tableName !== "auth_users" && tableName !== "auth_sessions") {
|
|
344
|
+
return c.json({ error: "Table not found" }, 404);
|
|
345
|
+
}
|
|
346
|
+
const limit = parseInt(c.req.query("limit") || "50", 10);
|
|
347
|
+
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
348
|
+
try {
|
|
349
|
+
const db = dbManager2.emulatorDb;
|
|
350
|
+
const columnsResult = db.prepare(`PRAGMA table_info("${tableName}")`).all();
|
|
351
|
+
const columns = columnsResult.map((col) => col.name);
|
|
352
|
+
const countResult = db.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`).get();
|
|
353
|
+
const total = countResult.count;
|
|
354
|
+
let data;
|
|
355
|
+
if (tableName === "auth_users") {
|
|
356
|
+
data = db.prepare(`SELECT id, email, email_verified, created_at, updated_at, metadata FROM "${tableName}" LIMIT ? OFFSET ?`).all(limit, offset);
|
|
357
|
+
} else {
|
|
358
|
+
data = db.prepare(`SELECT * FROM "${tableName}" LIMIT ? OFFSET ?`).all(limit, offset);
|
|
359
|
+
}
|
|
360
|
+
const visibleColumns = tableName === "auth_users" ? columns.filter((c2) => c2 !== "password_hash") : columns;
|
|
361
|
+
return c.json({ data, columns: visibleColumns, total });
|
|
362
|
+
} catch (err) {
|
|
363
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
app.get("/api/auth/schema", (c) => {
|
|
367
|
+
try {
|
|
368
|
+
const db = dbManager2.emulatorDb;
|
|
369
|
+
const tables = ["auth_users", "auth_sessions"].map((tableName) => {
|
|
370
|
+
const columnsResult = db.prepare(`PRAGMA table_info("${tableName}")`).all();
|
|
371
|
+
const visibleColumns = tableName === "auth_users" ? columnsResult.filter((col) => col.name !== "password_hash") : columnsResult;
|
|
372
|
+
return {
|
|
373
|
+
name: tableName,
|
|
374
|
+
columns: visibleColumns.map((col) => ({
|
|
375
|
+
name: col.name,
|
|
376
|
+
type: col.type,
|
|
377
|
+
notNull: col.notnull === 1,
|
|
378
|
+
primaryKey: col.pk === 1
|
|
379
|
+
}))
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
return c.json({ tables });
|
|
383
|
+
} catch (err) {
|
|
384
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
app.post("/api/auth/query", async (c) => {
|
|
388
|
+
const body = await c.req.json();
|
|
389
|
+
const { query } = body;
|
|
390
|
+
if (!query) {
|
|
391
|
+
return c.json({ error: "Query is required" }, 400);
|
|
392
|
+
}
|
|
393
|
+
const normalizedQuery = query.trim().toUpperCase();
|
|
394
|
+
if (!normalizedQuery.startsWith("SELECT")) {
|
|
395
|
+
return c.json({ error: "Only SELECT queries are allowed on auth tables" }, 400);
|
|
396
|
+
}
|
|
397
|
+
const allowedTables = ["auth_users", "auth_sessions"];
|
|
398
|
+
const hasDisallowedTable = !allowedTables.some((table) => query.toLowerCase().includes(`from ${table}`) || query.toLowerCase().includes(`join ${table}`));
|
|
399
|
+
if (hasDisallowedTable) {
|
|
400
|
+
return c.json({
|
|
401
|
+
error: "Query must reference auth tables (auth_users or auth_sessions)"
|
|
402
|
+
}, 400);
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const db = dbManager2.emulatorDb;
|
|
406
|
+
const startTime = Date.now();
|
|
407
|
+
const stmt = db.prepare(query);
|
|
408
|
+
const results = stmt.all();
|
|
409
|
+
const sanitizedResults = results.map((row) => {
|
|
410
|
+
const { password_hash: _, ...rest } = row;
|
|
411
|
+
return rest;
|
|
412
|
+
});
|
|
413
|
+
const duration = Date.now() - startTime;
|
|
414
|
+
return c.json({
|
|
415
|
+
results: sanitizedResults,
|
|
416
|
+
success: true,
|
|
417
|
+
meta: {
|
|
418
|
+
duration,
|
|
419
|
+
rows_read: results.length,
|
|
420
|
+
rows_written: 0
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
} catch (err) {
|
|
424
|
+
return c.json({
|
|
425
|
+
results: [],
|
|
426
|
+
success: false,
|
|
427
|
+
error: err instanceof Error ? err.message : String(err),
|
|
428
|
+
meta: { duration: 0, rows_read: 0, rows_written: 0 }
|
|
429
|
+
}, 400);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
app.get("/api/auth/settings", (c) => {
|
|
433
|
+
try {
|
|
434
|
+
const db = dbManager2.emulatorDb;
|
|
435
|
+
const settings = db.prepare("SELECT * FROM auth_settings WHERE id = 1").get();
|
|
436
|
+
if (!settings) {
|
|
437
|
+
return c.json({
|
|
438
|
+
sessionTokenExpiry: 604800,
|
|
439
|
+
allowSignups: true,
|
|
440
|
+
requireEmailVerification: false,
|
|
441
|
+
requireName: false
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
return c.json({
|
|
445
|
+
sessionTokenExpiry: settings.session_token_expiry,
|
|
446
|
+
allowSignups: settings.allow_signups === 1,
|
|
447
|
+
requireEmailVerification: settings.require_email_verification === 1,
|
|
448
|
+
requireName: settings.require_name === 1
|
|
449
|
+
});
|
|
450
|
+
} catch (err) {
|
|
451
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
app.patch("/api/auth/settings", async (c) => {
|
|
455
|
+
try {
|
|
456
|
+
const body = await c.req.json();
|
|
457
|
+
const db = dbManager2.emulatorDb;
|
|
458
|
+
const updates = [];
|
|
459
|
+
const values = [];
|
|
460
|
+
if (body.sessionTokenExpiry !== void 0) {
|
|
461
|
+
updates.push("session_token_expiry = ?");
|
|
462
|
+
values.push(body.sessionTokenExpiry);
|
|
463
|
+
}
|
|
464
|
+
if (body.allowSignups !== void 0) {
|
|
465
|
+
updates.push("allow_signups = ?");
|
|
466
|
+
values.push(body.allowSignups ? 1 : 0);
|
|
467
|
+
}
|
|
468
|
+
if (body.requireEmailVerification !== void 0) {
|
|
469
|
+
updates.push("require_email_verification = ?");
|
|
470
|
+
values.push(body.requireEmailVerification ? 1 : 0);
|
|
471
|
+
}
|
|
472
|
+
if (body.requireName !== void 0) {
|
|
473
|
+
updates.push("require_name = ?");
|
|
474
|
+
values.push(body.requireName ? 1 : 0);
|
|
475
|
+
}
|
|
476
|
+
if (updates.length > 0) {
|
|
477
|
+
updates.push("updated_at = strftime('%s', 'now')");
|
|
478
|
+
const sql = `UPDATE auth_settings SET ${updates.join(", ")} WHERE id = 1`;
|
|
479
|
+
db.prepare(sql).run(...values);
|
|
480
|
+
}
|
|
481
|
+
const settings = db.prepare("SELECT * FROM auth_settings WHERE id = 1").get();
|
|
482
|
+
return c.json({
|
|
483
|
+
sessionTokenExpiry: settings.session_token_expiry,
|
|
484
|
+
allowSignups: settings.allow_signups === 1,
|
|
485
|
+
requireEmailVerification: settings.require_email_verification === 1,
|
|
486
|
+
requireName: settings.require_name === 1
|
|
487
|
+
});
|
|
488
|
+
} catch (err) {
|
|
489
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
83
493
|
app.post("/api/db/:binding/query", async (c) => {
|
|
84
494
|
const binding = c.req.param("binding");
|
|
85
495
|
const resourceName = getDbResourceName(binding);
|
|
@@ -302,7 +712,7 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
302
712
|
}
|
|
303
713
|
try {
|
|
304
714
|
const db = dbManager2.emulatorDb;
|
|
305
|
-
const execution = db.prepare(`SELECT id, workflow_name, status, error, started_at, completed_at, created_at
|
|
715
|
+
const execution = db.prepare(`SELECT id, workflow_name, status, input, output, error, started_at, completed_at, created_at
|
|
306
716
|
FROM workflow_executions
|
|
307
717
|
WHERE id = ?`).get(executionId);
|
|
308
718
|
if (!execution) {
|
|
@@ -316,6 +726,8 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
316
726
|
execution: {
|
|
317
727
|
id: execution.id,
|
|
318
728
|
status: execution.status.toUpperCase(),
|
|
729
|
+
input: execution.input ? JSON.parse(execution.input) : null,
|
|
730
|
+
output: execution.output ? JSON.parse(execution.output) : null,
|
|
319
731
|
startedAt: execution.started_at ? new Date(execution.started_at * 1e3).toISOString() : null,
|
|
320
732
|
completedAt: execution.completed_at ? new Date(execution.completed_at * 1e3).toISOString() : null,
|
|
321
733
|
durationMs: execution.started_at && execution.completed_at ? (execution.completed_at - execution.started_at) * 1e3 : null,
|
|
@@ -375,8 +787,12 @@ function createDbHandler(getDatabase) {
|
|
|
375
787
|
const startTime = Date.now();
|
|
376
788
|
try {
|
|
377
789
|
const body = await c.req.json();
|
|
378
|
-
const { bindingName, method, query, params, statements } = body;
|
|
379
|
-
const
|
|
790
|
+
const { bindingName, databaseId, method, query, params, statements } = body;
|
|
791
|
+
const dbName = bindingName ?? databaseId;
|
|
792
|
+
if (!dbName) {
|
|
793
|
+
return c.json({ error: "Missing bindingName or databaseId" }, 400);
|
|
794
|
+
}
|
|
795
|
+
const db = getDatabase(dbName);
|
|
380
796
|
if (method === "prepare" && query) {
|
|
381
797
|
const stmt = db.prepare(query);
|
|
382
798
|
const isSelect = query.trim().toUpperCase().startsWith("SELECT");
|
|
@@ -537,8 +953,10 @@ function createQueueHandlers(db) {
|
|
|
537
953
|
try {
|
|
538
954
|
const body = await c.req.json();
|
|
539
955
|
const { messageId, deliveryId } = body;
|
|
540
|
-
const
|
|
541
|
-
|
|
956
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
957
|
+
const result = db.prepare(`UPDATE queue_messages
|
|
958
|
+
SET status = 'acknowledged', updated_at = ?
|
|
959
|
+
WHERE id = ? AND delivery_id = ?`).run(now, messageId, deliveryId);
|
|
542
960
|
if (result.changes === 0) {
|
|
543
961
|
return c.json({ success: false, error: "Message not found or already processed" }, 404);
|
|
544
962
|
}
|
|
@@ -775,6 +1193,13 @@ async function startMockServer(dbManager2, config, options = {}) {
|
|
|
775
1193
|
app.post("/workflow/complete", workflowHandlers.completeHandler);
|
|
776
1194
|
app.post("/workflow/fail", workflowHandlers.failHandler);
|
|
777
1195
|
}
|
|
1196
|
+
if (config.auth) {
|
|
1197
|
+
const authHandlers = createAuthHandlers(dbManager2.emulatorDb);
|
|
1198
|
+
app.post("/auth/signup", authHandlers.signupHandler);
|
|
1199
|
+
app.post("/auth/signin", authHandlers.signinHandler);
|
|
1200
|
+
app.get("/auth/me", authHandlers.meHandler);
|
|
1201
|
+
app.post("/auth/signout", authHandlers.signoutHandler);
|
|
1202
|
+
}
|
|
778
1203
|
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
779
1204
|
if (options.dashboardEnabled !== false) {
|
|
780
1205
|
createDashboardRoutes(app, dbManager2, config);
|
|
@@ -855,6 +1280,49 @@ CREATE TABLE IF NOT EXISTS workflow_steps (
|
|
|
855
1280
|
|
|
856
1281
|
CREATE INDEX IF NOT EXISTS idx_workflow_steps_execution
|
|
857
1282
|
ON workflow_steps(execution_id, step_index);
|
|
1283
|
+
|
|
1284
|
+
-- Auth users table
|
|
1285
|
+
CREATE TABLE IF NOT EXISTS auth_users (
|
|
1286
|
+
id TEXT PRIMARY KEY,
|
|
1287
|
+
email TEXT UNIQUE NOT NULL,
|
|
1288
|
+
email_verified INTEGER NOT NULL DEFAULT 0,
|
|
1289
|
+
password_hash TEXT NOT NULL,
|
|
1290
|
+
created_at TEXT NOT NULL,
|
|
1291
|
+
updated_at TEXT NOT NULL,
|
|
1292
|
+
metadata TEXT
|
|
1293
|
+
);
|
|
1294
|
+
|
|
1295
|
+
CREATE INDEX IF NOT EXISTS idx_auth_users_email
|
|
1296
|
+
ON auth_users(email);
|
|
1297
|
+
|
|
1298
|
+
-- Auth sessions table
|
|
1299
|
+
CREATE TABLE IF NOT EXISTS auth_sessions (
|
|
1300
|
+
id TEXT PRIMARY KEY,
|
|
1301
|
+
user_id TEXT NOT NULL,
|
|
1302
|
+
token_hash TEXT UNIQUE NOT NULL,
|
|
1303
|
+
expires_at TEXT NOT NULL,
|
|
1304
|
+
created_at TEXT NOT NULL,
|
|
1305
|
+
revoked INTEGER NOT NULL DEFAULT 0,
|
|
1306
|
+
FOREIGN KEY (user_id) REFERENCES auth_users(id) ON DELETE CASCADE
|
|
1307
|
+
);
|
|
1308
|
+
|
|
1309
|
+
CREATE INDEX IF NOT EXISTS idx_auth_sessions_user
|
|
1310
|
+
ON auth_sessions(user_id);
|
|
1311
|
+
CREATE INDEX IF NOT EXISTS idx_auth_sessions_hash
|
|
1312
|
+
ON auth_sessions(token_hash);
|
|
1313
|
+
|
|
1314
|
+
-- Auth settings table
|
|
1315
|
+
CREATE TABLE IF NOT EXISTS auth_settings (
|
|
1316
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
1317
|
+
session_token_expiry INTEGER NOT NULL DEFAULT 604800,
|
|
1318
|
+
allow_signups INTEGER NOT NULL DEFAULT 1,
|
|
1319
|
+
require_email_verification INTEGER NOT NULL DEFAULT 0,
|
|
1320
|
+
require_name INTEGER NOT NULL DEFAULT 0,
|
|
1321
|
+
updated_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
1322
|
+
);
|
|
1323
|
+
|
|
1324
|
+
-- Insert default settings if not exists
|
|
1325
|
+
INSERT OR IGNORE INTO auth_settings (id) VALUES (1);
|
|
858
1326
|
`;
|
|
859
1327
|
function initializeDatabases(projectDir) {
|
|
860
1328
|
const dataDir = ensureDataDir(projectDir);
|
|
@@ -987,6 +1455,38 @@ function createDevD1(databaseId, apiUrl) {
|
|
|
987
1455
|
}
|
|
988
1456
|
};
|
|
989
1457
|
}
|
|
1458
|
+
function createDevPloyAuth(apiUrl) {
|
|
1459
|
+
return {
|
|
1460
|
+
async getUser(token) {
|
|
1461
|
+
try {
|
|
1462
|
+
const response = await fetch(`${apiUrl}/auth/me`, {
|
|
1463
|
+
headers: {
|
|
1464
|
+
Authorization: `Bearer ${token}`
|
|
1465
|
+
}
|
|
1466
|
+
});
|
|
1467
|
+
if (!response.ok) {
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
const data = await response.json();
|
|
1471
|
+
return data.user;
|
|
1472
|
+
} catch {
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
},
|
|
1476
|
+
async verifyToken(token) {
|
|
1477
|
+
try {
|
|
1478
|
+
const response = await fetch(`${apiUrl}/auth/me`, {
|
|
1479
|
+
headers: {
|
|
1480
|
+
Authorization: `Bearer ${token}`
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
return response.ok;
|
|
1484
|
+
} catch {
|
|
1485
|
+
return false;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
990
1490
|
var mockServer = null;
|
|
991
1491
|
var dbManager = null;
|
|
992
1492
|
async function initPloyForDev(config) {
|
|
@@ -997,6 +1497,56 @@ async function initPloyForDev(config) {
|
|
|
997
1497
|
return;
|
|
998
1498
|
}
|
|
999
1499
|
globalThis.__PLOY_DEV_INITIALIZED__ = true;
|
|
1500
|
+
const cliMockServerUrl = process.env.PLOY_MOCK_SERVER_URL;
|
|
1501
|
+
if (cliMockServerUrl) {
|
|
1502
|
+
const configPath2 = config?.configPath || "./ploy.yaml";
|
|
1503
|
+
const projectDir2 = process.cwd();
|
|
1504
|
+
let ployConfig2;
|
|
1505
|
+
try {
|
|
1506
|
+
ployConfig2 = readPloyConfig(projectDir2, configPath2);
|
|
1507
|
+
} catch {
|
|
1508
|
+
if (config?.bindings?.db) {
|
|
1509
|
+
ployConfig2 = { db: config.bindings.db };
|
|
1510
|
+
} else {
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
if (config?.bindings?.db) {
|
|
1515
|
+
ployConfig2 = { ...ployConfig2, db: config.bindings.db };
|
|
1516
|
+
}
|
|
1517
|
+
const hasDbBindings2 = ployConfig2.db && Object.keys(ployConfig2.db).length > 0;
|
|
1518
|
+
const hasAuthConfig2 = !!ployConfig2.auth;
|
|
1519
|
+
if (!hasDbBindings2 && !hasAuthConfig2) {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
const env2 = {};
|
|
1523
|
+
if (hasDbBindings2 && ployConfig2.db) {
|
|
1524
|
+
for (const [bindingName, databaseId] of Object.entries(ployConfig2.db)) {
|
|
1525
|
+
env2[bindingName] = createDevD1(databaseId, cliMockServerUrl);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (hasAuthConfig2) {
|
|
1529
|
+
env2.PLOY_AUTH = createDevPloyAuth(cliMockServerUrl);
|
|
1530
|
+
}
|
|
1531
|
+
const context2 = { env: env2, cf: void 0, ctx: void 0 };
|
|
1532
|
+
globalThis.__PLOY_DEV_CONTEXT__ = context2;
|
|
1533
|
+
Object.defineProperty(globalThis, PLOY_CONTEXT_SYMBOL, {
|
|
1534
|
+
get() {
|
|
1535
|
+
return context2;
|
|
1536
|
+
},
|
|
1537
|
+
configurable: true
|
|
1538
|
+
});
|
|
1539
|
+
const bindingNames2 = Object.keys(env2);
|
|
1540
|
+
const features2 = [];
|
|
1541
|
+
if (bindingNames2.length > 0) {
|
|
1542
|
+
features2.push(`bindings: ${bindingNames2.join(", ")}`);
|
|
1543
|
+
}
|
|
1544
|
+
if (hasAuthConfig2) {
|
|
1545
|
+
features2.push("auth");
|
|
1546
|
+
}
|
|
1547
|
+
console.log(`[Ploy] Using CLI mock server at ${cliMockServerUrl} (${features2.join(", ")})`);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1000
1550
|
const configPath = config?.configPath || "./ploy.yaml";
|
|
1001
1551
|
const projectDir = process.cwd();
|
|
1002
1552
|
let ployConfig;
|
|
@@ -1012,7 +1562,9 @@ async function initPloyForDev(config) {
|
|
|
1012
1562
|
if (config?.bindings?.db) {
|
|
1013
1563
|
ployConfig = { ...ployConfig, db: config.bindings.db };
|
|
1014
1564
|
}
|
|
1015
|
-
|
|
1565
|
+
const hasDbBindings = ployConfig.db && Object.keys(ployConfig.db).length > 0;
|
|
1566
|
+
const hasAuthConfig = !!ployConfig.auth;
|
|
1567
|
+
if (!hasDbBindings && !hasAuthConfig) {
|
|
1016
1568
|
return;
|
|
1017
1569
|
}
|
|
1018
1570
|
ensureDataDir(projectDir);
|
|
@@ -1020,8 +1572,14 @@ async function initPloyForDev(config) {
|
|
|
1020
1572
|
mockServer = await startMockServer(dbManager, ployConfig, {});
|
|
1021
1573
|
const apiUrl = `http://localhost:${mockServer.port}`;
|
|
1022
1574
|
const env = {};
|
|
1023
|
-
|
|
1024
|
-
|
|
1575
|
+
if (hasDbBindings && ployConfig.db) {
|
|
1576
|
+
for (const [bindingName, databaseId] of Object.entries(ployConfig.db)) {
|
|
1577
|
+
env[bindingName] = createDevD1(databaseId, apiUrl);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
if (hasAuthConfig) {
|
|
1581
|
+
env.PLOY_AUTH = createDevPloyAuth(apiUrl);
|
|
1582
|
+
process.env.NEXT_PUBLIC_PLOY_AUTH_URL = `${apiUrl}/auth`;
|
|
1025
1583
|
}
|
|
1026
1584
|
const context = {
|
|
1027
1585
|
env,
|
|
@@ -1036,7 +1594,14 @@ async function initPloyForDev(config) {
|
|
|
1036
1594
|
configurable: true
|
|
1037
1595
|
});
|
|
1038
1596
|
const bindingNames = Object.keys(env);
|
|
1039
|
-
|
|
1597
|
+
const features = [];
|
|
1598
|
+
if (bindingNames.length > 0) {
|
|
1599
|
+
features.push(`bindings: ${bindingNames.join(", ")}`);
|
|
1600
|
+
}
|
|
1601
|
+
if (hasAuthConfig) {
|
|
1602
|
+
features.push("auth");
|
|
1603
|
+
}
|
|
1604
|
+
console.log(`[Ploy] Development context initialized with ${features.join(", ")}`);
|
|
1040
1605
|
console.log(`[Ploy] Mock server running at ${apiUrl}`);
|
|
1041
1606
|
const cleanup = async () => {
|
|
1042
1607
|
if (mockServer) {
|