@meetploy/cli 1.12.1 → 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 +573 -12
- package/dist/index.js +604 -13
- package/package.json +1 -1
- package/dist/dashboard-dist/assets/main-BNiZvT9K.css +0 -1
- package/dist/dashboard-dist/assets/main-CYxpKFOS.js +0 -304
package/dist/index.js
CHANGED
|
@@ -7,8 +7,9 @@ import { promisify } from 'util';
|
|
|
7
7
|
import { parse } from 'yaml';
|
|
8
8
|
import { build } from 'esbuild';
|
|
9
9
|
import { watch } from 'chokidar';
|
|
10
|
+
import { randomBytes, randomUUID, createHmac, pbkdf2Sync, timingSafeEqual, createHash } from 'crypto';
|
|
11
|
+
import { getCookie, deleteCookie, setCookie } from 'hono/cookie';
|
|
10
12
|
import { URL, fileURLToPath } from 'url';
|
|
11
|
-
import { randomBytes, randomUUID, createHash } from 'crypto';
|
|
12
13
|
import { serve } from '@hono/node-server';
|
|
13
14
|
import { Hono } from 'hono';
|
|
14
15
|
import { homedir, tmpdir } from 'os';
|
|
@@ -220,6 +221,20 @@ function validatePloyConfig(config, configFile = "ploy.yaml", options = {}) {
|
|
|
220
221
|
throw new Error(`'compatibility_date' in ${configFile} must be in YYYY-MM-DD format (e.g., 2025-04-02)`);
|
|
221
222
|
}
|
|
222
223
|
}
|
|
224
|
+
if (config.auth !== void 0) {
|
|
225
|
+
if (typeof config.auth !== "object" || config.auth === null) {
|
|
226
|
+
throw new Error(`'auth' in ${configFile} must be an object`);
|
|
227
|
+
}
|
|
228
|
+
if (!config.auth.binding) {
|
|
229
|
+
throw new Error(`'auth.binding' is required in ${configFile} when auth is configured`);
|
|
230
|
+
}
|
|
231
|
+
if (typeof config.auth.binding !== "string") {
|
|
232
|
+
throw new Error(`'auth.binding' in ${configFile} must be a string`);
|
|
233
|
+
}
|
|
234
|
+
if (!BINDING_NAME_REGEX.test(config.auth.binding)) {
|
|
235
|
+
throw new Error(`Invalid auth binding name '${config.auth.binding}' in ${configFile}. Binding names must be uppercase with underscores (e.g., AUTH_DB, AUTH)`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
223
238
|
return validatedConfig;
|
|
224
239
|
}
|
|
225
240
|
async function readPloyConfig(projectDir, configPath) {
|
|
@@ -260,7 +275,7 @@ function readAndValidatePloyConfigSync(projectDir, configPath, validationOptions
|
|
|
260
275
|
return validatePloyConfig(config, configFile, validationOptions);
|
|
261
276
|
}
|
|
262
277
|
function hasBindings(config) {
|
|
263
|
-
return !!(config.db || config.queue || config.workflow || config.ai);
|
|
278
|
+
return !!(config.db || config.queue || config.workflow || config.ai || config.auth);
|
|
264
279
|
}
|
|
265
280
|
function getWorkerEntryPoint(projectDir, config) {
|
|
266
281
|
if (config.main) {
|
|
@@ -597,6 +612,12 @@ var init_trace_event = __esm({
|
|
|
597
612
|
}
|
|
598
613
|
});
|
|
599
614
|
|
|
615
|
+
// ../shared/dist/trace-id.js
|
|
616
|
+
var init_trace_id = __esm({
|
|
617
|
+
"../shared/dist/trace-id.js"() {
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
600
621
|
// ../shared/dist/unified-log.js
|
|
601
622
|
var init_unified_log = __esm({
|
|
602
623
|
"../shared/dist/unified-log.js"() {
|
|
@@ -616,6 +637,7 @@ var init_dist = __esm({
|
|
|
616
637
|
init_error();
|
|
617
638
|
init_health_check();
|
|
618
639
|
init_trace_event();
|
|
640
|
+
init_trace_id();
|
|
619
641
|
init_unified_log();
|
|
620
642
|
init_url_validation();
|
|
621
643
|
}
|
|
@@ -1316,6 +1338,255 @@ var init_workerd_config = __esm({
|
|
|
1316
1338
|
"../emulator/dist/config/workerd-config.js"() {
|
|
1317
1339
|
}
|
|
1318
1340
|
});
|
|
1341
|
+
function generateId() {
|
|
1342
|
+
return randomBytes(16).toString("hex");
|
|
1343
|
+
}
|
|
1344
|
+
function hashPassword(password) {
|
|
1345
|
+
const salt = randomBytes(32).toString("hex");
|
|
1346
|
+
const hash = pbkdf2Sync(password, salt, 1e5, 64, "sha512").toString("hex");
|
|
1347
|
+
return `${salt}:${hash}`;
|
|
1348
|
+
}
|
|
1349
|
+
function verifyPassword(password, storedHash) {
|
|
1350
|
+
const [salt, hash] = storedHash.split(":");
|
|
1351
|
+
const derivedHash = pbkdf2Sync(password, salt, 1e5, 64, "sha512").toString("hex");
|
|
1352
|
+
return timingSafeEqual(Buffer.from(hash, "hex"), Buffer.from(derivedHash, "hex"));
|
|
1353
|
+
}
|
|
1354
|
+
function hashToken(token) {
|
|
1355
|
+
return createHmac("sha256", "emulator-secret").update(token).digest("hex");
|
|
1356
|
+
}
|
|
1357
|
+
function base64UrlEncode(str) {
|
|
1358
|
+
return Buffer.from(str).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
1359
|
+
}
|
|
1360
|
+
function base64UrlDecode(str) {
|
|
1361
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
1362
|
+
while (base64.length % 4) {
|
|
1363
|
+
base64 += "=";
|
|
1364
|
+
}
|
|
1365
|
+
return Buffer.from(base64, "base64").toString();
|
|
1366
|
+
}
|
|
1367
|
+
function createJWT(payload) {
|
|
1368
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
1369
|
+
const headerB64 = base64UrlEncode(JSON.stringify(header));
|
|
1370
|
+
const payloadB64 = base64UrlEncode(JSON.stringify(payload));
|
|
1371
|
+
const signature = createHmac("sha256", JWT_SECRET).update(`${headerB64}.${payloadB64}`).digest("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
1372
|
+
return `${headerB64}.${payloadB64}.${signature}`;
|
|
1373
|
+
}
|
|
1374
|
+
function verifyJWT(token) {
|
|
1375
|
+
try {
|
|
1376
|
+
const parts = token.split(".");
|
|
1377
|
+
if (parts.length !== 3) {
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
const [headerB64, payloadB64, signature] = parts;
|
|
1381
|
+
const expectedSig = createHmac("sha256", JWT_SECRET).update(`${headerB64}.${payloadB64}`).digest("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
1382
|
+
if (signature !== expectedSig) {
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
const payload = JSON.parse(base64UrlDecode(payloadB64));
|
|
1386
|
+
if (payload.exp < Math.floor(Date.now() / 1e3)) {
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
return payload;
|
|
1390
|
+
} catch {
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
function createSessionToken(userId, email) {
|
|
1395
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1396
|
+
const sessionId = generateId();
|
|
1397
|
+
const token = createJWT({
|
|
1398
|
+
sub: userId,
|
|
1399
|
+
email,
|
|
1400
|
+
iat: now,
|
|
1401
|
+
exp: now + SESSION_TOKEN_EXPIRY,
|
|
1402
|
+
jti: sessionId
|
|
1403
|
+
});
|
|
1404
|
+
return {
|
|
1405
|
+
token,
|
|
1406
|
+
sessionId,
|
|
1407
|
+
expiresAt: new Date((now + SESSION_TOKEN_EXPIRY) * 1e3)
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
function validateEmail(email) {
|
|
1411
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1412
|
+
if (!emailRegex.test(email)) {
|
|
1413
|
+
return "Invalid email format";
|
|
1414
|
+
}
|
|
1415
|
+
return null;
|
|
1416
|
+
}
|
|
1417
|
+
function validatePassword(password) {
|
|
1418
|
+
if (password.length < 8) {
|
|
1419
|
+
return "Password must be at least 8 characters";
|
|
1420
|
+
}
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
function setSessionCookie(c, sessionToken) {
|
|
1424
|
+
setCookie(c, "ploy_session", sessionToken, {
|
|
1425
|
+
httpOnly: true,
|
|
1426
|
+
secure: false,
|
|
1427
|
+
sameSite: "Lax",
|
|
1428
|
+
path: "/",
|
|
1429
|
+
maxAge: SESSION_TOKEN_EXPIRY
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
function clearSessionCookie(c) {
|
|
1433
|
+
deleteCookie(c, "ploy_session", { path: "/" });
|
|
1434
|
+
}
|
|
1435
|
+
function createAuthHandlers(db) {
|
|
1436
|
+
const signupHandler = async (c) => {
|
|
1437
|
+
try {
|
|
1438
|
+
const body = await c.req.json();
|
|
1439
|
+
const { email, password, metadata } = body;
|
|
1440
|
+
const emailError = validateEmail(email);
|
|
1441
|
+
if (emailError) {
|
|
1442
|
+
return c.json({ error: emailError }, 400);
|
|
1443
|
+
}
|
|
1444
|
+
const passwordError = validatePassword(password);
|
|
1445
|
+
if (passwordError) {
|
|
1446
|
+
return c.json({ error: passwordError }, 400);
|
|
1447
|
+
}
|
|
1448
|
+
const existingUser = db.prepare("SELECT id FROM auth_users WHERE email = ?").get(email.toLowerCase());
|
|
1449
|
+
if (existingUser) {
|
|
1450
|
+
return c.json({ error: "User already exists" }, 409);
|
|
1451
|
+
}
|
|
1452
|
+
const userId = generateId();
|
|
1453
|
+
const passwordHash = hashPassword(password);
|
|
1454
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1455
|
+
db.prepare(`INSERT INTO auth_users (id, email, password_hash, created_at, updated_at, metadata)
|
|
1456
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(userId, email.toLowerCase(), passwordHash, now, now, metadata ? JSON.stringify(metadata) : null);
|
|
1457
|
+
const { token: sessionToken, sessionId, expiresAt } = createSessionToken(userId, email.toLowerCase());
|
|
1458
|
+
const sessionTokenHash = hashToken(sessionToken);
|
|
1459
|
+
db.prepare(`INSERT INTO auth_sessions (id, user_id, token_hash, expires_at, created_at)
|
|
1460
|
+
VALUES (?, ?, ?, ?, ?)`).run(sessionId, userId, sessionTokenHash, expiresAt.toISOString(), now);
|
|
1461
|
+
setSessionCookie(c, sessionToken);
|
|
1462
|
+
return c.json({
|
|
1463
|
+
user: {
|
|
1464
|
+
id: userId,
|
|
1465
|
+
email: email.toLowerCase(),
|
|
1466
|
+
emailVerified: false,
|
|
1467
|
+
createdAt: now,
|
|
1468
|
+
metadata: metadata ?? null
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
} catch (err) {
|
|
1472
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1473
|
+
return c.json({ error: message }, 500);
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
const signinHandler = async (c) => {
|
|
1477
|
+
try {
|
|
1478
|
+
const body = await c.req.json();
|
|
1479
|
+
const { email, password } = body;
|
|
1480
|
+
const user = db.prepare("SELECT * FROM auth_users WHERE email = ?").get(email.toLowerCase());
|
|
1481
|
+
if (!user) {
|
|
1482
|
+
return c.json({ error: "Invalid credentials" }, 401);
|
|
1483
|
+
}
|
|
1484
|
+
if (!verifyPassword(password, user.password_hash)) {
|
|
1485
|
+
return c.json({ error: "Invalid credentials" }, 401);
|
|
1486
|
+
}
|
|
1487
|
+
const { token: sessionToken, sessionId, expiresAt } = createSessionToken(user.id, user.email);
|
|
1488
|
+
const sessionTokenHash = hashToken(sessionToken);
|
|
1489
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1490
|
+
db.prepare(`INSERT INTO auth_sessions (id, user_id, token_hash, expires_at, created_at)
|
|
1491
|
+
VALUES (?, ?, ?, ?, ?)`).run(sessionId, user.id, sessionTokenHash, expiresAt.toISOString(), now);
|
|
1492
|
+
let metadata = null;
|
|
1493
|
+
if (user.metadata) {
|
|
1494
|
+
try {
|
|
1495
|
+
metadata = JSON.parse(user.metadata);
|
|
1496
|
+
} catch {
|
|
1497
|
+
metadata = null;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
setSessionCookie(c, sessionToken);
|
|
1501
|
+
return c.json({
|
|
1502
|
+
user: {
|
|
1503
|
+
id: user.id,
|
|
1504
|
+
email: user.email,
|
|
1505
|
+
emailVerified: user.email_verified === 1,
|
|
1506
|
+
createdAt: user.created_at,
|
|
1507
|
+
metadata
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
} catch (err) {
|
|
1511
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1512
|
+
return c.json({ error: message }, 500);
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
const meHandler = async (c) => {
|
|
1516
|
+
try {
|
|
1517
|
+
const cookieToken = getCookie(c, "ploy_session");
|
|
1518
|
+
const authHeader = c.req.header("Authorization");
|
|
1519
|
+
let token;
|
|
1520
|
+
if (cookieToken) {
|
|
1521
|
+
token = cookieToken;
|
|
1522
|
+
} else if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
1523
|
+
token = authHeader.slice(7);
|
|
1524
|
+
}
|
|
1525
|
+
if (!token) {
|
|
1526
|
+
return c.json({ error: "Missing authentication" }, 401);
|
|
1527
|
+
}
|
|
1528
|
+
const payload = verifyJWT(token);
|
|
1529
|
+
if (!payload) {
|
|
1530
|
+
return c.json({ error: "Invalid or expired session" }, 401);
|
|
1531
|
+
}
|
|
1532
|
+
const user = db.prepare("SELECT id, email, email_verified, created_at, updated_at, metadata FROM auth_users WHERE id = ?").get(payload.sub);
|
|
1533
|
+
if (!user) {
|
|
1534
|
+
return c.json({ error: "User not found" }, 401);
|
|
1535
|
+
}
|
|
1536
|
+
let metadata = null;
|
|
1537
|
+
if (user.metadata) {
|
|
1538
|
+
try {
|
|
1539
|
+
metadata = JSON.parse(user.metadata);
|
|
1540
|
+
} catch {
|
|
1541
|
+
metadata = null;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return c.json({
|
|
1545
|
+
user: {
|
|
1546
|
+
id: user.id,
|
|
1547
|
+
email: user.email,
|
|
1548
|
+
emailVerified: user.email_verified === 1,
|
|
1549
|
+
createdAt: user.created_at,
|
|
1550
|
+
updatedAt: user.updated_at,
|
|
1551
|
+
metadata
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
} catch (err) {
|
|
1555
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1556
|
+
return c.json({ error: message }, 500);
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
const signoutHandler = async (c) => {
|
|
1560
|
+
try {
|
|
1561
|
+
const sessionToken = getCookie(c, "ploy_session");
|
|
1562
|
+
if (sessionToken) {
|
|
1563
|
+
const payload = verifyJWT(sessionToken);
|
|
1564
|
+
if (payload) {
|
|
1565
|
+
const tokenHash = hashToken(sessionToken);
|
|
1566
|
+
db.prepare("UPDATE auth_sessions SET revoked = 1 WHERE token_hash = ?").run(tokenHash);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
clearSessionCookie(c);
|
|
1570
|
+
return c.json({ success: true });
|
|
1571
|
+
} catch (err) {
|
|
1572
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1573
|
+
return c.json({ error: message }, 500);
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
return {
|
|
1577
|
+
signupHandler,
|
|
1578
|
+
signinHandler,
|
|
1579
|
+
meHandler,
|
|
1580
|
+
signoutHandler
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
var JWT_SECRET, SESSION_TOKEN_EXPIRY;
|
|
1584
|
+
var init_auth_service = __esm({
|
|
1585
|
+
"../emulator/dist/services/auth-service.js"() {
|
|
1586
|
+
JWT_SECRET = "ploy-emulator-dev-secret";
|
|
1587
|
+
SESSION_TOKEN_EXPIRY = 7 * 24 * 60 * 60;
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1319
1590
|
function findDashboardDistPath() {
|
|
1320
1591
|
const possiblePaths = [
|
|
1321
1592
|
join(__dirname, "dashboard-dist"),
|
|
@@ -1343,9 +1614,174 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
1343
1614
|
return c.json({
|
|
1344
1615
|
db: config.db,
|
|
1345
1616
|
queue: config.queue,
|
|
1346
|
-
workflow: config.workflow
|
|
1617
|
+
workflow: config.workflow,
|
|
1618
|
+
auth: config.auth
|
|
1347
1619
|
});
|
|
1348
1620
|
});
|
|
1621
|
+
if (config.auth) {
|
|
1622
|
+
app.get("/api/auth/tables", (c) => {
|
|
1623
|
+
try {
|
|
1624
|
+
const db = dbManager2.emulatorDb;
|
|
1625
|
+
const tables = db.prepare(`SELECT name FROM sqlite_master
|
|
1626
|
+
WHERE type='table' AND (name = 'auth_users' OR name = 'auth_sessions')
|
|
1627
|
+
ORDER BY name`).all();
|
|
1628
|
+
return c.json({ tables });
|
|
1629
|
+
} catch (err) {
|
|
1630
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
app.get("/api/auth/tables/:tableName", (c) => {
|
|
1634
|
+
const tableName = c.req.param("tableName");
|
|
1635
|
+
if (tableName !== "auth_users" && tableName !== "auth_sessions") {
|
|
1636
|
+
return c.json({ error: "Table not found" }, 404);
|
|
1637
|
+
}
|
|
1638
|
+
const limit = parseInt(c.req.query("limit") || "50", 10);
|
|
1639
|
+
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
1640
|
+
try {
|
|
1641
|
+
const db = dbManager2.emulatorDb;
|
|
1642
|
+
const columnsResult = db.prepare(`PRAGMA table_info("${tableName}")`).all();
|
|
1643
|
+
const columns = columnsResult.map((col) => col.name);
|
|
1644
|
+
const countResult = db.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`).get();
|
|
1645
|
+
const total = countResult.count;
|
|
1646
|
+
let data;
|
|
1647
|
+
if (tableName === "auth_users") {
|
|
1648
|
+
data = db.prepare(`SELECT id, email, email_verified, created_at, updated_at, metadata FROM "${tableName}" LIMIT ? OFFSET ?`).all(limit, offset);
|
|
1649
|
+
} else {
|
|
1650
|
+
data = db.prepare(`SELECT * FROM "${tableName}" LIMIT ? OFFSET ?`).all(limit, offset);
|
|
1651
|
+
}
|
|
1652
|
+
const visibleColumns = tableName === "auth_users" ? columns.filter((c2) => c2 !== "password_hash") : columns;
|
|
1653
|
+
return c.json({ data, columns: visibleColumns, total });
|
|
1654
|
+
} catch (err) {
|
|
1655
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
app.get("/api/auth/schema", (c) => {
|
|
1659
|
+
try {
|
|
1660
|
+
const db = dbManager2.emulatorDb;
|
|
1661
|
+
const tables = ["auth_users", "auth_sessions"].map((tableName) => {
|
|
1662
|
+
const columnsResult = db.prepare(`PRAGMA table_info("${tableName}")`).all();
|
|
1663
|
+
const visibleColumns = tableName === "auth_users" ? columnsResult.filter((col) => col.name !== "password_hash") : columnsResult;
|
|
1664
|
+
return {
|
|
1665
|
+
name: tableName,
|
|
1666
|
+
columns: visibleColumns.map((col) => ({
|
|
1667
|
+
name: col.name,
|
|
1668
|
+
type: col.type,
|
|
1669
|
+
notNull: col.notnull === 1,
|
|
1670
|
+
primaryKey: col.pk === 1
|
|
1671
|
+
}))
|
|
1672
|
+
};
|
|
1673
|
+
});
|
|
1674
|
+
return c.json({ tables });
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
app.post("/api/auth/query", async (c) => {
|
|
1680
|
+
const body = await c.req.json();
|
|
1681
|
+
const { query } = body;
|
|
1682
|
+
if (!query) {
|
|
1683
|
+
return c.json({ error: "Query is required" }, 400);
|
|
1684
|
+
}
|
|
1685
|
+
const normalizedQuery = query.trim().toUpperCase();
|
|
1686
|
+
if (!normalizedQuery.startsWith("SELECT")) {
|
|
1687
|
+
return c.json({ error: "Only SELECT queries are allowed on auth tables" }, 400);
|
|
1688
|
+
}
|
|
1689
|
+
const allowedTables = ["auth_users", "auth_sessions"];
|
|
1690
|
+
const hasDisallowedTable = !allowedTables.some((table) => query.toLowerCase().includes(`from ${table}`) || query.toLowerCase().includes(`join ${table}`));
|
|
1691
|
+
if (hasDisallowedTable) {
|
|
1692
|
+
return c.json({
|
|
1693
|
+
error: "Query must reference auth tables (auth_users or auth_sessions)"
|
|
1694
|
+
}, 400);
|
|
1695
|
+
}
|
|
1696
|
+
try {
|
|
1697
|
+
const db = dbManager2.emulatorDb;
|
|
1698
|
+
const startTime = Date.now();
|
|
1699
|
+
const stmt = db.prepare(query);
|
|
1700
|
+
const results = stmt.all();
|
|
1701
|
+
const sanitizedResults = results.map((row) => {
|
|
1702
|
+
const { password_hash: _, ...rest } = row;
|
|
1703
|
+
return rest;
|
|
1704
|
+
});
|
|
1705
|
+
const duration = Date.now() - startTime;
|
|
1706
|
+
return c.json({
|
|
1707
|
+
results: sanitizedResults,
|
|
1708
|
+
success: true,
|
|
1709
|
+
meta: {
|
|
1710
|
+
duration,
|
|
1711
|
+
rows_read: results.length,
|
|
1712
|
+
rows_written: 0
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
} catch (err) {
|
|
1716
|
+
return c.json({
|
|
1717
|
+
results: [],
|
|
1718
|
+
success: false,
|
|
1719
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1720
|
+
meta: { duration: 0, rows_read: 0, rows_written: 0 }
|
|
1721
|
+
}, 400);
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
app.get("/api/auth/settings", (c) => {
|
|
1725
|
+
try {
|
|
1726
|
+
const db = dbManager2.emulatorDb;
|
|
1727
|
+
const settings = db.prepare("SELECT * FROM auth_settings WHERE id = 1").get();
|
|
1728
|
+
if (!settings) {
|
|
1729
|
+
return c.json({
|
|
1730
|
+
sessionTokenExpiry: 604800,
|
|
1731
|
+
allowSignups: true,
|
|
1732
|
+
requireEmailVerification: false,
|
|
1733
|
+
requireName: false
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
return c.json({
|
|
1737
|
+
sessionTokenExpiry: settings.session_token_expiry,
|
|
1738
|
+
allowSignups: settings.allow_signups === 1,
|
|
1739
|
+
requireEmailVerification: settings.require_email_verification === 1,
|
|
1740
|
+
requireName: settings.require_name === 1
|
|
1741
|
+
});
|
|
1742
|
+
} catch (err) {
|
|
1743
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
app.patch("/api/auth/settings", async (c) => {
|
|
1747
|
+
try {
|
|
1748
|
+
const body = await c.req.json();
|
|
1749
|
+
const db = dbManager2.emulatorDb;
|
|
1750
|
+
const updates = [];
|
|
1751
|
+
const values = [];
|
|
1752
|
+
if (body.sessionTokenExpiry !== void 0) {
|
|
1753
|
+
updates.push("session_token_expiry = ?");
|
|
1754
|
+
values.push(body.sessionTokenExpiry);
|
|
1755
|
+
}
|
|
1756
|
+
if (body.allowSignups !== void 0) {
|
|
1757
|
+
updates.push("allow_signups = ?");
|
|
1758
|
+
values.push(body.allowSignups ? 1 : 0);
|
|
1759
|
+
}
|
|
1760
|
+
if (body.requireEmailVerification !== void 0) {
|
|
1761
|
+
updates.push("require_email_verification = ?");
|
|
1762
|
+
values.push(body.requireEmailVerification ? 1 : 0);
|
|
1763
|
+
}
|
|
1764
|
+
if (body.requireName !== void 0) {
|
|
1765
|
+
updates.push("require_name = ?");
|
|
1766
|
+
values.push(body.requireName ? 1 : 0);
|
|
1767
|
+
}
|
|
1768
|
+
if (updates.length > 0) {
|
|
1769
|
+
updates.push("updated_at = strftime('%s', 'now')");
|
|
1770
|
+
const sql = `UPDATE auth_settings SET ${updates.join(", ")} WHERE id = 1`;
|
|
1771
|
+
db.prepare(sql).run(...values);
|
|
1772
|
+
}
|
|
1773
|
+
const settings = db.prepare("SELECT * FROM auth_settings WHERE id = 1").get();
|
|
1774
|
+
return c.json({
|
|
1775
|
+
sessionTokenExpiry: settings.session_token_expiry,
|
|
1776
|
+
allowSignups: settings.allow_signups === 1,
|
|
1777
|
+
requireEmailVerification: settings.require_email_verification === 1,
|
|
1778
|
+
requireName: settings.require_name === 1
|
|
1779
|
+
});
|
|
1780
|
+
} catch (err) {
|
|
1781
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1349
1785
|
app.post("/api/db/:binding/query", async (c) => {
|
|
1350
1786
|
const binding = c.req.param("binding");
|
|
1351
1787
|
const resourceName = getDbResourceName(binding);
|
|
@@ -1568,7 +2004,7 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
1568
2004
|
}
|
|
1569
2005
|
try {
|
|
1570
2006
|
const db = dbManager2.emulatorDb;
|
|
1571
|
-
const execution = db.prepare(`SELECT id, workflow_name, status, error, started_at, completed_at, created_at
|
|
2007
|
+
const execution = db.prepare(`SELECT id, workflow_name, status, input, output, error, started_at, completed_at, created_at
|
|
1572
2008
|
FROM workflow_executions
|
|
1573
2009
|
WHERE id = ?`).get(executionId);
|
|
1574
2010
|
if (!execution) {
|
|
@@ -1582,6 +2018,8 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
1582
2018
|
execution: {
|
|
1583
2019
|
id: execution.id,
|
|
1584
2020
|
status: execution.status.toUpperCase(),
|
|
2021
|
+
input: execution.input ? JSON.parse(execution.input) : null,
|
|
2022
|
+
output: execution.output ? JSON.parse(execution.output) : null,
|
|
1585
2023
|
startedAt: execution.started_at ? new Date(execution.started_at * 1e3).toISOString() : null,
|
|
1586
2024
|
completedAt: execution.completed_at ? new Date(execution.completed_at * 1e3).toISOString() : null,
|
|
1587
2025
|
durationMs: execution.started_at && execution.completed_at ? (execution.completed_at - execution.started_at) * 1e3 : null,
|
|
@@ -1830,8 +2268,10 @@ function createQueueHandlers(db) {
|
|
|
1830
2268
|
try {
|
|
1831
2269
|
const body = await c.req.json();
|
|
1832
2270
|
const { messageId, deliveryId } = body;
|
|
1833
|
-
const
|
|
1834
|
-
|
|
2271
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2272
|
+
const result = db.prepare(`UPDATE queue_messages
|
|
2273
|
+
SET status = 'acknowledged', updated_at = ?
|
|
2274
|
+
WHERE id = ? AND delivery_id = ?`).run(now, messageId, deliveryId);
|
|
1835
2275
|
if (result.changes === 0) {
|
|
1836
2276
|
return c.json({ success: false, error: "Message not found or already processed" }, 404);
|
|
1837
2277
|
}
|
|
@@ -1906,7 +2346,7 @@ function createQueueProcessor(db, queueBindings, workerUrl) {
|
|
|
1906
2346
|
body: row.payload
|
|
1907
2347
|
});
|
|
1908
2348
|
if (response.ok) {
|
|
1909
|
-
db.prepare(`
|
|
2349
|
+
db.prepare(`UPDATE queue_messages SET status = 'acknowledged', updated_at = ? WHERE id = ?`).run(now, row.id);
|
|
1910
2350
|
} else {
|
|
1911
2351
|
db.prepare(`UPDATE queue_messages
|
|
1912
2352
|
SET status = 'pending', delivery_id = NULL, visible_at = ?, updated_at = ?
|
|
@@ -2143,6 +2583,13 @@ async function startMockServer(dbManager2, config, options = {}) {
|
|
|
2143
2583
|
app.post("/workflow/complete", workflowHandlers.completeHandler);
|
|
2144
2584
|
app.post("/workflow/fail", workflowHandlers.failHandler);
|
|
2145
2585
|
}
|
|
2586
|
+
if (config.auth) {
|
|
2587
|
+
const authHandlers = createAuthHandlers(dbManager2.emulatorDb);
|
|
2588
|
+
app.post("/auth/signup", authHandlers.signupHandler);
|
|
2589
|
+
app.post("/auth/signin", authHandlers.signinHandler);
|
|
2590
|
+
app.get("/auth/me", authHandlers.meHandler);
|
|
2591
|
+
app.post("/auth/signout", authHandlers.signoutHandler);
|
|
2592
|
+
}
|
|
2146
2593
|
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
2147
2594
|
if (options.dashboardEnabled !== false) {
|
|
2148
2595
|
createDashboardRoutes(app, dbManager2, config);
|
|
@@ -2165,6 +2612,7 @@ async function startMockServer(dbManager2, config, options = {}) {
|
|
|
2165
2612
|
var DEFAULT_MOCK_SERVER_PORT;
|
|
2166
2613
|
var init_mock_server = __esm({
|
|
2167
2614
|
"../emulator/dist/services/mock-server.js"() {
|
|
2615
|
+
init_auth_service();
|
|
2168
2616
|
init_dashboard_routes();
|
|
2169
2617
|
init_db_service();
|
|
2170
2618
|
init_queue_service();
|
|
@@ -2281,6 +2729,49 @@ CREATE TABLE IF NOT EXISTS workflow_steps (
|
|
|
2281
2729
|
|
|
2282
2730
|
CREATE INDEX IF NOT EXISTS idx_workflow_steps_execution
|
|
2283
2731
|
ON workflow_steps(execution_id, step_index);
|
|
2732
|
+
|
|
2733
|
+
-- Auth users table
|
|
2734
|
+
CREATE TABLE IF NOT EXISTS auth_users (
|
|
2735
|
+
id TEXT PRIMARY KEY,
|
|
2736
|
+
email TEXT UNIQUE NOT NULL,
|
|
2737
|
+
email_verified INTEGER NOT NULL DEFAULT 0,
|
|
2738
|
+
password_hash TEXT NOT NULL,
|
|
2739
|
+
created_at TEXT NOT NULL,
|
|
2740
|
+
updated_at TEXT NOT NULL,
|
|
2741
|
+
metadata TEXT
|
|
2742
|
+
);
|
|
2743
|
+
|
|
2744
|
+
CREATE INDEX IF NOT EXISTS idx_auth_users_email
|
|
2745
|
+
ON auth_users(email);
|
|
2746
|
+
|
|
2747
|
+
-- Auth sessions table
|
|
2748
|
+
CREATE TABLE IF NOT EXISTS auth_sessions (
|
|
2749
|
+
id TEXT PRIMARY KEY,
|
|
2750
|
+
user_id TEXT NOT NULL,
|
|
2751
|
+
token_hash TEXT UNIQUE NOT NULL,
|
|
2752
|
+
expires_at TEXT NOT NULL,
|
|
2753
|
+
created_at TEXT NOT NULL,
|
|
2754
|
+
revoked INTEGER NOT NULL DEFAULT 0,
|
|
2755
|
+
FOREIGN KEY (user_id) REFERENCES auth_users(id) ON DELETE CASCADE
|
|
2756
|
+
);
|
|
2757
|
+
|
|
2758
|
+
CREATE INDEX IF NOT EXISTS idx_auth_sessions_user
|
|
2759
|
+
ON auth_sessions(user_id);
|
|
2760
|
+
CREATE INDEX IF NOT EXISTS idx_auth_sessions_hash
|
|
2761
|
+
ON auth_sessions(token_hash);
|
|
2762
|
+
|
|
2763
|
+
-- Auth settings table
|
|
2764
|
+
CREATE TABLE IF NOT EXISTS auth_settings (
|
|
2765
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
2766
|
+
session_token_expiry INTEGER NOT NULL DEFAULT 604800,
|
|
2767
|
+
allow_signups INTEGER NOT NULL DEFAULT 1,
|
|
2768
|
+
require_email_verification INTEGER NOT NULL DEFAULT 0,
|
|
2769
|
+
require_name INTEGER NOT NULL DEFAULT 0,
|
|
2770
|
+
updated_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
2771
|
+
);
|
|
2772
|
+
|
|
2773
|
+
-- Insert default settings if not exists
|
|
2774
|
+
INSERT OR IGNORE INTO auth_settings (id) VALUES (1);
|
|
2284
2775
|
`;
|
|
2285
2776
|
}
|
|
2286
2777
|
});
|
|
@@ -2635,6 +3126,38 @@ function createDevD1(databaseId, apiUrl) {
|
|
|
2635
3126
|
}
|
|
2636
3127
|
};
|
|
2637
3128
|
}
|
|
3129
|
+
function createDevPloyAuth(apiUrl) {
|
|
3130
|
+
return {
|
|
3131
|
+
async getUser(token) {
|
|
3132
|
+
try {
|
|
3133
|
+
const response = await fetch(`${apiUrl}/auth/me`, {
|
|
3134
|
+
headers: {
|
|
3135
|
+
Authorization: `Bearer ${token}`
|
|
3136
|
+
}
|
|
3137
|
+
});
|
|
3138
|
+
if (!response.ok) {
|
|
3139
|
+
return null;
|
|
3140
|
+
}
|
|
3141
|
+
const data = await response.json();
|
|
3142
|
+
return data.user;
|
|
3143
|
+
} catch {
|
|
3144
|
+
return null;
|
|
3145
|
+
}
|
|
3146
|
+
},
|
|
3147
|
+
async verifyToken(token) {
|
|
3148
|
+
try {
|
|
3149
|
+
const response = await fetch(`${apiUrl}/auth/me`, {
|
|
3150
|
+
headers: {
|
|
3151
|
+
Authorization: `Bearer ${token}`
|
|
3152
|
+
}
|
|
3153
|
+
});
|
|
3154
|
+
return response.ok;
|
|
3155
|
+
} catch {
|
|
3156
|
+
return false;
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
2638
3161
|
async function initPloyForDev(config) {
|
|
2639
3162
|
if (process.env.NODE_ENV !== "development") {
|
|
2640
3163
|
return;
|
|
@@ -2643,6 +3166,56 @@ async function initPloyForDev(config) {
|
|
|
2643
3166
|
return;
|
|
2644
3167
|
}
|
|
2645
3168
|
globalThis.__PLOY_DEV_INITIALIZED__ = true;
|
|
3169
|
+
const cliMockServerUrl = process.env.PLOY_MOCK_SERVER_URL;
|
|
3170
|
+
if (cliMockServerUrl) {
|
|
3171
|
+
const configPath2 = config?.configPath || "./ploy.yaml";
|
|
3172
|
+
const projectDir2 = process.cwd();
|
|
3173
|
+
let ployConfig2;
|
|
3174
|
+
try {
|
|
3175
|
+
ployConfig2 = readPloyConfig2(projectDir2, configPath2);
|
|
3176
|
+
} catch {
|
|
3177
|
+
if (config?.bindings?.db) {
|
|
3178
|
+
ployConfig2 = { db: config.bindings.db };
|
|
3179
|
+
} else {
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
if (config?.bindings?.db) {
|
|
3184
|
+
ployConfig2 = { ...ployConfig2, db: config.bindings.db };
|
|
3185
|
+
}
|
|
3186
|
+
const hasDbBindings2 = ployConfig2.db && Object.keys(ployConfig2.db).length > 0;
|
|
3187
|
+
const hasAuthConfig2 = !!ployConfig2.auth;
|
|
3188
|
+
if (!hasDbBindings2 && !hasAuthConfig2) {
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
const env2 = {};
|
|
3192
|
+
if (hasDbBindings2 && ployConfig2.db) {
|
|
3193
|
+
for (const [bindingName, databaseId] of Object.entries(ployConfig2.db)) {
|
|
3194
|
+
env2[bindingName] = createDevD1(databaseId, cliMockServerUrl);
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
if (hasAuthConfig2) {
|
|
3198
|
+
env2.PLOY_AUTH = createDevPloyAuth(cliMockServerUrl);
|
|
3199
|
+
}
|
|
3200
|
+
const context2 = { env: env2, cf: void 0, ctx: void 0 };
|
|
3201
|
+
globalThis.__PLOY_DEV_CONTEXT__ = context2;
|
|
3202
|
+
Object.defineProperty(globalThis, PLOY_CONTEXT_SYMBOL, {
|
|
3203
|
+
get() {
|
|
3204
|
+
return context2;
|
|
3205
|
+
},
|
|
3206
|
+
configurable: true
|
|
3207
|
+
});
|
|
3208
|
+
const bindingNames2 = Object.keys(env2);
|
|
3209
|
+
const features2 = [];
|
|
3210
|
+
if (bindingNames2.length > 0) {
|
|
3211
|
+
features2.push(`bindings: ${bindingNames2.join(", ")}`);
|
|
3212
|
+
}
|
|
3213
|
+
if (hasAuthConfig2) {
|
|
3214
|
+
features2.push("auth");
|
|
3215
|
+
}
|
|
3216
|
+
console.log(`[Ploy] Using CLI mock server at ${cliMockServerUrl} (${features2.join(", ")})`);
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
2646
3219
|
const configPath = config?.configPath || "./ploy.yaml";
|
|
2647
3220
|
const projectDir = process.cwd();
|
|
2648
3221
|
let ployConfig;
|
|
@@ -2658,7 +3231,9 @@ async function initPloyForDev(config) {
|
|
|
2658
3231
|
if (config?.bindings?.db) {
|
|
2659
3232
|
ployConfig = { ...ployConfig, db: config.bindings.db };
|
|
2660
3233
|
}
|
|
2661
|
-
|
|
3234
|
+
const hasDbBindings = ployConfig.db && Object.keys(ployConfig.db).length > 0;
|
|
3235
|
+
const hasAuthConfig = !!ployConfig.auth;
|
|
3236
|
+
if (!hasDbBindings && !hasAuthConfig) {
|
|
2662
3237
|
return;
|
|
2663
3238
|
}
|
|
2664
3239
|
ensureDataDir(projectDir);
|
|
@@ -2666,8 +3241,14 @@ async function initPloyForDev(config) {
|
|
|
2666
3241
|
mockServer = await startMockServer(dbManager, ployConfig, {});
|
|
2667
3242
|
const apiUrl = `http://localhost:${mockServer.port}`;
|
|
2668
3243
|
const env = {};
|
|
2669
|
-
|
|
2670
|
-
|
|
3244
|
+
if (hasDbBindings && ployConfig.db) {
|
|
3245
|
+
for (const [bindingName, databaseId] of Object.entries(ployConfig.db)) {
|
|
3246
|
+
env[bindingName] = createDevD1(databaseId, apiUrl);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
if (hasAuthConfig) {
|
|
3250
|
+
env.PLOY_AUTH = createDevPloyAuth(apiUrl);
|
|
3251
|
+
process.env.NEXT_PUBLIC_PLOY_AUTH_URL = `${apiUrl}/auth`;
|
|
2671
3252
|
}
|
|
2672
3253
|
const context = {
|
|
2673
3254
|
env,
|
|
@@ -2682,7 +3263,14 @@ async function initPloyForDev(config) {
|
|
|
2682
3263
|
configurable: true
|
|
2683
3264
|
});
|
|
2684
3265
|
const bindingNames = Object.keys(env);
|
|
2685
|
-
|
|
3266
|
+
const features = [];
|
|
3267
|
+
if (bindingNames.length > 0) {
|
|
3268
|
+
features.push(`bindings: ${bindingNames.join(", ")}`);
|
|
3269
|
+
}
|
|
3270
|
+
if (hasAuthConfig) {
|
|
3271
|
+
features.push("auth");
|
|
3272
|
+
}
|
|
3273
|
+
console.log(`[Ploy] Development context initialized with ${features.join(", ")}`);
|
|
2686
3274
|
console.log(`[Ploy] Mock server running at ${apiUrl}`);
|
|
2687
3275
|
const cleanup = async () => {
|
|
2688
3276
|
if (mockServer) {
|
|
@@ -4476,7 +5064,9 @@ async function startNextJsDev(options) {
|
|
|
4476
5064
|
env: {
|
|
4477
5065
|
...process.env,
|
|
4478
5066
|
// Set environment variable so initPloyForDev knows the mock server URL
|
|
4479
|
-
PLOY_MOCK_SERVER_URL: `http://localhost:${dashboard.port}
|
|
5067
|
+
PLOY_MOCK_SERVER_URL: `http://localhost:${dashboard.port}`,
|
|
5068
|
+
// Set PORT so initPloyForDev can construct the correct handler URL
|
|
5069
|
+
PORT: String(nextPort)
|
|
4480
5070
|
}
|
|
4481
5071
|
});
|
|
4482
5072
|
} else {
|
|
@@ -4486,7 +5076,8 @@ async function startNextJsDev(options) {
|
|
|
4486
5076
|
shell: true,
|
|
4487
5077
|
env: {
|
|
4488
5078
|
...process.env,
|
|
4489
|
-
PLOY_MOCK_SERVER_URL: `http://localhost:${dashboard.port}
|
|
5079
|
+
PLOY_MOCK_SERVER_URL: `http://localhost:${dashboard.port}`,
|
|
5080
|
+
PORT: String(nextPort)
|
|
4490
5081
|
}
|
|
4491
5082
|
});
|
|
4492
5083
|
}
|