@hearthkit/payments 0.3.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.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/auth": "0.3.0",
37
- "@hearthkit/config": "0.3.0",
38
- "@hearthkit/db": "0.3.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
- expectContractStringExport(
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 unique literal prefix every returned failure message starts with. */
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
- paymentsCustomerSchema,
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: each function's options, success-only and full result schemas, so a narrowed result can be validated without rebuilding the schema. */
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'
@@ -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
- export const paymentsClientCreatedSchema = z.object({
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
- export const paymentsCatalogSyncedSchema = z.object({
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
- export const paymentsCheckoutSessionCreatedSchema = z.object({
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
- export const paymentsPortalSessionCreatedSchema = z.object({
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; a replay of the same event writes the same row and moves no count. */
903
- export const paymentsWebhookProcessedSchema = z.object({
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; a successful answer, not a failure, because most events are none of our business. */
911
- export const paymentsWebhookIgnoredSchema = z.object({
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; the row is returned whatever its status, and the caller decides what counts as entitled. */
946
- export const paymentsSubscriptionFoundSchema = z.object({
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; nobody having a subscription is a normal answer, not a failure. */
952
- export const paymentsSubscriptionAbsentSchema = z.object({
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; an empty array is a success, because "has bought nothing" is a correct answer. */
984
- export const paymentsPurchasesListedSchema = z.object({
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
- export const paymentsTablesPresentSchema = z.object({
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; this is a successful check reporting a negative answer. */
1020
- export const paymentsTablesMissingSchema = z.object({
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
- runPaymentsContractUnderBareNode,
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('re-exports by name every value payments-contract.ts exports, plus the eight functions and the Drizzle schema', async () => {
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
- // Both lists are compared whole rather than one name at a time, so a failure names every export
157
- // that is wrong instead of stopping at the first and hiding the rest behind a rerun.
158
- const missingFromTheEntryPoint = requiredExportNames.filter(
159
- (exportName) => namespace[exportName] === undefined,
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
- missingFromTheEntryPoint,
163
- 'src/index.ts must re-export these by name from payments-contract.ts',
164
- ).toEqual([])
165
-
166
- const rebuiltInsteadOfReExported = requiredExportNames.filter(
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
- const missingImplementationExports = entryPointOnlyExportNames.filter(
175
- (exportName) => namespace[exportName] === undefined,
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
- missingImplementationExports,
179
- 'src/index.ts must re-export the public functions and the Drizzle schema by name',
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 that a bare node process can load', async () => {
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 this file imports zod at runtime and nothing else — its stripe,
193
- // drizzle-orm/node-postgres and @hearthkit/auth/auth-contract imports are type-only and erased.
194
- const bareNodeRun = await runPaymentsContractUnderBareNode()
195
- expect(bareNodeRun.exitCode, bareNodeRun.standardError).toBe(0)
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
  })