@stardeck-customer-apps/testing 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +50 -0
- package/dist/index.d.mts +110 -67
- package/dist/index.d.ts +110 -67
- package/dist/index.js +754 -231
- package/dist/index.mjs +752 -233
- package/dist/next/headers-shim.js +10 -1
- package/dist/next/headers-shim.mjs +10 -1
- package/dist/setup.js +502 -111
- package/dist/setup.mjs +502 -111
- package/package.json +12 -3
package/dist/setup.js
CHANGED
|
@@ -40,7 +40,16 @@ var state = globalSingleton("state", () => ({
|
|
|
40
40
|
emailCounter: 0,
|
|
41
41
|
identities: /* @__PURE__ */ new Map(),
|
|
42
42
|
identityLinks: [],
|
|
43
|
-
|
|
43
|
+
checkouts: [],
|
|
44
|
+
checkoutCounter: 0,
|
|
45
|
+
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
46
|
+
paymentLinks: /* @__PURE__ */ new Map(),
|
|
47
|
+
products: [],
|
|
48
|
+
uploads: [],
|
|
49
|
+
uploadCounter: 0,
|
|
50
|
+
storageFiles: /* @__PURE__ */ new Map(),
|
|
51
|
+
presignPending: /* @__PURE__ */ new Map(),
|
|
52
|
+
messages: [],
|
|
44
53
|
allowNetwork: false
|
|
45
54
|
}));
|
|
46
55
|
function requireDb() {
|
|
@@ -56,13 +65,16 @@ function requireDb() {
|
|
|
56
65
|
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
57
66
|
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
58
67
|
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
68
|
+
var STORAGE_TEST_URL = "https://storage.stardeck.test";
|
|
69
|
+
var STORAGE_TEST_HOST = "storage.stardeck.test";
|
|
59
70
|
var TEST_ENV_DEFAULTS = {
|
|
60
71
|
CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
|
|
61
72
|
DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
|
|
62
73
|
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
63
74
|
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
64
75
|
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
65
|
-
DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main
|
|
76
|
+
DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`,
|
|
77
|
+
STORAGE_URL: STORAGE_TEST_URL
|
|
66
78
|
};
|
|
67
79
|
|
|
68
80
|
// src/simulator/hmac.ts
|
|
@@ -110,6 +122,13 @@ function success(data) {
|
|
|
110
122
|
function failure(error, status = 400) {
|
|
111
123
|
return json({ success: false, error }, status);
|
|
112
124
|
}
|
|
125
|
+
async function readJsonBody(request) {
|
|
126
|
+
try {
|
|
127
|
+
return await request.json();
|
|
128
|
+
} catch {
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
113
132
|
|
|
114
133
|
// src/simulator/data-store.ts
|
|
115
134
|
function quoteIdent(name) {
|
|
@@ -590,13 +609,6 @@ function now() {
|
|
|
590
609
|
function linksFor(identityId) {
|
|
591
610
|
return state.identityLinks.filter((l) => l.identityId === identityId);
|
|
592
611
|
}
|
|
593
|
-
async function readBody(request) {
|
|
594
|
-
try {
|
|
595
|
-
return await request.json();
|
|
596
|
-
} catch {
|
|
597
|
-
return {};
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
612
|
function handleList(request) {
|
|
601
613
|
const typeParam = new URL(request.url).searchParams.get("type");
|
|
602
614
|
const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
|
|
@@ -604,7 +616,7 @@ function handleList(request) {
|
|
|
604
616
|
return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
|
|
605
617
|
}
|
|
606
618
|
async function handleCreate(request) {
|
|
607
|
-
const body = await
|
|
619
|
+
const body = await readJsonBody(request);
|
|
608
620
|
const type = body.type;
|
|
609
621
|
if (type !== "person" && type !== "account") {
|
|
610
622
|
return failure("type must be 'person' or 'account'");
|
|
@@ -641,7 +653,7 @@ function handleGet(identityId) {
|
|
|
641
653
|
async function handleUpdate(identityId, request) {
|
|
642
654
|
const identity = state.identities.get(identityId);
|
|
643
655
|
if (!identity) return failure("identity not found", 404);
|
|
644
|
-
const body = await
|
|
656
|
+
const body = await readJsonBody(request);
|
|
645
657
|
if (body.displayName !== void 0) {
|
|
646
658
|
identity.displayName = body.displayName;
|
|
647
659
|
}
|
|
@@ -663,7 +675,7 @@ async function handleAttachLink(identityId, request) {
|
|
|
663
675
|
if (identity.status !== "active") {
|
|
664
676
|
return failure("links attach only to active persons");
|
|
665
677
|
}
|
|
666
|
-
const body = await
|
|
678
|
+
const body = await readJsonBody(request);
|
|
667
679
|
const kind = body.kind;
|
|
668
680
|
const externalId = body.externalId;
|
|
669
681
|
if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
|
|
@@ -688,108 +700,16 @@ async function handleAttachLink(identityId, request) {
|
|
|
688
700
|
state.identityLinks.push(link);
|
|
689
701
|
return success({ link });
|
|
690
702
|
}
|
|
691
|
-
function memoryDto(m) {
|
|
692
|
-
return {
|
|
693
|
-
id: m.id,
|
|
694
|
-
source: m.source,
|
|
695
|
-
kind: m.kind,
|
|
696
|
-
content: m.content,
|
|
697
|
-
metadata: m.metadata,
|
|
698
|
-
createdAt: m.createdAt
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
async function handleResolve(request) {
|
|
702
|
-
const body = await readBody(request);
|
|
703
|
-
const type = body.type;
|
|
704
|
-
if (type !== "person" && type !== "account") {
|
|
705
|
-
return failure("type must be 'person' or 'account'");
|
|
706
|
-
}
|
|
707
|
-
if (type === "account") {
|
|
708
|
-
return failure(
|
|
709
|
-
"resolveOrCreate is for channel-linked persons; an account is not resolvable by link \u2014 create it explicitly instead.",
|
|
710
|
-
409
|
|
711
|
-
);
|
|
712
|
-
}
|
|
713
|
-
const link = body.link;
|
|
714
|
-
const kind = link?.kind;
|
|
715
|
-
const externalId = link?.externalId;
|
|
716
|
-
if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
|
|
717
|
-
return failure(`link.kind must be one of: ${[...LINK_KINDS].join(", ")}`);
|
|
718
|
-
}
|
|
719
|
-
if (typeof externalId !== "string" || !externalId) {
|
|
720
|
-
return failure("link.externalId is required");
|
|
721
|
-
}
|
|
722
|
-
const existingLink = state.identityLinks.find(
|
|
723
|
-
(l) => l.kind === kind && l.externalId === externalId
|
|
724
|
-
);
|
|
725
|
-
if (existingLink) {
|
|
726
|
-
const identity2 = state.identities.get(existingLink.identityId);
|
|
727
|
-
if (identity2) return success({ identity: identity2, created: false });
|
|
728
|
-
}
|
|
729
|
-
const identity = {
|
|
730
|
-
id: import_node_crypto3.default.randomUUID(),
|
|
731
|
-
type,
|
|
732
|
-
parentId: null,
|
|
733
|
-
displayName: body.displayName ?? null,
|
|
734
|
-
profile: body.profile ?? {},
|
|
735
|
-
status: "active",
|
|
736
|
-
mergedIntoId: null,
|
|
737
|
-
externalRef: null,
|
|
738
|
-
createdAt: now(),
|
|
739
|
-
updatedAt: now()
|
|
740
|
-
};
|
|
741
|
-
state.identities.set(identity.id, identity);
|
|
742
|
-
state.identityLinks.push({
|
|
743
|
-
id: import_node_crypto3.default.randomUUID(),
|
|
744
|
-
identityId: identity.id,
|
|
745
|
-
kind,
|
|
746
|
-
externalId,
|
|
747
|
-
verified: true,
|
|
748
|
-
createdAt: now()
|
|
749
|
-
});
|
|
750
|
-
return success({ identity, created: true });
|
|
751
|
-
}
|
|
752
|
-
async function handleWriteMemory(identityId, request) {
|
|
753
|
-
if (!state.identities.get(identityId)) return failure("Identity not found", 404);
|
|
754
|
-
const body = await readBody(request);
|
|
755
|
-
if (typeof body.content !== "string" || !body.content) return failure("content is required");
|
|
756
|
-
const entry = {
|
|
757
|
-
id: import_node_crypto3.default.randomUUID(),
|
|
758
|
-
identityId,
|
|
759
|
-
source: typeof body.source === "string" ? body.source : "app",
|
|
760
|
-
kind: typeof body.kind === "string" ? body.kind : "fact",
|
|
761
|
-
content: body.content,
|
|
762
|
-
metadata: body.metadata ?? {},
|
|
763
|
-
createdAt: now()
|
|
764
|
-
};
|
|
765
|
-
state.identityMemory.push(entry);
|
|
766
|
-
return success({ memory: memoryDto(entry) });
|
|
767
|
-
}
|
|
768
|
-
function handleListMemory(identityId, request) {
|
|
769
|
-
if (!state.identities.get(identityId)) return failure("Identity not found", 404);
|
|
770
|
-
let rows = state.identityMemory.filter((m) => m.identityId === identityId).reverse();
|
|
771
|
-
const limitParam = new URL(request.url).searchParams.get("limit");
|
|
772
|
-
if (limitParam) rows = rows.slice(0, Math.max(1, parseInt(limitParam, 10) || 0));
|
|
773
|
-
return success({ memories: rows.map(memoryDto) });
|
|
774
|
-
}
|
|
775
703
|
async function handleIdentitiesRequest(request, subPath) {
|
|
776
704
|
const method = request.method;
|
|
777
705
|
if (subPath === "" || subPath === "/") {
|
|
778
706
|
if (method === "GET") return handleList(request);
|
|
779
707
|
if (method === "POST") return handleCreate(request);
|
|
780
708
|
}
|
|
781
|
-
if (subPath === "/resolve" && method === "POST") {
|
|
782
|
-
return handleResolve(request);
|
|
783
|
-
}
|
|
784
709
|
const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
|
|
785
710
|
if (linksMatch && method === "POST") {
|
|
786
711
|
return handleAttachLink(linksMatch[1], request);
|
|
787
712
|
}
|
|
788
|
-
const memoryMatch = subPath.match(/^\/([^/]+)\/memory$/);
|
|
789
|
-
if (memoryMatch) {
|
|
790
|
-
if (method === "GET") return handleListMemory(memoryMatch[1], request);
|
|
791
|
-
if (method === "POST") return handleWriteMemory(memoryMatch[1], request);
|
|
792
|
-
}
|
|
793
713
|
const singleMatch = subPath.match(/^\/([^/]+)$/);
|
|
794
714
|
if (singleMatch) {
|
|
795
715
|
if (method === "GET") return handleGet(singleMatch[1]);
|
|
@@ -798,6 +718,447 @@ async function handleIdentitiesRequest(request, subPath) {
|
|
|
798
718
|
return failure(`No identities simulator for ${method} .../identities${subPath}`, 404);
|
|
799
719
|
}
|
|
800
720
|
|
|
721
|
+
// src/simulator/payments.ts
|
|
722
|
+
var import_node_crypto5 = __toESM(require("crypto"));
|
|
723
|
+
|
|
724
|
+
// src/next/headers-shim.ts
|
|
725
|
+
var import_node_async_hooks = require("async_hooks");
|
|
726
|
+
var requestScopeStorage = globalSingleton(
|
|
727
|
+
"request-scope",
|
|
728
|
+
() => new import_node_async_hooks.AsyncLocalStorage()
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
// src/simulator/webhook-signing.ts
|
|
732
|
+
var import_node_crypto4 = __toESM(require("crypto"));
|
|
733
|
+
|
|
734
|
+
// src/simulator/payments.ts
|
|
735
|
+
function payErr(error, status = 400, code) {
|
|
736
|
+
return json(code ? { error, code } : { error }, status);
|
|
737
|
+
}
|
|
738
|
+
function nextCheckoutId() {
|
|
739
|
+
state.checkoutCounter += 1;
|
|
740
|
+
return `cs_test_${state.checkoutCounter}`;
|
|
741
|
+
}
|
|
742
|
+
function nextPaymentLinkId() {
|
|
743
|
+
state.checkoutCounter += 1;
|
|
744
|
+
return `plink_test_${state.checkoutCounter}`;
|
|
745
|
+
}
|
|
746
|
+
function seedStripeSession(id, body) {
|
|
747
|
+
const lineItems = body.lineItems ?? [];
|
|
748
|
+
let amountTotal = null;
|
|
749
|
+
let currency = null;
|
|
750
|
+
if (lineItems.length > 0) {
|
|
751
|
+
amountTotal = lineItems.reduce(
|
|
752
|
+
(sum, item) => sum + (item.priceData?.unitAmount ?? 0) * (item.quantity ?? 1),
|
|
753
|
+
0
|
|
754
|
+
);
|
|
755
|
+
currency = lineItems[0].priceData?.currency ?? null;
|
|
756
|
+
}
|
|
757
|
+
state.sessionStatuses.set(id, {
|
|
758
|
+
id,
|
|
759
|
+
status: "open",
|
|
760
|
+
paymentStatus: "unpaid",
|
|
761
|
+
mode: body.mode ?? "payment",
|
|
762
|
+
amountTotal,
|
|
763
|
+
currency,
|
|
764
|
+
customerEmail: body.customerEmail ? String(body.customerEmail) : null,
|
|
765
|
+
metadata: body.metadata ?? {},
|
|
766
|
+
expiresAt: Math.floor(Date.now() / 1e3) + 3600
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
function seedBeamLink(id, body, merchantId) {
|
|
770
|
+
const order = body.order;
|
|
771
|
+
state.paymentLinks.set(id, {
|
|
772
|
+
paymentLinkId: id,
|
|
773
|
+
merchantId,
|
|
774
|
+
url: `https://beam.test/pay/${id}`,
|
|
775
|
+
status: "ACTIVE",
|
|
776
|
+
order: {
|
|
777
|
+
netAmount: Number(order?.netAmount ?? 0),
|
|
778
|
+
currency: String(order?.currency ?? "THB"),
|
|
779
|
+
description: String(order?.description ?? ""),
|
|
780
|
+
referenceId: order?.referenceId ? String(order.referenceId) : void 0,
|
|
781
|
+
internalNote: order?.internalNote ? String(order.internalNote) : void 0,
|
|
782
|
+
orderItems: order?.orderItems
|
|
783
|
+
},
|
|
784
|
+
redirectUrl: body.redirectUrl ? String(body.redirectUrl) : void 0,
|
|
785
|
+
linkSettings: body.linkSettings,
|
|
786
|
+
collectDeliveryAddress: body.collectDeliveryAddress === true
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
async function handlePaymentsRequest(request, url) {
|
|
790
|
+
const pathname = url.pathname;
|
|
791
|
+
if (/\/bolt-connections/.test(pathname) || /\/bolt-intents/.test(pathname) || /\/charges(\/|$)/.test(pathname) || /\/billing-portal$/.test(pathname)) {
|
|
792
|
+
return payErr("Not found", 404, "NOT_FOUND");
|
|
793
|
+
}
|
|
794
|
+
const beamProductsMatch = pathname.match(
|
|
795
|
+
/^\/api\/store\/beam\/([^/]+)\/payment-links(?:\/([^/]+))?$/
|
|
796
|
+
);
|
|
797
|
+
if (beamProductsMatch) {
|
|
798
|
+
const merchantId = beamProductsMatch[1];
|
|
799
|
+
const linkId = beamProductsMatch[2];
|
|
800
|
+
if (!linkId && request.method === "POST") {
|
|
801
|
+
const body = await readJsonBody(request);
|
|
802
|
+
const id = nextPaymentLinkId();
|
|
803
|
+
const checkoutUrl = `https://beam.test/pay/${id}`;
|
|
804
|
+
const captured = {
|
|
805
|
+
id,
|
|
806
|
+
url: checkoutUrl,
|
|
807
|
+
provider: "beam",
|
|
808
|
+
options: body,
|
|
809
|
+
metadata: body.metadata,
|
|
810
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
811
|
+
};
|
|
812
|
+
state.checkouts.push(captured);
|
|
813
|
+
seedBeamLink(id, body, merchantId);
|
|
814
|
+
return json({ id, url: checkoutUrl });
|
|
815
|
+
}
|
|
816
|
+
if (linkId && request.method === "GET") {
|
|
817
|
+
const link = state.paymentLinks.get(linkId);
|
|
818
|
+
if (!link) return payErr("Payment link not found", 404, "NOT_FOUND");
|
|
819
|
+
return json({ paymentLink: link });
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
const stripeStoreMatch = pathname.match(/^\/api\/store\/([^/]+)\/(.+)$/);
|
|
823
|
+
if (stripeStoreMatch) {
|
|
824
|
+
const accountId = stripeStoreMatch[1];
|
|
825
|
+
const subPath = stripeStoreMatch[2];
|
|
826
|
+
if (accountId === "beam") {
|
|
827
|
+
return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
|
|
828
|
+
}
|
|
829
|
+
if (subPath === "products" && request.method === "GET") {
|
|
830
|
+
return json({ products: state.products });
|
|
831
|
+
}
|
|
832
|
+
const productMatch = subPath.match(/^products\/([^/]+)$/);
|
|
833
|
+
if (productMatch && request.method === "GET") {
|
|
834
|
+
const product = state.products.find((p) => p.id === productMatch[1]);
|
|
835
|
+
if (!product) return payErr("Product not found", 404, "NOT_FOUND");
|
|
836
|
+
return json({ product });
|
|
837
|
+
}
|
|
838
|
+
if (subPath === "checkout" && request.method === "POST") {
|
|
839
|
+
const body = await readJsonBody(request);
|
|
840
|
+
const id = nextCheckoutId();
|
|
841
|
+
const checkoutUrl = `https://checkout.stripe.test/c/pay/${id}`;
|
|
842
|
+
const captured = {
|
|
843
|
+
id,
|
|
844
|
+
url: checkoutUrl,
|
|
845
|
+
provider: "stripe",
|
|
846
|
+
options: body,
|
|
847
|
+
mode: body.mode,
|
|
848
|
+
metadata: body.metadata,
|
|
849
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
850
|
+
};
|
|
851
|
+
state.checkouts.push(captured);
|
|
852
|
+
seedStripeSession(id, body);
|
|
853
|
+
return json({ id, url: checkoutUrl });
|
|
854
|
+
}
|
|
855
|
+
const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
|
|
856
|
+
if (sessionMatch && request.method === "GET") {
|
|
857
|
+
const session = state.sessionStatuses.get(sessionMatch[1]);
|
|
858
|
+
if (!session) return payErr("Checkout session not found", 404, "NOT_FOUND");
|
|
859
|
+
return json({ session });
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/simulator/storage.ts
|
|
866
|
+
var import_node_crypto6 = __toESM(require("crypto"));
|
|
867
|
+
function storageErr(error, status = 400) {
|
|
868
|
+
return json({ error }, status);
|
|
869
|
+
}
|
|
870
|
+
function fileExtension(filename) {
|
|
871
|
+
const dot = filename.lastIndexOf(".");
|
|
872
|
+
return dot === -1 ? "bin" : filename.slice(dot + 1);
|
|
873
|
+
}
|
|
874
|
+
function buildKey(fileId, filename) {
|
|
875
|
+
const orgId = process.env.ORGANIZATION_ID ?? TEST_ENV_DEFAULTS.ORGANIZATION_ID;
|
|
876
|
+
const projectId = process.env.PROJECT_ID ?? TEST_ENV_DEFAULTS.PROJECT_ID;
|
|
877
|
+
const deploymentId = process.env.DEPLOYMENT_ID ?? TEST_ENV_DEFAULTS.DEPLOYMENT_ID;
|
|
878
|
+
return `${orgId}/${projectId}/${deploymentId}/${fileId}.${fileExtension(filename)}`;
|
|
879
|
+
}
|
|
880
|
+
function buildUrl(key) {
|
|
881
|
+
return `${STORAGE_TEST_URL}/files/${key}`;
|
|
882
|
+
}
|
|
883
|
+
function toUploadResponse(record) {
|
|
884
|
+
return {
|
|
885
|
+
id: record.id,
|
|
886
|
+
key: record.key,
|
|
887
|
+
filename: record.filename,
|
|
888
|
+
contentType: record.contentType,
|
|
889
|
+
sizeBytes: record.sizeBytes,
|
|
890
|
+
url: record.url,
|
|
891
|
+
uploadedAt: record.uploadedAt,
|
|
892
|
+
isPublic: record.isPublic,
|
|
893
|
+
metadata: record.metadata
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
function captureUpload(record, method, path) {
|
|
897
|
+
const captured = {
|
|
898
|
+
id: record.id,
|
|
899
|
+
key: record.key,
|
|
900
|
+
filename: record.filename,
|
|
901
|
+
contentType: record.contentType,
|
|
902
|
+
sizeBytes: record.sizeBytes,
|
|
903
|
+
url: record.url,
|
|
904
|
+
uploadedAt: record.uploadedAt,
|
|
905
|
+
isPublic: record.isPublic,
|
|
906
|
+
metadata: record.metadata,
|
|
907
|
+
method,
|
|
908
|
+
path
|
|
909
|
+
};
|
|
910
|
+
state.uploads.push(captured);
|
|
911
|
+
}
|
|
912
|
+
function createFileRecord(input) {
|
|
913
|
+
state.uploadCounter += 1;
|
|
914
|
+
const id = `file_test_${state.uploadCounter}`;
|
|
915
|
+
const key = buildKey(id, input.filename);
|
|
916
|
+
return {
|
|
917
|
+
id,
|
|
918
|
+
key,
|
|
919
|
+
filename: input.filename,
|
|
920
|
+
contentType: input.contentType,
|
|
921
|
+
sizeBytes: input.sizeBytes,
|
|
922
|
+
url: buildUrl(key),
|
|
923
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
924
|
+
metadata: input.metadata,
|
|
925
|
+
isPublic: input.isPublic,
|
|
926
|
+
deleted: false
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function activeFiles() {
|
|
930
|
+
return [...state.storageFiles.values()].filter((f) => !f.deleted);
|
|
931
|
+
}
|
|
932
|
+
async function handleStorageRequest(request, url) {
|
|
933
|
+
const pathname = url.pathname;
|
|
934
|
+
if (pathname.startsWith("/upload/presign/multipart")) {
|
|
935
|
+
return storageErr("Not found", 404);
|
|
936
|
+
}
|
|
937
|
+
const presignedPutMatch = pathname.match(/^\/presigned\/([^/]+)$/);
|
|
938
|
+
if (presignedPutMatch && request.method === "PUT") {
|
|
939
|
+
const fileId = presignedPutMatch[1];
|
|
940
|
+
if (!state.presignPending.has(fileId)) {
|
|
941
|
+
return storageErr("Presign session not found", 404);
|
|
942
|
+
}
|
|
943
|
+
return new Response(null, {
|
|
944
|
+
status: 200,
|
|
945
|
+
headers: { etag: `"${import_node_crypto6.default.randomUUID()}"` }
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
if (pathname === "/upload/presign/complete" && request.method === "POST") {
|
|
949
|
+
const body = await readJsonBody(request);
|
|
950
|
+
const fileId = String(body.fileId ?? "");
|
|
951
|
+
const pending = state.presignPending.get(fileId);
|
|
952
|
+
if (!pending) return storageErr("Presign session not found", 404);
|
|
953
|
+
const record = {
|
|
954
|
+
id: fileId,
|
|
955
|
+
key: pending.key,
|
|
956
|
+
filename: pending.fileName,
|
|
957
|
+
contentType: pending.contentType,
|
|
958
|
+
sizeBytes: pending.sizeBytes,
|
|
959
|
+
url: buildUrl(pending.key),
|
|
960
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
961
|
+
metadata: pending.metadata,
|
|
962
|
+
isPublic: pending.isPublic,
|
|
963
|
+
deleted: false
|
|
964
|
+
};
|
|
965
|
+
state.storageFiles.set(record.id, record);
|
|
966
|
+
state.presignPending.delete(fileId);
|
|
967
|
+
captureUpload(record, "POST", "/upload/presign/complete");
|
|
968
|
+
return json(toUploadResponse(record));
|
|
969
|
+
}
|
|
970
|
+
if (pathname === "/upload/presign" && request.method === "POST") {
|
|
971
|
+
const body = await readJsonBody(request);
|
|
972
|
+
const fileName = String(body.fileName ?? "upload.bin");
|
|
973
|
+
const contentType = String(body.contentType ?? "application/octet-stream");
|
|
974
|
+
const sizeBytes = Number(body.sizeBytes ?? 0);
|
|
975
|
+
const isPublic = body.isPublic !== false;
|
|
976
|
+
const metadata = body.metadata;
|
|
977
|
+
state.uploadCounter += 1;
|
|
978
|
+
const fileId = `file_test_${state.uploadCounter}`;
|
|
979
|
+
const key = buildKey(fileId, fileName);
|
|
980
|
+
state.presignPending.set(fileId, {
|
|
981
|
+
fileName,
|
|
982
|
+
contentType,
|
|
983
|
+
sizeBytes,
|
|
984
|
+
isPublic,
|
|
985
|
+
metadata,
|
|
986
|
+
key
|
|
987
|
+
});
|
|
988
|
+
return json({
|
|
989
|
+
fileId,
|
|
990
|
+
key,
|
|
991
|
+
presignedUrl: `${STORAGE_TEST_URL}/presigned/${fileId}`,
|
|
992
|
+
contentType,
|
|
993
|
+
expiresIn: 3600
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
if (pathname === "/upload" && request.method === "POST") {
|
|
997
|
+
const isPublic = url.searchParams.get("isPublic") !== "false";
|
|
998
|
+
const formData = await request.formData();
|
|
999
|
+
const file = formData.get("file");
|
|
1000
|
+
if (!(file instanceof Blob)) {
|
|
1001
|
+
return storageErr("file is required");
|
|
1002
|
+
}
|
|
1003
|
+
const filename = formData.get("filename") ?? (file instanceof File ? file.name : "upload.bin");
|
|
1004
|
+
const metadataRaw = formData.get("metadata");
|
|
1005
|
+
let metadata;
|
|
1006
|
+
if (typeof metadataRaw === "string" && metadataRaw) {
|
|
1007
|
+
try {
|
|
1008
|
+
metadata = JSON.parse(metadataRaw);
|
|
1009
|
+
} catch {
|
|
1010
|
+
return storageErr("metadata must be valid JSON");
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
const record = createFileRecord({
|
|
1014
|
+
filename,
|
|
1015
|
+
contentType: file.type || "application/octet-stream",
|
|
1016
|
+
sizeBytes: file.size,
|
|
1017
|
+
isPublic,
|
|
1018
|
+
metadata
|
|
1019
|
+
});
|
|
1020
|
+
state.storageFiles.set(record.id, record);
|
|
1021
|
+
captureUpload(record, "POST", "/upload");
|
|
1022
|
+
return json(toUploadResponse(record));
|
|
1023
|
+
}
|
|
1024
|
+
const fileMatch = pathname.match(/^\/api\/files\/([^/]+)$/);
|
|
1025
|
+
if (fileMatch) {
|
|
1026
|
+
const fileId = fileMatch[1];
|
|
1027
|
+
const record = state.storageFiles.get(fileId);
|
|
1028
|
+
if (!record || record.deleted) {
|
|
1029
|
+
return storageErr("File not found", 404);
|
|
1030
|
+
}
|
|
1031
|
+
if (request.method === "GET") {
|
|
1032
|
+
return json(toUploadResponse(record));
|
|
1033
|
+
}
|
|
1034
|
+
if (request.method === "PATCH") {
|
|
1035
|
+
const body = await readJsonBody(request);
|
|
1036
|
+
if (body.fileName !== void 0) record.filename = String(body.fileName);
|
|
1037
|
+
if (body.metadata !== void 0) {
|
|
1038
|
+
record.metadata = body.metadata;
|
|
1039
|
+
}
|
|
1040
|
+
if (body.isPublic !== void 0) record.isPublic = body.isPublic === true;
|
|
1041
|
+
return json(toUploadResponse(record));
|
|
1042
|
+
}
|
|
1043
|
+
if (request.method === "DELETE") {
|
|
1044
|
+
record.deleted = true;
|
|
1045
|
+
return json({ success: true });
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (pathname === "/api/files" && request.method === "GET") {
|
|
1049
|
+
const parseParam = (raw, fallback, min) => {
|
|
1050
|
+
const n = Number(raw);
|
|
1051
|
+
return Number.isFinite(n) && n >= min ? Math.floor(n) : fallback;
|
|
1052
|
+
};
|
|
1053
|
+
const limit = parseParam(url.searchParams.get("limit"), 50, 1);
|
|
1054
|
+
const offset = parseParam(url.searchParams.get("offset"), 0, 0);
|
|
1055
|
+
const prefix = url.searchParams.get("prefix") ?? "";
|
|
1056
|
+
let files = activeFiles();
|
|
1057
|
+
if (prefix) {
|
|
1058
|
+
files = files.filter((f) => f.key.startsWith(prefix) || f.filename.startsWith(prefix));
|
|
1059
|
+
}
|
|
1060
|
+
const total = files.length;
|
|
1061
|
+
const slice = files.slice(offset, offset + limit);
|
|
1062
|
+
return json({
|
|
1063
|
+
files: slice.map((f) => toUploadResponse(f)),
|
|
1064
|
+
total,
|
|
1065
|
+
limit,
|
|
1066
|
+
offset,
|
|
1067
|
+
hasMore: offset + slice.length < total
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
return storageErr(`No storage simulator for ${request.method} ${pathname}`, 404);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// src/simulator/messaging.ts
|
|
1074
|
+
var import_node_crypto7 = __toESM(require("crypto"));
|
|
1075
|
+
function captureMessage(channel, recipient, body, connectionId) {
|
|
1076
|
+
const message = {
|
|
1077
|
+
channel,
|
|
1078
|
+
recipient,
|
|
1079
|
+
body: {
|
|
1080
|
+
text: body.text ? String(body.text) : void 0,
|
|
1081
|
+
blocks: body.blocks,
|
|
1082
|
+
threadTs: body.threadTs ? String(body.threadTs) : void 0,
|
|
1083
|
+
messagingType: body.messagingType ? String(body.messagingType) : void 0,
|
|
1084
|
+
tag: body.tag ? String(body.tag) : void 0
|
|
1085
|
+
},
|
|
1086
|
+
connectionId,
|
|
1087
|
+
sentAt: /* @__PURE__ */ new Date()
|
|
1088
|
+
};
|
|
1089
|
+
state.messages.push(message);
|
|
1090
|
+
}
|
|
1091
|
+
async function handleMessagingRequest(request, channel, subPath) {
|
|
1092
|
+
const method = request.method;
|
|
1093
|
+
if ((subPath === "" || subPath === "/" || subPath === "/connections") && method === "GET") {
|
|
1094
|
+
return success({ connections: [] });
|
|
1095
|
+
}
|
|
1096
|
+
if (channel === "slack" && subPath === "/send" && method === "POST") {
|
|
1097
|
+
const body = await readJsonBody(request);
|
|
1098
|
+
if (!body.text && !body.blocks) {
|
|
1099
|
+
return failure("text or blocks is required");
|
|
1100
|
+
}
|
|
1101
|
+
const slackChannel = String(body.channel ?? "");
|
|
1102
|
+
if (!slackChannel) {
|
|
1103
|
+
return failure("channel is required");
|
|
1104
|
+
}
|
|
1105
|
+
captureMessage(
|
|
1106
|
+
"slack",
|
|
1107
|
+
slackChannel,
|
|
1108
|
+
body,
|
|
1109
|
+
body.connectionId ? String(body.connectionId) : void 0
|
|
1110
|
+
);
|
|
1111
|
+
return success({
|
|
1112
|
+
ok: true,
|
|
1113
|
+
ts: `${Date.now()}.${import_node_crypto7.default.randomUUID().slice(0, 6)}`,
|
|
1114
|
+
channel: slackChannel
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
if (channel === "line" && subPath === "/push" && method === "POST") {
|
|
1118
|
+
const body = await readJsonBody(request);
|
|
1119
|
+
const userId = String(body.userId ?? "");
|
|
1120
|
+
const message = body.message;
|
|
1121
|
+
if (!userId) return failure("userId is required");
|
|
1122
|
+
if (!message || message.type !== "text" || !message.text) {
|
|
1123
|
+
return failure("message must be { type: 'text', text: string }");
|
|
1124
|
+
}
|
|
1125
|
+
if (message.text.length > 5e3) {
|
|
1126
|
+
return failure("message text exceeds maximum length");
|
|
1127
|
+
}
|
|
1128
|
+
captureMessage(
|
|
1129
|
+
"line",
|
|
1130
|
+
userId,
|
|
1131
|
+
{ text: message.text },
|
|
1132
|
+
body.connectionId ? String(body.connectionId) : void 0
|
|
1133
|
+
);
|
|
1134
|
+
return success({ success: true });
|
|
1135
|
+
}
|
|
1136
|
+
if (channel === "facebook" && subPath === "/send" && method === "POST") {
|
|
1137
|
+
const body = await readJsonBody(request);
|
|
1138
|
+
const recipientId = String(body.recipientId ?? "");
|
|
1139
|
+
const message = body.message;
|
|
1140
|
+
if (!recipientId) return failure("recipientId is required");
|
|
1141
|
+
if (!message || message.type !== "text" || !message.text) {
|
|
1142
|
+
return failure("message must be { type: 'text', text: string }");
|
|
1143
|
+
}
|
|
1144
|
+
if (message.text.length > 2e3) {
|
|
1145
|
+
return failure("message text exceeds maximum length");
|
|
1146
|
+
}
|
|
1147
|
+
captureMessage(
|
|
1148
|
+
"facebook",
|
|
1149
|
+
recipientId,
|
|
1150
|
+
{
|
|
1151
|
+
text: message.text,
|
|
1152
|
+
messagingType: body.messagingType ? String(body.messagingType) : void 0,
|
|
1153
|
+
tag: body.tag ? String(body.tag) : void 0
|
|
1154
|
+
},
|
|
1155
|
+
body.connectionId ? String(body.connectionId) : void 0
|
|
1156
|
+
);
|
|
1157
|
+
return success({ success: true });
|
|
1158
|
+
}
|
|
1159
|
+
return failure(`No messaging simulator for ${method} .../integrations/${channel}${subPath}`, 404);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
801
1162
|
// src/simulator/router.ts
|
|
802
1163
|
var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
803
1164
|
originalFetch: null
|
|
@@ -805,6 +1166,21 @@ var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
|
805
1166
|
function isLocalHost(hostname) {
|
|
806
1167
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0";
|
|
807
1168
|
}
|
|
1169
|
+
function requiresDeploymentHmac(request, url) {
|
|
1170
|
+
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
1171
|
+
const isPresignedPut = isStorageHost && request.method === "PUT" && /^\/presigned\/[^/]+$/.test(url.pathname);
|
|
1172
|
+
if (isStorageHost) {
|
|
1173
|
+
return !isPresignedPut;
|
|
1174
|
+
}
|
|
1175
|
+
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
1176
|
+
const isEmail = url.pathname === "/api/email/send";
|
|
1177
|
+
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
1178
|
+
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
1179
|
+
const messagingMatch = url.pathname.match(
|
|
1180
|
+
/^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
|
|
1181
|
+
);
|
|
1182
|
+
return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch);
|
|
1183
|
+
}
|
|
808
1184
|
async function handleSimulatedRequest(request, url) {
|
|
809
1185
|
if (url.pathname === "/sql") {
|
|
810
1186
|
return handleNeonSql(requireDb(), request);
|
|
@@ -816,7 +1192,12 @@ async function handleSimulatedRequest(request, url) {
|
|
|
816
1192
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
817
1193
|
const isEmail = url.pathname === "/api/email/send";
|
|
818
1194
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
819
|
-
|
|
1195
|
+
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
1196
|
+
const messagingMatch = url.pathname.match(
|
|
1197
|
+
/^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
|
|
1198
|
+
);
|
|
1199
|
+
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
1200
|
+
if (requiresDeploymentHmac(request, url)) {
|
|
820
1201
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
821
1202
|
if (!authHeader) {
|
|
822
1203
|
return failure("Missing authentication header", 401);
|
|
@@ -826,34 +1207,44 @@ async function handleSimulatedRequest(request, url) {
|
|
|
826
1207
|
return failure("Invalid authentication", 401);
|
|
827
1208
|
}
|
|
828
1209
|
}
|
|
1210
|
+
if (isStorageHost) {
|
|
1211
|
+
return handleStorageRequest(request, url);
|
|
1212
|
+
}
|
|
829
1213
|
if (isEmail && request.method === "POST") {
|
|
830
1214
|
return handleEmailSend(request);
|
|
831
1215
|
}
|
|
832
1216
|
if (identitiesMatch) {
|
|
833
1217
|
return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
|
|
834
1218
|
}
|
|
1219
|
+
if (storeMatch) {
|
|
1220
|
+
return handlePaymentsRequest(request, url);
|
|
1221
|
+
}
|
|
1222
|
+
if (messagingMatch) {
|
|
1223
|
+
const channel = messagingMatch[1];
|
|
1224
|
+
return handleMessagingRequest(request, channel, messagingMatch[2] ?? "");
|
|
1225
|
+
}
|
|
835
1226
|
if (dataStoreMatch) {
|
|
836
1227
|
const subPath = dataStoreMatch[1] ?? "";
|
|
837
1228
|
const db = requireDb();
|
|
838
|
-
const
|
|
1229
|
+
const readBody = async () => await request.json();
|
|
839
1230
|
if (subPath === "/query" && request.method === "POST") {
|
|
840
|
-
return handleQuery(db, await
|
|
1231
|
+
return handleQuery(db, await readBody());
|
|
841
1232
|
}
|
|
842
1233
|
if (subPath === "/mutate" && request.method === "POST") {
|
|
843
|
-
return handleMutate(db, await
|
|
1234
|
+
return handleMutate(db, await readBody());
|
|
844
1235
|
}
|
|
845
1236
|
if (subPath === "/schema" && request.method === "GET") {
|
|
846
1237
|
return handleGetSchema(db);
|
|
847
1238
|
}
|
|
848
1239
|
if (subPath === "/schema/tables" && request.method === "POST") {
|
|
849
|
-
return handleCreateTable(db, await
|
|
1240
|
+
return handleCreateTable(db, await readBody());
|
|
850
1241
|
}
|
|
851
1242
|
if (subPath === "/schema/columns" && request.method === "POST") {
|
|
852
|
-
return handleAddColumn(db, await
|
|
1243
|
+
return handleAddColumn(db, await readBody());
|
|
853
1244
|
}
|
|
854
1245
|
}
|
|
855
1246
|
return failure(
|
|
856
|
-
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, auth verify/refresh, Neon /sql.`,
|
|
1247
|
+
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, auth verify/refresh, Neon /sql.`,
|
|
857
1248
|
404
|
|
858
1249
|
);
|
|
859
1250
|
}
|