@firedrill-tools/stripe 0.1.1

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.
Files changed (59) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +433 -0
  3. package/firedrill/agent.target.json +17 -0
  4. package/firedrill/api-unavailable.scenario.json +11 -0
  5. package/firedrill/baseline.scenario.json +5 -0
  6. package/firedrill/conformance.suite.json +17 -0
  7. package/firedrill/rate-limited.scenario.json +11 -0
  8. package/firedrill/refund-committed-lost.scenario.json +11 -0
  9. package/firedrill/stripe-api-unavailable.drill.json +463 -0
  10. package/firedrill/stripe-denied.drill.json +74 -0
  11. package/firedrill/stripe-large-pages.drill.json +153 -0
  12. package/firedrill/stripe-live-mode.drill.json +136 -0
  13. package/firedrill/stripe-mcp-aliases.drill.json +406 -0
  14. package/firedrill/stripe-no-permissions.drill.json +1143 -0
  15. package/firedrill/stripe-rate-limited.drill.json +616 -0
  16. package/firedrill/stripe-refund-committed-lost.drill.json +139 -0
  17. package/firedrill/stripe-rest-flow.drill.json +1401 -0
  18. package/firedrill/stripe-restricted-key.drill.json +171 -0
  19. package/firedrill/tools/stripe/app/assets/ATTRIBUTION.md +36 -0
  20. package/firedrill/tools/stripe/app/assets/fonts/OFL.txt +93 -0
  21. package/firedrill/tools/stripe/app/assets/stripe-s.svg +1 -0
  22. package/firedrill/tools/stripe/app/assets/stripe.svg +1 -0
  23. package/firedrill/tools/stripe/app/site/app.js +456 -0
  24. package/firedrill/tools/stripe/app/site/assets/fonts/inter-latin.woff2 +0 -0
  25. package/firedrill/tools/stripe/app/site/assets/stripe-s.svg +1 -0
  26. package/firedrill/tools/stripe/app/site/assets/stripe.svg +1 -0
  27. package/firedrill/tools/stripe/app/site/icons.js +90 -0
  28. package/firedrill/tools/stripe/app/site/index.html +137 -0
  29. package/firedrill/tools/stripe/app/site/pages-billing.js +902 -0
  30. package/firedrill/tools/stripe/app/site/pages-catalog.js +314 -0
  31. package/firedrill/tools/stripe/app/site/pages-customers.js +416 -0
  32. package/firedrill/tools/stripe/app/site/pages-home.js +373 -0
  33. package/firedrill/tools/stripe/app/site/pages-payments.js +502 -0
  34. package/firedrill/tools/stripe/app/site/store.js +99 -0
  35. package/firedrill/tools/stripe/app/site/styles.css +2512 -0
  36. package/firedrill/tools/stripe/app/site/ui.js +767 -0
  37. package/firedrill/tools/stripe/app/site/widgets.js +707 -0
  38. package/firedrill/tools/stripe/behavior.mjs +148 -0
  39. package/firedrill/tools/stripe/lib/cards.mjs +53 -0
  40. package/firedrill/tools/stripe/lib/form.mjs +204 -0
  41. package/firedrill/tools/stripe/lib/ids.mjs +85 -0
  42. package/firedrill/tools/stripe/lib/money.mjs +35 -0
  43. package/firedrill/tools/stripe/lib/objects.mjs +229 -0
  44. package/firedrill/tools/stripe/lib/periods.mjs +41 -0
  45. package/firedrill/tools/stripe/lib/size.mjs +55 -0
  46. package/firedrill/tools/stripe/lib/state.mjs +230 -0
  47. package/firedrill/tools/stripe/lib/validate.mjs +184 -0
  48. package/firedrill/tools/stripe/lib/wire.mjs +98 -0
  49. package/firedrill/tools/stripe/ops/billing.mjs +914 -0
  50. package/firedrill/tools/stripe/ops/catalog.mjs +203 -0
  51. package/firedrill/tools/stripe/ops/customers.mjs +241 -0
  52. package/firedrill/tools/stripe/ops/dashboard.mjs +29 -0
  53. package/firedrill/tools/stripe/ops/payments.mjs +608 -0
  54. package/firedrill/tools/stripe/stripe.tool.json +28833 -0
  55. package/firedrill/world.json +8527 -0
  56. package/firedrill.json +5 -0
  57. package/package.json +64 -0
  58. package/starter.json +7999 -0
  59. package/test/conformance.mjs +1133 -0
