@weft-js/pay 0.1.0 → 0.1.3

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/docs/pay.md CHANGED
@@ -12,8 +12,13 @@ App code names products, never a rail:
12
12
  // src/pay/catalog.ts: reaches the client, so names and amounts only
13
13
  import { product } from '@weft-js/pay'
14
14
 
15
- export const pro = product({ id: 'pro', kind: 'subscription', use: 'digital',
16
- plans: { month: { usd: 999 }, year: { usd: 7999, trialDays: 7 } }, grants: ['pro'] })
15
+ export const pro = product({
16
+ id: 'pro',
17
+ kind: 'subscription',
18
+ use: 'digital',
19
+ plans: { month: { usd: 999 }, year: { usd: 7999, trialDays: 7 } },
20
+ grants: ['pro'],
21
+ })
17
22
  ```
18
23
 
19
24
  <!-- example: skip needs what `weft add pay` writes -->
@@ -27,8 +32,15 @@ export default function Pricing() {
27
32
  const mine = entitlements.live()
28
33
  return (
29
34
  <section>
30
- <button type="button" onClick={() => checkout({ product: 'pro', plan: 'year' })}>Go Pro · {price(pro, 'year').display}</button>
31
- <button type="button" onClick={() => portal()}>Manage</button>
35
+ <button
36
+ type="button"
37
+ onClick={() => checkout({ product: 'pro', plan: 'year' })}
38
+ >
39
+ Go Pro · {price(pro, 'year').display}
40
+ </button>
41
+ <button type="button" onClick={() => portal()}>
42
+ Manage
43
+ </button>
32
44
  {mine.value?.includes('pro') && <p>You have Pro.</p>}
33
45
  </section>
34
46
  )
@@ -48,8 +60,13 @@ A bad signature answers 400; a failure answers 500, so the rail delivers it agai
48
60
 
49
61
  <!-- example: skip fragments of a server module -->
50
62
  ```ts
51
- export const report = query({ auth: 'user', requires: ['pro'], run: ({ ctx }) => buildReport(ctx) }) // a typed 403: ENTITLEMENT_REQUIRED
52
- if (!(await ctx.entitlements.has('pro'))) redirect('/pricing') // await it (PAY015)
63
+ export const report = query({
64
+ auth: 'user',
65
+ requires: ['pro'],
66
+ run: ({ ctx }) => buildReport(ctx),
67
+ }) // a typed 403: ENTITLEMENT_REQUIRED
68
+ // Await it (PAY015).
69
+ if (!(await ctx.entitlements.has('pro'))) redirect('/pricing')
53
70
  ```
54
71
 
55
72
  `ctx.entitlements` has `has(id)` and `list()`, and works in any `query`, `mutation` or `load`. A query with `cache: { life }`
@@ -82,13 +99,15 @@ generated file (PAY003).
82
99
  ledger, so a test sees what a user would:
83
100
 
84
101
  ```tsx
85
- const fake = fakePay(pay) // your `definePay`, or fakePay({ catalog })
102
+ const fake = fakePay(pay) // your `definePay`, or fakePay({ catalog })
86
103
  net.context({ user: { id: 'u1' } })
87
104
  render(Paywall)
88
105
  await user.click(screen.getByRole('button', { name: /Go Pro/ }))
89
- await fake.complete() // the buyer finishes the hosted page
106
+ await fake.complete() // the buyer finishes the hosted page
90
107
  expect(screen.getByLabelText('plan').textContent).toBe('Pro')
