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