@@ -0,0 +1,184 @@
1
+ // Parameter validation shared by the operation handlers: e-mail addresses, metadata maps, addresses, shipping
2
+ // blocks, currencies and amounts. Every failure is a declared `INVALID_REQUEST` in Stripe's wording.
3
+
4
+ import { CURRENCIES, MAX_CHARGE_AMOUNT, MIN_CHARGE_AMOUNT, formatAmount, isCurrency, isInteger, normalizeCurrency } from "./money.mjs";
5
+ import { invalid, invalidEmpty, invalidInteger, parameterMissing, parameterUnknown } from "./state.mjs";
6
+
7
+ export const MAX_METADATA_KEYS = 50;
8
+ export const MAX_METADATA_KEY = 40;
9
+ export const MAX_METADATA_VALUE = 500;
10
+ const ADDRESS_FIELDS = ["city", "country", "line1", "line2", "postal_code", "state"];
11
+
12
+ export function isPlainObject(value) {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ }
15
+
16
+ /** `""` clears a nullable string parameter (Stripe); undefined keeps the stored value. */
17
+ export function optionalString(context, input, name, current, maxLength = 5_000) {
18
+ const value = input[name];
19
+ if (value === undefined) return current;
20
+ if (value === null) return null;
21
+ if (typeof value !== "string") return invalid(context, `Invalid string: ${String(value)}`, "parameter_invalid_string", name);
22
+ if (value.length > maxLength) return invalid(context, `The '${name}' parameter cannot be longer than ${maxLength} characters.`, "parameter_invalid_string", name);
23
+ return value.length === 0 ? null : value;
24
+ }
25
+
26
+ export function requireString(context, input, name, maxLength = 5_000) {
27
+ const value = input[name];
28
+ if (value === undefined || value === null) return parameterMissing(context, name);
29
+ if (typeof value !== "string") return invalid(context, `Invalid string: ${String(value)}`, "parameter_invalid_string", name);
30
+ if (value.length === 0) return invalidEmpty(context, name);
31
+ if (value.length > maxLength) return invalid(context, `The '${name}' parameter cannot be longer than ${maxLength} characters.`, "parameter_invalid_string", name);
32
+ return value;
33
+ }
34
+
35
+ export function optionalEnum(context, input, name, values, current) {
36
+ const value = input[name];
37
+ if (value === undefined) return current;
38
+ if (!values.includes(value)) return invalid(context, `Invalid ${name}: must be one of ${values.join(", ")}`, "parameter_invalid", name);
39
+ return value;
40
+ }
41
+
42
+ export function optionalBoolean(context, input, name, current) {
43
+ const value = input[name];
44
+ if (value === undefined) return current;
45
+ if (typeof value !== "boolean") return invalid(context, `Invalid boolean: ${String(value)}`, "parameter_invalid_boolean", name);
46
+ return value;
47
+ }
48
+
49
+ export function optionalInteger(context, input, name, current, { min, max } = {}) {
50
+ const value = input[name];
51
+ if (value === undefined) return current;
52
+ if (!isInteger(value)) return invalidInteger(context, name, value);
53
+ if (min !== undefined && value < min) return invalid(context, `Invalid integer: ${value}. ${name} must be at least ${min}.`, "parameter_invalid_integer", name);
54
+ if (max !== undefined && value > max) return invalid(context, `Invalid integer: ${value}. ${name} must be at most ${max}.`, "parameter_invalid_integer", name);
55
+ return value;
56
+ }
57
+
58
+ export function requireInteger(context, input, name, bounds) {
59
+ if (input[name] === undefined || input[name] === null) return parameterMissing(context, name);
60
+ return optionalInteger(context, input, name, undefined, bounds);
61
+ }
62
+
63
+ export function validateEmail(context, value, name = "email") {
64
+ if (value === null) return null;
65
+ const match = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) && value.length <= 512;
66
+ return match ? value : invalid(context, `Invalid email address: ${value}`, "email_invalid", name);
67
+ }
68
+
69
+ export function requireCurrency(context, value, name = "currency") {
70
+ if (value === undefined || value === null) return parameterMissing(context, name);
71
+ if (typeof value !== "string" || value.length === 0) return invalidEmpty(context, name);
72
+ if (!isCurrency(value)) return invalid(context, `Invalid currency: ${value}. Stripe currently supports these currencies: ${CURRENCIES.join(", ")}`, "parameter_invalid", name);
73
+ return normalizeCurrency(value);
74
+ }
75
+
76
+ /** Charge-able amount: integer, ≥ 50 minor units (every currency), ≤ 99,999,999. */
77
+ export function requireChargeAmount(context, value, currency, name = "amount") {
78
+ if (value === undefined || value === null) return parameterMissing(context, name);
79
+ if (!isInteger(value)) return invalidInteger(context, name, value);
80
+ if (value < MIN_CHARGE_AMOUNT) {
81
+ return invalid(context, `Amount must be at least ${formatAmount(MIN_CHARGE_AMOUNT, currency)} ${currency}`, "amount_too_small", name);
82
+ }
83
+ if (value > MAX_CHARGE_AMOUNT) {
84
+ return invalid(context, `Amount must be no more than ${formatAmount(MAX_CHARGE_AMOUNT, currency)} ${currency}`, "amount_too_large", name);
85
+ }
86
+ return value;
87
+ }
88
+
89
+ /** Validate an incoming metadata map and merge it over `current` (`""` deletes a key). */
90
+ export function mergeMetadata(context, incoming, current = {}) {
91
+ if (incoming === undefined) return { ...current };
92
+ if (incoming === "" || incoming === null) return {};
93
+ if (!isPlainObject(incoming)) return invalid(context, "Invalid metadata: must be a hash of string keys and string values.", "parameter_invalid", "metadata");
94
+ const next = { ...current };
95
+ for (const [key, value] of Object.entries(incoming)) {
96
+ if (key === "__proto__" || key === "constructor" || key === "prototype") return parameterUnknown(context, `metadata[${key}]`);
97
+ if (key.length === 0 || key.length > MAX_METADATA_KEY) {
98
+ return invalid(context, `Metadata keys can be at most ${MAX_METADATA_KEY} characters.`, "parameter_invalid", `metadata[${key}]`);
99
+ }
100
+ if (typeof value !== "string") return invalid(context, "Metadata values must be strings.", "parameter_invalid", `metadata[${key}]`);
101
+ if (value.length > MAX_METADATA_VALUE) return invalid(context, `Metadata values can be at most ${MAX_METADATA_VALUE} characters.`, "parameter_invalid", `metadata[${key}]`);
102
+ if (value.length === 0) delete next[key];
103
+ else next[key] = value;
104
+ }
105
+ if (Object.keys(next).length > MAX_METADATA_KEYS) {
106
+ return invalid(context, `You can specify up to ${MAX_METADATA_KEYS} keys, with key names up to ${MAX_METADATA_KEY} characters long and values up to ${MAX_METADATA_VALUE} characters long.`, "parameter_invalid", "metadata");
107
+ }
108
+ return next;
109
+ }
110
+
111
+ /** `{ city, country, line1, line2, postal_code, state }` with nulls for absent fields; `""` clears the address. */
112
+ export function normalizeAddress(context, value, name = "address", current = null) {
113
+ if (value === undefined) return current;
114
+ if (value === null || value === "") return null;
115
+ if (!isPlainObject(value)) return invalid(context, `Invalid ${name}: must be a hash.`, "parameter_invalid", name);
116
+ const address = {};
117
+ for (const [key, entry] of Object.entries(value)) {
118
+ if (!ADDRESS_FIELDS.includes(key)) return parameterUnknown(context, `${name}[${key}]`);
119
+ if (entry !== null && typeof entry !== "string") return invalid(context, `Invalid string: ${String(entry)}`, "parameter_invalid_string", `${name}[${key}]`);
120
+ }
121
+ for (const field of ADDRESS_FIELDS) {
122
+ const entry = value[field];
123
+ address[field] = entry === undefined || entry === null || entry === "" ? null : entry;
124
+ }
125
+ if (address.country !== null && !/^[A-Za-z]{2}$/.test(address.country)) {
126
+ return invalid(context, `Invalid country: ${address.country}. Use a two-letter ISO 3166-1 alpha-2 code.`, "parameter_invalid", `${name}[country]`);
127
+ }
128
+ if (address.country !== null) address.country = address.country.toUpperCase();
129
+ return address;
130
+ }
131
+
132
+ export function normalizeShipping(context, value, current = null) {
133
+ if (value === undefined) return current;
134
+ if (value === null || value === "") return null;
135
+ if (!isPlainObject(value)) return invalid(context, "Invalid shipping: must be a hash.", "parameter_invalid", "shipping");
136
+ for (const key of Object.keys(value)) if (!["address", "name", "phone"].includes(key)) return parameterUnknown(context, `shipping[${key}]`);
137
+ if (typeof value.name !== "string" || value.name.length === 0) return parameterMissing(context, "shipping[name]");
138
+ if (value.address === undefined) return parameterMissing(context, "shipping[address]");
139
+ const address = normalizeAddress(context, value.address, "shipping[address]");
140
+ if (address === null) return parameterMissing(context, "shipping[address]");
141
+ return { address, name: value.name, phone: typeof value.phone === "string" && value.phone.length > 0 ? value.phone : null };
142
+ }
143
+
144
+ export function optionalStringArray(context, input, name, current) {
145
+ const value = input[name];
146
+ if (value === undefined) return current;
147
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
148
+ return invalid(context, `Invalid array: ${name} must be an array of strings.`, "parameter_invalid", name);
149
+ }
150
+ return value;
151
+ }
152
+
153
+ /** Exactly one of the listed parameters must be present. */
154
+ export function requireExactlyOne(context, input, names) {
155
+ const present = names.filter((name) => input[name] !== undefined);
156
+ if (present.length === 1) return present[0];
157
+ if (present.length === 0) return parameterMissing(context, names[0]);
158
+ return invalid(context, `You may only specify one of these parameters: ${names.join(", ")}.`, "parameter_invalid", present[1]);
159
+ }
160
+
161
+ /**
162
+ * The framework decodes query strings and form bodies leniently, so malformed percent-encoding (`%E0%A4%A`) reaches
163
+ * a Tool as U+FFFD instead of failing the request. A filter value carrying U+FFFD is a mangled request, not a value
164
+ * that legitimately matches nothing, so every list filter rejects it with Stripe's 400 `invalid_request_error`
165
+ * rather than silently running a corrupted filter that returns an empty page. `%ZZ` (which stays literal) and
166
+ * correctly encoded non-ASCII text (`caf%C3%A9`, CJK, emoji) are unaffected.
167
+ */
168
+ export function rejectMangled(context, input, names) {
169
+ for (const name of names) {
170
+ const value = input[name];
171
+ const values = Array.isArray(value) ? value : [value];
172
+ for (const entry of values) {
173
+ if (typeof entry === "string" && entry.includes("�")) {
174
+ return invalid(
175
+ context,
176
+ `Invalid ${name}: the value contains an invalid character (U+FFFD); check the percent-encoding of the request.`,
177
+ "parameter_invalid",
178
+ name,
179
+ );
180
+ }
181
+ }
182
+ }
183
+ return undefined;
184
+ }
@@ -0,0 +1,98 @@
1
+ // Stripe REST wire helpers: the error envelope and the response headers. Pure functions only, so the HTTP
2
+ // codecs can use them without state or a clock. Statuses are framework-owned (declared per route).
3
+
4
+ export const API_VERSION = "2026-08-26.dahlia";
5
+
6
+ const TYPES = {
7
+ INVALID_REQUEST: "invalid_request_error",
8
+ INVALID_STATE: "invalid_request_error",
9
+ RESOURCE_MISSING: "invalid_request_error",
10
+ PERMISSION_DENIED: "invalid_request_error",
11
+ RATE_LIMITED: "invalid_request_error",
12
+ CARD_DECLINED: "card_error",
13
+ API_ERROR: "api_error",
14
+ };
15
+
16
+ export function docUrl(code) {
17
+ return `https://stripe.com/docs/error-codes/${code.replace(/_/g, "-")}`;
18
+ }
19
+
20
+ function requestLogUrl(correlationId) {
21
+ return `https://dashboard.stripe.test/logs/req_${correlationId}`;
22
+ }
23
+
24
+ /** Response decoration every Stripe response carries; `Idempotency-Key` is echoed when the request sent one. */
25
+ export function responseHeaders(invocation) {
26
+ const headers = {
27
+ "request-id": `req_${invocation.correlationId}`,
28
+ "stripe-version": API_VERSION,
29
+ };
30
+ if (typeof invocation.idempotencyKey === "string") headers["idempotency-key"] = invocation.idempotencyKey;
31
+ return headers;
32
+ }
33
+
34
+ /** `{ error: { ... } }` for a non-ok outcome, in Stripe's shape. */
35
+ export function stripeError(outcome, correlationId) {
36
+ const error = outcome.error ?? {};
37
+ const message = typeof error.message === "string" && error.message.length > 0 ? error.message : "An unknown error occurred.";
38
+ const base = { request_log_url: requestLogUrl(correlationId) };
39
+ if (outcome.status === "denied") {
40
+ return {
41
+ error: {
42
+ ...base,
43
+ type: "invalid_request_error",
44
+ message: "This API key is not granted the requested operation in this Firedrill world. Grant the operation to the calling actor to continue.",
45
+ },
46
+ };
47
+ }
48
+ if (outcome.status === "unsupported") {
49
+ return { error: { ...base, type: "invalid_request_error", message: `Unrecognized request: ${message}` } };
50
+ }
51
+ if (outcome.status === "invalid") {
52
+ // Framework schema rejection: its message ("arguments do not match stripe.<operation>") names no property,
53
+ // so no `param` can be derived (README "Wire format").
54
+ return {
55
+ error: {
56
+ ...base,
57
+ type: "invalid_request_error",
58
+ code: "parameter_invalid",
59
+ message: `Invalid request: ${message}`,
60
+ doc_url: docUrl("parameter_invalid"),
61
+ },
62
+ };
63
+ }
64
+ const code = String(error.code ?? "").replace(/^tool\./, "");
65
+ const details = typeof error.details === "object" && error.details !== null ? error.details : {};
66
+ const body = { ...base, type: TYPES[code] ?? "api_error", message };
67
+ if (code === "RATE_LIMITED") {
68
+ body.code = "rate_limit";
69
+ body.doc_url = docUrl("rate_limit");
70
+ }
71
+ if (typeof details.code === "string") {
72
+ body.code = details.code;
73
+ body.doc_url = docUrl(details.code);
74
+ }
75
+ if (typeof details.param === "string") body.param = details.param;
76
+ if (typeof details.decline_code === "string") body.decline_code = details.decline_code;
77
+ if (typeof details.payment_intent === "object" && details.payment_intent !== null) body.payment_intent = details.payment_intent;
78
+ if (typeof details.payment_method === "object" && details.payment_method !== null) body.payment_method = details.payment_method;
79
+ if (typeof details.advice_code === "string") body.advice_code = details.advice_code;
80
+ return { error: body };
81
+ }
82
+
83
+ /** The decoding error `behavior.mjs` carried under `key`, when the arguments hold exactly that shape. */
84
+ export function formError(args, key) {
85
+ if (typeof args !== "object" || args === null || Array.isArray(args)) return undefined;
86
+ const keys = Object.keys(args);
87
+ if (keys.length !== 1 || keys[0] !== key) return undefined;
88
+ const error = args[key];
89
+ if (typeof error !== "object" || error === null || typeof error.code !== "string" || typeof error.message !== "string") return undefined;
90
+ return error;
91
+ }
92
+
93
+ /** Stripe's 400 `invalid_request_error` for a form or query string the decoder could not map. */
94
+ export function formErrorEnvelope(error, correlationId) {
95
+ const body = { request_log_url: requestLogUrl(correlationId), type: "invalid_request_error", code: error.code, message: error.message, doc_url: docUrl(error.code) };
96
+ if (typeof error.param === "string") body.param = error.param;
97
+ return { error: body };
98
+ }