@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.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/test-app.ts
|
|
2
|
-
import
|
|
2
|
+
import crypto8 from "crypto";
|
|
3
3
|
import { readFileSync, existsSync } from "fs";
|
|
4
4
|
import { resolve } from "path";
|
|
5
5
|
|
|
@@ -54,6 +54,16 @@ var state = globalSingleton("state", () => ({
|
|
|
54
54
|
emailCounter: 0,
|
|
55
55
|
identities: /* @__PURE__ */ new Map(),
|
|
56
56
|
identityLinks: [],
|
|
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: [],
|
|
57
67
|
allowNetwork: false
|
|
58
68
|
}));
|
|
59
69
|
function requireDb() {
|
|
@@ -69,13 +79,16 @@ function requireDb() {
|
|
|
69
79
|
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
70
80
|
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
71
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";
|
|
72
84
|
var TEST_ENV_DEFAULTS = {
|
|
73
85
|
CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
|
|
74
86
|
DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
|
|
75
87
|
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
76
88
|
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
77
89
|
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
78
|
-
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
|
|
79
92
|
};
|
|
80
93
|
var DEFAULT_TEST_USER = {
|
|
81
94
|
id: "test-user-1",
|
|
@@ -131,6 +144,13 @@ function success(data) {
|
|
|
131
144
|
function failure(error, status = 400) {
|
|
132
145
|
return json({ success: false, error }, status);
|
|
133
146
|
}
|
|
147
|
+
async function readJsonBody(request) {
|
|
148
|
+
try {
|
|
149
|
+
return await request.json();
|
|
150
|
+
} catch {
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
134
154
|
|
|
135
155
|
// src/simulator/data-store.ts
|
|
136
156
|
function quoteIdent(name) {
|
|
@@ -613,13 +633,6 @@ function now() {
|
|
|
613
633
|
function linksFor(identityId) {
|
|
614
634
|
return state.identityLinks.filter((l) => l.identityId === identityId);
|
|
615
635
|
}
|
|
616
|
-
async function readBody(request) {
|
|
617
|
-
try {
|
|
618
|
-
return await request.json();
|
|
619
|
-
} catch {
|
|
620
|
-
return {};
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
636
|
function handleList(request) {
|
|
624
637
|
const typeParam = new URL(request.url).searchParams.get("type");
|
|
625
638
|
const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
|
|
@@ -627,7 +640,7 @@ function handleList(request) {
|
|
|
627
640
|
return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
|
|
628
641
|
}
|
|
629
642
|
async function handleCreate(request) {
|
|
630
|
-
const body = await
|
|
643
|
+
const body = await readJsonBody(request);
|
|
631
644
|
const type = body.type;
|
|
632
645
|
if (type !== "person" && type !== "account") {
|
|
633
646
|
return failure("type must be 'person' or 'account'");
|
|
@@ -664,7 +677,7 @@ function handleGet(identityId) {
|
|
|
664
677
|
async function handleUpdate(identityId, request) {
|
|
665
678
|
const identity = state.identities.get(identityId);
|
|
666
679
|
if (!identity) return failure("identity not found", 404);
|
|
667
|
-
const body = await
|
|
680
|
+
const body = await readJsonBody(request);
|
|
668
681
|
if (body.displayName !== void 0) {
|
|
669
682
|
identity.displayName = body.displayName;
|
|
670
683
|
}
|
|
@@ -686,7 +699,7 @@ async function handleAttachLink(identityId, request) {
|
|
|
686
699
|
if (identity.status !== "active") {
|
|
687
700
|
return failure("links attach only to active persons");
|
|
688
701
|
}
|
|
689
|
-
const body = await
|
|
702
|
+
const body = await readJsonBody(request);
|
|
690
703
|
const kind = body.kind;
|
|
691
704
|
const externalId = body.externalId;
|
|
692
705
|
if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
|
|
@@ -743,6 +756,679 @@ function createDirectory() {
|
|
|
743
756
|
};
|
|
744
757
|
}
|
|
745
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
|
+
|
|
746
1432
|
// src/simulator/router.ts
|
|
747
1433
|
var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
748
1434
|
originalFetch: null
|
|
@@ -750,6 +1436,21 @@ var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
|
750
1436
|
function isLocalHost(hostname) {
|
|
751
1437
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0";
|
|
752
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
|
+
}
|
|
753
1454
|
async function handleSimulatedRequest(request, url) {
|
|
754
1455
|
if (url.pathname === "/sql") {
|
|
755
1456
|
return handleNeonSql(requireDb(), request);
|
|
@@ -761,7 +1462,12 @@ async function handleSimulatedRequest(request, url) {
|
|
|
761
1462
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
762
1463
|
const isEmail = url.pathname === "/api/email/send";
|
|
763
1464
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
764
|
-
|
|
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)) {
|
|
765
1471
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
766
1472
|
if (!authHeader) {
|
|
767
1473
|
return failure("Missing authentication header", 401);
|
|
@@ -771,34 +1477,44 @@ async function handleSimulatedRequest(request, url) {
|
|
|
771
1477
|
return failure("Invalid authentication", 401);
|
|
772
1478
|
}
|
|
773
1479
|
}
|
|
1480
|
+
if (isStorageHost) {
|
|
1481
|
+
return handleStorageRequest(request, url);
|
|
1482
|
+
}
|
|
774
1483
|
if (isEmail && request.method === "POST") {
|
|
775
1484
|
return handleEmailSend(request);
|
|
776
1485
|
}
|
|
777
1486
|
if (identitiesMatch) {
|
|
778
1487
|
return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
|
|
779
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
|
+
}
|
|
780
1496
|
if (dataStoreMatch) {
|
|
781
1497
|
const subPath = dataStoreMatch[1] ?? "";
|
|
782
1498
|
const db = requireDb();
|
|
783
|
-
const
|
|
1499
|
+
const readBody = async () => await request.json();
|
|
784
1500
|
if (subPath === "/query" && request.method === "POST") {
|
|
785
|
-
return handleQuery(db, await
|
|
1501
|
+
return handleQuery(db, await readBody());
|
|
786
1502
|
}
|
|
787
1503
|
if (subPath === "/mutate" && request.method === "POST") {
|
|
788
|
-
return handleMutate(db, await
|
|
1504
|
+
return handleMutate(db, await readBody());
|
|
789
1505
|
}
|
|
790
1506
|
if (subPath === "/schema" && request.method === "GET") {
|
|
791
1507
|
return handleGetSchema(db);
|
|
792
1508
|
}
|
|
793
1509
|
if (subPath === "/schema/tables" && request.method === "POST") {
|
|
794
|
-
return handleCreateTable(db, await
|
|
1510
|
+
return handleCreateTable(db, await readBody());
|
|
795
1511
|
}
|
|
796
1512
|
if (subPath === "/schema/columns" && request.method === "POST") {
|
|
797
|
-
return handleAddColumn(db, await
|
|
1513
|
+
return handleAddColumn(db, await readBody());
|
|
798
1514
|
}
|
|
799
1515
|
}
|
|
800
1516
|
return failure(
|
|
801
|
-
`[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.`,
|
|
802
1518
|
404
|
|
803
1519
|
);
|
|
804
1520
|
}
|
|
@@ -884,10 +1600,16 @@ async function createTestApp(options = {}) {
|
|
|
884
1600
|
}
|
|
885
1601
|
const inbox = createInbox();
|
|
886
1602
|
const directory = createDirectory();
|
|
1603
|
+
const payments = createPayments();
|
|
1604
|
+
const storage = createStorage();
|
|
1605
|
+
const messages = createMessages();
|
|
887
1606
|
const app = {
|
|
888
1607
|
db,
|
|
889
1608
|
inbox,
|
|
890
1609
|
identities: directory,
|
|
1610
|
+
payments,
|
|
1611
|
+
storage,
|
|
1612
|
+
messages,
|
|
891
1613
|
async query(sql, params = []) {
|
|
892
1614
|
const result = await db.query(sql, params);
|
|
893
1615
|
return result.rows;
|
|
@@ -903,8 +1625,8 @@ async function createTestApp(options = {}) {
|
|
|
903
1625
|
issueSession(user) {
|
|
904
1626
|
const fullUser = buildUser(user);
|
|
905
1627
|
const tokens = {
|
|
906
|
-
accessToken: `test-access-${
|
|
907
|
-
refreshToken: `test-refresh-${
|
|
1628
|
+
accessToken: `test-access-${crypto8.randomUUID()}`,
|
|
1629
|
+
refreshToken: `test-refresh-${crypto8.randomUUID()}`
|
|
908
1630
|
};
|
|
909
1631
|
state.sessions.set(tokens.accessToken, fullUser);
|
|
910
1632
|
state.refreshSessions.set(tokens.refreshToken, fullUser);
|
|
@@ -920,6 +1642,9 @@ async function createTestApp(options = {}) {
|
|
|
920
1642
|
state.emailCounter = 0;
|
|
921
1643
|
state.identities.clear();
|
|
922
1644
|
state.identityLinks = [];
|
|
1645
|
+
payments.clear();
|
|
1646
|
+
storage.clear();
|
|
1647
|
+
messages.clear();
|
|
923
1648
|
},
|
|
924
1649
|
async close() {
|
|
925
1650
|
state.db = null;
|
|
@@ -927,8 +1652,12 @@ async function createTestApp(options = {}) {
|
|
|
927
1652
|
state.sessions.clear();
|
|
928
1653
|
state.refreshSessions.clear();
|
|
929
1654
|
state.emails = [];
|
|
1655
|
+
state.emailCounter = 0;
|
|
930
1656
|
state.identities.clear();
|
|
931
1657
|
state.identityLinks = [];
|
|
1658
|
+
payments.clear();
|
|
1659
|
+
storage.clear();
|
|
1660
|
+
messages.clear();
|
|
932
1661
|
uninstallFetchRouter();
|
|
933
1662
|
await db.close();
|
|
934
1663
|
}
|
|
@@ -936,122 +1665,6 @@ async function createTestApp(options = {}) {
|
|
|
936
1665
|
return app;
|
|
937
1666
|
}
|
|
938
1667
|
|
|
939
|
-
// src/module-app.ts
|
|
940
|
-
import {
|
|
941
|
-
makeSqlPort,
|
|
942
|
-
renderSchemaOpsToSql
|
|
943
|
-
} from "@stardeck-customer-apps/core";
|
|
944
|
-
async function createModuleApp(options) {
|
|
945
|
-
const schemaSql = options.modules.map((m) => renderSchemaOpsToSql(m.schema)).join("\n\n");
|
|
946
|
-
const app = await createTestApp({ schemaSql, allowNetwork: options.allowNetwork });
|
|
947
|
-
const sql = { query: (text, params) => app.db.query(text, params ?? []) };
|
|
948
|
-
const data = makeSqlPort(sql);
|
|
949
|
-
const { createIntegrationsClient } = await import("@stardeck-customer-apps/integrations-sdk");
|
|
950
|
-
const identities = createIntegrationsClient({
|
|
951
|
-
controlPlaneUrl: TEST_ENV_DEFAULTS.CONTROL_PLANE_URL,
|
|
952
|
-
organizationId: TEST_ENV_DEFAULTS.ORGANIZATION_ID,
|
|
953
|
-
projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
|
|
954
|
-
deploymentId: TEST_ENV_DEFAULTS.DEPLOYMENT_ID,
|
|
955
|
-
deploymentSecret: TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET
|
|
956
|
-
}).identities;
|
|
957
|
-
const runSeed = async () => {
|
|
958
|
-
if (options.seed) await options.seed({ data, identities });
|
|
959
|
-
};
|
|
960
|
-
await runSeed();
|
|
961
|
-
return {
|
|
962
|
-
app,
|
|
963
|
-
data,
|
|
964
|
-
identities,
|
|
965
|
-
async reset() {
|
|
966
|
-
await app.reset();
|
|
967
|
-
await runSeed();
|
|
968
|
-
},
|
|
969
|
-
async close() {
|
|
970
|
-
await app.close();
|
|
971
|
-
}
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// src/next/headers-shim.ts
|
|
976
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
977
|
-
var requestScopeStorage = globalSingleton(
|
|
978
|
-
"request-scope",
|
|
979
|
-
() => new AsyncLocalStorage()
|
|
980
|
-
);
|
|
981
|
-
|
|
982
|
-
// src/next/call-route.ts
|
|
983
|
-
function parseCookieHeader(header) {
|
|
984
|
-
const map = /* @__PURE__ */ new Map();
|
|
985
|
-
if (!header) return map;
|
|
986
|
-
for (const part of header.split(";")) {
|
|
987
|
-
const eq = part.indexOf("=");
|
|
988
|
-
if (eq === -1) continue;
|
|
989
|
-
map.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
|
|
990
|
-
}
|
|
991
|
-
return map;
|
|
992
|
-
}
|
|
993
|
-
async function importNextServer() {
|
|
994
|
-
try {
|
|
995
|
-
return await import("next/server.js");
|
|
996
|
-
} catch {
|
|
997
|
-
return await import("next/server");
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
async function callRoute(handler, options = {}) {
|
|
1001
|
-
const { NextRequest } = await importNextServer();
|
|
1002
|
-
const path = options.path ?? "/api/test-route";
|
|
1003
|
-
const url = new URL(`http://localhost:3333${path}`);
|
|
1004
|
-
for (const [key, value] of Object.entries(options.searchParams ?? {})) {
|
|
1005
|
-
url.searchParams.set(key, value);
|
|
1006
|
-
}
|
|
1007
|
-
const method = options.method ?? (options.body !== void 0 ? "POST" : "GET");
|
|
1008
|
-
const headers = new Headers(options.headers);
|
|
1009
|
-
const user = options.user !== void 0 ? options.user : state.currentUser;
|
|
1010
|
-
if (user && !headers.has("x-stardeck-user")) {
|
|
1011
|
-
headers.set("x-stardeck-user", JSON.stringify(user));
|
|
1012
|
-
}
|
|
1013
|
-
if (options.body !== void 0 && !headers.has("Content-Type")) {
|
|
1014
|
-
headers.set("Content-Type", "application/json");
|
|
1015
|
-
}
|
|
1016
|
-
const cookiePairs = Object.entries(options.cookies ?? {});
|
|
1017
|
-
if (cookiePairs.length > 0) {
|
|
1018
|
-
const existing = headers.get("cookie");
|
|
1019
|
-
const cookieString = cookiePairs.map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1020
|
-
headers.set("cookie", existing ? `${existing}; ${cookieString}` : cookieString);
|
|
1021
|
-
}
|
|
1022
|
-
const request = new NextRequest(url, {
|
|
1023
|
-
method,
|
|
1024
|
-
headers,
|
|
1025
|
-
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
1026
|
-
});
|
|
1027
|
-
const scope = {
|
|
1028
|
-
headers,
|
|
1029
|
-
cookies: parseCookieHeader(headers.get("cookie"))
|
|
1030
|
-
};
|
|
1031
|
-
try {
|
|
1032
|
-
return await requestScopeStorage.run(
|
|
1033
|
-
scope,
|
|
1034
|
-
() => Promise.resolve(
|
|
1035
|
-
handler(request, {
|
|
1036
|
-
params: Promise.resolve(options.params ?? {})
|
|
1037
|
-
})
|
|
1038
|
-
)
|
|
1039
|
-
);
|
|
1040
|
-
} catch (error) {
|
|
1041
|
-
const redirect = decodeNextRedirect(error);
|
|
1042
|
-
if (redirect) return redirect;
|
|
1043
|
-
throw error;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
function decodeNextRedirect(error) {
|
|
1047
|
-
const digest = error?.digest;
|
|
1048
|
-
if (typeof digest !== "string" || !digest.startsWith("NEXT_REDIRECT")) return null;
|
|
1049
|
-
const parts = digest.split(";");
|
|
1050
|
-
const location = parts[2] ?? "/";
|
|
1051
|
-
const status = Number(parts[3]) || 307;
|
|
1052
|
-
return new Response(null, { status, headers: { location } });
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
1668
|
// src/workflow.ts
|
|
1056
1669
|
import { describe } from "vitest";
|
|
1057
1670
|
function describeWorkflow(name, fn) {
|
|
@@ -1065,10 +1678,11 @@ export {
|
|
|
1065
1678
|
CONTROL_PLANE_TEST_URL,
|
|
1066
1679
|
DATA_STORE_TEST_HOST,
|
|
1067
1680
|
DEFAULT_SCHEMA_PATH,
|
|
1681
|
+
STORAGE_TEST_HOST,
|
|
1682
|
+
STORAGE_TEST_URL,
|
|
1068
1683
|
TEST_ENV_DEFAULTS,
|
|
1069
1684
|
WORKFLOW_NAME_PREFIX,
|
|
1070
1685
|
callRoute,
|
|
1071
|
-
createModuleApp,
|
|
1072
1686
|
createTestApp,
|
|
1073
1687
|
describeWorkflow,
|
|
1074
1688
|
parseWorkflowName
|