@stardeck-customer-apps/testing 0.3.1 → 0.5.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 +65 -0
- package/dist/index.d.mts +212 -67
- package/dist/index.d.ts +212 -67
- package/dist/index.js +990 -233
- package/dist/index.mjs +988 -235
- package/dist/next/headers-shim.js +17 -1
- package/dist/next/headers-shim.mjs +17 -1
- package/dist/setup.js +683 -108
- package/dist/setup.mjs +683 -108
- package/package.json +17 -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,23 @@ 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: [],
|
|
67
|
+
edgePrints: [],
|
|
68
|
+
edgeDisplays: [],
|
|
69
|
+
edgeTestPrints: [],
|
|
70
|
+
edgePrintCounter: 0,
|
|
71
|
+
edgeDevices: [],
|
|
72
|
+
edgePeripherals: [],
|
|
73
|
+
edgeBindings: /* @__PURE__ */ new Map(),
|
|
58
74
|
allowNetwork: false
|
|
59
75
|
}));
|
|
60
76
|
function requireDb() {
|
|
@@ -70,13 +86,16 @@ function requireDb() {
|
|
|
70
86
|
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
71
87
|
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
72
88
|
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
89
|
+
var STORAGE_TEST_URL = "https://storage.stardeck.test";
|
|
90
|
+
var STORAGE_TEST_HOST = "storage.stardeck.test";
|
|
73
91
|
var TEST_ENV_DEFAULTS = {
|
|
74
92
|
CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
|
|
75
93
|
DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
|
|
76
94
|
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
77
95
|
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
78
96
|
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
79
|
-
DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main
|
|
97
|
+
DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`,
|
|
98
|
+
STORAGE_URL: STORAGE_TEST_URL
|
|
80
99
|
};
|
|
81
100
|
var DEFAULT_TEST_USER = {
|
|
82
101
|
id: "test-user-1",
|
|
@@ -114,8 +133,8 @@ function verifyDeploymentAuthHeader(secret, header) {
|
|
|
114
133
|
return null;
|
|
115
134
|
}
|
|
116
135
|
if (payload.type !== "deployment-request") return null;
|
|
117
|
-
const
|
|
118
|
-
if (Math.abs(
|
|
136
|
+
const now3 = Math.floor(Date.now() / 1e3);
|
|
137
|
+
if (Math.abs(now3 - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
|
|
119
138
|
return payload;
|
|
120
139
|
}
|
|
121
140
|
|
|
@@ -132,6 +151,13 @@ function success(data) {
|
|
|
132
151
|
function failure(error, status = 400) {
|
|
133
152
|
return json({ success: false, error }, status);
|
|
134
153
|
}
|
|
154
|
+
async function readJsonBody(request) {
|
|
155
|
+
try {
|
|
156
|
+
return await request.json();
|
|
157
|
+
} catch {
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
135
161
|
|
|
136
162
|
// src/simulator/data-store.ts
|
|
137
163
|
function quoteIdent(name) {
|
|
@@ -614,13 +640,6 @@ function now() {
|
|
|
614
640
|
function linksFor(identityId) {
|
|
615
641
|
return state.identityLinks.filter((l) => l.identityId === identityId);
|
|
616
642
|
}
|
|
617
|
-
async function readBody(request) {
|
|
618
|
-
try {
|
|
619
|
-
return await request.json();
|
|
620
|
-
} catch {
|
|
621
|
-
return {};
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
643
|
function handleList(request) {
|
|
625
644
|
const typeParam = new URL(request.url).searchParams.get("type");
|
|
626
645
|
const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
|
|
@@ -628,7 +647,7 @@ function handleList(request) {
|
|
|
628
647
|
return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
|
|
629
648
|
}
|
|
630
649
|
async function handleCreate(request) {
|
|
631
|
-
const body = await
|
|
650
|
+
const body = await readJsonBody(request);
|
|
632
651
|
const type = body.type;
|
|
633
652
|
if (type !== "person" && type !== "account") {
|
|
634
653
|
return failure("type must be 'person' or 'account'");
|
|
@@ -665,7 +684,7 @@ function handleGet(identityId) {
|
|
|
665
684
|
async function handleUpdate(identityId, request) {
|
|
666
685
|
const identity = state.identities.get(identityId);
|
|
667
686
|
if (!identity) return failure("identity not found", 404);
|
|
668
|
-
const body = await
|
|
687
|
+
const body = await readJsonBody(request);
|
|
669
688
|
if (body.displayName !== void 0) {
|
|
670
689
|
identity.displayName = body.displayName;
|
|
671
690
|
}
|
|
@@ -687,7 +706,7 @@ async function handleAttachLink(identityId, request) {
|
|
|
687
706
|
if (identity.status !== "active") {
|
|
688
707
|
return failure("links attach only to active persons");
|
|
689
708
|
}
|
|
690
|
-
const body = await
|
|
709
|
+
const body = await readJsonBody(request);
|
|
691
710
|
const kind = body.kind;
|
|
692
711
|
const externalId = body.externalId;
|
|
693
712
|
if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
|
|
@@ -712,108 +731,16 @@ async function handleAttachLink(identityId, request) {
|
|
|
712
731
|
state.identityLinks.push(link);
|
|
713
732
|
return success({ link });
|
|
714
733
|
}
|
|
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
734
|
async function handleIdentitiesRequest(request, subPath) {
|
|
800
735
|
const method = request.method;
|
|
801
736
|
if (subPath === "" || subPath === "/") {
|
|
802
737
|
if (method === "GET") return handleList(request);
|
|
803
738
|
if (method === "POST") return handleCreate(request);
|
|
804
739
|
}
|
|
805
|
-
if (subPath === "/resolve" && method === "POST") {
|
|
806
|
-
return handleResolve(request);
|
|
807
|
-
}
|
|
808
740
|
const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
|
|
809
741
|
if (linksMatch && method === "POST") {
|
|
810
742
|
return handleAttachLink(linksMatch[1], request);
|
|
811
743
|
}
|
|
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
744
|
const singleMatch = subPath.match(/^\/([^/]+)$/);
|
|
818
745
|
if (singleMatch) {
|
|
819
746
|
if (method === "GET") return handleGet(singleMatch[1]);
|
|
@@ -836,6 +763,897 @@ function createDirectory() {
|
|
|
836
763
|
};
|
|
837
764
|
}
|
|
838
765
|
|
|
766
|
+
// src/simulator/payments.ts
|
|
767
|
+
import crypto5 from "crypto";
|
|
768
|
+
|
|
769
|
+
// src/next/headers-shim.ts
|
|
770
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
771
|
+
var requestScopeStorage = globalSingleton(
|
|
772
|
+
"request-scope",
|
|
773
|
+
() => new AsyncLocalStorage()
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
// src/next/call-route.ts
|
|
777
|
+
function parseCookieHeader(header) {
|
|
778
|
+
const map = /* @__PURE__ */ new Map();
|
|
779
|
+
if (!header) return map;
|
|
780
|
+
for (const part of header.split(";")) {
|
|
781
|
+
const eq = part.indexOf("=");
|
|
782
|
+
if (eq === -1) continue;
|
|
783
|
+
map.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
|
|
784
|
+
}
|
|
785
|
+
return map;
|
|
786
|
+
}
|
|
787
|
+
async function importNextServer() {
|
|
788
|
+
try {
|
|
789
|
+
return await import("next/server.js");
|
|
790
|
+
} catch {
|
|
791
|
+
return await import("next/server");
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
async function callRoute(handler, options = {}) {
|
|
795
|
+
const { NextRequest } = await importNextServer();
|
|
796
|
+
const path = options.path ?? "/api/test-route";
|
|
797
|
+
const url = new URL(`http://localhost:3333${path}`);
|
|
798
|
+
for (const [key, value] of Object.entries(options.searchParams ?? {})) {
|
|
799
|
+
url.searchParams.set(key, value);
|
|
800
|
+
}
|
|
801
|
+
const method = options.method ?? (options.body !== void 0 ? "POST" : "GET");
|
|
802
|
+
const headers = new Headers(options.headers);
|
|
803
|
+
const user = options.user !== void 0 ? options.user : state.currentUser;
|
|
804
|
+
if (user && !headers.has("x-stardeck-user")) {
|
|
805
|
+
headers.set("x-stardeck-user", JSON.stringify(user));
|
|
806
|
+
}
|
|
807
|
+
if (options.body !== void 0 && !headers.has("Content-Type")) {
|
|
808
|
+
headers.set("Content-Type", "application/json");
|
|
809
|
+
}
|
|
810
|
+
const cookiePairs = Object.entries(options.cookies ?? {});
|
|
811
|
+
if (cookiePairs.length > 0) {
|
|
812
|
+
const existing = headers.get("cookie");
|
|
813
|
+
const cookieString = cookiePairs.map(([k, v]) => `${k}=${v}`).join("; ");
|
|
814
|
+
headers.set("cookie", existing ? `${existing}; ${cookieString}` : cookieString);
|
|
815
|
+
}
|
|
816
|
+
const request = new NextRequest(url, {
|
|
817
|
+
method,
|
|
818
|
+
headers,
|
|
819
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
820
|
+
});
|
|
821
|
+
const scope = {
|
|
822
|
+
headers,
|
|
823
|
+
cookies: parseCookieHeader(headers.get("cookie"))
|
|
824
|
+
};
|
|
825
|
+
try {
|
|
826
|
+
return await requestScopeStorage.run(
|
|
827
|
+
scope,
|
|
828
|
+
() => Promise.resolve(
|
|
829
|
+
handler(request, {
|
|
830
|
+
params: Promise.resolve(options.params ?? {})
|
|
831
|
+
})
|
|
832
|
+
)
|
|
833
|
+
);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
const redirect = decodeNextRedirect(error);
|
|
836
|
+
if (redirect) return redirect;
|
|
837
|
+
throw error;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function decodeNextRedirect(error) {
|
|
841
|
+
const digest = error?.digest;
|
|
842
|
+
if (typeof digest !== "string" || !digest.startsWith("NEXT_REDIRECT")) return null;
|
|
843
|
+
const parts = digest.split(";");
|
|
844
|
+
const location = parts[2] ?? "/";
|
|
845
|
+
const status = Number(parts[3]) || 307;
|
|
846
|
+
return new Response(null, { status, headers: { location } });
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/simulator/webhook-signing.ts
|
|
850
|
+
import crypto4 from "crypto";
|
|
851
|
+
function signEventDelivery(secret, context, rawBody) {
|
|
852
|
+
const payload = {
|
|
853
|
+
type: "deployment-request",
|
|
854
|
+
organizationId: context.organizationId,
|
|
855
|
+
projectId: context.projectId,
|
|
856
|
+
deploymentId: context.deploymentId,
|
|
857
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
858
|
+
nonce: crypto4.randomUUID()
|
|
859
|
+
};
|
|
860
|
+
const payloadJson = JSON.stringify(payload);
|
|
861
|
+
const payloadB64 = Buffer.from(payloadJson).toString("base64");
|
|
862
|
+
const signature = crypto4.createHmac("sha256", secret).update(payloadJson).update(rawBody).digest("hex");
|
|
863
|
+
return `${payloadB64}.${signature}`;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/simulator/payments.ts
|
|
867
|
+
function payErr(error, status = 400, code) {
|
|
868
|
+
return json(code ? { error, code } : { error }, status);
|
|
869
|
+
}
|
|
870
|
+
function nextCheckoutId() {
|
|
871
|
+
state.checkoutCounter += 1;
|
|
872
|
+
return `cs_test_${state.checkoutCounter}`;
|
|
873
|
+
}
|
|
874
|
+
function nextPaymentLinkId() {
|
|
875
|
+
state.checkoutCounter += 1;
|
|
876
|
+
return `plink_test_${state.checkoutCounter}`;
|
|
877
|
+
}
|
|
878
|
+
function seedStripeSession(id, body) {
|
|
879
|
+
const lineItems = body.lineItems ?? [];
|
|
880
|
+
let amountTotal = null;
|
|
881
|
+
let currency = null;
|
|
882
|
+
if (lineItems.length > 0) {
|
|
883
|
+
amountTotal = lineItems.reduce(
|
|
884
|
+
(sum, item) => sum + (item.priceData?.unitAmount ?? 0) * (item.quantity ?? 1),
|
|
885
|
+
0
|
|
886
|
+
);
|
|
887
|
+
currency = lineItems[0].priceData?.currency ?? null;
|
|
888
|
+
}
|
|
889
|
+
state.sessionStatuses.set(id, {
|
|
890
|
+
id,
|
|
891
|
+
status: "open",
|
|
892
|
+
paymentStatus: "unpaid",
|
|
893
|
+
mode: body.mode ?? "payment",
|
|
894
|
+
amountTotal,
|
|
895
|
+
currency,
|
|
896
|
+
customerEmail: body.customerEmail ? String(body.customerEmail) : null,
|
|
897
|
+
metadata: body.metadata ?? {},
|
|
898
|
+
expiresAt: Math.floor(Date.now() / 1e3) + 3600
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
function seedBeamLink(id, body, merchantId) {
|
|
902
|
+
const order = body.order;
|
|
903
|
+
state.paymentLinks.set(id, {
|
|
904
|
+
paymentLinkId: id,
|
|
905
|
+
merchantId,
|
|
906
|
+
url: `https://beam.test/pay/${id}`,
|
|
907
|
+
status: "ACTIVE",
|
|
908
|
+
order: {
|
|
909
|
+
netAmount: Number(order?.netAmount ?? 0),
|
|
910
|
+
currency: String(order?.currency ?? "THB"),
|
|
911
|
+
description: String(order?.description ?? ""),
|
|
912
|
+
referenceId: order?.referenceId ? String(order.referenceId) : void 0,
|
|
913
|
+
internalNote: order?.internalNote ? String(order.internalNote) : void 0,
|
|
914
|
+
orderItems: order?.orderItems
|
|
915
|
+
},
|
|
916
|
+
redirectUrl: body.redirectUrl ? String(body.redirectUrl) : void 0,
|
|
917
|
+
linkSettings: body.linkSettings,
|
|
918
|
+
collectDeliveryAddress: body.collectDeliveryAddress === true
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
async function deliverEvent(envelope, handler, options) {
|
|
922
|
+
const rawBody = JSON.stringify(envelope);
|
|
923
|
+
const secret = options?.deploymentSecret ?? TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET;
|
|
924
|
+
const authHeader = signEventDelivery(
|
|
925
|
+
secret,
|
|
926
|
+
{
|
|
927
|
+
organizationId: TEST_ENV_DEFAULTS.ORGANIZATION_ID,
|
|
928
|
+
projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
|
|
929
|
+
deploymentId: TEST_ENV_DEFAULTS.DEPLOYMENT_ID
|
|
930
|
+
},
|
|
931
|
+
rawBody
|
|
932
|
+
);
|
|
933
|
+
return callRoute(handler, {
|
|
934
|
+
method: "POST",
|
|
935
|
+
path: options?.path ?? "/api/payments/webhooks",
|
|
936
|
+
params: { path: ["webhooks"] },
|
|
937
|
+
headers: { "X-Stardeck-Auth": authHeader },
|
|
938
|
+
body: envelope
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
async function handlePaymentsRequest(request, url) {
|
|
942
|
+
const pathname = url.pathname;
|
|
943
|
+
if (/\/bolt-connections/.test(pathname) || /\/bolt-intents/.test(pathname) || /\/charges(\/|$)/.test(pathname) || /\/billing-portal$/.test(pathname)) {
|
|
944
|
+
return payErr("Not found", 404, "NOT_FOUND");
|
|
945
|
+
}
|
|
946
|
+
const beamProductsMatch = pathname.match(
|
|
947
|
+
/^\/api\/store\/beam\/([^/]+)\/payment-links(?:\/([^/]+))?$/
|
|
948
|
+
);
|
|
949
|
+
if (beamProductsMatch) {
|
|
950
|
+
const merchantId = beamProductsMatch[1];
|
|
951
|
+
const linkId = beamProductsMatch[2];
|
|
952
|
+
if (!linkId && request.method === "POST") {
|
|
953
|
+
const body = await readJsonBody(request);
|
|
954
|
+
const id = nextPaymentLinkId();
|
|
955
|
+
const checkoutUrl = `https://beam.test/pay/${id}`;
|
|
956
|
+
const captured = {
|
|
957
|
+
id,
|
|
958
|
+
url: checkoutUrl,
|
|
959
|
+
provider: "beam",
|
|
960
|
+
options: body,
|
|
961
|
+
metadata: body.metadata,
|
|
962
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
963
|
+
};
|
|
964
|
+
state.checkouts.push(captured);
|
|
965
|
+
seedBeamLink(id, body, merchantId);
|
|
966
|
+
return json({ id, url: checkoutUrl });
|
|
967
|
+
}
|
|
968
|
+
if (linkId && request.method === "GET") {
|
|
969
|
+
const link = state.paymentLinks.get(linkId);
|
|
970
|
+
if (!link) return payErr("Payment link not found", 404, "NOT_FOUND");
|
|
971
|
+
return json({ paymentLink: link });
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
const stripeStoreMatch = pathname.match(/^\/api\/store\/([^/]+)\/(.+)$/);
|
|
975
|
+
if (stripeStoreMatch) {
|
|
976
|
+
const accountId = stripeStoreMatch[1];
|
|
977
|
+
const subPath = stripeStoreMatch[2];
|
|
978
|
+
if (accountId === "beam") {
|
|
979
|
+
return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
|
|
980
|
+
}
|
|
981
|
+
if (subPath === "products" && request.method === "GET") {
|
|
982
|
+
return json({ products: state.products });
|
|
983
|
+
}
|
|
984
|
+
const productMatch = subPath.match(/^products\/([^/]+)$/);
|
|
985
|
+
if (productMatch && request.method === "GET") {
|
|
986
|
+
const product = state.products.find((p) => p.id === productMatch[1]);
|
|
987
|
+
if (!product) return payErr("Product not found", 404, "NOT_FOUND");
|
|
988
|
+
return json({ product });
|
|
989
|
+
}
|
|
990
|
+
if (subPath === "checkout" && request.method === "POST") {
|
|
991
|
+
const body = await readJsonBody(request);
|
|
992
|
+
const id = nextCheckoutId();
|
|
993
|
+
const checkoutUrl = `https://checkout.stripe.test/c/pay/${id}`;
|
|
994
|
+
const captured = {
|
|
995
|
+
id,
|
|
996
|
+
url: checkoutUrl,
|
|
997
|
+
provider: "stripe",
|
|
998
|
+
options: body,
|
|
999
|
+
mode: body.mode,
|
|
1000
|
+
metadata: body.metadata,
|
|
1001
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1002
|
+
};
|
|
1003
|
+
state.checkouts.push(captured);
|
|
1004
|
+
seedStripeSession(id, body);
|
|
1005
|
+
return json({ id, url: checkoutUrl });
|
|
1006
|
+
}
|
|
1007
|
+
const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
|
|
1008
|
+
if (sessionMatch && request.method === "GET") {
|
|
1009
|
+
const session = state.sessionStatuses.get(sessionMatch[1]);
|
|
1010
|
+
if (!session) return payErr("Checkout session not found", 404, "NOT_FOUND");
|
|
1011
|
+
return json({ session });
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
|
|
1015
|
+
}
|
|
1016
|
+
function createPayments() {
|
|
1017
|
+
return {
|
|
1018
|
+
get checkouts() {
|
|
1019
|
+
return [...state.checkouts];
|
|
1020
|
+
},
|
|
1021
|
+
latest() {
|
|
1022
|
+
return state.checkouts[state.checkouts.length - 1];
|
|
1023
|
+
},
|
|
1024
|
+
setProducts(products) {
|
|
1025
|
+
state.products = products;
|
|
1026
|
+
},
|
|
1027
|
+
markPaid(id) {
|
|
1028
|
+
const session = state.sessionStatuses.get(id);
|
|
1029
|
+
if (session) {
|
|
1030
|
+
session.status = "complete";
|
|
1031
|
+
session.paymentStatus = "paid";
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const link = state.paymentLinks.get(id);
|
|
1035
|
+
if (link) {
|
|
1036
|
+
link.status = "PAID";
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
throw new Error(`[stardeck-testing] Unknown checkout or payment link id: ${id}`);
|
|
1040
|
+
},
|
|
1041
|
+
setSessionStatus(id, status) {
|
|
1042
|
+
const session = state.sessionStatuses.get(id);
|
|
1043
|
+
if (!session) {
|
|
1044
|
+
throw new Error(`[stardeck-testing] Unknown checkout session id: ${id}`);
|
|
1045
|
+
}
|
|
1046
|
+
Object.assign(session, status);
|
|
1047
|
+
},
|
|
1048
|
+
setPaymentLinkStatus(id, status) {
|
|
1049
|
+
const link = state.paymentLinks.get(id);
|
|
1050
|
+
if (!link) {
|
|
1051
|
+
throw new Error(`[stardeck-testing] Unknown payment link id: ${id}`);
|
|
1052
|
+
}
|
|
1053
|
+
link.status = status;
|
|
1054
|
+
},
|
|
1055
|
+
async deliverStripeEvent(handler, event, options) {
|
|
1056
|
+
const envelope = {
|
|
1057
|
+
id: `evt_test_${crypto5.randomUUID()}`,
|
|
1058
|
+
kind: "stripe_webhook",
|
|
1059
|
+
timestamp: Date.now(),
|
|
1060
|
+
stripeEvent: {
|
|
1061
|
+
type: event.type,
|
|
1062
|
+
accountId: event.accountId ?? process.env.STRIPE_CONNECT_ACCOUNT_ID ?? "acct_test",
|
|
1063
|
+
data: event.data
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
return deliverEvent(envelope, handler, options);
|
|
1067
|
+
},
|
|
1068
|
+
async deliverBeamEvent(handler, event, options) {
|
|
1069
|
+
const envelope = {
|
|
1070
|
+
id: `beam_evt_test_${crypto5.randomUUID()}`,
|
|
1071
|
+
kind: "beam_webhook",
|
|
1072
|
+
timestamp: Date.now(),
|
|
1073
|
+
beamEvent: {
|
|
1074
|
+
type: event.type,
|
|
1075
|
+
payload: event.payload
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
return deliverEvent(envelope, handler, options);
|
|
1079
|
+
},
|
|
1080
|
+
clear() {
|
|
1081
|
+
state.checkouts = [];
|
|
1082
|
+
state.checkoutCounter = 0;
|
|
1083
|
+
state.sessionStatuses.clear();
|
|
1084
|
+
state.paymentLinks.clear();
|
|
1085
|
+
state.products = [];
|
|
1086
|
+
},
|
|
1087
|
+
get count() {
|
|
1088
|
+
return state.checkouts.length;
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/simulator/storage.ts
|
|
1094
|
+
import crypto6 from "crypto";
|
|
1095
|
+
function storageErr(error, status = 400) {
|
|
1096
|
+
return json({ error }, status);
|
|
1097
|
+
}
|
|
1098
|
+
function fileExtension(filename) {
|
|
1099
|
+
const dot = filename.lastIndexOf(".");
|
|
1100
|
+
return dot === -1 ? "bin" : filename.slice(dot + 1);
|
|
1101
|
+
}
|
|
1102
|
+
function buildKey(fileId, filename) {
|
|
1103
|
+
const orgId = process.env.ORGANIZATION_ID ?? TEST_ENV_DEFAULTS.ORGANIZATION_ID;
|
|
1104
|
+
const projectId = process.env.PROJECT_ID ?? TEST_ENV_DEFAULTS.PROJECT_ID;
|
|
1105
|
+
const deploymentId = process.env.DEPLOYMENT_ID ?? TEST_ENV_DEFAULTS.DEPLOYMENT_ID;
|
|
1106
|
+
return `${orgId}/${projectId}/${deploymentId}/${fileId}.${fileExtension(filename)}`;
|
|
1107
|
+
}
|
|
1108
|
+
function buildUrl(key) {
|
|
1109
|
+
return `${STORAGE_TEST_URL}/files/${key}`;
|
|
1110
|
+
}
|
|
1111
|
+
function toUploadResponse(record) {
|
|
1112
|
+
return {
|
|
1113
|
+
id: record.id,
|
|
1114
|
+
key: record.key,
|
|
1115
|
+
filename: record.filename,
|
|
1116
|
+
contentType: record.contentType,
|
|
1117
|
+
sizeBytes: record.sizeBytes,
|
|
1118
|
+
url: record.url,
|
|
1119
|
+
uploadedAt: record.uploadedAt,
|
|
1120
|
+
isPublic: record.isPublic,
|
|
1121
|
+
metadata: record.metadata
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
function captureUpload(record, method, path) {
|
|
1125
|
+
const captured = {
|
|
1126
|
+
id: record.id,
|
|
1127
|
+
key: record.key,
|
|
1128
|
+
filename: record.filename,
|
|
1129
|
+
contentType: record.contentType,
|
|
1130
|
+
sizeBytes: record.sizeBytes,
|
|
1131
|
+
url: record.url,
|
|
1132
|
+
uploadedAt: record.uploadedAt,
|
|
1133
|
+
isPublic: record.isPublic,
|
|
1134
|
+
metadata: record.metadata,
|
|
1135
|
+
method,
|
|
1136
|
+
path
|
|
1137
|
+
};
|
|
1138
|
+
state.uploads.push(captured);
|
|
1139
|
+
}
|
|
1140
|
+
function createFileRecord(input) {
|
|
1141
|
+
state.uploadCounter += 1;
|
|
1142
|
+
const id = `file_test_${state.uploadCounter}`;
|
|
1143
|
+
const key = buildKey(id, input.filename);
|
|
1144
|
+
return {
|
|
1145
|
+
id,
|
|
1146
|
+
key,
|
|
1147
|
+
filename: input.filename,
|
|
1148
|
+
contentType: input.contentType,
|
|
1149
|
+
sizeBytes: input.sizeBytes,
|
|
1150
|
+
url: buildUrl(key),
|
|
1151
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1152
|
+
metadata: input.metadata,
|
|
1153
|
+
isPublic: input.isPublic,
|
|
1154
|
+
deleted: false
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
function activeFiles() {
|
|
1158
|
+
return [...state.storageFiles.values()].filter((f) => !f.deleted);
|
|
1159
|
+
}
|
|
1160
|
+
async function handleStorageRequest(request, url) {
|
|
1161
|
+
const pathname = url.pathname;
|
|
1162
|
+
if (pathname.startsWith("/upload/presign/multipart")) {
|
|
1163
|
+
return storageErr("Not found", 404);
|
|
1164
|
+
}
|
|
1165
|
+
const presignedPutMatch = pathname.match(/^\/presigned\/([^/]+)$/);
|
|
1166
|
+
if (presignedPutMatch && request.method === "PUT") {
|
|
1167
|
+
const fileId = presignedPutMatch[1];
|
|
1168
|
+
if (!state.presignPending.has(fileId)) {
|
|
1169
|
+
return storageErr("Presign session not found", 404);
|
|
1170
|
+
}
|
|
1171
|
+
return new Response(null, {
|
|
1172
|
+
status: 200,
|
|
1173
|
+
headers: { etag: `"${crypto6.randomUUID()}"` }
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
if (pathname === "/upload/presign/complete" && request.method === "POST") {
|
|
1177
|
+
const body = await readJsonBody(request);
|
|
1178
|
+
const fileId = String(body.fileId ?? "");
|
|
1179
|
+
const pending = state.presignPending.get(fileId);
|
|
1180
|
+
if (!pending) return storageErr("Presign session not found", 404);
|
|
1181
|
+
const record = {
|
|
1182
|
+
id: fileId,
|
|
1183
|
+
key: pending.key,
|
|
1184
|
+
filename: pending.fileName,
|
|
1185
|
+
contentType: pending.contentType,
|
|
1186
|
+
sizeBytes: pending.sizeBytes,
|
|
1187
|
+
url: buildUrl(pending.key),
|
|
1188
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1189
|
+
metadata: pending.metadata,
|
|
1190
|
+
isPublic: pending.isPublic,
|
|
1191
|
+
deleted: false
|
|
1192
|
+
};
|
|
1193
|
+
state.storageFiles.set(record.id, record);
|
|
1194
|
+
state.presignPending.delete(fileId);
|
|
1195
|
+
captureUpload(record, "POST", "/upload/presign/complete");
|
|
1196
|
+
return json(toUploadResponse(record));
|
|
1197
|
+
}
|
|
1198
|
+
if (pathname === "/upload/presign" && request.method === "POST") {
|
|
1199
|
+
const body = await readJsonBody(request);
|
|
1200
|
+
const fileName = String(body.fileName ?? "upload.bin");
|
|
1201
|
+
const contentType = String(body.contentType ?? "application/octet-stream");
|
|
1202
|
+
const sizeBytes = Number(body.sizeBytes ?? 0);
|
|
1203
|
+
const isPublic = body.isPublic !== false;
|
|
1204
|
+
const metadata = body.metadata;
|
|
1205
|
+
state.uploadCounter += 1;
|
|
1206
|
+
const fileId = `file_test_${state.uploadCounter}`;
|
|
1207
|
+
const key = buildKey(fileId, fileName);
|
|
1208
|
+
state.presignPending.set(fileId, {
|
|
1209
|
+
fileName,
|
|
1210
|
+
contentType,
|
|
1211
|
+
sizeBytes,
|
|
1212
|
+
isPublic,
|
|
1213
|
+
metadata,
|
|
1214
|
+
key
|
|
1215
|
+
});
|
|
1216
|
+
return json({
|
|
1217
|
+
fileId,
|
|
1218
|
+
key,
|
|
1219
|
+
presignedUrl: `${STORAGE_TEST_URL}/presigned/${fileId}`,
|
|
1220
|
+
contentType,
|
|
1221
|
+
expiresIn: 3600
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
if (pathname === "/upload" && request.method === "POST") {
|
|
1225
|
+
const isPublic = url.searchParams.get("isPublic") !== "false";
|
|
1226
|
+
const formData = await request.formData();
|
|
1227
|
+
const file = formData.get("file");
|
|
1228
|
+
if (!(file instanceof Blob)) {
|
|
1229
|
+
return storageErr("file is required");
|
|
1230
|
+
}
|
|
1231
|
+
const filename = formData.get("filename") ?? (file instanceof File ? file.name : "upload.bin");
|
|
1232
|
+
const metadataRaw = formData.get("metadata");
|
|
1233
|
+
let metadata;
|
|
1234
|
+
if (typeof metadataRaw === "string" && metadataRaw) {
|
|
1235
|
+
try {
|
|
1236
|
+
metadata = JSON.parse(metadataRaw);
|
|
1237
|
+
} catch {
|
|
1238
|
+
return storageErr("metadata must be valid JSON");
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
const record = createFileRecord({
|
|
1242
|
+
filename,
|
|
1243
|
+
contentType: file.type || "application/octet-stream",
|
|
1244
|
+
sizeBytes: file.size,
|
|
1245
|
+
isPublic,
|
|
1246
|
+
metadata
|
|
1247
|
+
});
|
|
1248
|
+
state.storageFiles.set(record.id, record);
|
|
1249
|
+
captureUpload(record, "POST", "/upload");
|
|
1250
|
+
return json(toUploadResponse(record));
|
|
1251
|
+
}
|
|
1252
|
+
const fileMatch = pathname.match(/^\/api\/files\/([^/]+)$/);
|
|
1253
|
+
if (fileMatch) {
|
|
1254
|
+
const fileId = fileMatch[1];
|
|
1255
|
+
const record = state.storageFiles.get(fileId);
|
|
1256
|
+
if (!record || record.deleted) {
|
|
1257
|
+
return storageErr("File not found", 404);
|
|
1258
|
+
}
|
|
1259
|
+
if (request.method === "GET") {
|
|
1260
|
+
return json(toUploadResponse(record));
|
|
1261
|
+
}
|
|
1262
|
+
if (request.method === "PATCH") {
|
|
1263
|
+
const body = await readJsonBody(request);
|
|
1264
|
+
if (body.fileName !== void 0) record.filename = String(body.fileName);
|
|
1265
|
+
if (body.metadata !== void 0) {
|
|
1266
|
+
record.metadata = body.metadata;
|
|
1267
|
+
}
|
|
1268
|
+
if (body.isPublic !== void 0) record.isPublic = body.isPublic === true;
|
|
1269
|
+
return json(toUploadResponse(record));
|
|
1270
|
+
}
|
|
1271
|
+
if (request.method === "DELETE") {
|
|
1272
|
+
record.deleted = true;
|
|
1273
|
+
return json({ success: true });
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (pathname === "/api/files" && request.method === "GET") {
|
|
1277
|
+
const parseParam = (raw, fallback, min) => {
|
|
1278
|
+
const n = Number(raw);
|
|
1279
|
+
return Number.isFinite(n) && n >= min ? Math.floor(n) : fallback;
|
|
1280
|
+
};
|
|
1281
|
+
const limit = parseParam(url.searchParams.get("limit"), 50, 1);
|
|
1282
|
+
const offset = parseParam(url.searchParams.get("offset"), 0, 0);
|
|
1283
|
+
const prefix = url.searchParams.get("prefix") ?? "";
|
|
1284
|
+
let files = activeFiles();
|
|
1285
|
+
if (prefix) {
|
|
1286
|
+
files = files.filter((f) => f.key.startsWith(prefix) || f.filename.startsWith(prefix));
|
|
1287
|
+
}
|
|
1288
|
+
const total = files.length;
|
|
1289
|
+
const slice = files.slice(offset, offset + limit);
|
|
1290
|
+
return json({
|
|
1291
|
+
files: slice.map((f) => toUploadResponse(f)),
|
|
1292
|
+
total,
|
|
1293
|
+
limit,
|
|
1294
|
+
offset,
|
|
1295
|
+
hasMore: offset + slice.length < total
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
return storageErr(`No storage simulator for ${request.method} ${pathname}`, 404);
|
|
1299
|
+
}
|
|
1300
|
+
function createStorage() {
|
|
1301
|
+
return {
|
|
1302
|
+
get uploads() {
|
|
1303
|
+
return [...state.uploads];
|
|
1304
|
+
},
|
|
1305
|
+
latest() {
|
|
1306
|
+
return state.uploads[state.uploads.length - 1];
|
|
1307
|
+
},
|
|
1308
|
+
clear() {
|
|
1309
|
+
state.uploads = [];
|
|
1310
|
+
state.uploadCounter = 0;
|
|
1311
|
+
state.storageFiles.clear();
|
|
1312
|
+
state.presignPending.clear();
|
|
1313
|
+
},
|
|
1314
|
+
get count() {
|
|
1315
|
+
return state.uploads.length;
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/simulator/messaging.ts
|
|
1321
|
+
import crypto7 from "crypto";
|
|
1322
|
+
function captureMessage(channel, recipient, body, connectionId) {
|
|
1323
|
+
const message = {
|
|
1324
|
+
channel,
|
|
1325
|
+
recipient,
|
|
1326
|
+
body: {
|
|
1327
|
+
text: body.text ? String(body.text) : void 0,
|
|
1328
|
+
blocks: body.blocks,
|
|
1329
|
+
threadTs: body.threadTs ? String(body.threadTs) : void 0,
|
|
1330
|
+
messagingType: body.messagingType ? String(body.messagingType) : void 0,
|
|
1331
|
+
tag: body.tag ? String(body.tag) : void 0
|
|
1332
|
+
},
|
|
1333
|
+
connectionId,
|
|
1334
|
+
sentAt: /* @__PURE__ */ new Date()
|
|
1335
|
+
};
|
|
1336
|
+
state.messages.push(message);
|
|
1337
|
+
}
|
|
1338
|
+
async function handleMessagingRequest(request, channel, subPath) {
|
|
1339
|
+
const method = request.method;
|
|
1340
|
+
if ((subPath === "" || subPath === "/" || subPath === "/connections") && method === "GET") {
|
|
1341
|
+
return success({ connections: [] });
|
|
1342
|
+
}
|
|
1343
|
+
if (channel === "slack" && subPath === "/send" && method === "POST") {
|
|
1344
|
+
const body = await readJsonBody(request);
|
|
1345
|
+
if (!body.text && !body.blocks) {
|
|
1346
|
+
return failure("text or blocks is required");
|
|
1347
|
+
}
|
|
1348
|
+
const slackChannel = String(body.channel ?? "");
|
|
1349
|
+
if (!slackChannel) {
|
|
1350
|
+
return failure("channel is required");
|
|
1351
|
+
}
|
|
1352
|
+
captureMessage(
|
|
1353
|
+
"slack",
|
|
1354
|
+
slackChannel,
|
|
1355
|
+
body,
|
|
1356
|
+
body.connectionId ? String(body.connectionId) : void 0
|
|
1357
|
+
);
|
|
1358
|
+
return success({
|
|
1359
|
+
ok: true,
|
|
1360
|
+
ts: `${Date.now()}.${crypto7.randomUUID().slice(0, 6)}`,
|
|
1361
|
+
channel: slackChannel
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
if (channel === "line" && subPath === "/push" && method === "POST") {
|
|
1365
|
+
const body = await readJsonBody(request);
|
|
1366
|
+
const userId = String(body.userId ?? "");
|
|
1367
|
+
const message = body.message;
|
|
1368
|
+
if (!userId) return failure("userId is required");
|
|
1369
|
+
if (!message || message.type !== "text" || !message.text) {
|
|
1370
|
+
return failure("message must be { type: 'text', text: string }");
|
|
1371
|
+
}
|
|
1372
|
+
if (message.text.length > 5e3) {
|
|
1373
|
+
return failure("message text exceeds maximum length");
|
|
1374
|
+
}
|
|
1375
|
+
captureMessage(
|
|
1376
|
+
"line",
|
|
1377
|
+
userId,
|
|
1378
|
+
{ text: message.text },
|
|
1379
|
+
body.connectionId ? String(body.connectionId) : void 0
|
|
1380
|
+
);
|
|
1381
|
+
return success({ success: true });
|
|
1382
|
+
}
|
|
1383
|
+
if (channel === "facebook" && subPath === "/send" && method === "POST") {
|
|
1384
|
+
const body = await readJsonBody(request);
|
|
1385
|
+
const recipientId = String(body.recipientId ?? "");
|
|
1386
|
+
const message = body.message;
|
|
1387
|
+
if (!recipientId) return failure("recipientId is required");
|
|
1388
|
+
if (!message || message.type !== "text" || !message.text) {
|
|
1389
|
+
return failure("message must be { type: 'text', text: string }");
|
|
1390
|
+
}
|
|
1391
|
+
if (message.text.length > 2e3) {
|
|
1392
|
+
return failure("message text exceeds maximum length");
|
|
1393
|
+
}
|
|
1394
|
+
captureMessage(
|
|
1395
|
+
"facebook",
|
|
1396
|
+
recipientId,
|
|
1397
|
+
{
|
|
1398
|
+
text: message.text,
|
|
1399
|
+
messagingType: body.messagingType ? String(body.messagingType) : void 0,
|
|
1400
|
+
tag: body.tag ? String(body.tag) : void 0
|
|
1401
|
+
},
|
|
1402
|
+
body.connectionId ? String(body.connectionId) : void 0
|
|
1403
|
+
);
|
|
1404
|
+
return success({ success: true });
|
|
1405
|
+
}
|
|
1406
|
+
return failure(`No messaging simulator for ${method} .../integrations/${channel}${subPath}`, 404);
|
|
1407
|
+
}
|
|
1408
|
+
function createMessages() {
|
|
1409
|
+
return {
|
|
1410
|
+
all() {
|
|
1411
|
+
return [...state.messages];
|
|
1412
|
+
},
|
|
1413
|
+
latest() {
|
|
1414
|
+
return state.messages[state.messages.length - 1];
|
|
1415
|
+
},
|
|
1416
|
+
to(recipient) {
|
|
1417
|
+
return state.messages.filter((m) => m.recipient === recipient);
|
|
1418
|
+
},
|
|
1419
|
+
channel(kind) {
|
|
1420
|
+
const filtered = () => state.messages.filter((m) => m.channel === kind);
|
|
1421
|
+
return {
|
|
1422
|
+
all: () => filtered(),
|
|
1423
|
+
latest: () => {
|
|
1424
|
+
const pool = filtered();
|
|
1425
|
+
return pool[pool.length - 1];
|
|
1426
|
+
},
|
|
1427
|
+
to: (recipient) => filtered().filter((m) => m.recipient === recipient)
|
|
1428
|
+
};
|
|
1429
|
+
},
|
|
1430
|
+
clear() {
|
|
1431
|
+
state.messages = [];
|
|
1432
|
+
},
|
|
1433
|
+
get count() {
|
|
1434
|
+
return state.messages.length;
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// src/simulator/edge.ts
|
|
1440
|
+
function edgeFailure(error, status = 400, code) {
|
|
1441
|
+
return json({ success: false, error, ...code ? { code } : {} }, status);
|
|
1442
|
+
}
|
|
1443
|
+
function bindingNotFound(alias) {
|
|
1444
|
+
return edgeFailure(`No peripheral is paired to alias "${alias}"`, 404, "BINDING_NOT_FOUND");
|
|
1445
|
+
}
|
|
1446
|
+
function now2() {
|
|
1447
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1448
|
+
}
|
|
1449
|
+
function nextJobId() {
|
|
1450
|
+
state.edgePrintCounter += 1;
|
|
1451
|
+
return `job_${state.edgePrintCounter}`;
|
|
1452
|
+
}
|
|
1453
|
+
function nextConfigVersion() {
|
|
1454
|
+
return state.edgeDisplays.length + 1;
|
|
1455
|
+
}
|
|
1456
|
+
function findPeripheral(id) {
|
|
1457
|
+
return state.edgePeripherals.find((p) => p.id === id);
|
|
1458
|
+
}
|
|
1459
|
+
function buildBinding(alias, peripheralId) {
|
|
1460
|
+
const peripheral = findPeripheral(peripheralId);
|
|
1461
|
+
return {
|
|
1462
|
+
alias,
|
|
1463
|
+
state: peripheral ? "ok" : "peripheral_missing",
|
|
1464
|
+
peripheral: peripheral ? {
|
|
1465
|
+
id: peripheral.id,
|
|
1466
|
+
displayName: peripheral.displayName,
|
|
1467
|
+
driver: peripheral.driver,
|
|
1468
|
+
connected: peripheral.connected
|
|
1469
|
+
} : null,
|
|
1470
|
+
device: peripheral ? {
|
|
1471
|
+
id: peripheral.device.id,
|
|
1472
|
+
displayName: peripheral.device.displayName,
|
|
1473
|
+
status: peripheral.device.status
|
|
1474
|
+
} : null,
|
|
1475
|
+
updatedAt: now2()
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
function handleListDevices() {
|
|
1479
|
+
return success({ devices: [...state.edgeDevices] });
|
|
1480
|
+
}
|
|
1481
|
+
function handleListPeripherals(request) {
|
|
1482
|
+
const deviceId = new URL(request.url).searchParams.get("deviceId");
|
|
1483
|
+
const peripherals = deviceId ? state.edgePeripherals.filter((p) => p.device.id === deviceId) : [...state.edgePeripherals];
|
|
1484
|
+
return success({ peripherals });
|
|
1485
|
+
}
|
|
1486
|
+
function handleListBindings() {
|
|
1487
|
+
return success({ bindings: [...state.edgeBindings.values()] });
|
|
1488
|
+
}
|
|
1489
|
+
function handleGetBinding(alias) {
|
|
1490
|
+
const binding = state.edgeBindings.get(alias);
|
|
1491
|
+
if (!binding) return bindingNotFound(alias);
|
|
1492
|
+
return success({ binding });
|
|
1493
|
+
}
|
|
1494
|
+
async function handlePair(request) {
|
|
1495
|
+
const body = await readJsonBody(request);
|
|
1496
|
+
const alias = String(body.alias ?? "");
|
|
1497
|
+
const peripheralId = String(body.peripheralId ?? "");
|
|
1498
|
+
if (!alias) return edgeFailure("alias is required");
|
|
1499
|
+
if (!peripheralId) return edgeFailure("peripheralId is required");
|
|
1500
|
+
if (!findPeripheral(peripheralId)) {
|
|
1501
|
+
return edgeFailure("Peripheral not found or its device is not granted to this project", 404);
|
|
1502
|
+
}
|
|
1503
|
+
const binding = buildBinding(alias, peripheralId);
|
|
1504
|
+
state.edgeBindings.set(alias, binding);
|
|
1505
|
+
return success({ binding });
|
|
1506
|
+
}
|
|
1507
|
+
function handleUnpair(alias) {
|
|
1508
|
+
const existed = state.edgeBindings.delete(alias);
|
|
1509
|
+
if (!existed) return bindingNotFound(alias);
|
|
1510
|
+
return success({ deleted: true });
|
|
1511
|
+
}
|
|
1512
|
+
async function handlePrint(request) {
|
|
1513
|
+
const body = await readJsonBody(request);
|
|
1514
|
+
const jobId = nextJobId();
|
|
1515
|
+
const captured = {
|
|
1516
|
+
jobId,
|
|
1517
|
+
deploymentId: String(body.deploymentId ?? ""),
|
|
1518
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
1519
|
+
deviceId: body.deviceId ? String(body.deviceId) : void 0,
|
|
1520
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
|
|
1521
|
+
receipt: body.receipt ?? {},
|
|
1522
|
+
openDrawer: body.openDrawer === true,
|
|
1523
|
+
logo: body.logo === true ? true : body.logo === false ? false : void 0,
|
|
1524
|
+
copies: typeof body.copies === "number" ? body.copies : 1
|
|
1525
|
+
};
|
|
1526
|
+
state.edgePrints.push(captured);
|
|
1527
|
+
return success({
|
|
1528
|
+
jobId,
|
|
1529
|
+
status: "completed"
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
async function handleShowDisplay(request) {
|
|
1533
|
+
const body = await readJsonBody(request);
|
|
1534
|
+
const captured = {
|
|
1535
|
+
action: "show",
|
|
1536
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
1537
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
|
|
1538
|
+
url: body.url ? String(body.url) : void 0
|
|
1539
|
+
};
|
|
1540
|
+
state.edgeDisplays.push(captured);
|
|
1541
|
+
return success({
|
|
1542
|
+
configVersion: nextConfigVersion(),
|
|
1543
|
+
pushed: true,
|
|
1544
|
+
state: "showing"
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
function handleClearDisplay(request) {
|
|
1548
|
+
const url = new URL(request.url);
|
|
1549
|
+
const alias = url.searchParams.get("alias");
|
|
1550
|
+
const peripheralId = url.searchParams.get("peripheralId");
|
|
1551
|
+
const captured = {
|
|
1552
|
+
action: "clear",
|
|
1553
|
+
alias: alias ?? void 0,
|
|
1554
|
+
peripheralId: peripheralId ?? void 0
|
|
1555
|
+
};
|
|
1556
|
+
state.edgeDisplays.push(captured);
|
|
1557
|
+
return success({
|
|
1558
|
+
configVersion: nextConfigVersion(),
|
|
1559
|
+
pushed: true,
|
|
1560
|
+
state: "cleared"
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
async function handleTestPrint(request) {
|
|
1564
|
+
const body = await readJsonBody(request);
|
|
1565
|
+
const captured = {
|
|
1566
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
1567
|
+
deviceId: body.deviceId ? String(body.deviceId) : void 0,
|
|
1568
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0
|
|
1569
|
+
};
|
|
1570
|
+
state.edgeTestPrints.push(captured);
|
|
1571
|
+
return success({
|
|
1572
|
+
status: "ok",
|
|
1573
|
+
peripheralId: captured.peripheralId ?? captured.alias ?? "default"
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
async function handleEdgeRequest(request, subPath) {
|
|
1577
|
+
const method = request.method;
|
|
1578
|
+
if (subPath === "/print" && method === "POST") {
|
|
1579
|
+
return handlePrint(request);
|
|
1580
|
+
}
|
|
1581
|
+
if (subPath === "/devices" && method === "GET") {
|
|
1582
|
+
return handleListDevices();
|
|
1583
|
+
}
|
|
1584
|
+
if (subPath === "/peripherals" && method === "GET") {
|
|
1585
|
+
return handleListPeripherals(request);
|
|
1586
|
+
}
|
|
1587
|
+
if (subPath === "/bindings" && method === "GET") {
|
|
1588
|
+
return handleListBindings();
|
|
1589
|
+
}
|
|
1590
|
+
if (subPath === "/bindings" && method === "PUT") {
|
|
1591
|
+
return handlePair(request);
|
|
1592
|
+
}
|
|
1593
|
+
const bindingMatch = subPath.match(/^\/bindings\/([^/]+)$/);
|
|
1594
|
+
if (bindingMatch) {
|
|
1595
|
+
const alias = decodeURIComponent(bindingMatch[1]);
|
|
1596
|
+
if (method === "GET") return handleGetBinding(alias);
|
|
1597
|
+
if (method === "DELETE") return handleUnpair(alias);
|
|
1598
|
+
}
|
|
1599
|
+
if (subPath === "/display" && method === "POST") {
|
|
1600
|
+
return handleShowDisplay(request);
|
|
1601
|
+
}
|
|
1602
|
+
if (subPath === "/display" && method === "DELETE") {
|
|
1603
|
+
return handleClearDisplay(request);
|
|
1604
|
+
}
|
|
1605
|
+
if (subPath === "/test-print" && method === "POST") {
|
|
1606
|
+
return handleTestPrint(request);
|
|
1607
|
+
}
|
|
1608
|
+
return edgeFailure(`No edge simulator for ${method} .../edge${subPath}`, 404);
|
|
1609
|
+
}
|
|
1610
|
+
function createEdge() {
|
|
1611
|
+
return {
|
|
1612
|
+
get prints() {
|
|
1613
|
+
return [...state.edgePrints];
|
|
1614
|
+
},
|
|
1615
|
+
get displays() {
|
|
1616
|
+
return [...state.edgeDisplays];
|
|
1617
|
+
},
|
|
1618
|
+
get testPrints() {
|
|
1619
|
+
return [...state.edgeTestPrints];
|
|
1620
|
+
},
|
|
1621
|
+
latestPrint() {
|
|
1622
|
+
return state.edgePrints[state.edgePrints.length - 1];
|
|
1623
|
+
},
|
|
1624
|
+
latestDisplay() {
|
|
1625
|
+
return state.edgeDisplays[state.edgeDisplays.length - 1];
|
|
1626
|
+
},
|
|
1627
|
+
get bindings() {
|
|
1628
|
+
return [...state.edgeBindings.values()];
|
|
1629
|
+
},
|
|
1630
|
+
seedDevices(devices) {
|
|
1631
|
+
state.edgeDevices = [...devices];
|
|
1632
|
+
},
|
|
1633
|
+
seedPeripherals(peripherals) {
|
|
1634
|
+
state.edgePeripherals = [...peripherals];
|
|
1635
|
+
},
|
|
1636
|
+
seedBindings(bindings) {
|
|
1637
|
+
state.edgeBindings.clear();
|
|
1638
|
+
for (const binding of bindings) {
|
|
1639
|
+
state.edgeBindings.set(binding.alias, binding);
|
|
1640
|
+
}
|
|
1641
|
+
},
|
|
1642
|
+
clear() {
|
|
1643
|
+
state.edgePrints = [];
|
|
1644
|
+
state.edgeDisplays = [];
|
|
1645
|
+
state.edgeTestPrints = [];
|
|
1646
|
+
state.edgePrintCounter = 0;
|
|
1647
|
+
state.edgeDevices = [];
|
|
1648
|
+
state.edgePeripherals = [];
|
|
1649
|
+
state.edgeBindings.clear();
|
|
1650
|
+
},
|
|
1651
|
+
get count() {
|
|
1652
|
+
return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length;
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
|
|
839
1657
|
// src/simulator/router.ts
|
|
840
1658
|
var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
841
1659
|
originalFetch: null
|
|
@@ -843,6 +1661,22 @@ var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
|
843
1661
|
function isLocalHost(hostname) {
|
|
844
1662
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0";
|
|
845
1663
|
}
|
|
1664
|
+
function requiresDeploymentHmac(request, url) {
|
|
1665
|
+
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
1666
|
+
const isPresignedPut = isStorageHost && request.method === "PUT" && /^\/presigned\/[^/]+$/.test(url.pathname);
|
|
1667
|
+
if (isStorageHost) {
|
|
1668
|
+
return !isPresignedPut;
|
|
1669
|
+
}
|
|
1670
|
+
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
1671
|
+
const isEmail = url.pathname === "/api/email/send";
|
|
1672
|
+
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
1673
|
+
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
1674
|
+
const messagingMatch = url.pathname.match(
|
|
1675
|
+
/^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
|
|
1676
|
+
);
|
|
1677
|
+
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
1678
|
+
return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
1679
|
+
}
|
|
846
1680
|
async function handleSimulatedRequest(request, url) {
|
|
847
1681
|
if (url.pathname === "/sql") {
|
|
848
1682
|
return handleNeonSql(requireDb(), request);
|
|
@@ -854,7 +1688,13 @@ async function handleSimulatedRequest(request, url) {
|
|
|
854
1688
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
855
1689
|
const isEmail = url.pathname === "/api/email/send";
|
|
856
1690
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
857
|
-
|
|
1691
|
+
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
1692
|
+
const messagingMatch = url.pathname.match(
|
|
1693
|
+
/^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
|
|
1694
|
+
);
|
|
1695
|
+
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
1696
|
+
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
1697
|
+
if (requiresDeploymentHmac(request, url)) {
|
|
858
1698
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
859
1699
|
if (!authHeader) {
|
|
860
1700
|
return failure("Missing authentication header", 401);
|
|
@@ -864,34 +1704,47 @@ async function handleSimulatedRequest(request, url) {
|
|
|
864
1704
|
return failure("Invalid authentication", 401);
|
|
865
1705
|
}
|
|
866
1706
|
}
|
|
1707
|
+
if (isStorageHost) {
|
|
1708
|
+
return handleStorageRequest(request, url);
|
|
1709
|
+
}
|
|
867
1710
|
if (isEmail && request.method === "POST") {
|
|
868
1711
|
return handleEmailSend(request);
|
|
869
1712
|
}
|
|
870
1713
|
if (identitiesMatch) {
|
|
871
1714
|
return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
|
|
872
1715
|
}
|
|
1716
|
+
if (storeMatch) {
|
|
1717
|
+
return handlePaymentsRequest(request, url);
|
|
1718
|
+
}
|
|
1719
|
+
if (messagingMatch) {
|
|
1720
|
+
const channel = messagingMatch[1];
|
|
1721
|
+
return handleMessagingRequest(request, channel, messagingMatch[2] ?? "");
|
|
1722
|
+
}
|
|
1723
|
+
if (edgeMatch) {
|
|
1724
|
+
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
1725
|
+
}
|
|
873
1726
|
if (dataStoreMatch) {
|
|
874
1727
|
const subPath = dataStoreMatch[1] ?? "";
|
|
875
1728
|
const db = requireDb();
|
|
876
|
-
const
|
|
1729
|
+
const readBody = async () => await request.json();
|
|
877
1730
|
if (subPath === "/query" && request.method === "POST") {
|
|
878
|
-
return handleQuery(db, await
|
|
1731
|
+
return handleQuery(db, await readBody());
|
|
879
1732
|
}
|
|
880
1733
|
if (subPath === "/mutate" && request.method === "POST") {
|
|
881
|
-
return handleMutate(db, await
|
|
1734
|
+
return handleMutate(db, await readBody());
|
|
882
1735
|
}
|
|
883
1736
|
if (subPath === "/schema" && request.method === "GET") {
|
|
884
1737
|
return handleGetSchema(db);
|
|
885
1738
|
}
|
|
886
1739
|
if (subPath === "/schema/tables" && request.method === "POST") {
|
|
887
|
-
return handleCreateTable(db, await
|
|
1740
|
+
return handleCreateTable(db, await readBody());
|
|
888
1741
|
}
|
|
889
1742
|
if (subPath === "/schema/columns" && request.method === "POST") {
|
|
890
|
-
return handleAddColumn(db, await
|
|
1743
|
+
return handleAddColumn(db, await readBody());
|
|
891
1744
|
}
|
|
892
1745
|
}
|
|
893
1746
|
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.`,
|
|
1747
|
+
`[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, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
|
|
895
1748
|
404
|
|
896
1749
|
);
|
|
897
1750
|
}
|
|
@@ -977,10 +1830,18 @@ async function createTestApp(options = {}) {
|
|
|
977
1830
|
}
|
|
978
1831
|
const inbox = createInbox();
|
|
979
1832
|
const directory = createDirectory();
|
|
1833
|
+
const payments = createPayments();
|
|
1834
|
+
const storage = createStorage();
|
|
1835
|
+
const messages = createMessages();
|
|
1836
|
+
const edge = createEdge();
|
|
980
1837
|
const app = {
|
|
981
1838
|
db,
|
|
982
1839
|
inbox,
|
|
983
1840
|
identities: directory,
|
|
1841
|
+
payments,
|
|
1842
|
+
storage,
|
|
1843
|
+
messages,
|
|
1844
|
+
edge,
|
|
984
1845
|
async query(sql, params = []) {
|
|
985
1846
|
const result = await db.query(sql, params);
|
|
986
1847
|
return result.rows;
|
|
@@ -996,8 +1857,8 @@ async function createTestApp(options = {}) {
|
|
|
996
1857
|
issueSession(user) {
|
|
997
1858
|
const fullUser = buildUser(user);
|
|
998
1859
|
const tokens = {
|
|
999
|
-
accessToken: `test-access-${
|
|
1000
|
-
refreshToken: `test-refresh-${
|
|
1860
|
+
accessToken: `test-access-${crypto8.randomUUID()}`,
|
|
1861
|
+
refreshToken: `test-refresh-${crypto8.randomUUID()}`
|
|
1001
1862
|
};
|
|
1002
1863
|
state.sessions.set(tokens.accessToken, fullUser);
|
|
1003
1864
|
state.refreshSessions.set(tokens.refreshToken, fullUser);
|
|
@@ -1013,7 +1874,10 @@ async function createTestApp(options = {}) {
|
|
|
1013
1874
|
state.emailCounter = 0;
|
|
1014
1875
|
state.identities.clear();
|
|
1015
1876
|
state.identityLinks = [];
|
|
1016
|
-
|
|
1877
|
+
payments.clear();
|
|
1878
|
+
storage.clear();
|
|
1879
|
+
messages.clear();
|
|
1880
|
+
edge.clear();
|
|
1017
1881
|
},
|
|
1018
1882
|
async close() {
|
|
1019
1883
|
state.db = null;
|
|
@@ -1021,9 +1885,13 @@ async function createTestApp(options = {}) {
|
|
|
1021
1885
|
state.sessions.clear();
|
|
1022
1886
|
state.refreshSessions.clear();
|
|
1023
1887
|
state.emails = [];
|
|
1888
|
+
state.emailCounter = 0;
|
|
1024
1889
|
state.identities.clear();
|
|
1025
1890
|
state.identityLinks = [];
|
|
1026
|
-
|
|
1891
|
+
payments.clear();
|
|
1892
|
+
storage.clear();
|
|
1893
|
+
messages.clear();
|
|
1894
|
+
edge.clear();
|
|
1027
1895
|
uninstallFetchRouter();
|
|
1028
1896
|
await db.close();
|
|
1029
1897
|
}
|
|
@@ -1031,122 +1899,6 @@ async function createTestApp(options = {}) {
|
|
|
1031
1899
|
return app;
|
|
1032
1900
|
}
|
|
1033
1901
|
|
|
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
1902
|
// src/workflow.ts
|
|
1151
1903
|
import { describe } from "vitest";
|
|
1152
1904
|
function describeWorkflow(name, fn) {
|
|
@@ -1160,10 +1912,11 @@ export {
|
|
|
1160
1912
|
CONTROL_PLANE_TEST_URL,
|
|
1161
1913
|
DATA_STORE_TEST_HOST,
|
|
1162
1914
|
DEFAULT_SCHEMA_PATH,
|
|
1915
|
+
STORAGE_TEST_HOST,
|
|
1916
|
+
STORAGE_TEST_URL,
|
|
1163
1917
|
TEST_ENV_DEFAULTS,
|
|
1164
1918
|
WORKFLOW_NAME_PREFIX,
|
|
1165
1919
|
callRoute,
|
|
1166
|
-
createModuleApp,
|
|
1167
1920
|
createTestApp,
|
|
1168
1921
|
describeWorkflow,
|
|
1169
1922
|
parseWorkflowName
|