@stardeck-customer-apps/testing 0.26.0 → 0.28.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 +51 -1
- package/dist/index.d.mts +117 -2
- package/dist/index.d.ts +117 -2
- package/dist/index.js +569 -17
- package/dist/index.mjs +569 -17
- package/dist/next/headers-shim.js +13 -0
- package/dist/next/headers-shim.mjs +13 -0
- package/dist/setup.js +418 -16
- package/dist/setup.mjs +418 -16
- package/package.json +10 -3
|
@@ -79,6 +79,11 @@ var state = globalSingleton("state", () => ({
|
|
|
79
79
|
cardTokens: /* @__PURE__ */ new Map(),
|
|
80
80
|
cardTokenCounter: 0,
|
|
81
81
|
cardTokenizationEnabled: true,
|
|
82
|
+
stripePaymentIntents: /* @__PURE__ */ new Map(),
|
|
83
|
+
stripePaymentIntentCounter: 0,
|
|
84
|
+
stripeIdempotentIntents: /* @__PURE__ */ new Map(),
|
|
85
|
+
stripeAllowedMethods: ["card", "promptpay"],
|
|
86
|
+
stripeChargesEnabled: true,
|
|
82
87
|
uploads: [],
|
|
83
88
|
uploadCounter: 0,
|
|
84
89
|
storageFiles: /* @__PURE__ */ new Map(),
|
|
@@ -98,6 +103,14 @@ var state = globalSingleton("state", () => ({
|
|
|
98
103
|
starlensCalls: [],
|
|
99
104
|
starlensReplies: [],
|
|
100
105
|
starlensDefaultReply: null,
|
|
106
|
+
controls: {
|
|
107
|
+
seed: {},
|
|
108
|
+
values: {},
|
|
109
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
110
|
+
lastUsage: null,
|
|
111
|
+
writes: [],
|
|
112
|
+
refusal: null
|
|
113
|
+
},
|
|
101
114
|
allowNetwork: false
|
|
102
115
|
}));
|
|
103
116
|
|
|
@@ -52,6 +52,11 @@ var state = globalSingleton("state", () => ({
|
|
|
52
52
|
cardTokens: /* @__PURE__ */ new Map(),
|
|
53
53
|
cardTokenCounter: 0,
|
|
54
54
|
cardTokenizationEnabled: true,
|
|
55
|
+
stripePaymentIntents: /* @__PURE__ */ new Map(),
|
|
56
|
+
stripePaymentIntentCounter: 0,
|
|
57
|
+
stripeIdempotentIntents: /* @__PURE__ */ new Map(),
|
|
58
|
+
stripeAllowedMethods: ["card", "promptpay"],
|
|
59
|
+
stripeChargesEnabled: true,
|
|
55
60
|
uploads: [],
|
|
56
61
|
uploadCounter: 0,
|
|
57
62
|
storageFiles: /* @__PURE__ */ new Map(),
|
|
@@ -71,6 +76,14 @@ var state = globalSingleton("state", () => ({
|
|
|
71
76
|
starlensCalls: [],
|
|
72
77
|
starlensReplies: [],
|
|
73
78
|
starlensDefaultReply: null,
|
|
79
|
+
controls: {
|
|
80
|
+
seed: {},
|
|
81
|
+
values: {},
|
|
82
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
83
|
+
lastUsage: null,
|
|
84
|
+
writes: [],
|
|
85
|
+
refusal: null
|
|
86
|
+
},
|
|
74
87
|
allowNetwork: false
|
|
75
88
|
}));
|
|
76
89
|
|
package/dist/setup.js
CHANGED
|
@@ -84,6 +84,11 @@ var state = globalSingleton("state", () => ({
|
|
|
84
84
|
cardTokens: /* @__PURE__ */ new Map(),
|
|
85
85
|
cardTokenCounter: 0,
|
|
86
86
|
cardTokenizationEnabled: true,
|
|
87
|
+
stripePaymentIntents: /* @__PURE__ */ new Map(),
|
|
88
|
+
stripePaymentIntentCounter: 0,
|
|
89
|
+
stripeIdempotentIntents: /* @__PURE__ */ new Map(),
|
|
90
|
+
stripeAllowedMethods: ["card", "promptpay"],
|
|
91
|
+
stripeChargesEnabled: true,
|
|
87
92
|
uploads: [],
|
|
88
93
|
uploadCounter: 0,
|
|
89
94
|
storageFiles: /* @__PURE__ */ new Map(),
|
|
@@ -103,6 +108,14 @@ var state = globalSingleton("state", () => ({
|
|
|
103
108
|
starlensCalls: [],
|
|
104
109
|
starlensReplies: [],
|
|
105
110
|
starlensDefaultReply: null,
|
|
111
|
+
controls: {
|
|
112
|
+
seed: {},
|
|
113
|
+
values: {},
|
|
114
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
115
|
+
lastUsage: null,
|
|
116
|
+
writes: [],
|
|
117
|
+
refusal: null
|
|
118
|
+
},
|
|
106
119
|
allowNetwork: false
|
|
107
120
|
}));
|
|
108
121
|
function requireDb() {
|
|
@@ -570,8 +583,8 @@ async function handleNeonSql(db, request) {
|
|
|
570
583
|
rowAsArray: arrayMode
|
|
571
584
|
});
|
|
572
585
|
} catch (error) {
|
|
573
|
-
const
|
|
574
|
-
return json({ message:
|
|
586
|
+
const err2 = error;
|
|
587
|
+
return json({ message: err2.message, code: err2.code }, 400);
|
|
575
588
|
}
|
|
576
589
|
}
|
|
577
590
|
|
|
@@ -1536,6 +1549,281 @@ var requestScopeStorage = globalSingleton(
|
|
|
1536
1549
|
() => new import_node_async_hooks.AsyncLocalStorage()
|
|
1537
1550
|
);
|
|
1538
1551
|
|
|
1552
|
+
// src/simulator/stripe-payment-intents.ts
|
|
1553
|
+
var SIM_STRIPE_PUBLISHABLE_KEY = "pk_test_sim_stripe";
|
|
1554
|
+
var SIM_STRIPE_DEFAULT_ACCOUNT = "acct_sim_default";
|
|
1555
|
+
var METHODS = ["card", "promptpay"];
|
|
1556
|
+
var RESERVED_METADATA_KEYS = ["projectId", "stardeckReferenceId", "stardeckStagingDeploymentId"];
|
|
1557
|
+
var PROMPTPAY_QR_TTL_SECONDS = 15 * 60;
|
|
1558
|
+
var PATH_RE = /^\/api\/store\/(?!beam\/)([^/]+)\/(payment-intents(?:\/([^/]+)(?:\/(cancel))?)?|elements-config|readiness)$/;
|
|
1559
|
+
function err(error, status, code) {
|
|
1560
|
+
return json({ error, code }, status);
|
|
1561
|
+
}
|
|
1562
|
+
function decode(segment) {
|
|
1563
|
+
try {
|
|
1564
|
+
return decodeURIComponent(segment);
|
|
1565
|
+
} catch {
|
|
1566
|
+
return segment;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
function isPlayground(account) {
|
|
1570
|
+
return account === "playground";
|
|
1571
|
+
}
|
|
1572
|
+
function resolveAccount(account) {
|
|
1573
|
+
if (isPlayground(account)) return null;
|
|
1574
|
+
if (account === "default") {
|
|
1575
|
+
return process.env.STRIPE_CONNECT_ACCOUNT_ID || SIM_STRIPE_DEFAULT_ACCOUNT;
|
|
1576
|
+
}
|
|
1577
|
+
return account;
|
|
1578
|
+
}
|
|
1579
|
+
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;
|
|
1580
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1581
|
+
var APP_METADATA_MAX_KEYS = 50 - RESERVED_METADATA_KEYS.length;
|
|
1582
|
+
function isStringRecord(value) {
|
|
1583
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
|
|
1584
|
+
}
|
|
1585
|
+
function isValidMetadata(value) {
|
|
1586
|
+
if (!isStringRecord(value)) return false;
|
|
1587
|
+
const entries = Object.entries(value);
|
|
1588
|
+
return entries.length <= APP_METADATA_MAX_KEYS && entries.every(([k, v]) => k.length >= 1 && k.length <= 40 && v.length <= 500);
|
|
1589
|
+
}
|
|
1590
|
+
function isOptional(value, test) {
|
|
1591
|
+
return value === void 0 || test(value);
|
|
1592
|
+
}
|
|
1593
|
+
var isEmail = (v) => typeof v === "string" && EMAIL_RE.test(v);
|
|
1594
|
+
function validateCreate(body) {
|
|
1595
|
+
const { amount, currency, method, referenceId, idempotencyKey } = body;
|
|
1596
|
+
const checks = [
|
|
1597
|
+
[
|
|
1598
|
+
typeof amount === "number" && Number.isInteger(amount) && amount > 0,
|
|
1599
|
+
"amount must be a positive integer in minor units"
|
|
1600
|
+
],
|
|
1601
|
+
[typeof currency === "string" && currency.length === 3, "currency must be a 3-letter ISO code"],
|
|
1602
|
+
[
|
|
1603
|
+
typeof method === "string" && METHODS.includes(method),
|
|
1604
|
+
`method must be one of: ${METHODS.join(", ")}`
|
|
1605
|
+
],
|
|
1606
|
+
[
|
|
1607
|
+
typeof referenceId === "string" && referenceId.length >= 1 && referenceId.length <= 200,
|
|
1608
|
+
"referenceId must be 1 to 200 characters"
|
|
1609
|
+
],
|
|
1610
|
+
[
|
|
1611
|
+
typeof idempotencyKey === "string" && idempotencyKey.length >= 1 && idempotencyKey.length <= 200,
|
|
1612
|
+
"idempotencyKey is required (1 to 200 characters)"
|
|
1613
|
+
],
|
|
1614
|
+
[
|
|
1615
|
+
isOptional(body.description, (v) => typeof v === "string" && v.length <= 1e3),
|
|
1616
|
+
"description must be at most 1000 characters"
|
|
1617
|
+
],
|
|
1618
|
+
[isOptional(body.receiptEmail, isEmail), "receiptEmail must be an email address"],
|
|
1619
|
+
[isOptional(body.billingEmail, isEmail), "billingEmail must be an email address"],
|
|
1620
|
+
[
|
|
1621
|
+
isOptional(body.returnUrl, (v) => typeof v === "string" && URL.canParse(v)),
|
|
1622
|
+
"returnUrl must be an absolute URL"
|
|
1623
|
+
],
|
|
1624
|
+
[
|
|
1625
|
+
isOptional(body.metadata, isValidMetadata),
|
|
1626
|
+
`metadata must map keys (1-40 chars) to strings (max 500), at most ${APP_METADATA_MAX_KEYS} keys`
|
|
1627
|
+
],
|
|
1628
|
+
[
|
|
1629
|
+
typeof body.deploymentId === "string" && UUID_RE2.test(body.deploymentId),
|
|
1630
|
+
"Invalid deployment ID"
|
|
1631
|
+
]
|
|
1632
|
+
];
|
|
1633
|
+
const failed = checks.find(([ok]) => !ok);
|
|
1634
|
+
return failed ? failed[1] : null;
|
|
1635
|
+
}
|
|
1636
|
+
function appMetadata(raw) {
|
|
1637
|
+
if (!isStringRecord(raw)) return {};
|
|
1638
|
+
const out = {};
|
|
1639
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
1640
|
+
if (!RESERVED_METADATA_KEYS.includes(key)) out[key] = value;
|
|
1641
|
+
}
|
|
1642
|
+
return out;
|
|
1643
|
+
}
|
|
1644
|
+
function stableStringify(value) {
|
|
1645
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1646
|
+
if (value && typeof value === "object") {
|
|
1647
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1648
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
1649
|
+
}
|
|
1650
|
+
return JSON.stringify(value);
|
|
1651
|
+
}
|
|
1652
|
+
function toStripePaymentIntentSummary(intent) {
|
|
1653
|
+
const open = intent.status !== "succeeded" && intent.status !== "canceled";
|
|
1654
|
+
let actionRequired = "NONE";
|
|
1655
|
+
if (open && intent.method === "card") actionRequired = "CLIENT_CONFIRM";
|
|
1656
|
+
if (open && intent.method === "promptpay" && intent.status === "requires_action") {
|
|
1657
|
+
actionRequired = "ENCODED_IMAGE";
|
|
1658
|
+
}
|
|
1659
|
+
return {
|
|
1660
|
+
id: intent.id,
|
|
1661
|
+
status: intent.status,
|
|
1662
|
+
amount: intent.amount,
|
|
1663
|
+
currency: intent.currency,
|
|
1664
|
+
method: intent.method,
|
|
1665
|
+
referenceId: intent.referenceId,
|
|
1666
|
+
metadata: { ...intent.appMetadata },
|
|
1667
|
+
actionRequired,
|
|
1668
|
+
clientSecret: intent.method === "card" ? intent.clientSecret : null,
|
|
1669
|
+
qr: actionRequired === "ENCODED_IMAGE" ? { ...intent.qr } : null,
|
|
1670
|
+
latestChargeId: intent.latestChargeId,
|
|
1671
|
+
amountRefunded: intent.amountRefunded,
|
|
1672
|
+
lastPaymentError: intent.lastPaymentError ? { ...intent.lastPaymentError } : null,
|
|
1673
|
+
created: intent.created,
|
|
1674
|
+
livemode: false,
|
|
1675
|
+
playground: intent.playground
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function promptPayQr(id, created) {
|
|
1679
|
+
return {
|
|
1680
|
+
imageUrlPng: `https://qr.stripe.test/${id}.png`,
|
|
1681
|
+
imageUrlSvg: `https://qr.stripe.test/${id}.svg`,
|
|
1682
|
+
data: `00020101021230SIM${id}`,
|
|
1683
|
+
hostedInstructionsUrl: `https://payments.stripe.test/promptpay/${id}`,
|
|
1684
|
+
expiresAt: created + PROMPTPAY_QR_TTL_SECONDS
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
function createIntent(body, account, projectId) {
|
|
1688
|
+
const problem = validateCreate(body);
|
|
1689
|
+
if (problem) return err(problem, 400, "invalid_request");
|
|
1690
|
+
const method = body.method;
|
|
1691
|
+
const currency = body.currency.toLowerCase();
|
|
1692
|
+
if (!state.stripeAllowedMethods.includes(method)) {
|
|
1693
|
+
return err(
|
|
1694
|
+
`The ${method} payment method is turned off for this account`,
|
|
1695
|
+
422,
|
|
1696
|
+
"method_not_allowed"
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
if (method === "promptpay" && currency !== "thb") {
|
|
1700
|
+
return err("PromptPay only takes THB", 422, "currency_not_supported");
|
|
1701
|
+
}
|
|
1702
|
+
if (!state.stripeChargesEnabled) {
|
|
1703
|
+
return err("This Stripe account can't take payments yet", 503, "account_not_ready");
|
|
1704
|
+
}
|
|
1705
|
+
const scopedKey = JSON.stringify(["pi", projectId, account, body.idempotencyKey]);
|
|
1706
|
+
const comparable = { ...body };
|
|
1707
|
+
delete comparable.deploymentId;
|
|
1708
|
+
delete comparable.idempotencyKey;
|
|
1709
|
+
const fingerprint = stableStringify(comparable);
|
|
1710
|
+
const saved = state.stripeIdempotentIntents.get(scopedKey);
|
|
1711
|
+
if (saved) {
|
|
1712
|
+
if (saved.fingerprint !== fingerprint) {
|
|
1713
|
+
return err(
|
|
1714
|
+
"This idempotency key was already used with different parameters. Use a new key for a different payment.",
|
|
1715
|
+
409,
|
|
1716
|
+
"idempotency_conflict"
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
const existing = state.stripePaymentIntents.get(saved.id);
|
|
1720
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(existing) }, 201);
|
|
1721
|
+
}
|
|
1722
|
+
state.stripePaymentIntentCounter += 1;
|
|
1723
|
+
const id = `pi_sim_${state.stripePaymentIntentCounter}`;
|
|
1724
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
1725
|
+
const referenceId = body.referenceId;
|
|
1726
|
+
const metadata = appMetadata(body.metadata);
|
|
1727
|
+
const intent = {
|
|
1728
|
+
id,
|
|
1729
|
+
projectId,
|
|
1730
|
+
account,
|
|
1731
|
+
connectedAccountId: resolveAccount(account),
|
|
1732
|
+
playground: isPlayground(account),
|
|
1733
|
+
status: method === "card" ? "requires_payment_method" : "requires_action",
|
|
1734
|
+
amount: body.amount,
|
|
1735
|
+
currency,
|
|
1736
|
+
method,
|
|
1737
|
+
referenceId,
|
|
1738
|
+
appMetadata: metadata,
|
|
1739
|
+
metadata: { ...metadata, projectId, stardeckReferenceId: referenceId },
|
|
1740
|
+
description: typeof body.description === "string" ? body.description : null,
|
|
1741
|
+
receiptEmail: typeof body.receiptEmail === "string" ? body.receiptEmail : null,
|
|
1742
|
+
returnUrl: typeof body.returnUrl === "string" ? body.returnUrl : null,
|
|
1743
|
+
clientSecret: `${id}_secret_sim`,
|
|
1744
|
+
qr: method === "promptpay" ? promptPayQr(id, created) : null,
|
|
1745
|
+
latestChargeId: null,
|
|
1746
|
+
amountRefunded: 0,
|
|
1747
|
+
lastPaymentError: null,
|
|
1748
|
+
created
|
|
1749
|
+
};
|
|
1750
|
+
state.stripePaymentIntents.set(id, intent);
|
|
1751
|
+
state.stripeIdempotentIntents.set(scopedKey, { fingerprint, id });
|
|
1752
|
+
console.log(
|
|
1753
|
+
`[stardeck-testing] stripe payment intent ${id} created method=${method} amount=${intent.amount} ${currency}`
|
|
1754
|
+
);
|
|
1755
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(intent) }, 201);
|
|
1756
|
+
}
|
|
1757
|
+
function ownedIntent(id, projectId) {
|
|
1758
|
+
const intent = state.stripePaymentIntents.get(id);
|
|
1759
|
+
return intent && intent.projectId === projectId ? intent : void 0;
|
|
1760
|
+
}
|
|
1761
|
+
function cancelIntent(intent) {
|
|
1762
|
+
if (intent.status === "succeeded") {
|
|
1763
|
+
return err("This payment already succeeded and can't be canceled", 409, "already_succeeded");
|
|
1764
|
+
}
|
|
1765
|
+
intent.status = "canceled";
|
|
1766
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(intent) });
|
|
1767
|
+
}
|
|
1768
|
+
function handleIntents(request, url, account, intentId, action, projectId) {
|
|
1769
|
+
if (!intentId && request.method === "POST") {
|
|
1770
|
+
return readJsonBody(request).then((body) => createIntent(body, account, projectId));
|
|
1771
|
+
}
|
|
1772
|
+
if (!intentId && request.method === "GET") {
|
|
1773
|
+
const referenceId = url.searchParams.get("referenceId");
|
|
1774
|
+
if (!referenceId) return err("referenceId is required", 400, "invalid_request");
|
|
1775
|
+
const paymentIntents = [...state.stripePaymentIntents.values()].filter((i) => i.projectId === projectId && i.referenceId === referenceId).map(toStripePaymentIntentSummary);
|
|
1776
|
+
return json({ paymentIntents });
|
|
1777
|
+
}
|
|
1778
|
+
if (!intentId) return null;
|
|
1779
|
+
const intent = ownedIntent(decode(intentId), projectId);
|
|
1780
|
+
if (!action && request.method === "GET") {
|
|
1781
|
+
if (!intent) return err("Payment intent not found", 404, "not_found");
|
|
1782
|
+
return json({ paymentIntent: toStripePaymentIntentSummary(intent) });
|
|
1783
|
+
}
|
|
1784
|
+
if (action === "cancel" && request.method === "POST") {
|
|
1785
|
+
return readJsonBody(request).then((body) => {
|
|
1786
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE2.test(body.deploymentId)) {
|
|
1787
|
+
return err("deploymentId is required", 400, "invalid_request");
|
|
1788
|
+
}
|
|
1789
|
+
if (!intent) return err("Payment intent not found", 404, "not_found");
|
|
1790
|
+
return cancelIntent(intent);
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
return null;
|
|
1794
|
+
}
|
|
1795
|
+
async function handleStripePaymentIntentRequest(request, url, projectId) {
|
|
1796
|
+
const match = url.pathname.match(PATH_RE);
|
|
1797
|
+
if (!match) return null;
|
|
1798
|
+
if (!projectId) return err("Missing authentication", 401, "unauthorized");
|
|
1799
|
+
const account = decode(match[1]);
|
|
1800
|
+
const resource = match[2];
|
|
1801
|
+
if (request.method === "GET" && !url.searchParams.get("deploymentId")) {
|
|
1802
|
+
return err("deploymentId is required", 400, "invalid_request");
|
|
1803
|
+
}
|
|
1804
|
+
if (resource === "elements-config" && request.method === "GET") {
|
|
1805
|
+
return json({
|
|
1806
|
+
publishableKey: SIM_STRIPE_PUBLISHABLE_KEY,
|
|
1807
|
+
stripeAccount: resolveAccount(account),
|
|
1808
|
+
playground: isPlayground(account)
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
if (resource === "readiness" && request.method === "GET") {
|
|
1812
|
+
const methods = [...state.stripeAllowedMethods];
|
|
1813
|
+
return json({
|
|
1814
|
+
provider: "stripe",
|
|
1815
|
+
ready: state.stripeChargesEnabled && methods.length > 0,
|
|
1816
|
+
chargesEnabled: state.stripeChargesEnabled,
|
|
1817
|
+
methods,
|
|
1818
|
+
playground: isPlayground(account)
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
if (resource.startsWith("payment-intents")) {
|
|
1822
|
+
return handleIntents(request, url, account, match[3], match[4], projectId);
|
|
1823
|
+
}
|
|
1824
|
+
return null;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1539
1827
|
// src/simulator/webhook-signing.ts
|
|
1540
1828
|
var import_node_crypto4 = __toESM(require("crypto"));
|
|
1541
1829
|
|
|
@@ -1555,7 +1843,7 @@ var BEAM_BOLT_PAYMENT_METHODS = [
|
|
|
1555
1843
|
"SPAY_LATER"
|
|
1556
1844
|
];
|
|
1557
1845
|
var BEAM_BOLT_INTENT_STATUSES = ["PENDING", "PAID", "FAILED", "CANCELED", "EXPIRED"];
|
|
1558
|
-
var
|
|
1846
|
+
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;
|
|
1559
1847
|
function payErr(error, status = 400, code, details) {
|
|
1560
1848
|
const body = { error };
|
|
1561
1849
|
if (code) body.code = code;
|
|
@@ -1581,7 +1869,23 @@ function paymentOperationForRequest(request, pathname) {
|
|
|
1581
1869
|
if (request.method === "GET" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout-sessions\/[^/]+$/.test(pathname)) {
|
|
1582
1870
|
return "stripe.getCheckoutSessionStatus";
|
|
1583
1871
|
}
|
|
1584
|
-
return
|
|
1872
|
+
return stripeInlineOperation(request.method, pathname);
|
|
1873
|
+
}
|
|
1874
|
+
var STRIPE_INLINE_OPERATIONS = [
|
|
1875
|
+
["POST", /^\/api\/store\/(?!beam\/)[^/]+\/payment-intents$/, "stripe.createPaymentIntent"],
|
|
1876
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/payment-intents$/, "stripe.findPaymentIntents"],
|
|
1877
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/payment-intents\/[^/]+$/, "stripe.getPaymentIntent"],
|
|
1878
|
+
[
|
|
1879
|
+
"POST",
|
|
1880
|
+
/^\/api\/store\/(?!beam\/)[^/]+\/payment-intents\/[^/]+\/cancel$/,
|
|
1881
|
+
"stripe.cancelPaymentIntent"
|
|
1882
|
+
],
|
|
1883
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/elements-config$/, "stripe.getElementsConfig"],
|
|
1884
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/readiness$/, "stripe.getReadiness"]
|
|
1885
|
+
];
|
|
1886
|
+
function stripeInlineOperation(method, pathname) {
|
|
1887
|
+
const hit = STRIPE_INLINE_OPERATIONS.find(([m, re]) => m === method && re.test(pathname));
|
|
1888
|
+
return hit ? hit[2] : null;
|
|
1585
1889
|
}
|
|
1586
1890
|
function consumePaymentFailure(request, pathname) {
|
|
1587
1891
|
const operation = paymentOperationForRequest(request, pathname);
|
|
@@ -1611,11 +1915,11 @@ function isBoltPaymentMethod(value) {
|
|
|
1611
1915
|
function isBoltIntentStatus(value) {
|
|
1612
1916
|
return BEAM_BOLT_INTENT_STATUSES.includes(value);
|
|
1613
1917
|
}
|
|
1614
|
-
function
|
|
1615
|
-
if (Array.isArray(value)) return `[${value.map(
|
|
1918
|
+
function stableStringify2(value) {
|
|
1919
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify2).join(",")}]`;
|
|
1616
1920
|
if (value && typeof value === "object") {
|
|
1617
1921
|
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1618
|
-
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${
|
|
1922
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify2(v)}`).join(",")}}`;
|
|
1619
1923
|
}
|
|
1620
1924
|
return JSON.stringify(value);
|
|
1621
1925
|
}
|
|
@@ -1721,10 +2025,16 @@ function storedStatusesForFilter(statuses) {
|
|
|
1721
2025
|
}
|
|
1722
2026
|
return [...stored];
|
|
1723
2027
|
}
|
|
1724
|
-
async function handlePaymentsRequest(request, url) {
|
|
2028
|
+
async function handlePaymentsRequest(request, url, signedProjectId = null) {
|
|
1725
2029
|
const pathname = url.pathname;
|
|
1726
2030
|
const injectedFailure = consumePaymentFailure(request, pathname);
|
|
1727
2031
|
if (injectedFailure) return injectedFailure;
|
|
2032
|
+
const inlineStripeResponse = await handleStripePaymentIntentRequest(
|
|
2033
|
+
request,
|
|
2034
|
+
url,
|
|
2035
|
+
signedProjectId
|
|
2036
|
+
);
|
|
2037
|
+
if (inlineStripeResponse) return inlineStripeResponse;
|
|
1728
2038
|
const directChargeResponse = await handleDirectChargeRequest(request, pathname);
|
|
1729
2039
|
if (directChargeResponse) return directChargeResponse;
|
|
1730
2040
|
if (/\/billing-portal$/.test(pathname)) {
|
|
@@ -1823,7 +2133,7 @@ async function handlePaymentsRequest(request, url) {
|
|
|
1823
2133
|
code: "too_small"
|
|
1824
2134
|
});
|
|
1825
2135
|
}
|
|
1826
|
-
if (typeof body.deploymentId !== "string" || !
|
|
2136
|
+
if (typeof body.deploymentId !== "string" || !UUID_RE3.test(body.deploymentId)) {
|
|
1827
2137
|
issues.push({
|
|
1828
2138
|
path: ["deploymentId"],
|
|
1829
2139
|
message: "Invalid uuid",
|
|
@@ -2007,7 +2317,7 @@ async function handlePaymentsRequest(request, url) {
|
|
|
2007
2317
|
const comparable = { ...body };
|
|
2008
2318
|
delete comparable.deploymentId;
|
|
2009
2319
|
delete comparable.idempotencyKey;
|
|
2010
|
-
const fingerprint =
|
|
2320
|
+
const fingerprint = stableStringify2(comparable);
|
|
2011
2321
|
if (scopedKey) {
|
|
2012
2322
|
const saved = state.idempotentCheckouts.get(scopedKey);
|
|
2013
2323
|
if (saved) {
|
|
@@ -2708,6 +3018,91 @@ async function handleEdgeRequest(request, subPath) {
|
|
|
2708
3018
|
return edgeFailure(`No edge simulator for ${method} .../edge${subPath}`, 404);
|
|
2709
3019
|
}
|
|
2710
3020
|
|
|
3021
|
+
// src/simulator/controls.ts
|
|
3022
|
+
var USER_TOKEN_HEADER = "x-stardeck-user-token";
|
|
3023
|
+
var REFUSAL_STATUS = {
|
|
3024
|
+
NOT_WRITABLE: 400,
|
|
3025
|
+
INVALID_VALUE: 400,
|
|
3026
|
+
UNAUTHENTICATED: 401,
|
|
3027
|
+
FORBIDDEN: 403,
|
|
3028
|
+
WRITE_NOT_ALLOWED_HERE: 403,
|
|
3029
|
+
PROVIDER_NOT_READY: 409,
|
|
3030
|
+
UNAVAILABLE: 503
|
|
3031
|
+
};
|
|
3032
|
+
var controlsModule = null;
|
|
3033
|
+
function loadControls() {
|
|
3034
|
+
controlsModule ??= import("@stardeck-customer-apps/controls");
|
|
3035
|
+
return controlsModule;
|
|
3036
|
+
}
|
|
3037
|
+
function currentControlsBody() {
|
|
3038
|
+
const controls = {};
|
|
3039
|
+
for (const [key, value] of Object.entries(state.controls.values)) {
|
|
3040
|
+
controls[key] = state.controls.unavailable.has(key) ? { ok: false, reason: "unavailable (simulated)" } : { ok: true, value };
|
|
3041
|
+
}
|
|
3042
|
+
return controls;
|
|
3043
|
+
}
|
|
3044
|
+
function refusalResponse(refusal) {
|
|
3045
|
+
return json({ error: refusal }, REFUSAL_STATUS[refusal.code]);
|
|
3046
|
+
}
|
|
3047
|
+
function handleSync(body) {
|
|
3048
|
+
const usage = body.usage;
|
|
3049
|
+
if (typeof usage === "object" && usage !== null) {
|
|
3050
|
+
const locations = usage.locations;
|
|
3051
|
+
state.controls.lastUsage = typeof locations === "number" && Number.isInteger(locations) && locations >= 0 ? { locations } : {};
|
|
3052
|
+
}
|
|
3053
|
+
return json({ controls: currentControlsBody() });
|
|
3054
|
+
}
|
|
3055
|
+
async function handleWrite(request, key) {
|
|
3056
|
+
const { CONTROLS, isWritableControl } = await loadControls();
|
|
3057
|
+
if (!isWritableControl(key)) {
|
|
3058
|
+
return refusalResponse({
|
|
3059
|
+
code: "NOT_WRITABLE",
|
|
3060
|
+
message: `${key} cannot be changed from the app`
|
|
3061
|
+
});
|
|
3062
|
+
}
|
|
3063
|
+
const userToken = request.headers.get(USER_TOKEN_HEADER);
|
|
3064
|
+
if (!userToken) {
|
|
3065
|
+
return refusalResponse({
|
|
3066
|
+
code: "UNAUTHENTICATED",
|
|
3067
|
+
message: "A signed-in person is required (X-Stardeck-User-Token)"
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
3070
|
+
const body = await readJsonBody(request);
|
|
3071
|
+
const parsed = CONTROLS[key].schema.safeParse(body.value);
|
|
3072
|
+
if (!parsed.success) {
|
|
3073
|
+
return refusalResponse({
|
|
3074
|
+
code: "INVALID_VALUE",
|
|
3075
|
+
message: parsed.error.issues[0]?.message ?? "Invalid value"
|
|
3076
|
+
});
|
|
3077
|
+
}
|
|
3078
|
+
const refusal = state.controls.refusal;
|
|
3079
|
+
if (refusal) {
|
|
3080
|
+
state.controls.refusal = null;
|
|
3081
|
+
return refusalResponse(refusal);
|
|
3082
|
+
}
|
|
3083
|
+
state.controls.values[key] = parsed.data;
|
|
3084
|
+
state.controls.unavailable.delete(key);
|
|
3085
|
+
state.controls.writes.push({ key, value: parsed.data, userToken });
|
|
3086
|
+
return json({ controls: currentControlsBody() });
|
|
3087
|
+
}
|
|
3088
|
+
var CONTROLS_PATH = /^\/api\/deployments\/[^/]+\/controls(?:\/([^/]+))?$/;
|
|
3089
|
+
async function handleControlsRequest(request, url) {
|
|
3090
|
+
const match = url.pathname.match(CONTROLS_PATH);
|
|
3091
|
+
const key = match?.[1] ? decodeURIComponent(match[1]) : null;
|
|
3092
|
+
if (!key && request.method === "POST") {
|
|
3093
|
+
return handleSync(await readJsonBody(request));
|
|
3094
|
+
}
|
|
3095
|
+
if (key && request.method === "PUT") {
|
|
3096
|
+
return handleWrite(request, key);
|
|
3097
|
+
}
|
|
3098
|
+
return json(
|
|
3099
|
+
{
|
|
3100
|
+
error: { code: "UNAVAILABLE", message: `No simulator for ${request.method} ${url.pathname}` }
|
|
3101
|
+
},
|
|
3102
|
+
404
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3105
|
+
|
|
2711
3106
|
// src/simulator/schedules.ts
|
|
2712
3107
|
async function handleScheduleWake(request, scheduleKey) {
|
|
2713
3108
|
if (request.method !== "POST") return failure(`Method ${request.method} not allowed`, 405);
|
|
@@ -2847,7 +3242,7 @@ function requiresDeploymentHmac(request, url) {
|
|
|
2847
3242
|
return !isPresignedPut;
|
|
2848
3243
|
}
|
|
2849
3244
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
2850
|
-
const
|
|
3245
|
+
const isEmail2 = url.pathname === "/api/email/send";
|
|
2851
3246
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
2852
3247
|
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
2853
3248
|
const messagingMatch = url.pathname.match(
|
|
@@ -2856,8 +3251,10 @@ function requiresDeploymentHmac(request, url) {
|
|
|
2856
3251
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2857
3252
|
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
2858
3253
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
2859
|
-
|
|
3254
|
+
const controlsMatch = url.pathname.match(CONTROLS_PATH2);
|
|
3255
|
+
return !!(controlsMatch || isStarlens || scheduleWakeMatch || dataStoreMatch || isEmail2 || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
2860
3256
|
}
|
|
3257
|
+
var CONTROLS_PATH2 = /^\/api\/deployments\/[^/]+\/controls(\/[^/]+)?$/;
|
|
2861
3258
|
var SCHEDULE_WAKE_PATH = /^\/api\/deployments\/[^/]+\/schedules\/([^/]+)\/wake$/;
|
|
2862
3259
|
function deploymentIdFromPath(url) {
|
|
2863
3260
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
@@ -2879,7 +3276,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2879
3276
|
return authMatch[1].endsWith("verify") ? handleAuthVerify(request) : handleAuthRefresh(request);
|
|
2880
3277
|
}
|
|
2881
3278
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
2882
|
-
const
|
|
3279
|
+
const isEmail2 = url.pathname === "/api/email/send";
|
|
2883
3280
|
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
2884
3281
|
const storeMatch = url.pathname.match(/^\/api\/store\//);
|
|
2885
3282
|
const messagingMatch = url.pathname.match(
|
|
@@ -2888,6 +3285,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2888
3285
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2889
3286
|
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
2890
3287
|
let signedDeploymentId = null;
|
|
3288
|
+
let signedProjectId = null;
|
|
2891
3289
|
if (requiresDeploymentHmac(request, url)) {
|
|
2892
3290
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
2893
3291
|
if (!authHeader) {
|
|
@@ -2900,6 +3298,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2900
3298
|
return failure("Invalid authentication", 401);
|
|
2901
3299
|
}
|
|
2902
3300
|
signedDeploymentId = authPayload.deploymentId;
|
|
3301
|
+
signedProjectId = authPayload.projectId;
|
|
2903
3302
|
}
|
|
2904
3303
|
if (isStorageHost) {
|
|
2905
3304
|
return handleStorageRequest(request, url);
|
|
@@ -2907,14 +3306,14 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2907
3306
|
if (url.hostname === SIM_BEAM_HOST) {
|
|
2908
3307
|
return handleCardTokenRequest(request, url);
|
|
2909
3308
|
}
|
|
2910
|
-
if (
|
|
3309
|
+
if (isEmail2 && request.method === "POST") {
|
|
2911
3310
|
return handleEmailSend(request);
|
|
2912
3311
|
}
|
|
2913
3312
|
if (identitiesMatch) {
|
|
2914
3313
|
return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
|
|
2915
3314
|
}
|
|
2916
3315
|
if (storeMatch) {
|
|
2917
|
-
return handlePaymentsRequest(request, url);
|
|
3316
|
+
return handlePaymentsRequest(request, url, signedProjectId);
|
|
2918
3317
|
}
|
|
2919
3318
|
if (messagingMatch) {
|
|
2920
3319
|
const channel = messagingMatch[1];
|
|
@@ -2923,6 +3322,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2923
3322
|
if (edgeMatch) {
|
|
2924
3323
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
2925
3324
|
}
|
|
3325
|
+
if (CONTROLS_PATH2.test(url.pathname)) {
|
|
3326
|
+
return handleControlsRequest(request, url);
|
|
3327
|
+
}
|
|
2926
3328
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
2927
3329
|
if (scheduleWakeMatch) {
|
|
2928
3330
|
return handleScheduleWake(request, decodeURIComponent(scheduleWakeMatch[1]));
|
|
@@ -2951,7 +3353,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2951
3353
|
}
|
|
2952
3354
|
}
|
|
2953
3355
|
return failure(
|
|
2954
|
-
`[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.`,
|
|
3356
|
+
`[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, app controls sync/write, starlens extract, schedule wakes, auth verify/refresh, Neon /sql.`,
|
|
2955
3357
|
404
|
|
2956
3358
|
);
|
|
2957
3359
|
}
|