@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/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 result = db.prepare(`DELETE FROM queue_messages
1834
- 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);
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(`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);
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
- 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) {
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
- for (const [bindingName, databaseId] of Object.entries(ployConfig.db)) {
2670
- 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`;
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
- 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(", ")}`);
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
  }