91
- await fake.simulate('renewal-failed') // renewal, recovered, grace-expired, cancel, pause, resume, refund, revoke
108
+ // renewal, recovered, grace-expired, cancel, pause, resume,
109
+ // refund, revoke
110
+ await fake.simulate('renewal-failed')
92
111
  ```
93
112
 
94
113
  `fake.buy(user, product, plan)` skips the page. `renewal` and `renewal-failed` move `clock` to the period's end when it's
package/package.json CHANGED
@@ -1,8 +1,17 @@
1
1
  {
2
2
  "name": "@weft-js/pay",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "@weft-js/pay: products, one ledger for every rail, and entitlements. weft's reference plugin: read it to build your own",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Ravonus/weft.git",
9
+ "directory": "packages/pay"
10
+ },
11
+ "homepage": "https://github.com/Ravonus/weft/tree/main/packages/pay#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/Ravonus/weft/issues"
14
+ },
6
15
  "type": "module",
7
16
  "exports": {
8
17
  ".": "./src/index.ts",
@@ -27,9 +36,9 @@
27
36
  "typescript": "6.0.3"
28
37
  },
29
38
  "peerDependencies": {
30
- "@weft-js/start": "^0.1.0",
39
+ "@weft-js/start": "^0.1.3",
31
40
  "drizzle-orm": "^0.45.0",
32
- "@weft-js/core": "^0.1.0"
41
+ "@weft-js/core": "^0.1.3"
33
42
  },
34
43
  "peerDependenciesMeta": {
35
44
  "drizzle-orm": {
@@ -45,4 +54,4 @@
45
54
  "publishConfig": {
46
55
  "access": "public"
47
56
  }
48
- }
57
+ }
package/plugin.mjs CHANGED
@@ -5,22 +5,92 @@
5
5
  import { definePlugin, defineSlot, provide } from '@weft-js/core/plugin'
6
6
  import { setup } from './src/setup.mjs'
7
7
 
8
- const extension = defineSlot({ id: 'server.extension', version: 1, cardinality: 'keyed', sides: ['server'] })
8
+ const extension = defineSlot({
9
+ id: 'server.extension',
10
+ version: 1,
11
+ cardinality: 'keyed',
12
+ sides: ['server'],
13
+ })
9
14
 
10
15
  const diagnostics = [
11
- ['PAY003', 'error', 'A Stripe secret, restricted or webhook key in the source, a server secret read in client code, or a key in plugin options', 'Move it to the server env (`src/env.server.ts`), and roll any real key that was committed'],
12
- ['PAY004', 'error', "Paid state written on the client after `checkout()`, which only opens the rail's page", 'Read `entitlements` instead: a grant arrives from a verified event'],
13
- ['PAY005', 'warning', 'A price typed into a page that sells something', '`price(product, plan).display`'],
14
- ['PAY008', 'error', 'Money as a float, or with no currency, in `product()`', 'Whole minor units with a currency: `{ usd: 999 }`'],
15
- ['PAY010', 'error', "A mutation that opens a checkout without `auth: 'user'`", "Add `auth: 'user'`: a purchase has to belong to someone"],
16
- ['PAY011', 'error', 'A webhook left unverified: a handler of payment events with no signature check, a `PayRail` whose `verify` ignores `headers`, or no `STRIPE_WEBHOOK_SECRET`', 'Delete the handler: `/_weft/x/pay/<rail>` verifies. Give Stripe that URL, and set `STRIPE_WEBHOOK_SECRET`'],
17
- ['PAY012', 'error', "A rail's price or product id in client code", "Name the catalog: `checkout({ product: 'pro', plan: 'month' })`. Map ids on the server: `stripe({ prices: { 'pro.month': … } })`"],
18
- ['PAY013', 'error', 'An entitlement checked only on the client: nothing on the server enforces it', "`requires: ['pro']` on the query or mutation, or `await ctx.entitlements.has('pro')` in a `load`"],
19
- ['PAY014', 'error', 'The plugin with no `src/pay.server.ts`: no webhook is mounted, so payments grant nothing', 'Default-export `definePay({ … })` from `src/pay.server.ts`'],
20
- ['PAY015', 'error', '`ctx.entitlements.has()` tested without `await`: a promise is truthy, so everyone passes', 'Await it (`weft fix` does, in an `async` function)'],
21
- ['PAY016', 'warning', '`memoryPayStore()` in `src/pay.server.ts`: a production server refuses to start with it', '`drizzlePayStore(db, { purchases, payEvents })`, or your own `PayStore`'],
22
- ['PAY017', 'error', '`@weft-js/pay/server` or `/drizzle` imported in client code', 'Import them in a server module; the client gets `@weft-js/pay`'],
23
- ['PAY018', 'error', '`src/pay.server.ts` with no `pay.rail` provider', 'Add a rail to `plugins`: `weft add pay-stripe`'],
16
+ [
17
+ 'PAY003',
18
+ 'error',
19
+ 'A Stripe secret, restricted or webhook key in the source, a server secret read in client code, or a key in plugin options',
20
+ 'Move it to the server env (`src/env.server.ts`), and roll any real key that was committed',
21
+ ],
22
+ [
23
+ 'PAY004',
24
+ 'error',
25
+ "Paid state written on the client after `checkout()`, which only opens the rail's page",
26
+ 'Read `entitlements` instead: a grant arrives from a verified event',
27
+ ],
28
+ [
29
+ 'PAY005',
30
+ 'warning',
31
+ 'A price typed into a page that sells something',
32
+ '`price(product, plan).display`',
33
+ ],
34
+ [
35
+ 'PAY008',
36
+ 'error',
37
+ 'Money as a float, or with no currency, in `product()`',
38
+ 'Whole minor units with a currency: `{ usd: 999 }`',
39
+ ],
40
+ [
41
+ 'PAY010',
42
+ 'error',
43
+ "A mutation that opens a checkout without `auth: 'user'`",
44
+ "Add `auth: 'user'`: a purchase has to belong to someone",
45
+ ],
46
+ [
47
+ 'PAY011',
48
+ 'error',
49
+ 'A webhook left unverified: a handler of payment events with no signature check, a `PayRail` whose `verify` ignores `headers`, or no `STRIPE_WEBHOOK_SECRET`',
50
+ 'Delete the handler: `/_weft/x/pay/<rail>` verifies. Give Stripe that URL, and set `STRIPE_WEBHOOK_SECRET`',
51
+ ],
52
+ [
53
+ 'PAY012',
54
+ 'error',
55
+ "A rail's price or product id in client code",
56
+ "Name the catalog: `checkout({ product: 'pro', plan: 'month' })`. Map ids on the server: `stripe({ prices: { 'pro.month': … } })`",
57
+ ],
58
+ [
59
+ 'PAY013',
60
+ 'error',
61
+ 'An entitlement checked only on the client: nothing on the server enforces it',
62
+ "`requires: ['pro']` on the query or mutation, or `await ctx.entitlements.has('pro')` in a `load`",
63
+ ],
64
+ [
65
+ 'PAY014',
66
+ 'error',
67
+ 'The plugin with no `src/pay.server.ts`: no webhook is mounted, so payments grant nothing',
68
+ 'Default-export `definePay({ … })` from `src/pay.server.ts`',
69
+ ],
70
+ [
71
+ 'PAY015',
72
+ 'error',
73
+ '`ctx.entitlements.has()` tested without `await`: a promise is truthy, so everyone passes',
74
+ 'Await it (`weft fix` does, in an `async` function)',
75
+ ],
76
+ [
77
+ 'PAY016',
78
+ 'warning',
79
+ '`memoryPayStore()` in `src/pay.server.ts`: a production server refuses to start with it',
80
+ '`drizzlePayStore(db, { purchases, payEvents })`, or your own `PayStore`',
81
+ ],
82
+ [
83
+ 'PAY017',
84
+ 'error',
85
+ '`@weft-js/pay/server` or `/drizzle` imported in client code',
86
+ 'Import them in a server module; the client gets `@weft-js/pay`',
87
+ ],
88
+ [
89
+ 'PAY018',
90
+ 'error',
91
+ '`src/pay.server.ts` with no `pay.rail` provider',
92
+ 'Add a rail to `plugins`: `weft add pay-stripe`',
93
+ ],
24
94
  ['PAY031', 'error', 'A live key in a test', '`fakePay` from `@weft-js/pay/test`'],
25
95
  ].map(([code, severity, catches, fix]) => ({ code, severity, catches, fix }))
26
96
 
@@ -31,19 +101,57 @@ export const pay = definePlugin({
31
101
  namespace: 'PAY',
32
102
  permissions: {},
33
103
  contribute: () => ({
34
- provides: [provide(extension, { key: 'pay', sides: { server: { module: '@weft-js/pay/server', export: 'extension' } } })],
104
+ provides: [
105
+ provide(extension, {
106
+ key: 'pay',
107
+ sides: { server: { module: '@weft-js/pay/server', export: 'extension' } },
108
+ }),
109
+ ],
35
110
  diagnostics,
36
- check: { include: ['src/**/*.{ts,tsx,js,jsx,mts,mjs}', 'test/**/*.{ts,tsx,js,mjs}', 'weft.config.*'], run: ctx => import('./src/checks.mjs').then(c => c.run(ctx)) },
37
- docs: { topic: 'pay', summary: 'Payments: products, checkout, entitlements, webhooks, fakePay, another provider; PAY-codes', file: new URL('./docs/pay.md', import.meta.url).pathname },
111
+ check: {
112
+ include: ['src/**/*.{ts,tsx,js,jsx,mts,mjs}', 'test/**/*.{ts,tsx,js,mjs}', 'weft.config.*'],
113
+ run: (ctx) => import('./src/checks.mjs').then((c) => c.run(ctx)),
114
+ },
115
+ docs: {
116
+ topic: 'pay',
117
+ summary:
118
+ 'Payments: products, checkout, entitlements, webhooks, fakePay, another provider; PAY-codes',
119
+ file: new URL('./docs/pay.md', import.meta.url).pathname,
120
+ },
38
121
  setup,
39
- agents: { title: 'Payments', text: 'Products are in `src/pay/catalog.ts`, in minor units. `checkout` and `entitlements` are in `src/server/pay.ts`. App code never names a rail or a price id, and `checkout` grants nothing: a verified webhook does. Enforce on the server, with `requires: [...]` or `await ctx.entitlements.has(...)`: a check only on the client protects nothing. Test with `fakePay` from `@weft-js/pay/test`. `weft docs pay`.' },
122
+ agents: {
123
+ title: 'Payments',
124
+ text: 'Products are in `src/pay/catalog.ts`, in minor units. `checkout` and `entitlements` are in `src/server/pay.ts`. App code never names a rail or a price id, and `checkout` grants nothing: a verified webhook does. Enforce on the server, with `requires: [...]` or `await ctx.entitlements.has(...)`: a check only on the client protects nothing. Test with `fakePay` from `@weft-js/pay/test`. `weft docs pay`.',
125
+ },
40
126
  catalog: [
41
- { name: 'product', import: '@weft-js/pay', summary: 'One thing you sell, in integer minor units' },
42
- { name: 'price', import: '@weft-js/pay', summary: "A product's price, formatted for the reader" },
43
- { name: 'definePay', import: '@weft-js/pay/server', summary: 'The ledger: src/pay.server.ts default-exports it' },
44
- { name: 'fakePay', import: '@weft-js/pay/test', summary: 'A rail for tests: renewals, refunds and disputes on the clock' },
127
+ {
128
+ name: 'product',
129
+ import: '@weft-js/pay',
130
+ summary: 'One thing you sell, in integer minor units',
131
+ },
132
+ {
133
+ name: 'price',
134
+ import: '@weft-js/pay',
135
+ summary: "A product's price, formatted for the reader",
136
+ },
137
+ {
138
+ name: 'definePay',
139
+ import: '@weft-js/pay/server',
140
+ summary: 'The ledger: src/pay.server.ts default-exports it',
141
+ },
142
+ {
143
+ name: 'fakePay',
144
+ import: '@weft-js/pay/test',
145
+ summary: 'A rail for tests: renewals, refunds and disputes on the clock',
146
+ },
147
+ ],
148
+ map: [
149
+ {
150
+ want: 'Sell something',
151
+ call: 'add it to `src/pay/catalog.ts`, then `checkout({ product, plan })`',
152
+ have: '`weft docs pay`',
153
+ },
45
154
  ],
46
- map: [{ want: 'Sell something', call: 'add it to `src/pay/catalog.ts`, then `checkout({ product, plan })`', have: '`weft docs pay`' }],
47
155
  }),
48
156
  })
49
157
 
@@ -0,0 +1,24 @@
1
+ /** Check config and test files for exposed credentials. */
2
+ export function configChecks({ ctx, out, secrets, spanOf, isTest, SECRET_OPTION }) {
3
+ for (const rel of ctx.files.filter((r) => r.startsWith('weft.config.'))) {
4
+ const text = ctx.read(rel)
5
+ secrets(rel, text, true, out)
6
+ const m = SECRET_OPTION.exec(text)
7
+ if (m)
8
+ out.push({
9
+ code: 'PAY003',
10
+ ...spanOf(rel, m.index, m[0].length),
11
+ why: 'Plugin options are written into `.weft/slots/`, so a key given here sits in a generated file.',
12
+ })
13
+ }
14
+
15
+ for (const rel of ctx.files.filter(isTest)) {
16
+ const text = ctx.read(rel)
17
+ for (const m of text.matchAll(/\b(?:sk|rk)_live_\w+/g))
18
+ out.push({
19
+ code: 'PAY031',
20
+ ...spanOf(rel, m.index, m[0].length),
21
+ why: 'A live key in a test: it charges real cards, and it is now in git.',
22
+ })
23
+ }
24
+ }