glyphpay 0.1.0 → 0.2.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 +22 -1
- package/dist/cli.d.ts +58 -0
- package/dist/cli.js +409 -0
- package/package.json +35 -8
package/README.md
CHANGED
|
@@ -8,7 +8,28 @@ Drop-in connector that lets a website take payments through [GlyphPay](https://g
|
|
|
8
8
|
| `glyphpay/react` | Browser | `<GlyphPayCheckout />` |
|
|
9
9
|
| `glyphpay/browser` | Browser, any site | `mountCheckout(el, options)` for sites without React |
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Connect a site from the CLI
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx glyphpay connect --url https://client-site.com
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
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`:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
GLYPHPAY_TOKEN=dgsvc_test_...
|
|
21
|
+
GLYPHPAY_CALLBACK_SECRET=dgcb_...
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `--url <site>` registers `<site>/api/glyphpay/webhook`. Pass a full URL to use a different route.
|
|
25
|
+
- `--env <path>` targets another env file; `--print` prints the values instead of writing.
|
|
26
|
+
- `--no-open`, `--base-url` and `--timeout` help scripts and local testing.
|
|
27
|
+
- Codes expire after ten minutes. Approving again rotates the token: the previous token stops working immediately.
|
|
28
|
+
- Pairing is disabled while the platform is in live mode.
|
|
29
|
+
|
|
30
|
+
The browser never receives the credential. Keep the token in server environment variables and never prefix it with `NEXT_PUBLIC_`, `VITE_` or `PUBLIC_`.
|
|
31
|
+
|
|
32
|
+
## Manual setup: connect the client in Design Glyph admin
|
|
12
33
|
|
|
13
34
|
1. **Admin → Clients → (client)**: make sure Stripe onboarding is complete (charges enabled).
|
|
14
35
|
2. **Issue a service credential** (test first). Copy the `dgsvc_test_…` token. It is shown only once.
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `glyphpay connect` - browser pairing for a client site.
|
|
4
|
+
*
|
|
5
|
+
* The CLI never asks for a password or an API token. It creates a short-lived
|
|
6
|
+
* pairing request, opens the Glyph Pay site, and waits while the signed-in
|
|
7
|
+
* owner approves it in the browser. The service credential and webhook signing
|
|
8
|
+
* secret are returned only to this process, then written to the site's server
|
|
9
|
+
* environment file.
|
|
10
|
+
*
|
|
11
|
+
* Zero runtime dependencies: Node 18+ built-ins and global fetch only.
|
|
12
|
+
*/
|
|
13
|
+
export declare class CliUsageError extends Error {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class CliApiError extends Error {
|
|
17
|
+
readonly status: number;
|
|
18
|
+
readonly code: string;
|
|
19
|
+
constructor(status: number, code: string, message: string);
|
|
20
|
+
}
|
|
21
|
+
export interface ConnectOptions {
|
|
22
|
+
/** Site URL (a webhook path is appended when the URL has no path), or the exact callback URL. */
|
|
23
|
+
url?: string;
|
|
24
|
+
/** Env file to update. Defaults to `.env.local`. */
|
|
25
|
+
envFile?: string;
|
|
26
|
+
/** Print the credentials instead of writing them to a file. */
|
|
27
|
+
print?: boolean;
|
|
28
|
+
/** Skip opening a browser. */
|
|
29
|
+
noOpen?: boolean;
|
|
30
|
+
/** API origin. Defaults to GLYPHPAY_API_URL or production. */
|
|
31
|
+
baseUrl?: string;
|
|
32
|
+
/** How long to wait for browser approval. */
|
|
33
|
+
timeoutSeconds?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface ConnectResult {
|
|
36
|
+
businessName: string;
|
|
37
|
+
environment: "test" | "live";
|
|
38
|
+
callbackUrl: string | null;
|
|
39
|
+
writtenTo: string | null;
|
|
40
|
+
}
|
|
41
|
+
interface ParsedCommand {
|
|
42
|
+
command: "connect" | "help" | "version";
|
|
43
|
+
options: ConnectOptions;
|
|
44
|
+
}
|
|
45
|
+
export declare function parseArgs(argv: string[]): ParsedCommand;
|
|
46
|
+
export declare function helpText(): string;
|
|
47
|
+
/** `shop.example.com` -> `https://shop.example.com/api/glyphpay/webhook`. */
|
|
48
|
+
export declare function toCallbackUrl(raw: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Replace values in an env file without touching unrelated lines. The first
|
|
51
|
+
* occurrence of a managed key is rewritten; duplicate managed keys are dropped
|
|
52
|
+
* so the file keeps a single source of truth.
|
|
53
|
+
*/
|
|
54
|
+
export declare function upsertEnv(contents: string, values: Record<string, string>): string;
|
|
55
|
+
export declare function openBrowser(url: string): boolean;
|
|
56
|
+
export declare function runConnect(options?: ConnectOptions): Promise<ConnectResult>;
|
|
57
|
+
export declare function main(argv?: string[]): Promise<number>;
|
|
58
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `glyphpay connect` - browser pairing for a client site.
|
|
4
|
+
*
|
|
5
|
+
* The CLI never asks for a password or an API token. It creates a short-lived
|
|
6
|
+
* pairing request, opens the Glyph Pay site, and waits while the signed-in
|
|
7
|
+
* owner approves it in the browser. The service credential and webhook signing
|
|
8
|
+
* secret are returned only to this process, then written to the site's server
|
|
9
|
+
* environment file.
|
|
10
|
+
*
|
|
11
|
+
* Zero runtime dependencies: Node 18+ built-ins and global fetch only.
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
15
|
+
import { dirname, relative, resolve } from "node:path";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
const VERSION = "0.2.0";
|
|
18
|
+
const DEFAULT_BASE_URL = "https://designglyph.web.app";
|
|
19
|
+
const DEFAULT_ENV_FILE = ".env.local";
|
|
20
|
+
const DEFAULT_TIMEOUT_SECONDS = 600;
|
|
21
|
+
const TOKEN_KEY = "GLYPHPAY_TOKEN";
|
|
22
|
+
const CALLBACK_SECRET_KEY = "GLYPHPAY_CALLBACK_SECRET";
|
|
23
|
+
const WEBHOOK_PATH = "/api/glyphpay/webhook";
|
|
24
|
+
export class CliUsageError extends Error {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "CliUsageError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class CliApiError extends Error {
|
|
31
|
+
status;
|
|
32
|
+
code;
|
|
33
|
+
constructor(status, code, message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "CliApiError";
|
|
36
|
+
this.status = status;
|
|
37
|
+
this.code = code;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const useColor = !!process.stdout.isTTY && !process.env.NO_COLOR;
|
|
41
|
+
function style(code, text) {
|
|
42
|
+
return useColor ? `\u001b[${code}m${text}\u001b[0m` : text;
|
|
43
|
+
}
|
|
44
|
+
function say(text = "") {
|
|
45
|
+
process.stdout.write(`${text}\n`);
|
|
46
|
+
}
|
|
47
|
+
function sleep(ms) {
|
|
48
|
+
return new Promise((done) => setTimeout(done, ms));
|
|
49
|
+
}
|
|
50
|
+
/* ------------------------------------------------------------------ */
|
|
51
|
+
/* Argument parsing */
|
|
52
|
+
/* ------------------------------------------------------------------ */
|
|
53
|
+
function takeValue(argv, index, flag) {
|
|
54
|
+
const value = argv[index + 1];
|
|
55
|
+
if (!value || value.startsWith("-"))
|
|
56
|
+
throw new CliUsageError(`${flag} needs a value.`);
|
|
57
|
+
return [value, index + 1];
|
|
58
|
+
}
|
|
59
|
+
export function parseArgs(argv) {
|
|
60
|
+
const options = {};
|
|
61
|
+
let command = "connect";
|
|
62
|
+
let sawCommand = false;
|
|
63
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
64
|
+
const argument = argv[index];
|
|
65
|
+
if (argument === "--help" || argument === "-h" || argument === "help")
|
|
66
|
+
return { command: "help", options };
|
|
67
|
+
if (argument === "--version" || argument === "-v")
|
|
68
|
+
return { command: "version", options };
|
|
69
|
+
if (argument === "connect" || argument === "login") {
|
|
70
|
+
if (sawCommand)
|
|
71
|
+
throw new CliUsageError("Only one command can run at a time.");
|
|
72
|
+
sawCommand = true;
|
|
73
|
+
command = "connect";
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (argument === "--print") {
|
|
77
|
+
options.print = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (argument === "--no-open") {
|
|
81
|
+
options.noOpen = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const [flag, inlineValue] = argument.includes("=") ? argument.split(/=(.*)/s, 2) : [argument, undefined];
|
|
85
|
+
if (flag === "--url" || flag === "--callback-url") {
|
|
86
|
+
if (inlineValue !== undefined) {
|
|
87
|
+
if (!inlineValue)
|
|
88
|
+
throw new CliUsageError(`${flag} needs a value.`);
|
|
89
|
+
options.url = inlineValue;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const [value, next] = takeValue(argv, index, flag);
|
|
93
|
+
options.url = value;
|
|
94
|
+
index = next;
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (flag === "--env") {
|
|
99
|
+
if (inlineValue !== undefined) {
|
|
100
|
+
if (!inlineValue)
|
|
101
|
+
throw new CliUsageError("--env needs a path.");
|
|
102
|
+
options.envFile = inlineValue;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const [value, next] = takeValue(argv, index, flag);
|
|
106
|
+
options.envFile = value;
|
|
107
|
+
index = next;
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (flag === "--base-url") {
|
|
112
|
+
if (inlineValue !== undefined) {
|
|
113
|
+
if (!inlineValue)
|
|
114
|
+
throw new CliUsageError("--base-url needs a URL.");
|
|
115
|
+
options.baseUrl = inlineValue;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const [value, next] = takeValue(argv, index, flag);
|
|
119
|
+
options.baseUrl = value;
|
|
120
|
+
index = next;
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (flag === "--timeout") {
|
|
125
|
+
const raw = inlineValue ?? takeValue(argv, index, flag)[0];
|
|
126
|
+
if (inlineValue === undefined)
|
|
127
|
+
index += 1;
|
|
128
|
+
const parsed = Number(raw);
|
|
129
|
+
if (!Number.isFinite(parsed) || parsed < 10 || parsed > 1800) {
|
|
130
|
+
throw new CliUsageError("--timeout must be between 10 and 1800 seconds.");
|
|
131
|
+
}
|
|
132
|
+
options.timeoutSeconds = Math.floor(parsed);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (argument.startsWith("-"))
|
|
136
|
+
throw new CliUsageError(`Unknown option: ${argument}`);
|
|
137
|
+
throw new CliUsageError(`Unknown command: ${argument}`);
|
|
138
|
+
}
|
|
139
|
+
return { command, options };
|
|
140
|
+
}
|
|
141
|
+
export function helpText() {
|
|
142
|
+
return `glyphpay - take payments on your site through Glyph Pay
|
|
143
|
+
|
|
144
|
+
Usage
|
|
145
|
+
glyphpay connect [options] Connect a site (alias: glyphpay login)
|
|
146
|
+
glyphpay --version
|
|
147
|
+
|
|
148
|
+
Options
|
|
149
|
+
--url <site-url> Site to connect. Adds /api/glyphpay/webhook when the
|
|
150
|
+
URL has no path. Or pass the exact callback URL.
|
|
151
|
+
--env <path> Env file to update (default: ${DEFAULT_ENV_FILE})
|
|
152
|
+
--print Print the credentials instead of writing a file
|
|
153
|
+
--no-open Do not open a browser automatically
|
|
154
|
+
--base-url <url> Glyph Pay API origin (default: ${DEFAULT_BASE_URL})
|
|
155
|
+
--timeout <seconds> Wait for approval (default: ${DEFAULT_TIMEOUT_SECONDS})
|
|
156
|
+
-h, --help Show this help
|
|
157
|
+
-v, --version Show the version
|
|
158
|
+
|
|
159
|
+
The browser step signs in to Glyph Pay, lets you pick the client account, and
|
|
160
|
+
approves this machine. The service credential goes only to this terminal.`;
|
|
161
|
+
}
|
|
162
|
+
/* ------------------------------------------------------------------ */
|
|
163
|
+
/* URL and env-file helpers */
|
|
164
|
+
/* ------------------------------------------------------------------ */
|
|
165
|
+
/** `shop.example.com` -> `https://shop.example.com/api/glyphpay/webhook`. */
|
|
166
|
+
export function toCallbackUrl(raw) {
|
|
167
|
+
let value = raw.trim();
|
|
168
|
+
if (!value)
|
|
169
|
+
throw new CliUsageError("Enter the site's URL.");
|
|
170
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value))
|
|
171
|
+
value = `https://${value}`;
|
|
172
|
+
let url;
|
|
173
|
+
try {
|
|
174
|
+
url = new URL(value);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
throw new CliUsageError(`"${raw}" is not a valid URL.`);
|
|
178
|
+
}
|
|
179
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
180
|
+
throw new CliUsageError("The site URL must start with https:// (or http:// for local development).");
|
|
181
|
+
}
|
|
182
|
+
if (url.username || url.password)
|
|
183
|
+
throw new CliUsageError("The site URL must not contain a username or password.");
|
|
184
|
+
if (url.pathname === "/" || url.pathname === "")
|
|
185
|
+
url.pathname = WEBHOOK_PATH;
|
|
186
|
+
url.hash = "";
|
|
187
|
+
return url.toString();
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Replace values in an env file without touching unrelated lines. The first
|
|
191
|
+
* occurrence of a managed key is rewritten; duplicate managed keys are dropped
|
|
192
|
+
* so the file keeps a single source of truth.
|
|
193
|
+
*/
|
|
194
|
+
export function upsertEnv(contents, values) {
|
|
195
|
+
const eol = contents.includes("\r\n") ? "\r\n" : "\n";
|
|
196
|
+
const lines = contents.length
|
|
197
|
+
? contents.replace(/\r\n/g, "\n").replace(/\n+$/, "").split("\n")
|
|
198
|
+
: [];
|
|
199
|
+
const pending = new Map(Object.entries(values));
|
|
200
|
+
const handled = new Set();
|
|
201
|
+
const output = [];
|
|
202
|
+
for (const line of lines) {
|
|
203
|
+
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line);
|
|
204
|
+
const key = match?.[1];
|
|
205
|
+
if (key && pending.has(key)) {
|
|
206
|
+
output.push(`${key}=${pending.get(key)}`);
|
|
207
|
+
pending.delete(key);
|
|
208
|
+
handled.add(key);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (key && handled.has(key))
|
|
212
|
+
continue;
|
|
213
|
+
output.push(line);
|
|
214
|
+
}
|
|
215
|
+
for (const [key, value] of pending)
|
|
216
|
+
output.push(`${key}=${value}`);
|
|
217
|
+
return `${output.join(eol)}${eol}`;
|
|
218
|
+
}
|
|
219
|
+
function isNotFound(error) {
|
|
220
|
+
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
221
|
+
}
|
|
222
|
+
async function readEnvFile(path) {
|
|
223
|
+
try {
|
|
224
|
+
return await readFile(path, "utf8");
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
if (isNotFound(error))
|
|
228
|
+
return "";
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function writeEnvFile(path, contents) {
|
|
233
|
+
await mkdir(dirname(path), { recursive: true });
|
|
234
|
+
await writeFile(path, contents, { encoding: "utf8", mode: 0o600 });
|
|
235
|
+
try {
|
|
236
|
+
await chmod(path, 0o600);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// Windows does not use POSIX modes; the file still lives in the project.
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/* ------------------------------------------------------------------ */
|
|
243
|
+
/* Browser and API */
|
|
244
|
+
/* ------------------------------------------------------------------ */
|
|
245
|
+
export function openBrowser(url) {
|
|
246
|
+
const command = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
247
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
248
|
+
try {
|
|
249
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
250
|
+
child.on("error", () => undefined);
|
|
251
|
+
child.unref();
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function api(baseUrl, path, body) {
|
|
259
|
+
let response;
|
|
260
|
+
try {
|
|
261
|
+
response = await fetch(`${baseUrl.replace(/\/$/, "")}/api/cli/v1${path}`, {
|
|
262
|
+
method: body === undefined ? "GET" : "POST",
|
|
263
|
+
headers: {
|
|
264
|
+
accept: "application/json",
|
|
265
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
266
|
+
"user-agent": `glyphpay-cli/${VERSION}`,
|
|
267
|
+
},
|
|
268
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
269
|
+
signal: AbortSignal.timeout(20_000),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
throw new CliApiError(0, "network", "Could not reach Glyph Pay. Check your connection and try again.");
|
|
274
|
+
}
|
|
275
|
+
const payload = (await response.json().catch(() => null));
|
|
276
|
+
if (!response.ok || !payload?.ok) {
|
|
277
|
+
throw new CliApiError(response.status, payload && "error" in payload ? payload.error?.code ?? "request_failed" : "request_failed", (payload && "error" in payload ? payload.error?.message : null) ??
|
|
278
|
+
`Glyph Pay request failed (HTTP ${response.status}).`);
|
|
279
|
+
}
|
|
280
|
+
return payload.data;
|
|
281
|
+
}
|
|
282
|
+
async function waitForApproval(baseUrl, deviceCode, firstInterval, timeoutMs) {
|
|
283
|
+
const deadline = Date.now() + timeoutMs;
|
|
284
|
+
let interval = Math.max(1, firstInterval);
|
|
285
|
+
let lastNetworkError = null;
|
|
286
|
+
while (Date.now() < deadline) {
|
|
287
|
+
await sleep(interval * 1000);
|
|
288
|
+
try {
|
|
289
|
+
const result = await api(baseUrl, "/device/token", { deviceCode });
|
|
290
|
+
if (result.status === "approved")
|
|
291
|
+
return result;
|
|
292
|
+
interval = Math.max(1, result.interval || interval);
|
|
293
|
+
lastNetworkError = null;
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
if (error instanceof CliApiError && error.status === 429) {
|
|
297
|
+
interval = Math.min(interval + 2, 10);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (error instanceof CliApiError && (error.status === 404 || error.status === 410))
|
|
301
|
+
throw error;
|
|
302
|
+
lastNetworkError = error instanceof Error ? error.message : String(error);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
throw new CliApiError(0, "timeout", `Timed out waiting for approval${lastNetworkError ? ` (last error: ${lastNetworkError})` : ""}. ` +
|
|
306
|
+
"Run the command again for a fresh code.");
|
|
307
|
+
}
|
|
308
|
+
/* ------------------------------------------------------------------ */
|
|
309
|
+
/* Connect */
|
|
310
|
+
/* ------------------------------------------------------------------ */
|
|
311
|
+
export async function runConnect(options = {}) {
|
|
312
|
+
const baseUrl = options.baseUrl?.trim() || process.env.GLYPHPAY_API_URL?.trim() || DEFAULT_BASE_URL;
|
|
313
|
+
const timeoutSeconds = options.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
|
|
314
|
+
const callbackUrl = options.url ? toCallbackUrl(options.url) : null;
|
|
315
|
+
const created = await api(baseUrl, "/device", { callbackUrl });
|
|
316
|
+
say();
|
|
317
|
+
say(style(1, "Glyph Pay - connect a site"));
|
|
318
|
+
say();
|
|
319
|
+
say(`1. Open ${style(4, created.verificationUrl)}`);
|
|
320
|
+
say(`2. Sign in and approve pairing code ${style(1, created.userCode)}.`);
|
|
321
|
+
say(` The code expires at ${new Date(created.expiresAt).toLocaleTimeString()}.`);
|
|
322
|
+
if (options.noOpen) {
|
|
323
|
+
say(" (Browser opening skipped: --no-open.)");
|
|
324
|
+
}
|
|
325
|
+
else if (openBrowser(created.verificationUrl)) {
|
|
326
|
+
say(" A browser window should open.");
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
say(" Could not open a browser. Open the link above manually.");
|
|
330
|
+
}
|
|
331
|
+
say();
|
|
332
|
+
say("Waiting for approval...");
|
|
333
|
+
const approved = await waitForApproval(baseUrl, created.deviceCode, created.interval, timeoutSeconds * 1000);
|
|
334
|
+
const values = { [TOKEN_KEY]: approved.token };
|
|
335
|
+
if (approved.callbackSecret)
|
|
336
|
+
values[CALLBACK_SECRET_KEY] = approved.callbackSecret;
|
|
337
|
+
say();
|
|
338
|
+
say(style(1, `Connected ${approved.businessName} (${approved.environment} mode).`));
|
|
339
|
+
if (approved.callbackUrl)
|
|
340
|
+
say(`Webhook registered: ${approved.callbackUrl}`);
|
|
341
|
+
else
|
|
342
|
+
say("No webhook was registered. Add one later in Glyph Pay admin if your site needs payment events.");
|
|
343
|
+
let writtenTo = null;
|
|
344
|
+
if (options.print) {
|
|
345
|
+
say();
|
|
346
|
+
say("Add these to your server environment (never NEXT_PUBLIC_):");
|
|
347
|
+
for (const [key, value] of Object.entries(values))
|
|
348
|
+
say(`${key}=${value}`);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
const file = resolve(options.envFile ?? DEFAULT_ENV_FILE);
|
|
352
|
+
try {
|
|
353
|
+
const next = upsertEnv(await readEnvFile(file), values);
|
|
354
|
+
await writeEnvFile(file, next);
|
|
355
|
+
writtenTo = file;
|
|
356
|
+
const display = relative(process.cwd(), file) || file;
|
|
357
|
+
say(`Wrote ${Object.keys(values).join(" and ")} to ${display}.`);
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
say();
|
|
361
|
+
say(style(33, `Could not write ${file}: ${error instanceof Error ? error.message : String(error)}`));
|
|
362
|
+
say("Copy these values into your server environment now; they are shown only this once:");
|
|
363
|
+
for (const [key, value] of Object.entries(values))
|
|
364
|
+
say(`${key}=${value}`);
|
|
365
|
+
throw new CliApiError(1, "env_write_failed", "The credentials were printed above but not saved to a file.");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
say();
|
|
369
|
+
say("The token is shown only once. Run this command again to rotate it.");
|
|
370
|
+
return {
|
|
371
|
+
businessName: approved.businessName,
|
|
372
|
+
environment: approved.environment,
|
|
373
|
+
callbackUrl: approved.callbackUrl,
|
|
374
|
+
writtenTo,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/* ------------------------------------------------------------------ */
|
|
378
|
+
/* Entry point */
|
|
379
|
+
/* ------------------------------------------------------------------ */
|
|
380
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
381
|
+
try {
|
|
382
|
+
const parsed = parseArgs(argv);
|
|
383
|
+
if (parsed.command === "help") {
|
|
384
|
+
say(helpText());
|
|
385
|
+
return 0;
|
|
386
|
+
}
|
|
387
|
+
if (parsed.command === "version") {
|
|
388
|
+
say(VERSION);
|
|
389
|
+
return 0;
|
|
390
|
+
}
|
|
391
|
+
await runConnect(parsed.options);
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
if (error instanceof CliUsageError) {
|
|
396
|
+
say(style(31, error.message));
|
|
397
|
+
say("Run `glyphpay --help` for usage.");
|
|
398
|
+
return 2;
|
|
399
|
+
}
|
|
400
|
+
say();
|
|
401
|
+
say(style(31, error instanceof Error ? error.message : String(error)));
|
|
402
|
+
return 1;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
406
|
+
void main().then((code) => {
|
|
407
|
+
process.exitCode = code;
|
|
408
|
+
});
|
|
409
|
+
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glyphpay",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Drop-in GlyphPay checkout for websites
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Drop-in GlyphPay checkout for websites, plus `glyphpay connect` for browser-based setup.",
|
|
5
5
|
"homepage": "https://glyphpay.net",
|
|
6
|
-
"keywords": [
|
|
6
|
+
"keywords": [
|
|
7
|
+
"glyphpay",
|
|
8
|
+
"payments",
|
|
9
|
+
"checkout",
|
|
10
|
+
"stripe",
|
|
11
|
+
"react",
|
|
12
|
+
"nextjs"
|
|
13
|
+
],
|
|
7
14
|
"license": "UNLICENSED",
|
|
8
15
|
"type": "module",
|
|
9
|
-
"
|
|
16
|
+
"bin": {
|
|
17
|
+
"glyphpay": "./dist/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
10
25
|
"exports": {
|
|
11
|
-
"./server": {
|
|
12
|
-
|
|
13
|
-
|
|
26
|
+
"./server": {
|
|
27
|
+
"types": "./dist/server.d.ts",
|
|
28
|
+
"default": "./dist/server.js"
|
|
29
|
+
},
|
|
30
|
+
"./browser": {
|
|
31
|
+
"types": "./dist/browser.d.ts",
|
|
32
|
+
"default": "./dist/browser.js"
|
|
33
|
+
},
|
|
34
|
+
"./react": {
|
|
35
|
+
"types": "./dist/react.d.ts",
|
|
36
|
+
"default": "./dist/react.js"
|
|
37
|
+
}
|
|
14
38
|
},
|
|
15
39
|
"scripts": {
|
|
16
40
|
"build": "tsc",
|
|
@@ -24,9 +48,12 @@
|
|
|
24
48
|
"react": ">=18"
|
|
25
49
|
},
|
|
26
50
|
"peerDependenciesMeta": {
|
|
27
|
-
"react": {
|
|
51
|
+
"react": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
28
54
|
},
|
|
29
55
|
"devDependencies": {
|
|
56
|
+
"@types/node": "^22.20.5",
|
|
30
57
|
"@types/react": "^19",
|
|
31
58
|
"typescript": "^5"
|
|
32
59
|
}
|