@stardeck-customer-apps/testing 0.27.0 → 0.29.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 +61 -2
- package/dist/index.d.mts +122 -1
- package/dist/index.d.ts +122 -1
- package/dist/index.js +737 -6
- package/dist/index.mjs +737 -6
- package/dist/next/headers-shim.js +14 -0
- package/dist/next/headers-shim.mjs +14 -0
- package/dist/setup.js +479 -4
- package/dist/setup.mjs +479 -4
- package/package.json +6 -1
|
@@ -82,6 +82,12 @@ var state = globalSingleton("state", () => ({
|
|
|
82
82
|
stripePaymentIntents: /* @__PURE__ */ new Map(),
|
|
83
83
|
stripePaymentIntentCounter: 0,
|
|
84
84
|
stripeIdempotentIntents: /* @__PURE__ */ new Map(),
|
|
85
|
+
stripeRefunds: /* @__PURE__ */ new Map(),
|
|
86
|
+
stripeRefundCounter: 0,
|
|
87
|
+
stripeIdempotentRefunds: /* @__PURE__ */ new Map(),
|
|
88
|
+
beamRefunds: /* @__PURE__ */ new Map(),
|
|
89
|
+
beamRefundCounter: 0,
|
|
90
|
+
beamIdempotentRefunds: /* @__PURE__ */ new Map(),
|
|
85
91
|
stripeAllowedMethods: ["card", "promptpay"],
|
|
86
92
|
stripeChargesEnabled: true,
|
|
87
93
|
uploads: [],
|
|
@@ -103,6 +109,14 @@ var state = globalSingleton("state", () => ({
|
|
|
103
109
|
starlensCalls: [],
|
|
104
110
|
starlensReplies: [],
|
|
105
111
|
starlensDefaultReply: null,
|
|
112
|
+
controls: {
|
|
113
|
+
seed: {},
|
|
114
|
+
values: {},
|
|
115
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
116
|
+
lastUsage: null,
|
|
117
|
+
writes: [],
|
|
118
|
+
refusal: null
|
|
119
|
+
},
|
|
106
120
|
allowNetwork: false
|
|
107
121
|
}));
|
|
108
122
|
|
|
@@ -55,6 +55,12 @@ var state = globalSingleton("state", () => ({
|
|
|
55
55
|
stripePaymentIntents: /* @__PURE__ */ new Map(),
|
|
56
56
|
stripePaymentIntentCounter: 0,
|
|
57
57
|
stripeIdempotentIntents: /* @__PURE__ */ new Map(),
|
|
58
|
+
stripeRefunds: /* @__PURE__ */ new Map(),
|
|
59
|
+
stripeRefundCounter: 0,
|
|
60
|
+
stripeIdempotentRefunds: /* @__PURE__ */ new Map(),
|
|
61
|
+
beamRefunds: /* @__PURE__ */ new Map(),
|
|
62
|
+
beamRefundCounter: 0,
|
|
63
|
+
beamIdempotentRefunds: /* @__PURE__ */ new Map(),
|
|
58
64
|
stripeAllowedMethods: ["card", "promptpay"],
|
|
59
65
|
stripeChargesEnabled: true,
|
|
60
66
|
uploads: [],
|
|
@@ -76,6 +82,14 @@ var state = globalSingleton("state", () => ({
|
|
|
76
82
|
starlensCalls: [],
|
|
77
83
|
starlensReplies: [],
|
|
78
84
|
starlensDefaultReply: null,
|
|
85
|
+
controls: {
|
|
86
|
+
seed: {},
|
|
87
|
+
values: {},
|
|
88
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
89
|
+
lastUsage: null,
|
|
90
|
+
writes: [],
|
|
91
|
+
refusal: null
|
|
92
|
+
},
|
|
79
93
|
allowNetwork: false
|
|
80
94
|
}));
|
|
81
95
|
|
package/dist/setup.js
CHANGED
|
@@ -87,6 +87,12 @@ var state = globalSingleton("state", () => ({
|
|
|
87
87
|
stripePaymentIntents: /* @__PURE__ */ new Map(),
|
|
88
88
|
stripePaymentIntentCounter: 0,
|
|
89
89
|
stripeIdempotentIntents: /* @__PURE__ */ new Map(),
|
|
90
|
+
stripeRefunds: /* @__PURE__ */ new Map(),
|
|
91
|
+
stripeRefundCounter: 0,
|
|
92
|
+
stripeIdempotentRefunds: /* @__PURE__ */ new Map(),
|
|
93
|
+
beamRefunds: /* @__PURE__ */ new Map(),
|
|
94
|
+
beamRefundCounter: 0,
|
|
95
|
+
beamIdempotentRefunds: /* @__PURE__ */ new Map(),
|
|
90
96
|
stripeAllowedMethods: ["card", "promptpay"],
|
|
91
97
|
stripeChargesEnabled: true,
|
|
92
98
|
uploads: [],
|
|
@@ -108,6 +114,14 @@ var state = globalSingleton("state", () => ({
|
|
|
108
114
|
starlensCalls: [],
|
|
109
115
|
starlensReplies: [],
|
|
110
116
|
starlensDefaultReply: null,
|
|
117
|
+
controls: {
|
|
118
|
+
seed: {},
|
|
119
|
+
values: {},
|
|
120
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
121
|
+
lastUsage: null,
|
|
122
|
+
writes: [],
|
|
123
|
+
refusal: null
|
|
124
|
+
},
|
|
111
125
|
allowNetwork: false
|
|
112
126
|
}));
|
|
113
127
|
function requireDb() {
|
|
@@ -1545,7 +1559,11 @@ var requestScopeStorage = globalSingleton(
|
|
|
1545
1559
|
var SIM_STRIPE_PUBLISHABLE_KEY = "pk_test_sim_stripe";
|
|
1546
1560
|
var SIM_STRIPE_DEFAULT_ACCOUNT = "acct_sim_default";
|
|
1547
1561
|
var METHODS = ["card", "promptpay"];
|
|
1548
|
-
var RESERVED_METADATA_KEYS = [
|
|
1562
|
+
var RESERVED_METADATA_KEYS = [
|
|
1563
|
+
"projectId",
|
|
1564
|
+
"stardeckReferenceId",
|
|
1565
|
+
"stardeckStagingDeploymentId"
|
|
1566
|
+
];
|
|
1549
1567
|
var PROMPTPAY_QR_TTL_SECONDS = 15 * 60;
|
|
1550
1568
|
var PATH_RE = /^\/api\/store\/(?!beam\/)([^/]+)\/(payment-intents(?:\/([^/]+)(?:\/(cancel))?)?|elements-config|readiness)$/;
|
|
1551
1569
|
function err(error, status, code) {
|
|
@@ -1816,6 +1834,350 @@ async function handleStripePaymentIntentRequest(request, url, projectId) {
|
|
|
1816
1834
|
return null;
|
|
1817
1835
|
}
|
|
1818
1836
|
|
|
1837
|
+
// src/simulator/refunds.ts
|
|
1838
|
+
var REFUND_ROW_KEY = "refundRowId";
|
|
1839
|
+
var REFUND_METADATA_MAX_KEYS = 50 - RESERVED_METADATA_KEYS.length - 1;
|
|
1840
|
+
var REFUND_REASONS = ["duplicate", "fraudulent", "requested_by_customer"];
|
|
1841
|
+
var PROMPTPAY_BANK_DETAILS_WINDOW_SECONDS = 7 * 24 * 60 * 60;
|
|
1842
|
+
var BEAM_PARTIAL_METHODS = ["CARD", "CARD_TOKEN"];
|
|
1843
|
+
function refundFailure(plan) {
|
|
1844
|
+
return err(plan.message, plan.status, plan.code);
|
|
1845
|
+
}
|
|
1846
|
+
function isPositiveInt(value) {
|
|
1847
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
1848
|
+
}
|
|
1849
|
+
function hasValidKey(value) {
|
|
1850
|
+
return typeof value === "string" && value.length >= 1 && value.length <= 255;
|
|
1851
|
+
}
|
|
1852
|
+
function isValidDeploymentId(value) {
|
|
1853
|
+
return typeof value === "string" && UUID_RE2.test(value);
|
|
1854
|
+
}
|
|
1855
|
+
function isValidRefundMetadata(value) {
|
|
1856
|
+
if (!isStringRecord(value)) return false;
|
|
1857
|
+
const entries = Object.entries(value);
|
|
1858
|
+
return entries.length <= REFUND_METADATA_MAX_KEYS && entries.every(
|
|
1859
|
+
([k, v]) => k.length >= 1 && k.length <= 40 && v.length <= 500 && !RESERVED_METADATA_KEYS.includes(k) && k !== REFUND_ROW_KEY
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
var COUNTED_STRIPE_STATUSES = [
|
|
1863
|
+
"pending",
|
|
1864
|
+
"requires_action",
|
|
1865
|
+
"succeeded"
|
|
1866
|
+
];
|
|
1867
|
+
function refundedAmount(intentId) {
|
|
1868
|
+
let total = 0;
|
|
1869
|
+
for (const refund of state.stripeRefunds.values()) {
|
|
1870
|
+
if (refund.paymentIntentId === intentId && COUNTED_STRIPE_STATUSES.includes(refund.status)) {
|
|
1871
|
+
total += refund.amount;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
return total;
|
|
1875
|
+
}
|
|
1876
|
+
function syncAmountRefunded(intent) {
|
|
1877
|
+
intent.amountRefunded = refundedAmount(intent.id);
|
|
1878
|
+
}
|
|
1879
|
+
function validateStripeRefund(body) {
|
|
1880
|
+
const checks = [
|
|
1881
|
+
[isValidDeploymentId(body.deploymentId), "Invalid deployment ID"],
|
|
1882
|
+
[hasValidKey(body.idempotencyKey), "idempotencyKey is required (1 to 255 characters)"],
|
|
1883
|
+
[isOptional(body.amount, isPositiveInt), "amount must be a positive integer in minor units"],
|
|
1884
|
+
[
|
|
1885
|
+
isOptional(body.reason, (v) => typeof v === "string" && REFUND_REASONS.includes(v)),
|
|
1886
|
+
`reason must be one of: ${REFUND_REASONS.join(", ")}`
|
|
1887
|
+
],
|
|
1888
|
+
[isOptional(body.instructionsEmail, isEmail), "instructionsEmail must be an email address"],
|
|
1889
|
+
[
|
|
1890
|
+
isOptional(
|
|
1891
|
+
body.refundRowId,
|
|
1892
|
+
(v) => typeof v === "string" && v.length >= 1 && v.length <= 500
|
|
1893
|
+
),
|
|
1894
|
+
"refundRowId must be 1 to 500 characters"
|
|
1895
|
+
],
|
|
1896
|
+
[
|
|
1897
|
+
isOptional(body.metadata, isValidRefundMetadata),
|
|
1898
|
+
`metadata must map keys (1-40 chars) to strings (max 500), at most ${REFUND_METADATA_MAX_KEYS} keys, without the keys the platform reserves (use refundRowId instead)`
|
|
1899
|
+
]
|
|
1900
|
+
];
|
|
1901
|
+
const failed = checks.find(([ok]) => !ok);
|
|
1902
|
+
return failed ? failed[1] : null;
|
|
1903
|
+
}
|
|
1904
|
+
function planStripeRefund(intent, requested) {
|
|
1905
|
+
if (intent.status !== "succeeded") {
|
|
1906
|
+
return {
|
|
1907
|
+
ok: false,
|
|
1908
|
+
status: 409,
|
|
1909
|
+
code: "payment_not_refundable",
|
|
1910
|
+
message: "This payment hasn't succeeded, so there is nothing to refund"
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
const remaining = intent.amount - refundedAmount(intent.id);
|
|
1914
|
+
const amount = requested ?? remaining;
|
|
1915
|
+
if (remaining <= 0 || amount > remaining) {
|
|
1916
|
+
return {
|
|
1917
|
+
ok: false,
|
|
1918
|
+
status: 409,
|
|
1919
|
+
code: "refund_exceeds_remaining",
|
|
1920
|
+
message: `Only ${remaining} can still be refunded on this payment`
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
return { ok: true, amount };
|
|
1924
|
+
}
|
|
1925
|
+
function toStripeRefundSummary(refund) {
|
|
1926
|
+
const intent = state.stripePaymentIntents.get(refund.paymentIntentId);
|
|
1927
|
+
return {
|
|
1928
|
+
id: refund.id,
|
|
1929
|
+
paymentIntentId: refund.paymentIntentId,
|
|
1930
|
+
amount: refund.amount,
|
|
1931
|
+
currency: refund.currency,
|
|
1932
|
+
status: refund.status,
|
|
1933
|
+
failureReason: refund.failureReason,
|
|
1934
|
+
pendingReason: refund.pendingReason,
|
|
1935
|
+
nextActionExpiresAt: refund.nextActionExpiresAt,
|
|
1936
|
+
refundRowId: refund.refundRowId,
|
|
1937
|
+
metadata: { ...refund.appMetadata },
|
|
1938
|
+
created: refund.created,
|
|
1939
|
+
livemode: false,
|
|
1940
|
+
playground: intent?.playground ?? false
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
function newStripeRefund(input) {
|
|
1944
|
+
state.stripeRefundCounter += 1;
|
|
1945
|
+
const refund = {
|
|
1946
|
+
id: `re_sim_${state.stripeRefundCounter}`,
|
|
1947
|
+
paymentIntentId: input.intent.id,
|
|
1948
|
+
projectId: input.intent.projectId,
|
|
1949
|
+
amount: input.amount,
|
|
1950
|
+
currency: input.intent.currency,
|
|
1951
|
+
// A refund made in the Stripe dashboard is already done; the app's waits for `settleRefund`.
|
|
1952
|
+
status: input.origin === "dashboard" ? "succeeded" : "pending",
|
|
1953
|
+
failureReason: null,
|
|
1954
|
+
pendingReason: null,
|
|
1955
|
+
nextActionExpiresAt: null,
|
|
1956
|
+
refundRowId: input.refundRowId ?? null,
|
|
1957
|
+
appMetadata: input.appMetadata ?? {},
|
|
1958
|
+
reason: input.reason ?? null,
|
|
1959
|
+
origin: input.origin,
|
|
1960
|
+
revision: 0,
|
|
1961
|
+
created: Math.floor(Date.now() / 1e3)
|
|
1962
|
+
};
|
|
1963
|
+
state.stripeRefunds.set(refund.id, refund);
|
|
1964
|
+
syncAmountRefunded(input.intent);
|
|
1965
|
+
console.log(
|
|
1966
|
+
`[stardeck-testing] stripe refund ${refund.id} created payment=${refund.paymentIntentId} amount=${refund.amount} origin=${refund.origin}`
|
|
1967
|
+
);
|
|
1968
|
+
return refund;
|
|
1969
|
+
}
|
|
1970
|
+
function createStripeRefund(body, intent, projectId) {
|
|
1971
|
+
const scopedKey = JSON.stringify(["refund", projectId, intent.account, body.idempotencyKey]);
|
|
1972
|
+
const fingerprint = stableStringify({
|
|
1973
|
+
paymentIntentId: intent.id,
|
|
1974
|
+
amount: body.amount,
|
|
1975
|
+
reason: body.reason,
|
|
1976
|
+
instructionsEmail: body.instructionsEmail,
|
|
1977
|
+
refundRowId: body.refundRowId,
|
|
1978
|
+
metadata: body.metadata
|
|
1979
|
+
});
|
|
1980
|
+
const saved = state.stripeIdempotentRefunds.get(scopedKey);
|
|
1981
|
+
if (saved) {
|
|
1982
|
+
if (saved.fingerprint !== fingerprint) {
|
|
1983
|
+
return err(
|
|
1984
|
+
"This idempotency key was already used with different parameters. Use a new key for a different refund.",
|
|
1985
|
+
409,
|
|
1986
|
+
"idempotency_conflict"
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
return json({ refund: toStripeRefundSummary(state.stripeRefunds.get(saved.id)) });
|
|
1990
|
+
}
|
|
1991
|
+
const plan = planStripeRefund(intent, body.amount);
|
|
1992
|
+
if (!plan.ok) return refundFailure(plan);
|
|
1993
|
+
const refund = newStripeRefund({
|
|
1994
|
+
intent,
|
|
1995
|
+
amount: plan.amount,
|
|
1996
|
+
origin: "app",
|
|
1997
|
+
refundRowId: typeof body.refundRowId === "string" ? body.refundRowId : null,
|
|
1998
|
+
appMetadata: appMetadata(body.metadata),
|
|
1999
|
+
reason: typeof body.reason === "string" ? body.reason : null
|
|
2000
|
+
});
|
|
2001
|
+
state.stripeIdempotentRefunds.set(scopedKey, { fingerprint, id: refund.id });
|
|
2002
|
+
return json({ refund: toStripeRefundSummary(refund) });
|
|
2003
|
+
}
|
|
2004
|
+
var STRIPE_REFUND_PATH = /^\/api\/store\/(?!beam\/)[^/]+\/(?:payment-intents\/([^/]+)\/refunds|refunds\/([^/]+))$/;
|
|
2005
|
+
async function handleStripeRefundRequest(request, url, projectId) {
|
|
2006
|
+
const match = url.pathname.match(STRIPE_REFUND_PATH);
|
|
2007
|
+
if (!match) return null;
|
|
2008
|
+
const [, intentId, refundId] = match;
|
|
2009
|
+
const isCreate = request.method === "POST" && intentId !== void 0;
|
|
2010
|
+
const isRead = request.method === "GET" && refundId !== void 0;
|
|
2011
|
+
if (!isCreate && !isRead) return null;
|
|
2012
|
+
if (!projectId) return err("Missing authentication", 401, "unauthorized");
|
|
2013
|
+
if (isRead) {
|
|
2014
|
+
if (!url.searchParams.get("deploymentId")) {
|
|
2015
|
+
return err("deploymentId is required", 400, "invalid_request");
|
|
2016
|
+
}
|
|
2017
|
+
const refund = state.stripeRefunds.get(decode(refundId));
|
|
2018
|
+
if (!refund || refund.projectId !== projectId) {
|
|
2019
|
+
return err("Refund not found", 404, "refund_not_found");
|
|
2020
|
+
}
|
|
2021
|
+
return json({ refund: toStripeRefundSummary(refund) });
|
|
2022
|
+
}
|
|
2023
|
+
const body = await readJsonBody(request);
|
|
2024
|
+
const problem = validateStripeRefund(body);
|
|
2025
|
+
if (problem) return err(problem, 400, "invalid_request");
|
|
2026
|
+
const intent = ownedIntent(decode(intentId), projectId);
|
|
2027
|
+
if (!intent) return err("No such payment for this app", 404, "payment_not_owned");
|
|
2028
|
+
return createStripeRefund(body, intent, projectId);
|
|
2029
|
+
}
|
|
2030
|
+
function refundsOfCharge(chargeId) {
|
|
2031
|
+
return [...state.beamRefunds.values()].filter((r) => r.chargeId === chargeId);
|
|
2032
|
+
}
|
|
2033
|
+
function planBeamRefund(charge, requested) {
|
|
2034
|
+
if (charge.status !== "SUCCEEDED") {
|
|
2035
|
+
return {
|
|
2036
|
+
ok: false,
|
|
2037
|
+
status: 409,
|
|
2038
|
+
code: "payment_not_refundable",
|
|
2039
|
+
message: "This charge hasn't succeeded, so there is nothing to refund"
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
const refunds = refundsOfCharge(charge.id);
|
|
2043
|
+
if (refunds.some((r) => r.status === "PENDING")) {
|
|
2044
|
+
return {
|
|
2045
|
+
ok: false,
|
|
2046
|
+
status: 409,
|
|
2047
|
+
code: "refund_in_progress",
|
|
2048
|
+
message: "This charge already has a refund in progress. Wait for it to finish."
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
const refunded = refunds.filter((r) => r.status === "SUCCEEDED").reduce((n, r) => n + r.amount, 0);
|
|
2052
|
+
const remaining = charge.amount - refunded;
|
|
2053
|
+
const amount = requested ?? remaining;
|
|
2054
|
+
if (remaining <= 0 || amount > remaining) {
|
|
2055
|
+
return {
|
|
2056
|
+
ok: false,
|
|
2057
|
+
status: 409,
|
|
2058
|
+
code: "refund_exceeds_remaining",
|
|
2059
|
+
message: `Only ${remaining} can still be refunded on this charge`
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
if (amount < charge.amount && !BEAM_PARTIAL_METHODS.includes(charge.paymentMethodType)) {
|
|
2063
|
+
return {
|
|
2064
|
+
ok: false,
|
|
2065
|
+
status: 409,
|
|
2066
|
+
code: "refund_partial_not_allowed",
|
|
2067
|
+
message: `${charge.paymentMethodType} charges can only be refunded in full`
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
return { ok: true, amount };
|
|
2071
|
+
}
|
|
2072
|
+
function toBeamRefundSummary(refund) {
|
|
2073
|
+
return {
|
|
2074
|
+
refundId: refund.refundId,
|
|
2075
|
+
chargeId: refund.chargeId,
|
|
2076
|
+
amount: refund.amount,
|
|
2077
|
+
currency: refund.currency,
|
|
2078
|
+
status: refund.status,
|
|
2079
|
+
failureCode: refund.failureCode,
|
|
2080
|
+
reason: refund.reason,
|
|
2081
|
+
createdAt: refund.createdAt
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
function newBeamRefund(input) {
|
|
2085
|
+
state.beamRefundCounter += 1;
|
|
2086
|
+
const refund = {
|
|
2087
|
+
refundId: `rf_sim_${state.beamRefundCounter}`,
|
|
2088
|
+
chargeId: input.charge.id,
|
|
2089
|
+
amount: input.amount,
|
|
2090
|
+
currency: input.charge.currency,
|
|
2091
|
+
status: input.origin === "dashboard" ? "SUCCEEDED" : "PENDING",
|
|
2092
|
+
failureCode: null,
|
|
2093
|
+
reason: input.reason ?? null,
|
|
2094
|
+
origin: input.origin,
|
|
2095
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2096
|
+
};
|
|
2097
|
+
state.beamRefunds.set(refund.refundId, refund);
|
|
2098
|
+
console.log(
|
|
2099
|
+
`[stardeck-testing] beam refund ${refund.refundId} created charge=${refund.chargeId} amount=${refund.amount} origin=${refund.origin}`
|
|
2100
|
+
);
|
|
2101
|
+
return refund;
|
|
2102
|
+
}
|
|
2103
|
+
function validateBeamRefund(body) {
|
|
2104
|
+
const checks = [
|
|
2105
|
+
[isValidDeploymentId(body.deploymentId), "Invalid deployment ID"],
|
|
2106
|
+
[hasValidKey(body.idempotencyKey), "idempotencyKey is required (1 to 255 characters)"],
|
|
2107
|
+
[isOptional(body.amount, isPositiveInt), "amount must be a positive integer in satang"],
|
|
2108
|
+
[
|
|
2109
|
+
isOptional(body.reason, (v) => typeof v === "string" && v.length <= 500),
|
|
2110
|
+
"reason must be at most 500 characters"
|
|
2111
|
+
]
|
|
2112
|
+
];
|
|
2113
|
+
const failed = checks.find(([ok]) => !ok);
|
|
2114
|
+
return failed ? failed[1] : null;
|
|
2115
|
+
}
|
|
2116
|
+
function createBeamRefund(body, charge, merchant) {
|
|
2117
|
+
const scopedKey = JSON.stringify(["beam-refund", merchant, body.idempotencyKey]);
|
|
2118
|
+
const fingerprint = stableStringify({
|
|
2119
|
+
chargeId: charge.id,
|
|
2120
|
+
amount: body.amount,
|
|
2121
|
+
reason: body.reason
|
|
2122
|
+
});
|
|
2123
|
+
const saved = state.beamIdempotentRefunds.get(scopedKey);
|
|
2124
|
+
if (saved) {
|
|
2125
|
+
if (saved.fingerprint !== fingerprint) {
|
|
2126
|
+
return err(
|
|
2127
|
+
"This idempotency key was already used with different parameters. Use a new key for a different refund.",
|
|
2128
|
+
409,
|
|
2129
|
+
"idempotency_conflict"
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
return json({ refund: toBeamRefundSummary(state.beamRefunds.get(saved.id)) });
|
|
2133
|
+
}
|
|
2134
|
+
const plan = planBeamRefund(charge, body.amount);
|
|
2135
|
+
if (!plan.ok) return refundFailure(plan);
|
|
2136
|
+
const refund = newBeamRefund({
|
|
2137
|
+
charge,
|
|
2138
|
+
amount: plan.amount,
|
|
2139
|
+
origin: "app",
|
|
2140
|
+
reason: typeof body.reason === "string" ? body.reason : null
|
|
2141
|
+
});
|
|
2142
|
+
state.beamIdempotentRefunds.set(scopedKey, { fingerprint, id: refund.refundId });
|
|
2143
|
+
return json({ refund: toBeamRefundSummary(refund) });
|
|
2144
|
+
}
|
|
2145
|
+
var BEAM_REFUND_PATH = /^\/api\/store\/beam\/([^/]+)\/(?:charges\/([^/]+)\/refunds|refunds\/([^/]+))$/;
|
|
2146
|
+
async function handleBeamRefundRequest(request, url) {
|
|
2147
|
+
const match = url.pathname.match(BEAM_REFUND_PATH);
|
|
2148
|
+
if (!match) return null;
|
|
2149
|
+
const [, merchant, chargeId, refundId] = match;
|
|
2150
|
+
const isCreate = request.method === "POST" && chargeId !== void 0;
|
|
2151
|
+
const isRead = request.method === "GET" && refundId !== void 0;
|
|
2152
|
+
if (!isCreate && !isRead) return null;
|
|
2153
|
+
if (isRead) {
|
|
2154
|
+
if (!url.searchParams.get("deploymentId")) {
|
|
2155
|
+
return err("deploymentId is required", 400, "invalid_request");
|
|
2156
|
+
}
|
|
2157
|
+
const refund = state.beamRefunds.get(decode(refundId));
|
|
2158
|
+
if (!refund) return err("Refund not found", 404, "refund_not_found");
|
|
2159
|
+
return json({ refund: toBeamRefundSummary(refund) });
|
|
2160
|
+
}
|
|
2161
|
+
const body = await readJsonBody(request);
|
|
2162
|
+
const problem = validateBeamRefund(body);
|
|
2163
|
+
if (problem) return err(problem, 400, "invalid_request");
|
|
2164
|
+
const charge = state.directCharges.get(decode(chargeId));
|
|
2165
|
+
if (!charge) return err("No such charge for this app", 404, "payment_not_owned");
|
|
2166
|
+
return createBeamRefund(body, charge, decode(merchant));
|
|
2167
|
+
}
|
|
2168
|
+
function findCheckoutSessionByIntent(url, projectId) {
|
|
2169
|
+
if (!projectId) return err("Missing authentication", 401, "unauthorized");
|
|
2170
|
+
if (!url.searchParams.get("deploymentId")) {
|
|
2171
|
+
return err("deploymentId is required", 400, "invalid_request");
|
|
2172
|
+
}
|
|
2173
|
+
const intentId = url.searchParams.get("paymentIntentId");
|
|
2174
|
+
if (!intentId) return err("paymentIntentId is required", 400, "invalid_request");
|
|
2175
|
+
const intent = state.stripePaymentIntents.get(intentId);
|
|
2176
|
+
if (intent && intent.projectId !== projectId) return json({ session: null });
|
|
2177
|
+
const session = [...state.sessionStatuses.values()].find((s) => s.paymentIntentId === intentId);
|
|
2178
|
+
return json({ session: session ?? null });
|
|
2179
|
+
}
|
|
2180
|
+
|
|
1819
2181
|
// src/simulator/webhook-signing.ts
|
|
1820
2182
|
var import_node_crypto4 = __toESM(require("crypto"));
|
|
1821
2183
|
|
|
@@ -1861,6 +2223,15 @@ function paymentOperationForRequest(request, pathname) {
|
|
|
1861
2223
|
if (request.method === "GET" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout-sessions\/[^/]+$/.test(pathname)) {
|
|
1862
2224
|
return "stripe.getCheckoutSessionStatus";
|
|
1863
2225
|
}
|
|
2226
|
+
if (request.method === "POST" && /^\/api\/store\/beam\/[^/]+\/charges\/[^/]+\/refunds$/.test(pathname)) {
|
|
2227
|
+
return "beam.refundCharge";
|
|
2228
|
+
}
|
|
2229
|
+
if (request.method === "GET" && /^\/api\/store\/beam\/[^/]+\/refunds\/[^/]+$/.test(pathname)) {
|
|
2230
|
+
return "beam.getRefund";
|
|
2231
|
+
}
|
|
2232
|
+
if (request.method === "GET" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout-sessions$/.test(pathname)) {
|
|
2233
|
+
return "stripe.findCheckoutSessionByIntent";
|
|
2234
|
+
}
|
|
1864
2235
|
return stripeInlineOperation(request.method, pathname);
|
|
1865
2236
|
}
|
|
1866
2237
|
var STRIPE_INLINE_OPERATIONS = [
|
|
@@ -1872,6 +2243,12 @@ var STRIPE_INLINE_OPERATIONS = [
|
|
|
1872
2243
|
/^\/api\/store\/(?!beam\/)[^/]+\/payment-intents\/[^/]+\/cancel$/,
|
|
1873
2244
|
"stripe.cancelPaymentIntent"
|
|
1874
2245
|
],
|
|
2246
|
+
[
|
|
2247
|
+
"POST",
|
|
2248
|
+
/^\/api\/store\/(?!beam\/)[^/]+\/payment-intents\/[^/]+\/refunds$/,
|
|
2249
|
+
"stripe.refundPaymentIntent"
|
|
2250
|
+
],
|
|
2251
|
+
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/refunds\/[^/]+$/, "stripe.getRefund"],
|
|
1875
2252
|
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/elements-config$/, "stripe.getElementsConfig"],
|
|
1876
2253
|
["GET", /^\/api\/store\/(?!beam\/)[^/]+\/readiness$/, "stripe.getReadiness"]
|
|
1877
2254
|
];
|
|
@@ -1952,7 +2329,8 @@ function seedStripeSession(id, body, expiresAt) {
|
|
|
1952
2329
|
currency,
|
|
1953
2330
|
customerEmail: body.customerEmail ? String(body.customerEmail) : null,
|
|
1954
2331
|
metadata: body.metadata ?? {},
|
|
1955
|
-
expiresAt
|
|
2332
|
+
expiresAt,
|
|
2333
|
+
paymentIntentId: null
|
|
1956
2334
|
});
|
|
1957
2335
|
}
|
|
1958
2336
|
function seedBeamLink(id, body, merchantId) {
|
|
@@ -2027,6 +2405,10 @@ async function handlePaymentsRequest(request, url, signedProjectId = null) {
|
|
|
2027
2405
|
signedProjectId
|
|
2028
2406
|
);
|
|
2029
2407
|
if (inlineStripeResponse) return inlineStripeResponse;
|
|
2408
|
+
const stripeRefundResponse = await handleStripeRefundRequest(request, url, signedProjectId);
|
|
2409
|
+
if (stripeRefundResponse) return stripeRefundResponse;
|
|
2410
|
+
const beamRefundResponse = await handleBeamRefundRequest(request, url);
|
|
2411
|
+
if (beamRefundResponse) return beamRefundResponse;
|
|
2030
2412
|
const directChargeResponse = await handleDirectChargeRequest(request, pathname);
|
|
2031
2413
|
if (directChargeResponse) return directChargeResponse;
|
|
2032
2414
|
if (/\/billing-portal$/.test(pathname)) {
|
|
@@ -2348,6 +2730,9 @@ async function handlePaymentsRequest(request, url, signedProjectId = null) {
|
|
|
2348
2730
|
if (scopedKey) state.idempotentCheckouts.set(scopedKey, { fingerprint, response });
|
|
2349
2731
|
return json(response);
|
|
2350
2732
|
}
|
|
2733
|
+
if (subPath === "checkout-sessions" && request.method === "GET") {
|
|
2734
|
+
return findCheckoutSessionByIntent(url, signedProjectId);
|
|
2735
|
+
}
|
|
2351
2736
|
const sessionMatch = subPath.match(/^checkout-sessions\/([^/]+)$/);
|
|
2352
2737
|
if (sessionMatch && request.method === "GET") {
|
|
2353
2738
|
const session = state.sessionStatuses.get(sessionMatch[1]);
|
|
@@ -3010,6 +3395,91 @@ async function handleEdgeRequest(request, subPath) {
|
|
|
3010
3395
|
return edgeFailure(`No edge simulator for ${method} .../edge${subPath}`, 404);
|
|
3011
3396
|
}
|
|
3012
3397
|
|
|
3398
|
+
// src/simulator/controls.ts
|
|
3399
|
+
var USER_TOKEN_HEADER = "x-stardeck-user-token";
|
|
3400
|
+
var REFUSAL_STATUS = {
|
|
3401
|
+
NOT_WRITABLE: 400,
|
|
3402
|
+
INVALID_VALUE: 400,
|
|
3403
|
+
UNAUTHENTICATED: 401,
|
|
3404
|
+
FORBIDDEN: 403,
|
|
3405
|
+
WRITE_NOT_ALLOWED_HERE: 403,
|
|
3406
|
+
PROVIDER_NOT_READY: 409,
|
|
3407
|
+
UNAVAILABLE: 503
|
|
3408
|
+
};
|
|
3409
|
+
var controlsModule = null;
|
|
3410
|
+
function loadControls() {
|
|
3411
|
+
controlsModule ??= import("@stardeck-customer-apps/controls");
|
|
3412
|
+
return controlsModule;
|
|
3413
|
+
}
|
|
3414
|
+
function currentControlsBody() {
|
|
3415
|
+
const controls = {};
|
|
3416
|
+
for (const [key, value] of Object.entries(state.controls.values)) {
|
|
3417
|
+
controls[key] = state.controls.unavailable.has(key) ? { ok: false, reason: "unavailable (simulated)" } : { ok: true, value };
|
|
3418
|
+
}
|
|
3419
|
+
return controls;
|
|
3420
|
+
}
|
|
3421
|
+
function refusalResponse(refusal) {
|
|
3422
|
+
return json({ error: refusal }, REFUSAL_STATUS[refusal.code]);
|
|
3423
|
+
}
|
|
3424
|
+
function handleSync(body) {
|
|
3425
|
+
const usage = body.usage;
|
|
3426
|
+
if (typeof usage === "object" && usage !== null) {
|
|
3427
|
+
const locations = usage.locations;
|
|
3428
|
+
state.controls.lastUsage = typeof locations === "number" && Number.isInteger(locations) && locations >= 0 ? { locations } : {};
|
|
3429
|
+
}
|
|
3430
|
+
return json({ controls: currentControlsBody() });
|
|
3431
|
+
}
|
|
3432
|
+
async function handleWrite(request, key) {
|
|
3433
|
+
const { CONTROLS, isWritableControl } = await loadControls();
|
|
3434
|
+
if (!isWritableControl(key)) {
|
|
3435
|
+
return refusalResponse({
|
|
3436
|
+
code: "NOT_WRITABLE",
|
|
3437
|
+
message: `${key} cannot be changed from the app`
|
|
3438
|
+
});
|
|
3439
|
+
}
|
|
3440
|
+
const userToken = request.headers.get(USER_TOKEN_HEADER);
|
|
3441
|
+
if (!userToken) {
|
|
3442
|
+
return refusalResponse({
|
|
3443
|
+
code: "UNAUTHENTICATED",
|
|
3444
|
+
message: "A signed-in person is required (X-Stardeck-User-Token)"
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
const body = await readJsonBody(request);
|
|
3448
|
+
const parsed = CONTROLS[key].schema.safeParse(body.value);
|
|
3449
|
+
if (!parsed.success) {
|
|
3450
|
+
return refusalResponse({
|
|
3451
|
+
code: "INVALID_VALUE",
|
|
3452
|
+
message: parsed.error.issues[0]?.message ?? "Invalid value"
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
const refusal = state.controls.refusal;
|
|
3456
|
+
if (refusal) {
|
|
3457
|
+
state.controls.refusal = null;
|
|
3458
|
+
return refusalResponse(refusal);
|
|
3459
|
+
}
|
|
3460
|
+
state.controls.values[key] = parsed.data;
|
|
3461
|
+
state.controls.unavailable.delete(key);
|
|
3462
|
+
state.controls.writes.push({ key, value: parsed.data, userToken });
|
|
3463
|
+
return json({ controls: currentControlsBody() });
|
|
3464
|
+
}
|
|
3465
|
+
var CONTROLS_PATH = /^\/api\/deployments\/[^/]+\/controls(?:\/([^/]+))?$/;
|
|
3466
|
+
async function handleControlsRequest(request, url) {
|
|
3467
|
+
const match = url.pathname.match(CONTROLS_PATH);
|
|
3468
|
+
const key = match?.[1] ? decodeURIComponent(match[1]) : null;
|
|
3469
|
+
if (!key && request.method === "POST") {
|
|
3470
|
+
return handleSync(await readJsonBody(request));
|
|
3471
|
+
}
|
|
3472
|
+
if (key && request.method === "PUT") {
|
|
3473
|
+
return handleWrite(request, key);
|
|
3474
|
+
}
|
|
3475
|
+
return json(
|
|
3476
|
+
{
|
|
3477
|
+
error: { code: "UNAVAILABLE", message: `No simulator for ${request.method} ${url.pathname}` }
|
|
3478
|
+
},
|
|
3479
|
+
404
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3013
3483
|
// src/simulator/schedules.ts
|
|
3014
3484
|
async function handleScheduleWake(request, scheduleKey) {
|
|
3015
3485
|
if (request.method !== "POST") return failure(`Method ${request.method} not allowed`, 405);
|
|
@@ -3158,8 +3628,10 @@ function requiresDeploymentHmac(request, url) {
|
|
|
3158
3628
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
3159
3629
|
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
3160
3630
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
3161
|
-
|
|
3631
|
+
const controlsMatch = url.pathname.match(CONTROLS_PATH2);
|
|
3632
|
+
return !!(controlsMatch || isStarlens || scheduleWakeMatch || dataStoreMatch || isEmail2 || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
3162
3633
|
}
|
|
3634
|
+
var CONTROLS_PATH2 = /^\/api\/deployments\/[^/]+\/controls(\/[^/]+)?$/;
|
|
3163
3635
|
var SCHEDULE_WAKE_PATH = /^\/api\/deployments\/[^/]+\/schedules\/([^/]+)\/wake$/;
|
|
3164
3636
|
function deploymentIdFromPath(url) {
|
|
3165
3637
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
@@ -3227,6 +3699,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3227
3699
|
if (edgeMatch) {
|
|
3228
3700
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
3229
3701
|
}
|
|
3702
|
+
if (CONTROLS_PATH2.test(url.pathname)) {
|
|
3703
|
+
return handleControlsRequest(request, url);
|
|
3704
|
+
}
|
|
3230
3705
|
const scheduleWakeMatch = url.pathname.match(SCHEDULE_WAKE_PATH);
|
|
3231
3706
|
if (scheduleWakeMatch) {
|
|
3232
3707
|
return handleScheduleWake(request, decodeURIComponent(scheduleWakeMatch[1]));
|
|
@@ -3255,7 +3730,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
3255
3730
|
}
|
|
3256
3731
|
}
|
|
3257
3732
|
return failure(
|
|
3258
|
-
`[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.`,
|
|
3733
|
+
`[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.`,
|
|
3259
3734
|
404
|
|
3260
3735
|
);
|
|
3261
3736
|
}
|