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.
Files changed (4) hide show
  1. package/README.md +184 -165
  2. package/dist/cli.d.ts +34 -2
  3. package/dist/cli.js +341 -94
  4. 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 init` | Scaffold the Next.js App Router routes and env placeholders. |
20
-
21
- ## Connect a site from the CLI
22
-
23
- ```sh
24
- npx glyphpay connect --url https://client-site.com
25
- ```
26
-
27
- 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`:
28
-
29
- ```sh
30
- GLYPHPAY_TOKEN=dgsvc_test_...
31
- GLYPHPAY_CALLBACK_SECRET=dgcb_...
32
- ```
33
-
34
- - `--url <site>` registers `<site>/api/glyphpay/webhook`. Pass a full URL to use a different route.
35
- - `--env <path>` targets another env file; `--print` prints the values instead of writing.
36
- - `--no-open`, `--base-url` and `--timeout` help scripts and local testing.
37
- - Codes expire after ten minutes. Approving again rotates the token: the previous token stops working immediately.
38
- - Pairing is disabled while the platform is in live mode.
39
-
40
- The browser never receives the credential. Keep the token in server environment variables and never prefix it with `NEXT_PUBLIC_`, `VITE_` or `PUBLIC_`.
41
-
42
- ## Check, rotate, and disconnect
43
-
44
- ```sh
45
- npx glyphpay doctor
46
- npx glyphpay doctor --json
47
- npx glyphpay rotate
48
- npx glyphpay disconnect
49
- ```
50
-
51
- `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:
52
-
53
- ```sh
54
- npx glyphpay doctor --send-test-event
55
- ```
56
-
57
- 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.
58
-
59
- `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.
60
-
61
- ## Scaffold a Next.js site
62
-
63
- ```sh
64
- npx glyphpay init
65
- ```
66
-
67
- The command detects `app` or `src/app`, creates `lib/glyphpay.ts` (or `.js`), adds the checkout and webhook route handlers, and appends placeholders to `.env.example` without changing existing values. 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.
68
-
69
- ## Manual setup: connect the client in Design Glyph admin
70
-
71
- 1. **Admin → Clients → (client)**: make sure Stripe onboarding is complete (charges enabled).
72
- 2. **Issue a service credential** (test first). Copy the `dgsvc_test_…` token. It is shown only once.
73
- 3. **Set the callback URL** to `https://<client-site>/api/glyphpay/webhook`. Copy the signing secret. It is also shown only once.
74
-
75
- ## Add to the client site
76
-
77
- ```sh
78
- pnpm add glyphpay
79
- ```
80
-
81
- `.env` (server only, never `NEXT_PUBLIC_`):
82
-
83
- ```sh
84
- GLYPHPAY_TOKEN=dgsvc_test_...
85
- GLYPHPAY_CALLBACK_SECRET=...
86
- ```
87
-
88
- `lib/glyphpay.ts`
89
-
90
- ```ts
91
- import { glyphPay, CheckoutError } from "glyphpay/server";
92
-
93
- export const glyphpay = glyphPay({
94
- token: process.env.GLYPHPAY_TOKEN,
95
- callbackSecret: process.env.GLYPHPAY_CALLBACK_SECRET,
96
-
97
- // Price the order from YOUR data. Never trust an amount sent by the browser.
98
- async priceOrder(body) {
99
- const cart = await db.carts.get((body as { cartId: string }).cartId);
100
- if (!cart?.items.length) throw new CheckoutError("Your cart is empty.");
101
- // orderId must change whenever the total changes, or GlyphPay returns 409.
102
- return { orderId: `${cart.id}-v${cart.version}`, amountCents: cart.totalCents, description: `Order ${cart.id}` };
103
- },
104
-
105
- // Signed and verified. At-least-once delivery: dedupe on event.id. Throw to have it retried.
106
- async onEvent(event) {
107
- if (event.type === "payment.succeeded") await db.orders.markPaid(event.data.orderId, event.id);
108
- },
109
- });
110
- ```
111
-
112
- `app/api/glyphpay/checkout/route.ts`
113
-
114
- ```ts
115
- import { glyphpay } from "@/lib/glyphpay";
116
- export const POST = glyphpay.checkout;
117
- ```
118
-
119
- `app/api/glyphpay/webhook/route.ts`
120
-
121
- ```ts
122
- import { glyphpay } from "@/lib/glyphpay";
123
- export const POST = glyphpay.webhook;
124
- ```
125
-
126
- Checkout page:
127
-
128
- ```tsx
129
- import { GlyphPayCheckout } from "glyphpay/react";
130
-
131
- <GlyphPayCheckout order={{ cartId }} returnUrl={`/thank-you?cart=${cartId}`} appearance={{ theme: "stripe" }} />
132
- ```
133
-
134
- Style the button with `.glyphpay button { … }`. Errors appear in `.glyphpay [role=alert]`.
135
-
136
- Without React:
137
-
138
- ```html
139
- <div id="pay"></div>
140
- <script type="module">
141
- import { mountCheckout } from "glyphpay/browser";
142
- mountCheckout(document.getElementById("pay"), { order: { cartId: "42" }, returnUrl: "/thank-you" });
143
- </script>
144
- ```
145
-
146
- 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, use `createPayment` and `verifyCallback` directly.
147
-
148
- ## How a payment flows
149
-
150
- 1. The component POSTs `order` to `/api/glyphpay/checkout`. `priceOrder` sets the amount, and the site's server calls GlyphPay with the token.
151
- 2. The browser gets back only `clientSecret`, `publishableKey`, and `stripeAccount`, then mounts Stripe's Payment Element.
152
- 3. The shopper pays, and Stripe redirects to `returnUrl`. **The redirect doesn't prove payment.**
153
- 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)`.
154
-
155
- Refunds run server-side:
156
-
157
- ```ts
158
- await glyphpay.refund(paymentId, { idempotencyKey: `${orderId}-refund-1` });
159
- ```
160
-
161
- ## Develop
162
-
163
- ```sh
164
- cd packages/glyphpay && npm install && npm test
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 the Next.js App Router integration.
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
- type Command = "connect" | "rotate" | "disconnect" | "doctor" | "init" | "help" | "version";
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 the Next.js App Router integration.
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.3.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 init [options] Scaffold a Next.js App Router integration
206
- glyphpay --version
207
-
208
- Connect / rotate
209
- --url <site-url> Site to connect. Adds ${WEBHOOK_PATH} when the URL
210
- has no path. Or pass the exact callback URL.
211
- --env <path> Env file to update (default: ${DEFAULT_ENV_FILE})
212
- --print Print the credentials instead of writing a file
213
- --no-open Do not open a browser automatically
214
- --base-url <url> Glyph Pay API origin (default: ${DEFAULT_BASE_URL})
215
- --timeout <seconds> Wait for approval (default: ${DEFAULT_TIMEOUT_SECONDS})
216
-
217
- Doctor
218
- --json Print a machine-readable report
219
- --send-test-event POST a signed webhook.test event to the callback URL
220
-
221
- Disconnect
222
- --yes, -y Do not ask for confirmation
223
- --keep-env Keep the local env values after revocation
224
-
225
- Init
226
- --dir <path> Project directory (default: current directory)
227
- --dry-run Show planned files without writing them
228
- --force Overwrite generated files
229
-
230
- The browser step signs in to Glyph Pay, lets you pick the client account, and
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 "../../../../lib/glyphpay";
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 "../../../../lib/glyphpay";
877
-
878
- export const POST = glyphpay.webhook;
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 Next.js project.`);
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
- if (!dependencies.next) {
905
- throw new CliUsageError("glyphpay init currently supports Next.js projects only.");
906
- }
907
- const hasSrcApp = await directoryExists(join(projectRoot, "src", "app"));
908
- const hasRootApp = await directoryExists(join(projectRoot, "app"));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glyphpay",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Drop-in GlyphPay checkout for websites, with a CLI for setup, diagnostics, and credential lifecycle.",
5
5
  "homepage": "https://glyphpay.net",
6
6
  "keywords": [