@yiaany/ghostapi 0.1.9 → 0.2.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/CHANGELOG.md +21 -0
- package/CONTRIBUTING.md +132 -119
- package/LICENSE +21 -21
- package/PROVENANCE.md +13 -0
- package/README.md +78 -84
- package/SECURITY.md +56 -53
- package/dist/actions/gateway.js +320 -72
- package/dist/actions/gateway.js.map +1 -1
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +1 -1
- package/dist/actions/index.js.map +1 -1
- package/dist/agents/agentPrompt.js +7 -5
- package/dist/agents/agentPrompt.js.map +1 -1
- package/dist/ai/aiClient.js +5 -5
- package/dist/ai/aiClient.js.map +1 -1
- package/dist/ai/aiGenerator.js +66 -26
- package/dist/ai/aiGenerator.js.map +1 -1
- package/dist/ai/genericInference.js +151 -36
- package/dist/ai/genericInference.js.map +1 -1
- package/dist/ai/prompts.js +55 -49
- package/dist/ai/prompts.js.map +1 -1
- package/dist/approvals/approvalInbox.js +649 -133
- package/dist/approvals/approvalInbox.js.map +1 -1
- package/dist/approvals/index.d.ts +2 -2
- package/dist/approvals/index.js +1 -1
- package/dist/approvals/index.js.map +1 -1
- package/dist/behavior/behaviorStore.js +34 -14
- package/dist/behavior/behaviorStore.js.map +1 -1
- package/dist/cache/index.js +14 -5
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/index.js +226 -93
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/openUrl.js +17 -3
- package/dist/cli/openUrl.js.map +1 -1
- package/dist/cli/parser.js +79 -18
- package/dist/cli/parser.js.map +1 -1
- package/dist/config/dataPaths.js +4 -2
- package/dist/config/dataPaths.js.map +1 -1
- package/dist/config/localConfig.js +5 -2
- package/dist/config/localConfig.js.map +1 -1
- package/dist/config/serverConfig.js +11 -5
- package/dist/config/serverConfig.js.map +1 -1
- package/dist/contracts/contract.js +311 -83
- package/dist/contracts/contract.js.map +1 -1
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/index.js.map +1 -1
- package/dist/credentials/credentialBroker.js +391 -79
- package/dist/credentials/credentialBroker.js.map +1 -1
- package/dist/credentials/index.d.ts +2 -2
- package/dist/credentials/index.js +1 -1
- package/dist/credentials/index.js.map +1 -1
- package/dist/dashboard/app.js +823 -685
- package/dist/dashboard/dashboard.js.map +1 -1
- package/dist/dashboard/index.html +704 -267
- package/dist/dashboard/styles.css +1040 -349
- package/dist/egress/capabilities.js +55 -25
- package/dist/egress/capabilities.js.map +1 -1
- package/dist/egress/linuxBootstrap.js +47 -11
- package/dist/egress/linuxBootstrap.js.map +1 -1
- package/dist/egress/run.js +170 -31
- package/dist/egress/run.js.map +1 -1
- package/dist/errors/errorEngine.js +15 -3
- package/dist/errors/errorEngine.js.map +1 -1
- package/dist/errors/providerErrors.js.map +1 -1
- package/dist/evals/evals.js +304 -78
- package/dist/evals/evals.js.map +1 -1
- package/dist/evals/index.d.ts +2 -2
- package/dist/evals/index.js +1 -1
- package/dist/evals/index.js.map +1 -1
- package/dist/evidence/index.d.ts +2 -2
- package/dist/evidence/index.js +1 -1
- package/dist/evidence/index.js.map +1 -1
- package/dist/evidence/report.d.ts +1 -1
- package/dist/evidence/report.js +263 -79
- package/dist/evidence/report.js.map +1 -1
- package/dist/fault/faultLab.js +24 -9
- package/dist/fault/faultLab.js.map +1 -1
- package/dist/index.d.ts +42 -42
- package/dist/index.js +21 -21
- package/dist/index.js.map +1 -1
- package/dist/inventory/analysis.d.ts +1 -1
- package/dist/inventory/analysis.js +174 -50
- package/dist/inventory/analysis.js.map +1 -1
- package/dist/inventory/index.d.ts +4 -4
- package/dist/inventory/index.js +3 -3
- package/dist/inventory/index.js.map +1 -1
- package/dist/inventory/inventoryController.js +362 -115
- package/dist/inventory/inventoryController.js.map +1 -1
- package/dist/inventory/types.js +680 -140
- package/dist/inventory/types.js.map +1 -1
- package/dist/ledger/actionLedger.js +355 -78
- package/dist/ledger/actionLedger.js.map +1 -1
- package/dist/ledger/index.d.ts +2 -2
- package/dist/ledger/index.js +1 -1
- package/dist/ledger/index.js.map +1 -1
- package/dist/mcp/server.js +26 -11
- package/dist/mcp/server.js.map +1 -1
- package/dist/policy/index.d.ts +2 -2
- package/dist/policy/index.js +1 -1
- package/dist/policy/index.js.map +1 -1
- package/dist/policy/policy.js +112 -32
- package/dist/policy/policy.js.map +1 -1
- package/dist/productTelemetry/index.js +71 -20
- package/dist/productTelemetry/index.js.map +1 -1
- package/dist/providers/conformance.js +10 -5
- package/dist/providers/conformance.js.map +1 -1
- package/dist/providers/discord.js +2 -2
- package/dist/providers/generic.js +3 -3
- package/dist/providers/github.js +2 -2
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.js +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.js +3 -3
- package/dist/providers/packs/resendPack.js +51 -25
- package/dist/providers/packs/resendPack.js.map +1 -1
- package/dist/providers/packs/stripePack.js +590 -121
- package/dist/providers/packs/stripePack.js.map +1 -1
- package/dist/providers/registry.js +4 -4
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/runtime.js +7 -6
- package/dist/providers/runtime.js.map +1 -1
- package/dist/providers/stripeWebhook.js +39 -10
- package/dist/providers/stripeWebhook.js.map +1 -1
- package/dist/providers/twilio.js +2 -2
- package/dist/proxy/cacheKey.js +9 -2
- package/dist/proxy/cacheKey.js.map +1 -1
- package/dist/proxy/providerDetector.js +22 -9
- package/dist/proxy/providerDetector.js.map +1 -1
- package/dist/proxy/proxyHandler.js +57 -22
- package/dist/proxy/proxyHandler.js.map +1 -1
- package/dist/proxy/requestNormalizer.js +4 -2
- package/dist/proxy/requestNormalizer.js.map +1 -1
- package/dist/reliability/costGovernance.js +211 -49
- package/dist/reliability/costGovernance.js.map +1 -1
- package/dist/reliability/index.d.ts +8 -8
- package/dist/reliability/index.js +4 -4
- package/dist/reliability/index.js.map +1 -1
- package/dist/reliability/reconciliation.js +274 -65
- package/dist/reliability/reconciliation.js.map +1 -1
- package/dist/reliability/runtimeHealth.js +127 -35
- package/dist/reliability/runtimeHealth.js.map +1 -1
- package/dist/reliability/slo.js +257 -50
- package/dist/reliability/slo.js.map +1 -1
- package/dist/report/safetyReport.d.ts +6 -0
- package/dist/report/safetyReport.js +56 -12
- package/dist/report/safetyReport.js.map +1 -1
- package/dist/rules/aiRules.js +5 -5
- package/dist/rules/aiRules.js.map +1 -1
- package/dist/safety/index.d.ts +2 -2
- package/dist/safety/index.js +1 -1
- package/dist/safety/index.js.map +1 -1
- package/dist/safety/safetyController.js +675 -165
- package/dist/safety/safetyController.js.map +1 -1
- package/dist/scenarios/scenarioBundle.js +204 -71
- package/dist/scenarios/scenarioBundle.js.map +1 -1
- package/dist/scenarios/scenarioStore.js +88 -29
- package/dist/scenarios/scenarioStore.js.map +1 -1
- package/dist/security/headerSanitizer.js +52 -10
- package/dist/security/headerSanitizer.js.map +1 -1
- package/dist/security/secrets.js +10 -5
- package/dist/security/secrets.js.map +1 -1
- package/dist/server/accessControl.js +37 -10
- package/dist/server/accessControl.js.map +1 -1
- package/dist/server/createServer.js +1 -1
- package/dist/server/createServer.js.map +1 -1
- package/dist/server/eventsStore.js +15 -5
- package/dist/server/eventsStore.js.map +1 -1
- package/dist/server/routes.js +61 -19
- package/dist/server/routes.js.map +1 -1
- package/dist/server/sse.js +1 -1
- package/dist/server/sse.js.map +1 -1
- package/dist/setup/sdkDetector.js +7 -2
- package/dist/setup/sdkDetector.js.map +1 -1
- package/dist/setup/setupGenerator.js +136 -48
- package/dist/setup/setupGenerator.js.map +1 -1
- package/dist/state/stateExtractor.js +3 -1
- package/dist/state/stateExtractor.js.map +1 -1
- package/dist/state/stateResolver.js +10 -7
- package/dist/state/stateResolver.js.map +1 -1
- package/dist/state/stateStore.js +13 -4
- package/dist/state/stateStore.js.map +1 -1
- package/dist/storage/fileStore.js +12 -4
- package/dist/storage/fileStore.js.map +1 -1
- package/dist/teamControl/controlPlane.js +846 -174
- package/dist/teamControl/controlPlane.js.map +1 -1
- package/dist/teamControl/deployment.js +19 -5
- package/dist/teamControl/deployment.js.map +1 -1
- package/dist/teamControl/index.d.ts +4 -4
- package/dist/teamControl/index.js +2 -2
- package/dist/teamControl/index.js.map +1 -1
- package/dist/tests/testGenerator.js +15 -7
- package/dist/tests/testGenerator.js.map +1 -1
- package/dist/trust/index.d.ts +2 -2
- package/dist/trust/index.js +1 -1
- package/dist/trust/index.js.map +1 -1
- package/dist/trust/trustLadder.js +309 -63
- package/dist/trust/trustLadder.js.map +1 -1
- package/dist/worlds/index.d.ts +2 -2
- package/dist/worlds/index.js +1 -1
- package/dist/worlds/index.js.map +1 -1
- package/dist/worlds/worlds.js +364 -65
- package/dist/worlds/worlds.js.map +1 -1
- package/docs/assets/dashboard.png +0 -0
- package/docs/ci.md +49 -49
- package/docs/github-actions.md +84 -84
- package/docs/mcp.md +58 -52
- package/docs/policy.md +63 -61
- package/docs/providers/authoring-packs.md +180 -175
- package/docs/providers/stripe-core-pack.md +84 -82
- package/docs/releases/README.md +13 -0
- package/docs/security/action-gateway-threat-model.md +34 -34
- package/docs/security/action-ledger-incident-replay-threat-model.md +42 -42
- package/docs/security/approval-inbox-threat-model.md +35 -35
- package/docs/security/credential-broker-threat-model.md +52 -52
- package/docs/security/egress-threat-model.md +126 -126
- package/docs/security/inventory-threat-model.md +104 -104
- package/docs/security/kill-switch-budgets-threat-model.md +37 -37
- package/docs/security/reliability-threat-model.md +111 -111
- package/docs/security/trust-ladder-threat-model.md +33 -33
- package/docs/telemetry.md +14 -0
- package/docs/usage.md +680 -571
- package/examples/README.md +10 -10
- package/examples/agent-instructions/README.md +39 -39
- package/examples/ci-smoke/README.md +21 -21
- package/examples/ci-smoke/ghostapi.policy.yaml +22 -22
- package/examples/ci-smoke/package.json +9 -9
- package/examples/ci-smoke/production-egress.mjs +4 -2
- package/examples/ci-smoke/safe.mjs +21 -17
- package/examples/evals/README.md +24 -24
- package/examples/evals/retry-after.eval.json +71 -52
- package/examples/generic-rest/README.md +27 -27
- package/examples/github-fetch/README.md +38 -35
- package/examples/openai-streaming/README.md +16 -16
- package/examples/openai-streaming/streaming-tool-call.mjs +44 -31
- package/examples/policy/ghostapi.policy.yaml +27 -27
- package/examples/record-replay/README.md +19 -19
- package/examples/record-replay/replay-requests.json +22 -22
- package/examples/record-replay/stripe-checkout.har.json +30 -30
- package/examples/resend-node/README.md +22 -22
- package/examples/stripe-node/README.md +35 -35
- package/examples/stripe-node/checkout-flow.mjs +87 -61
- package/examples/twilio-node/README.md +21 -21
- package/examples/worlds/README.md +19 -19
- package/examples/worlds/subscription-recovery.mjs +21 -12
- package/package.json +28 -6
- package/dist/landing/assets/index-BXR41fAD.css +0 -1
- package/dist/landing/assets/index-CxWsopzP.css +0 -1
- package/dist/landing/assets/index-D8_ggMRE.js +0 -55
- package/dist/landing/assets/index-DZlpyhqd.js +0 -55
- package/dist/landing/assets/index-Dk-Xezs3.js +0 -55
- package/dist/landing/index.html +0 -15
- package/dist/landing/landing.d.ts +0 -3
- package/dist/landing/landing.js +0 -12
- package/dist/landing/landing.js.map +0 -1
- package/docs/commercial/README.md +0 -29
- package/docs/commercial/data-inventory.md +0 -36
- package/docs/commercial/manual-invoicing.md +0 -44
- package/docs/commercial/metrics.md +0 -51
- package/docs/commercial/pricing.md +0 -103
- package/docs/commercial/privacy-policy-draft.md +0 -41
- package/docs/commercial/terms-topics-for-counsel.md +0 -51
- package/docs/design-partners/README.md +0 -52
- package/docs/design-partners/demo-narratives.md +0 -33
- package/docs/design-partners/design-partner-offer.md +0 -39
- package/docs/design-partners/discovery-questions.md +0 -28
- package/docs/design-partners/feedback-capture-template.md +0 -48
- package/docs/design-partners/icp-one-pager.md +0 -43
- package/docs/design-partners/onboarding-checklist.md +0 -33
- package/docs/design-partners/pilot-success-criteria.md +0 -27
- package/docs/design-partners/pricing-interview-script.md +0 -18
- package/docs/design-partners/telemetry-plan.md +0 -48
- package/docs/development/baseline.md +0 -114
- package/docs/development/onboarding-smoke.md +0 -37
- package/docs/development/verification-0.1.8.md +0 -25
- package/docs/enterprise-product-roadmap-ru.md +0 -1879
- package/docs/fundraising/README.md +0 -87
- package/docs/fundraising/data-room-checklist.md +0 -72
- package/docs/fundraising/demo-script.md +0 -52
- package/docs/fundraising/design-partners-50.md +0 -74
- package/docs/fundraising/launch-posts.md +0 -37
- package/docs/fundraising/metrics-and-evidence.md +0 -30
- package/docs/fundraising/roadmap-12-month.md +0 -25
- package/docs/fundraising/technical-due-diligence-index.md +0 -37
- package/docs/fundraising/yc-application.md +0 -25
- package/docs/hosted-pilot.md +0 -111
- package/docs/operations/disaster-recovery-runbook.md +0 -81
- package/docs/operations/kill-switch-runbook.md +0 -32
- package/docs/release-checklist.md +0 -83
- package/docs/release-migration-and-rollback.md +0 -63
- package/docs/release-readiness.md +0 -39
- package/docs/team-control-plane.md +0 -72
|
@@ -8,27 +8,47 @@ const stripeScenarios = [
|
|
|
8
8
|
title: "Stripe card declined",
|
|
9
9
|
provider: "stripe",
|
|
10
10
|
description: "A confirmed Payment Intent using pm_card_chargeDeclined returns a Stripe card_error.",
|
|
11
|
-
steps: [
|
|
11
|
+
steps: [
|
|
12
|
+
{
|
|
12
13
|
name: "Confirm declined payment intent",
|
|
13
14
|
method: "POST",
|
|
14
15
|
path: "/v1/payment_intents",
|
|
15
16
|
status: 402,
|
|
16
|
-
body: {
|
|
17
|
-
|
|
17
|
+
body: {
|
|
18
|
+
error: {
|
|
19
|
+
type: "card_error",
|
|
20
|
+
code: "card_declined",
|
|
21
|
+
decline_code: "generic_decline",
|
|
22
|
+
message: "Your card was declined.",
|
|
23
|
+
param: "payment_method",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
],
|
|
18
28
|
},
|
|
19
29
|
{
|
|
20
30
|
id: "stripe-subscription-payment-failed",
|
|
21
31
|
title: "Stripe subscription renewal payment failed",
|
|
22
32
|
provider: "stripe",
|
|
23
33
|
description: "A renewal with pm_card_chargeDeclined leaves the invoice open and subscription past_due.",
|
|
24
|
-
steps: [
|
|
34
|
+
steps: [
|
|
35
|
+
{
|
|
25
36
|
name: "Renew subscription with declined card",
|
|
26
37
|
method: "POST",
|
|
27
38
|
path: "/v1/subscriptions/sub_ghostapi/renew",
|
|
28
39
|
status: 402,
|
|
29
|
-
body: {
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
body: {
|
|
41
|
+
error: {
|
|
42
|
+
type: "card_error",
|
|
43
|
+
code: "card_declined",
|
|
44
|
+
decline_code: "generic_decline",
|
|
45
|
+
message: "Your card was declined.",
|
|
46
|
+
param: "payment_method",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
32
52
|
];
|
|
33
53
|
const stripeConformanceFixtures = [
|
|
34
54
|
{
|
|
@@ -37,12 +57,18 @@ const stripeConformanceFixtures = [
|
|
|
37
57
|
method: "POST",
|
|
38
58
|
path: "/v1/customers",
|
|
39
59
|
query: {},
|
|
40
|
-
headers: {
|
|
60
|
+
headers: {
|
|
61
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
62
|
+
"stripe-version": STRIPE_API_VERSION,
|
|
63
|
+
},
|
|
41
64
|
body: "email=ada%40example.com&name=Ada%20Lovelace&metadata%5Bsource%5D=fixture",
|
|
42
|
-
receivedAt: "2026-08-06T12:00:00.000Z"
|
|
65
|
+
receivedAt: "2026-08-06T12:00:00.000Z",
|
|
43
66
|
},
|
|
44
67
|
assertResponse(response) {
|
|
45
|
-
if (!isJsonObject(response.body) ||
|
|
68
|
+
if (!isJsonObject(response.body) ||
|
|
69
|
+
response.body.object !== "customer" ||
|
|
70
|
+
response.body.id !== "cus_fixture" ||
|
|
71
|
+
response.body.email !== "ada@example.com") {
|
|
46
72
|
return "response must contain a deterministic Stripe customer";
|
|
47
73
|
}
|
|
48
74
|
if (response.headers["request-id"] !== "req_fixture")
|
|
@@ -50,11 +76,13 @@ const stripeConformanceFixtures = [
|
|
|
50
76
|
return null;
|
|
51
77
|
},
|
|
52
78
|
assertStateTransition(transition, response) {
|
|
53
|
-
if (transition === null ||
|
|
79
|
+
if (transition === null ||
|
|
80
|
+
transition.key !== "stripe:cus_fixture" ||
|
|
81
|
+
transition.value !== response.body) {
|
|
54
82
|
return "state transition must persist the exact customer under its Stripe id";
|
|
55
83
|
}
|
|
56
84
|
return null;
|
|
57
|
-
}
|
|
85
|
+
},
|
|
58
86
|
},
|
|
59
87
|
{
|
|
60
88
|
name: "create a deterministic billing product",
|
|
@@ -62,23 +90,31 @@ const stripeConformanceFixtures = [
|
|
|
62
90
|
method: "POST",
|
|
63
91
|
path: "/v1/products",
|
|
64
92
|
query: {},
|
|
65
|
-
headers: {
|
|
93
|
+
headers: {
|
|
94
|
+
"content-type": "application/json",
|
|
95
|
+
"stripe-version": STRIPE_API_VERSION,
|
|
96
|
+
},
|
|
66
97
|
body: { name: "GhostAPI Pro", metadata: { tier: "pro" } },
|
|
67
|
-
receivedAt: "2026-08-06T12:00:00.000Z"
|
|
98
|
+
receivedAt: "2026-08-06T12:00:00.000Z",
|
|
68
99
|
},
|
|
69
100
|
assertResponse(response) {
|
|
70
|
-
if (!isJsonObject(response.body) ||
|
|
101
|
+
if (!isJsonObject(response.body) ||
|
|
102
|
+
response.body.object !== "product" ||
|
|
103
|
+
response.body.id !== "prod_fixture" ||
|
|
104
|
+
response.body.name !== "GhostAPI Pro") {
|
|
71
105
|
return "response must contain a deterministic Stripe billing product";
|
|
72
106
|
}
|
|
73
107
|
return null;
|
|
74
108
|
},
|
|
75
109
|
assertStateTransition(transition, response) {
|
|
76
|
-
if (transition === null ||
|
|
110
|
+
if (transition === null ||
|
|
111
|
+
transition.key !== "stripe:prod_fixture" ||
|
|
112
|
+
transition.value !== response.body) {
|
|
77
113
|
return "state transition must persist the exact billing product under its Stripe id";
|
|
78
114
|
}
|
|
79
115
|
return null;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
116
|
+
},
|
|
117
|
+
},
|
|
82
118
|
];
|
|
83
119
|
export const stripePack = {
|
|
84
120
|
name: "stripe",
|
|
@@ -89,7 +125,10 @@ export const stripePack = {
|
|
|
89
125
|
displayName: "Stripe",
|
|
90
126
|
implementation: "pack",
|
|
91
127
|
packVersion: STRIPE_PACK_VERSION,
|
|
92
|
-
apiVersions: {
|
|
128
|
+
apiVersions: {
|
|
129
|
+
default: STRIPE_API_VERSION,
|
|
130
|
+
supported: [STRIPE_API_VERSION],
|
|
131
|
+
},
|
|
93
132
|
capabilities: {
|
|
94
133
|
detection: true,
|
|
95
134
|
requestParsing: true,
|
|
@@ -99,12 +138,12 @@ export const stripePack = {
|
|
|
99
138
|
providerErrors: true,
|
|
100
139
|
scenarios: true,
|
|
101
140
|
webhooks: true,
|
|
102
|
-
conformanceFixtures: true
|
|
103
|
-
}
|
|
141
|
+
conformanceFixtures: true,
|
|
142
|
+
},
|
|
104
143
|
},
|
|
105
144
|
detection: {
|
|
106
145
|
priority: 400,
|
|
107
|
-
matches: ({ path }) => /^\/v1\/(?:customers|products|prices|subscriptions|invoices|events|payment_intents|payment_methods|checkout\/sessions|refunds)(?:\/|$)/.test(path.toLowerCase())
|
|
146
|
+
matches: ({ path }) => /^\/v1\/(?:customers|products|prices|subscriptions|invoices|events|payment_intents|payment_methods|checkout\/sessions|refunds)(?:\/|$)/.test(path.toLowerCase()),
|
|
108
147
|
},
|
|
109
148
|
parseRequest(request) {
|
|
110
149
|
return parseStripeRequest(request);
|
|
@@ -119,13 +158,15 @@ export const stripePack = {
|
|
|
119
158
|
type: "invalid_request_error",
|
|
120
159
|
code: "invalid_api_version",
|
|
121
160
|
param: "Stripe-Version",
|
|
122
|
-
message: `Unsupported Stripe API version: ${requested}. Supported version: ${STRIPE_API_VERSION}
|
|
123
|
-
}
|
|
161
|
+
message: `Unsupported Stripe API version: ${requested}. Supported version: ${STRIPE_API_VERSION}.`,
|
|
162
|
+
},
|
|
124
163
|
};
|
|
125
164
|
},
|
|
126
165
|
validate(parsedRequest, request) {
|
|
127
166
|
const parsed = readStripeRequest(parsedRequest);
|
|
128
|
-
if (request.method === "POST" &&
|
|
167
|
+
if (request.method === "POST" &&
|
|
168
|
+
request.path === "/v1/customers" &&
|
|
169
|
+
Object.keys(parsed.params).length === 0)
|
|
129
170
|
return missingParam("email");
|
|
130
171
|
if (request.method === "POST" && request.path === "/v1/payment_intents") {
|
|
131
172
|
if (parsed.params.amount === undefined)
|
|
@@ -135,7 +176,9 @@ export const stripePack = {
|
|
|
135
176
|
if (!hasCurrency(parsed.params.currency))
|
|
136
177
|
return missingParam("currency");
|
|
137
178
|
}
|
|
138
|
-
if (request.method === "POST" &&
|
|
179
|
+
if (request.method === "POST" &&
|
|
180
|
+
request.path === "/v1/payment_methods" &&
|
|
181
|
+
typeof parsed.params.type !== "string")
|
|
139
182
|
return missingParam("type");
|
|
140
183
|
if (request.method === "POST" && request.path === "/v1/checkout/sessions") {
|
|
141
184
|
if (!hasMode(parsed.params.mode))
|
|
@@ -145,7 +188,9 @@ export const stripePack = {
|
|
|
145
188
|
if (typeof parsed.params.cancel_url !== "string")
|
|
146
189
|
return missingParam("cancel_url");
|
|
147
190
|
}
|
|
148
|
-
if (request.method === "POST" &&
|
|
191
|
+
if (request.method === "POST" &&
|
|
192
|
+
request.path === "/v1/products" &&
|
|
193
|
+
typeof parsed.params.name !== "string")
|
|
149
194
|
return missingParam("name");
|
|
150
195
|
if (request.method === "POST" && request.path === "/v1/prices") {
|
|
151
196
|
if (!hasPositiveInteger(parsed.params.unit_amount))
|
|
@@ -159,10 +204,14 @@ export const stripePack = {
|
|
|
159
204
|
if (subscriptionPriceId(parsed.params.items) === undefined)
|
|
160
205
|
return missingParam("items[0][price]");
|
|
161
206
|
const trialDays = parsed.params.trial_period_days;
|
|
162
|
-
if (trialDays !== undefined &&
|
|
207
|
+
if (trialDays !== undefined &&
|
|
208
|
+
readNonNegativeInteger(trialDays) === undefined)
|
|
163
209
|
return invalidParamError("trial_period_days", "Invalid non-negative integer: trial_period_days.");
|
|
164
210
|
}
|
|
165
|
-
if (request.method === "POST" &&
|
|
211
|
+
if (request.method === "POST" &&
|
|
212
|
+
request.path === "/v1/refunds" &&
|
|
213
|
+
typeof parsed.params.payment_intent !== "string" &&
|
|
214
|
+
typeof parsed.params.charge !== "string") {
|
|
166
215
|
return missingParam("payment_intent");
|
|
167
216
|
}
|
|
168
217
|
return null;
|
|
@@ -181,14 +230,14 @@ export const stripePack = {
|
|
|
181
230
|
"content-type": "application/json",
|
|
182
231
|
...createStripeResponseHeaders(runtime),
|
|
183
232
|
"stripe-version": STRIPE_API_VERSION,
|
|
184
|
-
...response.headers
|
|
185
|
-
}
|
|
233
|
+
...response.headers,
|
|
234
|
+
},
|
|
186
235
|
};
|
|
187
236
|
},
|
|
188
237
|
createResponseHeaders({ runtime }) {
|
|
189
238
|
return createStripeResponseHeaders(runtime);
|
|
190
239
|
},
|
|
191
|
-
transitionState({ request, response, runtime }) {
|
|
240
|
+
transitionState({ request, response, runtime, }) {
|
|
192
241
|
if (response.headers["x-ghostapi-idempotency"] === "REPLAY")
|
|
193
242
|
return null;
|
|
194
243
|
const resource = resourceFromResponse(response.body);
|
|
@@ -196,19 +245,36 @@ export const stripePack = {
|
|
|
196
245
|
return null;
|
|
197
246
|
const parsed = parseStripeRequest(request);
|
|
198
247
|
const now = Math.floor(runtime.requireCapability("clock").now().getTime() / 1000);
|
|
199
|
-
const additionalWrites = [
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
248
|
+
const additionalWrites = [
|
|
249
|
+
...(response.stateWrites ?? []),
|
|
250
|
+
...(parsed.idempotencyKey === undefined || request.method !== "POST"
|
|
251
|
+
? []
|
|
252
|
+
: [
|
|
253
|
+
{
|
|
254
|
+
key: `stripe:idempotency:${parsed.idempotencyKey}`,
|
|
255
|
+
value: {
|
|
256
|
+
method: request.method,
|
|
257
|
+
path: request.path,
|
|
258
|
+
params: stableStringify(parsed.params),
|
|
259
|
+
created: now,
|
|
260
|
+
response: {
|
|
261
|
+
status: response.status,
|
|
262
|
+
headers: response.headers,
|
|
263
|
+
body: response.body,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
]),
|
|
268
|
+
];
|
|
209
269
|
if (resource === null) {
|
|
210
270
|
const [firstWrite, ...remainingWrites] = additionalWrites;
|
|
211
|
-
return firstWrite === undefined
|
|
271
|
+
return firstWrite === undefined
|
|
272
|
+
? null
|
|
273
|
+
: {
|
|
274
|
+
key: firstWrite.key,
|
|
275
|
+
value: firstWrite.value,
|
|
276
|
+
additionalWrites: remainingWrites,
|
|
277
|
+
};
|
|
212
278
|
}
|
|
213
279
|
return { key: `stripe:${resource.id}`, value: resource, additionalWrites };
|
|
214
280
|
},
|
|
@@ -218,10 +284,10 @@ export const stripePack = {
|
|
|
218
284
|
},
|
|
219
285
|
promptHints: [
|
|
220
286
|
"This is the Stripe API billing pack with deterministic Customers, Products, Prices, Subscriptions, Invoices, Payment Intents, Payment Methods, Checkout Sessions, Refunds, and pull-based signed webhook events.",
|
|
221
|
-
"Unsupported Stripe endpoints return a diagnostic invalid_request_error rather than a generated success."
|
|
287
|
+
"Unsupported Stripe endpoints return a diagnostic invalid_request_error rather than a generated success.",
|
|
222
288
|
],
|
|
223
289
|
scenarios: stripeScenarios,
|
|
224
|
-
conformanceFixtures: stripeConformanceFixtures
|
|
290
|
+
conformanceFixtures: stripeConformanceFixtures,
|
|
225
291
|
};
|
|
226
292
|
function dispatchStripeRequest(request, params, state, runtime, now) {
|
|
227
293
|
const segments = request.path.split("/").filter(Boolean);
|
|
@@ -256,7 +322,17 @@ function handleProducts(method, id, action, params, query, state, runtime, now)
|
|
|
256
322
|
if (method === "GET" && id === undefined)
|
|
257
323
|
return stripeList("/v1/products", listResources(state, "product"), query);
|
|
258
324
|
if (method === "POST" && id === undefined) {
|
|
259
|
-
return stripeObject({
|
|
325
|
+
return stripeObject({
|
|
326
|
+
id: runtime.requireCapability("idGenerator").create("prod"),
|
|
327
|
+
object: "product",
|
|
328
|
+
active: params.active !== false,
|
|
329
|
+
name: stringOrNull(params.name) ?? "GhostAPI product",
|
|
330
|
+
description: stringOrNull(params.description),
|
|
331
|
+
metadata: recordOrEmpty(params.metadata),
|
|
332
|
+
livemode: false,
|
|
333
|
+
created: now,
|
|
334
|
+
updated: now,
|
|
335
|
+
});
|
|
260
336
|
}
|
|
261
337
|
const product = id === undefined ? null : getResource(state, id, "product");
|
|
262
338
|
if (product === null)
|
|
@@ -264,7 +340,14 @@ function handleProducts(method, id, action, params, query, state, runtime, now)
|
|
|
264
340
|
if (method === "GET")
|
|
265
341
|
return stripeObject(product);
|
|
266
342
|
if (method === "POST")
|
|
267
|
-
return stripeObject({
|
|
343
|
+
return stripeObject({
|
|
344
|
+
...product,
|
|
345
|
+
...pick(params, ["name", "description", "active", "metadata"]),
|
|
346
|
+
updated: now,
|
|
347
|
+
metadata: isJsonObject(params.metadata)
|
|
348
|
+
? params.metadata
|
|
349
|
+
: product.metadata,
|
|
350
|
+
});
|
|
268
351
|
return unsupportedEndpointFor("products", method);
|
|
269
352
|
}
|
|
270
353
|
function handlePrices(method, id, action, params, query, state, runtime, now) {
|
|
@@ -274,7 +357,8 @@ function handlePrices(method, id, action, params, query, state, runtime, now) {
|
|
|
274
357
|
return stripeList("/v1/prices", listResources(state, "price"), query);
|
|
275
358
|
if (method === "POST" && id === undefined) {
|
|
276
359
|
const productId = typeof params.product === "string" ? params.product : undefined;
|
|
277
|
-
if (productId !== undefined &&
|
|
360
|
+
if (productId !== undefined &&
|
|
361
|
+
getResource(state, productId, "product") === null)
|
|
278
362
|
return notFound("product", productId);
|
|
279
363
|
if (!hasPositiveInteger(params.unit_amount))
|
|
280
364
|
return invalidParam("unit_amount", "Invalid positive integer: unit_amount.");
|
|
@@ -283,7 +367,19 @@ function handlePrices(method, id, action, params, query, state, runtime, now) {
|
|
|
283
367
|
const recurring = parseRecurring(params.recurring);
|
|
284
368
|
if (recurring.error !== null)
|
|
285
369
|
return stripeFailure(400, "invalid_request_error", recurring.error, "recurring[interval]", "parameter_invalid_enum");
|
|
286
|
-
return stripeObject({
|
|
370
|
+
return stripeObject({
|
|
371
|
+
id: runtime.requireCapability("idGenerator").create("price"),
|
|
372
|
+
object: "price",
|
|
373
|
+
active: params.active !== false,
|
|
374
|
+
currency: String(params.currency).toLowerCase(),
|
|
375
|
+
unit_amount: params.unit_amount,
|
|
376
|
+
product: productId ?? null,
|
|
377
|
+
nickname: stringOrNull(params.nickname),
|
|
378
|
+
recurring: recurring.value,
|
|
379
|
+
metadata: recordOrEmpty(params.metadata),
|
|
380
|
+
livemode: false,
|
|
381
|
+
created: now,
|
|
382
|
+
});
|
|
287
383
|
}
|
|
288
384
|
const price = id === undefined ? null : getResource(state, id, "price");
|
|
289
385
|
if (price === null)
|
|
@@ -291,7 +387,13 @@ function handlePrices(method, id, action, params, query, state, runtime, now) {
|
|
|
291
387
|
if (method === "GET")
|
|
292
388
|
return stripeObject(price);
|
|
293
389
|
if (method === "POST")
|
|
294
|
-
return stripeObject({
|
|
390
|
+
return stripeObject({
|
|
391
|
+
...price,
|
|
392
|
+
...pick(params, ["active", "nickname", "metadata"]),
|
|
393
|
+
metadata: isJsonObject(params.metadata)
|
|
394
|
+
? params.metadata
|
|
395
|
+
: price.metadata,
|
|
396
|
+
});
|
|
295
397
|
return unsupportedEndpointFor("prices", method);
|
|
296
398
|
}
|
|
297
399
|
function handleSubscriptions(method, id, action, params, query, state, runtime, now) {
|
|
@@ -311,7 +413,14 @@ function handleSubscriptions(method, id, action, params, query, state, runtime,
|
|
|
311
413
|
if (method === "POST" && action === undefined) {
|
|
312
414
|
if (params.cancel_at_period_end === true)
|
|
313
415
|
return responseWithWebhookWrites({ ...subscription, cancel_at_period_end: true, updated: now }, ["customer.subscription.updated"], runtime, now);
|
|
314
|
-
return responseWithWebhookWrites({
|
|
416
|
+
return responseWithWebhookWrites({
|
|
417
|
+
...subscription,
|
|
418
|
+
...pick(params, ["metadata", "default_payment_method"]),
|
|
419
|
+
metadata: isJsonObject(params.metadata)
|
|
420
|
+
? params.metadata
|
|
421
|
+
: subscription.metadata,
|
|
422
|
+
updated: now,
|
|
423
|
+
}, ["customer.subscription.updated"], runtime, now);
|
|
315
424
|
}
|
|
316
425
|
if (method === "DELETE" && action === undefined)
|
|
317
426
|
return updateSubscriptionStatus(subscription, "canceled", now, ["customer.subscription.deleted"], runtime);
|
|
@@ -334,7 +443,9 @@ function handleInvoices(method, id, action, params, query, state, runtime, now)
|
|
|
334
443
|
function handleEvents(method, id, action, _params, query, state) {
|
|
335
444
|
if (method === "GET" && id === undefined)
|
|
336
445
|
return stripeList("/v1/events", orderedWebhookEvents(state, query), query);
|
|
337
|
-
if (method === "GET" &&
|
|
446
|
+
if (method === "GET" &&
|
|
447
|
+
id !== undefined &&
|
|
448
|
+
(action === undefined || action === "deliver")) {
|
|
338
449
|
const event = getResource(state, id, "event");
|
|
339
450
|
if (event === null)
|
|
340
451
|
return notFound("event", id);
|
|
@@ -358,7 +469,8 @@ function createSubscription(params, state, runtime, now) {
|
|
|
358
469
|
return stripeFailure(400, "invalid_request_error", "Subscription prices require recurring configuration.", "items[0][price]", "parameter_invalid_empty");
|
|
359
470
|
const trialDays = readNonNegativeInteger(params.trial_period_days) ?? 0;
|
|
360
471
|
const id = runtime.requireCapability("idGenerator").create("sub");
|
|
361
|
-
const periodEnd = now +
|
|
472
|
+
const periodEnd = now +
|
|
473
|
+
(trialDays > 0 ? trialDays * 86_400 : recurringSeconds(price.recurring));
|
|
362
474
|
const subscription = {
|
|
363
475
|
id,
|
|
364
476
|
object: "subscription",
|
|
@@ -370,19 +482,45 @@ function createSubscription(params, state, runtime, now) {
|
|
|
370
482
|
trial_start: trialDays > 0 ? now : null,
|
|
371
483
|
trial_end: trialDays > 0 ? periodEnd : null,
|
|
372
484
|
default_payment_method: stringOrNull(params.default_payment_method),
|
|
373
|
-
items: {
|
|
485
|
+
items: {
|
|
486
|
+
object: "list",
|
|
487
|
+
data: [
|
|
488
|
+
{
|
|
489
|
+
id: runtime.requireCapability("idGenerator").create("si"),
|
|
490
|
+
object: "subscription_item",
|
|
491
|
+
price: priceId,
|
|
492
|
+
quantity: subscriptionQuantity(params.items),
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
},
|
|
374
496
|
latest_invoice: null,
|
|
375
497
|
metadata: recordOrEmpty(params.metadata),
|
|
376
498
|
livemode: false,
|
|
377
499
|
created: now,
|
|
378
|
-
updated: now
|
|
500
|
+
updated: now,
|
|
379
501
|
};
|
|
380
502
|
if (trialDays === 0) {
|
|
381
503
|
const invoice = createInvoice(subscription, price, runtime, now, "paid", "subscription_create");
|
|
382
|
-
const paymentIntent = createPaymentIntent({
|
|
504
|
+
const paymentIntent = createPaymentIntent({
|
|
505
|
+
amount: price.unit_amount,
|
|
506
|
+
currency: price.currency,
|
|
507
|
+
customer: customerId,
|
|
508
|
+
payment_method: params.default_payment_method,
|
|
509
|
+
}, runtime, now, true);
|
|
383
510
|
const createdSubscription = { ...subscription, latest_invoice: invoice.id };
|
|
384
|
-
const response = responseWithWebhookWrites(createdSubscription, [
|
|
385
|
-
|
|
511
|
+
const response = responseWithWebhookWrites(createdSubscription, [
|
|
512
|
+
"customer.subscription.created",
|
|
513
|
+
"invoice.paid",
|
|
514
|
+
"invoice.payment_succeeded",
|
|
515
|
+
], runtime, now, { "invoice.paid": invoice, "invoice.payment_succeeded": invoice });
|
|
516
|
+
response.stateWrites = [
|
|
517
|
+
...(response.stateWrites ?? []),
|
|
518
|
+
{
|
|
519
|
+
key: `stripe:${invoice.id}`,
|
|
520
|
+
value: { ...invoice, payment_intent: paymentIntent.id },
|
|
521
|
+
},
|
|
522
|
+
{ key: `stripe:${paymentIntent.id}`, value: paymentIntent },
|
|
523
|
+
];
|
|
386
524
|
return response;
|
|
387
525
|
}
|
|
388
526
|
return responseWithWebhookWrites(subscription, ["customer.subscription.created"], runtime, now);
|
|
@@ -394,7 +532,10 @@ function renewSubscription(subscription, params, state, runtime, now) {
|
|
|
394
532
|
const price = priceId === undefined ? null : getResource(state, priceId, "price");
|
|
395
533
|
if (price === null)
|
|
396
534
|
return stripeFailure(500, "api_error", "Subscription has no valid recurring price.", undefined, "subscription_invalid");
|
|
397
|
-
const declined = isDeclined(params) ||
|
|
535
|
+
const declined = isDeclined(params) ||
|
|
536
|
+
isDeclined({
|
|
537
|
+
payment_method: params.payment_method ?? subscription.default_payment_method,
|
|
538
|
+
});
|
|
398
539
|
const status = declined ? "open" : "paid";
|
|
399
540
|
const invoice = createInvoice(subscription, price, runtime, now, status, "subscription_cycle");
|
|
400
541
|
const nextStatus = declined ? "past_due" : "active";
|
|
@@ -404,7 +545,7 @@ function renewSubscription(subscription, params, state, runtime, now) {
|
|
|
404
545
|
current_period_start: now,
|
|
405
546
|
current_period_end: now + recurringSeconds(price.recurring),
|
|
406
547
|
latest_invoice: invoice.id,
|
|
407
|
-
updated: now
|
|
548
|
+
updated: now,
|
|
408
549
|
};
|
|
409
550
|
if (declined) {
|
|
410
551
|
const failure = cardDeclined();
|
|
@@ -413,13 +554,29 @@ function renewSubscription(subscription, params, state, runtime, now) {
|
|
|
413
554
|
stateWrites: [
|
|
414
555
|
{ key: `stripe:${invoice.id}`, value: invoice },
|
|
415
556
|
{ key: `stripe:${subscription.id}`, value: nextSubscription },
|
|
416
|
-
...createWebhookWrites(["invoice.payment_failed", "customer.subscription.updated"], nextSubscription, runtime, now)
|
|
417
|
-
]
|
|
557
|
+
...createWebhookWrites(["invoice.payment_failed", "customer.subscription.updated"], nextSubscription, runtime, now),
|
|
558
|
+
],
|
|
418
559
|
};
|
|
419
560
|
}
|
|
420
|
-
const paymentIntent = createPaymentIntent({
|
|
421
|
-
|
|
422
|
-
|
|
561
|
+
const paymentIntent = createPaymentIntent({
|
|
562
|
+
amount: price.unit_amount,
|
|
563
|
+
currency: price.currency,
|
|
564
|
+
customer: subscription.customer,
|
|
565
|
+
payment_method: params.payment_method ?? subscription.default_payment_method,
|
|
566
|
+
}, runtime, now, true);
|
|
567
|
+
const response = responseWithWebhookWrites(nextSubscription, [
|
|
568
|
+
"invoice.paid",
|
|
569
|
+
"invoice.payment_succeeded",
|
|
570
|
+
"customer.subscription.updated",
|
|
571
|
+
], runtime, now, { "invoice.paid": invoice, "invoice.payment_succeeded": invoice });
|
|
572
|
+
response.stateWrites = [
|
|
573
|
+
...(response.stateWrites ?? []),
|
|
574
|
+
{
|
|
575
|
+
key: `stripe:${invoice.id}`,
|
|
576
|
+
value: { ...invoice, payment_intent: paymentIntent.id },
|
|
577
|
+
},
|
|
578
|
+
{ key: `stripe:${paymentIntent.id}`, value: paymentIntent },
|
|
579
|
+
];
|
|
423
580
|
return response;
|
|
424
581
|
}
|
|
425
582
|
function payInvoice(invoice, params, state, runtime, now) {
|
|
@@ -427,37 +584,125 @@ function payInvoice(invoice, params, state, runtime, now) {
|
|
|
427
584
|
return stripeObject(invoice);
|
|
428
585
|
if (invoice.status === "void")
|
|
429
586
|
return stripeFailure(400, "invalid_request_error", "Void invoices cannot be paid.", "id", "invoice_invalid");
|
|
430
|
-
const subscription = typeof invoice.subscription === "string"
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
587
|
+
const subscription = typeof invoice.subscription === "string"
|
|
588
|
+
? getResource(state, invoice.subscription, "subscription")
|
|
589
|
+
: null;
|
|
590
|
+
if (isDeclined(params) ||
|
|
591
|
+
isDeclined({
|
|
592
|
+
payment_method: params.payment_method ?? subscription?.default_payment_method,
|
|
593
|
+
})) {
|
|
594
|
+
const failedInvoice = {
|
|
595
|
+
...invoice,
|
|
596
|
+
status: "open",
|
|
597
|
+
paid: false,
|
|
598
|
+
updated: now,
|
|
599
|
+
attempt_count: numberOr(invoice.attempt_count, 0) + 1,
|
|
600
|
+
};
|
|
601
|
+
const writes = [
|
|
602
|
+
{ key: `stripe:${String(invoice.id)}`, value: failedInvoice },
|
|
603
|
+
...(subscription === null
|
|
604
|
+
? []
|
|
605
|
+
: [
|
|
606
|
+
{
|
|
607
|
+
key: `stripe:${subscription.id}`,
|
|
608
|
+
value: { ...subscription, status: "past_due", updated: now },
|
|
609
|
+
},
|
|
610
|
+
]),
|
|
611
|
+
...createWebhookWrites([
|
|
612
|
+
"invoice.payment_failed",
|
|
613
|
+
...(subscription === null ? [] : ["customer.subscription.updated"]),
|
|
614
|
+
], failedInvoice, runtime, now),
|
|
615
|
+
];
|
|
434
616
|
return { ...cardDeclined(), stateWrites: writes };
|
|
435
617
|
}
|
|
436
|
-
const paidInvoice = {
|
|
618
|
+
const paidInvoice = {
|
|
619
|
+
...invoice,
|
|
620
|
+
status: "paid",
|
|
621
|
+
paid: true,
|
|
622
|
+
amount_paid: invoice.amount_due,
|
|
623
|
+
amount_remaining: 0,
|
|
624
|
+
updated: now,
|
|
625
|
+
attempt_count: numberOr(invoice.attempt_count, 0) + 1,
|
|
626
|
+
};
|
|
437
627
|
const response = responseWithWebhookWrites(paidInvoice, ["invoice.paid", "invoice.payment_succeeded"], runtime, now);
|
|
438
628
|
if (subscription !== null)
|
|
439
|
-
response.stateWrites = [
|
|
629
|
+
response.stateWrites = [
|
|
630
|
+
...(response.stateWrites ?? []),
|
|
631
|
+
{
|
|
632
|
+
key: `stripe:${subscription.id}`,
|
|
633
|
+
value: {
|
|
634
|
+
...subscription,
|
|
635
|
+
status: "active",
|
|
636
|
+
latest_invoice: invoice.id,
|
|
637
|
+
updated: now,
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
];
|
|
440
641
|
return response;
|
|
441
642
|
}
|
|
442
643
|
function updateSubscriptionStatus(subscription, status, now, eventTypes, runtime) {
|
|
443
|
-
return responseWithWebhookWrites({
|
|
644
|
+
return responseWithWebhookWrites({
|
|
645
|
+
...subscription,
|
|
646
|
+
status,
|
|
647
|
+
canceled_at: status === "canceled" ? now : null,
|
|
648
|
+
cancel_at_period_end: false,
|
|
649
|
+
updated: now,
|
|
650
|
+
}, eventTypes, runtime, now);
|
|
444
651
|
}
|
|
445
652
|
function createInvoice(subscription, price, runtime, now, status, billingReason) {
|
|
446
653
|
const id = runtime.requireCapability("idGenerator").create("in");
|
|
447
654
|
const amount = numberOr(price.unit_amount, 0);
|
|
448
|
-
return {
|
|
655
|
+
return {
|
|
656
|
+
id,
|
|
657
|
+
object: "invoice",
|
|
658
|
+
customer: subscription.customer,
|
|
659
|
+
subscription: subscription.id,
|
|
660
|
+
status,
|
|
661
|
+
paid: status === "paid",
|
|
662
|
+
amount_due: amount,
|
|
663
|
+
amount_paid: status === "paid" ? amount : 0,
|
|
664
|
+
amount_remaining: status === "paid" ? 0 : amount,
|
|
665
|
+
currency: price.currency,
|
|
666
|
+
billing_reason: billingReason,
|
|
667
|
+
payment_intent: null,
|
|
668
|
+
attempt_count: 1,
|
|
669
|
+
metadata: {},
|
|
670
|
+
livemode: false,
|
|
671
|
+
created: now,
|
|
672
|
+
updated: now,
|
|
673
|
+
};
|
|
449
674
|
}
|
|
450
675
|
function responseWithWebhookWrites(resource, eventTypes, runtime, now, eventObjects = {}) {
|
|
451
|
-
return {
|
|
676
|
+
return {
|
|
677
|
+
status: 200,
|
|
678
|
+
headers: {},
|
|
679
|
+
body: resource,
|
|
680
|
+
stateWrites: createWebhookWrites(eventTypes, resource, runtime, now, eventObjects),
|
|
681
|
+
};
|
|
452
682
|
}
|
|
453
683
|
function createWebhookWrites(eventTypes, dataObject, runtime, now, eventObjects = {}) {
|
|
454
684
|
return eventTypes.map((type) => {
|
|
455
685
|
const id = runtime.requireCapability("idGenerator").create("evt");
|
|
456
|
-
return {
|
|
686
|
+
return {
|
|
687
|
+
key: `stripe:${id}`,
|
|
688
|
+
value: {
|
|
689
|
+
id,
|
|
690
|
+
object: "event",
|
|
691
|
+
api_version: STRIPE_API_VERSION,
|
|
692
|
+
created: now,
|
|
693
|
+
livemode: false,
|
|
694
|
+
pending_webhooks: 1,
|
|
695
|
+
request: { id: null, idempotency_key: null },
|
|
696
|
+
type,
|
|
697
|
+
data: { object: structuredClone(eventObjects[type] ?? dataObject) },
|
|
698
|
+
},
|
|
699
|
+
};
|
|
457
700
|
});
|
|
458
701
|
}
|
|
459
702
|
function createStripeResponseHeaders(runtime) {
|
|
460
|
-
return {
|
|
703
|
+
return {
|
|
704
|
+
"request-id": runtime.requireCapability("idGenerator").create("req"),
|
|
705
|
+
};
|
|
461
706
|
}
|
|
462
707
|
function handleCustomers(method, id, action, params, query, state, runtime, now) {
|
|
463
708
|
if (action !== undefined)
|
|
@@ -465,14 +710,29 @@ function handleCustomers(method, id, action, params, query, state, runtime, now)
|
|
|
465
710
|
if (method === "GET" && id === undefined)
|
|
466
711
|
return stripeList("/v1/customers", listResources(state, "customer"), query);
|
|
467
712
|
if (method === "POST" && id === undefined)
|
|
468
|
-
return stripeObject({
|
|
713
|
+
return stripeObject({
|
|
714
|
+
id: runtime.requireCapability("idGenerator").create("cus"),
|
|
715
|
+
object: "customer",
|
|
716
|
+
email: stringOrNull(params.email),
|
|
717
|
+
name: stringOrNull(params.name),
|
|
718
|
+
description: stringOrNull(params.description),
|
|
719
|
+
metadata: recordOrEmpty(params.metadata),
|
|
720
|
+
livemode: false,
|
|
721
|
+
created: now,
|
|
722
|
+
});
|
|
469
723
|
const customer = id === undefined ? null : getResource(state, id, "customer");
|
|
470
724
|
if (customer === null)
|
|
471
725
|
return notFound("customer", id);
|
|
472
726
|
if (method === "GET")
|
|
473
727
|
return stripeObject(customer);
|
|
474
728
|
if (method === "POST")
|
|
475
|
-
return stripeObject({
|
|
729
|
+
return stripeObject({
|
|
730
|
+
...customer,
|
|
731
|
+
...pick(params, ["email", "name", "description", "metadata"]),
|
|
732
|
+
metadata: isJsonObject(params.metadata)
|
|
733
|
+
? params.metadata
|
|
734
|
+
: customer.metadata,
|
|
735
|
+
});
|
|
476
736
|
if (method === "DELETE")
|
|
477
737
|
return stripeObject({ id, object: "customer", deleted: true });
|
|
478
738
|
return unsupportedEndpointFor("customers", method);
|
|
@@ -486,7 +746,23 @@ function handlePaymentMethods(method, id, action, params, query, state, runtime,
|
|
|
486
746
|
const type = String(params.type);
|
|
487
747
|
if (type !== "card")
|
|
488
748
|
return stripeFailure(400, "invalid_request_error", "Only payment method type 'card' is supported by this GhostAPI Stripe pack.", "type", "parameter_invalid_enum");
|
|
489
|
-
return stripeObject({
|
|
749
|
+
return stripeObject({
|
|
750
|
+
id: runtime.requireCapability("idGenerator").create("pm"),
|
|
751
|
+
object: "payment_method",
|
|
752
|
+
type,
|
|
753
|
+
card: {
|
|
754
|
+
brand: "visa",
|
|
755
|
+
country: "US",
|
|
756
|
+
exp_month: 12,
|
|
757
|
+
exp_year: 2030,
|
|
758
|
+
funding: "credit",
|
|
759
|
+
last4: "4242",
|
|
760
|
+
},
|
|
761
|
+
billing_details: recordOrEmpty(params.billing_details),
|
|
762
|
+
customer: null,
|
|
763
|
+
livemode: false,
|
|
764
|
+
created: now,
|
|
765
|
+
});
|
|
490
766
|
}
|
|
491
767
|
const paymentMethod = id === undefined ? null : getResource(state, id, "payment_method");
|
|
492
768
|
if (paymentMethod === null)
|
|
@@ -502,7 +778,9 @@ function handlePaymentIntents(method, id, action, params, query, state, runtime,
|
|
|
502
778
|
if (isDeclined(params))
|
|
503
779
|
return cardDeclined();
|
|
504
780
|
const paymentIntent = createPaymentIntent(params, runtime, now, Boolean(params.confirm));
|
|
505
|
-
return params.confirm === true
|
|
781
|
+
return params.confirm === true
|
|
782
|
+
? responseWithWebhookWrites(paymentIntent, ["payment_intent.succeeded"], runtime, now)
|
|
783
|
+
: stripeObject(paymentIntent);
|
|
506
784
|
}
|
|
507
785
|
const paymentIntent = id === undefined ? null : getResource(state, id, "payment_intent");
|
|
508
786
|
if (paymentIntent === null)
|
|
@@ -512,10 +790,28 @@ function handlePaymentIntents(method, id, action, params, query, state, runtime,
|
|
|
512
790
|
if (method === "POST" && action === "confirm") {
|
|
513
791
|
if (isDeclined(params))
|
|
514
792
|
return cardDeclined();
|
|
515
|
-
return responseWithWebhookWrites({
|
|
793
|
+
return responseWithWebhookWrites({
|
|
794
|
+
...paymentIntent,
|
|
795
|
+
...pick(params, ["payment_method", "metadata"]),
|
|
796
|
+
status: "succeeded",
|
|
797
|
+
amount_received: paymentIntent.amount,
|
|
798
|
+
latest_charge: `ch_${id}`,
|
|
799
|
+
next_action: null,
|
|
800
|
+
}, ["payment_intent.succeeded"], runtime, now);
|
|
516
801
|
}
|
|
517
802
|
if (method === "POST" && action === undefined)
|
|
518
|
-
return stripeObject({
|
|
803
|
+
return stripeObject({
|
|
804
|
+
...paymentIntent,
|
|
805
|
+
...pick(params, [
|
|
806
|
+
"amount",
|
|
807
|
+
"currency",
|
|
808
|
+
"description",
|
|
809
|
+
"metadata",
|
|
810
|
+
"payment_method",
|
|
811
|
+
]),
|
|
812
|
+
amount: numberOr(paymentIntent.amount, params.amount),
|
|
813
|
+
status: paymentIntent.status,
|
|
814
|
+
});
|
|
519
815
|
return unsupportedEndpointFor("payment_intents", method);
|
|
520
816
|
}
|
|
521
817
|
function handleCheckoutSessions(method, id, action, params, query, state, runtime, now) {
|
|
@@ -524,8 +820,25 @@ function handleCheckoutSessions(method, id, action, params, query, state, runtim
|
|
|
524
820
|
if (method === "GET" && id === undefined)
|
|
525
821
|
return stripeList("/v1/checkout/sessions", listResources(state, "checkout.session"), query);
|
|
526
822
|
if (method === "POST" && id === undefined) {
|
|
527
|
-
const sessionId = runtime
|
|
528
|
-
|
|
823
|
+
const sessionId = runtime
|
|
824
|
+
.requireCapability("idGenerator")
|
|
825
|
+
.create("cs_test");
|
|
826
|
+
return stripeObject({
|
|
827
|
+
id: sessionId,
|
|
828
|
+
object: "checkout.session",
|
|
829
|
+
mode: params.mode,
|
|
830
|
+
status: "open",
|
|
831
|
+
payment_status: "unpaid",
|
|
832
|
+
customer: stringOrNull(params.customer),
|
|
833
|
+
success_url: params.success_url,
|
|
834
|
+
cancel_url: params.cancel_url,
|
|
835
|
+
url: `http://127.0.0.1:8080/v1/checkout/sessions/${sessionId}/mock-hosted-page`,
|
|
836
|
+
amount_total: checkoutAmount(params),
|
|
837
|
+
currency: checkoutCurrency(params),
|
|
838
|
+
livemode: false,
|
|
839
|
+
created: now,
|
|
840
|
+
metadata: recordOrEmpty(params.metadata),
|
|
841
|
+
});
|
|
529
842
|
}
|
|
530
843
|
const session = id === undefined ? null : getResource(state, id, "checkout.session");
|
|
531
844
|
if (session === null)
|
|
@@ -540,17 +853,49 @@ function handleRefunds(method, id, action, params, query, state, runtime, now) {
|
|
|
540
853
|
if (method === "GET" && id === undefined)
|
|
541
854
|
return stripeList("/v1/refunds", listResources(state, "refund"), query);
|
|
542
855
|
if (method === "POST" && id === undefined) {
|
|
543
|
-
const paymentIntentId = typeof params.payment_intent === "string"
|
|
544
|
-
|
|
856
|
+
const paymentIntentId = typeof params.payment_intent === "string"
|
|
857
|
+
? params.payment_intent
|
|
858
|
+
: undefined;
|
|
859
|
+
const paymentIntent = paymentIntentId === undefined
|
|
860
|
+
? null
|
|
861
|
+
: getResource(state, paymentIntentId, "payment_intent");
|
|
545
862
|
if (paymentIntentId !== undefined && paymentIntent === null)
|
|
546
863
|
return notFound("payment_intent", paymentIntentId);
|
|
547
|
-
const amount = typeof params.amount === "number"
|
|
864
|
+
const amount = typeof params.amount === "number"
|
|
865
|
+
? params.amount
|
|
866
|
+
: typeof paymentIntent?.amount === "number"
|
|
867
|
+
? paymentIntent.amount
|
|
868
|
+
: null;
|
|
548
869
|
if (amount === null || !Number.isInteger(amount) || amount <= 0)
|
|
549
870
|
return invalidParam("amount", "Invalid positive integer: amount.");
|
|
550
|
-
const refund = {
|
|
871
|
+
const refund = {
|
|
872
|
+
id: runtime.requireCapability("idGenerator").create("re"),
|
|
873
|
+
object: "refund",
|
|
874
|
+
amount,
|
|
875
|
+
currency: typeof paymentIntent?.currency === "string"
|
|
876
|
+
? paymentIntent.currency
|
|
877
|
+
: "usd",
|
|
878
|
+
payment_intent: paymentIntentId ?? null,
|
|
879
|
+
charge: typeof params.charge === "string" ? params.charge : null,
|
|
880
|
+
status: "succeeded",
|
|
881
|
+
reason: stringOrNull(params.reason),
|
|
882
|
+
metadata: recordOrEmpty(params.metadata),
|
|
883
|
+
livemode: false,
|
|
884
|
+
created: now,
|
|
885
|
+
};
|
|
551
886
|
const response = responseWithWebhookWrites(refund, ["charge.refunded"], runtime, now);
|
|
552
887
|
if (paymentIntent !== null)
|
|
553
|
-
response.stateWrites = [
|
|
888
|
+
response.stateWrites = [
|
|
889
|
+
...(response.stateWrites ?? []),
|
|
890
|
+
{
|
|
891
|
+
key: `stripe:${paymentIntent.id}`,
|
|
892
|
+
value: {
|
|
893
|
+
...paymentIntent,
|
|
894
|
+
amount_refunded: numberOr(paymentIntent.amount_refunded, 0) + amount,
|
|
895
|
+
status: "succeeded",
|
|
896
|
+
},
|
|
897
|
+
},
|
|
898
|
+
];
|
|
554
899
|
return response;
|
|
555
900
|
}
|
|
556
901
|
const refund = id === undefined ? null : getResource(state, id, "refund");
|
|
@@ -562,7 +907,24 @@ function handleRefunds(method, id, action, params, query, state, runtime, now) {
|
|
|
562
907
|
}
|
|
563
908
|
function createPaymentIntent(params, runtime, now, confirmed) {
|
|
564
909
|
const id = runtime.requireCapability("idGenerator").create("pi");
|
|
565
|
-
return {
|
|
910
|
+
return {
|
|
911
|
+
id,
|
|
912
|
+
object: "payment_intent",
|
|
913
|
+
amount: params.amount,
|
|
914
|
+
amount_received: confirmed ? params.amount : 0,
|
|
915
|
+
currency: params.currency,
|
|
916
|
+
customer: stringOrNull(params.customer),
|
|
917
|
+
description: stringOrNull(params.description),
|
|
918
|
+
metadata: recordOrEmpty(params.metadata),
|
|
919
|
+
payment_method: stringOrNull(params.payment_method),
|
|
920
|
+
client_secret: `${id}_secret_ghostapi`,
|
|
921
|
+
status: confirmed ? "succeeded" : "requires_payment_method",
|
|
922
|
+
confirmation_method: "automatic",
|
|
923
|
+
capture_method: "automatic",
|
|
924
|
+
latest_charge: confirmed ? `ch_${id}` : null,
|
|
925
|
+
livemode: false,
|
|
926
|
+
created: now,
|
|
927
|
+
};
|
|
566
928
|
}
|
|
567
929
|
function resolveIdempotencyReplay(state, request, parsed, now) {
|
|
568
930
|
if (parsed.idempotencyKey === undefined || !isMutation(request.method))
|
|
@@ -572,20 +934,22 @@ function resolveIdempotencyReplay(state, request, parsed, now) {
|
|
|
572
934
|
return null;
|
|
573
935
|
if (stored.created + IDEMPOTENCY_TTL_SECONDS <= now)
|
|
574
936
|
return null;
|
|
575
|
-
if (stored.method !== request.method ||
|
|
937
|
+
if (stored.method !== request.method ||
|
|
938
|
+
stored.path !== request.path ||
|
|
939
|
+
stored.params !== stableStringify(parsed.params)) {
|
|
576
940
|
return stripeFailure(400, "idempotency_error", "Keys for idempotent requests can only be used with the same parameters they were first used with.", undefined, "idempotency_key_in_use");
|
|
577
941
|
}
|
|
578
942
|
return {
|
|
579
943
|
...stored.response,
|
|
580
944
|
headers: { ...stored.response.headers, "x-ghostapi-idempotency": "REPLAY" },
|
|
581
|
-
body: restoreSyntheticClientSecret(stored.response.body)
|
|
945
|
+
body: restoreSyntheticClientSecret(stored.response.body),
|
|
582
946
|
};
|
|
583
947
|
}
|
|
584
948
|
function parseStripeRequest(request) {
|
|
585
949
|
const contentType = firstHeaderValue(request.headers["content-type"]) ?? "";
|
|
586
950
|
return {
|
|
587
951
|
params: parseStripeParams(request.body, contentType.includes("application/x-www-form-urlencoded")),
|
|
588
|
-
idempotencyKey: firstHeaderValue(request.headers["idempotency-key"])
|
|
952
|
+
idempotencyKey: firstHeaderValue(request.headers["idempotency-key"]),
|
|
589
953
|
};
|
|
590
954
|
}
|
|
591
955
|
function parseStripeParams(body, formEncoded) {
|
|
@@ -599,7 +963,10 @@ function parseStripeParams(body, formEncoded) {
|
|
|
599
963
|
return params;
|
|
600
964
|
}
|
|
601
965
|
function coerceFormParams(value) {
|
|
602
|
-
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
966
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
967
|
+
key,
|
|
968
|
+
coerceFormParameter(entry),
|
|
969
|
+
]));
|
|
603
970
|
}
|
|
604
971
|
function coerceFormParameter(value) {
|
|
605
972
|
if (Array.isArray(value))
|
|
@@ -664,19 +1031,47 @@ function stripeList(url, values, query) {
|
|
|
664
1031
|
start = Math.max(0, index - limit);
|
|
665
1032
|
}
|
|
666
1033
|
const data = values.slice(start, start + limit);
|
|
667
|
-
return stripeObject({
|
|
1034
|
+
return stripeObject({
|
|
1035
|
+
object: "list",
|
|
1036
|
+
data,
|
|
1037
|
+
has_more: start + limit < values.length,
|
|
1038
|
+
url,
|
|
1039
|
+
});
|
|
668
1040
|
}
|
|
669
1041
|
function stripeObject(body) {
|
|
670
1042
|
return { status: 200, headers: {}, body };
|
|
671
1043
|
}
|
|
672
1044
|
function stripeFailure(status, type, message, param, code) {
|
|
673
|
-
return {
|
|
1045
|
+
return {
|
|
1046
|
+
status,
|
|
1047
|
+
headers: {},
|
|
1048
|
+
body: stripeError({ status, type, message, param, code }),
|
|
1049
|
+
};
|
|
674
1050
|
}
|
|
675
1051
|
function stripeError(details) {
|
|
676
|
-
return {
|
|
1052
|
+
return {
|
|
1053
|
+
error: {
|
|
1054
|
+
type: details.type ?? "invalid_request_error",
|
|
1055
|
+
message: details.message,
|
|
1056
|
+
...(details.code === undefined ? {} : { code: details.code }),
|
|
1057
|
+
param: details.param ?? "unknown",
|
|
1058
|
+
},
|
|
1059
|
+
};
|
|
677
1060
|
}
|
|
678
1061
|
function cardDeclined() {
|
|
679
|
-
return {
|
|
1062
|
+
return {
|
|
1063
|
+
status: 402,
|
|
1064
|
+
headers: {},
|
|
1065
|
+
body: {
|
|
1066
|
+
error: {
|
|
1067
|
+
type: "card_error",
|
|
1068
|
+
code: "card_declined",
|
|
1069
|
+
decline_code: "generic_decline",
|
|
1070
|
+
message: "Your card was declined.",
|
|
1071
|
+
param: "payment_method",
|
|
1072
|
+
},
|
|
1073
|
+
},
|
|
1074
|
+
};
|
|
680
1075
|
}
|
|
681
1076
|
function unsupportedEndpoint(request) {
|
|
682
1077
|
return stripeFailure(404, "invalid_request_error", `Unsupported Stripe endpoint: ${request.method} ${request.path}. This GhostAPI Stripe pack supports Customers, Payment Intents, Payment Methods, Checkout Sessions, and Refunds.`, undefined, "resource_missing");
|
|
@@ -688,13 +1083,25 @@ function notFound(resource, id) {
|
|
|
688
1083
|
return stripeFailure(404, "invalid_request_error", `No such ${resource}: '${id ?? "unknown"}'`, "id", "resource_missing");
|
|
689
1084
|
}
|
|
690
1085
|
function missingParam(param) {
|
|
691
|
-
return {
|
|
1086
|
+
return {
|
|
1087
|
+
status: 400,
|
|
1088
|
+
type: "invalid_request_error",
|
|
1089
|
+
code: "parameter_missing",
|
|
1090
|
+
param,
|
|
1091
|
+
message: `Missing required param: ${param}.`,
|
|
1092
|
+
};
|
|
692
1093
|
}
|
|
693
1094
|
function invalidParam(param, message) {
|
|
694
1095
|
return stripeFailure(400, "invalid_request_error", message, param, "parameter_invalid_integer");
|
|
695
1096
|
}
|
|
696
1097
|
function invalidParamError(param, message) {
|
|
697
|
-
return {
|
|
1098
|
+
return {
|
|
1099
|
+
status: 400,
|
|
1100
|
+
type: "invalid_request_error",
|
|
1101
|
+
code: "parameter_invalid_integer",
|
|
1102
|
+
param,
|
|
1103
|
+
message,
|
|
1104
|
+
};
|
|
698
1105
|
}
|
|
699
1106
|
function getResource(state, id, object) {
|
|
700
1107
|
const value = state[`stripe:${id}`];
|
|
@@ -707,25 +1114,48 @@ function getResource(state, id, object) {
|
|
|
707
1114
|
}
|
|
708
1115
|
function listResources(state, object) {
|
|
709
1116
|
return Object.values(state)
|
|
710
|
-
.filter((value) => isJsonObject(value) &&
|
|
711
|
-
.
|
|
1117
|
+
.filter((value) => isJsonObject(value) &&
|
|
1118
|
+
value.object === object &&
|
|
1119
|
+
value.deleted !== true)
|
|
1120
|
+
.sort((left, right) => Number(right.created ?? 0) - Number(left.created ?? 0) ||
|
|
1121
|
+
String(right.id).localeCompare(String(left.id)))
|
|
712
1122
|
.map((value) => structuredClone(value));
|
|
713
1123
|
}
|
|
714
1124
|
function resourceFromResponse(body) {
|
|
715
|
-
return isJsonObject(body) &&
|
|
1125
|
+
return isJsonObject(body) &&
|
|
1126
|
+
typeof body.id === "string" &&
|
|
1127
|
+
typeof body.object === "string"
|
|
1128
|
+
? body
|
|
1129
|
+
: null;
|
|
716
1130
|
}
|
|
717
1131
|
function restoreSyntheticClientSecret(body) {
|
|
718
|
-
if (!isJsonObject(body) ||
|
|
1132
|
+
if (!isJsonObject(body) ||
|
|
1133
|
+
body.object !== "payment_intent" ||
|
|
1134
|
+
typeof body.id !== "string" ||
|
|
1135
|
+
body.client_secret !== "***")
|
|
719
1136
|
return body;
|
|
720
1137
|
return { ...body, client_secret: `${body.id}_secret_ghostapi` };
|
|
721
1138
|
}
|
|
722
1139
|
function readStripeRequest(value) {
|
|
723
1140
|
if (!isJsonObject(value) || !isJsonObject(value.params))
|
|
724
1141
|
return { params: {}, idempotencyKey: undefined };
|
|
725
|
-
return {
|
|
1142
|
+
return {
|
|
1143
|
+
params: value.params,
|
|
1144
|
+
idempotencyKey: typeof value.idempotencyKey === "string"
|
|
1145
|
+
? value.idempotencyKey
|
|
1146
|
+
: undefined,
|
|
1147
|
+
};
|
|
726
1148
|
}
|
|
727
1149
|
function isStoredIdempotency(value) {
|
|
728
|
-
return
|
|
1150
|
+
return (isJsonObject(value) &&
|
|
1151
|
+
typeof value.method === "string" &&
|
|
1152
|
+
typeof value.path === "string" &&
|
|
1153
|
+
typeof value.params === "string" &&
|
|
1154
|
+
typeof value.created === "number" &&
|
|
1155
|
+
isJsonObject(value.response) &&
|
|
1156
|
+
typeof value.response.status === "number" &&
|
|
1157
|
+
isJsonObject(value.response.headers) &&
|
|
1158
|
+
"body" in value.response);
|
|
729
1159
|
}
|
|
730
1160
|
function hasPositiveInteger(value) {
|
|
731
1161
|
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
@@ -740,26 +1170,43 @@ function parseRecurring(value) {
|
|
|
740
1170
|
if (value === undefined)
|
|
741
1171
|
return { value: null, error: null };
|
|
742
1172
|
if (!isJsonObject(value) || typeof value.interval !== "string")
|
|
743
|
-
return {
|
|
1173
|
+
return {
|
|
1174
|
+
value: null,
|
|
1175
|
+
error: "Recurring prices require recurring[interval] to be day, week, month, or year.",
|
|
1176
|
+
};
|
|
744
1177
|
if (!isRecurringInterval(value.interval))
|
|
745
|
-
return {
|
|
1178
|
+
return {
|
|
1179
|
+
value: null,
|
|
1180
|
+
error: "Recurring prices require recurring[interval] to be day, week, month, or year.",
|
|
1181
|
+
};
|
|
746
1182
|
const intervalCount = readPositiveInteger(value.interval_count) ?? 1;
|
|
747
|
-
return {
|
|
1183
|
+
return {
|
|
1184
|
+
value: { interval: value.interval, interval_count: intervalCount },
|
|
1185
|
+
error: null,
|
|
1186
|
+
};
|
|
748
1187
|
}
|
|
749
1188
|
function recurringSeconds(value) {
|
|
750
1189
|
if (!isJsonObject(value) || typeof value.interval !== "string")
|
|
751
1190
|
return 30 * 86_400;
|
|
752
1191
|
const count = readPositiveInteger(value.interval_count) ?? 1;
|
|
753
|
-
const unit = value.interval === "day"
|
|
1192
|
+
const unit = value.interval === "day"
|
|
1193
|
+
? 86_400
|
|
1194
|
+
: value.interval === "week"
|
|
1195
|
+
? 7 * 86_400
|
|
1196
|
+
: value.interval === "year"
|
|
1197
|
+
? 365 * 86_400
|
|
1198
|
+
: 30 * 86_400;
|
|
754
1199
|
return unit * count;
|
|
755
1200
|
}
|
|
756
1201
|
function subscriptionPriceId(items) {
|
|
757
1202
|
const first = firstSubscriptionItem(items);
|
|
758
|
-
return isJsonObject(first) && typeof first.price === "string"
|
|
1203
|
+
return isJsonObject(first) && typeof first.price === "string"
|
|
1204
|
+
? first.price
|
|
1205
|
+
: undefined;
|
|
759
1206
|
}
|
|
760
1207
|
function subscriptionQuantity(items) {
|
|
761
1208
|
const first = firstSubscriptionItem(items);
|
|
762
|
-
return isJsonObject(first) ? readPositiveInteger(first.quantity) ?? 1 : 1;
|
|
1209
|
+
return isJsonObject(first) ? (readPositiveInteger(first.quantity) ?? 1) : 1;
|
|
763
1210
|
}
|
|
764
1211
|
function firstSubscriptionItem(items) {
|
|
765
1212
|
if (Array.isArray(items))
|
|
@@ -779,13 +1226,17 @@ function missingParamResponse(param) {
|
|
|
779
1226
|
return stripeFailure(details.status, details.type ?? "invalid_request_error", details.message, details.param, String(details.code));
|
|
780
1227
|
}
|
|
781
1228
|
function isRecurringInterval(value) {
|
|
782
|
-
return value === "day" || value === "week" || value === "month" || value === "year";
|
|
1229
|
+
return (value === "day" || value === "week" || value === "month" || value === "year");
|
|
783
1230
|
}
|
|
784
1231
|
function isDeclined(params) {
|
|
785
|
-
return params.payment_method === "pm_card_chargeDeclined" ||
|
|
1232
|
+
return (params.payment_method === "pm_card_chargeDeclined" ||
|
|
1233
|
+
params.payment_method === "tok_chargeDeclined");
|
|
786
1234
|
}
|
|
787
1235
|
function isMutation(method) {
|
|
788
|
-
return method === "POST" ||
|
|
1236
|
+
return (method === "POST" ||
|
|
1237
|
+
method === "PUT" ||
|
|
1238
|
+
method === "PATCH" ||
|
|
1239
|
+
method === "DELETE");
|
|
789
1240
|
}
|
|
790
1241
|
function firstHeaderValue(value) {
|
|
791
1242
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -797,22 +1248,37 @@ function recordOrEmpty(value) {
|
|
|
797
1248
|
return isJsonObject(value) ? structuredClone(value) : {};
|
|
798
1249
|
}
|
|
799
1250
|
function pick(value, keys) {
|
|
800
|
-
return Object.fromEntries(keys
|
|
1251
|
+
return Object.fromEntries(keys
|
|
1252
|
+
.filter((key) => value[key] !== undefined)
|
|
1253
|
+
.map((key) => [key, structuredClone(value[key])]));
|
|
801
1254
|
}
|
|
802
1255
|
function numberOr(current, next) {
|
|
803
|
-
return typeof next === "number"
|
|
1256
|
+
return typeof next === "number"
|
|
1257
|
+
? next
|
|
1258
|
+
: typeof current === "number"
|
|
1259
|
+
? current
|
|
1260
|
+
: 0;
|
|
804
1261
|
}
|
|
805
1262
|
function checkoutAmount(params) {
|
|
806
1263
|
return checkoutLineItems(params).reduce((total, item) => {
|
|
807
|
-
if (total === null ||
|
|
1264
|
+
if (total === null ||
|
|
1265
|
+
!isJsonObject(item) ||
|
|
1266
|
+
!isJsonObject(item.price_data) ||
|
|
1267
|
+
typeof item.price_data.unit_amount !== "number")
|
|
808
1268
|
return null;
|
|
809
|
-
const quantity = typeof item.quantity === "number" && item.quantity > 0
|
|
1269
|
+
const quantity = typeof item.quantity === "number" && item.quantity > 0
|
|
1270
|
+
? item.quantity
|
|
1271
|
+
: 1;
|
|
810
1272
|
return total + item.price_data.unit_amount * quantity;
|
|
811
1273
|
}, 0);
|
|
812
1274
|
}
|
|
813
1275
|
function checkoutCurrency(params) {
|
|
814
1276
|
const first = checkoutLineItems(params)[0];
|
|
815
|
-
return isJsonObject(first) &&
|
|
1277
|
+
return isJsonObject(first) &&
|
|
1278
|
+
isJsonObject(first.price_data) &&
|
|
1279
|
+
typeof first.price_data.currency === "string"
|
|
1280
|
+
? first.price_data.currency
|
|
1281
|
+
: null;
|
|
816
1282
|
}
|
|
817
1283
|
function checkoutLineItems(params) {
|
|
818
1284
|
const lineItems = params.line_items;
|
|
@@ -846,7 +1312,10 @@ function stableStringify(value) {
|
|
|
846
1312
|
if (Array.isArray(value))
|
|
847
1313
|
return `[${value.map(stableStringify).join(",")}]`;
|
|
848
1314
|
if (isJsonObject(value))
|
|
849
|
-
return `{${Object.entries(value)
|
|
1315
|
+
return `{${Object.entries(value)
|
|
1316
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1317
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`)
|
|
1318
|
+
.join(",")}}`;
|
|
850
1319
|
return JSON.stringify(value);
|
|
851
1320
|
}
|
|
852
1321
|
//# sourceMappingURL=stripePack.js.map
|