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