@hearthkit/payments 0.2.0 → 0.4.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hearthkit/payments",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Stripe subscriptions and one-time purchases: catalog sync, hosted checkout, the hosted customer portal, and the webhook handler",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/chrisdevelops/hearthkit/tree/main/packages/payments",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"default": "./src/index.ts"
|
|
17
17
|
},
|
|
18
18
|
"./payments-contract": {
|
|
19
|
-
"types": "./src/payments-contract.ts",
|
|
20
|
-
"default": "./src/payments-contract.ts"
|
|
19
|
+
"types": "./src/payments-contract-entry.ts",
|
|
20
|
+
"default": "./src/payments-contract-entry.ts"
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"drizzle-orm": "0.45.2",
|
|
34
34
|
"stripe": "22.6.1",
|
|
35
35
|
"zod": "4.4.3",
|
|
36
|
-
"@hearthkit/config": "0.
|
|
37
|
-
"@hearthkit/db": "0.
|
|
38
|
-
"@hearthkit/auth": "0.
|
|
36
|
+
"@hearthkit/config": "0.4.0",
|
|
37
|
+
"@hearthkit/db": "0.4.0",
|
|
38
|
+
"@hearthkit/auth": "0.4.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "24.13.3",
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
import {
|
|
19
19
|
gatePaymentsCatalog,
|
|
20
20
|
gateWrongStripeWebhookSecret,
|
|
21
|
+
stripeWrongSchemeSignatureMessage,
|
|
22
|
+
stripeWrongSecretSignatureMessagePrefix,
|
|
21
23
|
uniqueGateBillingContactEmail,
|
|
22
24
|
uniqueGateBillingReferenceId,
|
|
23
25
|
uniqueGatePaymentsCatalogNames,
|
|
@@ -30,8 +32,6 @@ import {
|
|
|
30
32
|
import {
|
|
31
33
|
handleStripeWebhookResultSchema,
|
|
32
34
|
stripeSignatureHeaderName,
|
|
33
|
-
stripeWrongSchemeSignatureMessage,
|
|
34
|
-
stripeWrongSecretSignatureMessagePrefix,
|
|
35
35
|
type PaymentsClient,
|
|
36
36
|
} from './payments-contract.ts'
|
|
37
37
|
|
|
@@ -137,20 +137,11 @@ describe('payments-webhook-signature-invalid', () => {
|
|
|
137
137
|
// for payload.` is the wrong-secret message, and `No signatures found with expected scheme` is
|
|
138
138
|
// thrown one line away for a header carrying no v1 entry, which is a different cause. A
|
|
139
139
|
// substring test on `No signatures found` would satisfy both, so this matches the whole prefix
|
|
140
|
-
// and then proves the decoy is not what came back.
|
|
140
|
+
// and then proves the decoy is not what came back. Both strings are read off stripe@22.6.1 and
|
|
141
|
+
// live in the fixtures: their only reader is this gate, so they are not public surface.
|
|
141
142
|
const stripeFailureDetail = failure.stripeFailureDetail ?? ''
|
|
142
|
-
expect(stripeFailureDetail).toContain(
|
|
143
|
-
|
|
144
|
-
stripeWrongSecretSignatureMessagePrefix,
|
|
145
|
-
'stripeWrongSecretSignatureMessagePrefix',
|
|
146
|
-
),
|
|
147
|
-
)
|
|
148
|
-
expect(stripeFailureDetail).not.toContain(
|
|
149
|
-
expectContractStringExport(
|
|
150
|
-
stripeWrongSchemeSignatureMessage,
|
|
151
|
-
'stripeWrongSchemeSignatureMessage',
|
|
152
|
-
),
|
|
153
|
-
)
|
|
143
|
+
expect(stripeFailureDetail).toContain(stripeWrongSecretSignatureMessagePrefix)
|
|
144
|
+
expect(stripeFailureDetail).not.toContain(stripeWrongSchemeSignatureMessage)
|
|
154
145
|
|
|
155
146
|
// StripeSignatureVerificationError carries the raw webhook body on `.payload`, so it holds
|
|
156
147
|
// whatever customer data the event held. The detail quotes the message and never the payload.
|
package/src/index.ts
CHANGED
|
@@ -27,164 +27,37 @@ export { verifyPaymentsTablesExist } from './verify-payments-tables-exist.ts'
|
|
|
27
27
|
/** The Drizzle table map, always carrying all three tables whatever the organizations flag is. */
|
|
28
28
|
export { hearthkitPaymentsDrizzleSchema } from './hearthkit-payments-drizzle-schema.ts'
|
|
29
29
|
|
|
30
|
-
/** Contract values: the
|
|
30
|
+
/** Contract values: this package's env fragment, the failure union every function returns, and the catalog schema an app parses payments-catalog.ts with. */
|
|
31
31
|
export {
|
|
32
|
-
paymentsCatalogInvalidErrorPrefix,
|
|
33
|
-
paymentsCustomerNotFoundErrorPrefix,
|
|
34
|
-
paymentsDatabaseUnavailableErrorPrefix,
|
|
35
|
-
paymentsInputInvalidErrorPrefix,
|
|
36
|
-
paymentsPriceNotFoundErrorPrefix,
|
|
37
|
-
paymentsRequestFailedErrorPrefix,
|
|
38
|
-
paymentsStripeUnauthorizedErrorPrefix,
|
|
39
|
-
paymentsStripeUnreachableErrorPrefix,
|
|
40
|
-
paymentsWebhookSignatureInvalidErrorPrefix,
|
|
41
|
-
} from './payments-contract.ts'
|
|
42
|
-
|
|
43
|
-
/** Contract values: the Stripe literals this package matches by exact equality, each read off stripe@22.6.1 rather than from documentation. */
|
|
44
|
-
export {
|
|
45
|
-
stripeForbiddenHttpStatus,
|
|
46
|
-
stripeOneTimeCheckoutMode,
|
|
47
|
-
stripeOneTimePriceType,
|
|
48
|
-
stripeParsedBodySignatureMessagePrefix,
|
|
49
|
-
stripeRecurringPriceType,
|
|
50
|
-
stripeSignatureHeaderName,
|
|
51
|
-
stripeSignatureScheme,
|
|
52
|
-
stripeSignatureToleranceSeconds,
|
|
53
|
-
stripeSubscriptionCheckoutMode,
|
|
54
|
-
stripeUnauthorizedHttpStatus,
|
|
55
|
-
stripeWrongSchemeSignatureMessage,
|
|
56
|
-
stripeWrongSecretSignatureMessagePrefix,
|
|
57
|
-
} from './payments-contract.ts'
|
|
58
|
-
|
|
59
|
-
/** Contract values: the Stripe metadata keys createCheckoutSession writes and the webhook handler reads back. */
|
|
60
|
-
export {
|
|
61
|
-
hearthkitBillingReferenceMetadataKey,
|
|
62
|
-
hearthkitBillingScopeMetadataKey,
|
|
63
|
-
hearthkitPriceNameMetadataKey,
|
|
64
|
-
hearthkitQuantityMetadataKey,
|
|
65
|
-
hearthkitStripePriceIdMetadataKey,
|
|
66
|
-
} from './payments-contract.ts'
|
|
67
|
-
|
|
68
|
-
/** Contract values: the name lists, status lists and limits an app and a gate compare against. */
|
|
69
|
-
export {
|
|
70
|
-
defaultCheckoutQuantity,
|
|
71
|
-
handledStripeWebhookEventTypeSchema,
|
|
72
|
-
handledStripeWebhookEventTypes,
|
|
73
|
-
hearthkitPaymentsTableNameSchema,
|
|
74
|
-
hearthkitPaymentsTableNames,
|
|
75
|
-
maximumPaymentsPriceNameLength,
|
|
76
|
-
maximumPaymentsProductNameLength,
|
|
77
|
-
paymentsActiveSubscriptionStatuses,
|
|
78
|
-
paymentsDatabaseUnavailableCauseCodes,
|
|
79
|
-
paymentsKnownSubscriptionStatuses,
|
|
80
|
-
} from './payments-contract.ts'
|
|
81
|
-
|
|
82
|
-
/** Contract values: this package's env fragment and the two secret schemas it is built from. */
|
|
83
|
-
export {
|
|
84
|
-
paymentsEnvSchemaFragment,
|
|
85
|
-
stripeSecretKeySchema,
|
|
86
|
-
stripeWebhookSecretSchema,
|
|
87
|
-
} from './payments-contract.ts'
|
|
88
|
-
|
|
89
|
-
/** Contract values: the branded vocabulary schemas, so an app can parse an untrusted value before calling anything here. */
|
|
90
|
-
export {
|
|
91
|
-
billingContactEmailSchema,
|
|
92
|
-
billingReferenceIdSchema,
|
|
93
|
-
billingScopeSchema,
|
|
94
|
-
paymentsCurrencyCodeSchema,
|
|
95
|
-
paymentsCustomerIdSchema,
|
|
96
|
-
paymentsPriceNameSchema,
|
|
97
|
-
paymentsProductNameSchema,
|
|
98
|
-
paymentsPurchaseIdSchema,
|
|
99
|
-
paymentsQuantitySchema,
|
|
100
|
-
paymentsRecurringIntervalSchema,
|
|
101
|
-
paymentsRedirectUrlSchema,
|
|
102
|
-
paymentsStripeApiBaseUrlSchema,
|
|
103
|
-
paymentsSubscriptionIdSchema,
|
|
104
|
-
paymentsSubscriptionStatusSchema,
|
|
105
|
-
paymentsUnitAmountMinorUnitsSchema,
|
|
106
|
-
stripeCheckoutSessionIdSchema,
|
|
107
|
-
stripeCustomerIdSchema,
|
|
108
|
-
stripeEventIdSchema,
|
|
109
|
-
stripePaymentIntentIdSchema,
|
|
110
|
-
stripePriceIdSchema,
|
|
111
|
-
stripeProductIdSchema,
|
|
112
|
-
stripeSubscriptionIdSchema,
|
|
113
|
-
} from './payments-contract.ts'
|
|
114
|
-
|
|
115
|
-
/** Contract values: the catalog schemas an app validates payments-catalog.ts with, and the three row shapes this package reports. */
|
|
116
|
-
export {
|
|
117
|
-
paymentsCatalogPriceSchema,
|
|
118
|
-
paymentsCatalogProductSchema,
|
|
119
32
|
paymentsCatalogSchema,
|
|
120
|
-
|
|
121
|
-
paymentsOneTimeCatalogPriceSchema,
|
|
122
|
-
paymentsPurchaseSchema,
|
|
123
|
-
paymentsSubscriptionCatalogPriceSchema,
|
|
124
|
-
paymentsSubscriptionSchema,
|
|
125
|
-
} from './payments-contract.ts'
|
|
126
|
-
|
|
127
|
-
/** Contract values: each failure variant's schema, the discriminator enums, and the union of all nine. */
|
|
128
|
-
export {
|
|
129
|
-
paymentsCatalogInvalidFailureSchema,
|
|
130
|
-
paymentsCatalogIssueKindSchema,
|
|
131
|
-
paymentsCatalogIssueSchema,
|
|
132
|
-
paymentsCustomerNotFoundFailureSchema,
|
|
133
|
-
paymentsDatabaseUnavailableFailureSchema,
|
|
33
|
+
paymentsEnvSchemaFragment,
|
|
134
34
|
paymentsFailureSchema,
|
|
135
|
-
paymentsInputInvalidFailureSchema,
|
|
136
|
-
paymentsInvalidFieldNameSchema,
|
|
137
|
-
paymentsPriceNotFoundFailureSchema,
|
|
138
|
-
paymentsRequestFailedFailureSchema,
|
|
139
|
-
paymentsStripeUnauthorizedFailureSchema,
|
|
140
|
-
paymentsStripeUnreachableFailureSchema,
|
|
141
|
-
paymentsWebhookSignatureInvalidFailureSchema,
|
|
142
|
-
priceLookupFailureSchema,
|
|
143
|
-
webhookSignatureFailureReasonSchema,
|
|
144
|
-
} from './payments-contract.ts'
|
|
145
|
-
|
|
146
|
-
/** Contract values: the runtime checks for the three structural values this package takes in or hands back. */
|
|
147
|
-
export {
|
|
148
|
-
paymentsClientSchema,
|
|
149
|
-
paymentsDrizzleClientSchema,
|
|
150
|
-
paymentsRequestHeadersSchema,
|
|
151
35
|
} from './payments-contract.ts'
|
|
152
36
|
|
|
153
|
-
/** Contract values:
|
|
37
|
+
/** Contract values: the full result schema of each of the eight functions, for validating a value that crossed a process or network boundary. */
|
|
154
38
|
export {
|
|
155
|
-
createCheckoutSessionOptionsSchema,
|
|
156
39
|
createCheckoutSessionResultSchema,
|
|
157
|
-
createCustomerPortalSessionOptionsSchema,
|
|
158
40
|
createCustomerPortalSessionResultSchema,
|
|
159
|
-
createPaymentsClientOptionsSchema,
|
|
160
41
|
createPaymentsClientResultSchema,
|
|
161
|
-
handleStripeWebhookOptionsSchema,
|
|
162
42
|
handleStripeWebhookResultSchema,
|
|
163
|
-
listPaymentsPurchasesOptionsSchema,
|
|
164
43
|
listPaymentsPurchasesResultSchema,
|
|
165
|
-
paymentsCatalogSyncedSchema,
|
|
166
|
-
paymentsCheckoutSessionCreatedSchema,
|
|
167
|
-
paymentsClientCreatedSchema,
|
|
168
|
-
paymentsPortalSessionCreatedSchema,
|
|
169
|
-
paymentsPriceSyncActionSchema,
|
|
170
|
-
paymentsPurchasesListedSchema,
|
|
171
|
-
paymentsSubscriptionAbsentSchema,
|
|
172
|
-
paymentsSubscriptionFoundSchema,
|
|
173
|
-
paymentsSyncedPriceSchema,
|
|
174
|
-
paymentsTablesMissingSchema,
|
|
175
|
-
paymentsTablesPresentSchema,
|
|
176
|
-
paymentsWebhookIgnoredReasonSchema,
|
|
177
|
-
paymentsWebhookIgnoredSchema,
|
|
178
|
-
paymentsWebhookOutcomeSchema,
|
|
179
|
-
paymentsWebhookProcessedSchema,
|
|
180
|
-
readPaymentsSubscriptionOptionsSchema,
|
|
181
44
|
readPaymentsSubscriptionResultSchema,
|
|
182
|
-
syncPaymentsCatalogOptionsSchema,
|
|
183
45
|
syncPaymentsCatalogResultSchema,
|
|
184
|
-
verifyPaymentsTablesExistOptionsSchema,
|
|
185
46
|
verifyPaymentsTablesExistResultSchema,
|
|
186
47
|
} from './payments-contract.ts'
|
|
187
48
|
|
|
49
|
+
/** Contract values: the table names drizzle-kit is pointed at, and the per-price row shape a sync run reports. */
|
|
50
|
+
export { hearthkitPaymentsTableNames, paymentsSyncedPriceSchema } from './payments-contract.ts'
|
|
51
|
+
|
|
52
|
+
/** Contract values: the five wire constants an app needs to sign a webhook body and read Stripe metadata back. */
|
|
53
|
+
export {
|
|
54
|
+
hearthkitBillingReferenceMetadataKey,
|
|
55
|
+
hearthkitPriceNameMetadataKey,
|
|
56
|
+
hearthkitQuantityMetadataKey,
|
|
57
|
+
hearthkitStripePriceIdMetadataKey,
|
|
58
|
+
stripeSignatureHeaderName,
|
|
59
|
+
} from './payments-contract.ts'
|
|
60
|
+
|
|
188
61
|
/** Contract types: the branded vocabulary and the two scope words billing is keyed on. */
|
|
189
62
|
export type {
|
|
190
63
|
BillingContactEmail,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ./payments-contract subpath of @hearthkit/payments: a named re-export of the eighteen contract
|
|
3
|
+
* values that live in payments-contract.ts and every public type, and nothing that reaches an
|
|
4
|
+
* implementation module. It exists because @hearthkit/cli imports paymentsFailureSchema and
|
|
5
|
+
* paymentsSyncedPriceSchema at runtime (plus the PaymentsFailure, PaymentsCatalog and
|
|
6
|
+
* PaymentsEnvValues types, and paymentsCatalogSchema in its fixture) and templates/app imports
|
|
7
|
+
* hearthkitPaymentsTableNames and the five wire constants, while the `.` entry imports stripe,
|
|
8
|
+
* drizzle-orm/pg-core and the Drizzle table definitions, none of which the cli's `hearthkit payments
|
|
9
|
+
* sync` or templates/app's drizzle.config.ts should have to load to read a schema. This file and
|
|
10
|
+
* payments-contract.ts import only zod at runtime, so either loads under bare node with no side
|
|
11
|
+
* effect.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Contract values: this package's env fragment, the failure union every function returns, and the catalog schema an app parses payments-catalog.ts with. */
|
|
15
|
+
export {
|
|
16
|
+
paymentsCatalogSchema,
|
|
17
|
+
paymentsEnvSchemaFragment,
|
|
18
|
+
paymentsFailureSchema,
|
|
19
|
+
} from './payments-contract.ts'
|
|
20
|
+
|
|
21
|
+
/** Contract values: the full result schema of each of the eight functions, for validating a value that crossed a process or network boundary. */
|
|
22
|
+
export {
|
|
23
|
+
createCheckoutSessionResultSchema,
|
|
24
|
+
createCustomerPortalSessionResultSchema,
|
|
25
|
+
createPaymentsClientResultSchema,
|
|
26
|
+
handleStripeWebhookResultSchema,
|
|
27
|
+
listPaymentsPurchasesResultSchema,
|
|
28
|
+
readPaymentsSubscriptionResultSchema,
|
|
29
|
+
syncPaymentsCatalogResultSchema,
|
|
30
|
+
verifyPaymentsTablesExistResultSchema,
|
|
31
|
+
} from './payments-contract.ts'
|
|
32
|
+
|
|
33
|
+
/** Contract values: the table names drizzle-kit is pointed at, and the per-price row shape a sync run reports. */
|
|
34
|
+
export { hearthkitPaymentsTableNames, paymentsSyncedPriceSchema } from './payments-contract.ts'
|
|
35
|
+
|
|
36
|
+
/** Contract values: the five wire constants an app needs to sign a webhook body and read Stripe metadata back. */
|
|
37
|
+
export {
|
|
38
|
+
hearthkitBillingReferenceMetadataKey,
|
|
39
|
+
hearthkitPriceNameMetadataKey,
|
|
40
|
+
hearthkitQuantityMetadataKey,
|
|
41
|
+
hearthkitStripePriceIdMetadataKey,
|
|
42
|
+
stripeSignatureHeaderName,
|
|
43
|
+
} from './payments-contract.ts'
|
|
44
|
+
|
|
45
|
+
/** Contract types: the branded vocabulary and the two scope words billing is keyed on. */
|
|
46
|
+
export type {
|
|
47
|
+
BillingContactEmail,
|
|
48
|
+
BillingReferenceId,
|
|
49
|
+
BillingReferenceOwnerId,
|
|
50
|
+
BillingScope,
|
|
51
|
+
HandledStripeWebhookEventType,
|
|
52
|
+
HearthkitPaymentsTableName,
|
|
53
|
+
PaymentsCurrencyCode,
|
|
54
|
+
PaymentsCustomerId,
|
|
55
|
+
PaymentsPriceName,
|
|
56
|
+
PaymentsProductName,
|
|
57
|
+
PaymentsPurchaseId,
|
|
58
|
+
PaymentsRecurringInterval,
|
|
59
|
+
PaymentsSubscriptionId,
|
|
60
|
+
StripeCheckoutSessionId,
|
|
61
|
+
StripeCustomerId,
|
|
62
|
+
StripeEventId,
|
|
63
|
+
StripePaymentIntentId,
|
|
64
|
+
StripePriceId,
|
|
65
|
+
StripeProductId,
|
|
66
|
+
StripeSecretKey,
|
|
67
|
+
StripeSubscriptionId,
|
|
68
|
+
StripeWebhookSecret,
|
|
69
|
+
} from './payments-contract.ts'
|
|
70
|
+
|
|
71
|
+
/** Contract types: the env values, the catalog, the three records this package reports, and the Drizzle table map. */
|
|
72
|
+
export type {
|
|
73
|
+
HearthkitPaymentsDrizzleSchema,
|
|
74
|
+
PaymentsCatalog,
|
|
75
|
+
PaymentsCatalogPrice,
|
|
76
|
+
PaymentsCatalogProduct,
|
|
77
|
+
PaymentsClient,
|
|
78
|
+
PaymentsCustomer,
|
|
79
|
+
PaymentsEnvValues,
|
|
80
|
+
PaymentsPurchase,
|
|
81
|
+
PaymentsSubscription,
|
|
82
|
+
PaymentsSyncedPrice,
|
|
83
|
+
} from './payments-contract.ts'
|
|
84
|
+
|
|
85
|
+
/** Contract types: the failure union and every discriminator carried on one. */
|
|
86
|
+
export type {
|
|
87
|
+
PaymentsCatalogIssue,
|
|
88
|
+
PaymentsCatalogIssueKind,
|
|
89
|
+
PaymentsFailure,
|
|
90
|
+
PaymentsInvalidFieldName,
|
|
91
|
+
PaymentsPriceSyncAction,
|
|
92
|
+
PaymentsWebhookIgnoredReason,
|
|
93
|
+
PaymentsWebhookOutcome,
|
|
94
|
+
PriceLookupFailure,
|
|
95
|
+
WebhookSignatureFailureReason,
|
|
96
|
+
} from './payments-contract.ts'
|
|
97
|
+
|
|
98
|
+
/** Contract types: the option, result and function shapes of every function above. */
|
|
99
|
+
export type {
|
|
100
|
+
CreateCheckoutSession,
|
|
101
|
+
CreateCheckoutSessionOptions,
|
|
102
|
+
CreateCheckoutSessionResult,
|
|
103
|
+
CreateCustomerPortalSession,
|
|
104
|
+
CreateCustomerPortalSessionOptions,
|
|
105
|
+
CreateCustomerPortalSessionResult,
|
|
106
|
+
CreatePaymentsClient,
|
|
107
|
+
CreatePaymentsClientOptions,
|
|
108
|
+
CreatePaymentsClientResult,
|
|
109
|
+
HandleStripeWebhook,
|
|
110
|
+
HandleStripeWebhookOptions,
|
|
111
|
+
HandleStripeWebhookResult,
|
|
112
|
+
ListPaymentsPurchases,
|
|
113
|
+
ListPaymentsPurchasesOptions,
|
|
114
|
+
ListPaymentsPurchasesResult,
|
|
115
|
+
ReadPaymentsSubscription,
|
|
116
|
+
ReadPaymentsSubscriptionOptions,
|
|
117
|
+
ReadPaymentsSubscriptionResult,
|
|
118
|
+
SyncPaymentsCatalog,
|
|
119
|
+
SyncPaymentsCatalogOptions,
|
|
120
|
+
SyncPaymentsCatalogResult,
|
|
121
|
+
VerifyPaymentsTablesExist,
|
|
122
|
+
VerifyPaymentsTablesExistOptions,
|
|
123
|
+
VerifyPaymentsTablesExistResult,
|
|
124
|
+
} from './payments-contract.ts'
|
package/src/payments-contract.ts
CHANGED
|
@@ -34,27 +34,6 @@ export const paymentsRequestFailedErrorPrefix = 'hearthkit payments request fail
|
|
|
34
34
|
/** HTTP header Stripe signs a webhook delivery with; this package reads it off the request headers so the name is spelled once. */
|
|
35
35
|
export const stripeSignatureHeaderName = 'stripe-signature'
|
|
36
36
|
|
|
37
|
-
/** Signature scheme inside that header, read off stripe@22.6.1's EXPECTED_SCHEME; every entry this package verifies is a v1 entry. */
|
|
38
|
-
export const stripeSignatureScheme = 'v1'
|
|
39
|
-
|
|
40
|
-
/** Seconds a signed webhook stays acceptable, read off stripe@22.6.1's DEFAULT_TOLERANCE; an older signature is rejected as invalid. */
|
|
41
|
-
export const stripeSignatureToleranceSeconds = 300
|
|
42
|
-
|
|
43
|
-
// Measured in stripe@22.6.1's esm/Webhooks.js:184. THE DECOY IS ONE LINE AWAY IN THE SAME FILE:
|
|
44
|
-
// `No signatures found with expected scheme` (line 130) is thrown when the header parses but carries
|
|
45
|
-
// no v1 entry, which is a DIFFERENT cause. A gate matching the substring `No signatures found`
|
|
46
|
-
// satisfies both, so a wrong-secret gate must match this whole prefix and not the shared words.
|
|
47
|
-
/** Opening words of the message stripe throws when the signing secret is wrong; the value a wrong-secret gate matches on. */
|
|
48
|
-
export const stripeWrongSecretSignatureMessagePrefix =
|
|
49
|
-
'No signatures found matching the expected signature for payload.'
|
|
50
|
-
|
|
51
|
-
/** Message stripe throws when the header parses but carries no v1 entry; named only so nobody mistakes it for the wrong-secret one. */
|
|
52
|
-
export const stripeWrongSchemeSignatureMessage = 'No signatures found with expected scheme'
|
|
53
|
-
|
|
54
|
-
/** Opening words of the message stripe throws when a parsed object was passed instead of the raw request body. */
|
|
55
|
-
export const stripeParsedBodySignatureMessagePrefix =
|
|
56
|
-
'Webhook payload must be provided as a string or a Buffer'
|
|
57
|
-
|
|
58
37
|
/** HTTP status Stripe answers with when the API key is wrong or revoked; mapped to payments-stripe-unauthorized. */
|
|
59
38
|
export const stripeUnauthorizedHttpStatus = 401
|
|
60
39
|
|
|
@@ -695,8 +674,8 @@ export type CreatePaymentsClientOptions = {
|
|
|
695
674
|
stripeApiBaseUrl?: string
|
|
696
675
|
}
|
|
697
676
|
|
|
698
|
-
/** Success shape of createPaymentsClient; the flag and the scope are echoed back so a caller holding the result knows which mode it built. */
|
|
699
|
-
|
|
677
|
+
/** Success shape of createPaymentsClient; module-private, reachable only through the result union below. The flag and the scope are echoed back so a caller holding the result knows which mode it built. */
|
|
678
|
+
const paymentsClientCreatedSchema = z.object({
|
|
700
679
|
kind: z.literal('payments-client-created'),
|
|
701
680
|
paymentsClient: paymentsClientSchema,
|
|
702
681
|
organizationsEnabled: z.boolean(),
|
|
@@ -747,8 +726,8 @@ export type SyncPaymentsCatalogOptions = {
|
|
|
747
726
|
// stripeLivemode is read off the Stripe object the API answered with, whose own generated type says
|
|
748
727
|
// it is true in live mode and false in test mode. It is here so a gate can refuse to touch a live
|
|
749
728
|
// account without matching on an API key prefix, which is a constant nothing offline can verify.
|
|
750
|
-
/** Success shape of syncPaymentsCatalog; stripeLivemode is the measured guard a gate asserts false before creating anything. */
|
|
751
|
-
|
|
729
|
+
/** Success shape of syncPaymentsCatalog; module-private. stripeLivemode is the measured guard a gate asserts false before creating anything. */
|
|
730
|
+
const paymentsCatalogSyncedSchema = z.object({
|
|
752
731
|
kind: z.literal('payments-catalog-synced'),
|
|
753
732
|
syncedPrices: z.array(paymentsSyncedPriceSchema).min(1),
|
|
754
733
|
stripeLivemode: z.boolean(),
|
|
@@ -790,8 +769,8 @@ export type CreateCheckoutSessionOptions = {
|
|
|
790
769
|
cancelUrl: string
|
|
791
770
|
}
|
|
792
771
|
|
|
793
|
-
/** Success shape of createCheckoutSession; checkoutUrl is the hosted page to redirect the buyer to. */
|
|
794
|
-
|
|
772
|
+
/** Success shape of createCheckoutSession; module-private. checkoutUrl is the hosted page to redirect the buyer to. */
|
|
773
|
+
const paymentsCheckoutSessionCreatedSchema = z.object({
|
|
795
774
|
kind: z.literal('payments-checkout-session-created'),
|
|
796
775
|
stripeCheckoutSessionId: stripeCheckoutSessionIdSchema,
|
|
797
776
|
checkoutUrl: z.url(),
|
|
@@ -828,8 +807,8 @@ export type CreateCustomerPortalSessionOptions = {
|
|
|
828
807
|
returnUrl: string
|
|
829
808
|
}
|
|
830
809
|
|
|
831
|
-
/** Success shape of createCustomerPortalSession; portalUrl is short-lived, so it is redirected to rather than stored. */
|
|
832
|
-
|
|
810
|
+
/** Success shape of createCustomerPortalSession; module-private. portalUrl is short-lived, so it is redirected to rather than stored. */
|
|
811
|
+
const paymentsPortalSessionCreatedSchema = z.object({
|
|
833
812
|
kind: z.literal('payments-portal-session-created'),
|
|
834
813
|
portalUrl: z.url(),
|
|
835
814
|
stripeCustomerId: stripeCustomerIdSchema,
|
|
@@ -899,16 +878,16 @@ export type HandleStripeWebhookOptions = {
|
|
|
899
878
|
requestHeaders: Headers
|
|
900
879
|
}
|
|
901
880
|
|
|
902
|
-
/** Result shape when the delivery was acted on;
|
|
903
|
-
|
|
881
|
+
/** Result shape when the delivery was acted on; module-private. A replay of the same event writes the same row and moves no count. */
|
|
882
|
+
const paymentsWebhookProcessedSchema = z.object({
|
|
904
883
|
kind: z.literal('payments-webhook-processed'),
|
|
905
884
|
stripeEventId: stripeEventIdSchema,
|
|
906
885
|
stripeEventType: z.string().min(1),
|
|
907
886
|
webhookOutcome: paymentsWebhookOutcomeSchema,
|
|
908
887
|
})
|
|
909
888
|
|
|
910
|
-
/** Result shape when the delivery was verified but not acted on;
|
|
911
|
-
|
|
889
|
+
/** Result shape when the delivery was verified but not acted on; module-private. A successful answer, not a failure, because most events are none of our business. */
|
|
890
|
+
const paymentsWebhookIgnoredSchema = z.object({
|
|
912
891
|
kind: z.literal('payments-webhook-ignored'),
|
|
913
892
|
stripeEventId: stripeEventIdSchema,
|
|
914
893
|
stripeEventType: z.string().min(1),
|
|
@@ -942,14 +921,14 @@ export type ReadPaymentsSubscriptionOptions = {
|
|
|
942
921
|
billingReferenceId: string
|
|
943
922
|
}
|
|
944
923
|
|
|
945
|
-
/** Result shape when a subscription row exists;
|
|
946
|
-
|
|
924
|
+
/** Result shape when a subscription row exists; module-private. The row is returned whatever its status, and the caller decides what counts as entitled. */
|
|
925
|
+
const paymentsSubscriptionFoundSchema = z.object({
|
|
947
926
|
kind: z.literal('payments-subscription-found'),
|
|
948
927
|
paymentsSubscription: paymentsSubscriptionSchema,
|
|
949
928
|
})
|
|
950
929
|
|
|
951
|
-
/** Result shape when the reference has no subscription row at all;
|
|
952
|
-
|
|
930
|
+
/** Result shape when the reference has no subscription row at all; module-private. Nobody having a subscription is a normal answer, not a failure. */
|
|
931
|
+
const paymentsSubscriptionAbsentSchema = z.object({
|
|
953
932
|
kind: z.literal('payments-subscription-absent'),
|
|
954
933
|
})
|
|
955
934
|
|
|
@@ -980,8 +959,8 @@ export type ListPaymentsPurchasesOptions = {
|
|
|
980
959
|
billingReferenceId: string
|
|
981
960
|
}
|
|
982
961
|
|
|
983
|
-
/** Success shape of listPaymentsPurchases;
|
|
984
|
-
|
|
962
|
+
/** Success shape of listPaymentsPurchases; module-private. An empty array is a success, because "has bought nothing" is a correct answer. */
|
|
963
|
+
const paymentsPurchasesListedSchema = z.object({
|
|
985
964
|
kind: z.literal('payments-purchases-listed'),
|
|
986
965
|
paymentsPurchases: z.array(paymentsPurchaseSchema),
|
|
987
966
|
})
|
|
@@ -1010,14 +989,14 @@ export type VerifyPaymentsTablesExistOptions = {
|
|
|
1010
989
|
drizzleClient: NodePgDatabase<Record<string, unknown>>
|
|
1011
990
|
}
|
|
1012
991
|
|
|
1013
|
-
/** Result shape when every payments table is present in the public schema, in both user-scoped and org-scoped mode. */
|
|
1014
|
-
|
|
992
|
+
/** Result shape when every payments table is present in the public schema; module-private, and true in both user-scoped and org-scoped mode. */
|
|
993
|
+
const paymentsTablesPresentSchema = z.object({
|
|
1015
994
|
kind: z.literal('payments-tables-present'),
|
|
1016
995
|
presentTableNames: z.array(hearthkitPaymentsTableNameSchema).min(1),
|
|
1017
996
|
})
|
|
1018
997
|
|
|
1019
|
-
/** Result shape when at least one payments table is absent;
|
|
1020
|
-
|
|
998
|
+
/** Result shape when at least one payments table is absent; module-private, a successful check reporting a negative answer. */
|
|
999
|
+
const paymentsTablesMissingSchema = z.object({
|
|
1021
1000
|
kind: z.literal('payments-tables-missing'),
|
|
1022
1001
|
missingTableNames: z.array(hearthkitPaymentsTableNameSchema).min(1),
|
|
1023
1002
|
})
|
|
@@ -3,9 +3,12 @@ import { describe, expect, it } from 'vitest'
|
|
|
3
3
|
import { expectResultKind } from '../test-fixtures/payments-gate-expectations.ts'
|
|
4
4
|
import {
|
|
5
5
|
readPaymentsPackageManifest,
|
|
6
|
-
|
|
6
|
+
runPaymentsContractModulesUnderBareNode,
|
|
7
7
|
} from '../test-fixtures/payments-package-entry-points.ts'
|
|
8
8
|
import {
|
|
9
|
+
hearthkitPaymentsContractSubpathValueExportNames,
|
|
10
|
+
hearthkitPaymentsEntryValueExportNames,
|
|
11
|
+
importHearthkitPaymentsContractSubpathNamespace,
|
|
9
12
|
importHearthkitPaymentsNamespace,
|
|
10
13
|
loadHearthkitPaymentsEntry,
|
|
11
14
|
} from '../test-fixtures/hearthkit-payments-entry.ts'
|
|
@@ -18,61 +21,6 @@ const completePaymentsEnv = {
|
|
|
18
21
|
STRIPE_WEBHOOK_SECRET: 'whsec_GateSigningSecretThatIsNotRealEither',
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
// CONTRACT.md's entry point rule is mechanical: src/index.ts re-exports every value
|
|
22
|
-
// src/payments-contract.ts exports, with no exceptions. So the required list is read off the contract
|
|
23
|
-
// module's own namespace rather than typed out here. A hand-written list would fall behind the day
|
|
24
|
-
// someone forgot to extend it; a derived list cannot.
|
|
25
|
-
//
|
|
26
|
-
// A module namespace carries value exports only. Every `export type` in payments-contract.ts is
|
|
27
|
-
// erased before this file runs, so the types CONTRACT.md also asks the entry point to re-export are
|
|
28
|
-
// outside what this gate can see and are covered by typecheck instead.
|
|
29
|
-
function contractValueExportNames(contractModule: Record<string, unknown>): string[] {
|
|
30
|
-
return Object.keys(contractModule).toSorted()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Named in so many words by CONTRACT.md: the nine message prefixes, the env fragment, the failure
|
|
34
|
-
// union, the table name list, the two subscription status lists and the eight per-function result
|
|
35
|
-
// schemas. Spelled out as strings so renaming one in the contract fails this gate loudly, instead of
|
|
36
|
-
// quietly shrinking the derived list above to a set an entry point already satisfies.
|
|
37
|
-
const contractExportNamesTheContractNamesOutright = [
|
|
38
|
-
'paymentsInputInvalidErrorPrefix',
|
|
39
|
-
'paymentsCatalogInvalidErrorPrefix',
|
|
40
|
-
'paymentsPriceNotFoundErrorPrefix',
|
|
41
|
-
'paymentsCustomerNotFoundErrorPrefix',
|
|
42
|
-
'paymentsWebhookSignatureInvalidErrorPrefix',
|
|
43
|
-
'paymentsStripeUnauthorizedErrorPrefix',
|
|
44
|
-
'paymentsStripeUnreachableErrorPrefix',
|
|
45
|
-
'paymentsDatabaseUnavailableErrorPrefix',
|
|
46
|
-
'paymentsRequestFailedErrorPrefix',
|
|
47
|
-
'paymentsEnvSchemaFragment',
|
|
48
|
-
'paymentsFailureSchema',
|
|
49
|
-
'hearthkitPaymentsTableNames',
|
|
50
|
-
'paymentsKnownSubscriptionStatuses',
|
|
51
|
-
'paymentsActiveSubscriptionStatuses',
|
|
52
|
-
'createPaymentsClientResultSchema',
|
|
53
|
-
'syncPaymentsCatalogResultSchema',
|
|
54
|
-
'createCheckoutSessionResultSchema',
|
|
55
|
-
'createCustomerPortalSessionResultSchema',
|
|
56
|
-
'handleStripeWebhookResultSchema',
|
|
57
|
-
'readPaymentsSubscriptionResultSchema',
|
|
58
|
-
'listPaymentsPurchasesResultSchema',
|
|
59
|
-
'verifyPaymentsTablesExistResultSchema',
|
|
60
|
-
] as const
|
|
61
|
-
|
|
62
|
-
// Not exported by payments-contract.ts, so the derived list cannot cover them: the eight public
|
|
63
|
-
// functions and the Drizzle table map.
|
|
64
|
-
const entryPointOnlyExportNames = [
|
|
65
|
-
'createPaymentsClient',
|
|
66
|
-
'syncPaymentsCatalog',
|
|
67
|
-
'createCheckoutSession',
|
|
68
|
-
'createCustomerPortalSession',
|
|
69
|
-
'handleStripeWebhook',
|
|
70
|
-
'readPaymentsSubscription',
|
|
71
|
-
'listPaymentsPurchases',
|
|
72
|
-
'verifyPaymentsTablesExist',
|
|
73
|
-
'hearthkitPaymentsDrizzleSchema',
|
|
74
|
-
] as const
|
|
75
|
-
|
|
76
24
|
describe('paymentsEnvSchemaFragment', () => {
|
|
77
25
|
it('declares two required variables, names both at load when neither is set, treats an empty value as unset, and rejects one carrying whitespace', async () => {
|
|
78
26
|
const { paymentsEnvSchemaFragment } = await loadHearthkitPaymentsEntry()
|
|
@@ -140,30 +88,29 @@ describe('paymentsEnvSchemaFragment', () => {
|
|
|
140
88
|
})
|
|
141
89
|
|
|
142
90
|
describe('@hearthkit/payments entry point', () => {
|
|
143
|
-
it('
|
|
91
|
+
it('exports exactly the twenty-seven allowlisted values and nothing else, each contract value the one payments-contract.ts already exports', async () => {
|
|
144
92
|
const namespace = await importHearthkitPaymentsNamespace()
|
|
145
93
|
const contractModule = paymentsContract as unknown as Record<string, unknown>
|
|
146
|
-
const requiredExportNames = contractValueExportNames(contractModule)
|
|
147
|
-
|
|
148
|
-
const renamedInTheContract = contractExportNamesTheContractNamesOutright.filter(
|
|
149
|
-
(exportName) => !requiredExportNames.includes(exportName),
|
|
150
|
-
)
|
|
151
|
-
expect(
|
|
152
|
-
renamedInTheContract,
|
|
153
|
-
'payments-contract.ts must still export the constants and schemas CONTRACT.md names outright',
|
|
154
|
-
).toEqual([])
|
|
155
94
|
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
)
|
|
95
|
+
// A module namespace carries value exports only, so every `export type` is already erased here and
|
|
96
|
+
// the types CONTRACT.md keeps on the entry point are covered by typecheck instead. Both lists are
|
|
97
|
+
// compared whole rather than name by name, so a failure names every wrong export at once instead
|
|
98
|
+
// of stopping at the first and hiding the rest behind a rerun.
|
|
99
|
+
const actualValueExportNames = Object.keys(namespace)
|
|
100
|
+
.filter((exportName) => namespace[exportName] !== undefined)
|
|
101
|
+
.toSorted()
|
|
161
102
|
expect(
|
|
162
|
-
|
|
163
|
-
'src/index.ts must
|
|
164
|
-
).toEqual([])
|
|
165
|
-
|
|
166
|
-
|
|
103
|
+
actualValueExportNames,
|
|
104
|
+
'src/index.ts must export exactly the allowlist in CONTRACT.md "Package entry point"',
|
|
105
|
+
).toEqual([...hearthkitPaymentsEntryValueExportNames].toSorted())
|
|
106
|
+
|
|
107
|
+
// The eighteen contract values must be the identical values payments-contract.ts exports, not a
|
|
108
|
+
// second copy: an app comparing entry.hearthkitPaymentsTableNames against the subpath's value is
|
|
109
|
+
// comparing the same thing. The nine prefixes, the Stripe literals and HTTP statuses, the limits,
|
|
110
|
+
// the branded and record schemas, the options schemas and the eleven per-arm success shapes stay
|
|
111
|
+
// internal, so they are absent from the list above and a re-export of one of them fails the
|
|
112
|
+
// whole-list comparison by name.
|
|
113
|
+
const rebuiltInsteadOfReExported = hearthkitPaymentsContractSubpathValueExportNames.filter(
|
|
167
114
|
(exportName) => namespace[exportName] !== contractModule[exportName],
|
|
168
115
|
)
|
|
169
116
|
expect(
|
|
@@ -171,27 +118,62 @@ describe('@hearthkit/payments entry point', () => {
|
|
|
171
118
|
'these must be the identical value payments-contract.ts exports, not a second copy of it',
|
|
172
119
|
).toEqual([])
|
|
173
120
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
121
|
+
// Everything else on the allowlist comes from an implementation module: the eight functions, plus
|
|
122
|
+
// hearthkitPaymentsDrizzleSchema, which is the Drizzle table map and therefore an object. Whether
|
|
123
|
+
// it carries the right tables and columns is the schema gate's job, not this one's.
|
|
124
|
+
const wrongShape = hearthkitPaymentsEntryValueExportNames.filter((exportName) => {
|
|
125
|
+
const isContractValue = hearthkitPaymentsContractSubpathValueExportNames.includes(
|
|
126
|
+
exportName as (typeof hearthkitPaymentsContractSubpathValueExportNames)[number],
|
|
127
|
+
)
|
|
128
|
+
if (isContractValue) {
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
if (exportName === 'hearthkitPaymentsDrizzleSchema') {
|
|
132
|
+
return typeof namespace[exportName] !== 'object' || namespace[exportName] === null
|
|
133
|
+
}
|
|
134
|
+
return typeof namespace[exportName] !== 'function'
|
|
135
|
+
})
|
|
177
136
|
expect(
|
|
178
|
-
|
|
179
|
-
'
|
|
137
|
+
wrongShape,
|
|
138
|
+
'every allowlisted name outside the eighteen contract values must be one of the eight functions, or the Drizzle schema object',
|
|
180
139
|
).toEqual([])
|
|
181
140
|
})
|
|
182
141
|
|
|
183
|
-
it('publishes ./payments-contract as a second subpath
|
|
142
|
+
it('publishes ./payments-contract as a second subpath carrying the eighteen contract values, loadable by a bare node process', async () => {
|
|
184
143
|
const manifest = await readPaymentsPackageManifest()
|
|
185
144
|
expect(manifest.packageName).toBe('@hearthkit/payments')
|
|
186
145
|
expect(Object.keys(manifest.exportsMap).toSorted()).toEqual(['.', './payments-contract'])
|
|
187
146
|
expect(JSON.stringify(manifest.exportsMap['./payments-contract'])).toContain(
|
|
188
|
-
'./src/payments-contract.ts',
|
|
147
|
+
'./src/payments-contract-entry.ts',
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
const subpathNamespace = await importHearthkitPaymentsContractSubpathNamespace()
|
|
151
|
+
const contractModule = paymentsContract as unknown as Record<string, unknown>
|
|
152
|
+
const subpathValueExportNames = Object.keys(subpathNamespace)
|
|
153
|
+
.filter((exportName) => subpathNamespace[exportName] !== undefined)
|
|
154
|
+
.toSorted()
|
|
155
|
+
expect(
|
|
156
|
+
subpathValueExportNames,
|
|
157
|
+
'src/payments-contract-entry.ts must export exactly the eighteen allowlisted names that live in payments-contract.ts',
|
|
158
|
+
).toEqual([...hearthkitPaymentsContractSubpathValueExportNames].toSorted())
|
|
159
|
+
|
|
160
|
+
const rebuiltInsteadOfReExported = hearthkitPaymentsContractSubpathValueExportNames.filter(
|
|
161
|
+
(exportName) => subpathNamespace[exportName] !== contractModule[exportName],
|
|
189
162
|
)
|
|
163
|
+
expect(
|
|
164
|
+
rebuiltInsteadOfReExported,
|
|
165
|
+
'these must be the identical value payments-contract.ts exports, not a second copy of it',
|
|
166
|
+
).toEqual([])
|
|
190
167
|
|
|
191
168
|
// The reason the subpath exists: the `.` entry imports the Stripe SDK, drizzle-orm/pg-core and
|
|
192
|
-
// the table definitions, while
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
169
|
+
// the table definitions, while both of these files import zod at runtime and nothing else. Each
|
|
170
|
+
// is run on its own so a failure names the file, and stderr must be empty as well as the exit
|
|
171
|
+
// code zero, because a module that warns on load still breaks a bare consumer.
|
|
172
|
+
for (const bareNodeRun of await runPaymentsContractModulesUnderBareNode()) {
|
|
173
|
+
expect(bareNodeRun.exitCode, `${bareNodeRun.modulePath}: ${bareNodeRun.standardError}`).toBe(
|
|
174
|
+
0,
|
|
175
|
+
)
|
|
176
|
+
expect(bareNodeRun.standardError, `${bareNodeRun.modulePath} wrote to stderr`).toBe('')
|
|
177
|
+
}
|
|
196
178
|
})
|
|
197
179
|
})
|