@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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/test-app.ts
2
- import crypto4 from "crypto";
2
+ import crypto8 from "crypto";
3
3
  import { readFileSync, existsSync } from "fs";
4
4
  import { resolve } from "path";
5
5
 
@@ -54,7 +54,16 @@ var state = globalSingleton("state", () => ({
54
54
  emailCounter: 0,
55
55
  identities: /* @__PURE__ */ new Map(),
56
56
  identityLinks: [],
57
- identityMemory: [],
57
+ checkouts: [],
58
+ checkoutCounter: 0,
59
+ sessionStatuses: /* @__PURE__ */ new Map(),
60
+ paymentLinks: /* @__PURE__ */ new Map(),
61
+ products: [],
62
+ uploads: [],
63
+ uploadCounter: 0,
64
+ storageFiles: /* @__PURE__ */ new Map(),
65
+ presignPending: /* @__PURE__ */ new Map(),
66
+ messages: [],
58
67
  allowNetwork: false
59
68
  }));
60
69
  function requireDb() {
@@ -70,13 +79,16 @@ function requireDb() {
70
79
  var TEST_DOMAIN_SUFFIX = ".stardeck.test";
71
80
  var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
72
81
  var DATA_STORE_TEST_HOST = "db.stardeck.test";
82
+ var STORAGE_TEST_URL = "https://storage.stardeck.test";
83
+ var STORAGE_TEST_HOST = "storage.stardeck.test";
73
84
  var TEST_ENV_DEFAULTS = {
74
85
  CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
75
86
  DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
76
87
  ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
77
88
  PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
78
89
  DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
79
- DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`
90
+ DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`,
91
+ STORAGE_URL: STORAGE_TEST_URL
80
92
  };
81
93
  var DEFAULT_TEST_USER = {
82
94
  id: "test-user-1",
@@ -132,6 +144,13 @@ function success(data) {
132
144
  function failure(error, status = 400) {
133
145
  return json({ success: false, error }, status);
134
146
  }
147
+ async function readJsonBody(request) {
148
+ try {
149
+ return await request.json();
150
+ } catch {
151
+ return {};
152
+ }
153
+ }
135
154
 
136
155
  // src/simulator/data-store.ts
137
156
  function quoteIdent(name) {
@@ -614,13 +633,6 @@ function now() {
614
633
  function linksFor(identityId) {
615
634
  return state.identityLinks.filter((l) => l.identityId === identityId);
616
635
  }
617
- async function readBody(request) {
618
- try {
619
- return await request.json();
620
- } catch {
621
- return {};
622
- }
623
- }
624
636
  function handleList(request) {
625
637
  const typeParam = new URL(request.url).searchParams.get("type");
626
638
  const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
@@ -628,7 +640,7 @@ function handleList(request) {
628
640
  return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
629
641
  }
630
642
  async function handleCreate(request) {
631
- const body = await readBody(request);
643
+ const body = await readJsonBody(request);
632
644
  const type = body.type;
633
645
  if (type !== "person" && type !== "account") {
634
646
  return failure("type must be 'person' or 'account'");
@@ -665,7 +677,7 @@ function handleGet(identityId) {
665
677
  async function handleUpdate(identityId, request) {
666
678
  const identity = state.identities.get(identityId);
667
679
  if (!identity) return failure("identity not found", 404);
668
- const body = await readBody(request);
680
+ const body = await readJsonBody(request);
669
681
  if (body.displayName !== void 0) {
670
682
  identity.displayName = body.displayName;
671
683
  }
@@ -687,7 +699,7 @@ async function handleAttachLink(identityId, request) {
687
699
  if (identity.status !== "active") {
688
700
  return failure("links attach only to active persons");
689
701
  }
690
- const body = await readBody(request);
702
+ const body = await readJsonBody(request);
691
703
  const kind = body.kind;
692
704
  const externalId = body.externalId;
693
705
  if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
@@ -712,108 +724,16 @@ async function handleAttachLink(identityId, request) {
712
724
  state.identityLinks.push(link);
713
725
  return success({ link });
714
726
  }
715
- function memoryDto(m) {
716
- return {
717
- id: m.id,
718
- source: m.source,
719
- kind: m.kind,
720
- content: m.content,
721
- metadata: m.metadata,
722
- createdAt: m.createdAt
723
- };
724
- }
725
- async function handleResolve(request) {
726
- const body = await readBody(request);
727
- const type = body.type;
728
- if (type !== "person" && type !== "account") {
729
- return failure("type must be 'person' or 'account'");
730
- }
731
- if (type === "account") {
732
- return failure(
733
- "resolveOrCreate is for channel-linked persons; an account is not resolvable by link \u2014 create it explicitly instead.",
734
- 409
735
- );
736
- }
737
- const link = body.link;
738
- const kind = link?.kind;
739
- const externalId = link?.externalId;
740
- if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
741
- return failure(`link.kind must be one of: ${[...LINK_KINDS].join(", ")}`);
742
- }
743
- if (typeof externalId !== "string" || !externalId) {
744
- return failure("link.externalId is required");
745
- }
746
- const existingLink = state.identityLinks.find(
747
- (l) => l.kind === kind && l.externalId === externalId
748
- );
749
- if (existingLink) {
750
- const identity2 = state.identities.get(existingLink.identityId);
751
- if (identity2) return success({ identity: identity2, created: false });
752
- }
753
- const identity = {
754
- id: crypto3.randomUUID(),
755
- type,
756
- parentId: null,
757
- displayName: body.displayName ?? null,
758
- profile: body.profile ?? {},
759
- status: "active",
760
- mergedIntoId: null,
761
- externalRef: null,
762
- createdAt: now(),
763
- updatedAt: now()
764
- };
765
- state.identities.set(identity.id, identity);
766
- state.identityLinks.push({
767
- id: crypto3.randomUUID(),
768
- identityId: identity.id,
769
- kind,
770
- externalId,
771
- verified: true,
772
- createdAt: now()
773
- });
774
- return success({ identity, created: true });
775
- }
776
- async function handleWriteMemory(identityId, request) {
777
- if (!state.identities.get(identityId)) return failure("Identity not found", 404);
778
- const body = await readBody(request);
779
- if (typeof body.content !== "string" || !body.content) return failure("content is required");
780
- const entry = {
781
- id: crypto3.randomUUID(),
782
- identityId,
783
- source: typeof body.source === "string" ? body.source : "app",
784
- kind: typeof body.kind === "string" ? body.kind : "fact",
785
- content: body.content,
786
- metadata: body.metadata ?? {},
787
- createdAt: now()
788
- };
789
- state.identityMemory.push(entry);
790
- return success({ memory: memoryDto(entry) });
791
- }
792
- function handleListMemory(identityId, request) {
793
- if (!state.identities.get(identityId)) return failure("Identity not found", 404);
794
- let rows = state.identityMemory.filter((m) => m.identityId === identityId).reverse();
795
- const limitParam = new URL(request.url).searchParams.get("limit");
796
- if (limitParam) rows = rows.slice(0, Math.max(1, parseInt(limitParam, 10) || 0));
797
- return success({ memories: rows.map(memoryDto) });
798
- }
799
727
  async function handleIdentitiesRequest(request, subPath) {
800
728
  const method = request.method;
801
729
  if (subPath === "" || subPath === "/") {
802
730
  if (method === "GET") return handleList(request);
803
731
  if (method === "POST") return handleCreate(request);
804
732
  }
805
- if (subPath === "/resolve" && method === "POST") {
806
- return handleResolve(request);
807
- }
808
733
  const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
809
734
  if (linksMatch && method === "POST") {
810
735
  return handleAttachLink(linksMatch[1], request);
811
736
  }
812
- const memoryMatch = subPath.match(/^\/([^/]+)\/memory$/);
813
- if (memoryMatch) {
814
- if (method === "GET") return handleListMemory(memoryMatch[1], request);
815
- if (method === "POST") return handleWriteMemory(memoryMatch[1], request);
816
- }
817
737
  const singleMatch = subPath.match(/^\/([^/]+)$/);
818
738
  if (singleMatch) {
819
739
  if (method === "GET") return handleGet(singleMatch[1]);
@@ -836,6 +756,679 @@ function createDirectory() {
836
756
  };
837
757
  }
838
758
 
759
+ // src/simulator/payments.ts
760
+ import crypto5 from "crypto";
761
+
762
+ // src/next/headers-shim.ts
763
+ import { AsyncLocalStorage } from "async_hooks";
764
+ var requestScopeStorage = globalSingleton(
765
+ "request-scope",
766
+ () => new AsyncLocalStorage()
767
+ );
768
+
769
+ // src/next/call-route.ts
770
+ function parseCookieHeader(header) {
771
+ const map = /* @__PURE__ */ new Map();
772
+ if (!header) return map;
773
+ for (const part of header.split(";")) {
774
+ const eq = part.indexOf("=");
775
+ if (eq === -1) continue;
776
+ map.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
777
+ }
778
+ return map;
779
+ }
780
+ async function importNextServer() {
781
+ try {
782
+ return await import("next/server.js");
783
+ } catch {
784
+ return await import("next/server");
785
+ }
786
+ }
787
+ async function callRoute(handler, options = {}) {
788
+ const { NextRequest } = await importNextServer();
789
+ const path = options.path ?? "/api/test-route";
790
+ const url = new URL(`http://localhost:3333${path}`);
791
+ for (const [key, value] of Object.entries(options.searchParams ?? {})) {
792
+ url.searchParams.set(key, value);
793
+ }
794
+ const method = options.method ?? (options.body !== void 0 ? "POST" : "GET");
795
+ const headers = new Headers(options.headers);
796
+ const user = options.user !== void 0 ? options.user : state.currentUser;
797
+ if (user && !headers.has("x-stardeck-user")) {
798
+ headers.set("x-stardeck-user", JSON.stringify(user));
799
+ }
800
+ if (options.body !== void 0 && !headers.has("Content-Type")) {
801
+ headers.set("Content-Type", "application/json");
802
+ }
803
+ const cookiePairs = Object.entries(options.cookies ?? {});
804
+ if (cookiePairs.length > 0) {
805
+ const existing = headers.get("cookie");
806
+ const cookieString = cookiePairs.map(([k, v]) => `${k}=${v}`).join("; ");
807
+ headers.set("cookie", existing ? `${existing}; ${cookieString}` : cookieString);
808
+ }
809
+ const request = new NextRequest(url, {
810
+ method,
811
+ headers,
812
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
813
+ });
814
+ const scope = {
815
+ headers,
816
+ cookies: parseCookieHeader(headers.get("cookie"))
817
+ };
818
+ try {
819
+ return await requestScopeStorage.run(
820
+ scope,
821
+ () => Promise.resolve(
822
+ handler(request, {
823
+ params: Promise.resolve(options.params ?? {})
824
+ })
825
+ )
826
+ );
827
+ } catch (error) {
828
+ const redirect = decodeNextRedirect(error);
829
+ if (redirect) return redirect;
830
+ throw error;
831
+ }
832
+ }
833
+ function decodeNextRedirect(error) {
834
+ const digest = error?.digest;
835
+ if (typeof digest !== "string" || !digest.startsWith("NEXT_REDIRECT")) return null;
836
+ const parts = digest.split(";");
837
+ const location = parts[2] ?? "/";
838
+ const status = Number(parts[3]) || 307;
839
+ return new Response(null, { status, headers: { location } });
840
+ }
841
+
842
+ // src/simulator/webhook-signing.ts
843
+ import crypto4 from "crypto";
844
+ function signEventDelivery(secret, context, rawBody) {
845
+ const payload = {
846
+ type: "deployment-request",
847
+ organizationId: context.organizationId,
848
+ projectId: context.projectId,
849
+ deploymentId: context.deploymentId,
850
+ timestamp: Math.floor(Date.now() / 1e3),
851
+ nonce: crypto4.randomUUID()
852
+ };
853
+ const payloadJson = JSON.stringify(payload);
854
+ const payloadB64 = Buffer.from(payloadJson).toString("base64");
855
+ const signature = crypto4.createHmac("sha256", secret).update(payloadJson).update(rawBody).digest("hex");
856
+ return `${payloadB64}.${signature}`;
857
+ }
858
+
859
+ // src/simulator/payments.ts
860
+ function payErr(error, status = 400, code) {
861
+ return json(code ? { error, code } : { error }, status);
862
+ }
863
+ function nextCheckoutId() {
864
+ state.checkoutCounter += 1;
865
+ return `cs_test_${state.checkoutCounter}`;
866
+ }
867
+ function nextPaymentLinkId() {
868
+ state.checkoutCounter += 1;
869
+ return `plink_test_${state.checkoutCounter}`;
870
+ }
871
+ function seedStripeSession(id, body) {
872
+ const lineItems = body.lineItems ?? [];
873
+ let amountTotal = null;
874
+ let currency = null;
875
+ if (lineItems.length > 0) {
876
+ amountTotal = lineItems.reduce(
877
+ (sum, item) => sum + (item.priceData?.unitAmount ?? 0) * (item.quantity ?? 1),
878
+ 0
879
+ );
880
+ currency = lineItems[0].priceData?.currency ?? null;
881
+ }
882
+ state.sessionStatuses.set(id, {
883
+ id,
884
+ status: "open",
885
+ paymentStatus: "unpaid",
886
+ mode: body.mode ?? "payment",
887
+ amountTotal,
888
+ currency,
889
+ customerEmail: body.customerEmail ? String(body.customerEmail) : null,
890
+ metadata: body.metadata ?? {},
891
+ expiresAt: Math.floor(Date.now() / 1e3) + 3600
892
+ });
893
+ }
894
+ function seedBeamLink(id, body, merchantId) {
895
+ const order = body.order;
896
+ state.paymentLinks.set(id, {
897
+ paymentLinkId: id,
898
+ merchantId,
899
+ url: `https://beam.test/pay/${id}`,
900
+ status: "ACTIVE",
901
+ order: {
902
+ netAmount: Number(order?.netAmount ?? 0),
903
+ currency: String(order?.currency ?? "THB"),
904
+ description: String(order?.description ?? ""),
905
+ referenceId: order?.referenceId ? String(order.referenceId) : void 0,
906
+ internalNote: order?.internalNote ? String(order.internalNote) : void 0,
907
+ orderItems: order?.orderItems
908
+ },
909
+ redirectUrl: body.redirectUrl ? String(body.redirectUrl) : void 0,
910
+ linkSettings: body.linkSettings,
911
+ collectDeliveryAddress: body.collectDeliveryAddress === true
912
+ });
913
+ }
914
+ async function deliverEvent(envelope, handler, options) {
915
+ const rawBody = JSON.stringify(envelope);
916
+ const secret = options?.deploymentSecret ?? TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET;
917
+ const authHeader = signEventDelivery(
918
+ secret,
919
+ {
920
+ organizationId: TEST_ENV_DEFAULTS.ORGANIZATION_ID,
921
+ projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
922
+ deploymentId: TEST_ENV_DEFAULTS.DEPLOYMENT_ID
923
+ },
924
+ rawBody
925
+ );
926
+ return callRoute(handler, {
927
+ method: "POST",
928
+ path: options?.path ?? "/api/payments/webhooks",
929
+ params: { path: ["webhooks"] },
930
+ headers: { "X-Stardeck-Auth": authHeader },
931
+ body: envelope
932
+ });
933
+ }
934
+ async function handlePaymentsRequest(request, url) {
935
+ const pathname = url.pathname;
936
+ if (/\/bolt-connections/.test(pathname) || /\/bolt-intents/.test(pathname) || /\/charges(\/|$)/.test(pathname) || /\/billing-portal$/.test(pathname)) {
937
+ return payErr("Not found", 404, "NOT_FOUND");
938
+ }
939
+ const beamProductsMatch = pathname.match(
940
+ /^\/api\/store\/beam\/([^/]+)\/payment-links(?:\/([^/]+))?$/
941
+ );
942
+ if (beamProductsMatch) {
943
+ const merchantId = beamProductsMatch[1];
944
+ const linkId = beamProductsMatch[2];
945
+ if (!linkId && request.method === "POST") {
946
+ const body = await readJsonBody(request);
947
+ const id = nextPaymentLinkId();
948
+ const checkoutUrl = `https://beam.test/pay/${id}`;
949
+ const captured = {
950
+ id,
951
+ url: checkoutUrl,
952
+ provider: "beam",
953
+ options: body,
954
+ metadata: body.metadata,
955
+ createdAt: /* @__PURE__ */ new Date()
956
+ };
957
+ state.checkouts.push(captured);
958
+ seedBeamLink(id, body, merchantId);
959
+ return json({ id, url: checkoutUrl });
960
+ }
961
+ if (linkId && request.method === "GET") {
962
+ const link = state.paymentLinks.get(linkId);
963
+ if (!link) return payErr("Payment link not found", 404, "NOT_FOUND");
964
+ return json({ paymentLink: link });
965
+ }
966
+ }
967
+ const stripeStoreMatch = pathname.match(/^\/api\/store\/([^/]+)\/(.+)$/);
968
+ if (stripeStoreMatch) {
969
+ const accountId = stripeStoreMatch[1];
970
+ const subPath = stripeStoreMatch[2];
971
+ if (accountId === "beam") {
972
+ return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
973
+ }
974
+ if (subPath === "products" && request.method === "GET") {
975
+ return json({ products: state.products });
976
+ }
977
+ const productMatch = subPath.match(/^products\/([^/]+)$/);
978
+ if (productMatch && request.method === "GET") {
979
+ const product = state.products.find((p) => p.id === productMatch[1]);
980
+ if (!product) return payErr("Product not found", 404, "NOT_FOUND");
981
+ return json({ product });
982
+ }
983
+ if (subPath === "checkout" && request.method === "POST") {
984
+ const body = await readJsonBody(request);
985
+ const id = nextCheckoutId();
986
+ const checkoutUrl = `https://checkout.stripe.test/c/pay/${id}`;
987
+ const captured = {
988
+ id,
989
+ url: checkoutUrl,
990
+ provider: "stripe",
991
+ options: body,
992
+ mode: body.mode,
993
+ metadata: body.metadata,
994
+ createdAt: /* @__PURE__ */ new Date()
995
+ };
996
+ state.checkouts.push(captured);
997
+ seedStripeSession(id, body);
998
+ return json({ id, url: checkoutUrl });
999
+ }
1000
+ const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
1001
+ if (sessionMatch && request.method === "GET") {
1002
+ const session = state.sessionStatuses.get(sessionMatch[1]);
1003
+ if (!session) return payErr("Checkout session not found", 404, "NOT_FOUND");
1004
+ return json({ session });
1005
+ }
1006
+ }
1007
+ return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
1008
+ }
1009
+ function createPayments() {
1010
+ return {
1011
+ get checkouts() {
1012
+ return [...state.checkouts];
1013
+ },
1014
+ latest() {
1015
+ return state.checkouts[state.checkouts.length - 1];
1016
+ },
1017
+ setProducts(products) {
1018
+ state.products = products;
1019
+ },
1020
+ markPaid(id) {
1021
+ const session = state.sessionStatuses.get(id);
1022
+ if (session) {
1023
+ session.status = "complete";
1024
+ session.paymentStatus = "paid";
1025
+ return;
1026
+ }
1027
+ const link = state.paymentLinks.get(id);
1028
+ if (link) {
1029
+ link.status = "PAID";
1030
+ return;
1031
+ }
1032
+ throw new Error(`[stardeck-testing] Unknown checkout or payment link id: ${id}`);
1033
+ },
1034
+ setSessionStatus(id, status) {
1035
+ const session = state.sessionStatuses.get(id);
1036
+ if (!session) {
1037
+ throw new Error(`[stardeck-testing] Unknown checkout session id: ${id}`);
1038
+ }
1039
+ Object.assign(session, status);
1040
+ },
1041
+ setPaymentLinkStatus(id, status) {
1042
+ const link = state.paymentLinks.get(id);
1043
+ if (!link) {
1044
+ throw new Error(`[stardeck-testing] Unknown payment link id: ${id}`);
1045
+ }
1046
+ link.status = status;
1047
+ },
1048
+ async deliverStripeEvent(handler, event, options) {
1049
+ const envelope = {
1050
+ id: `evt_test_${crypto5.randomUUID()}`,
1051
+ kind: "stripe_webhook",
1052
+ timestamp: Date.now(),
1053
+ stripeEvent: {
1054
+ type: event.type,
1055
+ accountId: event.accountId ?? process.env.STRIPE_CONNECT_ACCOUNT_ID ?? "acct_test",
1056
+ data: event.data
1057
+ }
1058
+ };
1059
+ return deliverEvent(envelope, handler, options);
1060
+ },
1061
+ async deliverBeamEvent(handler, event, options) {
1062
+ const envelope = {
1063
+ id: `beam_evt_test_${crypto5.randomUUID()}`,
1064
+ kind: "beam_webhook",
1065
+ timestamp: Date.now(),
1066
+ beamEvent: {
1067
+ type: event.type,
1068
+ payload: event.payload
1069
+ }
1070
+ };
1071
+ return deliverEvent(envelope, handler, options);
1072
+ },
1073
+ clear() {
1074
+ state.checkouts = [];
1075
+ state.checkoutCounter = 0;
1076
+ state.sessionStatuses.clear();
1077
+ state.paymentLinks.clear();
1078
+ state.products = [];
1079
+ },
1080
+ get count() {
1081
+ return state.checkouts.length;
1082
+ }
1083
+ };
1084
+ }
1085
+
1086
+ // src/simulator/storage.ts
1087
+ import crypto6 from "crypto";
1088
+ function storageErr(error, status = 400) {
1089
+ return json({ error }, status);
1090
+ }
1091
+ function fileExtension(filename) {
1092
+ const dot = filename.lastIndexOf(".");
1093
+ return dot === -1 ? "bin" : filename.slice(dot + 1);
1094
+ }
1095
+ function buildKey(fileId, filename) {
1096
+ const orgId = process.env.ORGANIZATION_ID ?? TEST_ENV_DEFAULTS.ORGANIZATION_ID;
1097
+ const projectId = process.env.PROJECT_ID ?? TEST_ENV_DEFAULTS.PROJECT_ID;
1098
+ const deploymentId = process.env.DEPLOYMENT_ID ?? TEST_ENV_DEFAULTS.DEPLOYMENT_ID;
1099
+ return `${orgId}/${projectId}/${deploymentId}/${fileId}.${fileExtension(filename)}`;
1100
+ }
1101
+ function buildUrl(key) {
1102
+ return `${STORAGE_TEST_URL}/files/${key}`;
1103
+ }
1104
+ function toUploadResponse(record) {
1105
+ return {
1106
+ id: record.id,
1107
+ key: record.key,
1108
+ filename: record.filename,
1109
+ contentType: record.contentType,
1110
+ sizeBytes: record.sizeBytes,
1111
+ url: record.url,
1112
+ uploadedAt: record.uploadedAt,
1113
+ isPublic: record.isPublic,
1114
+ metadata: record.metadata
1115
+ };
1116
+ }
1117
+ function captureUpload(record, method, path) {
1118
+ const captured = {
1119
+ id: record.id,
1120
+ key: record.key,
1121
+ filename: record.filename,
1122
+ contentType: record.contentType,
1123
+ sizeBytes: record.sizeBytes,
1124
+ url: record.url,
1125
+ uploadedAt: record.uploadedAt,
1126
+ isPublic: record.isPublic,
1127
+ metadata: record.metadata,
1128
+ method,
1129
+ path
1130
+ };
1131
+ state.uploads.push(captured);
1132
+ }
1133
+ function createFileRecord(input) {
1134
+ state.uploadCounter += 1;
1135
+ const id = `file_test_${state.uploadCounter}`;
1136
+ const key = buildKey(id, input.filename);
1137
+ return {
1138
+ id,
1139
+ key,
1140
+ filename: input.filename,
1141
+ contentType: input.contentType,
1142
+ sizeBytes: input.sizeBytes,
1143
+ url: buildUrl(key),
1144
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
1145
+ metadata: input.metadata,
1146
+ isPublic: input.isPublic,
1147
+ deleted: false
1148
+ };
1149
+ }
1150
+ function activeFiles() {
1151
+ return [...state.storageFiles.values()].filter((f) => !f.deleted);
1152
+ }
1153
+ async function handleStorageRequest(request, url) {
1154
+ const pathname = url.pathname;
1155
+ if (pathname.startsWith("/upload/presign/multipart")) {
1156
+ return storageErr("Not found", 404);
1157
+ }
1158
+ const presignedPutMatch = pathname.match(/^\/presigned\/([^/]+)$/);
1159
+ if (presignedPutMatch && request.method === "PUT") {
1160
+ const fileId = presignedPutMatch[1];
1161
+ if (!state.presignPending.has(fileId)) {
1162
+ return storageErr("Presign session not found", 404);
1163
+ }
1164
+ return new Response(null, {
1165
+ status: 200,
1166
+ headers: { etag: `"${crypto6.randomUUID()}"` }
1167
+ });
1168
+ }
1169
+ if (pathname === "/upload/presign/complete" && request.method === "POST") {
1170
+ const body = await readJsonBody(request);
1171
+ const fileId = String(body.fileId ?? "");
1172
+ const pending = state.presignPending.get(fileId);
1173
+ if (!pending) return storageErr("Presign session not found", 404);
1174
+ const record = {
1175
+ id: fileId,
1176
+ key: pending.key,
1177
+ filename: pending.fileName,
1178
+ contentType: pending.contentType,
1179
+ sizeBytes: pending.sizeBytes,
1180
+ url: buildUrl(pending.key),
1181
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
1182
+ metadata: pending.metadata,
1183
+ isPublic: pending.isPublic,
1184
+ deleted: false
1185
+ };
1186
+ state.storageFiles.set(record.id, record);
1187
+ state.presignPending.delete(fileId);
1188
+ captureUpload(record, "POST", "/upload/presign/complete");
1189
+ return json(toUploadResponse(record));
1190
+ }
1191
+ if (pathname === "/upload/presign" && request.method === "POST") {
1192
+ const body = await readJsonBody(request);
1193
+ const fileName = String(body.fileName ?? "upload.bin");
1194
+ const contentType = String(body.contentType ?? "application/octet-stream");
1195
+ const sizeBytes = Number(body.sizeBytes ?? 0);
1196
+ const isPublic = body.isPublic !== false;
1197
+ const metadata = body.metadata;
1198
+ state.uploadCounter += 1;
1199
+ const fileId = `file_test_${state.uploadCounter}`;
1200
+ const key = buildKey(fileId, fileName);
1201
+ state.presignPending.set(fileId, {
1202
+ fileName,
1203
+ contentType,
1204
+ sizeBytes,
1205
+ isPublic,
1206
+ metadata,
1207
+ key
1208
+ });
1209
+ return json({
1210
+ fileId,
1211
+ key,
1212
+ presignedUrl: `${STORAGE_TEST_URL}/presigned/${fileId}`,
1213
+ contentType,
1214
+ expiresIn: 3600
1215
+ });
1216
+ }
1217
+ if (pathname === "/upload" && request.method === "POST") {
1218
+ const isPublic = url.searchParams.get("isPublic") !== "false";
1219
+ const formData = await request.formData();
1220
+ const file = formData.get("file");
1221
+ if (!(file instanceof Blob)) {
1222
+ return storageErr("file is required");
1223
+ }
1224
+ const filename = formData.get("filename") ?? (file instanceof File ? file.name : "upload.bin");
1225
+ const metadataRaw = formData.get("metadata");
1226
+ let metadata;
1227
+ if (typeof metadataRaw === "string" && metadataRaw) {
1228
+ try {
1229
+ metadata = JSON.parse(metadataRaw);
1230
+ } catch {
1231
+ return storageErr("metadata must be valid JSON");
1232
+ }
1233
+ }
1234
+ const record = createFileRecord({
1235
+ filename,
1236
+ contentType: file.type || "application/octet-stream",
1237
+ sizeBytes: file.size,
1238
+ isPublic,
1239
+ metadata
1240
+ });
1241
+ state.storageFiles.set(record.id, record);
1242
+ captureUpload(record, "POST", "/upload");
1243
+ return json(toUploadResponse(record));
1244
+ }
1245
+ const fileMatch = pathname.match(/^\/api\/files\/([^/]+)$/);
1246
+ if (fileMatch) {
1247
+ const fileId = fileMatch[1];
1248
+ const record = state.storageFiles.get(fileId);
1249
+ if (!record || record.deleted) {
1250
+ return storageErr("File not found", 404);
1251
+ }
1252
+ if (request.method === "GET") {
1253
+ return json(toUploadResponse(record));
1254
+ }
1255
+ if (request.method === "PATCH") {
1256
+ const body = await readJsonBody(request);
1257
+ if (body.fileName !== void 0) record.filename = String(body.fileName);
1258
+ if (body.metadata !== void 0) {
1259
+ record.metadata = body.metadata;
1260
+ }
1261
+ if (body.isPublic !== void 0) record.isPublic = body.isPublic === true;
1262
+ return json(toUploadResponse(record));
1263
+ }
1264
+ if (request.method === "DELETE") {
1265
+ record.deleted = true;
1266
+ return json({ success: true });
1267
+ }
1268
+ }
1269
+ if (pathname === "/api/files" && request.method === "GET") {
1270
+ const parseParam = (raw, fallback, min) => {
1271
+ const n = Number(raw);
1272
+ return Number.isFinite(n) && n >= min ? Math.floor(n) : fallback;
1273
+ };
1274
+ const limit = parseParam(url.searchParams.get("limit"), 50, 1);
1275
+ const offset = parseParam(url.searchParams.get("offset"), 0, 0);
1276
+ const prefix = url.searchParams.get("prefix") ?? "";
1277
+ let files = activeFiles();
1278
+ if (prefix) {
1279
+ files = files.filter((f) => f.key.startsWith(prefix) || f.filename.startsWith(prefix));
1280
+ }
1281
+ const total = files.length;
1282
+ const slice = files.slice(offset, offset + limit);
1283
+ return json({
1284
+ files: slice.map((f) => toUploadResponse(f)),
1285
+ total,
1286
+ limit,
1287
+ offset,
1288
+ hasMore: offset + slice.length < total
1289
+ });
1290
+ }
1291
+ return storageErr(`No storage simulator for ${request.method} ${pathname}`, 404);
1292
+ }
1293
+ function createStorage() {
1294
+ return {
1295
+ get uploads() {
1296
+ return [...state.uploads];
1297
+ },
1298
+ latest() {
1299
+ return state.uploads[state.uploads.length - 1];
1300
+ },
1301
+ clear() {
1302
+ state.uploads = [];
1303
+ state.uploadCounter = 0;
1304
+ state.storageFiles.clear();
1305
+ state.presignPending.clear();
1306
+ },
1307
+ get count() {
1308
+ return state.uploads.length;
1309
+ }
1310
+ };
1311
+ }
1312
+
1313
+ // src/simulator/messaging.ts
1314
+ import crypto7 from "crypto";
1315
+ function captureMessage(channel, recipient, body, connectionId) {
1316
+ const message = {
1317
+ channel,
1318
+ recipient,
1319
+ body: {
1320
+ text: body.text ? String(body.text) : void 0,
1321
+ blocks: body.blocks,
1322
+ threadTs: body.threadTs ? String(body.threadTs) : void 0,
1323
+ messagingType: body.messagingType ? String(body.messagingType) : void 0,
1324
+ tag: body.tag ? String(body.tag) : void 0
1325
+ },
1326
+ connectionId,
1327
+ sentAt: /* @__PURE__ */ new Date()
1328
+ };
1329
+ state.messages.push(message);
1330
+ }
1331
+ async function handleMessagingRequest(request, channel, subPath) {
1332
+ const method = request.method;
1333
+ if ((subPath === "" || subPath === "/" || subPath === "/connections") && method === "GET") {
1334
+ return success({ connections: [] });
1335
+ }
1336
+ if (channel === "slack" && subPath === "/send" && method === "POST") {
1337
+ const body = await readJsonBody(request);
1338
+ if (!body.text && !body.blocks) {
1339
+ return failure("text or blocks is required");
1340
+ }
1341
+ const slackChannel = String(body.channel ?? "");
1342
+ if (!slackChannel) {
1343
+ return failure("channel is required");
1344
+ }
1345
+ captureMessage(
1346
+ "slack",
1347
+ slackChannel,
1348
+ body,
1349
+ body.connectionId ? String(body.connectionId) : void 0
1350
+ );
1351
+ return success({
1352
+ ok: true,
1353
+ ts: `${Date.now()}.${crypto7.randomUUID().slice(0, 6)}`,
1354
+ channel: slackChannel
1355
+ });
1356
+ }
1357
+ if (channel === "line" && subPath === "/push" && method === "POST") {
1358
+ const body = await readJsonBody(request);
1359
+ const userId = String(body.userId ?? "");
1360
+ const message = body.message;
1361
+ if (!userId) return failure("userId is required");
1362
+ if (!message || message.type !== "text" || !message.text) {
1363
+ return failure("message must be { type: 'text', text: string }");
1364
+ }
1365
+ if (message.text.length > 5e3) {
1366
+ return failure("message text exceeds maximum length");
1367
+ }
1368
+ captureMessage(
1369
+ "line",
1370
+ userId,
1371
+ { text: message.text },
1372
+ body.connectionId ? String(body.connectionId) : void 0
1373
+ );
1374
+ return success({ success: true });
1375
+ }
1376
+ if (channel === "facebook" && subPath === "/send" && method === "POST") {
1377
+ const body = await readJsonBody(request);
1378
+ const recipientId = String(body.recipientId ?? "");
1379
+ const message = body.message;
1380
+ if (!recipientId) return failure("recipientId is required");
1381
+ if (!message || message.type !== "text" || !message.text) {
1382
+ return failure("message must be { type: 'text', text: string }");
1383
+ }
1384
+ if (message.text.length > 2e3) {
1385
+ return failure("message text exceeds maximum length");
1386
+ }
1387
+ captureMessage(
1388
+ "facebook",
1389
+ recipientId,
1390
+ {
1391
+ text: message.text,
1392
+ messagingType: body.messagingType ? String(body.messagingType) : void 0,
1393
+ tag: body.tag ? String(body.tag) : void 0
1394
+ },
1395
+ body.connectionId ? String(body.connectionId) : void 0
1396
+ );
1397
+ return success({ success: true });
1398
+ }
1399
+ return failure(`No messaging simulator for ${method} .../integrations/${channel}${subPath}`, 404);
1400
+ }
1401
+ function createMessages() {
1402
+ return {
1403
+ all() {
1404
+ return [...state.messages];
1405
+ },
1406
+ latest() {
1407
+ return state.messages[state.messages.length - 1];
1408
+ },
1409
+ to(recipient) {
1410
+ return state.messages.filter((m) => m.recipient === recipient);
1411
+ },
1412
+ channel(kind) {
1413
+ const filtered = () => state.messages.filter((m) => m.channel === kind);
1414
+ return {
1415
+ all: () => filtered(),
1416
+ latest: () => {
1417
+ const pool = filtered();
1418
+ return pool[pool.length - 1];
1419
+ },
1420
+ to: (recipient) => filtered().filter((m) => m.recipient === recipient)
1421
+ };
1422
+ },
1423
+ clear() {
1424
+ state.messages = [];
1425
+ },
1426
+ get count() {
1427
+ return state.messages.length;
1428
+ }
1429
+ };
1430
+ }
1431
+
839
1432
  // src/simulator/router.ts
840
1433
  var fetchHolder = globalSingleton("fetch-holder", () => ({
841
1434
  originalFetch: null
@@ -843,6 +1436,21 @@ var fetchHolder = globalSingleton("fetch-holder", () => ({
843
1436
  function isLocalHost(hostname) {
844
1437
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0";
845
1438
  }
1439
+ function requiresDeploymentHmac(request, url) {
1440
+ const isStorageHost = url.hostname === STORAGE_TEST_HOST;
1441
+ const isPresignedPut = isStorageHost && request.method === "PUT" && /^\/presigned\/[^/]+$/.test(url.pathname);
1442
+ if (isStorageHost) {
1443
+ return !isPresignedPut;
1444
+ }
1445
+ const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
1446
+ const isEmail = url.pathname === "/api/email/send";
1447
+ const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
1448
+ const storeMatch = url.pathname.match(/^\/api\/store\//);
1449
+ const messagingMatch = url.pathname.match(
1450
+ /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
1451
+ );
1452
+ return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch);
1453
+ }
846
1454
  async function handleSimulatedRequest(request, url) {
847
1455
  if (url.pathname === "/sql") {
848
1456
  return handleNeonSql(requireDb(), request);
@@ -854,7 +1462,12 @@ async function handleSimulatedRequest(request, url) {
854
1462
  const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
855
1463
  const isEmail = url.pathname === "/api/email/send";
856
1464
  const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
857
- if (dataStoreMatch || isEmail || identitiesMatch) {
1465
+ const storeMatch = url.pathname.match(/^\/api\/store\//);
1466
+ const messagingMatch = url.pathname.match(
1467
+ /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
1468
+ );
1469
+ const isStorageHost = url.hostname === STORAGE_TEST_HOST;
1470
+ if (requiresDeploymentHmac(request, url)) {
858
1471
  const authHeader = request.headers.get("X-Stardeck-Auth");
859
1472
  if (!authHeader) {
860
1473
  return failure("Missing authentication header", 401);
@@ -864,34 +1477,44 @@ async function handleSimulatedRequest(request, url) {
864
1477
  return failure("Invalid authentication", 401);
865
1478
  }
866
1479
  }
1480
+ if (isStorageHost) {
1481
+ return handleStorageRequest(request, url);
1482
+ }
867
1483
  if (isEmail && request.method === "POST") {
868
1484
  return handleEmailSend(request);
869
1485
  }
870
1486
  if (identitiesMatch) {
871
1487
  return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
872
1488
  }
1489
+ if (storeMatch) {
1490
+ return handlePaymentsRequest(request, url);
1491
+ }
1492
+ if (messagingMatch) {
1493
+ const channel = messagingMatch[1];
1494
+ return handleMessagingRequest(request, channel, messagingMatch[2] ?? "");
1495
+ }
873
1496
  if (dataStoreMatch) {
874
1497
  const subPath = dataStoreMatch[1] ?? "";
875
1498
  const db = requireDb();
876
- const readBody2 = async () => await request.json();
1499
+ const readBody = async () => await request.json();
877
1500
  if (subPath === "/query" && request.method === "POST") {
878
- return handleQuery(db, await readBody2());
1501
+ return handleQuery(db, await readBody());
879
1502
  }
880
1503
  if (subPath === "/mutate" && request.method === "POST") {
881
- return handleMutate(db, await readBody2());
1504
+ return handleMutate(db, await readBody());
882
1505
  }
883
1506
  if (subPath === "/schema" && request.method === "GET") {
884
1507
  return handleGetSchema(db);
885
1508
  }
886
1509
  if (subPath === "/schema/tables" && request.method === "POST") {
887
- return handleCreateTable(db, await readBody2());
1510
+ return handleCreateTable(db, await readBody());
888
1511
  }
889
1512
  if (subPath === "/schema/columns" && request.method === "POST") {
890
- return handleAddColumn(db, await readBody2());
1513
+ return handleAddColumn(db, await readBody());
891
1514
  }
892
1515
  }
893
1516
  return failure(
894
- `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, auth verify/refresh, Neon /sql.`,
1517
+ `[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.`,
895
1518
  404
896
1519
  );
897
1520
  }
@@ -977,10 +1600,16 @@ async function createTestApp(options = {}) {
977
1600
  }
978
1601
  const inbox = createInbox();
979
1602
  const directory = createDirectory();
1603
+ const payments = createPayments();
1604
+ const storage = createStorage();
1605
+ const messages = createMessages();
980
1606
  const app = {
981
1607
  db,
982
1608
  inbox,
983
1609
  identities: directory,
1610
+ payments,
1611
+ storage,
1612
+ messages,
984
1613
  async query(sql, params = []) {
985
1614
  const result = await db.query(sql, params);
986
1615
  return result.rows;
@@ -996,8 +1625,8 @@ async function createTestApp(options = {}) {
996
1625
  issueSession(user) {
997
1626
  const fullUser = buildUser(user);
998
1627
  const tokens = {
999
- accessToken: `test-access-${crypto4.randomUUID()}`,
1000
- refreshToken: `test-refresh-${crypto4.randomUUID()}`
1628
+ accessToken: `test-access-${crypto8.randomUUID()}`,
1629
+ refreshToken: `test-refresh-${crypto8.randomUUID()}`
1001
1630
  };
1002
1631
  state.sessions.set(tokens.accessToken, fullUser);
1003
1632
  state.refreshSessions.set(tokens.refreshToken, fullUser);
@@ -1013,7 +1642,9 @@ async function createTestApp(options = {}) {
1013
1642
  state.emailCounter = 0;
1014
1643
  state.identities.clear();
1015
1644
  state.identityLinks = [];
1016
- state.identityMemory = [];
1645
+ payments.clear();
1646
+ storage.clear();
1647
+ messages.clear();
1017
1648
  },
1018
1649
  async close() {
1019
1650
  state.db = null;
@@ -1021,9 +1652,12 @@ async function createTestApp(options = {}) {
1021
1652
  state.sessions.clear();
1022
1653
  state.refreshSessions.clear();
1023
1654
  state.emails = [];
1655
+ state.emailCounter = 0;
1024
1656
  state.identities.clear();
1025
1657
  state.identityLinks = [];
1026
- state.identityMemory = [];
1658
+ payments.clear();
1659
+ storage.clear();
1660
+ messages.clear();
1027
1661
  uninstallFetchRouter();
1028
1662
  await db.close();
1029
1663
  }
@@ -1031,122 +1665,6 @@ async function createTestApp(options = {}) {
1031
1665
  return app;
1032
1666
  }
1033
1667
 
1034
- // src/module-app.ts
1035
- import {
1036
- makeSqlPort,
1037
- renderSchemaOpsToSql
1038
- } from "@stardeck-customer-apps/core";
1039
- async function createModuleApp(options) {
1040
- const schemaSql = options.modules.map((m) => renderSchemaOpsToSql(m.schema)).join("\n\n");
1041
- const app = await createTestApp({ schemaSql, allowNetwork: options.allowNetwork });
1042
- const sql = { query: (text, params) => app.db.query(text, params ?? []) };
1043
- const data = makeSqlPort(sql);
1044
- const { createIntegrationsClient } = await import("@stardeck-customer-apps/integrations-sdk");
1045
- const identities = createIntegrationsClient({
1046
- controlPlaneUrl: TEST_ENV_DEFAULTS.CONTROL_PLANE_URL,
1047
- organizationId: TEST_ENV_DEFAULTS.ORGANIZATION_ID,
1048
- projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
1049
- deploymentId: TEST_ENV_DEFAULTS.DEPLOYMENT_ID,
1050
- deploymentSecret: TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET
1051
- }).identities;
1052
- const runSeed = async () => {
1053
- if (options.seed) await options.seed({ data, identities });
1054
- };
1055
- await runSeed();
1056
- return {
1057
- app,
1058
- data,
1059
- identities,
1060
- async reset() {
1061
- await app.reset();
1062
- await runSeed();
1063
- },
1064
- async close() {
1065
- await app.close();
1066
- }
1067
- };
1068
- }
1069
-
1070
- // src/next/headers-shim.ts
1071
- import { AsyncLocalStorage } from "async_hooks";
1072
- var requestScopeStorage = globalSingleton(
1073
- "request-scope",
1074
- () => new AsyncLocalStorage()
1075
- );
1076
-
1077
- // src/next/call-route.ts
1078
- function parseCookieHeader(header) {
1079
- const map = /* @__PURE__ */ new Map();
1080
- if (!header) return map;
1081
- for (const part of header.split(";")) {
1082
- const eq = part.indexOf("=");
1083
- if (eq === -1) continue;
1084
- map.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
1085
- }
1086
- return map;
1087
- }
1088
- async function importNextServer() {
1089
- try {
1090
- return await import("next/server.js");
1091
- } catch {
1092
- return await import("next/server");
1093
- }
1094
- }
1095
- async function callRoute(handler, options = {}) {
1096
- const { NextRequest } = await importNextServer();
1097
- const path = options.path ?? "/api/test-route";
1098
- const url = new URL(`http://localhost:3333${path}`);
1099
- for (const [key, value] of Object.entries(options.searchParams ?? {})) {
1100
- url.searchParams.set(key, value);
1101
- }
1102
- const method = options.method ?? (options.body !== void 0 ? "POST" : "GET");
1103
- const headers = new Headers(options.headers);
1104
- const user = options.user !== void 0 ? options.user : state.currentUser;
1105
- if (user && !headers.has("x-stardeck-user")) {
1106
- headers.set("x-stardeck-user", JSON.stringify(user));
1107
- }
1108
- if (options.body !== void 0 && !headers.has("Content-Type")) {
1109
- headers.set("Content-Type", "application/json");
1110
- }
1111
- const cookiePairs = Object.entries(options.cookies ?? {});
1112
- if (cookiePairs.length > 0) {
1113
- const existing = headers.get("cookie");
1114
- const cookieString = cookiePairs.map(([k, v]) => `${k}=${v}`).join("; ");
1115
- headers.set("cookie", existing ? `${existing}; ${cookieString}` : cookieString);
1116
- }
1117
- const request = new NextRequest(url, {
1118
- method,
1119
- headers,
1120
- body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
1121
- });
1122
- const scope = {
1123
- headers,
1124
- cookies: parseCookieHeader(headers.get("cookie"))
1125
- };
1126
- try {
1127
- return await requestScopeStorage.run(
1128
- scope,
1129
- () => Promise.resolve(
1130
- handler(request, {
1131
- params: Promise.resolve(options.params ?? {})
1132
- })
1133
- )
1134
- );
1135
- } catch (error) {
1136
- const redirect = decodeNextRedirect(error);
1137
- if (redirect) return redirect;
1138
- throw error;
1139
- }
1140
- }
1141
- function decodeNextRedirect(error) {
1142
- const digest = error?.digest;
1143
- if (typeof digest !== "string" || !digest.startsWith("NEXT_REDIRECT")) return null;
1144
- const parts = digest.split(";");
1145
- const location = parts[2] ?? "/";
1146
- const status = Number(parts[3]) || 307;
1147
- return new Response(null, { status, headers: { location } });
1148
- }
1149
-
1150
1668
  // src/workflow.ts
1151
1669
  import { describe } from "vitest";
1152
1670
  function describeWorkflow(name, fn) {
@@ -1160,10 +1678,11 @@ export {
1160
1678
  CONTROL_PLANE_TEST_URL,
1161
1679
  DATA_STORE_TEST_HOST,
1162
1680
  DEFAULT_SCHEMA_PATH,
1681
+ STORAGE_TEST_HOST,
1682
+ STORAGE_TEST_URL,
1163
1683
  TEST_ENV_DEFAULTS,
1164
1684
  WORKFLOW_NAME_PREFIX,
1165
1685
  callRoute,
1166
- createModuleApp,
1167
1686
  createTestApp,
1168
1687
  describeWorkflow,
1169
1688
  parseWorkflowName