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