@stardeck-customer-apps/testing 0.1.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/setup.js ADDED
@@ -0,0 +1,645 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/global-singleton.ts
26
+ function globalSingleton(key, create) {
27
+ const symbol = /* @__PURE__ */ Symbol.for(`stardeck-testing.${key}`);
28
+ const holder = globalThis;
29
+ holder[symbol] ??= create();
30
+ return holder[symbol];
31
+ }
32
+
33
+ // src/state.ts
34
+ var state = globalSingleton("state", () => ({
35
+ db: null,
36
+ currentUser: null,
37
+ sessions: /* @__PURE__ */ new Map(),
38
+ refreshSessions: /* @__PURE__ */ new Map(),
39
+ emails: [],
40
+ emailCounter: 0,
41
+ allowNetwork: false
42
+ }));
43
+ function requireDb() {
44
+ if (!state.db) {
45
+ throw new Error(
46
+ "[stardeck-testing] No active test app. Call `await createTestApp(...)` in beforeAll before using Stardeck SDKs in tests."
47
+ );
48
+ }
49
+ return state.db;
50
+ }
51
+
52
+ // src/constants.ts
53
+ var TEST_DOMAIN_SUFFIX = ".stardeck.test";
54
+ var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
55
+ var DATA_STORE_TEST_HOST = "db.stardeck.test";
56
+ var TEST_ENV_DEFAULTS = {
57
+ CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
58
+ DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
59
+ ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
60
+ PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
61
+ DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
62
+ DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`
63
+ };
64
+
65
+ // src/simulator/hmac.ts
66
+ var import_node_crypto = __toESM(require("crypto"));
67
+ var TIMESTAMP_TOLERANCE_SECONDS = 300;
68
+ function verifyDeploymentAuthHeader(secret, header) {
69
+ const dotIndex = header.lastIndexOf(".");
70
+ if (dotIndex === -1) return null;
71
+ const payloadB64 = header.slice(0, dotIndex);
72
+ const signature = header.slice(dotIndex + 1);
73
+ let payloadJson;
74
+ try {
75
+ payloadJson = Buffer.from(payloadB64, "base64").toString("utf-8");
76
+ } catch {
77
+ return null;
78
+ }
79
+ const expected = import_node_crypto.default.createHmac("sha256", secret).update(payloadJson).digest("hex");
80
+ const expectedBuf = Buffer.from(expected, "utf-8");
81
+ const actualBuf = Buffer.from(signature, "utf-8");
82
+ if (expectedBuf.length !== actualBuf.length || !import_node_crypto.default.timingSafeEqual(expectedBuf, actualBuf)) {
83
+ return null;
84
+ }
85
+ let payload;
86
+ try {
87
+ payload = JSON.parse(payloadJson);
88
+ } catch {
89
+ return null;
90
+ }
91
+ if (payload.type !== "deployment-request") return null;
92
+ const now = Math.floor(Date.now() / 1e3);
93
+ if (Math.abs(now - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
94
+ return payload;
95
+ }
96
+
97
+ // src/simulator/http.ts
98
+ function json(body, status = 200) {
99
+ return new Response(JSON.stringify(body), {
100
+ status,
101
+ headers: { "Content-Type": "application/json" }
102
+ });
103
+ }
104
+ function success(data) {
105
+ return json({ success: true, data });
106
+ }
107
+ function failure(error, status = 400) {
108
+ return json({ success: false, error }, status);
109
+ }
110
+
111
+ // src/simulator/data-store.ts
112
+ function quoteIdent(name) {
113
+ return `"${name.replace(/"/g, '""')}"`;
114
+ }
115
+ function normalizeIdent(name) {
116
+ return name.toLowerCase().replace(/\s+/g, "_");
117
+ }
118
+ async function getTableColumns(db, tableName) {
119
+ const { rows } = await db.query(
120
+ `SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 ORDER BY ordinal_position`,
121
+ [tableName]
122
+ );
123
+ if (rows.length === 0) return null;
124
+ return rows.map((r) => r.column_name);
125
+ }
126
+ async function getPrimaryKeyColumns(db, tableName) {
127
+ const { rows } = await db.query(
128
+ `SELECT ku.column_name
129
+ FROM information_schema.table_constraints tc
130
+ JOIN information_schema.key_column_usage ku
131
+ ON tc.constraint_name = ku.constraint_name AND tc.table_schema = ku.table_schema
132
+ WHERE tc.constraint_type = 'PRIMARY KEY'
133
+ AND tc.table_schema = 'public'
134
+ AND tc.table_name = $1`,
135
+ [tableName]
136
+ );
137
+ return rows.map((r) => r.column_name);
138
+ }
139
+ function coerceNumericFields(result) {
140
+ const numericFields = result.fields.filter((f) => f.dataTypeID === 1700).map((f) => f.name);
141
+ if (numericFields.length === 0) return result.rows;
142
+ return result.rows.map((row) => {
143
+ const coerced = { ...row };
144
+ for (const name of numericFields) {
145
+ if (typeof coerced[name] === "string") {
146
+ coerced[name] = parseFloat(coerced[name]);
147
+ }
148
+ }
149
+ return coerced;
150
+ });
151
+ }
152
+ function buildWhereClause(filters, validColumns) {
153
+ const conditions = [];
154
+ const params = [];
155
+ let paramIndex = 1;
156
+ for (const filter of filters) {
157
+ if (!validColumns.includes(filter.column)) continue;
158
+ const col = quoteIdent(filter.column);
159
+ switch (filter.operator) {
160
+ case "eq":
161
+ conditions.push(`${col} = $${paramIndex++}`);
162
+ params.push(filter.value);
163
+ break;
164
+ case "neq":
165
+ conditions.push(`${col} != $${paramIndex++}`);
166
+ params.push(filter.value);
167
+ break;
168
+ case "contains": {
169
+ conditions.push(`${col}::text ILIKE $${paramIndex++}`);
170
+ const escaped = String(filter.value ?? "").replace(/[%_\\]/g, "\\$&");
171
+ params.push(`%${escaped}%`);
172
+ break;
173
+ }
174
+ case "gt":
175
+ conditions.push(`${col} > $${paramIndex++}`);
176
+ params.push(filter.value);
177
+ break;
178
+ case "lt":
179
+ conditions.push(`${col} < $${paramIndex++}`);
180
+ params.push(filter.value);
181
+ break;
182
+ case "is_null":
183
+ conditions.push(`${col} IS NULL`);
184
+ break;
185
+ case "is_not_null":
186
+ conditions.push(`${col} IS NOT NULL`);
187
+ break;
188
+ }
189
+ }
190
+ if (conditions.length === 0) return { clause: "", params: [] };
191
+ return { clause: `WHERE ${conditions.join(" AND ")}`, params };
192
+ }
193
+ async function handleQuery(db, body) {
194
+ if (typeof body.tableName !== "string" || body.tableName.length === 0) {
195
+ return failure("Invalid request body", 400);
196
+ }
197
+ const tableName = body.tableName;
198
+ const limit = body.limit === void 0 ? 50 : Number(body.limit);
199
+ const offset = body.offset === void 0 ? 0 : Number(body.offset);
200
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
201
+ return failure("Invalid request body", 400);
202
+ }
203
+ if (!Number.isInteger(offset) || offset < 0) {
204
+ return failure("Invalid request body", 400);
205
+ }
206
+ if (body.orderDir !== void 0 && body.orderDir !== "asc" && body.orderDir !== "desc") {
207
+ return failure("Invalid request body", 400);
208
+ }
209
+ const orderBy = body.orderBy ? String(body.orderBy) : void 0;
210
+ const orderDir = body.orderDir === "desc" ? "desc" : "asc";
211
+ const filters = body.filters ?? [];
212
+ const validColumns = await getTableColumns(db, tableName);
213
+ if (!validColumns) return failure(`Table "${tableName}" not found`, 404);
214
+ if (orderBy && !validColumns.includes(orderBy)) {
215
+ return failure(`Column "${orderBy}" not found in table "${tableName}"`);
216
+ }
217
+ const { clause: whereClause, params: whereParams } = buildWhereClause(filters, validColumns);
218
+ const orderClause = orderBy ? `ORDER BY ${quoteIdent(orderBy)} ${orderDir === "desc" ? "DESC" : "ASC"}` : "";
219
+ const nextParamIdx = whereParams.length + 1;
220
+ const quotedTable = quoteIdent(tableName);
221
+ const preparedWhereParams = whereParams.map(prepareParam);
222
+ const dataResult = await db.query(
223
+ `SELECT * FROM ${quotedTable} ${whereClause} ${orderClause} LIMIT $${nextParamIdx} OFFSET $${nextParamIdx + 1}`,
224
+ [...preparedWhereParams, limit, offset]
225
+ );
226
+ const countResult = await db.query(
227
+ `SELECT count(*)::int as total FROM ${quotedTable} ${whereClause}`,
228
+ preparedWhereParams
229
+ );
230
+ return success({
231
+ rows: coerceNumericFields(dataResult),
232
+ columns: validColumns,
233
+ total: countResult.rows[0]?.total ?? 0,
234
+ limit,
235
+ offset
236
+ });
237
+ }
238
+ function buildPkWhere(primaryKey, pkColumns, startParamIdx) {
239
+ const conditions = [];
240
+ const params = [];
241
+ let paramIdx = startParamIdx;
242
+ for (const col of pkColumns) {
243
+ if (!(col in primaryKey)) {
244
+ throw new Error(`Missing primary key column: ${col}`);
245
+ }
246
+ conditions.push(`${quoteIdent(col)} = $${paramIdx++}`);
247
+ params.push(primaryKey[col]);
248
+ }
249
+ return { clause: conditions.join(" AND "), params };
250
+ }
251
+ async function handleMutate(db, body) {
252
+ if (typeof body.tableName !== "string" || body.tableName.length === 0) {
253
+ return failure("Invalid request body", 400);
254
+ }
255
+ const tableName = body.tableName;
256
+ const operation = String(body.operation ?? "");
257
+ if (!["insert", "update", "delete"].includes(operation)) {
258
+ return failure("Invalid request body", 400);
259
+ }
260
+ const columns = await getTableColumns(db, tableName);
261
+ if (!columns) return failure(`Table "${tableName}" not found`, 404);
262
+ const pkColumns = await getPrimaryKeyColumns(db, tableName);
263
+ switch (operation) {
264
+ case "insert": {
265
+ const row = body.row ?? {};
266
+ const insertColumns = Object.keys(row).filter((col) => columns.includes(col));
267
+ let sql;
268
+ let values;
269
+ if (insertColumns.length === 0) {
270
+ sql = `INSERT INTO ${quoteIdent(tableName)} DEFAULT VALUES RETURNING *`;
271
+ values = [];
272
+ } else {
273
+ values = insertColumns.map((col) => prepareParam(row[col]));
274
+ const colList = insertColumns.map((c) => quoteIdent(c)).join(", ");
275
+ const paramList = insertColumns.map((_, i) => `$${i + 1}`).join(", ");
276
+ sql = `INSERT INTO ${quoteIdent(tableName)} (${colList}) VALUES (${paramList}) RETURNING *`;
277
+ }
278
+ const result = await db.query(sql, values);
279
+ return success({ row: result.rows[0] });
280
+ }
281
+ case "update": {
282
+ const column = String(body.column ?? "");
283
+ if (!columns.includes(column)) {
284
+ return failure(`Column "${column}" not found in table "${tableName}"`);
285
+ }
286
+ if (pkColumns.length === 0) {
287
+ return failure(`Table "${tableName}" has no primary key`);
288
+ }
289
+ const { clause: pkClause, params: pkParams } = buildPkWhere(
290
+ body.primaryKey ?? {},
291
+ pkColumns,
292
+ 2
293
+ );
294
+ const updateValue = body.value === "" ? null : prepareParam(body.value);
295
+ const result = await db.query(
296
+ `UPDATE ${quoteIdent(tableName)} SET ${quoteIdent(column)} = $1 WHERE ${pkClause} RETURNING *`,
297
+ [updateValue, ...pkParams.map(prepareParam)]
298
+ );
299
+ if (result.rows.length === 0) return failure("Row not found", 404);
300
+ return success({ row: result.rows[0] });
301
+ }
302
+ case "delete": {
303
+ if (pkColumns.length === 0) {
304
+ return failure(`Table "${tableName}" has no primary key`);
305
+ }
306
+ const { clause: pkClause, params: pkParams } = buildPkWhere(
307
+ body.primaryKey ?? {},
308
+ pkColumns,
309
+ 1
310
+ );
311
+ const result = await db.query(
312
+ `DELETE FROM ${quoteIdent(tableName)} WHERE ${pkClause} RETURNING *`,
313
+ pkParams.map(prepareParam)
314
+ );
315
+ if (result.rows.length === 0) return failure("Row not found", 404);
316
+ return success({ deleted: true, row: result.rows[0] });
317
+ }
318
+ default:
319
+ return failure(`Unknown operation "${operation}"`);
320
+ }
321
+ }
322
+ function prepareParam(value) {
323
+ if (value instanceof Date) return value.toISOString();
324
+ if (Array.isArray(value)) return encodeArrayLiteral(value);
325
+ if (value !== null && typeof value === "object") return JSON.stringify(value);
326
+ return value;
327
+ }
328
+ function encodeArrayLiteral(values) {
329
+ const items = values.map((v) => {
330
+ if (v === null || v === void 0) return "NULL";
331
+ if (Array.isArray(v)) return quoteArrayElement(encodeArrayLiteral(v));
332
+ if (v instanceof Date) return quoteArrayElement(v.toISOString());
333
+ if (typeof v === "object") return quoteArrayElement(JSON.stringify(v));
334
+ return quoteArrayElement(String(v));
335
+ });
336
+ return `{${items.join(",")}}`;
337
+ }
338
+ function quoteArrayElement(raw) {
339
+ return `"${raw.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
340
+ }
341
+ async function handleGetSchema(db) {
342
+ const { rows } = await db.query(
343
+ `SELECT
344
+ t.table_name,
345
+ c.column_name,
346
+ c.data_type,
347
+ c.is_nullable,
348
+ c.column_default,
349
+ CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary_key
350
+ FROM information_schema.tables t
351
+ JOIN information_schema.columns c
352
+ ON t.table_name = c.table_name AND t.table_schema = c.table_schema
353
+ LEFT JOIN (
354
+ SELECT ku.column_name, ku.table_name
355
+ FROM information_schema.table_constraints tc
356
+ JOIN information_schema.key_column_usage ku
357
+ ON tc.constraint_name = ku.constraint_name AND tc.table_schema = ku.table_schema
358
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public'
359
+ ) pk ON pk.table_name = c.table_name AND pk.column_name = c.column_name
360
+ WHERE t.table_schema = 'public'
361
+ AND t.table_type = 'BASE TABLE'
362
+ AND t.table_name NOT LIKE '\\_deleted\\_%'
363
+ ORDER BY t.table_name, c.ordinal_position`
364
+ );
365
+ const tables = /* @__PURE__ */ new Map();
366
+ for (const row of rows) {
367
+ if (!tables.has(row.table_name)) tables.set(row.table_name, []);
368
+ const cols = tables.get(row.table_name);
369
+ cols.push({
370
+ columnName: row.column_name,
371
+ dataType: row.data_type,
372
+ isNullable: row.is_nullable === "YES",
373
+ columnDefault: row.column_default,
374
+ isPrimaryKey: row.is_primary_key,
375
+ position: cols.length
376
+ });
377
+ }
378
+ return success({
379
+ tables: Array.from(tables, ([tableName, columns]) => ({ tableName, columns }))
380
+ });
381
+ }
382
+ var FIELD_TYPE_TO_PG = {
383
+ text: "text",
384
+ long_text: "text",
385
+ number: "double precision",
386
+ boolean: "boolean",
387
+ date: "date",
388
+ datetime: "timestamptz",
389
+ select: "text",
390
+ multi_select: "jsonb",
391
+ url: "text",
392
+ email: "text",
393
+ phone: "text",
394
+ currency: "numeric",
395
+ rating: "integer",
396
+ relation: "uuid",
397
+ file_ref: "jsonb",
398
+ file_refs: "jsonb",
399
+ json: "jsonb"
400
+ };
401
+ function columnDdl(column) {
402
+ const pgType = FIELD_TYPE_TO_PG[column.fieldType];
403
+ if (!pgType) {
404
+ throw new Error(
405
+ `Unsupported fieldType "${column.fieldType}" in test simulator. Supported: ${Object.keys(FIELD_TYPE_TO_PG).join(", ")}`
406
+ );
407
+ }
408
+ let ddl = `${quoteIdent(normalizeIdent(column.name))} ${pgType}`;
409
+ if (column.nullable === false) ddl += " NOT NULL";
410
+ return ddl;
411
+ }
412
+ async function handleCreateTable(db, body) {
413
+ const name = normalizeIdent(String(body.name ?? ""));
414
+ const columns = body.columns ?? [];
415
+ if (!name) return failure("Table name is required");
416
+ try {
417
+ const columnDefs = [
418
+ `"id" uuid PRIMARY KEY DEFAULT gen_random_uuid()`,
419
+ ...columns.map(columnDdl),
420
+ `"created_at" timestamptz NOT NULL DEFAULT now()`,
421
+ `"updated_at" timestamptz NOT NULL DEFAULT now()`
422
+ ];
423
+ await db.exec(`CREATE TABLE ${quoteIdent(name)} (
424
+ ${columnDefs.join(",\n ")}
425
+ )`);
426
+ } catch (error) {
427
+ return failure(error instanceof Error ? error.message : String(error));
428
+ }
429
+ return success({ tableName: name });
430
+ }
431
+ async function handleAddColumn(db, body) {
432
+ const tableName = normalizeIdent(String(body.tableName ?? ""));
433
+ const column = body.column;
434
+ if (!tableName || !column) return failure("tableName and column are required");
435
+ try {
436
+ await db.exec(`ALTER TABLE ${quoteIdent(tableName)} ADD COLUMN ${columnDdl(column)}`);
437
+ } catch (error) {
438
+ return failure(error instanceof Error ? error.message : String(error));
439
+ }
440
+ return success({ columnName: normalizeIdent(column.name) });
441
+ }
442
+
443
+ // src/pglite/database.ts
444
+ var import_pglite = require("@electric-sql/pglite");
445
+ var import_citext = require("@electric-sql/pglite/contrib/citext");
446
+ var import_pg_trgm = require("@electric-sql/pglite/contrib/pg_trgm");
447
+ var import_pgcrypto = require("@electric-sql/pglite/contrib/pgcrypto");
448
+ var import_uuid_ossp = require("@electric-sql/pglite/contrib/uuid_ossp");
449
+ var identityParsers = null;
450
+ function getIdentityParsers() {
451
+ if (!identityParsers) {
452
+ identityParsers = {};
453
+ const identity = (value) => value;
454
+ for (let oid = 1; oid <= 13e3; oid++) {
455
+ identityParsers[oid] = identity;
456
+ }
457
+ }
458
+ return identityParsers;
459
+ }
460
+
461
+ // src/simulator/neon-proxy.ts
462
+ async function handleNeonSql(db, request) {
463
+ const body = await request.json();
464
+ if (body.queries) {
465
+ return json(
466
+ {
467
+ message: "Transactions over the Neon HTTP driver are not supported (matches production neon-http behavior). Use single statements in tests."
468
+ },
469
+ 400
470
+ );
471
+ }
472
+ if (!body.query) {
473
+ return json({ message: "Missing query" }, 400);
474
+ }
475
+ const arrayMode = (request.headers.get("Neon-Array-Mode") ?? request.headers.get("neon-array-mode")) === "true";
476
+ try {
477
+ const result = await db.query(body.query, body.params ?? [], {
478
+ parsers: getIdentityParsers(),
479
+ rowMode: arrayMode ? "array" : "object"
480
+ });
481
+ const command = body.query.trim().split(/\s+/)[0]?.toUpperCase() ?? "SELECT";
482
+ return json({
483
+ command,
484
+ rowCount: result.affectedRows || result.rows.length,
485
+ fields: result.fields.map((f) => ({ name: f.name, dataTypeID: f.dataTypeID })),
486
+ rows: result.rows,
487
+ rowAsArray: arrayMode
488
+ });
489
+ } catch (error) {
490
+ const err = error;
491
+ return json({ message: err.message, code: err.code }, 400);
492
+ }
493
+ }
494
+
495
+ // src/simulator/auth.ts
496
+ var import_node_crypto2 = __toESM(require("crypto"));
497
+ function handleAuthVerify(request) {
498
+ const authHeader = request.headers.get("Authorization");
499
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
500
+ const user = token ? state.sessions.get(token) : null;
501
+ if (!user) {
502
+ return json({ authenticated: false, user: null }, 401);
503
+ }
504
+ return json({ authenticated: true, user });
505
+ }
506
+ function handleAuthRefresh(request) {
507
+ const authHeader = request.headers.get("Authorization");
508
+ const refreshToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
509
+ const user = refreshToken ? state.refreshSessions.get(refreshToken) : null;
510
+ if (!user || !refreshToken) {
511
+ return json({ error: "invalid_grant" }, 401);
512
+ }
513
+ const accessToken = `test-access-${import_node_crypto2.default.randomUUID()}`;
514
+ state.sessions.set(accessToken, user);
515
+ return json({
516
+ access_token: accessToken,
517
+ refresh_token: refreshToken,
518
+ token_type: "Bearer",
519
+ expires_in: 3600,
520
+ refresh_token_expires_in: 86400
521
+ });
522
+ }
523
+
524
+ // src/simulator/email.ts
525
+ function toArray(value) {
526
+ if (!value) return [];
527
+ return Array.isArray(value) ? value.map(String) : [String(value)];
528
+ }
529
+ async function handleEmailSend(request) {
530
+ const body = await request.json();
531
+ const from = body.from ?? {};
532
+ if (!from.name || !from.prefix || !body.to || !body.subject) {
533
+ return failure("Missing required email fields");
534
+ }
535
+ state.emailCounter += 1;
536
+ const resendId = `test-email-${state.emailCounter}`;
537
+ const fromAddress = `${from.name} <${from.prefix}@test.stardeck.email>`;
538
+ const email = {
539
+ resendId,
540
+ fromAddress,
541
+ from: { name: from.name, prefix: from.prefix },
542
+ to: toArray(body.to),
543
+ cc: toArray(body.cc),
544
+ bcc: toArray(body.bcc),
545
+ subject: String(body.subject),
546
+ html: body.html ? String(body.html) : void 0,
547
+ text: body.text ? String(body.text) : void 0,
548
+ replyTo: body.replyTo ? String(body.replyTo) : void 0,
549
+ attachments: (body.attachments ?? []).map((a) => ({
550
+ filename: String(a.filename ?? ""),
551
+ contentType: a.contentType ? String(a.contentType) : void 0
552
+ })),
553
+ sentAt: /* @__PURE__ */ new Date()
554
+ };
555
+ state.emails.push(email);
556
+ return success({ resendId, fromAddress });
557
+ }
558
+
559
+ // src/simulator/router.ts
560
+ var fetchHolder = globalSingleton("fetch-holder", () => ({
561
+ originalFetch: null
562
+ }));
563
+ function isLocalHost(hostname) {
564
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0";
565
+ }
566
+ async function handleSimulatedRequest(request, url) {
567
+ if (url.pathname === "/sql") {
568
+ return handleNeonSql(requireDb(), request);
569
+ }
570
+ const authMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/auth\/(verify|refresh)$/);
571
+ if (authMatch) {
572
+ return authMatch[1] === "verify" ? handleAuthVerify(request) : handleAuthRefresh(request);
573
+ }
574
+ const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
575
+ const isEmail = url.pathname === "/api/email/send";
576
+ if (dataStoreMatch || isEmail) {
577
+ const authHeader = request.headers.get("X-Stardeck-Auth");
578
+ if (!authHeader) {
579
+ return failure("Missing authentication header", 401);
580
+ }
581
+ const secret = process.env.DEPLOYMENT_SECRET;
582
+ if (!secret || !verifyDeploymentAuthHeader(secret, authHeader)) {
583
+ return failure("Invalid authentication", 401);
584
+ }
585
+ }
586
+ if (isEmail && request.method === "POST") {
587
+ return handleEmailSend(request);
588
+ }
589
+ if (dataStoreMatch) {
590
+ const subPath = dataStoreMatch[1] ?? "";
591
+ const db = requireDb();
592
+ const readBody = async () => await request.json();
593
+ if (subPath === "/query" && request.method === "POST") {
594
+ return handleQuery(db, await readBody());
595
+ }
596
+ if (subPath === "/mutate" && request.method === "POST") {
597
+ return handleMutate(db, await readBody());
598
+ }
599
+ if (subPath === "/schema" && request.method === "GET") {
600
+ return handleGetSchema(db);
601
+ }
602
+ if (subPath === "/schema/tables" && request.method === "POST") {
603
+ return handleCreateTable(db, await readBody());
604
+ }
605
+ if (subPath === "/schema/columns" && request.method === "POST") {
606
+ return handleAddColumn(db, await readBody());
607
+ }
608
+ }
609
+ return failure(
610
+ `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, auth verify/refresh, Neon /sql.`,
611
+ 404
612
+ );
613
+ }
614
+ function installFetchRouter() {
615
+ if (fetchHolder.originalFetch) return;
616
+ fetchHolder.originalFetch = globalThis.fetch;
617
+ globalThis.fetch = async (input, init) => {
618
+ const request = input instanceof Request ? input : new Request(input, init);
619
+ const merged = input instanceof Request && init ? new Request(request, init) : request;
620
+ const url = new URL(merged.url);
621
+ if (url.hostname.endsWith(TEST_DOMAIN_SUFFIX)) {
622
+ return handleSimulatedRequest(merged, url);
623
+ }
624
+ if (isLocalHost(url.hostname) || state.allowNetwork) {
625
+ return fetchHolder.originalFetch(merged);
626
+ }
627
+ throw new Error(
628
+ `[stardeck-testing] Blocked outbound request to ${url.origin}${url.pathname}. Tests run offline by default for determinism. If this request is intentional, pass { allowNetwork: true } to createTestApp(), or point the code at a simulated service.`
629
+ );
630
+ };
631
+ }
632
+
633
+ // src/setup.ts
634
+ if (state.db) {
635
+ const stale = state.db;
636
+ state.db = null;
637
+ state.currentUser = null;
638
+ state.sessions.clear();
639
+ state.refreshSessions.clear();
640
+ state.emails = [];
641
+ state.emailCounter = 0;
642
+ void stale.close().catch(() => {
643
+ });
644
+ }
645
+ installFetchRouter();