@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/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) {
@@ -465,10 +480,12 @@ export function initializeDB(bindingName: string, serviceUrl: string): DBDatabas
465
480
  return {
466
481
  prepare(query) {
467
482
  let boundParams = [];
468
- return {
483
+ const stmt = {
484
+ __db_data: { query, params: boundParams },
469
485
  bind(...values) {
470
486
  boundParams = values;
471
- return this;
487
+ stmt.__db_data = { query, params: boundParams };
488
+ return stmt;
472
489
  },
473
490
  async run() {
474
491
  const response = await fetch(serviceUrl, {
@@ -516,6 +533,7 @@ export function initializeDB(bindingName: string, serviceUrl: string): DBDatabas
516
533
  return arrayRows;
517
534
  },
518
535
  };
536
+ return stmt;
519
537
  },
520
538
  async dump() {
521
539
  const response = await fetch(serviceUrl, {
@@ -594,6 +612,18 @@ var init_trace_event = __esm({
594
612
  }
595
613
  });
596
614
 
615
+ // ../shared/dist/trace-id.js
616
+ var init_trace_id = __esm({
617
+ "../shared/dist/trace-id.js"() {
618
+ }
619
+ });
620
+
621
+ // ../shared/dist/unified-log.js
622
+ var init_unified_log = __esm({
623
+ "../shared/dist/unified-log.js"() {
624
+ }
625
+ });
626
+
597
627
  // ../shared/dist/url-validation.js
598
628
  var init_url_validation = __esm({
599
629
  "../shared/dist/url-validation.js"() {
@@ -607,6 +637,8 @@ var init_dist = __esm({
607
637
  init_error();
608
638
  init_health_check();
609
639
  init_trace_event();
640
+ init_trace_id();
641
+ init_unified_log();
610
642
  init_url_validation();
611
643
  }
612
644
  });
@@ -1113,6 +1145,42 @@ var init_bundler = __esm({
1113
1145
  ];
1114
1146
  }
1115
1147
  });
1148
+
1149
+ // ../emulator/dist/utils/logger.js
1150
+ function timestamp() {
1151
+ return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
1152
+ }
1153
+ function log(message) {
1154
+ console.log(`${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.cyan}[ploy]${COLORS.reset} ${message}`);
1155
+ }
1156
+ function success(message) {
1157
+ console.log(`${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.green}[ploy]${COLORS.reset} ${message}`);
1158
+ }
1159
+ function warn(message) {
1160
+ console.log(`${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.yellow}[ploy]${COLORS.reset} ${message}`);
1161
+ }
1162
+ function error(message) {
1163
+ console.error(`${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.red}[ploy]${COLORS.reset} ${message}`);
1164
+ }
1165
+ function debug(message, verbose) {
1166
+ if (verbose) {
1167
+ console.log(`${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.magenta}[ploy:debug]${COLORS.reset} ${message}`);
1168
+ }
1169
+ }
1170
+ var COLORS;
1171
+ var init_logger = __esm({
1172
+ "../emulator/dist/utils/logger.js"() {
1173
+ COLORS = {
1174
+ reset: "\x1B[0m",
1175
+ dim: "\x1B[2m",
1176
+ cyan: "\x1B[36m",
1177
+ green: "\x1B[32m",
1178
+ yellow: "\x1B[33m",
1179
+ red: "\x1B[31m",
1180
+ magenta: "\x1B[35m"
1181
+ };
1182
+ }
1183
+ });
1116
1184
  function createFileWatcher(srcDir, onRebuild) {
1117
1185
  let watcher = null;
1118
1186
  let debounceTimer = null;
@@ -1145,7 +1213,27 @@ function createFileWatcher(srcDir, onRebuild) {
1145
1213
  watcher = watch(srcDir, {
1146
1214
  persistent: true,
1147
1215
  ignoreInitial: true,
1148
- ignored: ["**/node_modules/**", "**/dist/**", "**/.ploy/**", "**/.*"]
1216
+ ignored: [
1217
+ "**/node_modules/**",
1218
+ "**/dist/**",
1219
+ "**/.*",
1220
+ "**/.*/**",
1221
+ "**/coverage/**",
1222
+ "**/build/**",
1223
+ "**/*.log",
1224
+ "**/pnpm-lock.yaml",
1225
+ "**/package-lock.json",
1226
+ "**/yarn.lock"
1227
+ ]
1228
+ });
1229
+ watcher.on("error", (err) => {
1230
+ const error2 = err;
1231
+ if (error2.code === "EMFILE") {
1232
+ warn("Warning: Too many open files. Some file changes may not be detected.");
1233
+ warn("Consider increasing your system's file descriptor limit (ulimit -n).");
1234
+ } else {
1235
+ error(`File watcher error: ${error2.message || String(err)}`);
1236
+ }
1149
1237
  });
1150
1238
  watcher.on("change", (filePath) => {
1151
1239
  if (shouldRebuild(filePath)) {
@@ -1177,6 +1265,7 @@ function createFileWatcher(srcDir, onRebuild) {
1177
1265
  }
1178
1266
  var init_watcher = __esm({
1179
1267
  "../emulator/dist/bundler/watcher.js"() {
1268
+ init_logger();
1180
1269
  }
1181
1270
  });
1182
1271
 
@@ -1249,6 +1338,255 @@ var init_workerd_config = __esm({
1249
1338
  "../emulator/dist/config/workerd-config.js"() {
1250
1339
  }
1251
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
+ });
1252
1590
  function findDashboardDistPath() {
1253
1591
  const possiblePaths = [
1254
1592
  join(__dirname, "dashboard-dist"),
@@ -1276,9 +1614,174 @@ function createDashboardRoutes(app, dbManager2, config) {
1276
1614
  return c.json({
1277
1615
  db: config.db,
1278
1616
  queue: config.queue,
1279
- workflow: config.workflow
1617
+ workflow: config.workflow,
1618
+ auth: config.auth
1280
1619
  });
1281
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
+ }
1282
1785
  app.post("/api/db/:binding/query", async (c) => {
1283
1786
  const binding = c.req.param("binding");
1284
1787
  const resourceName = getDbResourceName(binding);
@@ -1501,7 +2004,7 @@ function createDashboardRoutes(app, dbManager2, config) {
1501
2004
  }
1502
2005
  try {
1503
2006
  const db = dbManager2.emulatorDb;
1504
- 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
1505
2008
  FROM workflow_executions
1506
2009
  WHERE id = ?`).get(executionId);
1507
2010
  if (!execution) {
@@ -1515,6 +2018,8 @@ function createDashboardRoutes(app, dbManager2, config) {
1515
2018
  execution: {
1516
2019
  id: execution.id,
1517
2020
  status: execution.status.toUpperCase(),
2021
+ input: execution.input ? JSON.parse(execution.input) : null,
2022
+ output: execution.output ? JSON.parse(execution.output) : null,
1518
2023
  startedAt: execution.started_at ? new Date(execution.started_at * 1e3).toISOString() : null,
1519
2024
  completedAt: execution.completed_at ? new Date(execution.completed_at * 1e3).toISOString() : null,
1520
2025
  durationMs: execution.started_at && execution.completed_at ? (execution.completed_at - execution.started_at) * 1e3 : null,
@@ -1593,8 +2098,12 @@ function createDbHandler(getDatabase) {
1593
2098
  const startTime = Date.now();
1594
2099
  try {
1595
2100
  const body = await c.req.json();
1596
- const { bindingName, method, query, params, statements } = body;
1597
- const db = getDatabase(bindingName);
2101
+ const { bindingName, databaseId, method, query, params, statements } = body;
2102
+ const dbName = bindingName ?? databaseId;
2103
+ if (!dbName) {
2104
+ return c.json({ error: "Missing bindingName or databaseId" }, 400);
2105
+ }
2106
+ const db = getDatabase(dbName);
1598
2107
  if (method === "prepare" && query) {
1599
2108
  const stmt = db.prepare(query);
1600
2109
  const isSelect = query.trim().toUpperCase().startsWith("SELECT");
@@ -1759,8 +2268,10 @@ function createQueueHandlers(db) {
1759
2268
  try {
1760
2269
  const body = await c.req.json();
1761
2270
  const { messageId, deliveryId } = body;
1762
- const result = db.prepare(`DELETE FROM queue_messages
1763
- WHERE id = ? AND delivery_id = ?`).run(messageId, deliveryId);
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);
1764
2275
  if (result.changes === 0) {
1765
2276
  return c.json({ success: false, error: "Message not found or already processed" }, 404);
1766
2277
  }
@@ -1835,7 +2346,7 @@ function createQueueProcessor(db, queueBindings, workerUrl) {
1835
2346
  body: row.payload
1836
2347
  });
1837
2348
  if (response.ok) {
1838
- db.prepare(`DELETE FROM queue_messages WHERE id = ?`).run(row.id);
2349
+ db.prepare(`UPDATE queue_messages SET status = 'acknowledged', updated_at = ? WHERE id = ?`).run(now, row.id);
1839
2350
  } else {
1840
2351
  db.prepare(`UPDATE queue_messages
1841
2352
  SET status = 'pending', delivery_id = NULL, visible_at = ?, updated_at = ?
@@ -2072,6 +2583,13 @@ async function startMockServer(dbManager2, config, options = {}) {
2072
2583
  app.post("/workflow/complete", workflowHandlers.completeHandler);
2073
2584
  app.post("/workflow/fail", workflowHandlers.failHandler);
2074
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
+ }
2075
2593
  app.get("/health", (c) => c.json({ status: "ok" }));
2076
2594
  if (options.dashboardEnabled !== false) {
2077
2595
  createDashboardRoutes(app, dbManager2, config);
@@ -2094,6 +2612,7 @@ async function startMockServer(dbManager2, config, options = {}) {
2094
2612
  var DEFAULT_MOCK_SERVER_PORT;
2095
2613
  var init_mock_server = __esm({
2096
2614
  "../emulator/dist/services/mock-server.js"() {
2615
+ init_auth_service();
2097
2616
  init_dashboard_routes();
2098
2617
  init_db_service();
2099
2618
  init_queue_service();
@@ -2101,39 +2620,6 @@ var init_mock_server = __esm({
2101
2620
  DEFAULT_MOCK_SERVER_PORT = 4003;
2102
2621
  }
2103
2622
  });
2104
-
2105
- // ../emulator/dist/utils/logger.js
2106
- function timestamp() {
2107
- return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
2108
- }
2109
- function log(message) {
2110
- console.log(`${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.cyan}[ploy]${COLORS.reset} ${message}`);
2111
- }
2112
- function success(message) {
2113
- console.log(`${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.green}[ploy]${COLORS.reset} ${message}`);
2114
- }
2115
- function error(message) {
2116
- console.error(`${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.red}[ploy]${COLORS.reset} ${message}`);
2117
- }
2118
- function debug(message, verbose) {
2119
- if (verbose) {
2120
- console.log(`${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.magenta}[ploy:debug]${COLORS.reset} ${message}`);
2121
- }
2122
- }
2123
- var COLORS;
2124
- var init_logger = __esm({
2125
- "../emulator/dist/utils/logger.js"() {
2126
- COLORS = {
2127
- reset: "\x1B[0m",
2128
- dim: "\x1B[2m",
2129
- cyan: "\x1B[36m",
2130
- green: "\x1B[32m",
2131
- yellow: "\x1B[33m",
2132
- red: "\x1B[31m",
2133
- magenta: "\x1B[35m"
2134
- };
2135
- }
2136
- });
2137
2623
  function getProjectHash(projectDir) {
2138
2624
  return createHash("sha256").update(projectDir).digest("hex").slice(0, 12);
2139
2625
  }
@@ -2243,6 +2729,49 @@ CREATE TABLE IF NOT EXISTS workflow_steps (
2243
2729
 
2244
2730
  CREATE INDEX IF NOT EXISTS idx_workflow_steps_execution
2245
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);
2246
2775
  `;
2247
2776
  }
2248
2777
  });
@@ -2597,6 +3126,38 @@ function createDevD1(databaseId, apiUrl) {
2597
3126
  }
2598
3127
  };
2599
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
+ }
2600
3161
  async function initPloyForDev(config) {
2601
3162
  if (process.env.NODE_ENV !== "development") {
2602
3163
  return;
@@ -2605,6 +3166,56 @@ async function initPloyForDev(config) {
2605
3166
  return;
2606
3167
  }
2607
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
+ }
2608
3219
  const configPath = config?.configPath || "./ploy.yaml";
2609
3220
  const projectDir = process.cwd();
2610
3221
  let ployConfig;
@@ -2620,7 +3231,9 @@ async function initPloyForDev(config) {
2620
3231
  if (config?.bindings?.db) {
2621
3232
  ployConfig = { ...ployConfig, db: config.bindings.db };
2622
3233
  }
2623
- if (!ployConfig.db || Object.keys(ployConfig.db).length === 0) {
3234
+ const hasDbBindings = ployConfig.db && Object.keys(ployConfig.db).length > 0;
3235
+ const hasAuthConfig = !!ployConfig.auth;
3236
+ if (!hasDbBindings && !hasAuthConfig) {
2624
3237
  return;
2625
3238
  }
2626
3239
  ensureDataDir(projectDir);
@@ -2628,8 +3241,14 @@ async function initPloyForDev(config) {
2628
3241
  mockServer = await startMockServer(dbManager, ployConfig, {});
2629
3242
  const apiUrl = `http://localhost:${mockServer.port}`;
2630
3243
  const env = {};
2631
- for (const [bindingName, databaseId] of Object.entries(ployConfig.db)) {
2632
- env[bindingName] = createDevD1(databaseId, apiUrl);
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`;
2633
3252
  }
2634
3253
  const context = {
2635
3254
  env,
@@ -2644,7 +3263,14 @@ async function initPloyForDev(config) {
2644
3263
  configurable: true
2645
3264
  });
2646
3265
  const bindingNames = Object.keys(env);
2647
- console.log(`[Ploy] Development context initialized with bindings: ${bindingNames.join(", ")}`);
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(", ")}`);
2648
3274
  console.log(`[Ploy] Mock server running at ${apiUrl}`);
2649
3275
  const cleanup = async () => {
2650
3276
  if (mockServer) {
@@ -4438,7 +5064,9 @@ async function startNextJsDev(options) {
4438
5064
  env: {
4439
5065
  ...process.env,
4440
5066
  // Set environment variable so initPloyForDev knows the mock server URL
4441
- 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)
4442
5070
  }
4443
5071
  });
4444
5072
  } else {
@@ -4448,7 +5076,8 @@ async function startNextJsDev(options) {
4448
5076
  shell: true,
4449
5077
  env: {
4450
5078
  ...process.env,
4451
- PLOY_MOCK_SERVER_URL: `http://localhost:${dashboard.port}`
5079
+ PLOY_MOCK_SERVER_URL: `http://localhost:${dashboard.port}`,
5080
+ PORT: String(nextPort)
4452
5081
  }
4453
5082
  });
4454
5083
  }