@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.js
CHANGED
|
@@ -33,10 +33,11 @@ __export(index_exports, {
|
|
|
33
33
|
CONTROL_PLANE_TEST_URL: () => CONTROL_PLANE_TEST_URL,
|
|
34
34
|
DATA_STORE_TEST_HOST: () => DATA_STORE_TEST_HOST,
|
|
35
35
|
DEFAULT_SCHEMA_PATH: () => DEFAULT_SCHEMA_PATH,
|
|
36
|
+
STORAGE_TEST_HOST: () => STORAGE_TEST_HOST,
|
|
37
|
+
STORAGE_TEST_URL: () => STORAGE_TEST_URL,
|
|
36
38
|
TEST_ENV_DEFAULTS: () => TEST_ENV_DEFAULTS,
|
|
37
39
|
WORKFLOW_NAME_PREFIX: () => WORKFLOW_NAME_PREFIX,
|
|
38
40
|
callRoute: () => callRoute,
|
|
39
|
-
createModuleApp: () => createModuleApp,
|
|
40
41
|
createTestApp: () => createTestApp,
|
|
41
42
|
describeWorkflow: () => describeWorkflow,
|
|
42
43
|
parseWorkflowName: () => parseWorkflowName
|
|
@@ -44,7 +45,7 @@ __export(index_exports, {
|
|
|
44
45
|
module.exports = __toCommonJS(index_exports);
|
|
45
46
|
|
|
46
47
|
// src/test-app.ts
|
|
47
|
-
var
|
|
48
|
+
var import_node_crypto8 = __toESM(require("crypto"));
|
|
48
49
|
var import_node_fs = require("fs");
|
|
49
50
|
var import_node_path = require("path");
|
|
50
51
|
|
|
@@ -99,7 +100,23 @@ var state = globalSingleton("state", () => ({
|
|
|
99
100
|
emailCounter: 0,
|
|
100
101
|
identities: /* @__PURE__ */ new Map(),
|
|
101
102
|
identityLinks: [],
|
|
102
|
-
|
|
103
|
+
checkouts: [],
|
|
104
|
+
checkoutCounter: 0,
|
|
105
|
+
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
106
|
+
paymentLinks: /* @__PURE__ */ new Map(),
|
|
107
|
+
products: [],
|
|
108
|
+
uploads: [],
|
|
109
|
+
uploadCounter: 0,
|
|
110
|
+
storageFiles: /* @__PURE__ */ new Map(),
|
|
111
|
+
presignPending: /* @__PURE__ */ new Map(),
|
|
112
|
+
messages: [],
|
|
113
|
+
edgePrints: [],
|
|
114
|
+
edgeDisplays: [],
|
|
115
|
+
edgeTestPrints: [],
|
|
116
|
+
edgePrintCounter: 0,
|
|
117
|
+
edgeDevices: [],
|
|
118
|
+
edgePeripherals: [],
|
|
119
|
+
edgeBindings: /* @__PURE__ */ new Map(),
|
|
103
120
|
allowNetwork: false
|
|
104
121
|
}));
|
|
105
122
|
function requireDb() {
|
|
@@ -115,13 +132,16 @@ function requireDb() {
|
|
|
115
132
|
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
116
133
|
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
117
134
|
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
135
|
+
var STORAGE_TEST_URL = "https://storage.stardeck.test";
|
|
136
|
+
var STORAGE_TEST_HOST = "storage.stardeck.test";
|
|
118
137
|
var TEST_ENV_DEFAULTS = {
|
|
119
138
|
CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
|
|
120
139
|
DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
|
|
121
140
|
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
122
141
|
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
123
142
|
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
124
|
-
DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main
|
|
143
|
+
DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`,
|
|
144
|
+
STORAGE_URL: STORAGE_TEST_URL
|
|
125
145
|
};
|
|
126
146
|
var DEFAULT_TEST_USER = {
|
|
127
147
|
id: "test-user-1",
|
|
@@ -159,8 +179,8 @@ function verifyDeploymentAuthHeader(secret, header) {
|
|
|
159
179
|
return null;
|
|
160
180
|
}
|
|
161
181
|
if (payload.type !== "deployment-request") return null;
|
|
162
|
-
const
|
|
163
|
-
if (Math.abs(
|
|
182
|
+
const now3 = Math.floor(Date.now() / 1e3);
|
|
183
|
+
if (Math.abs(now3 - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
|
|
164
184
|
return payload;
|
|
165
185
|
}
|
|
166
186
|
|
|
@@ -177,6 +197,13 @@ function success(data) {
|
|
|
177
197
|
function failure(error, status = 400) {
|
|
178
198
|
return json({ success: false, error }, status);
|
|
179
199
|
}
|
|
200
|
+
async function readJsonBody(request) {
|
|
201
|
+
try {
|
|
202
|
+
return await request.json();
|
|
203
|
+
} catch {
|
|
204
|
+
return {};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
180
207
|
|
|
181
208
|
// src/simulator/data-store.ts
|
|
182
209
|
function quoteIdent(name) {
|
|
@@ -659,13 +686,6 @@ function now() {
|
|
|
659
686
|
function linksFor(identityId) {
|
|
660
687
|
return state.identityLinks.filter((l) => l.identityId === identityId);
|
|
661
688
|
}
|
|
662
|
-
async function readBody(request) {
|
|
663
|
-
try {
|
|
664
|
-
return await request.json();
|
|
665
|
-
} catch {
|
|
666
|
-
return {};
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
689
|
function handleList(request) {
|
|
670
690
|
const typeParam = new URL(request.url).searchParams.get("type");
|
|
671
691
|
const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
|
|
@@ -673,7 +693,7 @@ function handleList(request) {
|
|
|
673
693
|
return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
|
|
674
694
|
}
|
|
675
695
|
async function handleCreate(request) {
|
|
676
|
-
const body = await
|
|
696
|
+
const body = await readJsonBody(request);
|
|
677
697
|
const type = body.type;
|
|
678
698
|
if (type !== "person" && type !== "account") {
|
|
679
699
|
return failure("type must be 'person' or 'account'");
|
|
@@ -710,7 +730,7 @@ function handleGet(identityId) {
|
|
|
710
730
|
async function handleUpdate(identityId, request) {
|
|
711
731
|
const identity = state.identities.get(identityId);
|
|
712
732
|
if (!identity) return failure("identity not found", 404);
|
|
713
|
-
const body = await
|
|
733
|
+
const body = await readJsonBody(request);
|
|
714
734
|
if (body.displayName !== void 0) {
|
|
715
735
|
identity.displayName = body.displayName;
|
|
716
736
|
}
|
|
@@ -732,7 +752,7 @@ async function handleAttachLink(identityId, request) {
|
|
|
732
752
|
if (identity.status !== "active") {
|
|
733
753
|
return failure("links attach only to active persons");
|
|
734
754
|
}
|
|
735
|
-
const body = await
|
|
755
|
+
const body = await readJsonBody(request);
|
|
736
756
|
const kind = body.kind;
|
|
737
757
|
const externalId = body.externalId;
|
|
738
758
|
if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
|
|
@@ -757,108 +777,16 @@ async function handleAttachLink(identityId, request) {
|
|
|
757
777
|
state.identityLinks.push(link);
|
|
758
778
|
return success({ link });
|
|
759
779
|
}
|
|
760
|
-
function memoryDto(m) {
|
|
761
|
-
return {
|
|
762
|
-
id: m.id,
|
|
763
|
-
source: m.source,
|
|
764
|
-
kind: m.kind,
|
|
765
|
-
content: m.content,
|
|
766
|
-
metadata: m.metadata,
|
|
767
|
-
createdAt: m.createdAt
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
async function handleResolve(request) {
|
|
771
|
-
const body = await readBody(request);
|
|
772
|
-
const type = body.type;
|
|
773
|
-
if (type !== "person" && type !== "account") {
|
|
774
|
-
return failure("type must be 'person' or 'account'");
|
|
775
|
-
}
|
|
776
|
-
if (type === "account") {
|
|
777
|
-
return failure(
|
|
778
|
-
"resolveOrCreate is for channel-linked persons; an account is not resolvable by link \u2014 create it explicitly instead.",
|
|
779
|
-
409
|
|
780
|
-
);
|
|
781
|
-
}
|
|
782
|
-
const link = body.link;
|
|
783
|
-
const kind = link?.kind;
|
|
784
|
-
const externalId = link?.externalId;
|
|
785
|
-
if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
|
|
786
|
-
return failure(`link.kind must be one of: ${[...LINK_KINDS].join(", ")}`);
|
|
787
|
-
}
|
|
788
|
-
if (typeof externalId !== "string" || !externalId) {
|
|
789
|
-
return failure("link.externalId is required");
|
|
790
|
-
}
|
|
791
|
-
const existingLink = state.identityLinks.find(
|
|
792
|
-
(l) => l.kind === kind && l.externalId === externalId
|
|
793
|
-
);
|
|
794
|
-
if (existingLink) {
|
|
795
|
-
const identity2 = state.identities.get(existingLink.identityId);
|
|
796
|
-
if (identity2) return success({ identity: identity2, created: false });
|
|
797
|
-
}
|
|
798
|
-
const identity = {
|
|
799
|
-
id: import_node_crypto3.default.randomUUID(),
|
|
800
|
-
type,
|
|
801
|
-
parentId: null,
|
|
802
|
-
displayName: body.displayName ?? null,
|
|
803
|
-
profile: body.profile ?? {},
|
|
804
|
-
status: "active",
|
|
805
|
-
mergedIntoId: null,
|
|
806
|
-
externalRef: null,
|
|
807
|
-
createdAt: now(),
|
|
808
|
-
updatedAt: now()
|
|
809
|
-
};
|
|
810
|
-
state.identities.set(identity.id, identity);
|
|
811
|
-
state.identityLinks.push({
|
|
812
|
-
id: import_node_crypto3.default.randomUUID(),
|
|
813
|
-
identityId: identity.id,
|
|
814
|
-
kind,
|
|
815
|
-
externalId,
|
|
816
|
-
verified: true,
|
|
817
|
-
createdAt: now()
|
|
818
|
-
});
|
|
819
|
-
return success({ identity, created: true });
|
|
820
|
-
}
|
|
821
|
-
async function handleWriteMemory(identityId, request) {
|
|
822
|
-
if (!state.identities.get(identityId)) return failure("Identity not found", 404);
|
|
823
|
-
const body = await readBody(request);
|
|
824
|
-
if (typeof body.content !== "string" || !body.content) return failure("content is required");
|
|
825
|
-
const entry = {
|
|
826
|
-
id: import_node_crypto3.default.randomUUID(),
|
|
827
|
-
identityId,
|
|
828
|
-
source: typeof body.source === "string" ? body.source : "app",
|
|
829
|
-
kind: typeof body.kind === "string" ? body.kind : "fact",
|
|
830
|
-
content: body.content,
|
|
831
|
-
metadata: body.metadata ?? {},
|
|
832
|
-
createdAt: now()
|
|
833
|
-
};
|
|
834
|
-
state.identityMemory.push(entry);
|
|
835
|
-
return success({ memory: memoryDto(entry) });
|
|
836
|
-
}
|
|
837
|
-
function handleListMemory(identityId, request) {
|
|
838
|
-
if (!state.identities.get(identityId)) return failure("Identity not found", 404);
|
|
839
|
-
let rows = state.identityMemory.filter((m) => m.identityId === identityId).reverse();
|
|
840
|
-
const limitParam = new URL(request.url).searchParams.get("limit");
|
|
841
|
-
if (limitParam) rows = rows.slice(0, Math.max(1, parseInt(limitParam, 10) || 0));
|
|
842
|
-
return success({ memories: rows.map(memoryDto) });
|
|
843
|
-
}
|
|
844
780
|
async function handleIdentitiesRequest(request, subPath) {
|
|
845
781
|
const method = request.method;
|
|
846
782
|
if (subPath === "" || subPath === "/") {
|
|
847
783
|
if (method === "GET") return handleList(request);
|
|
848
784
|
if (method === "POST") return handleCreate(request);
|
|
849
785
|
}
|
|
850
|
-
if (subPath === "/resolve" && method === "POST") {
|
|
851
|
-
return handleResolve(request);
|
|
852
|
-
}
|
|
853
786
|
const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
|
|
854
787
|
if (linksMatch && method === "POST") {
|
|
855
788
|
return handleAttachLink(linksMatch[1], request);
|
|
856
789
|
}
|
|
857
|
-
const memoryMatch = subPath.match(/^\/([^/]+)\/memory$/);
|
|
858
|
-
if (memoryMatch) {
|
|
859
|
-
if (method === "GET") return handleListMemory(memoryMatch[1], request);
|
|
860
|
-
if (method === "POST") return handleWriteMemory(memoryMatch[1], request);
|
|
861
|
-
}
|
|
862
790
|
const singleMatch = subPath.match(/^\/([^/]+)$/);
|
|
863
791
|
if (singleMatch) {
|
|
864
792
|
if (method === "GET") return handleGet(singleMatch[1]);
|
|
@@ -881,6 +809,897 @@ function createDirectory() {
|
|
|
881
809
|
};
|
|
882
810
|
}
|
|
883
811
|
|
|
812
|
+
// src/simulator/payments.ts
|
|
813
|
+
var import_node_crypto5 = __toESM(require("crypto"));
|
|
814
|
+
|
|
815
|
+
// src/next/headers-shim.ts
|
|
816
|
+
var import_node_async_hooks = require("async_hooks");
|
|
817
|
+
var requestScopeStorage = globalSingleton(
|
|
818
|
+
"request-scope",
|
|
819
|
+
() => new import_node_async_hooks.AsyncLocalStorage()
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
// src/next/call-route.ts
|
|
823
|
+
function parseCookieHeader(header) {
|
|
824
|
+
const map = /* @__PURE__ */ new Map();
|
|
825
|
+
if (!header) return map;
|
|
826
|
+
for (const part of header.split(";")) {
|
|
827
|
+
const eq = part.indexOf("=");
|
|
828
|
+
if (eq === -1) continue;
|
|
829
|
+
map.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
|
|
830
|
+
}
|
|
831
|
+
return map;
|
|
832
|
+
}
|
|
833
|
+
async function importNextServer() {
|
|
834
|
+
try {
|
|
835
|
+
return await import("next/server.js");
|
|
836
|
+
} catch {
|
|
837
|
+
return await import("next/server");
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
async function callRoute(handler, options = {}) {
|
|
841
|
+
const { NextRequest } = await importNextServer();
|
|
842
|
+
const path = options.path ?? "/api/test-route";
|
|
843
|
+
const url = new URL(`http://localhost:3333${path}`);
|
|
844
|
+
for (const [key, value] of Object.entries(options.searchParams ?? {})) {
|
|
845
|
+
url.searchParams.set(key, value);
|
|
846
|
+
}
|
|
847
|
+
const method = options.method ?? (options.body !== void 0 ? "POST" : "GET");
|
|
848
|
+
const headers = new Headers(options.headers);
|
|
849
|
+
const user = options.user !== void 0 ? options.user : state.currentUser;
|
|
850
|
+
if (user && !headers.has("x-stardeck-user")) {
|
|
851
|
+
headers.set("x-stardeck-user", JSON.stringify(user));
|
|
852
|
+
}
|
|
853
|
+
if (options.body !== void 0 && !headers.has("Content-Type")) {
|
|
854
|
+
headers.set("Content-Type", "application/json");
|
|
855
|
+
}
|
|
856
|
+
const cookiePairs = Object.entries(options.cookies ?? {});
|
|
857
|
+
if (cookiePairs.length > 0) {
|
|
858
|
+
const existing = headers.get("cookie");
|
|
859
|
+
const cookieString = cookiePairs.map(([k, v]) => `${k}=${v}`).join("; ");
|
|
860
|
+
headers.set("cookie", existing ? `${existing}; ${cookieString}` : cookieString);
|
|
861
|
+
}
|
|
862
|
+
const request = new NextRequest(url, {
|
|
863
|
+
method,
|
|
864
|
+
headers,
|
|
865
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
866
|
+
});
|
|
867
|
+
const scope = {
|
|
868
|
+
headers,
|
|
869
|
+
cookies: parseCookieHeader(headers.get("cookie"))
|
|
870
|
+
};
|
|
871
|
+
try {
|
|
872
|
+
return await requestScopeStorage.run(
|
|
873
|
+
scope,
|
|
874
|
+
() => Promise.resolve(
|
|
875
|
+
handler(request, {
|
|
876
|
+
params: Promise.resolve(options.params ?? {})
|
|
877
|
+
})
|
|
878
|
+
)
|
|
879
|
+
);
|
|
880
|
+
} catch (error) {
|
|
881
|
+
const redirect = decodeNextRedirect(error);
|
|
882
|
+
if (redirect) return redirect;
|
|
883
|
+
throw error;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
function decodeNextRedirect(error) {
|
|
887
|
+
const digest = error?.digest;
|
|
888
|
+
if (typeof digest !== "string" || !digest.startsWith("NEXT_REDIRECT")) return null;
|
|
889
|
+
const parts = digest.split(";");
|
|
890
|
+
const location = parts[2] ?? "/";
|
|
891
|
+
const status = Number(parts[3]) || 307;
|
|
892
|
+
return new Response(null, { status, headers: { location } });
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/simulator/webhook-signing.ts
|
|
896
|
+
var import_node_crypto4 = __toESM(require("crypto"));
|
|
897
|
+
function signEventDelivery(secret, context, rawBody) {
|
|
898
|
+
const payload = {
|
|
899
|
+
type: "deployment-request",
|
|
900
|
+
organizationId: context.organizationId,
|
|
901
|
+
projectId: context.projectId,
|
|
902
|
+
deploymentId: context.deploymentId,
|
|
903
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
904
|
+
nonce: import_node_crypto4.default.randomUUID()
|
|
905
|
+
};
|
|
906
|
+
const payloadJson = JSON.stringify(payload);
|
|
907
|
+
const payloadB64 = Buffer.from(payloadJson).toString("base64");
|
|
908
|
+
const signature = import_node_crypto4.default.createHmac("sha256", secret).update(payloadJson).update(rawBody).digest("hex");
|
|
909
|
+
return `${payloadB64}.${signature}`;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/simulator/payments.ts
|
|
913
|
+
function payErr(error, status = 400, code) {
|
|
914
|
+
return json(code ? { error, code } : { error }, status);
|
|
915
|
+
}
|
|
916
|
+
function nextCheckoutId() {
|
|
917
|
+
state.checkoutCounter += 1;
|
|
918
|
+
return `cs_test_${state.checkoutCounter}`;
|
|
919
|
+
}
|
|
920
|
+
function nextPaymentLinkId() {
|
|
921
|
+
state.checkoutCounter += 1;
|
|
922
|
+
return `plink_test_${state.checkoutCounter}`;
|
|
923
|
+
}
|
|
924
|
+
function seedStripeSession(id, body) {
|
|
925
|
+
const lineItems = body.lineItems ?? [];
|
|
926
|
+
let amountTotal = null;
|
|
927
|
+
let currency = null;
|
|
928
|
+
if (lineItems.length > 0) {
|
|
929
|
+
amountTotal = lineItems.reduce(
|
|
930
|
+
(sum, item) => sum + (item.priceData?.unitAmount ?? 0) * (item.quantity ?? 1),
|
|
931
|
+
0
|
|
932
|
+
);
|
|
933
|
+
currency = lineItems[0].priceData?.currency ?? null;
|
|
934
|
+
}
|
|
935
|
+
state.sessionStatuses.set(id, {
|
|
936
|
+
id,
|
|
937
|
+
status: "open",
|
|
938
|
+
paymentStatus: "unpaid",
|
|
939
|
+
mode: body.mode ?? "payment",
|
|
940
|
+
amountTotal,
|
|
941
|
+
currency,
|
|
942
|
+
customerEmail: body.customerEmail ? String(body.customerEmail) : null,
|
|
943
|
+
metadata: body.metadata ?? {},
|
|
944
|
+
expiresAt: Math.floor(Date.now() / 1e3) + 3600
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
function seedBeamLink(id, body, merchantId) {
|
|
948
|
+
const order = body.order;
|
|
949
|
+
state.paymentLinks.set(id, {
|
|
950
|
+
paymentLinkId: id,
|
|
951
|
+
merchantId,
|
|
952
|
+
url: `https://beam.test/pay/${id}`,
|
|
953
|
+
status: "ACTIVE",
|
|
954
|
+
order: {
|
|
955
|
+
netAmount: Number(order?.netAmount ?? 0),
|
|
956
|
+
currency: String(order?.currency ?? "THB"),
|
|
957
|
+
description: String(order?.description ?? ""),
|
|
958
|
+
referenceId: order?.referenceId ? String(order.referenceId) : void 0,
|
|
959
|
+
internalNote: order?.internalNote ? String(order.internalNote) : void 0,
|
|
960
|
+
orderItems: order?.orderItems
|
|
961
|
+
},
|
|
962
|
+
redirectUrl: body.redirectUrl ? String(body.redirectUrl) : void 0,
|
|
963
|
+
linkSettings: body.linkSettings,
|
|
964
|
+
collectDeliveryAddress: body.collectDeliveryAddress === true
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
async function deliverEvent(envelope, handler, options) {
|
|
968
|
+
const rawBody = JSON.stringify(envelope);
|
|
969
|
+
const secret = options?.deploymentSecret ?? TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET;
|
|
970
|
+
const authHeader = signEventDelivery(
|
|
971
|
+
secret,
|
|
972
|
+
{
|
|
973
|
+
organizationId: TEST_ENV_DEFAULTS.ORGANIZATION_ID,
|
|
974
|
+
projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
|
|
975
|
+
deploymentId: TEST_ENV_DEFAULTS.DEPLOYMENT_ID
|
|
976
|
+
},
|
|
977
|
+
rawBody
|
|
978
|
+
);
|
|
979
|
+
return callRoute(handler, {
|
|
980
|
+
method: "POST",
|
|
981
|
+
path: options?.path ?? "/api/payments/webhooks",
|
|
982
|
+
params: { path: ["webhooks"] },
|
|
983
|
+
headers: { "X-Stardeck-Auth": authHeader },
|
|
984
|
+
body: envelope
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
async function handlePaymentsRequest(request, url) {
|
|
988
|
+
const pathname = url.pathname;
|
|
989
|
+
if (/\/bolt-connections/.test(pathname) || /\/bolt-intents/.test(pathname) || /\/charges(\/|$)/.test(pathname) || /\/billing-portal$/.test(pathname)) {
|
|
990
|
+
return payErr("Not found", 404, "NOT_FOUND");
|
|
991
|
+
}
|
|
992
|
+
const beamProductsMatch = pathname.match(
|
|
993
|
+
/^\/api\/store\/beam\/([^/]+)\/payment-links(?:\/([^/]+))?$/
|
|
994
|
+
);
|
|
995
|
+
if (beamProductsMatch) {
|
|
996
|
+
const merchantId = beamProductsMatch[1];
|
|
997
|
+
const linkId = beamProductsMatch[2];
|
|
998
|
+
if (!linkId && request.method === "POST") {
|
|
999
|
+
const body = await readJsonBody(request);
|
|
1000
|
+
const id = nextPaymentLinkId();
|
|
1001
|
+
const checkoutUrl = `https://beam.test/pay/${id}`;
|
|
1002
|
+
const captured = {
|
|
1003
|
+
id,
|
|
1004
|
+
url: checkoutUrl,
|
|
1005
|
+
provider: "beam",
|
|
1006
|
+
options: body,
|
|
1007
|
+
metadata: body.metadata,
|
|
1008
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1009
|
+
};
|
|
1010
|
+
state.checkouts.push(captured);
|
|
1011
|
+
seedBeamLink(id, body, merchantId);
|
|
1012
|
+
return json({ id, url: checkoutUrl });
|
|
1013
|
+
}
|
|
1014
|
+
if (linkId && request.method === "GET") {
|
|
1015
|
+
const link = state.paymentLinks.get(linkId);
|
|
1016
|
+
if (!link) return payErr("Payment link not found", 404, "NOT_FOUND");
|
|
1017
|
+
return json({ paymentLink: link });
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
const stripeStoreMatch = pathname.match(/^\/api\/store\/([^/]+)\/(.+)$/);
|
|
1021
|
+
if (stripeStoreMatch) {
|
|
1022
|
+
const accountId = stripeStoreMatch[1];
|
|
1023
|
+
const subPath = stripeStoreMatch[2];
|
|
1024
|
+
if (accountId === "beam") {
|
|
1025
|
+
return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
|
|
1026
|
+
}
|
|
1027
|
+
if (subPath === "products" && request.method === "GET") {
|
|
1028
|
+
return json({ products: state.products });
|
|
1029
|
+
}
|
|
1030
|
+
const productMatch = subPath.match(/^products\/([^/]+)$/);
|
|
1031
|
+
if (productMatch && request.method === "GET") {
|
|
1032
|
+
const product = state.products.find((p) => p.id === productMatch[1]);
|
|
1033
|
+
if (!product) return payErr("Product not found", 404, "NOT_FOUND");
|
|
1034
|
+
return json({ product });
|
|
1035
|
+
}
|
|
1036
|
+
if (subPath === "checkout" && request.method === "POST") {
|
|
1037
|
+
const body = await readJsonBody(request);
|
|
1038
|
+
const id = nextCheckoutId();
|
|
1039
|
+
const checkoutUrl = `https://checkout.stripe.test/c/pay/${id}`;
|
|
1040
|
+
const captured = {
|
|
1041
|
+
id,
|
|
1042
|
+
url: checkoutUrl,
|
|
1043
|
+
provider: "stripe",
|
|
1044
|
+
options: body,
|
|
1045
|
+
mode: body.mode,
|
|
1046
|
+
metadata: body.metadata,
|
|
1047
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1048
|
+
};
|
|
1049
|
+
state.checkouts.push(captured);
|
|
1050
|
+
seedStripeSession(id, body);
|
|
1051
|
+
return json({ id, url: checkoutUrl });
|
|
1052
|
+
}
|
|
1053
|
+
const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
|
|
1054
|
+
if (sessionMatch && request.method === "GET") {
|
|
1055
|
+
const session = state.sessionStatuses.get(sessionMatch[1]);
|
|
1056
|
+
if (!session) return payErr("Checkout session not found", 404, "NOT_FOUND");
|
|
1057
|
+
return json({ session });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
|
|
1061
|
+
}
|
|
1062
|
+
function createPayments() {
|
|
1063
|
+
return {
|
|
1064
|
+
get checkouts() {
|
|
1065
|
+
return [...state.checkouts];
|
|
1066
|
+
},
|
|
1067
|
+
latest() {
|
|
1068
|
+
return state.checkouts[state.checkouts.length - 1];
|
|
1069
|
+
},
|
|
1070
|
+
setProducts(products) {
|
|
1071
|
+
state.products = products;
|
|
1072
|
+
},
|
|
1073
|
+
markPaid(id) {
|
|
1074
|
+
const session = state.sessionStatuses.get(id);
|
|
1075
|
+
if (session) {
|
|
1076
|
+
session.status = "complete";
|
|
1077
|
+
session.paymentStatus = "paid";
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const link = state.paymentLinks.get(id);
|
|
1081
|
+
if (link) {
|
|
1082
|
+
link.status = "PAID";
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
throw new Error(`[stardeck-testing] Unknown checkout or payment link id: ${id}`);
|
|
1086
|
+
},
|
|
1087
|
+
setSessionStatus(id, status) {
|
|
1088
|
+
const session = state.sessionStatuses.get(id);
|
|
1089
|
+
if (!session) {
|
|
1090
|
+
throw new Error(`[stardeck-testing] Unknown checkout session id: ${id}`);
|
|
1091
|
+
}
|
|
1092
|
+
Object.assign(session, status);
|
|
1093
|
+
},
|
|
1094
|
+
setPaymentLinkStatus(id, status) {
|
|
1095
|
+
const link = state.paymentLinks.get(id);
|
|
1096
|
+
if (!link) {
|
|
1097
|
+
throw new Error(`[stardeck-testing] Unknown payment link id: ${id}`);
|
|
1098
|
+
}
|
|
1099
|
+
link.status = status;
|
|
1100
|
+
},
|
|
1101
|
+
async deliverStripeEvent(handler, event, options) {
|
|
1102
|
+
const envelope = {
|
|
1103
|
+
id: `evt_test_${import_node_crypto5.default.randomUUID()}`,
|
|
1104
|
+
kind: "stripe_webhook",
|
|
1105
|
+
timestamp: Date.now(),
|
|
1106
|
+
stripeEvent: {
|
|
1107
|
+
type: event.type,
|
|
1108
|
+
accountId: event.accountId ?? process.env.STRIPE_CONNECT_ACCOUNT_ID ?? "acct_test",
|
|
1109
|
+
data: event.data
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
return deliverEvent(envelope, handler, options);
|
|
1113
|
+
},
|
|
1114
|
+
async deliverBeamEvent(handler, event, options) {
|
|
1115
|
+
const envelope = {
|
|
1116
|
+
id: `beam_evt_test_${import_node_crypto5.default.randomUUID()}`,
|
|
1117
|
+
kind: "beam_webhook",
|
|
1118
|
+
timestamp: Date.now(),
|
|
1119
|
+
beamEvent: {
|
|
1120
|
+
type: event.type,
|
|
1121
|
+
payload: event.payload
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
return deliverEvent(envelope, handler, options);
|
|
1125
|
+
},
|
|
1126
|
+
clear() {
|
|
1127
|
+
state.checkouts = [];
|
|
1128
|
+
state.checkoutCounter = 0;
|
|
1129
|
+
state.sessionStatuses.clear();
|
|
1130
|
+
state.paymentLinks.clear();
|
|
1131
|
+
state.products = [];
|
|
1132
|
+
},
|
|
1133
|
+
get count() {
|
|
1134
|
+
return state.checkouts.length;
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/simulator/storage.ts
|
|
1140
|
+
var import_node_crypto6 = __toESM(require("crypto"));
|
|
1141
|
+
function storageErr(error, status = 400) {
|
|
1142
|
+
return json({ error }, status);
|
|
1143
|
+
}
|
|
1144
|
+
function fileExtension(filename) {
|
|
1145
|
+
const dot = filename.lastIndexOf(".");
|
|
1146
|
+
return dot === -1 ? "bin" : filename.slice(dot + 1);
|
|
1147
|
+
}
|
|
1148
|
+
function buildKey(fileId, filename) {
|
|
1149
|
+
const orgId = process.env.ORGANIZATION_ID ?? TEST_ENV_DEFAULTS.ORGANIZATION_ID;
|
|
1150
|
+
const projectId = process.env.PROJECT_ID ?? TEST_ENV_DEFAULTS.PROJECT_ID;
|
|
1151
|
+
const deploymentId = process.env.DEPLOYMENT_ID ?? TEST_ENV_DEFAULTS.DEPLOYMENT_ID;
|
|
1152
|
+
return `${orgId}/${projectId}/${deploymentId}/${fileId}.${fileExtension(filename)}`;
|
|
1153
|
+
}
|
|
1154
|
+
function buildUrl(key) {
|
|
1155
|
+
return `${STORAGE_TEST_URL}/files/${key}`;
|
|
1156
|
+
}
|
|
1157
|
+
function toUploadResponse(record) {
|
|
1158
|
+
return {
|
|
1159
|
+
id: record.id,
|
|
1160
|
+
key: record.key,
|
|
1161
|
+
filename: record.filename,
|
|
1162
|
+
contentType: record.contentType,
|
|
1163
|
+
sizeBytes: record.sizeBytes,
|
|
1164
|
+
url: record.url,
|
|
1165
|
+
uploadedAt: record.uploadedAt,
|
|
1166
|
+
isPublic: record.isPublic,
|
|
1167
|
+
metadata: record.metadata
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function captureUpload(record, method, path) {
|
|
1171
|
+
const captured = {
|
|
1172
|
+
id: record.id,
|
|
1173
|
+
key: record.key,
|
|
1174
|
+
filename: record.filename,
|
|
1175
|
+
contentType: record.contentType,
|
|
1176
|
+
sizeBytes: record.sizeBytes,
|
|
1177
|
+
url: record.url,
|
|
1178
|
+
uploadedAt: record.uploadedAt,
|
|
1179
|
+
isPublic: record.isPublic,
|
|
1180
|
+
metadata: record.metadata,
|
|
1181
|
+
method,
|
|
1182
|
+
path
|
|
1183
|
+
};
|
|
1184
|
+
state.uploads.push(captured);
|
|
1185
|
+
}
|
|
1186
|
+
function createFileRecord(input) {
|
|
1187
|
+
state.uploadCounter += 1;
|
|
1188
|
+
const id = `file_test_${state.uploadCounter}`;
|
|
1189
|
+
const key = buildKey(id, input.filename);
|
|
1190
|
+
return {
|
|
1191
|
+
id,
|
|
1192
|
+
key,
|
|
1193
|
+
filename: input.filename,
|
|
1194
|
+
contentType: input.contentType,
|
|
1195
|
+
sizeBytes: input.sizeBytes,
|
|
1196
|
+
url: buildUrl(key),
|
|
1197
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1198
|
+
metadata: input.metadata,
|
|
1199
|
+
isPublic: input.isPublic,
|
|
1200
|
+
deleted: false
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
function activeFiles() {
|
|
1204
|
+
return [...state.storageFiles.values()].filter((f) => !f.deleted);
|
|
1205
|
+
}
|
|
1206
|
+
async function handleStorageRequest(request, url) {
|
|
1207
|
+
const pathname = url.pathname;
|
|
1208
|
+
if (pathname.startsWith("/upload/presign/multipart")) {
|
|
1209
|
+
return storageErr("Not found", 404);
|
|
1210
|
+
}
|
|
1211
|
+
const presignedPutMatch = pathname.match(/^\/presigned\/([^/]+)$/);
|
|
1212
|
+
if (presignedPutMatch && request.method === "PUT") {
|
|
1213
|
+
const fileId = presignedPutMatch[1];
|
|
1214
|
+
if (!state.presignPending.has(fileId)) {
|
|
1215
|
+
return storageErr("Presign session not found", 404);
|
|
1216
|
+
}
|
|
1217
|
+
return new Response(null, {
|
|
1218
|
+
status: 200,
|
|
1219
|
+
headers: { etag: `"${import_node_crypto6.default.randomUUID()}"` }
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
if (pathname === "/upload/presign/complete" && request.method === "POST") {
|
|
1223
|
+
const body = await readJsonBody(request);
|
|
1224
|
+
const fileId = String(body.fileId ?? "");
|
|
1225
|
+
const pending = state.presignPending.get(fileId);
|
|
1226
|
+
if (!pending) return storageErr("Presign session not found", 404);
|
|
1227
|
+
const record = {
|
|
1228
|
+
id: fileId,
|
|
1229
|
+
key: pending.key,
|
|
1230
|
+
filename: pending.fileName,
|
|
1231
|
+
contentType: pending.contentType,
|
|
1232
|
+
sizeBytes: pending.sizeBytes,
|
|
1233
|
+
url: buildUrl(pending.key),
|
|
1234
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1235
|
+
metadata: pending.metadata,
|
|
1236
|
+
isPublic: pending.isPublic,
|
|
1237
|
+
deleted: false
|
|
1238
|
+
};
|
|
1239
|
+
state.storageFiles.set(record.id, record);
|
|
1240
|
+
state.presignPending.delete(fileId);
|
|
1241
|
+
captureUpload(record, "POST", "/upload/presign/complete");
|
|
1242
|
+
return json(toUploadResponse(record));
|
|
1243
|
+
}
|
|
1244
|
+
if (pathname === "/upload/presign" && request.method === "POST") {
|
|
1245
|
+
const body = await readJsonBody(request);
|
|
1246
|
+
const fileName = String(body.fileName ?? "upload.bin");
|
|
1247
|
+
const contentType = String(body.contentType ?? "application/octet-stream");
|
|
1248
|
+
const sizeBytes = Number(body.sizeBytes ?? 0);
|
|
1249
|
+
const isPublic = body.isPublic !== false;
|
|
1250
|
+
const metadata = body.metadata;
|
|
1251
|
+
state.uploadCounter += 1;
|
|
1252
|
+
const fileId = `file_test_${state.uploadCounter}`;
|
|
1253
|
+
const key = buildKey(fileId, fileName);
|
|
1254
|
+
state.presignPending.set(fileId, {
|
|
1255
|
+
fileName,
|
|
1256
|
+
contentType,
|
|
1257
|
+
sizeBytes,
|
|
1258
|
+
isPublic,
|
|
1259
|
+
metadata,
|
|
1260
|
+
key
|
|
1261
|
+
});
|
|
1262
|
+
return json({
|
|
1263
|
+
fileId,
|
|
1264
|
+
key,
|
|
1265
|
+
presignedUrl: `${STORAGE_TEST_URL}/presigned/${fileId}`,
|
|
1266
|
+
contentType,
|
|
1267
|
+
expiresIn: 3600
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
if (pathname === "/upload" && request.method === "POST") {
|
|
1271
|
+
const isPublic = url.searchParams.get("isPublic") !== "false";
|
|
1272
|
+
const formData = await request.formData();
|
|
1273
|
+
const file = formData.get("file");
|
|
1274
|
+
if (!(file instanceof Blob)) {
|
|
1275
|
+
return storageErr("file is required");
|
|
1276
|
+
}
|
|
1277
|
+
const filename = formData.get("filename") ?? (file instanceof File ? file.name : "upload.bin");
|
|
1278
|
+
const metadataRaw = formData.get("metadata");
|
|
1279
|
+
let metadata;
|
|
1280
|
+
if (typeof metadataRaw === "string" && metadataRaw) {
|
|
1281
|
+
try {
|
|
1282
|
+
metadata = JSON.parse(metadataRaw);
|
|
1283
|
+
} catch {
|
|
1284
|
+
return storageErr("metadata must be valid JSON");
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
const record = createFileRecord({
|
|
1288
|
+
filename,
|
|
1289
|
+
contentType: file.type || "application/octet-stream",
|
|
1290
|
+
sizeBytes: file.size,
|
|
1291
|
+
isPublic,
|
|
1292
|
+
metadata
|
|
1293
|
+
});
|
|
1294
|
+
state.storageFiles.set(record.id, record);
|
|
1295
|
+
captureUpload(record, "POST", "/upload");
|
|
1296
|
+
return json(toUploadResponse(record));
|
|
1297
|
+
}
|
|
1298
|
+
const fileMatch = pathname.match(/^\/api\/files\/([^/]+)$/);
|
|
1299
|
+
if (fileMatch) {
|
|
1300
|
+
const fileId = fileMatch[1];
|
|
1301
|
+
const record = state.storageFiles.get(fileId);
|
|
1302
|
+
if (!record || record.deleted) {
|
|
1303
|
+
return storageErr("File not found", 404);
|
|
1304
|
+
}
|
|
1305
|
+
if (request.method === "GET") {
|
|
1306
|
+
return json(toUploadResponse(record));
|
|
1307
|
+
}
|
|
1308
|
+
if (request.method === "PATCH") {
|
|
1309
|
+
const body = await readJsonBody(request);
|
|
1310
|
+
if (body.fileName !== void 0) record.filename = String(body.fileName);
|
|
1311
|
+
if (body.metadata !== void 0) {
|
|
1312
|
+
record.metadata = body.metadata;
|
|
1313
|
+
}
|
|
1314
|
+
if (body.isPublic !== void 0) record.isPublic = body.isPublic === true;
|
|
1315
|
+
return json(toUploadResponse(record));
|
|
1316
|
+
}
|
|
1317
|
+
if (request.method === "DELETE") {
|
|
1318
|
+
record.deleted = true;
|
|
1319
|
+
return json({ success: true });
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (pathname === "/api/files" && request.method === "GET") {
|
|
1323
|
+
const parseParam = (raw, fallback, min) => {
|
|
1324
|
+
const n = Number(raw);
|
|
1325
|
+
return Number.isFinite(n) && n >= min ? Math.floor(n) : fallback;
|
|
1326
|
+
};
|
|
1327
|
+
const limit = parseParam(url.searchParams.get("limit"), 50, 1);
|
|
1328
|
+
const offset = parseParam(url.searchParams.get("offset"), 0, 0);
|
|
1329
|
+
const prefix = url.searchParams.get("prefix") ?? "";
|
|
1330
|
+
let files = activeFiles();
|
|
1331
|
+
if (prefix) {
|
|
1332
|
+
files = files.filter((f) => f.key.startsWith(prefix) || f.filename.startsWith(prefix));
|
|
1333
|
+
}
|
|
1334
|
+
const total = files.length;
|
|
1335
|
+
const slice = files.slice(offset, offset + limit);
|
|
1336
|
+
return json({
|
|
1337
|
+
files: slice.map((f) => toUploadResponse(f)),
|
|
1338
|
+
total,
|
|
1339
|
+
limit,
|
|
1340
|
+
offset,
|
|
1341
|
+
hasMore: offset + slice.length < total
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
return storageErr(`No storage simulator for ${request.method} ${pathname}`, 404);
|
|
1345
|
+
}
|
|
1346
|
+
function createStorage() {
|
|
1347
|
+
return {
|
|
1348
|
+
get uploads() {
|
|
1349
|
+
return [...state.uploads];
|
|
1350
|
+
},
|
|
1351
|
+
latest() {
|
|
1352
|
+
return state.uploads[state.uploads.length - 1];
|
|
1353
|
+
},
|
|
1354
|
+
clear() {
|
|
1355
|
+
state.uploads = [];
|
|
1356
|
+
state.uploadCounter = 0;
|
|
1357
|
+
state.storageFiles.clear();
|
|
1358
|
+
state.presignPending.clear();
|
|
1359
|
+
},
|
|
1360
|
+
get count() {
|
|
1361
|
+
return state.uploads.length;
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// src/simulator/messaging.ts
|
|
1367
|
+
var import_node_crypto7 = __toESM(require("crypto"));
|
|
1368
|
+
function captureMessage(channel, recipient, body, connectionId) {
|
|
1369
|
+
const message = {
|
|
1370
|
+
channel,
|
|
1371
|
+
recipient,
|
|
1372
|
+
body: {
|
|
1373
|
+
text: body.text ? String(body.text) : void 0,
|
|
1374
|
+
blocks: body.blocks,
|
|
1375
|
+
threadTs: body.threadTs ? String(body.threadTs) : void 0,
|
|
1376
|
+
messagingType: body.messagingType ? String(body.messagingType) : void 0,
|
|
1377
|
+
tag: body.tag ? String(body.tag) : void 0
|
|
1378
|
+
},
|
|
1379
|
+
connectionId,
|
|
1380
|
+
sentAt: /* @__PURE__ */ new Date()
|
|
1381
|
+
};
|
|
1382
|
+
state.messages.push(message);
|
|
1383
|
+
}
|
|
1384
|
+
async function handleMessagingRequest(request, channel, subPath) {
|
|
1385
|
+
const method = request.method;
|
|
1386
|
+
if ((subPath === "" || subPath === "/" || subPath === "/connections") && method === "GET") {
|
|
1387
|
+
return success({ connections: [] });
|
|
1388
|
+
}
|
|
1389
|
+
if (channel === "slack" && subPath === "/send" && method === "POST") {
|
|
1390
|
+
const body = await readJsonBody(request);
|
|
1391
|
+
if (!body.text && !body.blocks) {
|
|
1392
|
+
return failure("text or blocks is required");
|
|
1393
|
+
}
|
|
1394
|
+
const slackChannel = String(body.channel ?? "");
|
|
1395
|
+
if (!slackChannel) {
|
|
1396
|
+
return failure("channel is required");
|
|
1397
|
+
}
|
|
1398
|
+
captureMessage(
|
|
1399
|
+
"slack",
|
|
1400
|
+
slackChannel,
|
|
1401
|
+
body,
|
|
1402
|
+
body.connectionId ? String(body.connectionId) : void 0
|
|
1403
|
+
);
|
|
1404
|
+
return success({
|
|
1405
|
+
ok: true,
|
|
1406
|
+
ts: `${Date.now()}.${import_node_crypto7.default.randomUUID().slice(0, 6)}`,
|
|
1407
|
+
channel: slackChannel
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
if (channel === "line" && subPath === "/push" && method === "POST") {
|
|
1411
|
+
const body = await readJsonBody(request);
|
|
1412
|
+
const userId = String(body.userId ?? "");
|
|
1413
|
+
const message = body.message;
|
|
1414
|
+
if (!userId) return failure("userId is required");
|
|
1415
|
+
if (!message || message.type !== "text" || !message.text) {
|
|
1416
|
+
return failure("message must be { type: 'text', text: string }");
|
|
1417
|
+
}
|
|
1418
|
+
if (message.text.length > 5e3) {
|
|
1419
|
+
return failure("message text exceeds maximum length");
|
|
1420
|
+
}
|
|
1421
|
+
captureMessage(
|
|
1422
|
+
"line",
|
|
1423
|
+
userId,
|
|
1424
|
+
{ text: message.text },
|
|
1425
|
+
body.connectionId ? String(body.connectionId) : void 0
|
|
1426
|
+
);
|
|
1427
|
+
return success({ success: true });
|
|
1428
|
+
}
|
|
1429
|
+
if (channel === "facebook" && subPath === "/send" && method === "POST") {
|
|
1430
|
+
const body = await readJsonBody(request);
|
|
1431
|
+
const recipientId = String(body.recipientId ?? "");
|
|
1432
|
+
const message = body.message;
|
|
1433
|
+
if (!recipientId) return failure("recipientId is required");
|
|
1434
|
+
if (!message || message.type !== "text" || !message.text) {
|
|
1435
|
+
return failure("message must be { type: 'text', text: string }");
|
|
1436
|
+
}
|
|
1437
|
+
if (message.text.length > 2e3) {
|
|
1438
|
+
return failure("message text exceeds maximum length");
|
|
1439
|
+
}
|
|
1440
|
+
captureMessage(
|
|
1441
|
+
"facebook",
|
|
1442
|
+
recipientId,
|
|
1443
|
+
{
|
|
1444
|
+
text: message.text,
|
|
1445
|
+
messagingType: body.messagingType ? String(body.messagingType) : void 0,
|
|
1446
|
+
tag: body.tag ? String(body.tag) : void 0
|
|
1447
|
+
},
|
|
1448
|
+
body.connectionId ? String(body.connectionId) : void 0
|
|
1449
|
+
);
|
|
1450
|
+
return success({ success: true });
|
|
1451
|
+
}
|
|
1452
|
+
return failure(`No messaging simulator for ${method} .../integrations/${channel}${subPath}`, 404);
|
|
1453
|
+
}
|
|
1454
|
+
function createMessages() {
|
|
1455
|
+
return {
|
|
1456
|
+
all() {
|
|
1457
|
+
return [...state.messages];
|
|
1458
|
+
},
|
|
1459
|
+
latest() {
|
|
1460
|
+
return state.messages[state.messages.length - 1];
|
|
1461
|
+
},
|
|
1462
|
+
to(recipient) {
|
|
1463
|
+
return state.messages.filter((m) => m.recipient === recipient);
|
|
1464
|
+
},
|
|
1465
|
+
channel(kind) {
|
|
1466
|
+
const filtered = () => state.messages.filter((m) => m.channel === kind);
|
|
1467
|
+
return {
|
|
1468
|
+
all: () => filtered(),
|
|
1469
|
+
latest: () => {
|
|
1470
|
+
const pool = filtered();
|
|
1471
|
+
return pool[pool.length - 1];
|
|
1472
|
+
},
|
|
1473
|
+
to: (recipient) => filtered().filter((m) => m.recipient === recipient)
|
|
1474
|
+
};
|
|
1475
|
+
},
|
|
1476
|
+
clear() {
|
|
1477
|
+
state.messages = [];
|
|
1478
|
+
},
|
|
1479
|
+
get count() {
|
|
1480
|
+
return state.messages.length;
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// src/simulator/edge.ts
|
|
1486
|
+
function edgeFailure(error, status = 400, code) {
|
|
1487
|
+
return json({ success: false, error, ...code ? { code } : {} }, status);
|
|
1488
|
+
}
|
|
1489
|
+
function bindingNotFound(alias) {
|
|
1490
|
+
return edgeFailure(`No peripheral is paired to alias "${alias}"`, 404, "BINDING_NOT_FOUND");
|
|
1491
|
+
}
|
|
1492
|
+
function now2() {
|
|
1493
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1494
|
+
}
|
|
1495
|
+
function nextJobId() {
|
|
1496
|
+
state.edgePrintCounter += 1;
|
|
1497
|
+
return `job_${state.edgePrintCounter}`;
|
|
1498
|
+
}
|
|
1499
|
+
function nextConfigVersion() {
|
|
1500
|
+
return state.edgeDisplays.length + 1;
|
|
1501
|
+
}
|
|
1502
|
+
function findPeripheral(id) {
|
|
1503
|
+
return state.edgePeripherals.find((p) => p.id === id);
|
|
1504
|
+
}
|
|
1505
|
+
function buildBinding(alias, peripheralId) {
|
|
1506
|
+
const peripheral = findPeripheral(peripheralId);
|
|
1507
|
+
return {
|
|
1508
|
+
alias,
|
|
1509
|
+
state: peripheral ? "ok" : "peripheral_missing",
|
|
1510
|
+
peripheral: peripheral ? {
|
|
1511
|
+
id: peripheral.id,
|
|
1512
|
+
displayName: peripheral.displayName,
|
|
1513
|
+
driver: peripheral.driver,
|
|
1514
|
+
connected: peripheral.connected
|
|
1515
|
+
} : null,
|
|
1516
|
+
device: peripheral ? {
|
|
1517
|
+
id: peripheral.device.id,
|
|
1518
|
+
displayName: peripheral.device.displayName,
|
|
1519
|
+
status: peripheral.device.status
|
|
1520
|
+
} : null,
|
|
1521
|
+
updatedAt: now2()
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
function handleListDevices() {
|
|
1525
|
+
return success({ devices: [...state.edgeDevices] });
|
|
1526
|
+
}
|
|
1527
|
+
function handleListPeripherals(request) {
|
|
1528
|
+
const deviceId = new URL(request.url).searchParams.get("deviceId");
|
|
1529
|
+
const peripherals = deviceId ? state.edgePeripherals.filter((p) => p.device.id === deviceId) : [...state.edgePeripherals];
|
|
1530
|
+
return success({ peripherals });
|
|
1531
|
+
}
|
|
1532
|
+
function handleListBindings() {
|
|
1533
|
+
return success({ bindings: [...state.edgeBindings.values()] });
|
|
1534
|
+
}
|
|
1535
|
+
function handleGetBinding(alias) {
|
|
1536
|
+
const binding = state.edgeBindings.get(alias);
|
|
1537
|
+
if (!binding) return bindingNotFound(alias);
|
|
1538
|
+
return success({ binding });
|
|
1539
|
+
}
|
|
1540
|
+
async function handlePair(request) {
|
|
1541
|
+
const body = await readJsonBody(request);
|
|
1542
|
+
const alias = String(body.alias ?? "");
|
|
1543
|
+
const peripheralId = String(body.peripheralId ?? "");
|
|
1544
|
+
if (!alias) return edgeFailure("alias is required");
|
|
1545
|
+
if (!peripheralId) return edgeFailure("peripheralId is required");
|
|
1546
|
+
if (!findPeripheral(peripheralId)) {
|
|
1547
|
+
return edgeFailure("Peripheral not found or its device is not granted to this project", 404);
|
|
1548
|
+
}
|
|
1549
|
+
const binding = buildBinding(alias, peripheralId);
|
|
1550
|
+
state.edgeBindings.set(alias, binding);
|
|
1551
|
+
return success({ binding });
|
|
1552
|
+
}
|
|
1553
|
+
function handleUnpair(alias) {
|
|
1554
|
+
const existed = state.edgeBindings.delete(alias);
|
|
1555
|
+
if (!existed) return bindingNotFound(alias);
|
|
1556
|
+
return success({ deleted: true });
|
|
1557
|
+
}
|
|
1558
|
+
async function handlePrint(request) {
|
|
1559
|
+
const body = await readJsonBody(request);
|
|
1560
|
+
const jobId = nextJobId();
|
|
1561
|
+
const captured = {
|
|
1562
|
+
jobId,
|
|
1563
|
+
deploymentId: String(body.deploymentId ?? ""),
|
|
1564
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
1565
|
+
deviceId: body.deviceId ? String(body.deviceId) : void 0,
|
|
1566
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
|
|
1567
|
+
receipt: body.receipt ?? {},
|
|
1568
|
+
openDrawer: body.openDrawer === true,
|
|
1569
|
+
logo: body.logo === true ? true : body.logo === false ? false : void 0,
|
|
1570
|
+
copies: typeof body.copies === "number" ? body.copies : 1
|
|
1571
|
+
};
|
|
1572
|
+
state.edgePrints.push(captured);
|
|
1573
|
+
return success({
|
|
1574
|
+
jobId,
|
|
1575
|
+
status: "completed"
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
async function handleShowDisplay(request) {
|
|
1579
|
+
const body = await readJsonBody(request);
|
|
1580
|
+
const captured = {
|
|
1581
|
+
action: "show",
|
|
1582
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
1583
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
|
|
1584
|
+
url: body.url ? String(body.url) : void 0
|
|
1585
|
+
};
|
|
1586
|
+
state.edgeDisplays.push(captured);
|
|
1587
|
+
return success({
|
|
1588
|
+
configVersion: nextConfigVersion(),
|
|
1589
|
+
pushed: true,
|
|
1590
|
+
state: "showing"
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
function handleClearDisplay(request) {
|
|
1594
|
+
const url = new URL(request.url);
|
|
1595
|
+
const alias = url.searchParams.get("alias");
|
|
1596
|
+
const peripheralId = url.searchParams.get("peripheralId");
|
|
1597
|
+
const captured = {
|
|
1598
|
+
action: "clear",
|
|
1599
|
+
alias: alias ?? void 0,
|
|
1600
|
+
peripheralId: peripheralId ?? void 0
|
|
1601
|
+
};
|
|
1602
|
+
state.edgeDisplays.push(captured);
|
|
1603
|
+
return success({
|
|
1604
|
+
configVersion: nextConfigVersion(),
|
|
1605
|
+
pushed: true,
|
|
1606
|
+
state: "cleared"
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
async function handleTestPrint(request) {
|
|
1610
|
+
const body = await readJsonBody(request);
|
|
1611
|
+
const captured = {
|
|
1612
|
+
alias: body.alias ? String(body.alias) : void 0,
|
|
1613
|
+
deviceId: body.deviceId ? String(body.deviceId) : void 0,
|
|
1614
|
+
peripheralId: body.peripheralId ? String(body.peripheralId) : void 0
|
|
1615
|
+
};
|
|
1616
|
+
state.edgeTestPrints.push(captured);
|
|
1617
|
+
return success({
|
|
1618
|
+
status: "ok",
|
|
1619
|
+
peripheralId: captured.peripheralId ?? captured.alias ?? "default"
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
async function handleEdgeRequest(request, subPath) {
|
|
1623
|
+
const method = request.method;
|
|
1624
|
+
if (subPath === "/print" && method === "POST") {
|
|
1625
|
+
return handlePrint(request);
|
|
1626
|
+
}
|
|
1627
|
+
if (subPath === "/devices" && method === "GET") {
|
|
1628
|
+
return handleListDevices();
|
|
1629
|
+
}
|
|
1630
|
+
if (subPath === "/peripherals" && method === "GET") {
|
|
1631
|
+
return handleListPeripherals(request);
|
|
1632
|
+
}
|
|
1633
|
+
if (subPath === "/bindings" && method === "GET") {
|
|
1634
|
+
return handleListBindings();
|
|
1635
|
+
}
|
|
1636
|
+
if (subPath === "/bindings" && method === "PUT") {
|
|
1637
|
+
return handlePair(request);
|
|
1638
|
+
}
|
|
1639
|
+
const bindingMatch = subPath.match(/^\/bindings\/([^/]+)$/);
|
|
1640
|
+
if (bindingMatch) {
|
|
1641
|
+
const alias = decodeURIComponent(bindingMatch[1]);
|
|
1642
|
+
if (method === "GET") return handleGetBinding(alias);
|
|
1643
|
+
if (method === "DELETE") return handleUnpair(alias);
|
|
1644
|
+
}
|
|
1645
|
+
if (subPath === "/display" && method === "POST") {
|
|
1646
|
+
return handleShowDisplay(request);
|
|
1647
|
+
}
|
|
1648
|
+
if (subPath === "/display" && method === "DELETE") {
|
|
1649
|
+
return handleClearDisplay(request);
|
|
1650
|
+
}
|
|
1651
|
+
if (subPath === "/test-print" && method === "POST") {
|
|
1652
|
+
return handleTestPrint(request);
|
|
1653
|
+
}
|
|
1654
|
+
return edgeFailure(`No edge simulator for ${method} .../edge${subPath}`, 404);
|
|
1655
|
+
}
|
|
1656
|
+
function createEdge() {
|
|
1657
|
+
return {
|
|
1658
|
+
get prints() {
|
|
1659
|
+
return [...state.edgePrints];
|
|
1660
|
+
},
|
|
1661
|
+
get displays() {
|
|
1662
|
+
return [...state.edgeDisplays];
|
|
1663
|
+
},
|
|
1664
|
+
get testPrints() {
|
|
1665
|
+
return [...state.edgeTestPrints];
|
|
1666
|
+
},
|
|
1667
|
+
latestPrint() {
|
|
1668
|
+
return state.edgePrints[state.edgePrints.length - 1];
|
|
1669
|
+
},
|
|
1670
|
+
latestDisplay() {
|
|
1671
|
+
return state.edgeDisplays[state.edgeDisplays.length - 1];
|
|
1672
|
+
},
|
|
1673
|
+
get bindings() {
|
|
1674
|
+
return [...state.edgeBindings.values()];
|
|
1675
|
+
},
|
|
1676
|
+
seedDevices(devices) {
|
|
1677
|
+
state.edgeDevices = [...devices];
|
|
1678
|
+
},
|
|
1679
|
+
seedPeripherals(peripherals) {
|
|
1680
|
+
state.edgePeripherals = [...peripherals];
|
|
1681
|
+
},
|
|
1682
|
+
seedBindings(bindings) {
|
|
1683
|
+
state.edgeBindings.clear();
|
|
1684
|
+
for (const binding of bindings) {
|
|
1685
|
+
state.edgeBindings.set(binding.alias, binding);
|
|
1686
|
+
}
|
|
1687
|
+
},
|
|
1688
|
+
clear() {
|
|
1689
|
+
state.edgePrints = [];
|
|
1690
|
+
state.edgeDisplays = [];
|
|
1691
|
+
state.edgeTestPrints = [];
|
|
1692
|
+
state.edgePrintCounter = 0;
|
|
1693
|
+
state.edgeDevices = [];
|
|
1694
|
+
state.edgePeripherals = [];
|
|
1695
|
+
state.edgeBindings.clear();
|
|
1696
|
+
},
|
|
1697
|
+
get count() {
|
|
1698
|
+
return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length;
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
|
|
884
1703
|
// src/simulator/router.ts
|
|
885
1704
|
var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
886
1705
|
originalFetch: null
|
|
@@ -888,6 +1707,22 @@ var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
|
888
1707
|
function isLocalHost(hostname) {
|
|
889
1708
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0";
|
|
890
1709
|
}
|
|
1710
|
+
function requiresDeploymentHmac(request, url) {
|
|
1711
|
+
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
1712
|
+
const isPresignedPut = isStorageHost && request.method === "PUT" && /^\/presigned\/[^/]+$/.test(url.pathname);
|
|
1713
|
+
if (isStorageHost) {
|
|
1714
|
+
return !isPresignedPut;
|
|
1715
|
+
}
|
|
1716
|
+
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
1717
|
+
const isEmail = url.pathname === "/api/email/send";
|
|
1718
|
+
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
1719
|
+
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
1720
|
+
const messagingMatch = url.pathname.match(
|
|
1721
|
+
/^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
|
|
1722
|
+
);
|
|
1723
|
+
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
1724
|
+
return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
1725
|
+
}
|
|
891
1726
|
async function handleSimulatedRequest(request, url) {
|
|
892
1727
|
if (url.pathname === "/sql") {
|
|
893
1728
|
return handleNeonSql(requireDb(), request);
|
|
@@ -899,7 +1734,13 @@ async function handleSimulatedRequest(request, url) {
|
|
|
899
1734
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
900
1735
|
const isEmail = url.pathname === "/api/email/send";
|
|
901
1736
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
902
|
-
|
|
1737
|
+
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
1738
|
+
const messagingMatch = url.pathname.match(
|
|
1739
|
+
/^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
|
|
1740
|
+
);
|
|
1741
|
+
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
1742
|
+
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
1743
|
+
if (requiresDeploymentHmac(request, url)) {
|
|
903
1744
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
904
1745
|
if (!authHeader) {
|
|
905
1746
|
return failure("Missing authentication header", 401);
|
|
@@ -909,34 +1750,47 @@ async function handleSimulatedRequest(request, url) {
|
|
|
909
1750
|
return failure("Invalid authentication", 401);
|
|
910
1751
|
}
|
|
911
1752
|
}
|
|
1753
|
+
if (isStorageHost) {
|
|
1754
|
+
return handleStorageRequest(request, url);
|
|
1755
|
+
}
|
|
912
1756
|
if (isEmail && request.method === "POST") {
|
|
913
1757
|
return handleEmailSend(request);
|
|
914
1758
|
}
|
|
915
1759
|
if (identitiesMatch) {
|
|
916
1760
|
return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
|
|
917
1761
|
}
|
|
1762
|
+
if (storeMatch) {
|
|
1763
|
+
return handlePaymentsRequest(request, url);
|
|
1764
|
+
}
|
|
1765
|
+
if (messagingMatch) {
|
|
1766
|
+
const channel = messagingMatch[1];
|
|
1767
|
+
return handleMessagingRequest(request, channel, messagingMatch[2] ?? "");
|
|
1768
|
+
}
|
|
1769
|
+
if (edgeMatch) {
|
|
1770
|
+
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
1771
|
+
}
|
|
918
1772
|
if (dataStoreMatch) {
|
|
919
1773
|
const subPath = dataStoreMatch[1] ?? "";
|
|
920
1774
|
const db = requireDb();
|
|
921
|
-
const
|
|
1775
|
+
const readBody = async () => await request.json();
|
|
922
1776
|
if (subPath === "/query" && request.method === "POST") {
|
|
923
|
-
return handleQuery(db, await
|
|
1777
|
+
return handleQuery(db, await readBody());
|
|
924
1778
|
}
|
|
925
1779
|
if (subPath === "/mutate" && request.method === "POST") {
|
|
926
|
-
return handleMutate(db, await
|
|
1780
|
+
return handleMutate(db, await readBody());
|
|
927
1781
|
}
|
|
928
1782
|
if (subPath === "/schema" && request.method === "GET") {
|
|
929
1783
|
return handleGetSchema(db);
|
|
930
1784
|
}
|
|
931
1785
|
if (subPath === "/schema/tables" && request.method === "POST") {
|
|
932
|
-
return handleCreateTable(db, await
|
|
1786
|
+
return handleCreateTable(db, await readBody());
|
|
933
1787
|
}
|
|
934
1788
|
if (subPath === "/schema/columns" && request.method === "POST") {
|
|
935
|
-
return handleAddColumn(db, await
|
|
1789
|
+
return handleAddColumn(db, await readBody());
|
|
936
1790
|
}
|
|
937
1791
|
}
|
|
938
1792
|
return failure(
|
|
939
|
-
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, auth verify/refresh, Neon /sql.`,
|
|
1793
|
+
`[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.`,
|
|
940
1794
|
404
|
|
941
1795
|
);
|
|
942
1796
|
}
|
|
@@ -1022,10 +1876,18 @@ async function createTestApp(options = {}) {
|
|
|
1022
1876
|
}
|
|
1023
1877
|
const inbox = createInbox();
|
|
1024
1878
|
const directory = createDirectory();
|
|
1879
|
+
const payments = createPayments();
|
|
1880
|
+
const storage = createStorage();
|
|
1881
|
+
const messages = createMessages();
|
|
1882
|
+
const edge = createEdge();
|
|
1025
1883
|
const app = {
|
|
1026
1884
|
db,
|
|
1027
1885
|
inbox,
|
|
1028
1886
|
identities: directory,
|
|
1887
|
+
payments,
|
|
1888
|
+
storage,
|
|
1889
|
+
messages,
|
|
1890
|
+
edge,
|
|
1029
1891
|
async query(sql, params = []) {
|
|
1030
1892
|
const result = await db.query(sql, params);
|
|
1031
1893
|
return result.rows;
|
|
@@ -1041,8 +1903,8 @@ async function createTestApp(options = {}) {
|
|
|
1041
1903
|
issueSession(user) {
|
|
1042
1904
|
const fullUser = buildUser(user);
|
|
1043
1905
|
const tokens = {
|
|
1044
|
-
accessToken: `test-access-${
|
|
1045
|
-
refreshToken: `test-refresh-${
|
|
1906
|
+
accessToken: `test-access-${import_node_crypto8.default.randomUUID()}`,
|
|
1907
|
+
refreshToken: `test-refresh-${import_node_crypto8.default.randomUUID()}`
|
|
1046
1908
|
};
|
|
1047
1909
|
state.sessions.set(tokens.accessToken, fullUser);
|
|
1048
1910
|
state.refreshSessions.set(tokens.refreshToken, fullUser);
|
|
@@ -1058,7 +1920,10 @@ async function createTestApp(options = {}) {
|
|
|
1058
1920
|
state.emailCounter = 0;
|
|
1059
1921
|
state.identities.clear();
|
|
1060
1922
|
state.identityLinks = [];
|
|
1061
|
-
|
|
1923
|
+
payments.clear();
|
|
1924
|
+
storage.clear();
|
|
1925
|
+
messages.clear();
|
|
1926
|
+
edge.clear();
|
|
1062
1927
|
},
|
|
1063
1928
|
async close() {
|
|
1064
1929
|
state.db = null;
|
|
@@ -1066,9 +1931,13 @@ async function createTestApp(options = {}) {
|
|
|
1066
1931
|
state.sessions.clear();
|
|
1067
1932
|
state.refreshSessions.clear();
|
|
1068
1933
|
state.emails = [];
|
|
1934
|
+
state.emailCounter = 0;
|
|
1069
1935
|
state.identities.clear();
|
|
1070
1936
|
state.identityLinks = [];
|
|
1071
|
-
|
|
1937
|
+
payments.clear();
|
|
1938
|
+
storage.clear();
|
|
1939
|
+
messages.clear();
|
|
1940
|
+
edge.clear();
|
|
1072
1941
|
uninstallFetchRouter();
|
|
1073
1942
|
await db.close();
|
|
1074
1943
|
}
|
|
@@ -1076,119 +1945,6 @@ async function createTestApp(options = {}) {
|
|
|
1076
1945
|
return app;
|
|
1077
1946
|
}
|
|
1078
1947
|
|
|
1079
|
-
// src/module-app.ts
|
|
1080
|
-
var import_core = require("@stardeck-customer-apps/core");
|
|
1081
|
-
async function createModuleApp(options) {
|
|
1082
|
-
const schemaSql = options.modules.map((m) => (0, import_core.renderSchemaOpsToSql)(m.schema)).join("\n\n");
|
|
1083
|
-
const app = await createTestApp({ schemaSql, allowNetwork: options.allowNetwork });
|
|
1084
|
-
const sql = { query: (text, params) => app.db.query(text, params ?? []) };
|
|
1085
|
-
const data = (0, import_core.makeSqlPort)(sql);
|
|
1086
|
-
const { createIntegrationsClient } = await import("@stardeck-customer-apps/integrations-sdk");
|
|
1087
|
-
const identities = createIntegrationsClient({
|
|
1088
|
-
controlPlaneUrl: TEST_ENV_DEFAULTS.CONTROL_PLANE_URL,
|
|
1089
|
-
organizationId: TEST_ENV_DEFAULTS.ORGANIZATION_ID,
|
|
1090
|
-
projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
|
|
1091
|
-
deploymentId: TEST_ENV_DEFAULTS.DEPLOYMENT_ID,
|
|
1092
|
-
deploymentSecret: TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET
|
|
1093
|
-
}).identities;
|
|
1094
|
-
const runSeed = async () => {
|
|
1095
|
-
if (options.seed) await options.seed({ data, identities });
|
|
1096
|
-
};
|
|
1097
|
-
await runSeed();
|
|
1098
|
-
return {
|
|
1099
|
-
app,
|
|
1100
|
-
data,
|
|
1101
|
-
identities,
|
|
1102
|
-
async reset() {
|
|
1103
|
-
await app.reset();
|
|
1104
|
-
await runSeed();
|
|
1105
|
-
},
|
|
1106
|
-
async close() {
|
|
1107
|
-
await app.close();
|
|
1108
|
-
}
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
// src/next/headers-shim.ts
|
|
1113
|
-
var import_node_async_hooks = require("async_hooks");
|
|
1114
|
-
var requestScopeStorage = globalSingleton(
|
|
1115
|
-
"request-scope",
|
|
1116
|
-
() => new import_node_async_hooks.AsyncLocalStorage()
|
|
1117
|
-
);
|
|
1118
|
-
|
|
1119
|
-
// src/next/call-route.ts
|
|
1120
|
-
function parseCookieHeader(header) {
|
|
1121
|
-
const map = /* @__PURE__ */ new Map();
|
|
1122
|
-
if (!header) return map;
|
|
1123
|
-
for (const part of header.split(";")) {
|
|
1124
|
-
const eq = part.indexOf("=");
|
|
1125
|
-
if (eq === -1) continue;
|
|
1126
|
-
map.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
|
|
1127
|
-
}
|
|
1128
|
-
return map;
|
|
1129
|
-
}
|
|
1130
|
-
async function importNextServer() {
|
|
1131
|
-
try {
|
|
1132
|
-
return await import("next/server.js");
|
|
1133
|
-
} catch {
|
|
1134
|
-
return await import("next/server");
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
async function callRoute(handler, options = {}) {
|
|
1138
|
-
const { NextRequest } = await importNextServer();
|
|
1139
|
-
const path = options.path ?? "/api/test-route";
|
|
1140
|
-
const url = new URL(`http://localhost:3333${path}`);
|
|
1141
|
-
for (const [key, value] of Object.entries(options.searchParams ?? {})) {
|
|
1142
|
-
url.searchParams.set(key, value);
|
|
1143
|
-
}
|
|
1144
|
-
const method = options.method ?? (options.body !== void 0 ? "POST" : "GET");
|
|
1145
|
-
const headers = new Headers(options.headers);
|
|
1146
|
-
const user = options.user !== void 0 ? options.user : state.currentUser;
|
|
1147
|
-
if (user && !headers.has("x-stardeck-user")) {
|
|
1148
|
-
headers.set("x-stardeck-user", JSON.stringify(user));
|
|
1149
|
-
}
|
|
1150
|
-
if (options.body !== void 0 && !headers.has("Content-Type")) {
|
|
1151
|
-
headers.set("Content-Type", "application/json");
|
|
1152
|
-
}
|
|
1153
|
-
const cookiePairs = Object.entries(options.cookies ?? {});
|
|
1154
|
-
if (cookiePairs.length > 0) {
|
|
1155
|
-
const existing = headers.get("cookie");
|
|
1156
|
-
const cookieString = cookiePairs.map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1157
|
-
headers.set("cookie", existing ? `${existing}; ${cookieString}` : cookieString);
|
|
1158
|
-
}
|
|
1159
|
-
const request = new NextRequest(url, {
|
|
1160
|
-
method,
|
|
1161
|
-
headers,
|
|
1162
|
-
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
1163
|
-
});
|
|
1164
|
-
const scope = {
|
|
1165
|
-
headers,
|
|
1166
|
-
cookies: parseCookieHeader(headers.get("cookie"))
|
|
1167
|
-
};
|
|
1168
|
-
try {
|
|
1169
|
-
return await requestScopeStorage.run(
|
|
1170
|
-
scope,
|
|
1171
|
-
() => Promise.resolve(
|
|
1172
|
-
handler(request, {
|
|
1173
|
-
params: Promise.resolve(options.params ?? {})
|
|
1174
|
-
})
|
|
1175
|
-
)
|
|
1176
|
-
);
|
|
1177
|
-
} catch (error) {
|
|
1178
|
-
const redirect = decodeNextRedirect(error);
|
|
1179
|
-
if (redirect) return redirect;
|
|
1180
|
-
throw error;
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
function decodeNextRedirect(error) {
|
|
1184
|
-
const digest = error?.digest;
|
|
1185
|
-
if (typeof digest !== "string" || !digest.startsWith("NEXT_REDIRECT")) return null;
|
|
1186
|
-
const parts = digest.split(";");
|
|
1187
|
-
const location = parts[2] ?? "/";
|
|
1188
|
-
const status = Number(parts[3]) || 307;
|
|
1189
|
-
return new Response(null, { status, headers: { location } });
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
1948
|
// src/workflow.ts
|
|
1193
1949
|
var import_vitest = require("vitest");
|
|
1194
1950
|
function describeWorkflow(name, fn) {
|
|
@@ -1203,10 +1959,11 @@ function parseWorkflowName(describeTitle) {
|
|
|
1203
1959
|
CONTROL_PLANE_TEST_URL,
|
|
1204
1960
|
DATA_STORE_TEST_HOST,
|
|
1205
1961
|
DEFAULT_SCHEMA_PATH,
|
|
1962
|
+
STORAGE_TEST_HOST,
|
|
1963
|
+
STORAGE_TEST_URL,
|
|
1206
1964
|
TEST_ENV_DEFAULTS,
|
|
1207
1965
|
WORKFLOW_NAME_PREFIX,
|
|
1208
1966
|
callRoute,
|
|
1209
|
-
createModuleApp,
|
|
1210
1967
|
createTestApp,
|
|
1211
1968
|
describeWorkflow,
|
|
1212
1969
|
parseWorkflowName
|