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