glyphpay 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -4
- package/dist/cli.d.ts +27 -2
- package/dist/cli.js +233 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,8 @@ The CLI covers setup, diagnostics, and credential lifecycle:
|
|
|
16
16
|
| `glyphpay doctor` | Check env, token, Stripe readiness, webhook URL, and an optional signed test event. |
|
|
17
17
|
| `glyphpay rotate` | Re-pair the site and replace the credential. |
|
|
18
18
|
| `glyphpay disconnect` | Revoke the credential and remove the local keys. |
|
|
19
|
-
| `glyphpay
|
|
19
|
+
| `glyphpay deliveries` | List recent webhook deliveries and retry one with `--retry <id>`. |
|
|
20
|
+
| `glyphpay init` | Scaffold routes for Next.js, Remix, Astro, SvelteKit, Hono, or Express. |
|
|
20
21
|
|
|
21
22
|
## Connect a site from the CLI
|
|
22
23
|
|
|
@@ -58,13 +59,31 @@ The test event has type `webhook.test` and a zero-dollar `data` payload. Ignore
|
|
|
58
59
|
|
|
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.
|
|
60
61
|
|
|
61
|
-
##
|
|
62
|
+
## Inspect and retry webhook deliveries
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
npx glyphpay deliveries
|
|
66
|
+
npx glyphpay deliveries --retry evt_...
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`deliveries` lists the most recent callback outbox records for the credential's client and environment: event type, delivery state, attempt count, timestamp, and the last error. 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 operations.
|
|
70
|
+
|
|
71
|
+
## Scaffold a site
|
|
62
72
|
|
|
63
73
|
```sh
|
|
64
74
|
npx glyphpay init
|
|
65
75
|
```
|
|
66
76
|
|
|
67
|
-
The command detects
|
|
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.
|
|
68
87
|
|
|
69
88
|
## Manual setup: connect the client in Design Glyph admin
|
|
70
89
|
|
|
@@ -143,7 +162,7 @@ Without React:
|
|
|
143
162
|
</script>
|
|
144
163
|
```
|
|
145
164
|
|
|
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,
|
|
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.
|
|
147
166
|
|
|
148
167
|
## How a payment flows
|
|
149
168
|
|
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,8 @@ 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;
|
|
51
54
|
}
|
|
52
55
|
export interface ConnectResult {
|
|
53
56
|
businessName: string;
|
|
@@ -61,7 +64,28 @@ export interface DisconnectResult {
|
|
|
61
64
|
alreadyRevoked: boolean;
|
|
62
65
|
canceled: boolean;
|
|
63
66
|
}
|
|
64
|
-
|
|
67
|
+
export interface CallbackDeliveryView {
|
|
68
|
+
id: string;
|
|
69
|
+
paymentId: string;
|
|
70
|
+
type: string;
|
|
71
|
+
state: "pending" | "delivered" | "failed";
|
|
72
|
+
attempts: number;
|
|
73
|
+
lastError: string | null;
|
|
74
|
+
createdAt: string;
|
|
75
|
+
updatedAt: string;
|
|
76
|
+
}
|
|
77
|
+
export interface CallbackDeliveriesResponse {
|
|
78
|
+
businessId: string;
|
|
79
|
+
businessName: string;
|
|
80
|
+
environment: "test" | "live";
|
|
81
|
+
deliveries: CallbackDeliveryView[];
|
|
82
|
+
}
|
|
83
|
+
export interface CallbackReplayResponse {
|
|
84
|
+
outcome: "delivered" | "retry" | "failed";
|
|
85
|
+
detail?: string;
|
|
86
|
+
delivery: CallbackDeliveryView;
|
|
87
|
+
}
|
|
88
|
+
type Command = "connect" | "rotate" | "disconnect" | "doctor" | "init" | "deliveries" | "help" | "version";
|
|
65
89
|
interface ParsedCommand {
|
|
66
90
|
command: Command;
|
|
67
91
|
options: CliOptions;
|
|
@@ -107,6 +131,7 @@ export interface DoctorReport {
|
|
|
107
131
|
checks: DoctorCheck[];
|
|
108
132
|
}
|
|
109
133
|
export declare function runDoctor(options?: CliOptions): Promise<DoctorReport>;
|
|
134
|
+
export declare function runDeliveries(options?: CliOptions): Promise<CallbackDeliveriesResponse | CallbackReplayResponse>;
|
|
110
135
|
export declare function runInit(options?: CliOptions): Promise<void>;
|
|
111
136
|
export declare function main(argv?: string[]): Promise<number>;
|
|
112
137
|
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.4.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,19 @@ 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
|
+
}
|
|
167
183
|
if (flag === "--dir") {
|
|
168
184
|
if (inlineValue !== undefined) {
|
|
169
185
|
if (!inlineValue)
|
|
@@ -202,7 +218,8 @@ Usage
|
|
|
202
218
|
glyphpay rotate [options] Re-pair and rotate the current credential
|
|
203
219
|
glyphpay disconnect [options] Revoke the credential and remove local keys
|
|
204
220
|
glyphpay doctor [options] Check env, token, Stripe and webhook setup
|
|
205
|
-
glyphpay
|
|
221
|
+
glyphpay deliveries [options] List recent webhook deliveries
|
|
222
|
+
glyphpay init [options] Scaffold an integration for your framework
|
|
206
223
|
glyphpay --version
|
|
207
224
|
|
|
208
225
|
Connect / rotate
|
|
@@ -222,6 +239,10 @@ Disconnect
|
|
|
222
239
|
--yes, -y Do not ask for confirmation
|
|
223
240
|
--keep-env Keep the local env values after revocation
|
|
224
241
|
|
|
242
|
+
Deliveries
|
|
243
|
+
--retry <id> Send one stored webhook again (alias: webhooks)
|
|
244
|
+
--json Print a machine-readable response
|
|
245
|
+
|
|
225
246
|
Init
|
|
226
247
|
--dir <path> Project directory (default: current directory)
|
|
227
248
|
--dry-run Show planned files without writing them
|
|
@@ -838,6 +859,56 @@ export async function runDoctor(options = {}) {
|
|
|
838
859
|
}
|
|
839
860
|
return report;
|
|
840
861
|
}
|
|
862
|
+
/* ------------------------------------------------------------------ */
|
|
863
|
+
/* Webhook deliveries */
|
|
864
|
+
/* ------------------------------------------------------------------ */
|
|
865
|
+
function printDelivery(delivery) {
|
|
866
|
+
say(` ${delivery.id} ${delivery.type} ${delivery.state.toUpperCase()} ` +
|
|
867
|
+
`attempt ${delivery.attempts} ${delivery.updatedAt}`);
|
|
868
|
+
if (delivery.lastError)
|
|
869
|
+
say(` ${delivery.lastError}`);
|
|
870
|
+
}
|
|
871
|
+
export async function runDeliveries(options = {}) {
|
|
872
|
+
const baseUrl = baseUrlFor(options);
|
|
873
|
+
const envFile = resolve(options.envFile ?? DEFAULT_ENV_FILE);
|
|
874
|
+
const contents = await readEnvFile(envFile);
|
|
875
|
+
const token = readEnvValue(contents, TOKEN_KEY);
|
|
876
|
+
if (!token) {
|
|
877
|
+
throw new CliUsageError(`No ${TOKEN_KEY} found in ${relative(process.cwd(), envFile) || envFile}. Run glyphpay connect first.`);
|
|
878
|
+
}
|
|
879
|
+
if (options.retry) {
|
|
880
|
+
const result = await api(baseUrl, `/credential/deliveries/${encodeURIComponent(options.retry)}/replay`, {}, token);
|
|
881
|
+
if (options.json) {
|
|
882
|
+
say(JSON.stringify(result, null, 2));
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
say();
|
|
886
|
+
say(style(1, `Webhook ${result.outcome === "delivered" ? "delivered" : "not delivered"}.`));
|
|
887
|
+
printDelivery(result.delivery);
|
|
888
|
+
if (result.detail)
|
|
889
|
+
say(` ${result.detail}`);
|
|
890
|
+
}
|
|
891
|
+
return result;
|
|
892
|
+
}
|
|
893
|
+
const result = await api(baseUrl, "/credential/deliveries", undefined, token);
|
|
894
|
+
if (options.json) {
|
|
895
|
+
say(JSON.stringify(result, null, 2));
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
898
|
+
say();
|
|
899
|
+
say(style(1, `Webhook deliveries - ${result.businessName} (${result.environment} mode)`));
|
|
900
|
+
say();
|
|
901
|
+
if (result.deliveries.length === 0) {
|
|
902
|
+
say("No deliveries recorded yet.");
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
for (const delivery of result.deliveries)
|
|
906
|
+
printDelivery(delivery);
|
|
907
|
+
say();
|
|
908
|
+
say("Retry one with `glyphpay deliveries --retry <id>`.");
|
|
909
|
+
}
|
|
910
|
+
return result;
|
|
911
|
+
}
|
|
841
912
|
function libTemplate(ts) {
|
|
842
913
|
const cartId = ts
|
|
843
914
|
? `const cartId = (body as { cartId?: string } | null)?.cartId;`
|
|
@@ -866,17 +937,150 @@ export const glyphpay = glyphPay({
|
|
|
866
937
|
});
|
|
867
938
|
`;
|
|
868
939
|
}
|
|
869
|
-
function routeTemplate() {
|
|
870
|
-
return `import { glyphpay } from "
|
|
871
|
-
|
|
872
|
-
export const POST = glyphpay.checkout;
|
|
873
|
-
`;
|
|
940
|
+
function routeTemplate(libImport) {
|
|
941
|
+
return `import { glyphpay } from "${libImport}";\n\nexport const POST = glyphpay.checkout;\n`;
|
|
874
942
|
}
|
|
875
|
-
function webhookRouteTemplate() {
|
|
876
|
-
return `import { glyphpay } from "
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
943
|
+
function webhookRouteTemplate(libImport) {
|
|
944
|
+
return `import { glyphpay } from "${libImport}";\n\nexport const POST = glyphpay.webhook;\n`;
|
|
945
|
+
}
|
|
946
|
+
function detectFramework(dependencies) {
|
|
947
|
+
if (dependencies.next)
|
|
948
|
+
return "next";
|
|
949
|
+
if (dependencies["@sveltejs/kit"])
|
|
950
|
+
return "sveltekit";
|
|
951
|
+
if (dependencies.astro)
|
|
952
|
+
return "astro";
|
|
953
|
+
if (dependencies["@remix-run/node"] || dependencies["@remix-run/react"])
|
|
954
|
+
return "remix";
|
|
955
|
+
if (dependencies.hono)
|
|
956
|
+
return "hono";
|
|
957
|
+
if (dependencies.express)
|
|
958
|
+
return "express";
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
function svelteRoute(kind, isTs) {
|
|
962
|
+
const typeImport = isTs ? `import type { RequestHandler } from "./$types";\n\n` : "\n";
|
|
963
|
+
const annotation = isTs ? ": RequestHandler" : "";
|
|
964
|
+
return `import { glyphpay } from "$lib/server/glyphpay";\n${typeImport}export const POST${annotation} = ({ request }) => glyphpay.${kind}(request);\n`;
|
|
965
|
+
}
|
|
966
|
+
function astroRoute(kind, isTs) {
|
|
967
|
+
const typeImport = isTs ? `import type { APIRoute } from "astro";\n\n` : "\n";
|
|
968
|
+
const annotation = isTs ? ": APIRoute" : "";
|
|
969
|
+
return `import { glyphpay } from "../../../lib/glyphpay";\n${typeImport}export const POST${annotation} = ({ request }) => glyphpay.${kind}(request);\n`;
|
|
970
|
+
}
|
|
971
|
+
function remixRoute(kind, isTs) {
|
|
972
|
+
const typeImport = isTs ? `import type { ActionFunctionArgs } from "@remix-run/node";\n\n` : "\n";
|
|
973
|
+
const signature = isTs ? "{ request }: ActionFunctionArgs" : "{ request }";
|
|
974
|
+
return `import { glyphpay } from "~/lib/glyphpay.server";\n${typeImport}export async function action(${signature}) {\n return glyphpay.${kind}(request);\n}\n`;
|
|
975
|
+
}
|
|
976
|
+
function honoRoutes() {
|
|
977
|
+
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`;
|
|
978
|
+
}
|
|
979
|
+
function expressRoutes(isTs) {
|
|
980
|
+
const typeImport = isTs
|
|
981
|
+
? `import type { Request as ExpressRequest, Response as ExpressResponse } from "express";\n`
|
|
982
|
+
: "";
|
|
983
|
+
const signature = isTs ? ": ExpressRequest" : "";
|
|
984
|
+
const responseType = isTs ? ": ExpressResponse" : "";
|
|
985
|
+
const handlerType = isTs ? "(request: Request) => Promise<Response>" : "handler";
|
|
986
|
+
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`;
|
|
987
|
+
}
|
|
988
|
+
async function frameworkPlan(projectRoot, framework) {
|
|
989
|
+
const isTs = await fileExists(join(projectRoot, "tsconfig.json"));
|
|
990
|
+
const extension = isTs ? "ts" : "js";
|
|
991
|
+
const envComment = "# Glyph Pay — server-side only. Keep these in the server environment.\n";
|
|
992
|
+
if (framework === "next") {
|
|
993
|
+
const hasSrcApp = await directoryExists(join(projectRoot, "src", "app"));
|
|
994
|
+
const hasRootApp = await directoryExists(join(projectRoot, "app"));
|
|
995
|
+
const hasPages = (await directoryExists(join(projectRoot, "pages"))) ||
|
|
996
|
+
(await directoryExists(join(projectRoot, "src", "pages")));
|
|
997
|
+
if (!hasSrcApp && !hasRootApp && hasPages) {
|
|
998
|
+
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.");
|
|
999
|
+
}
|
|
1000
|
+
const appDir = hasSrcApp ? join("src", "app") : join("app");
|
|
1001
|
+
const libDir = hasSrcApp ? join("src", "lib") : join("lib");
|
|
1002
|
+
const importPath = "../../../../lib/glyphpay";
|
|
1003
|
+
return {
|
|
1004
|
+
framework,
|
|
1005
|
+
libPath: join(projectRoot, libDir, `glyphpay.${extension}`),
|
|
1006
|
+
files: [
|
|
1007
|
+
{ path: join(projectRoot, libDir, `glyphpay.${extension}`), contents: libTemplate(isTs) },
|
|
1008
|
+
{ path: join(projectRoot, appDir, "api", "glyphpay", "checkout", `route.${extension}`), contents: routeTemplate(importPath) },
|
|
1009
|
+
{ path: join(projectRoot, appDir, "api", "glyphpay", "webhook", `route.${extension}`), contents: webhookRouteTemplate(importPath) },
|
|
1010
|
+
],
|
|
1011
|
+
envComment,
|
|
1012
|
+
notes: ["The generated routes live at /api/glyphpay/checkout and /api/glyphpay/webhook."],
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
if (framework === "sveltekit") {
|
|
1016
|
+
const libPath = join(projectRoot, "src", "lib", "server", `glyphpay.${extension}`);
|
|
1017
|
+
return {
|
|
1018
|
+
framework,
|
|
1019
|
+
libPath,
|
|
1020
|
+
files: [
|
|
1021
|
+
{ path: libPath, contents: libTemplate(isTs) },
|
|
1022
|
+
{ path: join(projectRoot, "src", "routes", "api", "glyphpay", "checkout", `+server.${extension}`), contents: svelteRoute("checkout", isTs) },
|
|
1023
|
+
{ path: join(projectRoot, "src", "routes", "api", "glyphpay", "webhook", `+server.${extension}`), contents: svelteRoute("webhook", isTs) },
|
|
1024
|
+
],
|
|
1025
|
+
envComment,
|
|
1026
|
+
notes: ["Use adapter-node or another server adapter; static SvelteKit builds cannot run checkout."],
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
if (framework === "astro") {
|
|
1030
|
+
const libPath = join(projectRoot, "src", "lib", `glyphpay.${extension}`);
|
|
1031
|
+
return {
|
|
1032
|
+
framework,
|
|
1033
|
+
libPath,
|
|
1034
|
+
files: [
|
|
1035
|
+
{ path: libPath, contents: libTemplate(isTs) },
|
|
1036
|
+
{ path: join(projectRoot, "src", "pages", "api", "glyphpay", `checkout.${extension}`), contents: astroRoute("checkout", isTs) },
|
|
1037
|
+
{ path: join(projectRoot, "src", "pages", "api", "glyphpay", `webhook.${extension}`), contents: astroRoute("webhook", isTs) },
|
|
1038
|
+
],
|
|
1039
|
+
envComment,
|
|
1040
|
+
notes: ["Set output to server/SSR in astro.config and install a server adapter before deploying."],
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
if (framework === "remix") {
|
|
1044
|
+
const libPath = join(projectRoot, "app", "lib", `glyphpay.server.${extension}`);
|
|
1045
|
+
return {
|
|
1046
|
+
framework,
|
|
1047
|
+
libPath,
|
|
1048
|
+
files: [
|
|
1049
|
+
{ path: libPath, contents: libTemplate(isTs) },
|
|
1050
|
+
{ path: join(projectRoot, "app", "routes", `api.glyphpay.checkout.${extension}`), contents: remixRoute("checkout", isTs) },
|
|
1051
|
+
{ path: join(projectRoot, "app", "routes", `api.glyphpay.webhook.${extension}`), contents: remixRoute("webhook", isTs) },
|
|
1052
|
+
],
|
|
1053
|
+
envComment,
|
|
1054
|
+
notes: ["Routes assume the standard ~ alias; adjust the import if your tsconfig uses another alias."],
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
if (framework === "hono") {
|
|
1058
|
+
const libPath = join(projectRoot, "src", "lib", `glyphpay.${extension}`);
|
|
1059
|
+
return {
|
|
1060
|
+
framework,
|
|
1061
|
+
libPath,
|
|
1062
|
+
files: [
|
|
1063
|
+
{ path: libPath, contents: libTemplate(isTs) },
|
|
1064
|
+
{ path: join(projectRoot, "src", `glyphpay-routes.${extension}`), contents: honoRoutes() },
|
|
1065
|
+
],
|
|
1066
|
+
envComment,
|
|
1067
|
+
notes: ["Mount the router in your app: app.route(\"/api/glyphpay\", glyphpayRoutes)."],
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
const libPath = join(projectRoot, "src", "lib", `glyphpay.${extension}`);
|
|
1071
|
+
return {
|
|
1072
|
+
framework: "express",
|
|
1073
|
+
libPath,
|
|
1074
|
+
files: [
|
|
1075
|
+
{ path: libPath, contents: libTemplate(isTs) },
|
|
1076
|
+
{ path: join(projectRoot, "src", `glyphpay-routes.${extension}`), contents: expressRoutes(isTs) },
|
|
1077
|
+
],
|
|
1078
|
+
envComment,
|
|
1079
|
+
notes: [
|
|
1080
|
+
"Mount the router with app.use(\"/api/glyphpay\", glyphpayRoutes).",
|
|
1081
|
+
"The webhook route uses express.raw so the signature is verified against the original bytes.",
|
|
1082
|
+
],
|
|
1083
|
+
};
|
|
880
1084
|
}
|
|
881
1085
|
function installCommand(hasPnpmLock, hasYarnLock, hasBunLock) {
|
|
882
1086
|
if (hasPnpmLock)
|
|
@@ -891,7 +1095,7 @@ export async function runInit(options = {}) {
|
|
|
891
1095
|
const projectRoot = resolve(options.dir ?? process.cwd());
|
|
892
1096
|
const packagePath = join(projectRoot, "package.json");
|
|
893
1097
|
if (!(await fileExists(packagePath))) {
|
|
894
|
-
throw new CliUsageError(`No package.json found in ${projectRoot}. Run init in a
|
|
1098
|
+
throw new CliUsageError(`No package.json found in ${projectRoot}. Run init in a web project.`);
|
|
895
1099
|
}
|
|
896
1100
|
let pkg;
|
|
897
1101
|
try {
|
|
@@ -901,29 +1105,13 @@ export async function runInit(options = {}) {
|
|
|
901
1105
|
throw new CliUsageError("package.json is not valid JSON.");
|
|
902
1106
|
}
|
|
903
1107
|
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}`);
|
|
1108
|
+
const framework = detectFramework(dependencies);
|
|
1109
|
+
if (!framework) {
|
|
1110
|
+
throw new CliUsageError("glyphpay init supports Next.js, Remix, Astro, SvelteKit, Express, and Hono. Install one of those frameworks first.");
|
|
1111
|
+
}
|
|
1112
|
+
const plan = await frameworkPlan(projectRoot, framework);
|
|
921
1113
|
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
|
-
];
|
|
1114
|
+
const generated = plan.files;
|
|
927
1115
|
const existing = [];
|
|
928
1116
|
for (const file of generated) {
|
|
929
1117
|
if (await fileExists(file.path))
|
|
@@ -933,9 +1121,7 @@ export async function runInit(options = {}) {
|
|
|
933
1121
|
throw new CliUsageError(`Refusing to overwrite existing files:\n ${existing.join("\n ")}\nRe-run with --force to overwrite them.`);
|
|
934
1122
|
}
|
|
935
1123
|
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";
|
|
1124
|
+
const envBase = envExisting.trim().length > 0 ? envExisting : plan.envComment;
|
|
939
1125
|
const envNext = ensureEnvKeys(envBase, [TOKEN_KEY, CALLBACK_SECRET_KEY]);
|
|
940
1126
|
const envChanged = envNext !== envExisting;
|
|
941
1127
|
const display = (path) => relative(projectRoot, path).replace(/\\/g, "/");
|
|
@@ -949,6 +1135,7 @@ export async function runInit(options = {}) {
|
|
|
949
1135
|
}
|
|
950
1136
|
say();
|
|
951
1137
|
say(style(1, "Glyph Pay init"));
|
|
1138
|
+
say(`Framework: ${plan.framework}`);
|
|
952
1139
|
say();
|
|
953
1140
|
for (const file of generated) {
|
|
954
1141
|
const verb = existing.includes(display(file.path)) ? "Updated" : options.dryRun ? "Would create" : "Created";
|
|
@@ -962,7 +1149,9 @@ export async function runInit(options = {}) {
|
|
|
962
1149
|
say("Next steps:");
|
|
963
1150
|
say(` 1. ${installCommand(pnpm, yarn, bun)}`);
|
|
964
1151
|
say(" 2. npx glyphpay connect --url https://your-site.com");
|
|
965
|
-
say(` 3. Wire priceOrder() and onEvent() in ${display(libPath)}.`);
|
|
1152
|
+
say(` 3. Wire priceOrder() and onEvent() in ${display(plan.libPath)}.`);
|
|
1153
|
+
for (const note of plan.notes)
|
|
1154
|
+
say(` ${note}`);
|
|
966
1155
|
if (options.dryRun) {
|
|
967
1156
|
say();
|
|
968
1157
|
say("Dry run: no files were written.");
|
|
@@ -990,6 +1179,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
990
1179
|
await runInit(parsed.options);
|
|
991
1180
|
return 0;
|
|
992
1181
|
}
|
|
1182
|
+
if (parsed.command === "deliveries") {
|
|
1183
|
+
const result = await runDeliveries(parsed.options);
|
|
1184
|
+
return "outcome" in result && result.outcome !== "delivered" ? 1 : 0;
|
|
1185
|
+
}
|
|
993
1186
|
if (parsed.command === "disconnect") {
|
|
994
1187
|
await runDisconnect(parsed.options);
|
|
995
1188
|
return 0;
|
package/package.json
CHANGED