@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/SKILL.md +50 -0
- package/dist/index.d.mts +110 -67
- package/dist/index.d.ts +110 -67
- package/dist/index.js +754 -136
- package/dist/index.mjs +752 -138
- package/dist/next/headers-shim.js +10 -0
- package/dist/next/headers-shim.mjs +10 -0
- package/dist/setup.js +502 -18
- package/dist/setup.mjs +502 -18
- package/package.json +12 -3
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
|
|
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,6 +100,16 @@ var state = globalSingleton("state", () => ({
|
|
|
99
100
|
emailCounter: 0,
|
|
100
101
|
identities: /* @__PURE__ */ new Map(),
|
|
101
102
|
identityLinks: [],
|
|
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: [],
|
|
102
113
|
allowNetwork: false
|
|
103
114
|
}));
|
|
104
115
|
function requireDb() {
|
|
@@ -114,13 +125,16 @@ function requireDb() {
|
|
|
114
125
|
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
115
126
|
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
116
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";
|
|
117
130
|
var TEST_ENV_DEFAULTS = {
|
|
118
131
|
CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
|
|
119
132
|
DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
|
|
120
133
|
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
121
134
|
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
122
135
|
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
123
|
-
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
|
|
124
138
|
};
|
|
125
139
|
var DEFAULT_TEST_USER = {
|
|
126
140
|
id: "test-user-1",
|
|
@@ -176,6 +190,13 @@ function success(data) {
|
|
|
176
190
|
function failure(error, status = 400) {
|
|
177
191
|
return json({ success: false, error }, status);
|
|
178
192
|
}
|
|
193
|
+
async function readJsonBody(request) {
|
|
194
|
+
try {
|
|
195
|
+
return await request.json();
|
|
196
|
+
} catch {
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
179
200
|
|
|
180
201
|
// src/simulator/data-store.ts
|
|
181
202
|
function quoteIdent(name) {
|
|
@@ -658,13 +679,6 @@ function now() {
|
|
|
658
679
|
function linksFor(identityId) {
|
|
659
680
|
return state.identityLinks.filter((l) => l.identityId === identityId);
|
|
660
681
|
}
|
|
661
|
-
async function readBody(request) {
|
|
662
|
-
try {
|
|
663
|
-
return await request.json();
|
|
664
|
-
} catch {
|
|
665
|
-
return {};
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
682
|
function handleList(request) {
|
|
669
683
|
const typeParam = new URL(request.url).searchParams.get("type");
|
|
670
684
|
const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
|
|
@@ -672,7 +686,7 @@ function handleList(request) {
|
|
|
672
686
|
return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
|
|
673
687
|
}
|
|
674
688
|
async function handleCreate(request) {
|
|
675
|
-
const body = await
|
|
689
|
+
const body = await readJsonBody(request);
|
|
676
690
|
const type = body.type;
|
|
677
691
|
if (type !== "person" && type !== "account") {
|
|
678
692
|
return failure("type must be 'person' or 'account'");
|
|
@@ -709,7 +723,7 @@ function handleGet(identityId) {
|
|
|
709
723
|
async function handleUpdate(identityId, request) {
|
|
710
724
|
const identity = state.identities.get(identityId);
|
|
711
725
|
if (!identity) return failure("identity not found", 404);
|
|
712
|
-
const body = await
|
|
726
|
+
const body = await readJsonBody(request);
|
|
713
727
|
if (body.displayName !== void 0) {
|
|
714
728
|
identity.displayName = body.displayName;
|
|
715
729
|
}
|
|
@@ -731,7 +745,7 @@ async function handleAttachLink(identityId, request) {
|
|
|
731
745
|
if (identity.status !== "active") {
|
|
732
746
|
return failure("links attach only to active persons");
|
|
733
747
|
}
|
|
734
|
-
const body = await
|
|
748
|
+
const body = await readJsonBody(request);
|
|
735
749
|
const kind = body.kind;
|
|
736
750
|
const externalId = body.externalId;
|
|
737
751
|
if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
|
|
@@ -788,6 +802,679 @@ function createDirectory() {
|
|
|
788
802
|
};
|
|
789
803
|
}
|
|
790
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
|
+
|
|
791
1478
|
// src/simulator/router.ts
|
|
792
1479
|
var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
793
1480
|
originalFetch: null
|
|
@@ -795,6 +1482,21 @@ var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
|
795
1482
|
function isLocalHost(hostname) {
|
|
796
1483
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0";
|
|
797
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
|
+
}
|
|
798
1500
|
async function handleSimulatedRequest(request, url) {
|
|
799
1501
|
if (url.pathname === "/sql") {
|
|
800
1502
|
return handleNeonSql(requireDb(), request);
|
|
@@ -806,7 +1508,12 @@ async function handleSimulatedRequest(request, url) {
|
|
|
806
1508
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
807
1509
|
const isEmail = url.pathname === "/api/email/send";
|
|
808
1510
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
809
|
-
|
|
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)) {
|
|
810
1517
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
811
1518
|
if (!authHeader) {
|
|
812
1519
|
return failure("Missing authentication header", 401);
|
|
@@ -816,34 +1523,44 @@ async function handleSimulatedRequest(request, url) {
|
|
|
816
1523
|
return failure("Invalid authentication", 401);
|
|
817
1524
|
}
|
|
818
1525
|
}
|
|
1526
|
+
if (isStorageHost) {
|
|
1527
|
+
return handleStorageRequest(request, url);
|
|
1528
|
+
}
|
|
819
1529
|
if (isEmail && request.method === "POST") {
|
|
820
1530
|
return handleEmailSend(request);
|
|
821
1531
|
}
|
|
822
1532
|
if (identitiesMatch) {
|
|
823
1533
|
return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
|
|
824
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
|
+
}
|
|
825
1542
|
if (dataStoreMatch) {
|
|
826
1543
|
const subPath = dataStoreMatch[1] ?? "";
|
|
827
1544
|
const db = requireDb();
|
|
828
|
-
const
|
|
1545
|
+
const readBody = async () => await request.json();
|
|
829
1546
|
if (subPath === "/query" && request.method === "POST") {
|
|
830
|
-
return handleQuery(db, await
|
|
1547
|
+
return handleQuery(db, await readBody());
|
|
831
1548
|
}
|
|
832
1549
|
if (subPath === "/mutate" && request.method === "POST") {
|
|
833
|
-
return handleMutate(db, await
|
|
1550
|
+
return handleMutate(db, await readBody());
|
|
834
1551
|
}
|
|
835
1552
|
if (subPath === "/schema" && request.method === "GET") {
|
|
836
1553
|
return handleGetSchema(db);
|
|
837
1554
|
}
|
|
838
1555
|
if (subPath === "/schema/tables" && request.method === "POST") {
|
|
839
|
-
return handleCreateTable(db, await
|
|
1556
|
+
return handleCreateTable(db, await readBody());
|
|
840
1557
|
}
|
|
841
1558
|
if (subPath === "/schema/columns" && request.method === "POST") {
|
|
842
|
-
return handleAddColumn(db, await
|
|
1559
|
+
return handleAddColumn(db, await readBody());
|
|
843
1560
|
}
|
|
844
1561
|
}
|
|
845
1562
|
return failure(
|
|
846
|
-
`[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.`,
|
|
847
1564
|
404
|
|
848
1565
|
);
|
|
849
1566
|
}
|
|
@@ -929,10 +1646,16 @@ async function createTestApp(options = {}) {
|
|
|
929
1646
|
}
|
|
930
1647
|
const inbox = createInbox();
|
|
931
1648
|
const directory = createDirectory();
|
|
1649
|
+
const payments = createPayments();
|
|
1650
|
+
const storage = createStorage();
|
|
1651
|
+
const messages = createMessages();
|
|
932
1652
|
const app = {
|
|
933
1653
|
db,
|
|
934
1654
|
inbox,
|
|
935
1655
|
identities: directory,
|
|
1656
|
+
payments,
|
|
1657
|
+
storage,
|
|
1658
|
+
messages,
|
|
936
1659
|
async query(sql, params = []) {
|
|
937
1660
|
const result = await db.query(sql, params);
|
|
938
1661
|
return result.rows;
|
|
@@ -948,8 +1671,8 @@ async function createTestApp(options = {}) {
|
|
|
948
1671
|
issueSession(user) {
|
|
949
1672
|
const fullUser = buildUser(user);
|
|
950
1673
|
const tokens = {
|
|
951
|
-
accessToken: `test-access-${
|
|
952
|
-
refreshToken: `test-refresh-${
|
|
1674
|
+
accessToken: `test-access-${import_node_crypto8.default.randomUUID()}`,
|
|
1675
|
+
refreshToken: `test-refresh-${import_node_crypto8.default.randomUUID()}`
|
|
953
1676
|
};
|
|
954
1677
|
state.sessions.set(tokens.accessToken, fullUser);
|
|
955
1678
|
state.refreshSessions.set(tokens.refreshToken, fullUser);
|
|
@@ -965,6 +1688,9 @@ async function createTestApp(options = {}) {
|
|
|
965
1688
|
state.emailCounter = 0;
|
|
966
1689
|
state.identities.clear();
|
|
967
1690
|
state.identityLinks = [];
|
|
1691
|
+
payments.clear();
|
|
1692
|
+
storage.clear();
|
|
1693
|
+
messages.clear();
|
|
968
1694
|
},
|
|
969
1695
|
async close() {
|
|
970
1696
|
state.db = null;
|
|
@@ -972,8 +1698,12 @@ async function createTestApp(options = {}) {
|
|
|
972
1698
|
state.sessions.clear();
|
|
973
1699
|
state.refreshSessions.clear();
|
|
974
1700
|
state.emails = [];
|
|
1701
|
+
state.emailCounter = 0;
|
|
975
1702
|
state.identities.clear();
|
|
976
1703
|
state.identityLinks = [];
|
|
1704
|
+
payments.clear();
|
|
1705
|
+
storage.clear();
|
|
1706
|
+
messages.clear();
|
|
977
1707
|
uninstallFetchRouter();
|
|
978
1708
|
await db.close();
|
|
979
1709
|
}
|
|
@@ -981,119 +1711,6 @@ async function createTestApp(options = {}) {
|
|
|
981
1711
|
return app;
|
|
982
1712
|
}
|
|
983
1713
|
|
|
984
|
-
// src/module-app.ts
|
|
985
|
-
var import_core = require("@stardeck-customer-apps/core");
|
|
986
|
-
async function createModuleApp(options) {
|
|
987
|
-
const schemaSql = options.modules.map((m) => (0, import_core.renderSchemaOpsToSql)(m.schema)).join("\n\n");
|
|
988
|
-
const app = await createTestApp({ schemaSql, allowNetwork: options.allowNetwork });
|
|
989
|
-
const sql = { query: (text, params) => app.db.query(text, params ?? []) };
|
|
990
|
-
const data = (0, import_core.makeSqlPort)(sql);
|
|
991
|
-
const { createIntegrationsClient } = await import("@stardeck-customer-apps/integrations-sdk");
|
|
992
|
-
const identities = createIntegrationsClient({
|
|
993
|
-
controlPlaneUrl: TEST_ENV_DEFAULTS.CONTROL_PLANE_URL,
|
|
994
|
-
organizationId: TEST_ENV_DEFAULTS.ORGANIZATION_ID,
|
|
995
|
-
projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
|
|
996
|
-
deploymentId: TEST_ENV_DEFAULTS.DEPLOYMENT_ID,
|
|
997
|
-
deploymentSecret: TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET
|
|
998
|
-
}).identities;
|
|
999
|
-
const runSeed = async () => {
|
|
1000
|
-
if (options.seed) await options.seed({ data, identities });
|
|
1001
|
-
};
|
|
1002
|
-
await runSeed();
|
|
1003
|
-
return {
|
|
1004
|
-
app,
|
|
1005
|
-
data,
|
|
1006
|
-
identities,
|
|
1007
|
-
async reset() {
|
|
1008
|
-
await app.reset();
|
|
1009
|
-
await runSeed();
|
|
1010
|
-
},
|
|
1011
|
-
async close() {
|
|
1012
|
-
await app.close();
|
|
1013
|
-
}
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
// src/next/headers-shim.ts
|
|
1018
|
-
var import_node_async_hooks = require("async_hooks");
|
|
1019
|
-
var requestScopeStorage = globalSingleton(
|
|
1020
|
-
"request-scope",
|
|
1021
|
-
() => new import_node_async_hooks.AsyncLocalStorage()
|
|
1022
|
-
);
|
|
1023
|
-
|
|
1024
|
-
// src/next/call-route.ts
|
|
1025
|
-
function parseCookieHeader(header) {
|
|
1026
|
-
const map = /* @__PURE__ */ new Map();
|
|
1027
|
-
if (!header) return map;
|
|
1028
|
-
for (const part of header.split(";")) {
|
|
1029
|
-
const eq = part.indexOf("=");
|
|
1030
|
-
if (eq === -1) continue;
|
|
1031
|
-
map.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
|
|
1032
|
-
}
|
|
1033
|
-
return map;
|
|
1034
|
-
}
|
|
1035
|
-
async function importNextServer() {
|
|
1036
|
-
try {
|
|
1037
|
-
return await import("next/server.js");
|
|
1038
|
-
} catch {
|
|
1039
|
-
return await import("next/server");
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
async function callRoute(handler, options = {}) {
|
|
1043
|
-
const { NextRequest } = await importNextServer();
|
|
1044
|
-
const path = options.path ?? "/api/test-route";
|
|
1045
|
-
const url = new URL(`http://localhost:3333${path}`);
|
|
1046
|
-
for (const [key, value] of Object.entries(options.searchParams ?? {})) {
|
|
1047
|
-
url.searchParams.set(key, value);
|
|
1048
|
-
}
|
|
1049
|
-
const method = options.method ?? (options.body !== void 0 ? "POST" : "GET");
|
|
1050
|
-
const headers = new Headers(options.headers);
|
|
1051
|
-
const user = options.user !== void 0 ? options.user : state.currentUser;
|
|
1052
|
-
if (user && !headers.has("x-stardeck-user")) {
|
|
1053
|
-
headers.set("x-stardeck-user", JSON.stringify(user));
|
|
1054
|
-
}
|
|
1055
|
-
if (options.body !== void 0 && !headers.has("Content-Type")) {
|
|
1056
|
-
headers.set("Content-Type", "application/json");
|
|
1057
|
-
}
|
|
1058
|
-
const cookiePairs = Object.entries(options.cookies ?? {});
|
|
1059
|
-
if (cookiePairs.length > 0) {
|
|
1060
|
-
const existing = headers.get("cookie");
|
|
1061
|
-
const cookieString = cookiePairs.map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1062
|
-
headers.set("cookie", existing ? `${existing}; ${cookieString}` : cookieString);
|
|
1063
|
-
}
|
|
1064
|
-
const request = new NextRequest(url, {
|
|
1065
|
-
method,
|
|
1066
|
-
headers,
|
|
1067
|
-
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
1068
|
-
});
|
|
1069
|
-
const scope = {
|
|
1070
|
-
headers,
|
|
1071
|
-
cookies: parseCookieHeader(headers.get("cookie"))
|
|
1072
|
-
};
|
|
1073
|
-
try {
|
|
1074
|
-
return await requestScopeStorage.run(
|
|
1075
|
-
scope,
|
|
1076
|
-
() => Promise.resolve(
|
|
1077
|
-
handler(request, {
|
|
1078
|
-
params: Promise.resolve(options.params ?? {})
|
|
1079
|
-
})
|
|
1080
|
-
)
|
|
1081
|
-
);
|
|
1082
|
-
} catch (error) {
|
|
1083
|
-
const redirect = decodeNextRedirect(error);
|
|
1084
|
-
if (redirect) return redirect;
|
|
1085
|
-
throw error;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
function decodeNextRedirect(error) {
|
|
1089
|
-
const digest = error?.digest;
|
|
1090
|
-
if (typeof digest !== "string" || !digest.startsWith("NEXT_REDIRECT")) return null;
|
|
1091
|
-
const parts = digest.split(";");
|
|
1092
|
-
const location = parts[2] ?? "/";
|
|
1093
|
-
const status = Number(parts[3]) || 307;
|
|
1094
|
-
return new Response(null, { status, headers: { location } });
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
1714
|
// src/workflow.ts
|
|
1098
1715
|
var import_vitest = require("vitest");
|
|
1099
1716
|
function describeWorkflow(name, fn) {
|
|
@@ -1108,10 +1725,11 @@ function parseWorkflowName(describeTitle) {
|
|
|
1108
1725
|
CONTROL_PLANE_TEST_URL,
|
|
1109
1726
|
DATA_STORE_TEST_HOST,
|
|
1110
1727
|
DEFAULT_SCHEMA_PATH,
|
|
1728
|
+
STORAGE_TEST_HOST,
|
|
1729
|
+
STORAGE_TEST_URL,
|
|
1111
1730
|
TEST_ENV_DEFAULTS,
|
|
1112
1731
|
WORKFLOW_NAME_PREFIX,
|
|
1113
1732
|
callRoute,
|
|
1114
|
-
createModuleApp,
|
|
1115
1733
|
createTestApp,
|
|
1116
1734
|
describeWorkflow,
|
|
1117
1735
|
parseWorkflowName
|