glyphpay 0.3.0 → 0.5.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/README.md +184 -165
- package/dist/cli.d.ts +34 -2
- package/dist/cli.js +341 -94
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,165 +1,184 @@
|
|
|
1
|
-
# glyphpay
|
|
2
|
-
|
|
3
|
-
Drop-in connector that lets a website take payments through [GlyphPay](https://glyphpay.net) by Design Glyph. Payments go to the site owner's own Stripe connected account.
|
|
4
|
-
|
|
5
|
-
| Import | Runs on | What it gives you |
|
|
6
|
-
| --- | --- | --- |
|
|
7
|
-
| `glyphpay/server` | Site's server | `glyphPay()`: `checkout` and `webhook` route handlers, plus `createPayment`, `getPayment`, `refund`, `reconcile` |
|
|
8
|
-
| `glyphpay/react` | Browser | `<GlyphPayCheckout />` |
|
|
9
|
-
| `glyphpay/browser` | Browser, any site | `mountCheckout(el, options)` for sites without React |
|
|
10
|
-
|
|
11
|
-
The CLI covers setup, diagnostics, and credential lifecycle:
|
|
12
|
-
|
|
13
|
-
| Command | What it does |
|
|
14
|
-
| --- | --- |
|
|
15
|
-
| `glyphpay connect` | Pair a site through the browser and write the server credential. |
|
|
16
|
-
| `glyphpay doctor` | Check env, token, Stripe readiness, webhook URL, and an optional signed test event. |
|
|
17
|
-
| `glyphpay rotate` | Re-pair the site and replace the credential. |
|
|
18
|
-
| `glyphpay disconnect` | Revoke the credential and remove the local keys. |
|
|
19
|
-
| `glyphpay
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- `--
|
|
36
|
-
- `--
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
npx glyphpay doctor
|
|
47
|
-
npx glyphpay
|
|
48
|
-
npx glyphpay
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
|
|
1
|
+
# glyphpay
|
|
2
|
+
|
|
3
|
+
Drop-in connector that lets a website take payments through [GlyphPay](https://glyphpay.net) by Design Glyph. Payments go to the site owner's own Stripe connected account.
|
|
4
|
+
|
|
5
|
+
| Import | Runs on | What it gives you |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `glyphpay/server` | Site's server | `glyphPay()`: `checkout` and `webhook` route handlers, plus `createPayment`, `getPayment`, `refund`, `reconcile` |
|
|
8
|
+
| `glyphpay/react` | Browser | `<GlyphPayCheckout />` |
|
|
9
|
+
| `glyphpay/browser` | Browser, any site | `mountCheckout(el, options)` for sites without React |
|
|
10
|
+
|
|
11
|
+
The CLI covers setup, diagnostics, and credential lifecycle:
|
|
12
|
+
|
|
13
|
+
| Command | What it does |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `glyphpay connect` | Pair a site through the browser and write the server credential. |
|
|
16
|
+
| `glyphpay doctor` | Check env, token, Stripe readiness, webhook URL, and an optional signed test event. |
|
|
17
|
+
| `glyphpay rotate` | Re-pair the site and replace the credential. |
|
|
18
|
+
| `glyphpay disconnect` | Revoke the credential and remove the local keys. |
|
|
19
|
+
| `glyphpay deliveries` | Page/filter webhook deliveries and retry one with `--retry <id>`. |
|
|
20
|
+
| `glyphpay init` | Scaffold routes for Next.js, Remix, Astro, SvelteKit, Hono, or Express. |
|
|
21
|
+
|
|
22
|
+
## Connect a site from the CLI
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npx glyphpay connect --url https://client-site.com
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The command opens Glyph Pay in your browser. Sign in as the client's billing email (or a Design Glyph admin), pick the client account, confirm the webhook URL and approve the pairing code. The terminal receives a test service credential and, when a webhook URL was registered, its signing secret. It writes both to `.env.local`:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
GLYPHPAY_TOKEN=dgsvc_test_...
|
|
32
|
+
GLYPHPAY_CALLBACK_SECRET=dgcb_...
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- `--url <site>` registers `<site>/api/glyphpay/webhook`. Pass a full URL to use a different route.
|
|
36
|
+
- `--env <path>` targets another env file; `--print` prints the values instead of writing.
|
|
37
|
+
- `--no-open`, `--base-url` and `--timeout` help scripts and local testing.
|
|
38
|
+
- Codes expire after ten minutes. Approving again rotates the token: the previous token stops working immediately.
|
|
39
|
+
- Pairing is disabled while the platform is in live mode.
|
|
40
|
+
|
|
41
|
+
The browser never receives the credential. Keep the token in server environment variables and never prefix it with `NEXT_PUBLIC_`, `VITE_` or `PUBLIC_`.
|
|
42
|
+
|
|
43
|
+
## Check, rotate, and disconnect
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npx glyphpay doctor
|
|
47
|
+
npx glyphpay doctor --json
|
|
48
|
+
npx glyphpay rotate
|
|
49
|
+
npx glyphpay disconnect
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`doctor` verifies the local env file, checks for credentials leaked into public-prefixed variables, validates the token with Glyph Pay, checks Stripe charge readiness and the registered webhook, and optionally sends a signed test event:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npx glyphpay doctor --send-test-event
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The test event has type `webhook.test` and a zero-dollar `data` payload. Ignore event types you do not handle in `onEvent`; do not treat the test event as a real payment.
|
|
59
|
+
|
|
60
|
+
`rotate` re-runs browser approval and reuses the registered callback URL when the current token is still valid. `disconnect` revokes the credential immediately and removes `GLYPHPAY_TOKEN` and `GLYPHPAY_CALLBACK_SECRET` from the env file; pass `--yes` in scripts or `--keep-env` to leave the local values in place.
|
|
61
|
+
|
|
62
|
+
## Inspect and retry webhook deliveries
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
npx glyphpay deliveries
|
|
66
|
+
npx glyphpay deliveries --retry evt_...
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`deliveries` pages through callback outbox records for the credential's client and environment: event type, delivery state, attempt count, timestamp, and the last error. Filter with `--state pending|delivered|failed`, control the page with `--limit 1..100`, and pass the returned `--cursor` for the next page. A retry sends the exact stored signed event again and persists the result. Retry is one attempt; fix the endpoint first. The admin **Connections** tab exposes the same filtering, pagination, and retry operations.
|
|
70
|
+
|
|
71
|
+
## Scaffold a site
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
npx glyphpay init
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The command detects the project framework from `package.json`. It creates a server connector plus the framework-native checkout and webhook entry points, and appends placeholders to `.env.example` without changing existing values. Supported targets:
|
|
78
|
+
|
|
79
|
+
- **Next.js App Router** — `app/api/glyphpay/*` or `src/app/api/glyphpay/*` route handlers.
|
|
80
|
+
- **SvelteKit** — `src/routes/api/glyphpay/*/+server.ts` with `src/lib/server/glyphpay.ts`.
|
|
81
|
+
- **Astro** — `src/pages/api/glyphpay/*.ts`; use server/SSR output and an adapter.
|
|
82
|
+
- **Remix** — `app/routes/api.glyphpay.*.ts` with `app/lib/glyphpay.server.ts`.
|
|
83
|
+
- **Hono** — `src/glyphpay-routes.ts`; mount it with `app.route("/api/glyphpay", glyphpayRoutes)`.
|
|
84
|
+
- **Express** — `src/glyphpay-routes.ts`; mount it with `app.use("/api/glyphpay", glyphpayRoutes)`. The webhook route uses `express.raw` so signatures verify against the original bytes.
|
|
85
|
+
|
|
86
|
+
It refuses to overwrite existing files unless you pass `--force`; use `--dry-run` to preview. Then run `glyphpay connect` and wire `priceOrder()` and `onEvent()` to your order database.
|
|
87
|
+
|
|
88
|
+
## Manual setup: connect the client in Design Glyph admin
|
|
89
|
+
|
|
90
|
+
1. **Admin → Clients → (client)**: make sure Stripe onboarding is complete (charges enabled).
|
|
91
|
+
2. **Issue a service credential** (test first). Copy the `dgsvc_test_…` token. It is shown only once.
|
|
92
|
+
3. **Set the callback URL** to `https://<client-site>/api/glyphpay/webhook`. Copy the signing secret. It is also shown only once.
|
|
93
|
+
|
|
94
|
+
## Add to the client site
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
pnpm add glyphpay
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`.env` (server only, never `NEXT_PUBLIC_`):
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
GLYPHPAY_TOKEN=dgsvc_test_...
|
|
104
|
+
GLYPHPAY_CALLBACK_SECRET=...
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`lib/glyphpay.ts`
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { glyphPay, CheckoutError } from "glyphpay/server";
|
|
111
|
+
|
|
112
|
+
export const glyphpay = glyphPay({
|
|
113
|
+
token: process.env.GLYPHPAY_TOKEN,
|
|
114
|
+
callbackSecret: process.env.GLYPHPAY_CALLBACK_SECRET,
|
|
115
|
+
|
|
116
|
+
// Price the order from YOUR data. Never trust an amount sent by the browser.
|
|
117
|
+
async priceOrder(body) {
|
|
118
|
+
const cart = await db.carts.get((body as { cartId: string }).cartId);
|
|
119
|
+
if (!cart?.items.length) throw new CheckoutError("Your cart is empty.");
|
|
120
|
+
// orderId must change whenever the total changes, or GlyphPay returns 409.
|
|
121
|
+
return { orderId: `${cart.id}-v${cart.version}`, amountCents: cart.totalCents, description: `Order ${cart.id}` };
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// Signed and verified. At-least-once delivery: dedupe on event.id. Throw to have it retried.
|
|
125
|
+
async onEvent(event) {
|
|
126
|
+
if (event.type === "payment.succeeded") await db.orders.markPaid(event.data.orderId, event.id);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`app/api/glyphpay/checkout/route.ts`
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { glyphpay } from "@/lib/glyphpay";
|
|
135
|
+
export const POST = glyphpay.checkout;
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`app/api/glyphpay/webhook/route.ts`
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { glyphpay } from "@/lib/glyphpay";
|
|
142
|
+
export const POST = glyphpay.webhook;
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Checkout page:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
import { GlyphPayCheckout } from "glyphpay/react";
|
|
149
|
+
|
|
150
|
+
<GlyphPayCheckout order={{ cartId }} returnUrl={`/thank-you?cart=${cartId}`} appearance={{ theme: "stripe" }} />
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Style the button with `.glyphpay button { … }`. Errors appear in `.glyphpay [role=alert]`.
|
|
154
|
+
|
|
155
|
+
Without React:
|
|
156
|
+
|
|
157
|
+
```html
|
|
158
|
+
<div id="pay"></div>
|
|
159
|
+
<script type="module">
|
|
160
|
+
import { mountCheckout } from "glyphpay/browser";
|
|
161
|
+
mountCheckout(document.getElementById("pay"), { order: { cartId: "42" }, returnUrl: "/thank-you" });
|
|
162
|
+
</script>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The route handlers take a standard `Request` and return a `Response`, so the same two lines work in Remix, Astro, Hono, SvelteKit, and Cloudflare Workers. In Express, `glyphpay init` generates a small adapter that preserves the raw webhook body.
|
|
166
|
+
|
|
167
|
+
## How a payment flows
|
|
168
|
+
|
|
169
|
+
1. The component POSTs `order` to `/api/glyphpay/checkout`. `priceOrder` sets the amount, and the site's server calls GlyphPay with the token.
|
|
170
|
+
2. The browser gets back only `clientSecret`, `publishableKey`, and `stripeAccount`, then mounts Stripe's Payment Element.
|
|
171
|
+
3. The shopper pays, and Stripe redirects to `returnUrl`. **The redirect doesn't prove payment.**
|
|
172
|
+
4. GlyphPay POSTs `payment.succeeded` to `/api/glyphpay/webhook`, and `onEvent` marks the order paid. The thank-you page reads the order status from your database, or calls `glyphpay.getPayment(paymentId)`.
|
|
173
|
+
|
|
174
|
+
Refunds run server-side:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
await glyphpay.refund(paymentId, { idempotencyKey: `${orderId}-refund-1` });
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Develop
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
cd packages/glyphpay && npm install && npm test
|
|
184
|
+
```
|
package/dist/cli.d.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* `doctor` verifies the local setup and, optionally, delivers a signed test
|
|
11
11
|
* webhook. `disconnect` revokes the credential. `rotate` re-pairs and replaces
|
|
12
|
-
* it. `init` scaffolds
|
|
12
|
+
* it. `init` scaffolds a framework integration. `deliveries` lists callback
|
|
13
|
+
* attempts and replays one on demand.
|
|
13
14
|
*
|
|
14
15
|
* Zero runtime dependencies: Node 18+ built-ins and global fetch only.
|
|
15
16
|
*/
|
|
@@ -48,6 +49,14 @@ export interface CliOptions {
|
|
|
48
49
|
dryRun?: boolean;
|
|
49
50
|
/** Init: project directory. Defaults to the current directory. */
|
|
50
51
|
dir?: string;
|
|
52
|
+
/** Deliveries: replay one failed or delivered callback by id. */
|
|
53
|
+
retry?: string;
|
|
54
|
+
/** Deliveries: filter by delivery state. */
|
|
55
|
+
state?: "pending" | "delivered" | "failed";
|
|
56
|
+
/** Deliveries: opaque next-page cursor returned by the API. */
|
|
57
|
+
cursor?: string;
|
|
58
|
+
/** Deliveries: page size, 1-100. */
|
|
59
|
+
limit?: number;
|
|
51
60
|
}
|
|
52
61
|
export interface ConnectResult {
|
|
53
62
|
businessName: string;
|
|
@@ -61,7 +70,29 @@ export interface DisconnectResult {
|
|
|
61
70
|
alreadyRevoked: boolean;
|
|
62
71
|
canceled: boolean;
|
|
63
72
|
}
|
|
64
|
-
|
|
73
|
+
export interface CallbackDeliveryView {
|
|
74
|
+
id: string;
|
|
75
|
+
paymentId: string;
|
|
76
|
+
type: string;
|
|
77
|
+
state: "pending" | "delivered" | "failed";
|
|
78
|
+
attempts: number;
|
|
79
|
+
lastError: string | null;
|
|
80
|
+
createdAt: string;
|
|
81
|
+
updatedAt: string;
|
|
82
|
+
}
|
|
83
|
+
export interface CallbackDeliveriesResponse {
|
|
84
|
+
businessId: string;
|
|
85
|
+
businessName: string;
|
|
86
|
+
environment: "test" | "live";
|
|
87
|
+
deliveries: CallbackDeliveryView[];
|
|
88
|
+
nextCursor: string | null;
|
|
89
|
+
}
|
|
90
|
+
export interface CallbackReplayResponse {
|
|
91
|
+
outcome: "delivered" | "retry" | "failed";
|
|
92
|
+
detail?: string;
|
|
93
|
+
delivery: CallbackDeliveryView;
|
|
94
|
+
}
|
|
95
|
+
type Command = "connect" | "rotate" | "disconnect" | "doctor" | "init" | "deliveries" | "help" | "version";
|
|
65
96
|
interface ParsedCommand {
|
|
66
97
|
command: Command;
|
|
67
98
|
options: CliOptions;
|
|
@@ -107,6 +138,7 @@ export interface DoctorReport {
|
|
|
107
138
|
checks: DoctorCheck[];
|
|
108
139
|
}
|
|
109
140
|
export declare function runDoctor(options?: CliOptions): Promise<DoctorReport>;
|
|
141
|
+
export declare function runDeliveries(options?: CliOptions): Promise<CallbackDeliveriesResponse | CallbackReplayResponse>;
|
|
110
142
|
export declare function runInit(options?: CliOptions): Promise<void>;
|
|
111
143
|
export declare function main(argv?: string[]): Promise<number>;
|
|
112
144
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* `doctor` verifies the local setup and, optionally, delivers a signed test
|
|
11
11
|
* webhook. `disconnect` revokes the credential. `rotate` re-pairs and replaces
|
|
12
|
-
* it. `init` scaffolds
|
|
12
|
+
* it. `init` scaffolds a framework integration. `deliveries` lists callback
|
|
13
|
+
* attempts and replays one on demand.
|
|
13
14
|
*
|
|
14
15
|
* Zero runtime dependencies: Node 18+ built-ins and global fetch only.
|
|
15
16
|
*/
|
|
@@ -19,7 +20,7 @@ import { access, chmod, mkdir, readFile, stat, writeFile } from "node:fs/promise
|
|
|
19
20
|
import { dirname, join, relative, resolve } from "node:path";
|
|
20
21
|
import { createInterface } from "node:readline/promises";
|
|
21
22
|
import { pathToFileURL } from "node:url";
|
|
22
|
-
const VERSION = "0.
|
|
23
|
+
const VERSION = "0.5.0";
|
|
23
24
|
const DEFAULT_BASE_URL = "https://designglyph.web.app";
|
|
24
25
|
const DEFAULT_ENV_FILE = ".env.local";
|
|
25
26
|
const DEFAULT_TIMEOUT_SECONDS = 600;
|
|
@@ -72,6 +73,8 @@ function commandFor(value) {
|
|
|
72
73
|
return "doctor";
|
|
73
74
|
if (value === "init")
|
|
74
75
|
return "init";
|
|
76
|
+
if (value === "deliveries" || value === "webhooks")
|
|
77
|
+
return "deliveries";
|
|
75
78
|
return null;
|
|
76
79
|
}
|
|
77
80
|
export function parseArgs(argv) {
|
|
@@ -164,6 +167,53 @@ export function parseArgs(argv) {
|
|
|
164
167
|
}
|
|
165
168
|
continue;
|
|
166
169
|
}
|
|
170
|
+
if (flag === "--retry") {
|
|
171
|
+
if (inlineValue !== undefined) {
|
|
172
|
+
if (!inlineValue)
|
|
173
|
+
throw new CliUsageError("--retry needs a delivery id.");
|
|
174
|
+
options.retry = inlineValue;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const [value, next] = takeValue(argv, index, flag);
|
|
178
|
+
options.retry = value;
|
|
179
|
+
index = next;
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (flag === "--state") {
|
|
184
|
+
const raw = inlineValue ?? takeValue(argv, index, flag)[0];
|
|
185
|
+
if (inlineValue === undefined)
|
|
186
|
+
index += 1;
|
|
187
|
+
if (raw !== "pending" && raw !== "delivered" && raw !== "failed") {
|
|
188
|
+
throw new CliUsageError("--state must be pending, delivered, or failed.");
|
|
189
|
+
}
|
|
190
|
+
options.state = raw;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (flag === "--cursor") {
|
|
194
|
+
if (inlineValue !== undefined) {
|
|
195
|
+
if (!inlineValue)
|
|
196
|
+
throw new CliUsageError("--cursor needs a value.");
|
|
197
|
+
options.cursor = inlineValue;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const [value, next] = takeValue(argv, index, flag);
|
|
201
|
+
options.cursor = value;
|
|
202
|
+
index = next;
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (flag === "--limit") {
|
|
207
|
+
const raw = inlineValue ?? takeValue(argv, index, flag)[0];
|
|
208
|
+
if (inlineValue === undefined)
|
|
209
|
+
index += 1;
|
|
210
|
+
const parsed = Number(raw);
|
|
211
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
|
|
212
|
+
throw new CliUsageError("--limit must be an integer between 1 and 100.");
|
|
213
|
+
}
|
|
214
|
+
options.limit = parsed;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
167
217
|
if (flag === "--dir") {
|
|
168
218
|
if (inlineValue !== undefined) {
|
|
169
219
|
if (!inlineValue)
|
|
@@ -195,39 +245,47 @@ export function parseArgs(argv) {
|
|
|
195
245
|
return { command, options };
|
|
196
246
|
}
|
|
197
247
|
export function helpText() {
|
|
198
|
-
return `glyphpay - take payments on your site through Glyph Pay
|
|
199
|
-
|
|
200
|
-
Usage
|
|
201
|
-
glyphpay connect [options] Connect a site (alias: login)
|
|
202
|
-
glyphpay rotate [options] Re-pair and rotate the current credential
|
|
203
|
-
glyphpay disconnect [options] Revoke the credential and remove local keys
|
|
204
|
-
glyphpay doctor [options] Check env, token, Stripe and webhook setup
|
|
205
|
-
glyphpay
|
|
206
|
-
glyphpay
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
--
|
|
213
|
-
--
|
|
214
|
-
--
|
|
215
|
-
--
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
--
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
--
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
--
|
|
228
|
-
--
|
|
229
|
-
|
|
230
|
-
|
|
248
|
+
return `glyphpay - take payments on your site through Glyph Pay
|
|
249
|
+
|
|
250
|
+
Usage
|
|
251
|
+
glyphpay connect [options] Connect a site (alias: login)
|
|
252
|
+
glyphpay rotate [options] Re-pair and rotate the current credential
|
|
253
|
+
glyphpay disconnect [options] Revoke the credential and remove local keys
|
|
254
|
+
glyphpay doctor [options] Check env, token, Stripe and webhook setup
|
|
255
|
+
glyphpay deliveries [options] List recent webhook deliveries
|
|
256
|
+
glyphpay init [options] Scaffold an integration for your framework
|
|
257
|
+
glyphpay --version
|
|
258
|
+
|
|
259
|
+
Connect / rotate
|
|
260
|
+
--url <site-url> Site to connect. Adds ${WEBHOOK_PATH} when the URL
|
|
261
|
+
has no path. Or pass the exact callback URL.
|
|
262
|
+
--env <path> Env file to update (default: ${DEFAULT_ENV_FILE})
|
|
263
|
+
--print Print the credentials instead of writing a file
|
|
264
|
+
--no-open Do not open a browser automatically
|
|
265
|
+
--base-url <url> Glyph Pay API origin (default: ${DEFAULT_BASE_URL})
|
|
266
|
+
--timeout <seconds> Wait for approval (default: ${DEFAULT_TIMEOUT_SECONDS})
|
|
267
|
+
|
|
268
|
+
Doctor
|
|
269
|
+
--json Print a machine-readable report
|
|
270
|
+
--send-test-event POST a signed webhook.test event to the callback URL
|
|
271
|
+
|
|
272
|
+
Disconnect
|
|
273
|
+
--yes, -y Do not ask for confirmation
|
|
274
|
+
--keep-env Keep the local env values after revocation
|
|
275
|
+
|
|
276
|
+
Deliveries
|
|
277
|
+
--state <state> pending, delivered, or failed
|
|
278
|
+
--limit <n> Page size, 1-100 (default: 50)
|
|
279
|
+
--cursor <cursor> Next-page cursor from the previous response
|
|
280
|
+
--retry <id> Send one stored webhook again (alias: webhooks)
|
|
281
|
+
--json Print a machine-readable response
|
|
282
|
+
|
|
283
|
+
Init
|
|
284
|
+
--dir <path> Project directory (default: current directory)
|
|
285
|
+
--dry-run Show planned files without writing them
|
|
286
|
+
--force Overwrite generated files
|
|
287
|
+
|
|
288
|
+
The browser step signs in to Glyph Pay, lets you pick the client account, and
|
|
231
289
|
approves this machine. The service credential goes only to this terminal.`;
|
|
232
290
|
}
|
|
233
291
|
/* ------------------------------------------------------------------ */
|
|
@@ -838,45 +896,245 @@ export async function runDoctor(options = {}) {
|
|
|
838
896
|
}
|
|
839
897
|
return report;
|
|
840
898
|
}
|
|
899
|
+
/* ------------------------------------------------------------------ */
|
|
900
|
+
/* Webhook deliveries */
|
|
901
|
+
/* ------------------------------------------------------------------ */
|
|
902
|
+
function printDelivery(delivery) {
|
|
903
|
+
say(` ${delivery.id} ${delivery.type} ${delivery.state.toUpperCase()} ` +
|
|
904
|
+
`attempt ${delivery.attempts} ${delivery.updatedAt}`);
|
|
905
|
+
if (delivery.lastError)
|
|
906
|
+
say(` ${delivery.lastError}`);
|
|
907
|
+
}
|
|
908
|
+
export async function runDeliveries(options = {}) {
|
|
909
|
+
const baseUrl = baseUrlFor(options);
|
|
910
|
+
const envFile = resolve(options.envFile ?? DEFAULT_ENV_FILE);
|
|
911
|
+
const contents = await readEnvFile(envFile);
|
|
912
|
+
const token = readEnvValue(contents, TOKEN_KEY);
|
|
913
|
+
if (!token) {
|
|
914
|
+
throw new CliUsageError(`No ${TOKEN_KEY} found in ${relative(process.cwd(), envFile) || envFile}. Run glyphpay connect first.`);
|
|
915
|
+
}
|
|
916
|
+
if (options.retry) {
|
|
917
|
+
const result = await api(baseUrl, `/credential/deliveries/${encodeURIComponent(options.retry)}/replay`, {}, token);
|
|
918
|
+
if (options.json) {
|
|
919
|
+
say(JSON.stringify(result, null, 2));
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
say();
|
|
923
|
+
say(style(1, `Webhook ${result.outcome === "delivered" ? "delivered" : "not delivered"}.`));
|
|
924
|
+
printDelivery(result.delivery);
|
|
925
|
+
if (result.detail)
|
|
926
|
+
say(` ${result.detail}`);
|
|
927
|
+
}
|
|
928
|
+
return result;
|
|
929
|
+
}
|
|
930
|
+
const params = new URLSearchParams();
|
|
931
|
+
if (options.state)
|
|
932
|
+
params.set("state", options.state);
|
|
933
|
+
if (options.limit)
|
|
934
|
+
params.set("limit", String(options.limit));
|
|
935
|
+
if (options.cursor)
|
|
936
|
+
params.set("cursor", options.cursor);
|
|
937
|
+
const query = params.toString();
|
|
938
|
+
const result = await api(baseUrl, `/credential/deliveries${query ? `?${query}` : ""}`, undefined, token);
|
|
939
|
+
if (options.json) {
|
|
940
|
+
say(JSON.stringify(result, null, 2));
|
|
941
|
+
return result;
|
|
942
|
+
}
|
|
943
|
+
say();
|
|
944
|
+
say(style(1, `Webhook deliveries - ${result.businessName} (${result.environment} mode)`));
|
|
945
|
+
say();
|
|
946
|
+
if (result.deliveries.length === 0) {
|
|
947
|
+
say(options.state ? `No ${options.state} deliveries recorded.` : "No deliveries recorded yet.");
|
|
948
|
+
}
|
|
949
|
+
else {
|
|
950
|
+
for (const delivery of result.deliveries)
|
|
951
|
+
printDelivery(delivery);
|
|
952
|
+
say();
|
|
953
|
+
say("Retry one with `glyphpay deliveries --retry <id>`.");
|
|
954
|
+
if (result.nextCursor) {
|
|
955
|
+
const next = ["glyphpay deliveries"];
|
|
956
|
+
if (options.state)
|
|
957
|
+
next.push("--state", options.state);
|
|
958
|
+
if (options.limit)
|
|
959
|
+
next.push("--limit", String(options.limit));
|
|
960
|
+
next.push("--cursor", result.nextCursor);
|
|
961
|
+
say(`Next page: ${next.join(" ")}`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return result;
|
|
965
|
+
}
|
|
841
966
|
function libTemplate(ts) {
|
|
842
967
|
const cartId = ts
|
|
843
968
|
? `const cartId = (body as { cartId?: string } | null)?.cartId;`
|
|
844
969
|
: `const cartId = body?.cartId;`;
|
|
845
|
-
return `import { glyphPay, CheckoutError } from "glyphpay/server";
|
|
846
|
-
|
|
847
|
-
export const glyphpay = glyphPay({
|
|
848
|
-
token: process.env.GLYPHPAY_TOKEN,
|
|
849
|
-
callbackSecret: process.env.GLYPHPAY_CALLBACK_SECRET,
|
|
850
|
-
|
|
851
|
-
async priceOrder(body) {
|
|
852
|
-
// Load the cart/order from YOUR database and return its server-side price.
|
|
853
|
-
// Never trust an amount sent by the browser.
|
|
854
|
-
${cartId}
|
|
855
|
-
if (!cartId) throw new CheckoutError("Your cart is empty.");
|
|
856
|
-
throw new CheckoutError("Connect priceOrder() to your order database before accepting payments.");
|
|
857
|
-
},
|
|
858
|
-
|
|
859
|
-
async onEvent(event) {
|
|
860
|
-
// At-least-once delivery: dedupe on event.id before changing an order.
|
|
861
|
-
if (event.type === "payment.succeeded") {
|
|
862
|
-
// TODO: mark the order paid and store event.id.
|
|
863
|
-
console.log(\`GlyphPay payment succeeded for \${event.data.orderId}\`);
|
|
864
|
-
}
|
|
865
|
-
},
|
|
866
|
-
});
|
|
970
|
+
return `import { glyphPay, CheckoutError } from "glyphpay/server";
|
|
971
|
+
|
|
972
|
+
export const glyphpay = glyphPay({
|
|
973
|
+
token: process.env.GLYPHPAY_TOKEN,
|
|
974
|
+
callbackSecret: process.env.GLYPHPAY_CALLBACK_SECRET,
|
|
975
|
+
|
|
976
|
+
async priceOrder(body) {
|
|
977
|
+
// Load the cart/order from YOUR database and return its server-side price.
|
|
978
|
+
// Never trust an amount sent by the browser.
|
|
979
|
+
${cartId}
|
|
980
|
+
if (!cartId) throw new CheckoutError("Your cart is empty.");
|
|
981
|
+
throw new CheckoutError("Connect priceOrder() to your order database before accepting payments.");
|
|
982
|
+
},
|
|
983
|
+
|
|
984
|
+
async onEvent(event) {
|
|
985
|
+
// At-least-once delivery: dedupe on event.id before changing an order.
|
|
986
|
+
if (event.type === "payment.succeeded") {
|
|
987
|
+
// TODO: mark the order paid and store event.id.
|
|
988
|
+
console.log(\`GlyphPay payment succeeded for \${event.data.orderId}\`);
|
|
989
|
+
}
|
|
990
|
+
},
|
|
991
|
+
});
|
|
867
992
|
`;
|
|
868
993
|
}
|
|
869
|
-
function routeTemplate() {
|
|
870
|
-
return `import { glyphpay } from "
|
|
871
|
-
|
|
872
|
-
export const POST = glyphpay.checkout;
|
|
873
|
-
`;
|
|
994
|
+
function routeTemplate(libImport) {
|
|
995
|
+
return `import { glyphpay } from "${libImport}";\n\nexport const POST = glyphpay.checkout;\n`;
|
|
874
996
|
}
|
|
875
|
-
function webhookRouteTemplate() {
|
|
876
|
-
return `import { glyphpay } from "
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
997
|
+
function webhookRouteTemplate(libImport) {
|
|
998
|
+
return `import { glyphpay } from "${libImport}";\n\nexport const POST = glyphpay.webhook;\n`;
|
|
999
|
+
}
|
|
1000
|
+
function detectFramework(dependencies) {
|
|
1001
|
+
if (dependencies.next)
|
|
1002
|
+
return "next";
|
|
1003
|
+
if (dependencies["@sveltejs/kit"])
|
|
1004
|
+
return "sveltekit";
|
|
1005
|
+
if (dependencies.astro)
|
|
1006
|
+
return "astro";
|
|
1007
|
+
if (dependencies["@remix-run/node"] || dependencies["@remix-run/react"])
|
|
1008
|
+
return "remix";
|
|
1009
|
+
if (dependencies.hono)
|
|
1010
|
+
return "hono";
|
|
1011
|
+
if (dependencies.express)
|
|
1012
|
+
return "express";
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
function svelteRoute(kind, isTs) {
|
|
1016
|
+
const typeImport = isTs ? `import type { RequestHandler } from "./$types";\n\n` : "\n";
|
|
1017
|
+
const annotation = isTs ? ": RequestHandler" : "";
|
|
1018
|
+
return `import { glyphpay } from "$lib/server/glyphpay";\n${typeImport}export const POST${annotation} = ({ request }) => glyphpay.${kind}(request);\n`;
|
|
1019
|
+
}
|
|
1020
|
+
function astroRoute(kind, isTs) {
|
|
1021
|
+
const typeImport = isTs ? `import type { APIRoute } from "astro";\n\n` : "\n";
|
|
1022
|
+
const annotation = isTs ? ": APIRoute" : "";
|
|
1023
|
+
return `import { glyphpay } from "../../../lib/glyphpay";\n${typeImport}export const POST${annotation} = ({ request }) => glyphpay.${kind}(request);\n`;
|
|
1024
|
+
}
|
|
1025
|
+
function remixRoute(kind, isTs) {
|
|
1026
|
+
const typeImport = isTs ? `import type { ActionFunctionArgs } from "@remix-run/node";\n\n` : "\n";
|
|
1027
|
+
const signature = isTs ? "{ request }: ActionFunctionArgs" : "{ request }";
|
|
1028
|
+
return `import { glyphpay } from "~/lib/glyphpay.server";\n${typeImport}export async function action(${signature}) {\n return glyphpay.${kind}(request);\n}\n`;
|
|
1029
|
+
}
|
|
1030
|
+
function honoRoutes() {
|
|
1031
|
+
return `import { Hono } from "hono";\nimport { glyphpay } from "./lib/glyphpay";\n\nexport const glyphpayRoutes = new Hono();\n\nglyphpayRoutes.post("/checkout", (c) => glyphpay.checkout(c.req.raw));\nglyphpayRoutes.post("/webhook", (c) => glyphpay.webhook(c.req.raw));\n`;
|
|
1032
|
+
}
|
|
1033
|
+
function expressRoutes(isTs) {
|
|
1034
|
+
const typeImport = isTs
|
|
1035
|
+
? `import type { Request as ExpressRequest, Response as ExpressResponse } from "express";\n`
|
|
1036
|
+
: "";
|
|
1037
|
+
const signature = isTs ? ": ExpressRequest" : "";
|
|
1038
|
+
const responseType = isTs ? ": ExpressResponse" : "";
|
|
1039
|
+
const handlerType = isTs ? "(request: Request) => Promise<Response>" : "handler";
|
|
1040
|
+
return `import express from "express";\nimport { glyphpay } from "./lib/glyphpay";\n${typeImport}\nexport const glyphpayRoutes = express.Router();\n\nasync function adapt(handler${isTs ? `: ${handlerType}` : ""}, req${signature}, res${responseType}) {\n const body =\n req.method === "GET" || req.method === "HEAD"\n ? undefined\n : Buffer.isBuffer(req.body)\n ? req.body\n : typeof req.body === "string"\n ? req.body\n : JSON.stringify(req.body ?? {});\n const protocol = req.protocol || "http";\n const host = req.get("host") || "localhost";\n const request = new Request(\`\${protocol}://\${host}\${req.originalUrl}\`, {\n method: req.method,\n headers: req.headers as unknown as HeadersInit,\n body,\n });\n const response = await handler(request);\n res.status(response.status);\n response.headers.forEach((value, key) => res.setHeader(key, value));\n res.send(Buffer.from(await response.arrayBuffer()));\n}\n\nglyphpayRoutes.post("/checkout", express.json(), (req, res) => adapt(glyphpay.checkout, req, res));\nglyphpayRoutes.post("/webhook", express.raw({ type: "application/json" }), (req, res) => adapt(glyphpay.webhook, req, res));\n`;
|
|
1041
|
+
}
|
|
1042
|
+
async function frameworkPlan(projectRoot, framework) {
|
|
1043
|
+
const isTs = await fileExists(join(projectRoot, "tsconfig.json"));
|
|
1044
|
+
const extension = isTs ? "ts" : "js";
|
|
1045
|
+
const envComment = "# Glyph Pay — server-side only. Keep these in the server environment.\n";
|
|
1046
|
+
if (framework === "next") {
|
|
1047
|
+
const hasSrcApp = await directoryExists(join(projectRoot, "src", "app"));
|
|
1048
|
+
const hasRootApp = await directoryExists(join(projectRoot, "app"));
|
|
1049
|
+
const hasPages = (await directoryExists(join(projectRoot, "pages"))) ||
|
|
1050
|
+
(await directoryExists(join(projectRoot, "src", "pages")));
|
|
1051
|
+
if (!hasSrcApp && !hasRootApp && hasPages) {
|
|
1052
|
+
throw new CliUsageError("This project uses the Next.js Pages Router. glyphpay init supports the App Router; see the README for the two route files.");
|
|
1053
|
+
}
|
|
1054
|
+
const appDir = hasSrcApp ? join("src", "app") : join("app");
|
|
1055
|
+
const libDir = hasSrcApp ? join("src", "lib") : join("lib");
|
|
1056
|
+
const importPath = "../../../../lib/glyphpay";
|
|
1057
|
+
return {
|
|
1058
|
+
framework,
|
|
1059
|
+
libPath: join(projectRoot, libDir, `glyphpay.${extension}`),
|
|
1060
|
+
files: [
|
|
1061
|
+
{ path: join(projectRoot, libDir, `glyphpay.${extension}`), contents: libTemplate(isTs) },
|
|
1062
|
+
{ path: join(projectRoot, appDir, "api", "glyphpay", "checkout", `route.${extension}`), contents: routeTemplate(importPath) },
|
|
1063
|
+
{ path: join(projectRoot, appDir, "api", "glyphpay", "webhook", `route.${extension}`), contents: webhookRouteTemplate(importPath) },
|
|
1064
|
+
],
|
|
1065
|
+
envComment,
|
|
1066
|
+
notes: ["The generated routes live at /api/glyphpay/checkout and /api/glyphpay/webhook."],
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
if (framework === "sveltekit") {
|
|
1070
|
+
const libPath = join(projectRoot, "src", "lib", "server", `glyphpay.${extension}`);
|
|
1071
|
+
return {
|
|
1072
|
+
framework,
|
|
1073
|
+
libPath,
|
|
1074
|
+
files: [
|
|
1075
|
+
{ path: libPath, contents: libTemplate(isTs) },
|
|
1076
|
+
{ path: join(projectRoot, "src", "routes", "api", "glyphpay", "checkout", `+server.${extension}`), contents: svelteRoute("checkout", isTs) },
|
|
1077
|
+
{ path: join(projectRoot, "src", "routes", "api", "glyphpay", "webhook", `+server.${extension}`), contents: svelteRoute("webhook", isTs) },
|
|
1078
|
+
],
|
|
1079
|
+
envComment,
|
|
1080
|
+
notes: ["Use adapter-node or another server adapter; static SvelteKit builds cannot run checkout."],
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
if (framework === "astro") {
|
|
1084
|
+
const libPath = join(projectRoot, "src", "lib", `glyphpay.${extension}`);
|
|
1085
|
+
return {
|
|
1086
|
+
framework,
|
|
1087
|
+
libPath,
|
|
1088
|
+
files: [
|
|
1089
|
+
{ path: libPath, contents: libTemplate(isTs) },
|
|
1090
|
+
{ path: join(projectRoot, "src", "pages", "api", "glyphpay", `checkout.${extension}`), contents: astroRoute("checkout", isTs) },
|
|
1091
|
+
{ path: join(projectRoot, "src", "pages", "api", "glyphpay", `webhook.${extension}`), contents: astroRoute("webhook", isTs) },
|
|
1092
|
+
],
|
|
1093
|
+
envComment,
|
|
1094
|
+
notes: ["Set output to server/SSR in astro.config and install a server adapter before deploying."],
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
if (framework === "remix") {
|
|
1098
|
+
const libPath = join(projectRoot, "app", "lib", `glyphpay.server.${extension}`);
|
|
1099
|
+
return {
|
|
1100
|
+
framework,
|
|
1101
|
+
libPath,
|
|
1102
|
+
files: [
|
|
1103
|
+
{ path: libPath, contents: libTemplate(isTs) },
|
|
1104
|
+
{ path: join(projectRoot, "app", "routes", `api.glyphpay.checkout.${extension}`), contents: remixRoute("checkout", isTs) },
|
|
1105
|
+
{ path: join(projectRoot, "app", "routes", `api.glyphpay.webhook.${extension}`), contents: remixRoute("webhook", isTs) },
|
|
1106
|
+
],
|
|
1107
|
+
envComment,
|
|
1108
|
+
notes: ["Routes assume the standard ~ alias; adjust the import if your tsconfig uses another alias."],
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
if (framework === "hono") {
|
|
1112
|
+
const libPath = join(projectRoot, "src", "lib", `glyphpay.${extension}`);
|
|
1113
|
+
return {
|
|
1114
|
+
framework,
|
|
1115
|
+
libPath,
|
|
1116
|
+
files: [
|
|
1117
|
+
{ path: libPath, contents: libTemplate(isTs) },
|
|
1118
|
+
{ path: join(projectRoot, "src", `glyphpay-routes.${extension}`), contents: honoRoutes() },
|
|
1119
|
+
],
|
|
1120
|
+
envComment,
|
|
1121
|
+
notes: ["Mount the router in your app: app.route(\"/api/glyphpay\", glyphpayRoutes)."],
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
const libPath = join(projectRoot, "src", "lib", `glyphpay.${extension}`);
|
|
1125
|
+
return {
|
|
1126
|
+
framework: "express",
|
|
1127
|
+
libPath,
|
|
1128
|
+
files: [
|
|
1129
|
+
{ path: libPath, contents: libTemplate(isTs) },
|
|
1130
|
+
{ path: join(projectRoot, "src", `glyphpay-routes.${extension}`), contents: expressRoutes(isTs) },
|
|
1131
|
+
],
|
|
1132
|
+
envComment,
|
|
1133
|
+
notes: [
|
|
1134
|
+
"Mount the router with app.use(\"/api/glyphpay\", glyphpayRoutes).",
|
|
1135
|
+
"The webhook route uses express.raw so the signature is verified against the original bytes.",
|
|
1136
|
+
],
|
|
1137
|
+
};
|
|
880
1138
|
}
|
|
881
1139
|
function installCommand(hasPnpmLock, hasYarnLock, hasBunLock) {
|
|
882
1140
|
if (hasPnpmLock)
|
|
@@ -891,7 +1149,7 @@ export async function runInit(options = {}) {
|
|
|
891
1149
|
const projectRoot = resolve(options.dir ?? process.cwd());
|
|
892
1150
|
const packagePath = join(projectRoot, "package.json");
|
|
893
1151
|
if (!(await fileExists(packagePath))) {
|
|
894
|
-
throw new CliUsageError(`No package.json found in ${projectRoot}. Run init in a
|
|
1152
|
+
throw new CliUsageError(`No package.json found in ${projectRoot}. Run init in a web project.`);
|
|
895
1153
|
}
|
|
896
1154
|
let pkg;
|
|
897
1155
|
try {
|
|
@@ -901,29 +1159,13 @@ export async function runInit(options = {}) {
|
|
|
901
1159
|
throw new CliUsageError("package.json is not valid JSON.");
|
|
902
1160
|
}
|
|
903
1161
|
const dependencies = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
const
|
|
909
|
-
const hasPages = (await directoryExists(join(projectRoot, "pages"))) ||
|
|
910
|
-
(await directoryExists(join(projectRoot, "src", "pages")));
|
|
911
|
-
if (!hasSrcApp && !hasRootApp && hasPages) {
|
|
912
|
-
throw new CliUsageError("This project uses the Next.js Pages Router. glyphpay init supports the App Router; see the README for the two route files.");
|
|
913
|
-
}
|
|
914
|
-
const appDir = hasSrcApp ? join("src", "app") : join("app");
|
|
915
|
-
const libDir = hasSrcApp ? join("src", "lib") : join("lib");
|
|
916
|
-
const isTs = await fileExists(join(projectRoot, "tsconfig.json"));
|
|
917
|
-
const extension = isTs ? "ts" : "js";
|
|
918
|
-
const libPath = join(projectRoot, libDir, `glyphpay.${extension}`);
|
|
919
|
-
const checkoutPath = join(projectRoot, appDir, "api", "glyphpay", "checkout", `route.${extension}`);
|
|
920
|
-
const webhookPath = join(projectRoot, appDir, "api", "glyphpay", "webhook", `route.${extension}`);
|
|
1162
|
+
const framework = detectFramework(dependencies);
|
|
1163
|
+
if (!framework) {
|
|
1164
|
+
throw new CliUsageError("glyphpay init supports Next.js, Remix, Astro, SvelteKit, Express, and Hono. Install one of those frameworks first.");
|
|
1165
|
+
}
|
|
1166
|
+
const plan = await frameworkPlan(projectRoot, framework);
|
|
921
1167
|
const envPath = join(projectRoot, ".env.example");
|
|
922
|
-
const generated =
|
|
923
|
-
{ path: libPath, contents: libTemplate(isTs) },
|
|
924
|
-
{ path: checkoutPath, contents: routeTemplate() },
|
|
925
|
-
{ path: webhookPath, contents: webhookRouteTemplate() },
|
|
926
|
-
];
|
|
1168
|
+
const generated = plan.files;
|
|
927
1169
|
const existing = [];
|
|
928
1170
|
for (const file of generated) {
|
|
929
1171
|
if (await fileExists(file.path))
|
|
@@ -933,9 +1175,7 @@ export async function runInit(options = {}) {
|
|
|
933
1175
|
throw new CliUsageError(`Refusing to overwrite existing files:\n ${existing.join("\n ")}\nRe-run with --force to overwrite them.`);
|
|
934
1176
|
}
|
|
935
1177
|
const envExisting = await readEnvFile(envPath);
|
|
936
|
-
const envBase = envExisting.trim().length > 0
|
|
937
|
-
? envExisting
|
|
938
|
-
: "# Glyph Pay — server-side only. Never prefix these with NEXT_PUBLIC_.\n";
|
|
1178
|
+
const envBase = envExisting.trim().length > 0 ? envExisting : plan.envComment;
|
|
939
1179
|
const envNext = ensureEnvKeys(envBase, [TOKEN_KEY, CALLBACK_SECRET_KEY]);
|
|
940
1180
|
const envChanged = envNext !== envExisting;
|
|
941
1181
|
const display = (path) => relative(projectRoot, path).replace(/\\/g, "/");
|
|
@@ -949,6 +1189,7 @@ export async function runInit(options = {}) {
|
|
|
949
1189
|
}
|
|
950
1190
|
say();
|
|
951
1191
|
say(style(1, "Glyph Pay init"));
|
|
1192
|
+
say(`Framework: ${plan.framework}`);
|
|
952
1193
|
say();
|
|
953
1194
|
for (const file of generated) {
|
|
954
1195
|
const verb = existing.includes(display(file.path)) ? "Updated" : options.dryRun ? "Would create" : "Created";
|
|
@@ -962,7 +1203,9 @@ export async function runInit(options = {}) {
|
|
|
962
1203
|
say("Next steps:");
|
|
963
1204
|
say(` 1. ${installCommand(pnpm, yarn, bun)}`);
|
|
964
1205
|
say(" 2. npx glyphpay connect --url https://your-site.com");
|
|
965
|
-
say(` 3. Wire priceOrder() and onEvent() in ${display(libPath)}.`);
|
|
1206
|
+
say(` 3. Wire priceOrder() and onEvent() in ${display(plan.libPath)}.`);
|
|
1207
|
+
for (const note of plan.notes)
|
|
1208
|
+
say(` ${note}`);
|
|
966
1209
|
if (options.dryRun) {
|
|
967
1210
|
say();
|
|
968
1211
|
say("Dry run: no files were written.");
|
|
@@ -990,6 +1233,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
990
1233
|
await runInit(parsed.options);
|
|
991
1234
|
return 0;
|
|
992
1235
|
}
|
|
1236
|
+
if (parsed.command === "deliveries") {
|
|
1237
|
+
const result = await runDeliveries(parsed.options);
|
|
1238
|
+
return "outcome" in result && result.outcome !== "delivered" ? 1 : 0;
|
|
1239
|
+
}
|
|
993
1240
|
if (parsed.command === "disconnect") {
|
|
994
1241
|
await runDisconnect(parsed.options);
|
|
995
1242
|
return 0;
|
package/package.json
CHANGED