@stardeck-customer-apps/testing 0.3.1 → 0.5.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/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/test-app.ts
2
- import crypto4 from "crypto";
2
+ import crypto8 from "crypto";
3
3
  import { readFileSync, existsSync } from "fs";
4
4
  import { resolve } from "path";
5
5
 
@@ -54,7 +54,23 @@ var state = globalSingleton("state", () => ({
54
54
  emailCounter: 0,
55
55
  identities: /* @__PURE__ */ new Map(),
56
56
  identityLinks: [],
57
- identityMemory: [],
57
+ checkouts: [],
58
+ checkoutCounter: 0,
59
+ sessionStatuses: /* @__PURE__ */ new Map(),
60
+ paymentLinks: /* @__PURE__ */ new Map(),
61
+ products: [],
62
+ uploads: [],
63
+ uploadCounter: 0,
64
+ storageFiles: /* @__PURE__ */ new Map(),
65
+ presignPending: /* @__PURE__ */ new Map(),
66
+ messages: [],
67
+ edgePrints: [],
68
+ edgeDisplays: [],
69
+ edgeTestPrints: [],
70
+ edgePrintCounter: 0,
71
+ edgeDevices: [],
72
+ edgePeripherals: [],
73
+ edgeBindings: /* @__PURE__ */ new Map(),
58
74
  allowNetwork: false
59
75
  }));
