@stardeck-customer-apps/testing 0.3.0 → 0.4.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.
@@ -46,6 +46,16 @@ var state = globalSingleton("state", () => ({
46
46
  emailCounter: 0,
47
47
  identities: /* @__PURE__ */ new Map(),
48
48
  identityLinks: [],
49
+ checkouts: [],
50
+ checkoutCounter: 0,
51
+ sessionStatuses: /* @__PURE__ */ new Map(),
52
+ paymentLinks: /* @__PURE__ */ new Map(),
53
+ products: [],
54
+ uploads: [],
55
+ uploadCounter: 0,
56
+ storageFiles: /* @__PURE__ */ new Map(),
57
+ presignPending: /* @__PURE__ */ new Map(),
58
+ messages: [],
49
59
  allowNetwork: false
50
60
  }));
51
61
 
@@ -19,6 +19,16 @@ var state = globalSingleton("state", () => ({
19
19
  emailCounter: 0,
20
20
  identities: /* @__PURE__ */ new Map(),
21
21
  identityLinks: [],
22
+ checkouts: [],
23
+ checkoutCounter: 0,
24
+ sessionStatuses: /* @__PURE__ */ new Map(),
25
+ paymentLinks: /* @__PURE__ */ new Map(),
26
+ products: [],
27
+ uploads: [],
28
+ uploadCounter: 0,
29
+ storageFiles: /* @__PURE__ */ new Map(),
30
+ presignPending: /* @__PURE__ */ new Map(),
31
+ messages: [],
22
32
  allowNetwork: false
23
33
  }));
24
34
 
package/dist/setup.js CHANGED
@@ -40,6 +40,16 @@ var state = globalSingleton("state", () => ({
40
40
  emailCounter: 0,
41
41
  identities: /* @__PURE__ */ new Map(),
42
42
  identityLinks: [],
43
+ checkouts: [],
44
+ checkoutCounter: 0,
45
+ sessionStatuses: /* @__PURE__ */ new Map(),
46
+ paymentLinks: /* @__PURE__ */ new Map(),
47
+ products: [],
48
+ uploads: [],
49
+ uploadCounter: 0,
50
+ storageFiles: /* @__PURE__ */ new Map(),
51
+ presignPending: /* @__PURE__ */ new Map(),
52
+ messages: [],
43
53
  allowNetwork: false
44
54
  }));
