@stardeck-customer-apps/testing 0.3.1 → 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/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,16 @@ 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: [],
103
113
  allowNetwork: false
104
114
  }));
105
115
  function requireDb() {
@@ -115,13 +125,16 @@ function requireDb() {
115
125
  var TEST_DOMAIN_SUFFIX = ".stardeck.test";
116
126
  var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
117
127
  var DATA_STORE_TEST_HOST = "db.stardeck.test";
128
+ var STORAGE_TEST_URL = "https://storage.stardeck.test";
129
+ var STORAGE_TEST_HOST = "storage.stardeck.test";
118
130
  var TEST_ENV_DEFAULTS = {
119
131
  CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
120
132
  DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
121
133
  ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
122
134
  PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
123
135
  DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
124
- DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`
136
+ DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`,
137
+ STORAGE_URL: STORAGE_TEST_URL
125
138
  };
126
139
  var DEFAULT_TEST_USER = {
127
140
  id: "test-user-1",
@@ -177,6 +190,13 @@ function success(data) {
177
190
  function failure(error, status = 400) {
178
191
  return json({ success: false, error }, status);
179
192
  }
193
+ async function readJsonBody(request) {
194
+ try {
195
+ return await request.json();
196
+ } catch {
197
+ return {};
198
+ }
199
+ }
180
200
 
181
201
  // src/simulator/data-store.ts
182
202
  function quoteIdent(name) {
@@ -659,13 +679,6 @@ function now() {
659
679
  function linksFor(identityId) {
660
680
  return state.identityLinks.filter((l) => l.identityId === identityId);
661
681
  }
662
- async function readBody(request) {
663
- try {
664
- return await request.json();
665
- } catch {
666
- return {};
667
- }
668
- }
669
682
  function handleList(request) {
670
683
  const typeParam = new URL(request.url).searchParams.get("type");
671
684
  const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
@@ -673,7 +686,7 @@ function handleList(request) {
673
686
  return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
674
687
  }
675
688
  async function handleCreate(request) {
676
- const body = await readBody(request);
689
+ const body = await readJsonBody(request);
677
690
  const type = body.type;
678
691
  if (type !== "person" && type !== "account") {
679
692
  return failure("type must be 'person' or 'account'");
@@ -710,7 +723,7 @@ function handleGet(identityId) {
710
723
  async function handleUpdate(identityId, request) {
711
724
  const identity = state.identities.get(identityId);
712
725
  if (!identity) return failure("identity not found", 404);
713
- const body = await readBody(request);
726
+ const body = await readJsonBody(request);
714
727
  if (body.displayName !== void 0) {
715
728
  identity.displayName = body.displayName;
716
729
  }
@@ -732,7 +745,7 @@ async function handleAttachLink(identityId, request) {
732
745
  if (identity.status !== "active") {
733
746
  return failure("links attach only to active persons");
734
747
  }
735
- const body = await readBody(request);
748
+ const body = await readJsonBody(request);
736
749
  const kind = body.kind;
737
750
  const externalId = body.externalId;
738
751
  if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
@@ -757,108 +770,16 @@ async function handleAttachLink(identityId, request) {
757
770
  state.identityLinks.push(link);
758
771
  return success({ link });
759
772
  }
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
773
  async function handleIdentitiesRequest(request, subPath) {
845
774
  const method = request.method;
846
775
  if (subPath === "" || subPath === "/") {
847
776
  if (method === "GET") return handleList(request);
848
777
  if (method === "POST") return handleCreate(request);
849
778
  }
850
- if (subPath === "/resolve" && method === "POST") {
851
- return handleResolve(request);
852
- }
853
779
  const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
854
780
  if (linksMatch && method === "POST") {
855
781
  return handleAttachLink(linksMatch[1], request);
856
782
  }
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
783
  const singleMatch = subPath.match(/^\/([^/]+)$/);
863
784
  if (singleMatch) {
864
785
  if (method === "GET") return handleGet(singleMatch[1]);
@@ -881,6 +802,679 @@ function createDirectory() {
881
802
  };
882
803
  }
883
804
 
805
+ // src/simulator/payments.ts
806
+ var import_node_crypto5 = __toESM(require("crypto"));
807
+
808
+ // src/next/headers-shim.ts
809
+ var import_node_async_hooks = require("async_hooks");
810
+ var requestScopeStorage = globalSingleton(
811
+ "request-scope",
812
+ () => new import_node_async_hooks.AsyncLocalStorage()
813
+ );
814
+
815
+ // src/next/call-route.ts
816
+ function parseCookieHeader(header) {
817
+ const map = /* @__PURE__ */ new Map();
818
+ if (!header) return map;
819
+ for (const part of header.split(";")) {
820
+ const eq = part.indexOf("=");
821
+ if (eq === -1) continue;
822
+ map.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
823
+ }
824
+ return map;
825
+ }
826
+ async function importNextServer() {
827
+ try {
828
+ return await import("next/server.js");
829
+ } catch {
830
+ return await import("next/server");
831
+ }
832
+ }
833
+ async function callRoute(handler, options = {}) {
834
+ const { NextRequest } = await importNextServer();
835
+ const path = options.path ?? "/api/test-route";
836
+ const url = new URL(`http://localhost:3333${path}`);
837
+ for (const [key, value] of Object.entries(options.searchParams ?? {})) {
838
+ url.searchParams.set(key, value);
839
+ }
840
+ const method = options.method ?? (options.body !== void 0 ? "POST" : "GET");
841
+ const headers = new Headers(options.headers);
842
+ const user = options.user !== void 0 ? options.user : state.currentUser;
843
+ if (user && !headers.has("x-stardeck-user")) {
844
+ headers.set("x-stardeck-user", JSON.stringify(user));
845
+ }
846
+ if (options.body !== void 0 && !headers.has("Content-Type")) {
847
+ headers.set("Content-Type", "application/json");
848
+ }
849
+ const cookiePairs = Object.entries(options.cookies ?? {});
850
+ if (cookiePairs.length > 0) {
851
+ const existing = headers.get("cookie");
852
+ const cookieString = cookiePairs.map(([k, v]) => `${k}=${v}`).join("; ");
853
+ headers.set("cookie", existing ? `${existing}; ${cookieString}` : cookieString);
854
+ }
855
+ const request = new NextRequest(url, {
856
+ method,
857
+ headers,
858
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
859
+ });
860
+ const scope = {
861
+ headers,
862
+ cookies: parseCookieHeader(headers.get("cookie"))
863
+ };
864
+ try {
865
+ return await requestScopeStorage.run(
866
+ scope,
867
+ () => Promise.resolve(
868
+ handler(request, {
869
+ params: Promise.resolve(options.params ?? {})
870
+ })
871
+ )
872
+ );
873
+ } catch (error) {
874
+ const redirect = decodeNextRedirect(error);
875
+ if (redirect) return redirect;
876
+ throw error;
877
+ }
878
+ }
879
+ function decodeNextRedirect(error) {
880
+ const digest = error?.digest;
881
+ if (typeof digest !== "string" || !digest.startsWith("NEXT_REDIRECT")) return null;
882
+ const parts = digest.split(";");
883
+ const location = parts[2] ?? "/";
884
+ const status = Number(parts[3]) || 307;
885
+ return new Response(null, { status, headers: { location } });
886
+ }
887
+
888
+ // src/simulator/webhook-signing.ts
889
+ var import_node_crypto4 = __toESM(require("crypto"));
890
+ function signEventDelivery(secret, context, rawBody) {
891
+ const payload = {
892
+ type: "deployment-request",
893
+ organizationId: context.organizationId,
894
+ projectId: context.projectId,
895
+ deploymentId: context.deploymentId,
896
+ timestamp: Math.floor(Date.now() / 1e3),
897
+ nonce: import_node_crypto4.default.randomUUID()
898
+ };
899
+ const payloadJson = JSON.stringify(payload);
900
+ const payloadB64 = Buffer.from(payloadJson).toString("base64");
901
+ const signature = import_node_crypto4.default.createHmac("sha256", secret).update(payloadJson).update(rawBody).digest("hex");
902
+ return `${payloadB64}.${signature}`;
903
+ }
904
+
905
+ // src/simulator/payments.ts
906
+ function payErr(error, status = 400, code) {
907
+ return json(code ? { error, code } : { error }, status);
908
+ }
909
+ function nextCheckoutId() {
910
+ state.checkoutCounter += 1;
911
+ return `cs_test_${state.checkoutCounter}`;
912
+ }
913
+ function nextPaymentLinkId() {
914
+ state.checkoutCounter += 1;
915
+ return `plink_test_${state.checkoutCounter}`;
916
+ }
917
+ function seedStripeSession(id, body) {
918
+ const lineItems = body.lineItems ?? [];
919
+ let amountTotal = null;
920
+ let currency = null;
921
+ if (lineItems.length > 0) {
922
+ amountTotal = lineItems.reduce(
923
+ (sum, item) => sum + (item.priceData?.unitAmount ?? 0) * (item.quantity ?? 1),
924
+ 0
925
+ );
926
+ currency = lineItems[0].priceData?.currency ?? null;
927
+ }
928
+ state.sessionStatuses.set(id, {
929
+ id,
930
+ status: "open",
931
+ paymentStatus: "unpaid",
932
+ mode: body.mode ?? "payment",
933
+ amountTotal,
934
+ currency,
935
+ customerEmail: body.customerEmail ? String(body.customerEmail) : null,
936
+ metadata: body.metadata ?? {},
937
+ expiresAt: Math.floor(Date.now() / 1e3) + 3600
938
+ });
939
+ }
940
+ function seedBeamLink(id, body, merchantId) {
941
+ const order = body.order;
942
+ state.paymentLinks.set(id, {
943
+ paymentLinkId: id,
944
+ merchantId,
945
+ url: `https://beam.test/pay/${id}`,
946
+ status: "ACTIVE",
947
+ order: {
948
+ netAmount: Number(order?.netAmount ?? 0),
949
+ currency: String(order?.currency ?? "THB"),
950
+ description: String(order?.description ?? ""),
951
+ referenceId: order?.referenceId ? String(order.referenceId) : void 0,
952
+ internalNote: order?.internalNote ? String(order.internalNote) : void 0,
953
+ orderItems: order?.orderItems
954
+ },
955
+ redirectUrl: body.redirectUrl ? String(body.redirectUrl) : void 0,
956
+ linkSettings: body.linkSettings,
957
+ collectDeliveryAddress: body.collectDeliveryAddress === true
958
+ });
959
+ }
960
+ async function deliverEvent(envelope, handler, options) {
961
+ const rawBody = JSON.stringify(envelope);
962
+ const secret = options?.deploymentSecret ?? TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET;
963
+ const authHeader = signEventDelivery(
964
+ secret,
965
+ {
966
+ organizationId: TEST_ENV_DEFAULTS.ORGANIZATION_ID,
967
+ projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
968
+ deploymentId: TEST_ENV_DEFAULTS.DEPLOYMENT_ID
969
+ },
970
+ rawBody
971
+ );
972
+ return callRoute(handler, {
973
+ method: "POST",
974
+ path: options?.path ?? "/api/payments/webhooks",
975
+ params: { path: ["webhooks"] },
976
+ headers: { "X-Stardeck-Auth": authHeader },
977
+ body: envelope
978
+ });
979
+ }
980
+ async function handlePaymentsRequest(request, url) {
981
+ const pathname = url.pathname;
982
+ if (/\/bolt-connections/.test(pathname) || /\/bolt-intents/.test(pathname) || /\/charges(\/|$)/.test(pathname) || /\/billing-portal$/.test(pathname)) {
983
+ return payErr("Not found", 404, "NOT_FOUND");
984
+ }
985
+ const beamProductsMatch = pathname.match(
986
+ /^\/api\/store\/beam\/([^/]+)\/payment-links(?:\/([^/]+))?$/
987
+ );
988
+ if (beamProductsMatch) {
989
+ const merchantId = beamProductsMatch[1];
990
+ const linkId = beamProductsMatch[2];
991
+ if (!linkId && request.method === "POST") {
992
+ const body = await readJsonBody(request);
993
+ const id = nextPaymentLinkId();
994
+ const checkoutUrl = `https://beam.test/pay/${id}`;
995
+ const captured = {
996
+ id,
997
+ url: checkoutUrl,
998
+ provider: "beam",
999
+ options: body,
1000
+ metadata: body.metadata,
1001
+ createdAt: /* @__PURE__ */ new Date()
1002
+ };
1003
+ state.checkouts.push(captured);
1004
+ seedBeamLink(id, body, merchantId);
1005
+ return json({ id, url: checkoutUrl });
1006
+ }
1007
+ if (linkId && request.method === "GET") {
1008
+ const link = state.paymentLinks.get(linkId);
1009
+ if (!link) return payErr("Payment link not found", 404, "NOT_FOUND");
1010
+ return json({ paymentLink: link });
1011
+ }
1012
+ }
1013
+ const stripeStoreMatch = pathname.match(/^\/api\/store\/([^/]+)\/(.+)$/);
1014
+ if (stripeStoreMatch) {
1015
+ const accountId = stripeStoreMatch[1];
1016
+ const subPath = stripeStoreMatch[2];
1017
+ if (accountId === "beam") {
1018
+ return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
1019
+ }
1020
+ if (subPath === "products" && request.method === "GET") {
1021
+ return json({ products: state.products });
1022
+ }
1023
+ const productMatch = subPath.match(/^products\/([^/]+)$/);
1024
+ if (productMatch && request.method === "GET") {
1025
+ const product = state.products.find((p) => p.id === productMatch[1]);
1026
+ if (!product) return payErr("Product not found", 404, "NOT_FOUND");
1027
+ return json({ product });
1028
+ }
1029
+ if (subPath === "checkout" && request.method === "POST") {
1030
+ const body = await readJsonBody(request);
1031
+ const id = nextCheckoutId();
1032
+ const checkoutUrl = `https://checkout.stripe.test/c/pay/${id}`;
1033
+ const captured = {
1034
+ id,
1035
+ url: checkoutUrl,
1036
+ provider: "stripe",
1037
+ options: body,
1038
+ mode: body.mode,
1039
+ metadata: body.metadata,
1040
+ createdAt: /* @__PURE__ */ new Date()
1041
+ };
1042
+ state.checkouts.push(captured);
1043
+ seedStripeSession(id, body);
1044
+ return json({ id, url: checkoutUrl });
1045
+ }
1046
+ const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
1047
+ if (sessionMatch && request.method === "GET") {
1048
+ const session = state.sessionStatuses.get(sessionMatch[1]);
1049
+ if (!session) return payErr("Checkout session not found", 404, "NOT_FOUND");
1050
+ return json({ session });
1051
+ }
1052
+ }
1053
+ return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
1054
+ }
1055
+ function createPayments() {
1056
+ return {
1057
+ get checkouts() {
1058
+ return [...state.checkouts];
1059
+ },
1060
+ latest() {
1061
+ return state.checkouts[state.checkouts.length - 1];
1062
+ },
1063
+ setProducts(products) {
1064
+ state.products = products;
1065
+ },
1066
+ markPaid(id) {
1067
+ const session = state.sessionStatuses.get(id);
1068
+ if (session) {
1069
+ session.status = "complete";
1070
+ session.paymentStatus = "paid";
1071
+ return;
1072
+ }
1073
+ const link = state.paymentLinks.get(id);
1074
+ if (link) {
1075
+ link.status = "PAID";
1076
+ return;
1077
+ }
1078
+ throw new Error(`[stardeck-testing] Unknown checkout or payment link id: ${id}`);
1079
+ },
1080
+ setSessionStatus(id, status) {
1081
+ const session = state.sessionStatuses.get(id);
1082
+ if (!session) {
1083
+ throw new Error(`[stardeck-testing] Unknown checkout session id: ${id}`);
1084
+ }
1085
+ Object.assign(session, status);
1086
+ },
1087
+ setPaymentLinkStatus(id, status) {
1088
+ const link = state.paymentLinks.get(id);
1089
+ if (!link) {
1090
+ throw new Error(`[stardeck-testing] Unknown payment link id: ${id}`);
1091
+ }
1092
+ link.status = status;
1093
+ },
1094
+ async deliverStripeEvent(handler, event, options) {
1095
+ const envelope = {
1096
+ id: `evt_test_${import_node_crypto5.default.randomUUID()}`,
1097
+ kind: "stripe_webhook",
1098
+ timestamp: Date.now(),
1099
+ stripeEvent: {
1100
+ type: event.type,
1101
+ accountId: event.accountId ?? process.env.STRIPE_CONNECT_ACCOUNT_ID ?? "acct_test",
1102
+ data: event.data
1103
+ }
1104
+ };
1105
+ return deliverEvent(envelope, handler, options);
1106
+ },
1107
+ async deliverBeamEvent(handler, event, options) {
1108
+ const envelope = {
1109
+ id: `beam_evt_test_${import_node_crypto5.default.randomUUID()}`,
1110
+ kind: "beam_webhook",
1111
+ timestamp: Date.now(),
1112
+ beamEvent: {
1113
+ type: event.type,
1114
+ payload: event.payload
1115
+ }
1116
+ };
1117
+ return deliverEvent(envelope, handler, options);
1118
+ },
1119
+ clear() {
1120
+ state.checkouts = [];
1121
+ state.checkoutCounter = 0;
1122
+ state.sessionStatuses.clear();
1123
+ state.paymentLinks.clear();
1124
+ state.products = [];
1125
+ },
1126
+ get count() {
1127
+ return state.checkouts.length;
1128
+ }
1129
+ };
1130
+ }
1131
+
1132
+ // src/simulator/storage.ts
1133
+ var import_node_crypto6 = __toESM(require("crypto"));
1134
+ function storageErr(error, status = 400) {
1135
+ return json({ error }, status);
1136
+ }
1137
+ function fileExtension(filename) {
1138
+ const dot = filename.lastIndexOf(".");
1139
+ return dot === -1 ? "bin" : filename.slice(dot + 1);
1140
+ }
1141
+ function buildKey(fileId, filename) {
1142
+ const orgId = process.env.ORGANIZATION_ID ?? TEST_ENV_DEFAULTS.ORGANIZATION_ID;
1143
+ const projectId = process.env.PROJECT_ID ?? TEST_ENV_DEFAULTS.PROJECT_ID;
1144
+ const deploymentId = process.env.DEPLOYMENT_ID ?? TEST_ENV_DEFAULTS.DEPLOYMENT_ID;
1145
+ return `${orgId}/${projectId}/${deploymentId}/${fileId}.${fileExtension(filename)}`;
1146
+ }
1147
+ function buildUrl(key) {
1148
+ return `${STORAGE_TEST_URL}/files/${key}`;
1149
+ }
1150
+ function toUploadResponse(record) {
1151
+ return {
1152
+ id: record.id,
1153
+ key: record.key,
1154
+ filename: record.filename,
1155
+ contentType: record.contentType,
1156
+ sizeBytes: record.sizeBytes,
1157
+ url: record.url,
1158
+ uploadedAt: record.uploadedAt,
1159
+ isPublic: record.isPublic,
1160
+ metadata: record.metadata
1161
+ };
1162
+ }
1163
+ function captureUpload(record, method, path) {
1164
+ const captured = {
1165
+ id: record.id,
1166
+ key: record.key,
1167
+ filename: record.filename,
1168
+ contentType: record.contentType,
1169
+ sizeBytes: record.sizeBytes,
1170
+ url: record.url,
1171
+ uploadedAt: record.uploadedAt,
1172
+ isPublic: record.isPublic,
1173
+ metadata: record.metadata,
1174
+ method,
1175
+ path
1176
+ };
1177
+ state.uploads.push(captured);
1178
+ }
1179
+ function createFileRecord(input) {
1180
+ state.uploadCounter += 1;
1181
+ const id = `file_test_${state.uploadCounter}`;
1182
+ const key = buildKey(id, input.filename);
1183
+ return {
1184
+ id,
1185
+ key,
1186
+ filename: input.filename,
1187
+ contentType: input.contentType,
1188
+ sizeBytes: input.sizeBytes,
1189
+ url: buildUrl(key),
1190
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
1191
+ metadata: input.metadata,
1192
+ isPublic: input.isPublic,
1193
+ deleted: false
1194
+ };
1195
+ }
1196
+ function activeFiles() {
1197
+ return [...state.storageFiles.values()].filter((f) => !f.deleted);
1198
+ }
1199
+ async function handleStorageRequest(request, url) {
1200
+ const pathname = url.pathname;
1201
+ if (pathname.startsWith("/upload/presign/multipart")) {
1202
+ return storageErr("Not found", 404);
1203
+ }
1204
+ const presignedPutMatch = pathname.match(/^\/presigned\/([^/]+)$/);
1205
+ if (presignedPutMatch && request.method === "PUT") {
1206
+ const fileId = presignedPutMatch[1];
1207
+ if (!state.presignPending.has(fileId)) {
1208
+ return storageErr("Presign session not found", 404);
1209
+ }
1210
+ return new Response(null, {
1211
+ status: 200,
1212
+ headers: { etag: `"${import_node_crypto6.default.randomUUID()}"` }
1213
+ });
1214
+ }
1215
+ if (pathname === "/upload/presign/complete" && request.method === "POST") {
1216
+ const body = await readJsonBody(request);
1217
+ const fileId = String(body.fileId ?? "");
1218
+ const pending = state.presignPending.get(fileId);
1219
+ if (!pending) return storageErr("Presign session not found", 404);
1220
+ const record = {
1221
+ id: fileId,
1222
+ key: pending.key,
1223
+ filename: pending.fileName,
1224
+ contentType: pending.contentType,
1225
+ sizeBytes: pending.sizeBytes,
1226
+ url: buildUrl(pending.key),
1227
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
1228
+ metadata: pending.metadata,
1229
+ isPublic: pending.isPublic,
1230
+ deleted: false
1231
+ };
1232
+ state.storageFiles.set(record.id, record);
1233
+ state.presignPending.delete(fileId);
1234
+ captureUpload(record, "POST", "/upload/presign/complete");
1235
+ return json(toUploadResponse(record));
1236
+ }
1237
+ if (pathname === "/upload/presign" && request.method === "POST") {
1238
+ const body = await readJsonBody(request);
1239
+ const fileName = String(body.fileName ?? "upload.bin");
1240
+ const contentType = String(body.contentType ?? "application/octet-stream");
1241
+ const sizeBytes = Number(body.sizeBytes ?? 0);
1242
+ const isPublic = body.isPublic !== false;
1243
+ const metadata = body.metadata;
1244
+ state.uploadCounter += 1;
1245
+ const fileId = `file_test_${state.uploadCounter}`;
1246
+ const key = buildKey(fileId, fileName);
1247
+ state.presignPending.set(fileId, {
1248
+ fileName,
1249
+ contentType,
1250
+ sizeBytes,
1251
+ isPublic,
1252
+ metadata,
1253
+ key
1254
+ });
1255
+ return json({
1256
+ fileId,
1257
+ key,
1258
+ presignedUrl: `${STORAGE_TEST_URL}/presigned/${fileId}`,
1259
+ contentType,
1260
+ expiresIn: 3600
1261
+ });
1262
+ }
1263
+ if (pathname === "/upload" && request.method === "POST") {
1264
+ const isPublic = url.searchParams.get("isPublic") !== "false";
1265
+ const formData = await request.formData();
1266
+ const file = formData.get("file");
1267
+ if (!(file instanceof Blob)) {
1268
+ return storageErr("file is required");
1269
+ }
1270
+ const filename = formData.get("filename") ?? (file instanceof File ? file.name : "upload.bin");
1271
+ const metadataRaw = formData.get("metadata");
1272
+ let metadata;
1273
+ if (typeof metadataRaw === "string" && metadataRaw) {
1274
+ try {
1275
+ metadata = JSON.parse(metadataRaw);
1276
+ } catch {
1277
+ return storageErr("metadata must be valid JSON");
1278
+ }
1279
+ }
1280
+ const record = createFileRecord({
1281
+ filename,
1282
+ contentType: file.type || "application/octet-stream",
1283
+ sizeBytes: file.size,
1284
+ isPublic,
1285
+ metadata
1286
+ });
1287
+ state.storageFiles.set(record.id, record);
1288
+ captureUpload(record, "POST", "/upload");
1289
+ return json(toUploadResponse(record));
1290
+ }
1291
+ const fileMatch = pathname.match(/^\/api\/files\/([^/]+)$/);
1292
+ if (fileMatch) {
1293
+ const fileId = fileMatch[1];
1294
+ const record = state.storageFiles.get(fileId);
1295
+ if (!record || record.deleted) {
1296
+ return storageErr("File not found", 404);
1297
+ }
1298
+ if (request.method === "GET") {
1299
+ return json(toUploadResponse(record));
1300
+ }
1301
+ if (request.method === "PATCH") {
1302
+ const body = await readJsonBody(request);
1303
+ if (body.fileName !== void 0) record.filename = String(body.fileName);
1304
+ if (body.metadata !== void 0) {
1305
+ record.metadata = body.metadata;
1306
+ }
1307
+ if (body.isPublic !== void 0) record.isPublic = body.isPublic === true;
1308
+ return json(toUploadResponse(record));
1309
+ }
1310
+ if (request.method === "DELETE") {
1311
+ record.deleted = true;
1312
+ return json({ success: true });
1313
+ }
1314
+ }
1315
+ if (pathname === "/api/files" && request.method === "GET") {
1316
+ const parseParam = (raw, fallback, min) => {
1317
+ const n = Number(raw);
1318
+ return Number.isFinite(n) && n >= min ? Math.floor(n) : fallback;
1319
+ };
1320
+ const limit = parseParam(url.searchParams.get("limit"), 50, 1);
1321
+ const offset = parseParam(url.searchParams.get("offset"), 0, 0);
1322
+ const prefix = url.searchParams.get("prefix") ?? "";
1323
+ let files = activeFiles();
1324
+ if (prefix) {
1325
+ files = files.filter((f) => f.key.startsWith(prefix) || f.filename.startsWith(prefix));
1326
+ }
1327
+ const total = files.length;
1328
+ const slice = files.slice(offset, offset + limit);
1329
+ return json({
1330
+ files: slice.map((f) => toUploadResponse(f)),
1331
+ total,
1332
+ limit,
1333
+ offset,
1334
+ hasMore: offset + slice.length < total
1335
+ });
1336
+ }
1337
+ return storageErr(`No storage simulator for ${request.method} ${pathname}`, 404);
1338
+ }
1339
+ function createStorage() {
1340
+ return {
1341
+ get uploads() {
1342
+ return [...state.uploads];
1343
+ },
1344
+ latest() {
1345
+ return state.uploads[state.uploads.length - 1];
1346
+ },
1347
+ clear() {
1348
+ state.uploads = [];
1349
+ state.uploadCounter = 0;
1350
+ state.storageFiles.clear();
1351
+ state.presignPending.clear();
1352
+ },
1353
+ get count() {
1354
+ return state.uploads.length;
1355
+ }
1356
+ };
1357
+ }
1358
+
1359
+ // src/simulator/messaging.ts
1360
+ var import_node_crypto7 = __toESM(require("crypto"));
1361
+ function captureMessage(channel, recipient, body, connectionId) {
1362
+ const message = {
1363
+ channel,
1364
+ recipient,
1365
+ body: {
1366
+ text: body.text ? String(body.text) : void 0,
1367
+ blocks: body.blocks,
1368
+ threadTs: body.threadTs ? String(body.threadTs) : void 0,
1369
+ messagingType: body.messagingType ? String(body.messagingType) : void 0,
1370
+ tag: body.tag ? String(body.tag) : void 0
1371
+ },
1372
+ connectionId,
1373
+ sentAt: /* @__PURE__ */ new Date()
1374
+ };
1375
+ state.messages.push(message);
1376
+ }
1377
+ async function handleMessagingRequest(request, channel, subPath) {
1378
+ const method = request.method;
1379
+ if ((subPath === "" || subPath === "/" || subPath === "/connections") && method === "GET") {
1380
+ return success({ connections: [] });
1381
+ }
1382
+ if (channel === "slack" && subPath === "/send" && method === "POST") {
1383
+ const body = await readJsonBody(request);
1384
+ if (!body.text && !body.blocks) {
1385
+ return failure("text or blocks is required");
1386
+ }
1387
+ const slackChannel = String(body.channel ?? "");
1388
+ if (!slackChannel) {
1389
+ return failure("channel is required");
1390
+ }
1391
+ captureMessage(
1392
+ "slack",
1393
+ slackChannel,
1394
+ body,
1395
+ body.connectionId ? String(body.connectionId) : void 0
1396
+ );
1397
+ return success({
1398
+ ok: true,
1399
+ ts: `${Date.now()}.${import_node_crypto7.default.randomUUID().slice(0, 6)}`,
1400
+ channel: slackChannel
1401
+ });
1402
+ }
1403
+ if (channel === "line" && subPath === "/push" && method === "POST") {
1404
+ const body = await readJsonBody(request);
1405
+ const userId = String(body.userId ?? "");
1406
+ const message = body.message;
1407
+ if (!userId) return failure("userId is required");
1408
+ if (!message || message.type !== "text" || !message.text) {
1409
+ return failure("message must be { type: 'text', text: string }");
1410
+ }
1411
+ if (message.text.length > 5e3) {
1412
+ return failure("message text exceeds maximum length");
1413
+ }
1414
+ captureMessage(
1415
+ "line",
1416
+ userId,
1417
+ { text: message.text },
1418
+ body.connectionId ? String(body.connectionId) : void 0
1419
+ );
1420
+ return success({ success: true });
1421
+ }
1422
+ if (channel === "facebook" && subPath === "/send" && method === "POST") {
1423
+ const body = await readJsonBody(request);
1424
+ const recipientId = String(body.recipientId ?? "");
1425
+ const message = body.message;
1426
+ if (!recipientId) return failure("recipientId is required");
1427
+ if (!message || message.type !== "text" || !message.text) {
1428
+ return failure("message must be { type: 'text', text: string }");
1429
+ }
1430
+ if (message.text.length > 2e3) {
1431
+ return failure("message text exceeds maximum length");
1432
+ }
1433
+ captureMessage(
1434
+ "facebook",
1435
+ recipientId,
1436
+ {
1437
+ text: message.text,
1438
+ messagingType: body.messagingType ? String(body.messagingType) : void 0,
1439
+ tag: body.tag ? String(body.tag) : void 0
1440
+ },
1441
+ body.connectionId ? String(body.connectionId) : void 0
1442
+ );
1443
+ return success({ success: true });
1444
+ }
1445
+ return failure(`No messaging simulator for ${method} .../integrations/${channel}${subPath}`, 404);
1446
+ }
1447
+ function createMessages() {
1448
+ return {
1449
+ all() {
1450
+ return [...state.messages];
1451
+ },
1452
+ latest() {
1453
+ return state.messages[state.messages.length - 1];
1454
+ },
1455
+ to(recipient) {
1456
+ return state.messages.filter((m) => m.recipient === recipient);
1457
+ },
1458
+ channel(kind) {
1459
+ const filtered = () => state.messages.filter((m) => m.channel === kind);
1460
+ return {
1461
+ all: () => filtered(),
1462
+ latest: () => {
1463
+ const pool = filtered();
1464
+ return pool[pool.length - 1];
1465
+ },
1466
+ to: (recipient) => filtered().filter((m) => m.recipient === recipient)
1467
+ };
1468
+ },
1469
+ clear() {
1470
+ state.messages = [];
1471
+ },
1472
+ get count() {
1473
+ return state.messages.length;
1474
+ }
1475
+ };
1476
+ }
1477
+
884
1478
  // src/simulator/router.ts
885
1479
  var fetchHolder = globalSingleton("fetch-holder", () => ({
886
1480
  originalFetch: null
@@ -888,6 +1482,21 @@ var fetchHolder = globalSingleton("fetch-holder", () => ({
888
1482
  function isLocalHost(hostname) {
889
1483
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0";
890
1484
  }
1485
+ function requiresDeploymentHmac(request, url) {
1486
+ const isStorageHost = url.hostname === STORAGE_TEST_HOST;
1487
+ const isPresignedPut = isStorageHost && request.method === "PUT" && /^\/presigned\/[^/]+$/.test(url.pathname);
1488
+ if (isStorageHost) {
1489
+ return !isPresignedPut;
1490
+ }
1491
+ const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
1492
+ const isEmail = url.pathname === "/api/email/send";
1493
+ const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
1494
+ const storeMatch = url.pathname.match(/^\/api\/store\//);
1495
+ const messagingMatch = url.pathname.match(
1496
+ /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
1497
+ );
1498
+ return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch);
1499
+ }
891
1500
  async function handleSimulatedRequest(request, url) {
892
1501
  if (url.pathname === "/sql") {
893
1502
  return handleNeonSql(requireDb(), request);
@@ -899,7 +1508,12 @@ async function handleSimulatedRequest(request, url) {
899
1508
  const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
900
1509
  const isEmail = url.pathname === "/api/email/send";
901
1510
  const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
902
- if (dataStoreMatch || isEmail || identitiesMatch) {
1511
+ const storeMatch = url.pathname.match(/^\/api\/store\//);
1512
+ const messagingMatch = url.pathname.match(
1513
+ /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
1514
+ );
1515
+ const isStorageHost = url.hostname === STORAGE_TEST_HOST;
1516
+ if (requiresDeploymentHmac(request, url)) {
903
1517
  const authHeader = request.headers.get("X-Stardeck-Auth");
904
1518
  if (!authHeader) {
905
1519
  return failure("Missing authentication header", 401);
@@ -909,34 +1523,44 @@ async function handleSimulatedRequest(request, url) {
909
1523
  return failure("Invalid authentication", 401);
910
1524
  }
911
1525
  }
1526
+ if (isStorageHost) {
1527
+ return handleStorageRequest(request, url);
1528
+ }
912
1529
  if (isEmail && request.method === "POST") {
913
1530
  return handleEmailSend(request);
914
1531
  }
915
1532
  if (identitiesMatch) {
916
1533
  return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
917
1534
  }
1535
+ if (storeMatch) {
1536
+ return handlePaymentsRequest(request, url);
1537
+ }
1538
+ if (messagingMatch) {
1539
+ const channel = messagingMatch[1];
1540
+ return handleMessagingRequest(request, channel, messagingMatch[2] ?? "");
1541
+ }
918
1542
  if (dataStoreMatch) {
919
1543
  const subPath = dataStoreMatch[1] ?? "";
920
1544
  const db = requireDb();
921
- const readBody2 = async () => await request.json();
1545
+ const readBody = async () => await request.json();
922
1546
  if (subPath === "/query" && request.method === "POST") {
923
- return handleQuery(db, await readBody2());
1547
+ return handleQuery(db, await readBody());
924
1548
  }
925
1549
  if (subPath === "/mutate" && request.method === "POST") {
926
- return handleMutate(db, await readBody2());
1550
+ return handleMutate(db, await readBody());
927
1551
  }
928
1552
  if (subPath === "/schema" && request.method === "GET") {
929
1553
  return handleGetSchema(db);
930
1554
  }
931
1555
  if (subPath === "/schema/tables" && request.method === "POST") {
932
- return handleCreateTable(db, await readBody2());
1556
+ return handleCreateTable(db, await readBody());
933
1557
  }
934
1558
  if (subPath === "/schema/columns" && request.method === "POST") {
935
- return handleAddColumn(db, await readBody2());
1559
+ return handleAddColumn(db, await readBody());
936
1560
  }
937
1561
  }
938
1562
  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.`,
1563
+ `[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.`,
940
1564
  404
941
1565
  );
942
1566
  }
@@ -1022,10 +1646,16 @@ async function createTestApp(options = {}) {
1022
1646
  }
1023
1647
  const inbox = createInbox();
1024
1648
  const directory = createDirectory();
1649
+ const payments = createPayments();
1650
+ const storage = createStorage();
1651
+ const messages = createMessages();
1025
1652
  const app = {
1026
1653
  db,
1027
1654
  inbox,
1028
1655
  identities: directory,
1656
+ payments,
1657
+ storage,
1658
+ messages,
1029
1659
  async query(sql, params = []) {
1030
1660
  const result = await db.query(sql, params);
1031
1661
  return result.rows;
@@ -1041,8 +1671,8 @@ async function createTestApp(options = {}) {
1041
1671
  issueSession(user) {
1042
1672
  const fullUser = buildUser(user);
1043
1673
  const tokens = {
1044
- accessToken: `test-access-${import_node_crypto4.default.randomUUID()}`,
1045
- refreshToken: `test-refresh-${import_node_crypto4.default.randomUUID()}`
1674
+ accessToken: `test-access-${import_node_crypto8.default.randomUUID()}`,
1675
+ refreshToken: `test-refresh-${import_node_crypto8.default.randomUUID()}`
1046
1676
  };
1047
1677
  state.sessions.set(tokens.accessToken, fullUser);
1048
1678
  state.refreshSessions.set(tokens.refreshToken, fullUser);
@@ -1058,7 +1688,9 @@ async function createTestApp(options = {}) {
1058
1688
  state.emailCounter = 0;
1059
1689
  state.identities.clear();
1060
1690
  state.identityLinks = [];
1061
- state.identityMemory = [];
1691
+ payments.clear();
1692
+ storage.clear();
1693
+ messages.clear();
1062
1694
  },
1063
1695
  async close() {
1064
1696
  state.db = null;
@@ -1066,9 +1698,12 @@ async function createTestApp(options = {}) {
1066
1698
  state.sessions.clear();
1067
1699
  state.refreshSessions.clear();
1068
1700
  state.emails = [];
1701
+ state.emailCounter = 0;
1069
1702
  state.identities.clear();
1070
1703
  state.identityLinks = [];
1071
- state.identityMemory = [];
1704
+ payments.clear();
1705
+ storage.clear();
1706
+ messages.clear();
1072
1707
  uninstallFetchRouter();
1073
1708
  await db.close();
1074
1709
  }
@@ -1076,119 +1711,6 @@ async function createTestApp(options = {}) {
1076
1711
  return app;
1077
1712
  }
1078
1713
 
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
1714
  // src/workflow.ts
1193
1715
  var import_vitest = require("vitest");
1194
1716
  function describeWorkflow(name, fn) {
@@ -1203,10 +1725,11 @@ function parseWorkflowName(describeTitle) {
1203
1725
  CONTROL_PLANE_TEST_URL,
1204
1726
  DATA_STORE_TEST_HOST,
1205
1727
  DEFAULT_SCHEMA_PATH,
1728
+ STORAGE_TEST_HOST,
1729
+ STORAGE_TEST_URL,
1206
1730
  TEST_ENV_DEFAULTS,
1207
1731
  WORKFLOW_NAME_PREFIX,
1208
1732
  callRoute,
1209
- createModuleApp,
1210
1733
  createTestApp,
1211
1734
  describeWorkflow,
1212
1735
  parseWorkflowName