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