@stardeck-customer-apps/testing 0.25.1 → 0.27.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 +39 -1
- package/dist/index.d.mts +98 -3
- package/dist/index.d.ts +98 -3
- package/dist/index.js +453 -17
- package/dist/index.mjs +453 -17
- package/dist/next/headers-shim.js +6 -0
- package/dist/next/headers-shim.mjs +6 -0
- package/dist/setup.js +337 -16
- package/dist/setup.mjs +337 -16
- package/package.json +6 -3
package/dist/setup.mjs
CHANGED
|
@@ -41,6 +41,7 @@ var state = globalSingleton("state", () => ({
|
|
|
41
41
|
identitySuppressClaimDelivery: false,
|
|
42
42
|
identityAllowedReturnOrigins: /* @__PURE__ */ new Set([APP_TEST_ORIGIN]),
|
|
43
43
|
checkouts: [],
|
|
44
|
+
scheduleWakes: [],
|
|
44
45
|
checkoutCounter: 0,
|
|
45
46
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
46
47
|
idempotentCheckouts: /* @__PURE__ */ new Map(),
|
|
@@ -59,6 +60,11 @@ var state = globalSingleton("state", () => ({
|
|
|
59
60
|
cardTokens: /* @__PURE__ */ new Map(),
|
|
60
61
|
cardTokenCounter: 0,
|
|
61
62
|
cardTokenizationEnabled: true,
|
|
63
|
+
stripePaymentIntents: /* @__PURE__ */ new Map(),
|
|
64
|
+
stripePaymentIntentCounter: 0,
|
|
65
|
+
stripeIdempotentIntents: /* @__PURE__ */ new Map(),
|
|
66
|
+
stripeAllowedMethods: ["card", "promptpay"],
|
|
67
|
+
stripeChargesEnabled: true,
|
|
62
68
|
uploads: [],
|
|
63
69
|
uploadCounter: 0,
|
|
64
70
|
storageFiles: /* @__PURE__ */ new Map(),
|
|
@@ -545,8 +551,8 @@ async function handleNeonSql(db, request) {
|
|
|
545
551
|
rowAsArray: arrayMode
|
|
546
552
|
});
|
|
547
553
|
} catch (error) {
|
|
548
|
-
const
|
|
549
|
-
return json({ message:
|
|
554
|
+
const err2 = error;
|
|
555
|
+
return json({ message: err2.message, code: err2.code }, 400);
|
|
550
556
|
}
|
|
551
557
|
}
|
|
552
558
|
|
|
@@ -1511,6 +1517,281 @@ var requestScopeStorage = globalSingleton(
|
|
|
1511
1517
|
() => new AsyncLocalStorage()
|
|
1512
1518
|
);
|
|
1513
1519
|
|
|
1520
|
+
// src/simulator/stripe-payment-intents.ts
|
|
1521
|
+
var SIM_STRIPE_PUBLISHABLE_KEY = "pk_test_sim_stripe";
|
|
1522
|
+
var SIM_STRIPE_DEFAULT_ACCOUNT = "acct_sim_default";
|
|
1523
|
+
var METHODS = ["card", "promptpay"];
|
|
1524
|
+
var RESERVED_METADATA_KEYS = ["projectId", "stardeckReferenceId", "stardeckStagingDeploymentId"];
|
|
1525
|
+
var PROMPTPAY_QR_TTL_SECONDS = 15 * 60;
|
|
1526
|
+
var PATH_RE = /^\/api\/store\/(?!beam\/)([^/]+)\/(payment-intents(?:\/([^/]+)(?:\/(cancel))?)?|elements-config|readiness)$/;
|
|
1527
|
+
function err(error, status, code) {
|
|
1528
|
+
return json({ error, code }, status);
|
|
1529
|
+
}
|
|
1530
|
+
function decode(segment) {
|
|
1531
|
+
try {
|
|
1532
|
+
return decodeURIComponent(segment);
|
|
1533
|
+
} catch {
|
|
1534
|
+
return segment;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
function isPlayground(account) {
|
|
1538
|
+
return account === "playground";
|
|
1539
|
+
}
|
|
1540
|
+
function resolveAccount(account) {
|
|
1541
|
+
if (isPlayground(account)) return null;
|
|
1542
|
+
if (account === "default") {
|
|
1543
|
+
return process.env.STRIPE_CONNECT_ACCOUNT_ID || SIM_STRIPE_DEFAULT_ACCOUNT;
|
|
1544
|
+
}
|
|
1545
|
+
return account;
|
|
1546
|
+
}
|
|
1547
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
1548
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1549
|
+
var APP_METADATA_MAX_KEYS = 50 - RESERVED_METADATA_KEYS.length;
|
|
1550
|
+
function isStringRecord(value) {
|
|
1551
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
|
|
1552
|
+
}
|
|
1553
|
+
function isValidMetadata(value) {
|
|
1554
|
+
if (!isStringRecord(value)) return false;
|
|
1555
|
+
const entries = Object.entries(value);
|
|
1556
|
+
return entries.length <= APP_METADATA_MAX_KEYS && entries.every(([k, v]) => k.length >= 1 && k.length <= 40 && v.length <= 500);
|
|
1557
|
+
}
|
|
1558
|
+
function isOptional(value, test) {
|
|
1559
|
+
return value === void 0 || test(value);
|
|
1560
|
+
}
|
|
1561
|
+
var isEmail = (v) => typeof v === "string" && EMAIL_RE.test(v);
|
|
1562
|
+
function validateCreate(body) {
|
|
1563
|
+
const { amount, currency, method, referenceId, idempotencyKey } = body;
|
|
1564
|
+
const checks = [
|
|
1565
|
+
[
|
|
1566
|
+
typeof amount === "number" && Number.isInteger(amount) && amount > 0,
|
|
1567
|
+
"amount must be a positive integer in minor units"
|
|
1568
|
+
],
|
|
1569
|
+
[typeof currency === "string" && currency.length === 3, "currency must be a 3-letter ISO code"],
|
|
1570
|
+
[
|
|
1571
|
+
typeof method === "string" && METHODS.includes(method),
|
|
1572
|
+
`method must be one of: ${METHODS.join(", ")}`
|
|
1573
|
+
],
|
|
1574
|
+
[
|
|
1575
|
+
typeof referenceId === "string" && referenceId.length >= 1 && referenceId.length <= 200,
|
|
1576
|
+
"referenceId must be 1 to 200 characters"
|
|
1577
|
+
],
|
|
1578
|
+
[
|
|
1579
|
+
typeof idempotencyKey === "string" && idempotencyKey.length >= 1 && idempotencyKey.length <= 200,
|
|
1580
|
+
"idempotencyKey is required (1 to 200 characters)"
|
|
1581
|
+
],
|
|
1582
|
+
[
|
|
1583
|
+
isOptional(body.description, (v) => typeof v === "string" && v.length <= 1e3),
|
|
1584
|
+
"description must be at most 1000 characters"
|
|
1585
|
+
],
|
|
1586
|
+
[isOptional(body.receiptEmail, isEmail), "receiptEmail must be an email address"],
|
|
1587
|
+
[isOptional(body.billingEmail, isEmail), "billingEmail must be an email address"],
|
|
1588
|
+
[
|
|
1589
|
+
isOptional(body.returnUrl, (v) => typeof v === "string" && URL.canParse(v)),
|
|
1590
|
+
"returnUrl must be an absolute URL"
|
|
1591
|
+
],
|
|
1592
|
+
[
|
|
1593
|
+
isOptional(body.metadata, isValidMetadata),
|
|
1594
|
+
`metadata must map keys (1-40 chars) to strings (max 500), at most ${APP_METADATA_MAX_KEYS} keys`
|
|
1595
|
+
],
|
|
1596
|
+
[
|
|
1597
|
+
typeof body.deploymentId === "string" && UUID_RE2.test(body.deploymentId),
|
|
1598
|
+
"Invalid deployment ID"
|
|
1599
|
+
]
|
|
1600
|
+
];
|
|
1601
|
+
const failed = checks.find(([ok]) => !ok);
|
|
1602
|
+
return failed ? failed[1] : null;
|
|
1603
|
+
}
|
|
1604
|
+
function appMetadata(raw) {
|
|
1605
|
+
if (!isStringRecord(raw)) return {};
|
|
1606
|
+
const out = {};
|
|
1607
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
1608
|
+
if (!RESERVED_METADATA_KEYS.includes(key)) out[key] = value;
|
|
1609
|
+
}
|
|
1610
|
+
return out;
|
|
1611
|
+
}
|
|
1612
|
+
function stableStringify(value) {
|
|
1613
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1614
|
+
if (value && typeof value === "object") {
|
|
1615
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1616
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
1617
|
+
}
|
|
1618
|
+
return JSON.stringify(value);
|
|
1619
|
+
}
|
|
1620
|
+
function toStripePaymentIntentSummary(intent) {
|
|
1621
|
+
const open = intent.status !== "succeeded" && intent.status !== "canceled";
|
|
1622
|
+
let actionRequired = "NONE";
|
|
1623
|
+
if (open && intent.method === "card") actionRequired = "CLIENT_CONFIRM";
|
|
1624
|
+
if (open && intent.method === "promptpay" && intent.status === "requires_action") {
|
|
1625
|
+
actionRequired = "ENCODED_IMAGE";
|
|
1626
|
+
}
|
|
1627
|
+
return {
|
|
1628
|
+
id: intent.id,
|
|
1629
|
+
status: intent.status,
|
|
1630
|
+
amount: intent.amount,
|
|
1631
|
+
currency: intent.currency,
|
|
1632
|
+
method: intent.method,
|
|
1633
|
+
referenceId: intent.referenceId,
|
|
1634
|
+
metadata: { ...intent.appMetadata },
|
|
1635
|
+
actionRequired,
|
|
1636
|
+
clientSecret: intent.method === "card" ? intent.clientSecret : null,
|
|
1637
|
+
qr: actionRequired === "ENCODED_IMAGE" ? { ...intent.qr } : null,
|
|
1638
|
+
latestChargeId: intent.latestChargeId,
|
|
1639
|
+
amountRefunded: intent.amountRefunded,
|
|
1640
|
+
lastPaymentError: intent.lastPaymentError ? { ...intent.lastPaymentError } : null,
|
|
1641
|
+
created: intent.created,
|
|
1642
|
+
livemode: false,
|
|
1643
|
+
playground: intent.playground
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
function promptPayQr(id, created) {
|
|
1647
|
+
return {
|
|
1648
|
+
imageUrlPng: `https://qr.stripe.test/${id}.png`,
|
|
1649
|
+
imageUrlSvg: `https://qr.stripe.test/${id}.svg`,
|
|
1650
|
+
data: `00020101021230SIM${id}`,
|
|
1651
|
+
hostedInstructionsUrl: `https://payments.stripe.test/promptpay/${id}`,
|
|
1652
|
+
expiresAt: created + PROMPTPAY_QR_TTL_SECONDS
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
function createIntent(body, account, projectId) {
|
|
1656
|
+
const problem = validateCreate(body);
|
|
1657
|
+
if (problem) return err(problem, 400, "invalid_request");
|
|
1658
|
+
const method = body.method;
|
|
1659
|
+
const currency = body.currency.toLowerCase();
|
|
1660
|
+
if (!state.stripeAllowedMethods.includes(method)) {
|
|
1661
|
+
return err(
|
|
1662
|
+
`The ${method} payment method is turned off for this account`,
|
|
1663
|
+
422,
|
|
1664
|
+
"method_not_allowed"
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
if (method === "promptpay" && currency !== "thb") {
|
|
1668
|
+
return err("PromptPay only takes THB", 422, "currency_not_supported");
|
|
1669
|
+
}
|
|
1670
|
+
if (!state.stripeChargesEnabled) {
|
|
1671
|
+
return err("This Stripe account can't take payments yet", 503, "account_not_ready");
|
|
1672
|
+
}
|
|
1673
|
+
const scopedKey = JSON.stringify(["pi", projectId, account, body.idempotencyKey]);
|
|
1674
|
+
const comparable = { ...body };
|
|
1675
|
+
delete comparable.deploymentId;
|
|
1676
|
+
delete comparable.idempotencyKey;
|
|
1677
|
+
const fingerprint = stableStringify(comparable);
|
|
1678
|
+
const saved = state.stripeIdempotentIntents.get(scopedKey);
|
|
1679
|
+
if (saved) {
|
|
1680
|
+
if (saved.fingerprint !== fingerprint) {
|
|
1681
|
+
return err(
|
|
1682
|
+
"This idempotency key was already used with different parameters. Use a new key for a different payment.",
|
|
1683
|
+
409,
|
|
1684
|
+
"idempotency_conflict"
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
const existing = state.stripePaymentIntents.get(saved.id);
|
|
1688
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(existing) }, 201);
|
|
1689
|
+
}
|
|
1690
|
+
state.stripePaymentIntentCounter += 1;
|
|
1691
|
+
const id = `pi_sim_${state.stripePaymentIntentCounter}`;
|
|
1692
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
1693
|
+
const referenceId = body.referenceId;
|
|
1694
|
+
const metadata = appMetadata(body.metadata);
|
|
1695
|
+
const intent = {
|
|
1696
|
+
id,
|
|
1697
|
+
projectId,
|
|
1698
|
+
account,
|
|
1699
|
+
connectedAccountId: resolveAccount(account),
|
|
1700
|
+
playground: isPlayground(account),
|
|
1701
|
+
status: method === "card" ? "requires_payment_method" : "requires_action",
|
|
1702
|
+
amount: body.amount,
|
|
1703
|
+
currency,
|
|
1704
|
+
method,
|
|
1705
|
+
referenceId,
|
|
1706
|
+
appMetadata: metadata,
|
|
1707
|
+
metadata: { ...metadata, projectId, stardeckReferenceId: referenceId },
|
|
1708
|
+
description: typeof body.description === "string" ? body.description : null,
|
|
1709
|
+
receiptEmail: typeof body.receiptEmail === "string" ? body.receiptEmail : null,
|
|
1710
|
+
returnUrl: typeof body.returnUrl === "string" ? body.returnUrl : null,
|
|
1711
|
+
clientSecret: `${id}_secret_sim`,
|
|
1712
|
+
qr: method === "promptpay" ? promptPayQr(id, created) : null,
|
|
1713
|
+
latestChargeId: null,
|
|
1714
|
+
amountRefunded: 0,
|
|
1715
|
+
lastPaymentError: null,
|
|
1716
|
+
created
|
|
1717
|
+
};
|
|
1718
|
+
state.stripePaymentIntents.set(id, intent);
|
|
1719
|
+
state.stripeIdempotentIntents.set(scopedKey, { fingerprint, id });
|
|
1720
|
+
console.log(
|
|
1721
|
+
`[stardeck-testing] stripe payment intent ${id} created method=${method} amount=${intent.amount} ${currency}`
|
|
1722
|
+
);
|
|
1723
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(intent) }, 201);
|
|
1724
|
+
}
|
|
1725
|
+
function ownedIntent(id, projectId) {
|
|
1726
|
+
const intent = state.stripePaymentIntents.get(id);
|
|
1727
|
+
return intent && intent.projectId === projectId ? intent : void 0;
|
|
1728
|
+
}
|
|
1729
|
+
function cancelIntent(intent) {
|
|
1730
|
+
if (intent.status === "succeeded") {
|
|
1731
|
+
return err("This payment already succeeded and can't be canceled", 409, "already_succeeded");
|
|
1732
|
+
}
|
|
1733
|
+
intent.status = "canceled";
|
|
1734
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(intent) });
|
|
1735
|
+
}
|
|
1736
|
+
function handleIntents(request, url, account, intentId, action, projectId) {
|
|
1737
|
+
if (!intentId && request.method === "POST") {
|
|
1738
|
+
return readJsonBody(request).then((body) => createIntent(body, account, projectId));
|
|
1739
|
+
}
|
|
1740
|
+
if (!intentId && request.method === "GET") {
|
|
1741
|
+
const referenceId = url.searchParams.get("referenceId");
|
|
1742
|
+
if (!referenceId) return err("referenceId is required", 400, "invalid_request");
|
|
1743
|
+
const paymentIntents = [...state.stripePaymentIntents.values()].filter((i) => i.projectId === projectId && i.referenceId === referenceId).map(toStripePaymentIntentSummary);
|
|
1744
|
+
return json({ paymentIntents });
|
|
1745
|
+
}
|
|
1746
|
+
if (!intentId) return null;
|
|
1747
|
+
const intent = ownedIntent(decode(intentId), projectId);
|
|
1748
|
+
if (!action && request.method === "GET") {
|
|
1749
|
+
if (!intent) return err("Payment intent not found", 404, "not_found");
|
|
1750
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(intent) });
|
|
1751
|
+
}
|
|
1752
|
+
if (action === "cancel" && request.method === "POST") {
|
|
1753
|
+
return readJsonBody(request).then((body) => {
|
|
1754
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE2.test(body.deploymentId)) {
|
|
1755
|
+
return err("deploymentId is required", 400, "invalid_request");
|
|
1756
|
+
}
|
|
1757
|
+
if (!intent) return err("Payment intent not found", 404, "not_found");
|
|
1758
|
+
return cancelIntent(intent);
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
async function handleStripePaymentIntentRequest(request, url, projectId) {
|
|
1764
|
+
const match = url.pathname.match(PATH_RE);
|
|
1765
|
+
if (!match) return null;
|
|
1766
|
+
if (!projectId) return err("Missing authentication", 401, "unauthorized");
|
|
1767
|
+
const account = decode(match[1]);
|
|
1768
|
+
const resource = match[2];
|
|
1769
|
+
if (request.method === "GET" && !url.searchParams.get("deploymentId")) {
|
|
1770
|
+
return err("deploymentId is required", 400, "invalid_request");
|
|
1771
|
+
}
|
|
1772
|
+
if (resource === "elements-config" && request.method === "GET") {
|
|
1773
|
+
return json({
|
|
1774
|
+
publishableKey: SIM_STRIPE_PUBLISHABLE_KEY,
|
|
1775
|
+
stripeAccount: resolveAccount(account),
|
|
1776
|
+
playground: isPlayground(account)
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
if (resource === "readiness" && request.method === "GET") {
|
|
1780
|
+
const methods = [...state.stripeAllowedMethods];
|
|
1781
|
+
return json({
|
|
1782
|
+
provider: "stripe",
|
|
1783
|
+
ready: state.stripeChargesEnabled && methods.length > 0,
|
|
1784
|
+
chargesEnabled: state.stripeChargesEnabled,
|
|
1785
|
+
methods,
|
|
1786
|
+
playground: isPlayground(account)
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
if (resource.startsWith("payment-intents")) {
|
|
1790
|
+
return handleIntents(request, url, account, match[3], match[4], projectId);
|
|
1791
|
+
}
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1514
1795
|
// src/simulator/webhook-signing.ts
|
|
1515
1796
|
import crypto4 from "crypto";
|
|
1516
1797
|
|
|
@@ -1530,7 +1811,7 @@ var BEAM_BOLT_PAYMENT_METHODS = [
|
|
|
1530
1811
|
"SPAY_LATER"
|
|
1531
1812
|
];
|
|
1532
1813
|
var BEAM_BOLT_INTENT_STATUSES = ["PENDING", "PAID", "FAILED", "CANCELED", "EXPIRED"];
|
|
1533
|
-
var
|
|
1814
|
+
var UUID_RE3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
1534
1815
|
function payErr(error, status = 400, code, details) {
|
|
1535
1816
|
const body = { error };
|
|
1536
1817
|
if (code) body.code = code;
|
|
@@ -1556,7 +1837,23 @@ function paymentOperationForRequest(request, pathname) {
|
|
|
1556
1837
|
if (request.method === "GET" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout-sessions\/[^/]+$/.test(pathname)) {
|
|
1557
1838
|
return "stripe.getCheckoutSessionStatus";
|
|
1558
1839
|
}
|
|
1559
|
-
return
|
|
1840
|
+
return stripeInlineOperation(request.method, pathname);
|
|
1841
|
+
}
|
|
1842
|
+
var STRIPE_INLINE_OPERATIONS = [
|
|
1843
|
+
["POST", /^\/api\/store\/(?!beam\/)[^/]+\/payment-intents$/, "stripe.createPaymentIntent"],
|
|
1844
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/payment-intents$/, "stripe.findPaymentIntents"],
|
|
1845
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/payment-intents\/[^/]+$/, "stripe.getPaymentIntent"],
|
|
1846
|
+
[
|
|
1847
|
+
"POST",
|
|
1848
|
+
/^\/api\/store\/(?!beam\/)[^/]+\/payment-intents\/[^/]+\/cancel$/,
|
|
1849
|
+
"stripe.cancelPaymentIntent"
|
|
1850
|
+
],
|
|
1851
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/elements-config$/, "stripe.getElementsConfig"],
|
|
1852
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/readiness$/, "stripe.getReadiness"]
|
|
1853
|
+
];
|
|
1854
|
+
function stripeInlineOperation(method, pathname) {
|
|
1855
|
+
const hit = STRIPE_INLINE_OPERATIONS.find(([m, re]) => m === method && re.test(pathname));
|
|
1856
|
+
return hit ? hit[2] : null;
|
|
1560
1857
|
}
|
|
1561
1858
|
function consumePaymentFailure(request, pathname) {
|
|
1562
1859
|
const operation = paymentOperationForRequest(request, pathname);
|
|
@@ -1586,11 +1883,11 @@ function isBoltPaymentMethod(value) {
|
|
|
1586
1883
|
function isBoltIntentStatus(value) {
|
|
1587
1884
|
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
1588
1885
|
}
|
|
1589
|
-
function
|
|
1590
|
-
if (Array.isArray(value)) return `[${value.map(
|
|
1886
|
+
function stableStringify2(value) {
|
|
1887
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify2).join(",")}]`;
|
|
1591
1888
|
if (value && typeof value === "object") {
|
|
1592
1889
|
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1593
|
-
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${
|
|
1890
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify2(v)}`).join(",")}}`;
|
|
1594
1891
|
}
|
|
1595
1892
|
return JSON.stringify(value);
|
|
1596
1893
|
}
|
|
@@ -1696,10 +1993,16 @@ function storedStatusesForFilter(statuses) {
|
|
|
1696
1993
|
}
|
|
1697
1994
|
return [...stored];
|
|
1698
1995
|
}
|
|
1699
|
-
async function handlePaymentsRequest(request, url) {
|
|
1996
|
+
async function handlePaymentsRequest(request, url, signedProjectId = null) {
|
|
1700
1997
|
const pathname = url.pathname;
|
|
1701
1998
|
const injectedFailure = consumePaymentFailure(request, pathname);
|
|
1702
1999
|
if (injectedFailure) return injectedFailure;
|
|
2000
|
+
const inlineStripeResponse = await handleStripePaymentIntentRequest(
|
|
2001
|
+
request,
|
|
2002
|
+
url,
|
|
2003
|
+
signedProjectId
|
|
2004
|
+
);
|
|
2005
|
+
if (inlineStripeResponse) return inlineStripeResponse;
|
|
1703
2006
|
const directChargeResponse = await handleDirectChargeRequest(request, pathname);
|
|
1704
2007
|
if (directChargeResponse) return directChargeResponse;
|
|
1705
2008
|
if (/\/billing-portal$/.test(pathname)) {
|
|
@@ -1798,7 +2101,7 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1798
2101
|
code: "too_small"
|
|
1799
2102
|
});
|
|
1800
2103
|
}
|
|
1801
|
-
if (typeof body.deploymentId !== "string" || !
|
|
2104
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE3.test(body.deploymentId)) {
|
|
1802
2105
|
issues.push({
|
|
1803
2106
|
path: ["deploymentId"],
|
|
1804
2107
|
message: "Invalid uuid",
|
|
@@ -1982,7 +2285,7 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1982
2285
|
const comparable = { ...body };
|
|
1983
2286
|
delete comparable.deploymentId;
|
|
1984
2287
|
delete comparable.idempotencyKey;
|
|
1985
|
-
const fingerprint =
|
|
2288
|
+
const fingerprint = stableStringify2(comparable);
|
|
1986
2289
|
if (scopedKey) {
|
|
1987
2290
|
const saved = state.idempotentCheckouts.get(scopedKey);
|
|
1988
2291
|
if (saved) {
|
|
@@ -2683,6 +2986,16 @@ async function handleEdgeRequest(request, subPath) {
|
|
|
2683
2986
|
return edgeFailure(`No edge simulator for ${method} .../edge${subPath}`, 404);
|
|
2684
2987
|
}
|
|
2685
2988
|
|
|
2989
|
+
// src/simulator/schedules.ts
|
|
2990
|
+
async function handleScheduleWake(request, scheduleKey) {
|
|
2991
|
+
if (request.method !== "POST") return failure(`Method ${request.method} not allowed`, 405);
|
|
2992
|
+
const body = await readJsonBody(request);
|
|
2993
|
+
const at = typeof body.at === "string" ? new Date(body.at) : null;
|
|
2994
|
+
if (!at || Number.isNaN(at.getTime())) return failure("`at` must be an ISO date-time", 400);
|
|
2995
|
+
state.scheduleWakes.push({ scheduleKey, at, requestedAt: /* @__PURE__ */ new Date() });
|
|
2996
|
+
return success({ woken: true });
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2686
2999
|
// src/simulator/starlens.ts
|
|
2687
3000
|
var LATEST_VERSION = "v2";
|
|
2688
3001
|
function starlensFailure(error, status, code) {
|
|
@@ -2812,7 +3125,7 @@ function requiresDeploymentHmac(request, url) {
|
|
|
2812
3125
|
return !isPresignedPut;
|
|
2813
3126
|
}
|
|
2814
3127
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
2815
|
-
const
|
|
3128
|
+
const isEmail2 = url.pathname === "/api/email/send";
|
|
2816
3129
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
2817
3130
|
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
2818
3131
|
const messagingMatch = url.pathname.match(
|
|
@@ -2820,8 +3133,10 @@ function requiresDeploymentHmac(request, url) {
|
|
|
2820
3133
|
);
|
|
2821
3134
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2822
3135
|
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
2823
|
-
|
|
3136
|
+
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
3137
|
+
return !!(isStarlens || scheduleWakeMatch || dataStoreMatch || isEmail2 || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
2824
3138
|
}
|
|
3139
|
+
var SCHEDULE_WAKE_PATH = /^\/api\/deployments\/[^/]+\/schedules\/([^/]+)\/wake$/;
|
|
2825
3140
|
function deploymentIdFromPath(url) {
|
|
2826
3141
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
2827
3142
|
if (!match) return null;
|
|
@@ -2842,7 +3157,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2842
3157
|
return authMatch[1].endsWith("verify") ? handleAuthVerify(request) : handleAuthRefresh(request);
|
|
2843
3158
|
}
|
|
2844
3159
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
2845
|
-
const
|
|
3160
|
+
const isEmail2 = url.pathname === "/api/email/send";
|
|
2846
3161
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
2847
3162
|
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
2848
3163
|
const messagingMatch = url.pathname.match(
|
|
@@ -2851,6 +3166,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2851
3166
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2852
3167
|
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
2853
3168
|
let signedDeploymentId = null;
|
|
3169
|
+
let signedProjectId = null;
|
|
2854
3170
|
if (requiresDeploymentHmac(request, url)) {
|
|
2855
3171
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
2856
3172
|
if (!authHeader) {
|
|
@@ -2863,6 +3179,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2863
3179
|
return failure("Invalid authentication", 401);
|
|
2864
3180
|
}
|
|
2865
3181
|
signedDeploymentId = authPayload.deploymentId;
|
|
3182
|
+
signedProjectId = authPayload.projectId;
|
|
2866
3183
|
}
|
|
2867
3184
|
if (isStorageHost) {
|
|
2868
3185
|
return handleStorageRequest(request, url);
|
|
@@ -2870,14 +3187,14 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2870
3187
|
if (url.hostname === SIM_BEAM_HOST) {
|
|
2871
3188
|
return handleCardTokenRequest(request, url);
|
|
2872
3189
|
}
|
|
2873
|
-
if (
|
|
3190
|
+
if (isEmail2 && request.method === "POST") {
|
|
2874
3191
|
return handleEmailSend(request);
|
|
2875
3192
|
}
|
|
2876
3193
|
if (identitiesMatch) {
|
|
2877
3194
|
return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
|
|
2878
3195
|
}
|
|
2879
3196
|
if (storeMatch) {
|
|
2880
|
-
return handlePaymentsRequest(request, url);
|
|
3197
|
+
return handlePaymentsRequest(request, url, signedProjectId);
|
|
2881
3198
|
}
|
|
2882
3199
|
if (messagingMatch) {
|
|
2883
3200
|
const channel = messagingMatch[1];
|
|
@@ -2886,6 +3203,10 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2886
3203
|
if (edgeMatch) {
|
|
2887
3204
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
2888
3205
|
}
|
|
3206
|
+
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
3207
|
+
if (scheduleWakeMatch) {
|
|
3208
|
+
return handleScheduleWake(request, decodeURIComponent(scheduleWakeMatch[1]));
|
|
3209
|
+
}
|
|
2889
3210
|
if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
|
|
2890
3211
|
return handleStarlensExtract(request, signedDeploymentId);
|
|
2891
3212
|
}
|
|
@@ -2910,7 +3231,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2910
3231
|
}
|
|
2911
3232
|
}
|
|
2912
3233
|
return failure(
|
|
2913
|
-
`[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, starlens extract, auth verify/refresh, Neon /sql.`,
|
|
3234
|
+
`[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, starlens extract, schedule wakes, auth verify/refresh, Neon /sql.`,
|
|
2914
3235
|
404
|
|
2915
3236
|
);
|
|
2916
3237
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardeck-customer-apps/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Vitest test harness for Stardeck customer apps — in-process Postgres (PGlite) plus a control-plane simulator so the real Stardeck SDKs run unmodified in tests",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"build": "tsup src/index.ts src/config.ts src/setup.ts src/next/headers-shim.ts src/server-only-shim.ts --format cjs,esm --dts --no-splitting",
|
|
49
49
|
"dev": "tsup src/index.ts src/config.ts src/setup.ts src/next/headers-shim.ts src/server-only-shim.ts --format cjs,esm --dts --no-splitting --watch",
|
|
50
50
|
"format": "prettier --write . && oxlint . --fix",
|
|
51
|
-
"typecheck": "
|
|
51
|
+
"typecheck": "node ../../scripts/typecheck.mjs --noEmit && node ../../scripts/typecheck.mjs -p tsconfig.test.json",
|
|
52
52
|
"lint": "oxlint src/",
|
|
53
53
|
"test": "vitest run",
|
|
54
54
|
"test:watch": "vitest"
|
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
"@stardeck-customer-apps/email-sdk": "*",
|
|
101
101
|
"@stardeck-customer-apps/integrations-sdk": "*",
|
|
102
102
|
"@stardeck-customer-apps/payments-sdk": "*",
|
|
103
|
+
"@stardeck-customer-apps/scheduling-sdk": "*",
|
|
103
104
|
"@stardeck-customer-apps/storage-sdk": "*",
|
|
104
105
|
"@stardeck-customer-apps/tsconfig": "*",
|
|
105
106
|
"@types/node": "^24.10.1",
|
|
@@ -110,6 +111,8 @@
|
|
|
110
111
|
"typescript": "^5.0.0",
|
|
111
112
|
"vitest": "^3.2.4",
|
|
112
113
|
"@stardeck-customer-apps/starlens": "*",
|
|
113
|
-
"zod": "^4.3.5"
|
|
114
|
+
"zod": "^4.3.5",
|
|
115
|
+
"react": "19.2.8",
|
|
116
|
+
"react-dom": "19.2.8"
|
|
114
117
|
}
|
|
115
118
|
}
|