@meetploy/cli 1.12.1 → 1.13.0

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/dev.js CHANGED
@@ -9,14 +9,15 @@ import { promisify } from 'util';
9
9
  import { parse } from 'yaml';
10
10
  import { serve } from '@hono/node-server';
11
11
  import { Hono } from 'hono';
12
- import { randomUUID } from 'crypto';
12
+ import { randomUUID, createHmac, pbkdf2Sync, timingSafeEqual, randomBytes } from 'crypto';
13
+ import { getCookie, deleteCookie, setCookie } from 'hono/cookie';
13
14
  import 'os';
14
15
  import Database from 'better-sqlite3';
15
16
 
16
17
  createRequire(import.meta.url);
17
18
  promisify(readFile);
18
19
  function readPloyConfigSync(projectDir, configPath) {
19
- const configFile = configPath;
20
+ const configFile = configPath || "ploy.yaml";
20
21
  const fullPath = join(projectDir, configFile);
21
22
  if (!existsSync(fullPath)) {
22
23
  throw new Error(`Config file not found: ${fullPath}`);
@@ -29,13 +30,287 @@ function readPloyConfigSync(projectDir, configPath) {
29
30
  function readPloyConfig(projectDir, configPath) {
30
31
  const config = readPloyConfigSync(projectDir, configPath);
31
32
  if (!config.kind) {
32
- throw new Error(`Missing required field 'kind' in ${configPath}`);
33
+ throw new Error(`Missing required field 'kind' in ${configPath || "ploy.yaml"}`);
33
34
  }
34
35
  if (config.kind !== "dynamic" && config.kind !== "worker") {
35
- throw new Error(`Invalid kind '${config.kind}' in ${configPath}. Must be 'dynamic' or 'worker'`);
36
+ throw new Error(`Invalid kind '${config.kind}' in ${configPath || "ploy.yaml"}. Must be 'dynamic' or 'worker'`);
36
37
  }
37
38
  return config;
38
39
  }
40
+ function generateId() {
41
+ return randomBytes(16).toString("hex");
42
+ }
43
+ function hashPassword(password) {
44
+ const salt = randomBytes(32).toString("hex");
45
+ const hash = pbkdf2Sync(password, salt, 1e5, 64, "sha512").toString("hex");
46
+ return `${salt}:${hash}`;
47
+ }
48
+ function verifyPassword(password, storedHash) {
49
+ const [salt, hash] = storedHash.split(":");
50
+ const derivedHash = pbkdf2Sync(password, salt, 1e5, 64, "sha512").toString("hex");
51
+ return timingSafeEqual(Buffer.from(hash, "hex"), Buffer.from(derivedHash, "hex"));
52
+ }
53
+ function hashToken(token) {
54
+ return createHmac("sha256", "emulator-secret").update(token).digest("hex");
55
+ }
56
+ var JWT_SECRET = "ploy-emulator-dev-secret";
57
+ var SESSION_TOKEN_EXPIRY = 7 * 24 * 60 * 60;
58
+ function base64UrlEncode(str) {
59
+ return Buffer.from(str).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
60
+ }
61
+ function base64UrlDecode(str) {
62
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
63
+ while (base64.length % 4) {
64
+ base64 += "=";
65
+ }
66
+ return Buffer.from(base64, "base64").toString();
67
+ }
68
+ function createJWT(payload) {
69
+ const header = { alg: "HS256", typ: "JWT" };
70
+ const headerB64 = base64UrlEncode(JSON.stringify(header));
71
+ const payloadB64 = base64UrlEncode(JSON.stringify(payload));
72
+ const signature = createHmac("sha256", JWT_SECRET).update(`${headerB64}.${payloadB64}`).digest("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
73
+ return `${headerB64}.${payloadB64}.${signature}`;
74
+ }
75
+ function verifyJWT(token) {
76
+ try {
77
+ const parts = token.split(".");
78
+ if (parts.length !== 3) {
79
+ return null;
80
+ }
81
+ const [headerB64, payloadB64, signature] = parts;
82
+ const expectedSig = createHmac("sha256", JWT_SECRET).update(`${headerB64}.${payloadB64}`).digest("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
83
+ if (signature !== expectedSig) {
84
+ return null;
85
+ }
86
+ const payload = JSON.parse(base64UrlDecode(payloadB64));
87
+ if (payload.exp < Math.floor(Date.now() / 1e3)) {
88
+ return null;
89
+ }
90
+ return payload;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+ function createSessionToken(userId, email) {
96
+ const now = Math.floor(Date.now() / 1e3);
97
+ const sessionId = generateId();
98
+ const token = createJWT({
99
+ sub: userId,
100
+ email,
101
+ iat: now,
102
+ exp: now + SESSION_TOKEN_EXPIRY,
103
+ jti: sessionId
104
+ });
105
+ return {
106
+ token,
107
+ sessionId,
108
+ expiresAt: new Date((now + SESSION_TOKEN_EXPIRY) * 1e3)
109
+ };
110
+ }
111
+ function validateEmail(email) {
112
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
113
+ if (!emailRegex.test(email)) {
114
+ return "Invalid email format";
115
+ }
116
+ return null;
117
+ }
118
+ function validatePassword(password) {
119
+ if (password.length < 8) {
120
+ return "Password must be at least 8 characters";
121
+ }
122
+ return null;
123
+ }
124
+ function setSessionCookie(c, sessionToken) {
125
+ setCookie(c, "ploy_session", sessionToken, {
126
+ httpOnly: true,
127
+ secure: false,
128
+ sameSite: "Lax",
129
+ path: "/",
130
+ maxAge: SESSION_TOKEN_EXPIRY
131
+ });
132
+ }
133
+ function clearSessionCookie(c) {
134
+ deleteCookie(c, "ploy_session", { path: "/" });
135
+ }
136
+ function createAuthHandlers(db) {
137
+ const signupHandler = async (c) => {
138
+ try {
139
+ const body = await c.req.json();
140
+ const { email, password, metadata } = body;
141
+ const emailError = validateEmail(email);
142
+ if (emailError) {
143
+ return c.json({ error: emailError }, 400);
144
+ }
145
+ const passwordError = validatePassword(password);
146
+ if (passwordError) {
147
+ return c.json({ error: passwordError }, 400);
148
+ }
149
+ const existingUser = db.prepare("SELECT id FROM auth_users WHERE email = ?").get(email.toLowerCase());
150
+ if (existingUser) {
151
+ return c.json({ error: "User already exists" }, 409);
152
+ }
153
+ const userId = generateId();
154
+ const passwordHash = hashPassword(password);
155
+ const now = (/* @__PURE__ */ new Date()).toISOString();
156
+ db.prepare(`INSERT INTO auth_users (id, email, password_hash, created_at, updated_at, metadata)
157
+ VALUES (?, ?, ?, ?, ?, ?)`).run(userId, email.toLowerCase(), passwordHash, now, now, metadata ? JSON.stringify(metadata) : null);
158
+ const { token: sessionToken, sessionId, expiresAt } = createSessionToken(userId, email.toLowerCase());
159
+ const sessionTokenHash = hashToken(sessionToken);
160
+ db.prepare(`INSERT INTO auth_sessions (id, user_id, token_hash, expires_at, created_at)
161
+ VALUES (?, ?, ?, ?, ?)`).run(sessionId, userId, sessionTokenHash, expiresAt.toISOString(), now);
162
+ setSessionCookie(c, sessionToken);
163
+ return c.json({
164
+ user: {
165
+ id: userId,
166
+ email: email.toLowerCase(),
167
+ emailVerified: false,
168
+ createdAt: now,
169
+ metadata: metadata ?? null
170
+ }
171
+ });
172
+ } catch (err) {
173
+ const message = err instanceof Error ? err.message : String(err);
174
+ return c.json({ error: message }, 500);
175
+ }
176
+ };
177
+ const signinHandler = async (c) => {
178
+ try {
179
+ const body = await c.req.json();
180
+ const { email, password } = body;
181
+ const user = db.prepare("SELECT * FROM auth_users WHERE email = ?").get(email.toLowerCase());
182
+ if (!user) {
183
+ return c.json({ error: "Invalid credentials" }, 401);
184
+ }
185
+ if (!verifyPassword(password, user.password_hash)) {
186
+ return c.json({ error: "Invalid credentials" }, 401);
187
+ }
188
+ const { token: sessionToken, sessionId, expiresAt } = createSessionToken(user.id, user.email);
189
+ const sessionTokenHash = hashToken(sessionToken);
190
+ const now = (/* @__PURE__ */ new Date()).toISOString();
191
+ db.prepare(`INSERT INTO auth_sessions (id, user_id, token_hash, expires_at, created_at)
192
+ VALUES (?, ?, ?, ?, ?)`).run(sessionId, user.id, sessionTokenHash, expiresAt.toISOString(), now);
193
+ let metadata = null;
194
+ if (user.metadata) {
195
+ try {
196
+ metadata = JSON.parse(user.metadata);
197
+ } catch {
198
+ metadata = null;
199
+ }
200
+ }
201
+ setSessionCookie(c, sessionToken);
202
+ return c.json({
203
+ user: {
204
+ id: user.id,
205
+ email: user.email,
206
+ emailVerified: user.email_verified === 1,
207
+ createdAt: user.created_at,
208
+ metadata
209
+ }
210
+ });
211
+ } catch (err) {
212
+ const message = err instanceof Error ? err.message : String(err);
213
+ return c.json({ error: message }, 500);
214
+ }
215
+ };
216
+ const meHandler = async (c) => {
217
+ try {
218
+ const cookieToken = getCookie(c, "ploy_session");
219
+ const authHeader = c.req.header("Authorization");
220
+ let token;
221
+ if (cookieToken) {
222
+ token = cookieToken;
223
+ } else if (authHeader && authHeader.startsWith("Bearer ")) {
224
+ token = authHeader.slice(7);
225
+ }
226
+ if (!token) {
227
+ return c.json({ error: "Missing authentication" }, 401);
228
+ }
229
+ const payload = verifyJWT(token);
230
+ if (!payload) {
231
+ return c.json({ error: "Invalid or expired session" }, 401);
232
+ }
233
+ const user = db.prepare("SELECT id, email, email_verified, created_at, updated_at, metadata FROM auth_users WHERE id = ?").get(payload.sub);
234
+ if (!user) {
235
+ return c.json({ error: "User not found" }, 401);
236
+ }
237
+ let metadata = null;
238
+ if (user.metadata) {
239
+ try {
240
+ metadata = JSON.parse(user.metadata);
241
+ } catch {
242
+ metadata = null;
243
+ }
244
+ }
245
+ return c.json({
246
+ user: {
247
+ id: user.id,
248
+ email: user.email,
249
+ emailVerified: user.email_verified === 1,
250
+ createdAt: user.created_at,
251
+ updatedAt: user.updated_at,
252
+ metadata
253
+ }
254
+ });
255
+ } catch (err) {
256
+ const message = err instanceof Error ? err.message : String(err);
257
+ return c.json({ error: message }, 500);
258
+ }
259
+ };
260
+ const signoutHandler = async (c) => {
261
+ try {
262
+ const sessionToken = getCookie(c, "ploy_session");
263
+ if (sessionToken) {
264
+ const payload = verifyJWT(sessionToken);
265
+ if (payload) {
266
+ const tokenHash = hashToken(sessionToken);
267
+ db.prepare("UPDATE auth_sessions SET revoked = 1 WHERE token_hash = ?").run(tokenHash);
268
+ }
269
+ }
270
+ clearSessionCookie(c);
271
+ return c.json({ success: true });
272
+ } catch (err) {
273
+ const message = err instanceof Error ? err.message : String(err);
274
+ return c.json({ error: message }, 500);
275
+ }
276
+ };
277
+ return {
278
+ signupHandler,
279
+ signinHandler,
280
+ meHandler,
281
+ signoutHandler
282
+ };
283
+ }
284
+
285
+ // ../emulator/dist/services/cache-service.js
286
+ function createCacheHandlers(db) {
287
+ const getHandler = async (c) => {
288
+ const body = await c.req.json();
289
+ const { cacheName, key } = body;
290
+ const now = Math.floor(Date.now() / 1e3);
291
+ const row = db.prepare(`SELECT value FROM cache_entries WHERE cache_name = ? AND key = ? AND expires_at > ?`).get(cacheName, key, now);
292
+ return c.json({ value: row?.value ?? null });
293
+ };
294
+ const setHandler = async (c) => {
295
+ const body = await c.req.json();
296
+ const { cacheName, key, value, ttl } = body;
297
+ const now = Math.floor(Date.now() / 1e3);
298
+ const expiresAt = now + ttl;
299
+ db.prepare(`INSERT OR REPLACE INTO cache_entries (cache_name, key, value, expires_at) VALUES (?, ?, ?, ?)`).run(cacheName, key, value, expiresAt);
300
+ return c.json({ success: true });
301
+ };
302
+ const deleteHandler = async (c) => {
303
+ const body = await c.req.json();
304
+ const { cacheName, key } = body;
305
+ db.prepare(`DELETE FROM cache_entries WHERE cache_name = ? AND key = ?`).run(cacheName, key);
306
+ return c.json({ success: true });
307
+ };
308
+ return {
309
+ getHandler,
310
+ setHandler,
311
+ deleteHandler
312
+ };
313
+ }
39
314
  var __filename = fileURLToPath(import.meta.url);
40
315
  var __dirname = dirname(__filename);
41
316
  function findDashboardDistPath() {
@@ -77,9 +352,175 @@ function createDashboardRoutes(app, dbManager2, config) {
77
352
  return c.json({
78
353
  db: config.db,
79
354
  queue: config.queue,
80
- workflow: config.workflow
355
+ cache: config.cache,
356
+ workflow: config.workflow,
357
+ auth: config.auth
81
358
  });
82
359
  });
360
+ if (config.auth) {
361
+ app.get("/api/auth/tables", (c) => {
362
+ try {
363
+ const db = dbManager2.emulatorDb;
364
+ const tables = db.prepare(`SELECT name FROM sqlite_master
365
+ WHERE type='table' AND (name = 'auth_users' OR name = 'auth_sessions')
366
+ ORDER BY name`).all();
367
+ return c.json({ tables });
368
+ } catch (err) {
369
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
370
+ }
371
+ });
372
+ app.get("/api/auth/tables/:tableName", (c) => {
373
+ const tableName = c.req.param("tableName");
374
+ if (tableName !== "auth_users" && tableName !== "auth_sessions") {
375
+ return c.json({ error: "Table not found" }, 404);
376
+ }
377
+ const limit = parseInt(c.req.query("limit") || "50", 10);
378
+ const offset = parseInt(c.req.query("offset") || "0", 10);
379
+ try {
380
+ const db = dbManager2.emulatorDb;
381
+ const columnsResult = db.prepare(`PRAGMA table_info("${tableName}")`).all();
382
+ const columns = columnsResult.map((col) => col.name);
383
+ const countResult = db.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`).get();
384
+ const total = countResult.count;
385
+ let data;
386
+ if (tableName === "auth_users") {
387
+ data = db.prepare(`SELECT id, email, email_verified, created_at, updated_at, metadata FROM "${tableName}" LIMIT ? OFFSET ?`).all(limit, offset);
388
+ } else {
389
+ data = db.prepare(`SELECT * FROM "${tableName}" LIMIT ? OFFSET ?`).all(limit, offset);
390
+ }
391
+ const visibleColumns = tableName === "auth_users" ? columns.filter((c2) => c2 !== "password_hash") : columns;
392
+ return c.json({ data, columns: visibleColumns, total });
393
+ } catch (err) {
394
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
395
+ }
396
+ });
397
+ app.get("/api/auth/schema", (c) => {
398
+ try {
399
+ const db = dbManager2.emulatorDb;
400
+ const tables = ["auth_users", "auth_sessions"].map((tableName) => {
401
+ const columnsResult = db.prepare(`PRAGMA table_info("${tableName}")`).all();
402
+ const visibleColumns = tableName === "auth_users" ? columnsResult.filter((col) => col.name !== "password_hash") : columnsResult;
403
+ return {
404
+ name: tableName,
405
+ columns: visibleColumns.map((col) => ({
406
+ name: col.name,
407
+ type: col.type,
408
+ notNull: col.notnull === 1,
409
+ primaryKey: col.pk === 1
410
+ }))
411
+ };
412
+ });
413
+ return c.json({ tables });
414
+ } catch (err) {
415
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
416
+ }
417
+ });
418
+ app.post("/api/auth/query", async (c) => {
419
+ const body = await c.req.json();
420
+ const { query } = body;
421
+ if (!query) {
422
+ return c.json({ error: "Query is required" }, 400);
423
+ }
424
+ const normalizedQuery = query.trim().toUpperCase();
425
+ if (!normalizedQuery.startsWith("SELECT")) {
426
+ return c.json({ error: "Only SELECT queries are allowed on auth tables" }, 400);
427
+ }
428
+ const allowedTables = ["auth_users", "auth_sessions"];
429
+ const hasDisallowedTable = !allowedTables.some((table) => query.toLowerCase().includes(`from ${table}`) || query.toLowerCase().includes(`join ${table}`));
430
+ if (hasDisallowedTable) {
431
+ return c.json({
432
+ error: "Query must reference auth tables (auth_users or auth_sessions)"
433
+ }, 400);
434
+ }
435
+ try {
436
+ const db = dbManager2.emulatorDb;
437
+ const startTime = Date.now();
438
+ const stmt = db.prepare(query);
439
+ const results = stmt.all();
440
+ const sanitizedResults = results.map((row) => {
441
+ const { password_hash: _, ...rest } = row;
442
+ return rest;
443
+ });
444
+ const duration = Date.now() - startTime;
445
+ return c.json({
446
+ results: sanitizedResults,
447
+ success: true,
448
+ meta: {
449
+ duration,
450
+ rows_read: results.length,
451
+ rows_written: 0
452
+ }
453
+ });
454
+ } catch (err) {
455
+ return c.json({
456
+ results: [],
457
+ success: false,
458
+ error: err instanceof Error ? err.message : String(err),
459
+ meta: { duration: 0, rows_read: 0, rows_written: 0 }
460
+ }, 400);
461
+ }
462
+ });
463
+ app.get("/api/auth/settings", (c) => {
464
+ try {
465
+ const db = dbManager2.emulatorDb;
466
+ const settings = db.prepare("SELECT * FROM auth_settings WHERE id = 1").get();
467
+ if (!settings) {
468
+ return c.json({
469
+ sessionTokenExpiry: 604800,
470
+ allowSignups: true,
471
+ requireEmailVerification: false,
472
+ requireName: false
473
+ });
474
+ }
475
+ return c.json({
476
+ sessionTokenExpiry: settings.session_token_expiry,
477
+ allowSignups: settings.allow_signups === 1,
478
+ requireEmailVerification: settings.require_email_verification === 1,
479
+ requireName: settings.require_name === 1
480
+ });
481
+ } catch (err) {
482
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
483
+ }
484
+ });
485
+ app.patch("/api/auth/settings", async (c) => {
486
+ try {
487
+ const body = await c.req.json();
488
+ const db = dbManager2.emulatorDb;
489
+ const updates = [];
490
+ const values = [];
491
+ if (body.sessionTokenExpiry !== void 0) {
492
+ updates.push("session_token_expiry = ?");
493
+ values.push(body.sessionTokenExpiry);
494
+ }
495
+ if (body.allowSignups !== void 0) {
496
+ updates.push("allow_signups = ?");
497
+ values.push(body.allowSignups ? 1 : 0);
498
+ }
499
+ if (body.requireEmailVerification !== void 0) {
500
+ updates.push("require_email_verification = ?");
501
+ values.push(body.requireEmailVerification ? 1 : 0);
502
+ }
503
+ if (body.requireName !== void 0) {
504
+ updates.push("require_name = ?");
505
+ values.push(body.requireName ? 1 : 0);
506
+ }
507
+ if (updates.length > 0) {
508
+ updates.push("updated_at = strftime('%s', 'now')");
509
+ const sql = `UPDATE auth_settings SET ${updates.join(", ")} WHERE id = 1`;
510
+ db.prepare(sql).run(...values);
511
+ }
512
+ const settings = db.prepare("SELECT * FROM auth_settings WHERE id = 1").get();
513
+ return c.json({
514
+ sessionTokenExpiry: settings.session_token_expiry,
515
+ allowSignups: settings.allow_signups === 1,
516
+ requireEmailVerification: settings.require_email_verification === 1,
517
+ requireName: settings.require_name === 1
518
+ });
519
+ } catch (err) {
520
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
521
+ }
522
+ });
523
+ }
83
524
  app.post("/api/db/:binding/query", async (c) => {
84
525
  const binding = c.req.param("binding");
85
526
  const resourceName = getDbResourceName(binding);
@@ -252,6 +693,39 @@ function createDashboardRoutes(app, dbManager2, config) {
252
693
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
253
694
  }
254
695
  });
696
+ app.get("/api/cache/:binding/entries", (c) => {
697
+ const binding = c.req.param("binding");
698
+ const cacheName = config.cache?.[binding];
699
+ const limit = parseInt(c.req.query("limit") || "20", 10);
700
+ const offset = parseInt(c.req.query("offset") || "0", 10);
701
+ const now = Math.floor(Date.now() / 1e3);
702
+ if (!cacheName) {
703
+ return c.json({ error: "Cache binding not found" }, 404);
704
+ }
705
+ try {
706
+ const db = dbManager2.emulatorDb;
707
+ const total = db.prepare(`SELECT COUNT(*) as count FROM cache_entries
708
+ WHERE cache_name = ? AND expires_at > ?`).get(cacheName, now).count;
709
+ const entries = db.prepare(`SELECT key, value, expires_at
710
+ FROM cache_entries
711
+ WHERE cache_name = ? AND expires_at > ?
712
+ ORDER BY key ASC
713
+ LIMIT ? OFFSET ?`).all(cacheName, now, limit, offset);
714
+ return c.json({
715
+ entries: entries.map((e) => ({
716
+ key: e.key,
717
+ value: e.value,
718
+ ttlSeconds: Math.max(0, e.expires_at - now),
719
+ expiresAt: new Date(e.expires_at * 1e3).toISOString()
720
+ })),
721
+ total,
722
+ limit,
723
+ offset
724
+ });
725
+ } catch (err) {
726
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
727
+ }
728
+ });
255
729
  app.get("/api/workflow/:binding/executions", (c) => {
256
730
  const binding = c.req.param("binding");
257
731
  const workflowConfig = config.workflow?.[binding];
@@ -302,7 +776,7 @@ function createDashboardRoutes(app, dbManager2, config) {
302
776
  }
303
777
  try {
304
778
  const db = dbManager2.emulatorDb;
305
- const execution = db.prepare(`SELECT id, workflow_name, status, error, started_at, completed_at, created_at
779
+ const execution = db.prepare(`SELECT id, workflow_name, status, input, output, error, started_at, completed_at, created_at
306
780
  FROM workflow_executions
307
781
  WHERE id = ?`).get(executionId);
308
782
  if (!execution) {
@@ -316,6 +790,8 @@ function createDashboardRoutes(app, dbManager2, config) {
316
790
  execution: {
317
791
  id: execution.id,
318
792
  status: execution.status.toUpperCase(),
793
+ input: execution.input ? JSON.parse(execution.input) : null,
794
+ output: execution.output ? JSON.parse(execution.output) : null,
319
795
  startedAt: execution.started_at ? new Date(execution.started_at * 1e3).toISOString() : null,
320
796
  completedAt: execution.completed_at ? new Date(execution.completed_at * 1e3).toISOString() : null,
321
797
  durationMs: execution.started_at && execution.completed_at ? (execution.completed_at - execution.started_at) * 1e3 : null,
@@ -541,8 +1017,10 @@ function createQueueHandlers(db) {
541
1017
  try {
542
1018
  const body = await c.req.json();
543
1019
  const { messageId, deliveryId } = body;
544
- const result = db.prepare(`DELETE FROM queue_messages
545
- WHERE id = ? AND delivery_id = ?`).run(messageId, deliveryId);
1020
+ const now = Math.floor(Date.now() / 1e3);
1021
+ const result = db.prepare(`UPDATE queue_messages
1022
+ SET status = 'acknowledged', updated_at = ?
1023
+ WHERE id = ? AND delivery_id = ?`).run(now, messageId, deliveryId);
546
1024
  if (result.changes === 0) {
547
1025
  return c.json({ success: false, error: "Message not found or already processed" }, 404);
548
1026
  }
@@ -779,6 +1257,19 @@ async function startMockServer(dbManager2, config, options = {}) {
779
1257
  app.post("/workflow/complete", workflowHandlers.completeHandler);
780
1258
  app.post("/workflow/fail", workflowHandlers.failHandler);
781
1259
  }
1260
+ if (config.cache) {
1261
+ const cacheHandlers = createCacheHandlers(dbManager2.emulatorDb);
1262
+ app.post("/cache/get", cacheHandlers.getHandler);
1263
+ app.post("/cache/set", cacheHandlers.setHandler);
1264
+ app.post("/cache/delete", cacheHandlers.deleteHandler);
1265
+ }
1266
+ if (config.auth) {
1267
+ const authHandlers = createAuthHandlers(dbManager2.emulatorDb);
1268
+ app.post("/auth/signup", authHandlers.signupHandler);
1269
+ app.post("/auth/signin", authHandlers.signinHandler);
1270
+ app.get("/auth/me", authHandlers.meHandler);
1271
+ app.post("/auth/signout", authHandlers.signoutHandler);
1272
+ }
782
1273
  app.get("/health", (c) => c.json({ status: "ok" }));
783
1274
  if (options.dashboardEnabled !== false) {
784
1275
  createDashboardRoutes(app, dbManager2, config);
@@ -859,6 +1350,58 @@ CREATE TABLE IF NOT EXISTS workflow_steps (
859
1350
 
860
1351
  CREATE INDEX IF NOT EXISTS idx_workflow_steps_execution
861
1352
  ON workflow_steps(execution_id, step_index);
1353
+
1354
+ -- Auth users table
1355
+ CREATE TABLE IF NOT EXISTS auth_users (
1356
+ id TEXT PRIMARY KEY,
1357
+ email TEXT UNIQUE NOT NULL,
1358
+ email_verified INTEGER NOT NULL DEFAULT 0,
1359
+ password_hash TEXT NOT NULL,
1360
+ created_at TEXT NOT NULL,
1361
+ updated_at TEXT NOT NULL,
1362
+ metadata TEXT
1363
+ );
1364
+
1365
+ CREATE INDEX IF NOT EXISTS idx_auth_users_email
1366
+ ON auth_users(email);
1367
+
1368
+ -- Auth sessions table
1369
+ CREATE TABLE IF NOT EXISTS auth_sessions (
1370
+ id TEXT PRIMARY KEY,
1371
+ user_id TEXT NOT NULL,
1372
+ token_hash TEXT UNIQUE NOT NULL,
1373
+ expires_at TEXT NOT NULL,
1374
+ created_at TEXT NOT NULL,
1375
+ revoked INTEGER NOT NULL DEFAULT 0,
1376
+ FOREIGN KEY (user_id) REFERENCES auth_users(id) ON DELETE CASCADE
1377
+ );
1378
+
1379
+ CREATE INDEX IF NOT EXISTS idx_auth_sessions_user
1380
+ ON auth_sessions(user_id);
1381
+ CREATE INDEX IF NOT EXISTS idx_auth_sessions_hash
1382
+ ON auth_sessions(token_hash);
1383
+
1384
+ -- Auth settings table
1385
+ CREATE TABLE IF NOT EXISTS auth_settings (
1386
+ id INTEGER PRIMARY KEY CHECK (id = 1),
1387
+ session_token_expiry INTEGER NOT NULL DEFAULT 604800,
1388
+ allow_signups INTEGER NOT NULL DEFAULT 1,
1389
+ require_email_verification INTEGER NOT NULL DEFAULT 0,
1390
+ require_name INTEGER NOT NULL DEFAULT 0,
1391
+ updated_at INTEGER DEFAULT (strftime('%s', 'now'))
1392
+ );
1393
+
1394
+ -- Insert default settings if not exists
1395
+ INSERT OR IGNORE INTO auth_settings (id) VALUES (1);
1396
+
1397
+ -- Cache entries table
1398
+ CREATE TABLE IF NOT EXISTS cache_entries (
1399
+ cache_name TEXT NOT NULL,
1400
+ key TEXT NOT NULL,
1401
+ value TEXT NOT NULL,
1402
+ expires_at INTEGER NOT NULL,
1403
+ PRIMARY KEY (cache_name, key)
1404
+ );
862
1405
  `;
863
1406
  function initializeDatabases(projectDir) {
864
1407
  const dataDir = ensureDataDir(projectDir);
@@ -991,6 +1534,38 @@ function createDevD1(databaseId, apiUrl) {
991
1534
  }
992
1535
  };
993
1536
  }
1537
+ function createDevPloyAuth(apiUrl) {
1538
+ return {
1539
+ async getUser(token) {
1540
+ try {
1541
+ const response = await fetch(`${apiUrl}/auth/me`, {
1542
+ headers: {
1543
+ Authorization: `Bearer ${token}`
1544
+ }
1545
+ });
1546
+ if (!response.ok) {
1547
+ return null;
1548
+ }
1549
+ const data = await response.json();
1550
+ return data.user;
1551
+ } catch {
1552
+ return null;
1553
+ }
1554
+ },
1555
+ async verifyToken(token) {
1556
+ try {
1557
+ const response = await fetch(`${apiUrl}/auth/me`, {
1558
+ headers: {
1559
+ Authorization: `Bearer ${token}`
1560
+ }
1561
+ });
1562
+ return response.ok;
1563
+ } catch {
1564
+ return false;
1565
+ }
1566
+ }
1567
+ };
1568
+ }
994
1569
  var mockServer = null;
995
1570
  var dbManager = null;
996
1571
  async function initPloyForDev(config) {
@@ -1001,6 +1576,56 @@ async function initPloyForDev(config) {
1001
1576
  return;
1002
1577
  }
1003
1578
  globalThis.__PLOY_DEV_INITIALIZED__ = true;
1579
+ const cliMockServerUrl = process.env.PLOY_MOCK_SERVER_URL;
1580
+ if (cliMockServerUrl) {
1581
+ const configPath2 = config?.configPath || "./ploy.yaml";
1582
+ const projectDir2 = process.cwd();
1583
+ let ployConfig2;
1584
+ try {
1585
+ ployConfig2 = readPloyConfig(projectDir2, configPath2);
1586
+ } catch {
1587
+ if (config?.bindings?.db) {
1588
+ ployConfig2 = { db: config.bindings.db };
1589
+ } else {
1590
+ return;
1591
+ }
1592
+ }
1593
+ if (config?.bindings?.db) {
1594
+ ployConfig2 = { ...ployConfig2, db: config.bindings.db };
1595
+ }
1596
+ const hasDbBindings2 = ployConfig2.db && Object.keys(ployConfig2.db).length > 0;
1597
+ const hasAuthConfig2 = !!ployConfig2.auth;
1598
+ if (!hasDbBindings2 && !hasAuthConfig2) {
1599
+ return;
1600
+ }
1601
+ const env2 = {};
1602
+ if (hasDbBindings2 && ployConfig2.db) {
1603
+ for (const [bindingName, databaseId] of Object.entries(ployConfig2.db)) {
1604
+ env2[bindingName] = createDevD1(databaseId, cliMockServerUrl);
1605
+ }
1606
+ }
1607
+ if (hasAuthConfig2) {
1608
+ env2.PLOY_AUTH = createDevPloyAuth(cliMockServerUrl);
1609
+ }
1610
+ const context2 = { env: env2, cf: void 0, ctx: void 0 };
1611
+ globalThis.__PLOY_DEV_CONTEXT__ = context2;
1612
+ Object.defineProperty(globalThis, PLOY_CONTEXT_SYMBOL, {
1613
+ get() {
1614
+ return context2;
1615
+ },
1616
+ configurable: true
1617
+ });
1618
+ const bindingNames2 = Object.keys(env2);
1619
+ const features2 = [];
1620
+ if (bindingNames2.length > 0) {
1621
+ features2.push(`bindings: ${bindingNames2.join(", ")}`);
1622
+ }
1623
+ if (hasAuthConfig2) {
1624
+ features2.push("auth");
1625
+ }
1626
+ console.log(`[Ploy] Using CLI mock server at ${cliMockServerUrl} (${features2.join(", ")})`);
1627
+ return;
1628
+ }
1004
1629
  const configPath = config?.configPath || "./ploy.yaml";
1005
1630
  const projectDir = process.cwd();
1006
1631
  let ployConfig;
@@ -1016,7 +1641,9 @@ async function initPloyForDev(config) {
1016
1641
  if (config?.bindings?.db) {
1017
1642
  ployConfig = { ...ployConfig, db: config.bindings.db };
1018
1643
  }
1019
- if (!ployConfig.db || Object.keys(ployConfig.db).length === 0) {
1644
+ const hasDbBindings = ployConfig.db && Object.keys(ployConfig.db).length > 0;
1645
+ const hasAuthConfig = !!ployConfig.auth;
1646
+ if (!hasDbBindings && !hasAuthConfig) {
1020
1647
  return;
1021
1648
  }
1022
1649
  ensureDataDir(projectDir);
@@ -1024,8 +1651,14 @@ async function initPloyForDev(config) {
1024
1651
  mockServer = await startMockServer(dbManager, ployConfig, {});
1025
1652
  const apiUrl = `http://localhost:${mockServer.port}`;
1026
1653
  const env = {};
1027
- for (const [bindingName, databaseId] of Object.entries(ployConfig.db)) {
1028
- env[bindingName] = createDevD1(databaseId, apiUrl);
1654
+ if (hasDbBindings && ployConfig.db) {
1655
+ for (const [bindingName, databaseId] of Object.entries(ployConfig.db)) {
1656
+ env[bindingName] = createDevD1(databaseId, apiUrl);
1657
+ }
1658
+ }
1659
+ if (hasAuthConfig) {
1660
+ env.PLOY_AUTH = createDevPloyAuth(apiUrl);
1661
+ process.env.NEXT_PUBLIC_PLOY_AUTH_URL = `${apiUrl}/auth`;
1029
1662
  }
1030
1663
  const context = {
1031
1664
  env,
@@ -1040,7 +1673,14 @@ async function initPloyForDev(config) {
1040
1673
  configurable: true
1041
1674
  });
1042
1675
  const bindingNames = Object.keys(env);
1043
- console.log(`[Ploy] Development context initialized with bindings: ${bindingNames.join(", ")}`);
1676
+ const features = [];
1677
+ if (bindingNames.length > 0) {
1678
+ features.push(`bindings: ${bindingNames.join(", ")}`);
1679
+ }
1680
+ if (hasAuthConfig) {
1681
+ features.push("auth");
1682
+ }
1683
+ console.log(`[Ploy] Development context initialized with ${features.join(", ")}`);
1044
1684
  console.log(`[Ploy] Mock server running at ${apiUrl}`);
1045
1685
  const cleanup = async () => {
1046
1686
  if (mockServer) {