60
76
  function requireDb() {
@@ -70,13 +86,16 @@ function requireDb() {
70
86
  var TEST_DOMAIN_SUFFIX = ".stardeck.test";
71
87
  var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
72
88
  var DATA_STORE_TEST_HOST = "db.stardeck.test";
89
+ var STORAGE_TEST_URL = "https://storage.stardeck.test";
90
+ var STORAGE_TEST_HOST = "storage.stardeck.test";
73
91
  var TEST_ENV_DEFAULTS = {
74
92
  CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
75
93
  DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
76
94
  ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
77
95
  PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
78
96
  DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
79
- DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`
97
+ DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`,
98
+ STORAGE_URL: STORAGE_TEST_URL
80
99
  };
81
100
  var DEFAULT_TEST_USER = {
82
101
  id: "test-user-1",
@@ -114,8 +133,8 @@ function verifyDeploymentAuthHeader(secret, header) {
114
133
  return null;
115
134
  }
116
135
  if (payload.type !== "deployment-request") return null;
117
- const now2 = Math.floor(Date.now() / 1e3);
118
- if (Math.abs(now2 - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
136
+ const now3 = Math.floor(Date.now() / 1e3);
137
+ if (Math.abs(now3 - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
119
138
  return payload;
120
139
  }
121
140
 
@@ -132,6 +151,13 @@ function success(data) {
132
151
  function failure(error, status = 400) {
133
152
  return json({ success: false, error }, status);
134
153
  }
154
+ async function readJsonBody(request) {
155
+ try {
156
+ return await request.json();
157
+ } catch {
158
+ return {};
159
+ }
160
+ }
135
161
 
136
162
  // src/simulator/data-store.ts
137
163
  function quoteIdent(name) {
@@ -614,13 +640,6 @@ function now() {
614
640
  function linksFor(identityId) {
615
641
  return state.identityLinks.filter((l) => l.identityId === identityId);
616
642
  }
617
- async function readBody(request) {
618
- try {
619
- return await request.json();
620
- } catch {
621
- return {};
622
- }
623
- }
624
643
  function handleList(request) {
625
644
  const typeParam = new URL(request.url).searchParams.get("type");
626
645
  const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
@@ -628,7 +647,7 @@ function handleList(request) {
628
647
  return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
629
648
  }
630
649
  async function handleCreate(request) {
631
- const body = await readBody(request);
650
+ const body = await readJsonBody(request);
632
651
  const type = body.type;
633
652
  if (type !== "person" && type !== "account") {
634
653
  return failure("type must be 'person' or 'account'");
@@ -665,7 +684,7 @@ function handleGet(identityId) {
665
684
  async function handleUpdate(identityId, request) {
666
685
  const identity = state.identities.get(identityId);
667
686
  if (!identity) return failure("identity not found", 404);
668
- const body = await readBody(request);
687
+ const body = await readJsonBody(request);
669
688
  if (body.displayName !== void 0) {
670
689
  identity.displayName = body.displayName;
671
690
  }
@@ -687,7 +706,7 @@ async function handleAttachLink(identityId, request) {
687
706
  if (identity.status !== "active") {
688
707
  return failure("links attach only to active persons");
689
708
  }
690
- const body = await readBody(request);
709
+ const body = await readJsonBody(request);
691
710
  const kind = body.kind;
692
711
  const externalId = body.externalId;
693
712
  if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
@@ -712,108 +731,16 @@ async function handleAttachLink(identityId, request) {
712
731
  state.identityLinks.push(link);
713
732
  return success({ link });
714
733
  }
715
- function memoryDto(m) {
716
- return {
717
- id: m.id,
718
- source: m.source,
719
- kind: m.kind,
720
- content: m.content,
721
- metadata: m.metadata,
722
- createdAt: m.createdAt
723
- };
724
- }
725
- async function handleResolve(request) {
726
- const body = await readBody(request);
727
- const type = body.type;
728
- if (type !== "person" && type !== "account") {
729
- return failure("type must be 'person' or 'account'");
730
- }
731
- if (type === "account") {
732
- return failure(
733
- "resolveOrCreate is for channel-linked persons; an account is not resolvable by link \u2014 create it explicitly instead.",
734
- 409
735
- );
736
- }
737
- const link = body.link;
738
- const kind = link?.kind;
739
- const externalId = link?.externalId;
740
- if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
741
- return failure(`link.kind must be one of: ${[...LINK_KINDS].join(", ")}`);
742
- }
743
- if (typeof externalId !== "string" || !externalId) {
744
- return failure("link.externalId is required");
745
- }
746
- const existingLink = state.identityLinks.find(
747
- (l) => l.kind === kind && l.externalId === externalId
748
- );
749
- if (existingLink) {
750
- const identity2 = state.identities.get(existingLink.identityId);
751
- if (identity2) return success({ identity: identity2, created: false });
752
- }
753
- const identity = {
754
- id: crypto3.randomUUID(),
755
- type,
756
- parentId: null,
757
- displayName: body.displayName ?? null,
758
- profile: body.profile ?? {},
759
- status: "active",
760
- mergedIntoId: null,
761
- externalRef: null,
762
- createdAt: now(),
763
- updatedAt: now()
764
- };
765
- state.identities.set(identity.id, identity);
766
- state.identityLinks.push({
767
- id: crypto3.randomUUID(),
768
- identityId: identity.id,
769
- kind,
770
- externalId,
771
- verified: true,
772
- createdAt: now()
773
- });
774
- return success({ identity, created: true });
775
- }
776
- async function handleWriteMemory(identityId, request) {
777
- if (!state.identities.get(identityId)) return failure("Identity not found", 404);
778
- const body = await readBody(request);
779
- if (typeof body.content !== "string" || !body.content) return failure("content is required");
780
- const entry = {
781
- id: crypto3.randomUUID(),
782
- identityId,
783
- source: typeof body.source === "string" ? body.source : "app",
784
- kind: typeof body.kind === "string" ? body.kind : "fact",
785
- content: body.content,
786
- metadata: body.metadata ?? {},
787
- createdAt: now()
788
- };
789
- state.identityMemory.push(entry);
790
- return success({ memory: memoryDto(entry) });
791
- }
792
- function handleListMemory(identityId, request) {
793
- if (!state.identities.get(identityId)) return failure("Identity not found", 404);
794
- let rows = state.identityMemory.filter((m) => m.identityId === identityId).reverse();
795
- const limitParam = new URL(request.url).searchParams.get("limit");
796
- if (limitParam) rows = rows.slice(0, Math.max(1, parseInt(limitParam, 10) || 0));
797
- return success({ memories: rows.map(memoryDto) });
798
- }
799
734
  async function handleIdentitiesRequest(request, subPath) {
800
735
  const method = request.method;
801
736
  if (subPath === "" || subPath === "/") {
802
737
  if (method === "GET") return handleList(request);
803
738
  if (method === "POST") return handleCreate(request);
804
739
  }
805
- if (subPath === "/resolve" && method === "POST") {
806
- return handleResolve(request);
807
- }
808
740
  const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
809
741
  if (linksMatch && method === "POST") {
810
742
  return handleAttachLink(linksMatch[1], request);
811
743
  }
812
- const memoryMatch = subPath.match(/^\/([^/]+)\/memory$/);
813
- if (memoryMatch) {
814
- if (method === "GET") return handleListMemory(memoryMatch[1], request);
815
- if (method === "POST") return handleWriteMemory(memoryMatch[1], request);
816
- }
817
744
  const singleMatch = subPath.match(/^\/([^/]+)$/);
818
745
  if (singleMatch) {
819
746
  if (method === "GET") return handleGet(singleMatch[1]);
@@ -836,6 +763,897 @@ function createDirectory() {
836
763
  };
837
764
  }
838
765
 
766
+ // src/simulator/payments.ts
767
+ import crypto5 from "crypto";
768
+
769
+ // src/next/headers-shim.ts
770
+ import { AsyncLocalStorage } from "async_hooks";
771
+ var requestScopeStorage = globalSingleton(
772
+ "request-scope",
773
+ () => new AsyncLocalStorage()
774
+ );
775
+
776
+ // src/next/call-route.ts
777
+ function parseCookieHeader(header) {
778
+ const map = /* @__PURE__ */ new Map();
779
+ if (!header) return map;
780
+ for (const part of header.split(";")) {
781
+ const eq = part.indexOf("=");
782
+ if (eq === -1) continue;
783
+ map.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
784
+ }
785
+ return map;
786
+ }
787
+ async function importNextServer() {
788
+ try {
789
+ return await import("next/server.js");
790
+ } catch {
791
+ return await import("next/server");
792
+ }
793
+ }
794
+ async function callRoute(handler, options = {}) {
795
+ const { NextRequest } = await importNextServer();
796
+ const path = options.path ?? "/api/test-route";
797
+ const url = new URL(`http://localhost:3333${path}`);
798
+ for (const [key, value] of Object.entries(options.searchParams ?? {})) {
799
+ url.searchParams.set(key, value);
800
+ }
801
+ const method = options.method ?? (options.body !== void 0 ? "POST" : "GET");
802
+ const headers = new Headers(options.headers);
803
+ const user = options.user !== void 0 ? options.user : state.currentUser;
804
+ if (user && !headers.has("x-stardeck-user")) {
805
+ headers.set("x-stardeck-user", JSON.stringify(user));
806
+ }
807
+ if (options.body !== void 0 && !headers.has("Content-Type")) {
808
+ headers.set("Content-Type", "application/json");
809
+ }
810
+ const cookiePairs = Object.entries(options.cookies ?? {});
811
+ if (cookiePairs.length > 0) {
812
+ const existing = headers.get("cookie");
813
+ const cookieString = cookiePairs.map(([k, v]) => `${k}=${v}`).join("; ");
814
+ headers.set("cookie", existing ? `${existing}; ${cookieString}` : cookieString);
815
+ }
816
+ const request = new NextRequest(url, {
817
+ method,
818
+ headers,
819
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
820
+ });
821
+ const scope = {
822
+ headers,
823
+ cookies: parseCookieHeader(headers.get("cookie"))
824
+ };
825
+ try {
826
+ return await requestScopeStorage.run(
827
+ scope,
828
+ () => Promise.resolve(
829
+ handler(request, {
830
+ params: Promise.resolve(options.params ?? {})
831
+ })
832
+ )
833
+ );
834
+ } catch (error) {
835
+ const redirect = decodeNextRedirect(error);
836
+ if (redirect) return redirect;
837
+ throw error;
838
+ }
839
+ }
840
+ function decodeNextRedirect(error) {
841
+ const digest = error?.digest;
842
+ if (typeof digest !== "string" || !digest.startsWith("NEXT_REDIRECT")) return null;
843
+ const parts = digest.split(";");
844
+ const location = parts[2] ?? "/";
845
+ const status = Number(parts[3]) || 307;
846
+ return new Response(null, { status, headers: { location } });
847
+ }
848
+
849
+ // src/simulator/webhook-signing.ts
850
+ import crypto4 from "crypto";
851
+ function signEventDelivery(secret, context, rawBody) {
852
+ const payload = {
853
+ type: "deployment-request",
854
+ organizationId: context.organizationId,
855
+ projectId: context.projectId,
856
+ deploymentId: context.deploymentId,
857
+ timestamp: Math.floor(Date.now() / 1e3),
858
+ nonce: crypto4.randomUUID()
859
+ };
860
+ const payloadJson = JSON.stringify(payload);
861
+ const payloadB64 = Buffer.from(payloadJson).toString("base64");
862
+ const signature = crypto4.createHmac("sha256", secret).update(payloadJson).update(rawBody).digest("hex");
863
+ return `${payloadB64}.${signature}`;
864
+ }
865
+
866
+ // src/simulator/payments.ts
867
+ function payErr(error, status = 400, code) {
868
+ return json(code ? { error, code } : { error }, status);
869
+ }
870
+ function nextCheckoutId() {
871
+ state.checkoutCounter += 1;
872
+ return `cs_test_${state.checkoutCounter}`;
873
+ }
874
+ function nextPaymentLinkId() {
875
+ state.checkoutCounter += 1;
876
+ return `plink_test_${state.checkoutCounter}`;
877
+ }
878
+ function seedStripeSession(id, body) {
879
+ const lineItems = body.lineItems ?? [];
880
+ let amountTotal = null;
881
+ let currency = null;
882
+ if (lineItems.length > 0) {
883
+ amountTotal = lineItems.reduce(
884
+ (sum, item) => sum + (item.priceData?.unitAmount ?? 0) * (item.quantity ?? 1),
885
+ 0
886
+ );
887
+ currency = lineItems[0].priceData?.currency ?? null;
888
+ }
889
+ state.sessionStatuses.set(id, {
890
+ id,
891
+ status: "open",
892
+ paymentStatus: "unpaid",
893
+ mode: body.mode ?? "payment",
894
+ amountTotal,
895
+ currency,
896
+ customerEmail: body.customerEmail ? String(body.customerEmail) : null,
897
+ metadata: body.metadata ?? {},
898
+ expiresAt: Math.floor(Date.now() / 1e3) + 3600
899
+ });
900
+ }
901
+ function seedBeamLink(id, body, merchantId) {
902
+ const order = body.order;
903
+ state.paymentLinks.set(id, {
904
+ paymentLinkId: id,
905
+ merchantId,
906
+ url: `https://beam.test/pay/${id}`,
907
+ status: "ACTIVE",
908
+ order: {
909
+ netAmount: Number(order?.netAmount ?? 0),
910
+ currency: String(order?.currency ?? "THB"),
911
+ description: String(order?.description ?? ""),
912
+ referenceId: order?.referenceId ? String(order.referenceId) : void 0,
913
+ internalNote: order?.internalNote ? String(order.internalNote) : void 0,
914
+ orderItems: order?.orderItems
915
+ },
916
+ redirectUrl: body.redirectUrl ? String(body.redirectUrl) : void 0,
917
+ linkSettings: body.linkSettings,
918
+ collectDeliveryAddress: body.collectDeliveryAddress === true
919
+ });
920
+ }
921
+ async function deliverEvent(envelope, handler, options) {
922
+ const rawBody = JSON.stringify(envelope);
923
+ const secret = options?.deploymentSecret ?? TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET;
924
+ const authHeader = signEventDelivery(
925
+ secret,
926
+ {
927
+ organizationId: TEST_ENV_DEFAULTS.ORGANIZATION_ID,
928
+ projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
929
+ deploymentId: TEST_ENV_DEFAULTS.DEPLOYMENT_ID
930
+ },
931
+ rawBody
932
+ );
933
+ return callRoute(handler, {
934
+ method: "POST",
935
+ path: options?.path ?? "/api/payments/webhooks",
936
+ params: { path: ["webhooks"] },
937
+ headers: { "X-Stardeck-Auth": authHeader },
938
+ body: envelope
939
+ });
940
+ }
941
+ async function handlePaymentsRequest(request, url) {
942
+ const pathname = url.pathname;
943
+ if (/\/bolt-connections/.test(pathname) || /\/bolt-intents/.test(pathname) || /\/charges(\/|$)/.test(pathname) || /\/billing-portal$/.test(pathname)) {
944
+ return payErr("Not found", 404, "NOT_FOUND");
945
+ }
946
+ const beamProductsMatch = pathname.match(
947
+ /^\/api\/store\/beam\/([^/]+)\/payment-links(?:\/([^/]+))?$/
948
+ );
949
+ if (beamProductsMatch) {
950
+ const merchantId = beamProductsMatch[1];
951
+ const linkId = beamProductsMatch[2];
952
+ if (!linkId && request.method === "POST") {
953
+ const body = await readJsonBody(request);
954
+ const id = nextPaymentLinkId();
955
+ const checkoutUrl = `https://beam.test/pay/${id}`;
956
+ const captured = {
957
+ id,
958
+ url: checkoutUrl,
959
+ provider: "beam",
960
+ options: body,
961
+ metadata: body.metadata,
962
+ createdAt: /* @__PURE__ */ new Date()
963
+ };
964
+ state.checkouts.push(captured);
965
+ seedBeamLink(id, body, merchantId);
966
+ return json({ id, url: checkoutUrl });
967
+ }
968
+ if (linkId && request.method === "GET") {
969
+ const link = state.paymentLinks.get(linkId);
970
+ if (!link) return payErr("Payment link not found", 404, "NOT_FOUND");
971
+ return json({ paymentLink: link });
972
+ }
973
+ }
974
+ const stripeStoreMatch = pathname.match(/^\/api\/store\/([^/]+)\/(.+)$/);
975
+ if (stripeStoreMatch) {
976
+ const accountId = stripeStoreMatch[1];
977
+ const subPath = stripeStoreMatch[2];
978
+ if (accountId === "beam") {
979
+ return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
980
+ }
981
+ if (subPath === "products" && request.method === "GET") {
982
+ return json({ products: state.products });
983
+ }
984
+ const productMatch = subPath.match(/^products\/([^/]+)$/);
985
+ if (productMatch && request.method === "GET") {
986
+ const product = state.products.find((p) => p.id === productMatch[1]);
987
+ if (!product) return payErr("Product not found", 404, "NOT_FOUND");
988
+ return json({ product });
989
+ }
990
+ if (subPath === "checkout" && request.method === "POST") {
991
+ const body = await readJsonBody(request);
992
+ const id = nextCheckoutId();
993
+ const checkoutUrl = `https://checkout.stripe.test/c/pay/${id}`;
994
+ const captured = {
995
+ id,
996
+ url: checkoutUrl,
997
+ provider: "stripe",
998
+ options: body,
999
+ mode: body.mode,
1000
+ metadata: body.metadata,
1001
+ createdAt: /* @__PURE__ */ new Date()
1002
+ };
1003
+ state.checkouts.push(captured);
1004
+ seedStripeSession(id, body);
1005
+ return json({ id, url: checkoutUrl });
1006
+ }
1007
+ const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
1008
+ if (sessionMatch && request.method === "GET") {
1009
+ const session = state.sessionStatuses.get(sessionMatch[1]);
1010
+ if (!session) return payErr("Checkout session not found", 404, "NOT_FOUND");
1011
+ return json({ session });
1012
+ }
1013
+ }
1014
+ return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
1015
+ }
1016
+ function createPayments() {
1017
+ return {
1018
+ get checkouts() {
1019
+ return [...state.checkouts];
1020
+ },
1021
+ latest() {
1022
+ return state.checkouts[state.checkouts.length - 1];
1023
+ },
1024
+ setProducts(products) {
1025
+ state.products = products;
1026
+ },
1027
+ markPaid(id) {
1028
+ const session = state.sessionStatuses.get(id);
1029
+ if (session) {
1030
+ session.status = "complete";
1031
+ session.paymentStatus = "paid";
1032
+ return;
1033
+ }
1034
+ const link = state.paymentLinks.get(id);
1035
+ if (link) {
1036
+ link.status = "PAID";
1037
+ return;
1038
+ }
1039
+ throw new Error(`[stardeck-testing] Unknown checkout or payment link id: ${id}`);
1040
+ },
1041
+ setSessionStatus(id, status) {
1042
+ const session = state.sessionStatuses.get(id);
1043
+ if (!session) {
1044
+ throw new Error(`[stardeck-testing] Unknown checkout session id: ${id}`);
1045
+ }
1046
+ Object.assign(session, status);
1047
+ },
1048
+ setPaymentLinkStatus(id, status) {
1049
+ const link = state.paymentLinks.get(id);
1050
+ if (!link) {
1051
+ throw new Error(`[stardeck-testing] Unknown payment link id: ${id}`);
1052
+ }
1053
+ link.status = status;
1054
+ },
1055
+ async deliverStripeEvent(handler, event, options) {
1056
+ const envelope = {
1057
+ id: `evt_test_${crypto5.randomUUID()}`,
1058
+ kind: "stripe_webhook",
1059
+ timestamp: Date.now(),
1060
+ stripeEvent: {
1061
+ type: event.type,
1062
+ accountId: event.accountId ?? process.env.STRIPE_CONNECT_ACCOUNT_ID ?? "acct_test",
1063
+ data: event.data
1064
+ }
1065
+ };
1066
+ return deliverEvent(envelope, handler, options);
1067
+ },
1068
+ async deliverBeamEvent(handler, event, options) {
1069
+ const envelope = {
1070
+ id: `beam_evt_test_${crypto5.randomUUID()}`,
1071
+ kind: "beam_webhook",
1072
+ timestamp: Date.now(),
1073
+ beamEvent: {
1074
+ type: event.type,
1075
+ payload: event.payload
1076
+ }
1077
+ };
1078
+ return deliverEvent(envelope, handler, options);
1079
+ },
1080
+ clear() {
1081
+ state.checkouts = [];
1082
+ state.checkoutCounter = 0;
1083
+ state.sessionStatuses.clear();
1084
+ state.paymentLinks.clear();
1085
+ state.products = [];
1086
+ },
1087
+ get count() {
1088
+ return state.checkouts.length;
1089
+ }
1090
+ };
1091
+ }
1092
+
1093
+ // src/simulator/storage.ts
1094
+ import crypto6 from "crypto";
1095
+ function storageErr(error, status = 400) {
1096
+ return json({ error }, status);
1097
+ }
1098
+ function fileExtension(filename) {
1099
+ const dot = filename.lastIndexOf(".");
1100
+ return dot === -1 ? "bin" : filename.slice(dot + 1);
1101
+ }
1102
+ function buildKey(fileId, filename) {
1103
+ const orgId = process.env.ORGANIZATION_ID ?? TEST_ENV_DEFAULTS.ORGANIZATION_ID;
1104
+ const projectId = process.env.PROJECT_ID ?? TEST_ENV_DEFAULTS.PROJECT_ID;
1105
+ const deploymentId = process.env.DEPLOYMENT_ID ?? TEST_ENV_DEFAULTS.DEPLOYMENT_ID;
1106
+ return `${orgId}/${projectId}/${deploymentId}/${fileId}.${fileExtension(filename)}`;
1107
+ }
1108
+ function buildUrl(key) {
1109
+ return `${STORAGE_TEST_URL}/files/${key}`;
1110
+ }
1111
+ function toUploadResponse(record) {
1112
+ return {
1113
+ id: record.id,
1114
+ key: record.key,
1115
+ filename: record.filename,
1116
+ contentType: record.contentType,
1117
+ sizeBytes: record.sizeBytes,
1118
+ url: record.url,
1119
+ uploadedAt: record.uploadedAt,
1120
+ isPublic: record.isPublic,
1121
+ metadata: record.metadata
1122
+ };
1123
+ }
1124
+ function captureUpload(record, method, path) {
1125
+ const captured = {
1126
+ id: record.id,
1127
+ key: record.key,
1128
+ filename: record.filename,
1129
+ contentType: record.contentType,
1130
+ sizeBytes: record.sizeBytes,
1131
+ url: record.url,
1132
+ uploadedAt: record.uploadedAt,
1133
+ isPublic: record.isPublic,
1134
+ metadata: record.metadata,
1135
+ method,
1136
+ path
1137
+ };
1138
+ state.uploads.push(captured);
1139
+ }
1140
+ function createFileRecord(input) {
1141
+ state.uploadCounter += 1;
1142
+ const id = `file_test_${state.uploadCounter}`;
1143
+ const key = buildKey(id, input.filename);
1144
+ return {
1145
+ id,
1146
+ key,
1147
+ filename: input.filename,
1148
+ contentType: input.contentType,
1149
+ sizeBytes: input.sizeBytes,
1150
+ url: buildUrl(key),
1151
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
1152
+ metadata: input.metadata,
1153
+ isPublic: input.isPublic,
1154
+ deleted: false
1155
+ };
1156
+ }
1157
+ function activeFiles() {
1158
+ return [...state.storageFiles.values()].filter((f) => !f.deleted);
1159
+ }
1160
+ async function handleStorageRequest(request, url) {
1161
+ const pathname = url.pathname;
1162
+ if (pathname.startsWith("/upload/presign/multipart")) {
1163
+ return storageErr("Not found", 404);
1164
+ }
1165
+ const presignedPutMatch = pathname.match(/^\/presigned\/([^/]+)$/);
1166
+ if (presignedPutMatch && request.method === "PUT") {
1167
+ const fileId = presignedPutMatch[1];
1168
+ if (!state.presignPending.has(fileId)) {
1169
+ return storageErr("Presign session not found", 404);
1170
+ }
1171
+ return new Response(null, {
1172
+ status: 200,
1173
+ headers: { etag: `"${crypto6.randomUUID()}"` }
1174
+ });
1175
+ }
1176
+ if (pathname === "/upload/presign/complete" && request.method === "POST") {
1177
+ const body = await readJsonBody(request);
1178
+ const fileId = String(body.fileId ?? "");
1179
+ const pending = state.presignPending.get(fileId);
1180
+ if (!pending) return storageErr("Presign session not found", 404);
1181
+ const record = {
1182
+ id: fileId,
1183
+ key: pending.key,
1184
+ filename: pending.fileName,
1185
+ contentType: pending.contentType,
1186
+ sizeBytes: pending.sizeBytes,
1187
+ url: buildUrl(pending.key),
1188
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
1189
+ metadata: pending.metadata,
1190
+ isPublic: pending.isPublic,
1191
+ deleted: false
1192
+ };
1193
+ state.storageFiles.set(record.id, record);
1194
+ state.presignPending.delete(fileId);
1195
+ captureUpload(record, "POST", "/upload/presign/complete");
1196
+ return json(toUploadResponse(record));
1197
+ }
1198
+ if (pathname === "/upload/presign" && request.method === "POST") {
1199
+ const body = await readJsonBody(request);
1200
+ const fileName = String(body.fileName ?? "upload.bin");
1201
+ const contentType = String(body.contentType ?? "application/octet-stream");
1202
+ const sizeBytes = Number(body.sizeBytes ?? 0);
1203
+ const isPublic = body.isPublic !== false;
1204
+ const metadata = body.metadata;
1205
+ state.uploadCounter += 1;
1206
+ const fileId = `file_test_${state.uploadCounter}`;
1207
+ const key = buildKey(fileId, fileName);
1208
+ state.presignPending.set(fileId, {
1209
+ fileName,
1210
+ contentType,
1211
+ sizeBytes,
1212
+ isPublic,
1213
+ metadata,
1214
+ key
1215
+ });
1216
+ return json({
1217
+ fileId,
1218
+ key,
1219
+ presignedUrl: `${STORAGE_TEST_URL}/presigned/${fileId}`,
1220
+ contentType,
1221
+ expiresIn: 3600
1222
+ });
1223
+ }
1224
+ if (pathname === "/upload" && request.method === "POST") {
1225
+ const isPublic = url.searchParams.get("isPublic") !== "false";
1226
+ const formData = await request.formData();
1227
+ const file = formData.get("file");
1228
+ if (!(file instanceof Blob)) {
1229
+ return storageErr("file is required");
1230
+ }
1231
+ const filename = formData.get("filename") ?? (file instanceof File ? file.name : "upload.bin");
1232
+ const metadataRaw = formData.get("metadata");
1233
+ let metadata;
1234
+ if (typeof metadataRaw === "string" && metadataRaw) {
1235
+ try {
1236
+ metadata = JSON.parse(metadataRaw);
1237
+ } catch {
1238
+ return storageErr("metadata must be valid JSON");
1239
+ }
1240
+ }
1241
+ const record = createFileRecord({
1242
+ filename,
1243
+ contentType: file.type || "application/octet-stream",
1244
+ sizeBytes: file.size,
1245
+ isPublic,
1246
+ metadata
1247
+ });
1248
+ state.storageFiles.set(record.id, record);
1249
+ captureUpload(record, "POST", "/upload");
1250
+ return json(toUploadResponse(record));
1251
+ }
1252
+ const fileMatch = pathname.match(/^\/api\/files\/([^/]+)$/);
1253
+ if (fileMatch) {
1254
+ const fileId = fileMatch[1];
1255
+ const record = state.storageFiles.get(fileId);
1256
+ if (!record || record.deleted) {
1257
+ return storageErr("File not found", 404);
1258
+ }
1259
+ if (request.method === "GET") {
1260
+ return json(toUploadResponse(record));
1261
+ }
1262
+ if (request.method === "PATCH") {
1263
+ const body = await readJsonBody(request);
1264
+ if (body.fileName !== void 0) record.filename = String(body.fileName);
1265
+ if (body.metadata !== void 0) {
1266
+ record.metadata = body.metadata;
1267
+ }
1268
+ if (body.isPublic !== void 0) record.isPublic = body.isPublic === true;
1269
+ return json(toUploadResponse(record));
1270
+ }
1271
+ if (request.method === "DELETE") {
1272
+ record.deleted = true;
1273
+ return json({ success: true });
1274
+ }
1275
+ }
1276
+ if (pathname === "/api/files" && request.method === "GET") {
1277
+ const parseParam = (raw, fallback, min) => {
1278
+ const n = Number(raw);
1279
+ return Number.isFinite(n) && n >= min ? Math.floor(n) : fallback;
1280
+ };
1281
+ const limit = parseParam(url.searchParams.get("limit"), 50, 1);
1282
+ const offset = parseParam(url.searchParams.get("offset"), 0, 0);
1283
+ const prefix = url.searchParams.get("prefix") ?? "";
1284
+ let files = activeFiles();
1285
+ if (prefix) {
1286
+ files = files.filter((f) => f.key.startsWith(prefix) || f.filename.startsWith(prefix));
1287
+ }
1288
+ const total = files.length;
1289
+ const slice = files.slice(offset, offset + limit);
1290
+ return json({
1291
+ files: slice.map((f) => toUploadResponse(f)),
1292
+ total,
1293
+ limit,
1294
+ offset,
1295
+ hasMore: offset + slice.length < total
1296
+ });
1297
+ }
1298
+ return storageErr(`No storage simulator for ${request.method} ${pathname}`, 404);
1299
+ }
1300
+ function createStorage() {
1301
+ return {
1302
+ get uploads() {
1303
+ return [...state.uploads];
1304
+ },
1305
+ latest() {
1306
+ return state.uploads[state.uploads.length - 1];
1307
+ },
1308
+ clear() {
1309
+ state.uploads = [];
1310
+ state.uploadCounter = 0;
1311
+ state.storageFiles.clear();
1312
+ state.presignPending.clear();
1313
+ },
1314
+ get count() {
1315
+ return state.uploads.length;
1316
+ }
1317
+ };
1318
+ }
1319
+
1320
+ // src/simulator/messaging.ts
1321
+ import crypto7 from "crypto";
1322
+ function captureMessage(channel, recipient, body, connectionId) {
1323
+ const message = {
1324
+ channel,
1325
+ recipient,
1326
+ body: {
1327
+ text: body.text ? String(body.text) : void 0,
1328
+ blocks: body.blocks,
1329
+ threadTs: body.threadTs ? String(body.threadTs) : void 0,
1330
+ messagingType: body.messagingType ? String(body.messagingType) : void 0,
1331
+ tag: body.tag ? String(body.tag) : void 0
1332
+ },
1333
+ connectionId,
1334
+ sentAt: /* @__PURE__ */ new Date()
1335
+ };
1336
+ state.messages.push(message);
1337
+ }
1338
+ async function handleMessagingRequest(request, channel, subPath) {
1339
+ const method = request.method;
1340
+ if ((subPath === "" || subPath === "/" || subPath === "/connections") && method === "GET") {
1341
+ return success({ connections: [] });
1342
+ }
1343
+ if (channel === "slack" && subPath === "/send" && method === "POST") {
1344
+ const body = await readJsonBody(request);
1345
+ if (!body.text && !body.blocks) {
1346
+ return failure("text or blocks is required");
1347
+ }
1348
+ const slackChannel = String(body.channel ?? "");
1349
+ if (!slackChannel) {
1350
+ return failure("channel is required");
1351
+ }
1352
+ captureMessage(
1353
+ "slack",
1354
+ slackChannel,
1355
+ body,
1356
+ body.connectionId ? String(body.connectionId) : void 0
1357
+ );
1358
+ return success({
1359
+ ok: true,
1360
+ ts: `${Date.now()}.${crypto7.randomUUID().slice(0, 6)}`,
1361
+ channel: slackChannel
1362
+ });
1363
+ }
1364
+ if (channel === "line" && subPath === "/push" && method === "POST") {
1365
+ const body = await readJsonBody(request);
1366
+ const userId = String(body.userId ?? "");
1367
+ const message = body.message;
1368
+ if (!userId) return failure("userId is required");
1369
+ if (!message || message.type !== "text" || !message.text) {
1370
+ return failure("message must be { type: 'text', text: string }");
1371
+ }
1372
+ if (message.text.length > 5e3) {
1373
+ return failure("message text exceeds maximum length");
1374
+ }
1375
+ captureMessage(
1376
+ "line",
1377
+ userId,
1378
+ { text: message.text },
1379
+ body.connectionId ? String(body.connectionId) : void 0
1380
+ );
1381
+ return success({ success: true });
1382
+ }
1383
+ if (channel === "facebook" && subPath === "/send" && method === "POST") {
1384
+ const body = await readJsonBody(request);
1385
+ const recipientId = String(body.recipientId ?? "");
1386
+ const message = body.message;
1387
+ if (!recipientId) return failure("recipientId is required");
1388
+ if (!message || message.type !== "text" || !message.text) {
1389
+ return failure("message must be { type: 'text', text: string }");
1390
+ }
1391
+ if (message.text.length > 2e3) {
1392
+ return failure("message text exceeds maximum length");
1393
+ }
1394
+ captureMessage(
1395
+ "facebook",
1396
+ recipientId,
1397
+ {
1398
+ text: message.text,
1399
+ messagingType: body.messagingType ? String(body.messagingType) : void 0,
1400
+ tag: body.tag ? String(body.tag) : void 0
1401
+ },
1402
+ body.connectionId ? String(body.connectionId) : void 0
1403
+ );
1404
+ return success({ success: true });
1405
+ }
1406
+ return failure(`No messaging simulator for ${method} .../integrations/${channel}${subPath}`, 404);
1407
+ }
1408
+ function createMessages() {
1409
+ return {
1410
+ all() {
1411
+ return [...state.messages];
1412
+ },
1413
+ latest() {
1414
+ return state.messages[state.messages.length - 1];
1415
+ },
1416
+ to(recipient) {
1417
+ return state.messages.filter((m) => m.recipient === recipient);
1418
+ },
1419
+ channel(kind) {
1420
+ const filtered = () => state.messages.filter((m) => m.channel === kind);
1421
+ return {
1422
+ all: () => filtered(),
1423
+ latest: () => {
1424
+ const pool = filtered();
1425
+ return pool[pool.length - 1];
1426
+ },
1427
+ to: (recipient) => filtered().filter((m) => m.recipient === recipient)
1428
+ };
1429
+ },
1430
+ clear() {
1431
+ state.messages = [];
1432
+ },
1433
+ get count() {
1434
+ return state.messages.length;
1435
+ }
1436
+ };
1437
+ }
1438
+
1439
+ // src/simulator/edge.ts
1440
+ function edgeFailure(error, status = 400, code) {
1441
+ return json({ success: false, error, ...code ? { code } : {} }, status);
1442
+ }
1443
+ function bindingNotFound(alias) {
1444
+ return edgeFailure(`No peripheral is paired to alias "${alias}"`, 404, "BINDING_NOT_FOUND");
1445
+ }
1446
+ function now2() {
1447
+ return (/* @__PURE__ */ new Date()).toISOString();
1448
+ }
1449
+ function nextJobId() {
1450
+ state.edgePrintCounter += 1;
1451
+ return `job_${state.edgePrintCounter}`;
1452
+ }
1453
+ function nextConfigVersion() {
1454
+ return state.edgeDisplays.length + 1;
1455
+ }
1456
+ function findPeripheral(id) {
1457
+ return state.edgePeripherals.find((p) => p.id === id);
1458
+ }
1459
+ function buildBinding(alias, peripheralId) {
1460
+ const peripheral = findPeripheral(peripheralId);
1461
+ return {
1462
+ alias,
1463
+ state: peripheral ? "ok" : "peripheral_missing",
1464
+ peripheral: peripheral ? {
1465
+ id: peripheral.id,
1466
+ displayName: peripheral.displayName,
1467
+ driver: peripheral.driver,
1468
+ connected: peripheral.connected
1469
+ } : null,
1470
+ device: peripheral ? {
1471
+ id: peripheral.device.id,
1472
+ displayName: peripheral.device.displayName,
1473
+ status: peripheral.device.status
1474
+ } : null,
1475
+ updatedAt: now2()
1476
+ };
1477
+ }
1478
+ function handleListDevices() {
1479
+ return success({ devices: [...state.edgeDevices] });
1480
+ }
1481
+ function handleListPeripherals(request) {
1482
+ const deviceId = new URL(request.url).searchParams.get("deviceId");
1483
+ const peripherals = deviceId ? state.edgePeripherals.filter((p) => p.device.id === deviceId) : [...state.edgePeripherals];
1484
+ return success({ peripherals });
1485
+ }
1486
+ function handleListBindings() {
1487
+ return success({ bindings: [...state.edgeBindings.values()] });
1488
+ }
1489
+ function handleGetBinding(alias) {
1490
+ const binding = state.edgeBindings.get(alias);
1491
+ if (!binding) return bindingNotFound(alias);
1492
+ return success({ binding });
1493
+ }
1494
+ async function handlePair(request) {
1495
+ const body = await readJsonBody(request);
1496
+ const alias = String(body.alias ?? "");
1497
+ const peripheralId = String(body.peripheralId ?? "");
1498
+ if (!alias) return edgeFailure("alias is required");
1499
+ if (!peripheralId) return edgeFailure("peripheralId is required");
1500
+ if (!findPeripheral(peripheralId)) {
1501
+ return edgeFailure("Peripheral not found or its device is not granted to this project", 404);
1502
+ }
1503
+ const binding = buildBinding(alias, peripheralId);
1504
+ state.edgeBindings.set(alias, binding);
1505
+ return success({ binding });
1506
+ }
1507
+ function handleUnpair(alias) {
1508
+ const existed = state.edgeBindings.delete(alias);
1509
+ if (!existed) return bindingNotFound(alias);
1510
+ return success({ deleted: true });
1511
+ }
1512
+ async function handlePrint(request) {
1513
+ const body = await readJsonBody(request);
1514
+ const jobId = nextJobId();
1515
+ const captured = {
1516
+ jobId,
1517
+ deploymentId: String(body.deploymentId ?? ""),
1518
+ alias: body.alias ? String(body.alias) : void 0,
1519
+ deviceId: body.deviceId ? String(body.deviceId) : void 0,
1520
+ peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
1521
+ receipt: body.receipt ?? {},
1522
+ openDrawer: body.openDrawer === true,
1523
+ logo: body.logo === true ? true : body.logo === false ? false : void 0,
1524
+ copies: typeof body.copies === "number" ? body.copies : 1
1525
+ };
1526
+ state.edgePrints.push(captured);
1527
+ return success({
1528
+ jobId,
1529
+ status: "completed"
1530
+ });
1531
+ }
1532
+ async function handleShowDisplay(request) {
1533
+ const body = await readJsonBody(request);
1534
+ const captured = {
1535
+ action: "show",
1536
+ alias: body.alias ? String(body.alias) : void 0,
1537
+ peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
1538
+ url: body.url ? String(body.url) : void 0
1539
+ };
1540
+ state.edgeDisplays.push(captured);
1541
+ return success({
1542
+ configVersion: nextConfigVersion(),
1543
+ pushed: true,
1544
+ state: "showing"
1545
+ });
1546
+ }
1547
+ function handleClearDisplay(request) {
1548
+ const url = new URL(request.url);
1549
+ const alias = url.searchParams.get("alias");
1550
+ const peripheralId = url.searchParams.get("peripheralId");
1551
+ const captured = {
1552
+ action: "clear",
1553
+ alias: alias ?? void 0,
1554
+ peripheralId: peripheralId ?? void 0
1555
+ };
1556
+ state.edgeDisplays.push(captured);
1557
+ return success({
1558
+ configVersion: nextConfigVersion(),
1559
+ pushed: true,
1560
+ state: "cleared"
1561
+ });
1562
+ }
1563
+ async function handleTestPrint(request) {
1564
+ const body = await readJsonBody(request);
1565
+ const captured = {
1566
+ alias: body.alias ? String(body.alias) : void 0,
1567
+ deviceId: body.deviceId ? String(body.deviceId) : void 0,
1568
+ peripheralId: body.peripheralId ? String(body.peripheralId) : void 0
1569
+ };
1570
+ state.edgeTestPrints.push(captured);
1571
+ return success({
1572
+ status: "ok",
1573
+ peripheralId: captured.peripheralId ?? captured.alias ?? "default"
1574
+ });
1575
+ }
1576
+ async function handleEdgeRequest(request, subPath) {
1577
+ const method = request.method;
1578
+ if (subPath === "/print" && method === "POST") {
1579
+ return handlePrint(request);
1580
+ }
1581
+ if (subPath === "/devices" && method === "GET") {
1582
+ return handleListDevices();
1583
+ }
1584
+ if (subPath === "/peripherals" && method === "GET") {
1585
+ return handleListPeripherals(request);
1586
+ }
1587
+ if (subPath === "/bindings" && method === "GET") {
1588
+ return handleListBindings();
1589
+ }
1590
+ if (subPath === "/bindings" && method === "PUT") {
1591
+ return handlePair(request);
1592
+ }
1593
+ const bindingMatch = subPath.match(/^\/bindings\/([^/]+)$/);
1594
+ if (bindingMatch) {
1595
+ const alias = decodeURIComponent(bindingMatch[1]);
1596
+ if (method === "GET") return handleGetBinding(alias);
1597
+ if (method === "DELETE") return handleUnpair(alias);
1598
+ }
1599
+ if (subPath === "/display" && method === "POST") {
1600
+ return handleShowDisplay(request);
1601
+ }
1602
+ if (subPath === "/display" && method === "DELETE") {
1603
+ return handleClearDisplay(request);
1604
+ }
1605
+ if (subPath === "/test-print" && method === "POST") {
1606
+ return handleTestPrint(request);
1607
+ }
1608
+ return edgeFailure(`No edge simulator for ${method} .../edge${subPath}`, 404);
1609
+ }
1610
+ function createEdge() {
1611
+ return {
1612
+ get prints() {
1613
+ return [...state.edgePrints];
1614
+ },
1615
+ get displays() {
1616
+ return [...state.edgeDisplays];
1617
+ },
1618
+ get testPrints() {
1619
+ return [...state.edgeTestPrints];
1620
+ },
1621
+ latestPrint() {
1622
+ return state.edgePrints[state.edgePrints.length - 1];
1623
+ },
1624
+ latestDisplay() {
1625
+ return state.edgeDisplays[state.edgeDisplays.length - 1];
1626
+ },
1627
+ get bindings() {
1628
+ return [...state.edgeBindings.values()];
1629
+ },
1630
+ seedDevices(devices) {
1631
+ state.edgeDevices = [...devices];
1632
+ },
1633
+ seedPeripherals(peripherals) {
1634
+ state.edgePeripherals = [...peripherals];
1635
+ },
1636
+ seedBindings(bindings) {
1637
+ state.edgeBindings.clear();
1638
+ for (const binding of bindings) {
1639
+ state.edgeBindings.set(binding.alias, binding);
1640
+ }
1641
+ },
1642
+ clear() {
1643
+ state.edgePrints = [];
1644
+ state.edgeDisplays = [];
1645
+ state.edgeTestPrints = [];
1646
+ state.edgePrintCounter = 0;
1647
+ state.edgeDevices = [];
1648
+ state.edgePeripherals = [];
1649
+ state.edgeBindings.clear();
1650
+ },
1651
+ get count() {
1652
+ return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length;
1653
+ }
1654
+ };
1655
+ }
1656
+
839
1657
  // src/simulator/router.ts
840
1658
  var fetchHolder = globalSingleton("fetch-holder", () => ({
841
1659
  originalFetch: null
@@ -843,6 +1661,22 @@ var fetchHolder = globalSingleton("fetch-holder", () => ({
843
1661
  function isLocalHost(hostname) {
844
1662
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0";
845
1663
  }
1664
+ function requiresDeploymentHmac(request, url) {
1665
+ const isStorageHost = url.hostname === STORAGE_TEST_HOST;
1666
+ const isPresignedPut = isStorageHost && request.method === "PUT" && /^\/presigned\/[^/]+$/.test(url.pathname);
1667
+ if (isStorageHost) {
1668
+ return !isPresignedPut;
1669
+ }
1670
+ const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
1671
+ const isEmail = url.pathname === "/api/email/send";
1672
+ const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
1673
+ const storeMatch = url.pathname.match(/^\/api\/store\//);
1674
+ const messagingMatch = url.pathname.match(
1675
+ /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
1676
+ );
1677
+ const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
1678
+ return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
1679
+ }
846
1680
  async function handleSimulatedRequest(request, url) {
847
1681
  if (url.pathname === "/sql") {
848
1682
  return handleNeonSql(requireDb(), request);
@@ -854,7 +1688,13 @@ async function handleSimulatedRequest(request, url) {
854
1688
  const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
855
1689
  const isEmail = url.pathname === "/api/email/send";
856
1690
  const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
857
- if (dataStoreMatch || isEmail || identitiesMatch) {
1691
+ const storeMatch = url.pathname.match(/^\/api\/store\//);
1692
+ const messagingMatch = url.pathname.match(
1693
+ /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
1694
+ );
1695
+ const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
1696
+ const isStorageHost = url.hostname === STORAGE_TEST_HOST;
1697
+ if (requiresDeploymentHmac(request, url)) {
858
1698
  const authHeader = request.headers.get("X-Stardeck-Auth");
859
1699
  if (!authHeader) {
860
1700
  return failure("Missing authentication header", 401);
@@ -864,34 +1704,47 @@ async function handleSimulatedRequest(request, url) {
864
1704
  return failure("Invalid authentication", 401);
865
1705
  }
866
1706
  }
1707
+ if (isStorageHost) {
1708
+ return handleStorageRequest(request, url);
1709
+ }
867
1710
  if (isEmail && request.method === "POST") {
868
1711
  return handleEmailSend(request);
869
1712
  }
870
1713
  if (identitiesMatch) {
871
1714
  return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
872
1715
  }
1716
+ if (storeMatch) {
1717
+ return handlePaymentsRequest(request, url);
1718
+ }
1719
+ if (messagingMatch) {
1720
+ const channel = messagingMatch[1];
1721
+ return handleMessagingRequest(request, channel, messagingMatch[2] ?? "");
1722
+ }
1723
+ if (edgeMatch) {
1724
+ return handleEdgeRequest(request, edgeMatch[1] ?? "");
1725
+ }
873
1726
  if (dataStoreMatch) {
874
1727
  const subPath = dataStoreMatch[1] ?? "";
875
1728
  const db = requireDb();
876
- const readBody2 = async () => await request.json();
1729
+ const readBody = async () => await request.json();
877
1730
  if (subPath === "/query" && request.method === "POST") {
878
- return handleQuery(db, await readBody2());
1731
+ return handleQuery(db, await readBody());
879
1732
  }
880
1733
  if (subPath === "/mutate" && request.method === "POST") {
881
- return handleMutate(db, await readBody2());
1734
+ return handleMutate(db, await readBody());
882
1735
  }
883
1736
  if (subPath === "/schema" && request.method === "GET") {
884
1737
  return handleGetSchema(db);
885
1738
  }
886
1739
  if (subPath === "/schema/tables" && request.method === "POST") {
887
- return handleCreateTable(db, await readBody2());
1740
+ return handleCreateTable(db, await readBody());
888
1741
  }
889
1742
  if (subPath === "/schema/columns" && request.method === "POST") {
890
- return handleAddColumn(db, await readBody2());
1743
+ return handleAddColumn(db, await readBody());
891
1744
  }
892
1745
  }
893
1746
  return failure(
894
- `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, auth verify/refresh, Neon /sql.`,
1747
+ `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
895
1748
  404
896
1749
  );
897
1750
  }
@@ -977,10 +1830,18 @@ async function createTestApp(options = {}) {
977
1830
  }
978
1831
  const inbox = createInbox();
979
1832
  const directory = createDirectory();
1833
+ const payments = createPayments();
1834
+ const storage = createStorage();
1835
+ const messages = createMessages();
1836
+ const edge = createEdge();
980
1837
  const app = {
981
1838
  db,
982
1839
  inbox,
983
1840
  identities: directory,
1841
+ payments,
1842
+ storage,
1843
+ messages,
1844
+ edge,
984
1845
  async query(sql, params = []) {
985
1846
  const result = await db.query(sql, params);
986
1847
  return result.rows;
@@ -996,8 +1857,8 @@ async function createTestApp(options = {}) {
996
1857
  issueSession(user) {
997
1858
  const fullUser = buildUser(user);
998
1859
  const tokens = {
999
- accessToken: `test-access-${crypto4.randomUUID()}`,
1000
- refreshToken: `test-refresh-${crypto4.randomUUID()}`
1860
+ accessToken: `test-access-${crypto8.randomUUID()}`,
1861
+ refreshToken: `test-refresh-${crypto8.randomUUID()}`
1001
1862
  };
1002
1863
  state.sessions.set(tokens.accessToken, fullUser);
1003
1864
  state.refreshSessions.set(tokens.refreshToken, fullUser);
@@ -1013,7 +1874,10 @@ async function createTestApp(options = {}) {
1013
1874
  state.emailCounter = 0;
1014
1875
  state.identities.clear();
1015
1876
  state.identityLinks = [];
1016
- state.identityMemory = [];
1877
+ payments.clear();
1878
+ storage.clear();
1879
+ messages.clear();
1880
+ edge.clear();
1017
1881
  },
1018
1882
  async close() {
1019
1883
  state.db = null;
@@ -1021,9 +1885,13 @@ async function createTestApp(options = {}) {
1021
1885
  state.sessions.clear();
1022
1886
  state.refreshSessions.clear();
1023
1887
  state.emails = [];
1888
+ state.emailCounter = 0;
1024
1889
  state.identities.clear();
1025
1890
  state.identityLinks = [];
1026
- state.identityMemory = [];
1891
+ payments.clear();
1892
+ storage.clear();
1893
+ messages.clear();
1894
+ edge.clear();
1027
1895
  uninstallFetchRouter();
1028
1896
  await db.close();
1029
1897
  }
@@ -1031,122 +1899,6 @@ async function createTestApp(options = {}) {
1031
1899
  return app;
1032
1900
  }
1033
1901
 
1034
- // src/module-app.ts
1035
- import {
1036
- makeSqlPort,
1037
- renderSchemaOpsToSql
1038
- } from "@stardeck-customer-apps/core";
1039
- async function createModuleApp(options) {
1040
- const schemaSql = options.modules.map((m) => renderSchemaOpsToSql(m.schema)).join("\n\n");
1041
- const app = await createTestApp({ schemaSql, allowNetwork: options.allowNetwork });
1042
- const sql = { query: (text, params) => app.db.query(text, params ?? []) };
1043
- const data = makeSqlPort(sql);
1044
- const { createIntegrationsClient } = await import("@stardeck-customer-apps/integrations-sdk");
1045
- const identities = createIntegrationsClient({
1046
- controlPlaneUrl: TEST_ENV_DEFAULTS.CONTROL_PLANE_URL,
1047
- organizationId: TEST_ENV_DEFAULTS.ORGANIZATION_ID,
1048
- projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
1049
- deploymentId: TEST_ENV_DEFAULTS.DEPLOYMENT_ID,
1050
- deploymentSecret: TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET
1051
- }).identities;
1052
- const runSeed = async () => {
1053
- if (options.seed) await options.seed({ data, identities });
1054
- };
1055
- await runSeed();
1056
- return {
1057
- app,
1058
- data,
1059
- identities,
1060
- async reset() {
1061
- await app.reset();
1062
- await runSeed();
1063
- },
1064
- async close() {
1065
- await app.close();
1066
- }
1067
- };
1068
- }
1069
-
1070
- // src/next/headers-shim.ts
1071
- import { AsyncLocalStorage } from "async_hooks";
1072
- var requestScopeStorage = globalSingleton(
1073
- "request-scope",
1074
- () => new AsyncLocalStorage()
1075
- );
1076
-
1077
- // src/next/call-route.ts
1078
- function parseCookieHeader(header) {
1079
- const map = /* @__PURE__ */ new Map();
1080
- if (!header) return map;
1081
- for (const part of header.split(";")) {
1082
- const eq = part.indexOf("=");
1083
- if (eq === -1) continue;
1084
- map.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
1085
- }
1086
- return map;
1087
- }
1088
- async function importNextServer() {
1089
- try {
1090
- return await import("next/server.js");
1091
- } catch {
1092
- return await import("next/server");
1093
- }
1094
- }
1095
- async function callRoute(handler, options = {}) {
1096
- const { NextRequest } = await importNextServer();
1097
- const path = options.path ?? "/api/test-route";
1098
- const url = new URL(`http://localhost:3333${path}`);
1099
- for (const [key, value] of Object.entries(options.searchParams ?? {})) {
1100
- url.searchParams.set(key, value);
1101
- }
1102
- const method = options.method ?? (options.body !== void 0 ? "POST" : "GET");
1103
- const headers = new Headers(options.headers);
1104
- const user = options.user !== void 0 ? options.user : state.currentUser;
1105
- if (user && !headers.has("x-stardeck-user")) {
1106
- headers.set("x-stardeck-user", JSON.stringify(user));
1107
- }
1108
- if (options.body !== void 0 && !headers.has("Content-Type")) {
1109
- headers.set("Content-Type", "application/json");
1110
- }
1111
- const cookiePairs = Object.entries(options.cookies ?? {});
1112
- if (cookiePairs.length > 0) {
1113
- const existing = headers.get("cookie");
1114
- const cookieString = cookiePairs.map(([k, v]) => `${k}=${v}`).join("; ");
1115
- headers.set("cookie", existing ? `${existing}; ${cookieString}` : cookieString);
1116
- }
1117
- const request = new NextRequest(url, {
1118
- method,
1119
- headers,
1120
- body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
1121
- });
1122
- const scope = {
1123
- headers,
1124
- cookies: parseCookieHeader(headers.get("cookie"))
1125
- };
1126
- try {
1127
- return await requestScopeStorage.run(
1128
- scope,
1129
- () => Promise.resolve(
1130
- handler(request, {
1131
- params: Promise.resolve(options.params ?? {})
1132
- })
1133
- )
1134
- );
1135
- } catch (error) {
1136
- const redirect = decodeNextRedirect(error);
1137
- if (redirect) return redirect;
1138
- throw error;
1139
- }
1140
- }
1141
- function decodeNextRedirect(error) {
1142
- const digest = error?.digest;
1143
- if (typeof digest !== "string" || !digest.startsWith("NEXT_REDIRECT")) return null;
1144
- const parts = digest.split(";");
1145
- const location = parts[2] ?? "/";
1146
- const status = Number(parts[3]) || 307;
1147
- return new Response(null, { status, headers: { location } });
1148
- }
1149
-
1150
1902
  // src/workflow.ts
1151
1903
  import { describe } from "vitest";
1152
1904
  function describeWorkflow(name, fn) {
@@ -1160,10 +1912,11 @@ export {
1160
1912
  CONTROL_PLANE_TEST_URL,
1161
1913
  DATA_STORE_TEST_HOST,
1162
1914
  DEFAULT_SCHEMA_PATH,
1915
+ STORAGE_TEST_HOST,
1916
+ STORAGE_TEST_URL,
1163
1917
  TEST_ENV_DEFAULTS,
1164
1918
  WORKFLOW_NAME_PREFIX,
1165
1919
  callRoute,
1166
- createModuleApp,
1167
1920
  createTestApp,
1168
1921
  describeWorkflow,
1169
1922
  parseWorkflowName