45
55
  function requireDb() {
@@ -55,13 +65,16 @@ function requireDb() {
55
65
  var TEST_DOMAIN_SUFFIX = ".stardeck.test";
56
66
  var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
57
67
  var DATA_STORE_TEST_HOST = "db.stardeck.test";
68
+ var STORAGE_TEST_URL = "https://storage.stardeck.test";
69
+ var STORAGE_TEST_HOST = "storage.stardeck.test";
58
70
  var TEST_ENV_DEFAULTS = {
59
71
  CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
60
72
  DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
61
73
  ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
62
74
  PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
63
75
  DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
64
- DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`
76
+ DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`,
77
+ STORAGE_URL: STORAGE_TEST_URL
65
78
  };
66
79
 
67
80
  // src/simulator/hmac.ts
@@ -109,6 +122,13 @@ function success(data) {
109
122
  function failure(error, status = 400) {
110
123
  return json({ success: false, error }, status);
111
124
  }
125
+ async function readJsonBody(request) {
126
+ try {
127
+ return await request.json();
128
+ } catch {
129
+ return {};
130
+ }
131
+ }
112
132
 
113
133
  // src/simulator/data-store.ts
114
134
  function quoteIdent(name) {
@@ -589,13 +609,6 @@ function now() {
589
609
  function linksFor(identityId) {
590
610
  return state.identityLinks.filter((l) => l.identityId === identityId);
591
611
  }
592
- async function readBody(request) {
593
- try {
594
- return await request.json();
595
- } catch {
596
- return {};
597
- }
598
- }
599
612
  function handleList(request) {
600
613
  const typeParam = new URL(request.url).searchParams.get("type");
601
614
  const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
@@ -603,7 +616,7 @@ function handleList(request) {
603
616
  return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
604
617
  }
605
618
  async function handleCreate(request) {
606
- const body = await readBody(request);
619
+ const body = await readJsonBody(request);
607
620
  const type = body.type;
608
621
  if (type !== "person" && type !== "account") {
609
622
  return failure("type must be 'person' or 'account'");
@@ -640,7 +653,7 @@ function handleGet(identityId) {
640
653
  async function handleUpdate(identityId, request) {
641
654
  const identity = state.identities.get(identityId);
642
655
  if (!identity) return failure("identity not found", 404);
643
- const body = await readBody(request);
656
+ const body = await readJsonBody(request);
644
657
  if (body.displayName !== void 0) {
645
658
  identity.displayName = body.displayName;
646
659
  }
@@ -662,7 +675,7 @@ async function handleAttachLink(identityId, request) {
662
675
  if (identity.status !== "active") {
663
676
  return failure("links attach only to active persons");
664
677
  }
665
- const body = await readBody(request);
678
+ const body = await readJsonBody(request);
666
679
  const kind = body.kind;
667
680
  const externalId = body.externalId;
668
681
  if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
@@ -705,6 +718,447 @@ async function handleIdentitiesRequest(request, subPath) {
705
718
  return failure(`No identities simulator for ${method} .../identities${subPath}`, 404);
706
719
  }
707
720
 
721
+ // src/simulator/payments.ts
722
+ var import_node_crypto5 = __toESM(require("crypto"));
723
+
724
+ // src/next/headers-shim.ts
725
+ var import_node_async_hooks = require("async_hooks");
726
+ var requestScopeStorage = globalSingleton(
727
+ "request-scope",
728
+ () => new import_node_async_hooks.AsyncLocalStorage()
729
+ );
730
+
731
+ // src/simulator/webhook-signing.ts
732
+ var import_node_crypto4 = __toESM(require("crypto"));
733
+
734
+ // src/simulator/payments.ts
735
+ function payErr(error, status = 400, code) {
736
+ return json(code ? { error, code } : { error }, status);
737
+ }
738
+ function nextCheckoutId() {
739
+ state.checkoutCounter += 1;
740
+ return `cs_test_${state.checkoutCounter}`;
741
+ }
742
+ function nextPaymentLinkId() {
743
+ state.checkoutCounter += 1;
744
+ return `plink_test_${state.checkoutCounter}`;
745
+ }
746
+ function seedStripeSession(id, body) {
747
+ const lineItems = body.lineItems ?? [];
748
+ let amountTotal = null;
749
+ let currency = null;
750
+ if (lineItems.length > 0) {
751
+ amountTotal = lineItems.reduce(
752
+ (sum, item) => sum + (item.priceData?.unitAmount ?? 0) * (item.quantity ?? 1),
753
+ 0
754
+ );
755
+ currency = lineItems[0].priceData?.currency ?? null;
756
+ }
757
+ state.sessionStatuses.set(id, {
758
+ id,
759
+ status: "open",
760
+ paymentStatus: "unpaid",
761
+ mode: body.mode ?? "payment",
762
+ amountTotal,
763
+ currency,
764
+ customerEmail: body.customerEmail ? String(body.customerEmail) : null,
765
+ metadata: body.metadata ?? {},
766
+ expiresAt: Math.floor(Date.now() / 1e3) + 3600
767
+ });
768
+ }
769
+ function seedBeamLink(id, body, merchantId) {
770
+ const order = body.order;
771
+ state.paymentLinks.set(id, {
772
+ paymentLinkId: id,
773
+ merchantId,
774
+ url: `https://beam.test/pay/${id}`,
775
+ status: "ACTIVE",
776
+ order: {
777
+ netAmount: Number(order?.netAmount ?? 0),
778
+ currency: String(order?.currency ?? "THB"),
779
+ description: String(order?.description ?? ""),
780
+ referenceId: order?.referenceId ? String(order.referenceId) : void 0,
781
+ internalNote: order?.internalNote ? String(order.internalNote) : void 0,
782
+ orderItems: order?.orderItems
783
+ },
784
+ redirectUrl: body.redirectUrl ? String(body.redirectUrl) : void 0,
785
+ linkSettings: body.linkSettings,
786
+ collectDeliveryAddress: body.collectDeliveryAddress === true
787
+ });
788
+ }
789
+ async function handlePaymentsRequest(request, url) {
790
+ const pathname = url.pathname;
791
+ if (/\/bolt-connections/.test(pathname) || /\/bolt-intents/.test(pathname) || /\/charges(\/|$)/.test(pathname) || /\/billing-portal$/.test(pathname)) {
792
+ return payErr("Not found", 404, "NOT_FOUND");
793
+ }
794
+ const beamProductsMatch = pathname.match(
795
+ /^\/api\/store\/beam\/([^/]+)\/payment-links(?:\/([^/]+))?$/
796
+ );
797
+ if (beamProductsMatch) {
798
+ const merchantId = beamProductsMatch[1];
799
+ const linkId = beamProductsMatch[2];
800
+ if (!linkId && request.method === "POST") {
801
+ const body = await readJsonBody(request);
802
+ const id = nextPaymentLinkId();
803
+ const checkoutUrl = `https://beam.test/pay/${id}`;
804
+ const captured = {
805
+ id,
806
+ url: checkoutUrl,
807
+ provider: "beam",
808
+ options: body,
809
+ metadata: body.metadata,
810
+ createdAt: /* @__PURE__ */ new Date()
811
+ };
812
+ state.checkouts.push(captured);
813
+ seedBeamLink(id, body, merchantId);
814
+ return json({ id, url: checkoutUrl });
815
+ }
816
+ if (linkId && request.method === "GET") {
817
+ const link = state.paymentLinks.get(linkId);
818
+ if (!link) return payErr("Payment link not found", 404, "NOT_FOUND");
819
+ return json({ paymentLink: link });
820
+ }
821
+ }
822
+ const stripeStoreMatch = pathname.match(/^\/api\/store\/([^/]+)\/(.+)$/);
823
+ if (stripeStoreMatch) {
824
+ const accountId = stripeStoreMatch[1];
825
+ const subPath = stripeStoreMatch[2];
826
+ if (accountId === "beam") {
827
+ return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
828
+ }
829
+ if (subPath === "products" && request.method === "GET") {
830
+ return json({ products: state.products });
831
+ }
832
+ const productMatch = subPath.match(/^products\/([^/]+)$/);
833
+ if (productMatch && request.method === "GET") {
834
+ const product = state.products.find((p) => p.id === productMatch[1]);
835
+ if (!product) return payErr("Product not found", 404, "NOT_FOUND");
836
+ return json({ product });
837
+ }
838
+ if (subPath === "checkout" && request.method === "POST") {
839
+ const body = await readJsonBody(request);
840
+ const id = nextCheckoutId();
841
+ const checkoutUrl = `https://checkout.stripe.test/c/pay/${id}`;
842
+ const captured = {
843
+ id,
844
+ url: checkoutUrl,
845
+ provider: "stripe",
846
+ options: body,
847
+ mode: body.mode,
848
+ metadata: body.metadata,
849
+ createdAt: /* @__PURE__ */ new Date()
850
+ };
851
+ state.checkouts.push(captured);
852
+ seedStripeSession(id, body);
853
+ return json({ id, url: checkoutUrl });
854
+ }
855
+ const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
856
+ if (sessionMatch && request.method === "GET") {
857
+ const session = state.sessionStatuses.get(sessionMatch[1]);
858
+ if (!session) return payErr("Checkout session not found", 404, "NOT_FOUND");
859
+ return json({ session });
860
+ }
861
+ }
862
+ return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
863
+ }
864
+
865
+ // src/simulator/storage.ts
866
+ var import_node_crypto6 = __toESM(require("crypto"));
867
+ function storageErr(error, status = 400) {
868
+ return json({ error }, status);
869
+ }
870
+ function fileExtension(filename) {
871
+ const dot = filename.lastIndexOf(".");
872
+ return dot === -1 ? "bin" : filename.slice(dot + 1);
873
+ }
874
+ function buildKey(fileId, filename) {
875
+ const orgId = process.env.ORGANIZATION_ID ?? TEST_ENV_DEFAULTS.ORGANIZATION_ID;
876
+ const projectId = process.env.PROJECT_ID ?? TEST_ENV_DEFAULTS.PROJECT_ID;
877
+ const deploymentId = process.env.DEPLOYMENT_ID ?? TEST_ENV_DEFAULTS.DEPLOYMENT_ID;
878
+ return `${orgId}/${projectId}/${deploymentId}/${fileId}.${fileExtension(filename)}`;
879
+ }
880
+ function buildUrl(key) {
881
+ return `${STORAGE_TEST_URL}/files/${key}`;
882
+ }
883
+ function toUploadResponse(record) {
884
+ return {
885
+ id: record.id,
886
+ key: record.key,
887
+ filename: record.filename,
888
+ contentType: record.contentType,
889
+ sizeBytes: record.sizeBytes,
890
+ url: record.url,
891
+ uploadedAt: record.uploadedAt,
892
+ isPublic: record.isPublic,
893
+ metadata: record.metadata
894
+ };
895
+ }
896
+ function captureUpload(record, method, path) {
897
+ const captured = {
898
+ id: record.id,
899
+ key: record.key,
900
+ filename: record.filename,
901
+ contentType: record.contentType,
902
+ sizeBytes: record.sizeBytes,
903
+ url: record.url,
904
+ uploadedAt: record.uploadedAt,
905
+ isPublic: record.isPublic,
906
+ metadata: record.metadata,
907
+ method,
908
+ path
909
+ };
910
+ state.uploads.push(captured);
911
+ }
912
+ function createFileRecord(input) {
913
+ state.uploadCounter += 1;
914
+ const id = `file_test_${state.uploadCounter}`;
915
+ const key = buildKey(id, input.filename);
916
+ return {
917
+ id,
918
+ key,
919
+ filename: input.filename,
920
+ contentType: input.contentType,
921
+ sizeBytes: input.sizeBytes,
922
+ url: buildUrl(key),
923
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
924
+ metadata: input.metadata,
925
+ isPublic: input.isPublic,
926
+ deleted: false
927
+ };
928
+ }
929
+ function activeFiles() {
930
+ return [...state.storageFiles.values()].filter((f) => !f.deleted);
931
+ }
932
+ async function handleStorageRequest(request, url) {
933
+ const pathname = url.pathname;
934
+ if (pathname.startsWith("/upload/presign/multipart")) {
935
+ return storageErr("Not found", 404);
936
+ }
937
+ const presignedPutMatch = pathname.match(/^\/presigned\/([^/]+)$/);
938
+ if (presignedPutMatch && request.method === "PUT") {
939
+ const fileId = presignedPutMatch[1];
940
+ if (!state.presignPending.has(fileId)) {
941
+ return storageErr("Presign session not found", 404);
942
+ }
943
+ return new Response(null, {
944
+ status: 200,
945
+ headers: { etag: `"${import_node_crypto6.default.randomUUID()}"` }
946
+ });
947
+ }
948
+ if (pathname === "/upload/presign/complete" && request.method === "POST") {
949
+ const body = await readJsonBody(request);
950
+ const fileId = String(body.fileId ?? "");
951
+ const pending = state.presignPending.get(fileId);
952
+ if (!pending) return storageErr("Presign session not found", 404);
953
+ const record = {
954
+ id: fileId,
955
+ key: pending.key,
956
+ filename: pending.fileName,
957
+ contentType: pending.contentType,
958
+ sizeBytes: pending.sizeBytes,
959
+ url: buildUrl(pending.key),
960
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
961
+ metadata: pending.metadata,
962
+ isPublic: pending.isPublic,
963
+ deleted: false
964
+ };
965
+ state.storageFiles.set(record.id, record);
966
+ state.presignPending.delete(fileId);
967
+ captureUpload(record, "POST", "/upload/presign/complete");
968
+ return json(toUploadResponse(record));
969
+ }
970
+ if (pathname === "/upload/presign" && request.method === "POST") {
971
+ const body = await readJsonBody(request);
972
+ const fileName = String(body.fileName ?? "upload.bin");
973
+ const contentType = String(body.contentType ?? "application/octet-stream");
974
+ const sizeBytes = Number(body.sizeBytes ?? 0);
975
+ const isPublic = body.isPublic !== false;
976
+ const metadata = body.metadata;
977
+ state.uploadCounter += 1;
978
+ const fileId = `file_test_${state.uploadCounter}`;
979
+ const key = buildKey(fileId, fileName);
980
+ state.presignPending.set(fileId, {
981
+ fileName,
982
+ contentType,
983
+ sizeBytes,
984
+ isPublic,
985
+ metadata,
986
+ key
987
+ });
988
+ return json({
989
+ fileId,
990
+ key,
991
+ presignedUrl: `${STORAGE_TEST_URL}/presigned/${fileId}`,
992
+ contentType,
993
+ expiresIn: 3600
994
+ });
995
+ }
996
+ if (pathname === "/upload" && request.method === "POST") {
997
+ const isPublic = url.searchParams.get("isPublic") !== "false";
998
+ const formData = await request.formData();
999
+ const file = formData.get("file");
1000
+ if (!(file instanceof Blob)) {
1001
+ return storageErr("file is required");
1002
+ }
1003
+ const filename = formData.get("filename") ?? (file instanceof File ? file.name : "upload.bin");
1004
+ const metadataRaw = formData.get("metadata");
1005
+ let metadata;
1006
+ if (typeof metadataRaw === "string" && metadataRaw) {
1007
+ try {
1008
+ metadata = JSON.parse(metadataRaw);
1009
+ } catch {
1010
+ return storageErr("metadata must be valid JSON");
1011
+ }
1012
+ }
1013
+ const record = createFileRecord({
1014
+ filename,
1015
+ contentType: file.type || "application/octet-stream",
1016
+ sizeBytes: file.size,
1017
+ isPublic,
1018
+ metadata
1019
+ });
1020
+ state.storageFiles.set(record.id, record);
1021
+ captureUpload(record, "POST", "/upload");
1022
+ return json(toUploadResponse(record));
1023
+ }
1024
+ const fileMatch = pathname.match(/^\/api\/files\/([^/]+)$/);
1025
+ if (fileMatch) {
1026
+ const fileId = fileMatch[1];
1027
+ const record = state.storageFiles.get(fileId);
1028
+ if (!record || record.deleted) {
1029
+ return storageErr("File not found", 404);
1030
+ }
1031
+ if (request.method === "GET") {
1032
+ return json(toUploadResponse(record));
1033
+ }
1034
+ if (request.method === "PATCH") {
1035
+ const body = await readJsonBody(request);
1036
+ if (body.fileName !== void 0) record.filename = String(body.fileName);
1037
+ if (body.metadata !== void 0) {
1038
+ record.metadata = body.metadata;
1039
+ }
1040
+ if (body.isPublic !== void 0) record.isPublic = body.isPublic === true;
1041
+ return json(toUploadResponse(record));
1042
+ }
1043
+ if (request.method === "DELETE") {
1044
+ record.deleted = true;
1045
+ return json({ success: true });
1046
+ }
1047
+ }
1048
+ if (pathname === "/api/files" && request.method === "GET") {
1049
+ const parseParam = (raw, fallback, min) => {
1050
+ const n = Number(raw);
1051
+ return Number.isFinite(n) && n >= min ? Math.floor(n) : fallback;
1052
+ };
1053
+ const limit = parseParam(url.searchParams.get("limit"), 50, 1);
1054
+ const offset = parseParam(url.searchParams.get("offset"), 0, 0);
1055
+ const prefix = url.searchParams.get("prefix") ?? "";
1056
+ let files = activeFiles();
1057
+ if (prefix) {
1058
+ files = files.filter((f) => f.key.startsWith(prefix) || f.filename.startsWith(prefix));
1059
+ }
1060
+ const total = files.length;
1061
+ const slice = files.slice(offset, offset + limit);
1062
+ return json({
1063
+ files: slice.map((f) => toUploadResponse(f)),
1064
+ total,
1065
+ limit,
1066
+ offset,
1067
+ hasMore: offset + slice.length < total
1068
+ });
1069
+ }
1070
+ return storageErr(`No storage simulator for ${request.method} ${pathname}`, 404);
1071
+ }
1072
+
1073
+ // src/simulator/messaging.ts
1074
+ var import_node_crypto7 = __toESM(require("crypto"));
1075
+ function captureMessage(channel, recipient, body, connectionId) {
1076
+ const message = {
1077
+ channel,
1078
+ recipient,
1079
+ body: {
1080
+ text: body.text ? String(body.text) : void 0,
1081
+ blocks: body.blocks,
1082
+ threadTs: body.threadTs ? String(body.threadTs) : void 0,
1083
+ messagingType: body.messagingType ? String(body.messagingType) : void 0,
1084
+ tag: body.tag ? String(body.tag) : void 0
1085
+ },
1086
+ connectionId,
1087
+ sentAt: /* @__PURE__ */ new Date()
1088
+ };
1089
+ state.messages.push(message);
1090
+ }
1091
+ async function handleMessagingRequest(request, channel, subPath) {
1092
+ const method = request.method;
1093
+ if ((subPath === "" || subPath === "/" || subPath === "/connections") && method === "GET") {
1094
+ return success({ connections: [] });
1095
+ }
1096
+ if (channel === "slack" && subPath === "/send" && method === "POST") {
1097
+ const body = await readJsonBody(request);
1098
+ if (!body.text && !body.blocks) {
1099
+ return failure("text or blocks is required");
1100
+ }
1101
+ const slackChannel = String(body.channel ?? "");
1102
+ if (!slackChannel) {
1103
+ return failure("channel is required");
1104
+ }
1105
+ captureMessage(
1106
+ "slack",
1107
+ slackChannel,
1108
+ body,
1109
+ body.connectionId ? String(body.connectionId) : void 0
1110
+ );
1111
+ return success({
1112
+ ok: true,
1113
+ ts: `${Date.now()}.${import_node_crypto7.default.randomUUID().slice(0, 6)}`,
1114
+ channel: slackChannel
1115
+ });
1116
+ }
1117
+ if (channel === "line" && subPath === "/push" && method === "POST") {
1118
+ const body = await readJsonBody(request);
1119
+ const userId = String(body.userId ?? "");
1120
+ const message = body.message;
1121
+ if (!userId) return failure("userId is required");
1122
+ if (!message || message.type !== "text" || !message.text) {
1123
+ return failure("message must be { type: 'text', text: string }");
1124
+ }
1125
+ if (message.text.length > 5e3) {
1126
+ return failure("message text exceeds maximum length");
1127
+ }
1128
+ captureMessage(
1129
+ "line",
1130
+ userId,
1131
+ { text: message.text },
1132
+ body.connectionId ? String(body.connectionId) : void 0
1133
+ );
1134
+ return success({ success: true });
1135
+ }
1136
+ if (channel === "facebook" && subPath === "/send" && method === "POST") {
1137
+ const body = await readJsonBody(request);
1138
+ const recipientId = String(body.recipientId ?? "");
1139
+ const message = body.message;
1140
+ if (!recipientId) return failure("recipientId is required");
1141
+ if (!message || message.type !== "text" || !message.text) {
1142
+ return failure("message must be { type: 'text', text: string }");
1143
+ }
1144
+ if (message.text.length > 2e3) {
1145
+ return failure("message text exceeds maximum length");
1146
+ }
1147
+ captureMessage(
1148
+ "facebook",
1149
+ recipientId,
1150
+ {
1151
+ text: message.text,
1152
+ messagingType: body.messagingType ? String(body.messagingType) : void 0,
1153
+ tag: body.tag ? String(body.tag) : void 0
1154
+ },
1155
+ body.connectionId ? String(body.connectionId) : void 0
1156
+ );
1157
+ return success({ success: true });
1158
+ }
1159
+ return failure(`No messaging simulator for ${method} .../integrations/${channel}${subPath}`, 404);
1160
+ }
1161
+
708
1162
  // src/simulator/router.ts
709
1163
  var fetchHolder = globalSingleton("fetch-holder", () => ({
710
1164
  originalFetch: null
@@ -712,6 +1166,21 @@ var fetchHolder = globalSingleton("fetch-holder", () => ({
712
1166
  function isLocalHost(hostname) {
713
1167
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0";
714
1168
  }
1169
+ function requiresDeploymentHmac(request, url) {
1170
+ const isStorageHost = url.hostname === STORAGE_TEST_HOST;
1171
+ const isPresignedPut = isStorageHost && request.method === "PUT" && /^\/presigned\/[^/]+$/.test(url.pathname);
1172
+ if (isStorageHost) {
1173
+ return !isPresignedPut;
1174
+ }
1175
+ const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
1176
+ const isEmail = url.pathname === "/api/email/send";
1177
+ const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
1178
+ const storeMatch = url.pathname.match(/^\/api\/store\//);
1179
+ const messagingMatch = url.pathname.match(
1180
+ /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
1181
+ );
1182
+ return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch);
1183
+ }
715
1184
  async function handleSimulatedRequest(request, url) {
716
1185
  if (url.pathname === "/sql") {
717
1186
  return handleNeonSql(requireDb(), request);
@@ -723,7 +1192,12 @@ async function handleSimulatedRequest(request, url) {
723
1192
  const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
724
1193
  const isEmail = url.pathname === "/api/email/send";
725
1194
  const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
726
- if (dataStoreMatch || isEmail || identitiesMatch) {
1195
+ const storeMatch = url.pathname.match(/^\/api\/store\//);
1196
+ const messagingMatch = url.pathname.match(
1197
+ /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
1198
+ );
1199
+ const isStorageHost = url.hostname === STORAGE_TEST_HOST;
1200
+ if (requiresDeploymentHmac(request, url)) {
727
1201
  const authHeader = request.headers.get("X-Stardeck-Auth");
728
1202
  if (!authHeader) {
729
1203
  return failure("Missing authentication header", 401);
@@ -733,34 +1207,44 @@ async function handleSimulatedRequest(request, url) {
733
1207
  return failure("Invalid authentication", 401);
734
1208
  }
735
1209
  }
1210
+ if (isStorageHost) {
1211
+ return handleStorageRequest(request, url);
1212
+ }
736
1213
  if (isEmail && request.method === "POST") {
737
1214
  return handleEmailSend(request);
738
1215
  }
739
1216
  if (identitiesMatch) {
740
1217
  return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
741
1218
  }
1219
+ if (storeMatch) {
1220
+ return handlePaymentsRequest(request, url);
1221
+ }
1222
+ if (messagingMatch) {
1223
+ const channel = messagingMatch[1];
1224
+ return handleMessagingRequest(request, channel, messagingMatch[2] ?? "");
1225
+ }
742
1226
  if (dataStoreMatch) {
743
1227
  const subPath = dataStoreMatch[1] ?? "";
744
1228
  const db = requireDb();
745
- const readBody2 = async () => await request.json();
1229
+ const readBody = async () => await request.json();
746
1230
  if (subPath === "/query" && request.method === "POST") {
747
- return handleQuery(db, await readBody2());
1231
+ return handleQuery(db, await readBody());
748
1232
  }
749
1233
  if (subPath === "/mutate" && request.method === "POST") {
750
- return handleMutate(db, await readBody2());
1234
+ return handleMutate(db, await readBody());
751
1235
  }
752
1236
  if (subPath === "/schema" && request.method === "GET") {
753
1237
  return handleGetSchema(db);
754
1238
  }
755
1239
  if (subPath === "/schema/tables" && request.method === "POST") {
756
- return handleCreateTable(db, await readBody2());
1240
+ return handleCreateTable(db, await readBody());
757
1241
  }
758
1242
  if (subPath === "/schema/columns" && request.method === "POST") {
759
- return handleAddColumn(db, await readBody2());
1243
+ return handleAddColumn(db, await readBody());
760
1244
  }
761
1245
  }
762
1246
  return failure(
763
- `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, auth verify/refresh, Neon /sql.`,
1247
+ `[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, auth verify/refresh, Neon /sql.`,
764
1248
  404
765
1249
  );
766
1250
  }