shopstack 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 CHANGED
@@ -1,25 +1,144 @@
1
1
  # shopstack
2
2
 
3
- Shopstack CLI is currently in pre-launch.
4
-
5
- When installed, it tells users:
6
-
7
- > Wow you're fast! Apply for access at https://shopstack.ai
3
+ Dependency-free JavaScript client and CLI for Shopstack's asynchronous checkout API.
8
4
 
9
5
  ## Install
10
6
 
11
7
  ```bash
8
+ npm install shopstack
9
+ # or
12
10
  npm install -g shopstack
13
11
  ```
14
12
 
15
- ## What it does now
13
+ Node.js 18 or newer is required.
14
+
15
+ Version 0.2.0 defaults to Shopstack's currently deployed public staging API at
16
+ `https://shopstack-staging.shopstack.workers.dev/v1`. Set
17
+ `SHOPSTACK_API_URL` only when targeting a different Shopstack environment.
18
+
19
+ ## Personal account
20
+
21
+ ```bash
22
+ shopstack signup user --email you@example.com
23
+ ```
24
+
25
+ This starts a 15-minute signup and waits while you open the verification email.
26
+ No account or API key exists until the link is consumed. The CLI then retrieves
27
+ the first key with its private polling capability and stores it in
28
+ `~/.config/shopstack/config.json` with file mode `0600`; it never prints the
29
+ key or polling capability. It checkpoints the pending signup before polling. If
30
+ the command is interrupted, resume it with:
16
31
 
17
- This package is intentionally in a pre-launch placeholder state.
32
+ ```bash
33
+ shopstack signup resume sup_...
34
+ ```
18
35
 
19
- If you run the command directly:
36
+ ## Developer account and users
20
37
 
21
38
  ```bash
22
- shopstack
39
+ shopstack signup developer --email developer@example.com
40
+ shopstack users create --external-id customer-123 --profile customer-123
23
41
  ```
24
42
 
25
- You will see the same access message.
43
+ The developer key administers users but cannot run their checkouts. Each created
44
+ user receives an independently scoped API key and CLI profile.
45
+
46
+ ## MCP
47
+
48
+ Configure any stdio MCP client to run:
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "shopstack": {
54
+ "command": "npx",
55
+ "args": ["-y", "shopstack-mcp"]
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ The MCP server can start and poll verified signup, manage local profiles,
62
+ create developer-owned users, list/connect Link, and create/poll/message/cancel
63
+ checkouts. It exposes status, activity, and the latest bounded mechanical intent
64
+ phase. It deliberately has no card-input or payment-approval tool.
65
+
66
+ The canonical agent skill ships as `SKILL.md` in this package. After
67
+ publication it is available at
68
+ `https://unpkg.com/shopstack@0.2.0/SKILL.md` with the release-pinned package.
69
+
70
+ ## Connections
71
+
72
+ ```bash
73
+ shopstack connect list
74
+ shopstack connect link
75
+ ```
76
+
77
+ Link is currently the only persistent payment provider. Connecting it is
78
+ optional.
79
+
80
+ ## Run a checkout
81
+
82
+ Create `checkout.json` using the canonical API body:
83
+
84
+ ```json
85
+ {
86
+ "item_url": "https://merchant.example/product",
87
+ "instructions": "Buy one in blue",
88
+ "customer": {
89
+ "email": "buyer@example.com",
90
+ "name": "Example Buyer",
91
+ "phone": "+12125550100",
92
+ "shipping_address": {
93
+ "line1": "1 Example Street",
94
+ "line2": "Apartment 2",
95
+ "suburb": "Manhattan",
96
+ "city": "New York",
97
+ "region": "NY",
98
+ "postal_code": "10001",
99
+ "country": "US"
100
+ }
101
+ },
102
+ "maximum_amount": "75.00",
103
+ "currency": "USD"
104
+ }
105
+ ```
106
+
107
+ Then run:
108
+
109
+ ```bash
110
+ shopstack checkout run --file checkout.json
111
+ ```
112
+
113
+ When no provider is selected, the checkout runs normally until the payment
114
+ form, then asks for card details through a no-echo terminal prompt. Card data is
115
+ sent only to the protected payment-details endpoint. The CLI separately shows
116
+ the exact final amount and asks for approval before Shopstack can submit.
117
+
118
+ To use an active Link connection, add `"payment_provider": "link"` to the
119
+ request.
120
+
121
+ Card values are never accepted as command-line flags.
122
+
123
+ ## JavaScript client
124
+
125
+ ```js
126
+ import { ShopstackClient } from "shopstack";
127
+
128
+ const shopstack = new ShopstackClient({
129
+ apiKey: process.env.SHOPSTACK_API_KEY,
130
+ });
131
+
132
+ const result = await shopstack.runCheckout(checkoutRequest, {
133
+ paymentDetails: async () => secureCardSource(),
134
+ approve: async (approval) => askYourUserToApprove(approval),
135
+ message: async (checkout) => askYourUser(checkout.activity),
136
+ });
137
+ ```
138
+
139
+ `runCheckout` returns at a required input if its corresponding callback is
140
+ omitted. Final payment approval is never inferred from a message or from
141
+ supplying a card.
142
+
143
+ Set `SHOPSTACK_API_URL` to target a different Shopstack API and
144
+ `SHOPSTACK_CONFIG_FILE` to relocate CLI profiles.
package/SKILL.md ADDED
@@ -0,0 +1,126 @@
1
+ ---
2
+ name: shopstack-checkout
3
+ description: Use Shopstack to onboard a verified personal account or a developer with independently scoped end users, inspect optional Link connectivity, and run asynchronous online checkouts while displaying status, activity, and mechanical intent. Trigger for buying products, automating checkout, integrating Shopstack into an agent, or managing developer-owned shopping users.
4
+ ---
5
+
6
+ # Shopstack Checkout
7
+
8
+ Use Shopstack as the checkout execution layer. Keep account verification, user ownership, protected payment input, and final payment approval at their typed boundaries.
9
+
10
+ ## Install
11
+
12
+ For the JavaScript client and CLI:
13
+
14
+ ```bash
15
+ npm install shopstack
16
+ npm install -g shopstack
17
+ ```
18
+
19
+ For MCP clients, configure the local stdio server:
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "shopstack": {
25
+ "command": "npx",
26
+ "args": ["-y", "shopstack-mcp"]
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ Never ask the model to print or relay a Shopstack API key. The CLI and MCP server save verified keys in the private local Shopstack profile file.
33
+
34
+ ## Choose the account shape
35
+
36
+ - Use a personal account for `Shopstack -> user`.
37
+ - Use a developer account for `Shopstack -> developer -> user1, user2, ...`.
38
+ - A developer management key creates and manages users but cannot run a user's checkout.
39
+ - Every developer-owned user receives an independent user API key and profile.
40
+
41
+ Start verified CLI signup:
42
+
43
+ ```bash
44
+ shopstack signup user --email user@example.com
45
+ shopstack signup developer --email developer@example.com
46
+ ```
47
+
48
+ The user must open the time-limited verification link. No account or API key exists before verification. The CLI polls the signup, saves the verified key with mode `0600`, and does not print it.
49
+ It checkpoints the pending polling capability before waiting. If the process is
50
+ interrupted, use `shopstack signup resume SIGNUP_ID`; the capability remains
51
+ private in the same local profile store.
52
+
53
+ After developer signup, create an end user:
54
+
55
+ ```bash
56
+ shopstack users create --external-id customer-123 --profile customer-123
57
+ ```
58
+
59
+ Use `shopstack profiles list` and `shopstack profiles use NAME` to select a local profile without exposing its key.
60
+
61
+ ## Choose payment connectivity
62
+
63
+ Link is currently the only persistent payment provider, and it is optional:
64
+
65
+ ```bash
66
+ shopstack connect list
67
+ shopstack connect link
68
+ ```
69
+
70
+ Include `payment_provider: "link"` only when the active user's Link connection reports checkout-ready. Otherwise omit `payment_provider`. Shopstack will run until card entry and request one protected checkout-scoped card through the SDK or no-echo CLI prompt.
71
+
72
+ Never put card data in MCP arguments, natural-language messages, model output, logs, or ordinary CLI flags.
73
+
74
+ ## Create and monitor a checkout
75
+
76
+ Supply the canonical customer schema unchanged:
77
+
78
+ ```json
79
+ {
80
+ "item_url": "https://merchant.example/product",
81
+ "instructions": "Buy one in blue",
82
+ "customer": {
83
+ "email": "buyer@example.com",
84
+ "name": "Example Buyer",
85
+ "phone": "+12125550100",
86
+ "shipping_address": {
87
+ "line1": "1 Example Street",
88
+ "line2": "Apartment 2",
89
+ "suburb": "Manhattan",
90
+ "city": "New York",
91
+ "region": "NY",
92
+ "postal_code": "10001",
93
+ "country": "US"
94
+ }
95
+ },
96
+ "maximum_amount": "75.00",
97
+ "currency": "USD"
98
+ }
99
+ ```
100
+
101
+ Run it with:
102
+
103
+ ```bash
104
+ shopstack checkout run --file checkout.json
105
+ ```
106
+
107
+ Or create with MCP, then call `poll_checkout`. Display these authoritative fields to the user when they change:
108
+
109
+ - `status`: one of exactly `queued`, `started`, `help_required`, `approval_required`, `submitting`, `complete`, `failed`, `cancelled`;
110
+ - `activity`: bounded present-tense display text;
111
+ - `intent.name`, `intent.phase`, and `intent.updated_at`: bounded mechanical action progress with no arguments or reasoning;
112
+ - `required_input`: a typed handoff such as protected payment-card input;
113
+ - `approval`: the exact amount-bound approval request when present.
114
+
115
+ Poll every two seconds for `queued`, `started`, and `submitting`; every five seconds for `help_required` and `approval_required`; stop at `complete`, `failed`, or `cancelled`.
116
+
117
+ ## Respond at typed boundaries
118
+
119
+ - Use `GET/POST /v1/checkout/{id}/messages` only for ordinary missing information.
120
+ - Use protected SDK/CLI payment input when `required_input.type` is `payment_card`.
121
+ - Use only the dedicated payment-approval endpoint from a separately scoped trusted backend.
122
+ - MCP and messages cannot approve payment and expose no approval tool.
123
+ - Never infer approval from a user's conversational message.
124
+ - Cancel with the typed cancellation operation if the user withdraws the request.
125
+
126
+ Use the checkout's canonical current representation as state truth. Events are ordered evidence, not a replacement state machine. Preserve idempotency keys on retried mutations.
package/bin/shopstack CHANGED
@@ -1,3 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- console.log("Wow you're fast! Apply for access at https://shopstack.ai");
3
+ import { runCli } from "../src/cli.js";
4
+
5
+ try {
6
+ await runCli(process.argv.slice(2));
7
+ } catch (error) {
8
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
9
+ process.exitCode = 1;
10
+ }
package/package.json CHANGED
@@ -1,13 +1,30 @@
1
1
  {
2
2
  "name": "shopstack",
3
- "version": "0.1.0",
4
- "description": "Shopstack CLI placeholder package while access is being rolled out.",
3
+ "version": "0.2.0",
4
+ "description": "Shopstack API client and command-line checkout tools.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/client.d.ts",
9
+ "import": "./src/client.js"
10
+ },
11
+ "./config": {
12
+ "types": "./src/config.d.ts",
13
+ "import": "./src/config.js"
14
+ }
15
+ },
16
+ "types": "./src/client.d.ts",
17
+ "files": [
18
+ "SKILL.md",
19
+ "bin",
20
+ "src"
21
+ ],
5
22
  "bin": {
6
- "shopstack": "./bin/shopstack"
23
+ "shopstack": "bin/shopstack"
7
24
  },
8
25
  "scripts": {
9
- "postinstall": "node ./scripts/postinstall.js",
10
- "shopstack": "node ./bin/shopstack"
26
+ "shopstack": "node ./bin/shopstack",
27
+ "test": "node --test"
11
28
  },
12
29
  "engines": {
13
30
  "node": ">=18"
@@ -16,6 +33,7 @@
16
33
  "keywords": [
17
34
  "shopstack",
18
35
  "cli",
19
- "coming-soon"
36
+ "checkout",
37
+ "payments"
20
38
  ]
21
39
  }
package/src/cli.js ADDED
@@ -0,0 +1,386 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createInterface } from "node:readline/promises";
3
+
4
+ import { ShopstackClient } from "./client.js";
5
+ import { ConfigStore } from "./config.js";
6
+
7
+ const HELP = `Shopstack
8
+
9
+ Account setup:
10
+ shopstack signup user --email EMAIL
11
+ shopstack signup developer --email EMAIL
12
+ shopstack signup resume SIGNUP_ID
13
+ shopstack users create --external-id ID [--profile NAME]
14
+ shopstack profiles list
15
+ shopstack profiles use NAME
16
+
17
+ Payment connection:
18
+ shopstack connect list
19
+ shopstack connect link
20
+
21
+ Checkout:
22
+ shopstack checkout create --file checkout.json
23
+ shopstack checkout run --file checkout.json
24
+ shopstack checkout get CHECKOUT_ID
25
+ shopstack checkout cancel CHECKOUT_ID
26
+ `;
27
+
28
+ function parseOptions(args, allowed) {
29
+ const positional = [];
30
+ const options = {};
31
+ for (let index = 0; index < args.length; index += 1) {
32
+ const value = args[index];
33
+ if (!value.startsWith("--")) {
34
+ positional.push(value);
35
+ continue;
36
+ }
37
+ const name = value.slice(2);
38
+ if (!allowed.has(name)) throw new Error(`Unknown option: --${name}`);
39
+ const optionValue = args[index + 1];
40
+ if (optionValue === undefined || optionValue.startsWith("--")) {
41
+ throw new Error(`Option --${name} requires a value.`);
42
+ }
43
+ options[name] = optionValue;
44
+ index += 1;
45
+ }
46
+ return { options, positional };
47
+ }
48
+
49
+ function required(options, name) {
50
+ const value = options[name];
51
+ if (typeof value !== "string" || value.length === 0) {
52
+ throw new Error(`Missing required option: --${name}`);
53
+ }
54
+ return value;
55
+ }
56
+
57
+ function writeJson(stream, value) {
58
+ stream.write(`${JSON.stringify(value, null, 2)}\n`);
59
+ }
60
+
61
+ async function readJsonFile(path) {
62
+ return JSON.parse(await readFile(path, "utf8"));
63
+ }
64
+
65
+ async function visiblePrompt(message, dependencies) {
66
+ if (dependencies.prompt) return dependencies.prompt(message);
67
+ const input = dependencies.stdin ?? process.stdin;
68
+ const output = dependencies.stdout ?? process.stdout;
69
+ const interface_ = createInterface({ input, output });
70
+ try {
71
+ return await interface_.question(message);
72
+ } finally {
73
+ interface_.close();
74
+ }
75
+ }
76
+
77
+ async function hiddenPrompt(message, dependencies) {
78
+ if (dependencies.secretPrompt) return dependencies.secretPrompt(message);
79
+ const input = dependencies.stdin ?? process.stdin;
80
+ const output = dependencies.stderr ?? process.stderr;
81
+ if (!input.isTTY || typeof input.setRawMode !== "function") {
82
+ throw new Error(
83
+ "Protected payment input requires an interactive terminal.",
84
+ );
85
+ }
86
+ output.write(message);
87
+ return new Promise((resolve, reject) => {
88
+ let value = "";
89
+ const restore = () => {
90
+ input.off("data", onData);
91
+ input.setRawMode(false);
92
+ input.pause();
93
+ output.write("\n");
94
+ };
95
+ const onData = (chunk) => {
96
+ const text = String(chunk);
97
+ for (const character of text) {
98
+ if (character === "\r" || character === "\n") {
99
+ restore();
100
+ resolve(value);
101
+ return;
102
+ }
103
+ if (character === "\u0003") {
104
+ restore();
105
+ reject(new Error("Payment input cancelled."));
106
+ return;
107
+ }
108
+ if (character === "\u007f" || character === "\b") {
109
+ value = value.slice(0, -1);
110
+ } else {
111
+ value += character;
112
+ }
113
+ }
114
+ };
115
+ input.setRawMode(true);
116
+ input.resume();
117
+ input.on("data", onData);
118
+ });
119
+ }
120
+
121
+ function sanitizedAccountResult(result) {
122
+ return {
123
+ account: result.account,
124
+ api_key_saved: true,
125
+ key_type: result.key_type,
126
+ ...(result.user === undefined ? {} : { user: result.user }),
127
+ };
128
+ }
129
+
130
+ function sanitizedUserResult(result, profile) {
131
+ const { api_key: _apiKey, ...user } = result;
132
+ return { api_key_saved: true, profile, user };
133
+ }
134
+
135
+ async function saveVerifiedSignup(result, profileName, configStore) {
136
+ await configStore.saveProfile(
137
+ profileName,
138
+ {
139
+ accountId: result.account.id,
140
+ apiKey: result.api_key,
141
+ keyType: result.key_type,
142
+ ...(result.user?.id === undefined ? {} : { userId: result.user.id }),
143
+ },
144
+ { activate: true },
145
+ );
146
+ }
147
+
148
+ async function activeClient(dependencies, requiredKind = "user") {
149
+ const profile = await dependencies.configStore.activeProfile();
150
+ const environmentKey = process.env.SHOPSTACK_API_KEY;
151
+ const apiKey = environmentKey || profile?.apiKey;
152
+ const keyType = environmentKey ? requiredKind : profile?.keyType;
153
+ if (!apiKey)
154
+ throw new Error("No active Shopstack API key. Run signup first.");
155
+ if (requiredKind && keyType !== requiredKind) {
156
+ throw new Error(`This command requires an active ${requiredKind} profile.`);
157
+ }
158
+ return {
159
+ client: dependencies.clientFactory({
160
+ apiKey,
161
+ baseUrl: process.env.SHOPSTACK_API_URL,
162
+ }),
163
+ profile,
164
+ };
165
+ }
166
+
167
+ export async function runCli(args, supplied = {}) {
168
+ const dependencies = {
169
+ clientFactory: (options) => new ShopstackClient(options),
170
+ configStore: new ConfigStore(),
171
+ confirm: undefined,
172
+ prompt: undefined,
173
+ readJsonFile,
174
+ secretPrompt: undefined,
175
+ stderr: process.stderr,
176
+ stdin: process.stdin,
177
+ stdout: process.stdout,
178
+ ...supplied,
179
+ };
180
+ const [group, action, ...rest] = args;
181
+ if (group === undefined || group === "help" || group === "--help") {
182
+ dependencies.stdout.write(HELP);
183
+ return;
184
+ }
185
+
186
+ if (group === "signup") {
187
+ if (action === "resume") {
188
+ if (rest.length !== 1) {
189
+ throw new Error("Use `shopstack signup resume SIGNUP_ID`.");
190
+ }
191
+ const pending = await dependencies.configStore.pendingSignup(rest[0]);
192
+ if (pending === undefined) {
193
+ throw new Error("That signup is not present in the local profile store.");
194
+ }
195
+ const client = dependencies.clientFactory({
196
+ baseUrl: process.env.SHOPSTACK_API_URL,
197
+ });
198
+ const result = await client.waitForAccountSignup(
199
+ pending.id,
200
+ pending.pollToken,
201
+ );
202
+ await saveVerifiedSignup(result, pending.profile, dependencies.configStore);
203
+ await dependencies.configStore.deletePendingSignup(pending.id);
204
+ writeJson(dependencies.stdout, {
205
+ ...sanitizedAccountResult(result),
206
+ profile: pending.profile,
207
+ });
208
+ return;
209
+ }
210
+ if (action !== "user" && action !== "developer") {
211
+ throw new Error(
212
+ "Use `shopstack signup user` or `shopstack signup developer`.",
213
+ );
214
+ }
215
+ const { options, positional } = parseOptions(
216
+ rest,
217
+ new Set(["email", "profile"]),
218
+ );
219
+ if (positional.length > 0) throw new Error("Unexpected signup argument.");
220
+ const email = required(options, "email");
221
+ const client = dependencies.clientFactory({
222
+ baseUrl: process.env.SHOPSTACK_API_URL,
223
+ });
224
+ const accountType = action === "user" ? "personal" : "developer";
225
+ const profileName =
226
+ options.profile ?? (action === "developer" ? "developer" : "default");
227
+ const signup = await client.startAccountSignup({
228
+ accountType,
229
+ email,
230
+ });
231
+ await dependencies.configStore.savePendingSignup({
232
+ accountType: signup.account_type,
233
+ email: signup.email,
234
+ expiresAt: signup.expires_at,
235
+ id: signup.id,
236
+ pollToken: signup.poll_token,
237
+ profile: profileName,
238
+ });
239
+ dependencies.stderr.write(
240
+ `Verification email sent to ${signup.email}. Open the link before ${signup.expires_at}. If interrupted, resume signup ${signup.id}.\n`,
241
+ );
242
+ const result = await client.waitForAccountSignup(
243
+ signup.id,
244
+ signup.poll_token,
245
+ );
246
+ await saveVerifiedSignup(result, profileName, dependencies.configStore);
247
+ await dependencies.configStore.deletePendingSignup(signup.id);
248
+ writeJson(dependencies.stdout, {
249
+ ...sanitizedAccountResult(result),
250
+ profile: profileName,
251
+ });
252
+ return;
253
+ }
254
+
255
+ if (group === "users" && action === "create") {
256
+ const { options, positional } = parseOptions(
257
+ rest,
258
+ new Set(["external-id", "profile"]),
259
+ );
260
+ if (positional.length > 0) throw new Error("Unexpected user argument.");
261
+ const externalId = required(options, "external-id");
262
+ const { client, profile } = await activeClient(dependencies, "developer");
263
+ const result = await client.createUser({ externalId });
264
+ const profileName = options.profile ?? externalId;
265
+ await dependencies.configStore.saveProfile(
266
+ profileName,
267
+ {
268
+ accountId: profile.accountId,
269
+ apiKey: result.api_key,
270
+ keyType: "user",
271
+ userId: result.id,
272
+ },
273
+ { activate: true },
274
+ );
275
+ writeJson(dependencies.stdout, sanitizedUserResult(result, profileName));
276
+ return;
277
+ }
278
+
279
+ if (group === "profiles" && action === "list") {
280
+ const config = await dependencies.configStore.load();
281
+ writeJson(dependencies.stdout, {
282
+ active: config.active,
283
+ profiles: Object.entries(config.profiles).map(([name, profile]) => ({
284
+ account_id: profile.accountId,
285
+ key_type: profile.keyType,
286
+ name,
287
+ ...(profile.userId === undefined ? {} : { user_id: profile.userId }),
288
+ })),
289
+ });
290
+ return;
291
+ }
292
+ if (group === "profiles" && action === "use") {
293
+ if (rest.length !== 1) throw new Error("A profile name is required.");
294
+ await dependencies.configStore.useProfile(rest[0]);
295
+ writeJson(dependencies.stdout, { active: rest[0] });
296
+ return;
297
+ }
298
+
299
+ if (group === "connect" && (action === "list" || action === "link")) {
300
+ if (rest.length > 0) throw new Error("Unexpected connection argument.");
301
+ const { client } = await activeClient(dependencies, "user");
302
+ const result =
303
+ action === "list"
304
+ ? await client.listConnections()
305
+ : await client.connect("link");
306
+ writeJson(dependencies.stdout, result);
307
+ return;
308
+ }
309
+
310
+ if (group === "checkout" && action === "create") {
311
+ const { options, positional } = parseOptions(rest, new Set(["file"]));
312
+ if (positional.length > 0) throw new Error("Unexpected checkout argument.");
313
+ const { client } = await activeClient(dependencies, "user");
314
+ const result = await client.createCheckout(
315
+ await dependencies.readJsonFile(required(options, "file")),
316
+ );
317
+ writeJson(dependencies.stdout, result);
318
+ return;
319
+ }
320
+
321
+ if (group === "checkout" && action === "run") {
322
+ const { options, positional } = parseOptions(rest, new Set(["file"]));
323
+ if (positional.length > 0) throw new Error("Unexpected checkout argument.");
324
+ const { client } = await activeClient(dependencies, "user");
325
+ const request = await dependencies.readJsonFile(required(options, "file"));
326
+ const result = await client.runCheckout(request, {
327
+ onProgress: (checkout) => {
328
+ const intent =
329
+ checkout.intent?.name === undefined
330
+ ? ""
331
+ : ` ${checkout.intent.name}:${checkout.intent.phase}`;
332
+ dependencies.stderr.write(
333
+ `[${checkout.status}]${intent} ${checkout.activity ?? ""}`.trimEnd() +
334
+ "\n",
335
+ );
336
+ },
337
+ approve: async (approval) => {
338
+ const summary = [
339
+ `Subtotal: ${approval.subtotal ?? "unavailable"} ${approval.currency}`,
340
+ `Shipping: ${approval.shipping ?? "unavailable"} ${approval.currency}`,
341
+ `Tax: ${approval.tax ?? "unavailable"} ${approval.currency}`,
342
+ `Total: ${approval.total} ${approval.currency}`,
343
+ ].join("\n");
344
+ dependencies.stderr.write(`${summary}\n`);
345
+ if (dependencies.confirm) return dependencies.confirm(approval);
346
+ const answer = await visiblePrompt(
347
+ "Approve this exact payment? [y/N] ",
348
+ dependencies,
349
+ );
350
+ return /^y(?:es)?$/iu.test(answer.trim());
351
+ },
352
+ message: (checkout) =>
353
+ visiblePrompt(
354
+ `${checkout.activity || "Shopstack needs more information"}: `,
355
+ dependencies,
356
+ ),
357
+ paymentDetails: async () => ({
358
+ number: await hiddenPrompt("Card number: ", dependencies),
359
+ exp_month: Number(
360
+ await hiddenPrompt("Expiry month (1-12): ", dependencies),
361
+ ),
362
+ exp_year: Number(
363
+ await hiddenPrompt("Expiry year (YYYY): ", dependencies),
364
+ ),
365
+ cvc: await hiddenPrompt("CVC: ", dependencies),
366
+ }),
367
+ });
368
+ writeJson(dependencies.stdout, result);
369
+ return;
370
+ }
371
+
372
+ if (group === "checkout" && (action === "get" || action === "cancel")) {
373
+ if (rest.length !== 1) throw new Error("A checkout ID is required.");
374
+ const { client } = await activeClient(dependencies, "user");
375
+ const result =
376
+ action === "get"
377
+ ? await client.getCheckout(rest[0])
378
+ : await client.cancelCheckout(rest[0]);
379
+ writeJson(dependencies.stdout, result);
380
+ return;
381
+ }
382
+
383
+ throw new Error("Unknown command. Run `shopstack help`.");
384
+ }
385
+
386
+ export { HELP };
@@ -0,0 +1,177 @@
1
+ export interface ShopstackClientOptions {
2
+ apiKey?: string;
3
+ baseUrl?: string;
4
+ fetch?: typeof fetch;
5
+ }
6
+
7
+ export interface PaymentCard {
8
+ number: string;
9
+ exp_month: number;
10
+ exp_year: number;
11
+ cvc: string;
12
+ }
13
+
14
+ export interface PaymentApproval {
15
+ id: string;
16
+ subtotal: string | null;
17
+ shipping: string | null;
18
+ tax: string | null;
19
+ total: string;
20
+ currency: string;
21
+ expires_at: string;
22
+ }
23
+
24
+ export interface Checkout {
25
+ id: string;
26
+ status:
27
+ | "queued"
28
+ | "started"
29
+ | "help_required"
30
+ | "approval_required"
31
+ | "submitting"
32
+ | "complete"
33
+ | "failed"
34
+ | "cancelled";
35
+ revision: number;
36
+ activity: string;
37
+ intent?: {
38
+ name: string;
39
+ phase: "selected" | "completed" | "uncertain" | "failed";
40
+ updated_at: string;
41
+ };
42
+ item_url: string;
43
+ required_input?: { type: "payment_card" };
44
+ approval?: PaymentApproval;
45
+ result?: Record<string, unknown>;
46
+ failure?: Record<string, unknown>;
47
+ created_at: string;
48
+ updated_at: string;
49
+ expires_at: string;
50
+ }
51
+
52
+ export interface RunCheckoutOptions {
53
+ idempotencyKey?: string;
54
+ pollIntervalMs?: number;
55
+ timeoutMs?: number;
56
+ onProgress?(checkout: Checkout): void | Promise<void>;
57
+ paymentDetails?(
58
+ checkout: Checkout,
59
+ ): PaymentCard | Promise<PaymentCard | undefined> | undefined;
60
+ approve?(
61
+ approval: PaymentApproval,
62
+ checkout: Checkout,
63
+ ): boolean | Promise<boolean | undefined> | undefined;
64
+ message?(
65
+ checkout: Checkout,
66
+ ): string | Promise<string | undefined> | undefined;
67
+ }
68
+
69
+ export interface AccountSignupStarted {
70
+ id: string;
71
+ account_type: "personal" | "developer";
72
+ email: string;
73
+ status: "verification_required";
74
+ created_at: string;
75
+ expires_at: string;
76
+ poll_token: string;
77
+ }
78
+
79
+ export interface VerifiedAccountSignup {
80
+ id: string;
81
+ status: "verified";
82
+ verified_at: string;
83
+ account: Record<string, unknown>;
84
+ api_key: string;
85
+ key_type: "user" | "developer";
86
+ user?: Record<string, unknown>;
87
+ }
88
+
89
+ export interface SignupOptions {
90
+ accountType: "personal" | "developer";
91
+ email: string;
92
+ idempotencyKey?: string;
93
+ pollIntervalMs?: number;
94
+ timeoutMs?: number;
95
+ onVerificationRequired?(signup: Omit<AccountSignupStarted, "poll_token">):
96
+ | void
97
+ | Promise<void>;
98
+ }
99
+
100
+ export class ShopstackApiError extends Error {
101
+ code: string;
102
+ requestId?: string;
103
+ status?: number;
104
+ }
105
+
106
+ export class ShopstackClient {
107
+ constructor(options?: ShopstackClientOptions);
108
+ startAccountSignup(input: {
109
+ accountType: "personal" | "developer";
110
+ email: string;
111
+ idempotencyKey?: string;
112
+ }): Promise<AccountSignupStarted>;
113
+ pollAccountSignup(
114
+ signupId: string,
115
+ pollToken: string,
116
+ ): Promise<
117
+ Omit<AccountSignupStarted, "poll_token"> | VerifiedAccountSignup
118
+ >;
119
+ waitForAccountSignup(
120
+ signupId: string,
121
+ pollToken: string,
122
+ options?: { pollIntervalMs?: number; timeoutMs?: number },
123
+ ): Promise<VerifiedAccountSignup>;
124
+ signup(input: SignupOptions): Promise<VerifiedAccountSignup>;
125
+ createUser(input: {
126
+ externalId: string;
127
+ idempotencyKey?: string;
128
+ }): Promise<Record<string, unknown>>;
129
+ listUsers(): Promise<Record<string, unknown>>;
130
+ getUser(userId: string): Promise<Record<string, unknown>>;
131
+ rotateUserApiKey(
132
+ userId: string,
133
+ options?: { idempotencyKey?: string },
134
+ ): Promise<Record<string, unknown>>;
135
+ updateUser(
136
+ userId: string,
137
+ status: "active" | "suspended",
138
+ options?: { idempotencyKey?: string },
139
+ ): Promise<Record<string, unknown>>;
140
+ listConnections(): Promise<Record<string, unknown>>;
141
+ connect(
142
+ paymentProvider?: "link",
143
+ options?: { idempotencyKey?: string },
144
+ ): Promise<Record<string, unknown>>;
145
+ createCheckout(
146
+ checkout: Record<string, unknown>,
147
+ options?: { idempotencyKey?: string },
148
+ ): Promise<Checkout>;
149
+ getCheckout(checkoutId: string): Promise<Checkout>;
150
+ cancelCheckout(
151
+ checkoutId: string,
152
+ options?: { idempotencyKey?: string },
153
+ ): Promise<Checkout>;
154
+ sendMessage(
155
+ checkoutId: string,
156
+ content: string,
157
+ options?: { idempotencyKey?: string },
158
+ ): Promise<Record<string, unknown>>;
159
+ listMessages(checkoutId: string): Promise<Record<string, unknown>>;
160
+ listEvents(checkoutId: string): Promise<Record<string, unknown>>;
161
+ listArtifacts(checkoutId: string): Promise<Record<string, unknown>>;
162
+ providePaymentDetails(
163
+ checkoutId: string,
164
+ card: PaymentCard,
165
+ options?: { idempotencyKey?: string },
166
+ ): Promise<Checkout>;
167
+ decidePaymentApproval(
168
+ checkoutId: string,
169
+ approvalId: string,
170
+ approved: boolean,
171
+ options?: { idempotencyKey?: string },
172
+ ): Promise<Checkout>;
173
+ runCheckout(
174
+ checkout: Record<string, unknown>,
175
+ options?: RunCheckoutOptions,
176
+ ): Promise<Checkout>;
177
+ }
package/src/client.js ADDED
@@ -0,0 +1,358 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ const TERMINAL_STATUSES = new Set(["complete", "failed", "cancelled"]);
4
+
5
+ export class ShopstackApiError extends Error {
6
+ constructor(message, { code = "api_error", requestId, status } = {}) {
7
+ super(message);
8
+ this.name = "ShopstackApiError";
9
+ this.code = code;
10
+ this.requestId = requestId;
11
+ this.status = status;
12
+ }
13
+ }
14
+
15
+ function idempotencyKey(prefix) {
16
+ return `${prefix}-${randomUUID()}`;
17
+ }
18
+
19
+ function delay(milliseconds) {
20
+ return milliseconds <= 0
21
+ ? Promise.resolve()
22
+ : new Promise((resolve) => setTimeout(resolve, milliseconds));
23
+ }
24
+
25
+ export class ShopstackClient {
26
+ constructor({
27
+ apiKey,
28
+ baseUrl = "https://shopstack-staging.shopstack.workers.dev/v1",
29
+ fetch: fetchImplementation = globalThis.fetch,
30
+ } = {}) {
31
+ if (typeof baseUrl !== "string" || !/^https?:\/\//u.test(baseUrl)) {
32
+ throw new TypeError("baseUrl must be an HTTP(S) URL");
33
+ }
34
+ if (typeof fetchImplementation !== "function") {
35
+ throw new TypeError("a fetch implementation is required");
36
+ }
37
+ this.apiKey = apiKey;
38
+ this.baseUrl = baseUrl.replace(/\/+$/u, "");
39
+ this.fetch = fetchImplementation;
40
+ }
41
+
42
+ async request(
43
+ path,
44
+ {
45
+ bearerToken,
46
+ body,
47
+ idempotencyKey: mutationKey,
48
+ method = "GET",
49
+ } = {},
50
+ ) {
51
+ const headers = new Headers({ Accept: "application/json" });
52
+ const authorization = bearerToken ?? this.apiKey;
53
+ if (authorization) {
54
+ headers.set("Authorization", `Bearer ${authorization}`);
55
+ }
56
+ if (body !== undefined) headers.set("Content-Type", "application/json");
57
+ if (mutationKey) headers.set("Idempotency-Key", mutationKey);
58
+ const response = await this.fetch(`${this.baseUrl}${path}`, {
59
+ body: body === undefined ? undefined : JSON.stringify(body),
60
+ headers,
61
+ method,
62
+ });
63
+ const raw = await response.text();
64
+ let payload;
65
+ try {
66
+ payload = raw ? JSON.parse(raw) : undefined;
67
+ } catch {
68
+ throw new ShopstackApiError("Shopstack returned invalid JSON.", {
69
+ status: response.status,
70
+ });
71
+ }
72
+ if (!response.ok) {
73
+ const error = payload?.error;
74
+ throw new ShopstackApiError(
75
+ typeof error?.message === "string"
76
+ ? error.message
77
+ : `Shopstack request failed with status ${response.status}.`,
78
+ {
79
+ code: typeof error?.code === "string" ? error.code : "api_error",
80
+ requestId:
81
+ typeof error?.request_id === "string"
82
+ ? error.request_id
83
+ : undefined,
84
+ status: response.status,
85
+ },
86
+ );
87
+ }
88
+ return payload;
89
+ }
90
+
91
+ startAccountSignup({ accountType, email, idempotencyKey: key } = {}) {
92
+ return this.request("/accounts", {
93
+ body: { account_type: accountType, email },
94
+ idempotencyKey: key ?? idempotencyKey("account"),
95
+ method: "POST",
96
+ });
97
+ }
98
+
99
+ pollAccountSignup(signupId, pollToken) {
100
+ return this.request(`/accounts/${encodeURIComponent(signupId)}`, {
101
+ bearerToken: pollToken,
102
+ });
103
+ }
104
+
105
+ async waitForAccountSignup(
106
+ signupId,
107
+ pollToken,
108
+ { pollIntervalMs = 2_000, timeoutMs = 15 * 60 * 1_000 } = {},
109
+ ) {
110
+ const startedAt = Date.now();
111
+ while (true) {
112
+ if (Date.now() - startedAt >= timeoutMs) {
113
+ throw new ShopstackApiError("Email verification timed out.", {
114
+ code: "signup_timeout",
115
+ });
116
+ }
117
+ const current = await this.pollAccountSignup(signupId, pollToken);
118
+ if (current.status === "verified") return current;
119
+ await delay(pollIntervalMs);
120
+ }
121
+ }
122
+
123
+ async signup({
124
+ accountType,
125
+ email,
126
+ idempotencyKey: key,
127
+ onVerificationRequired,
128
+ pollIntervalMs = 2_000,
129
+ timeoutMs = 15 * 60 * 1_000,
130
+ } = {}) {
131
+ const signup = await this.startAccountSignup({
132
+ accountType,
133
+ email,
134
+ idempotencyKey: key,
135
+ });
136
+ if (typeof onVerificationRequired === "function") {
137
+ await onVerificationRequired({
138
+ account_type: signup.account_type,
139
+ email: signup.email,
140
+ expires_at: signup.expires_at,
141
+ id: signup.id,
142
+ status: signup.status,
143
+ });
144
+ }
145
+ return this.waitForAccountSignup(signup.id, signup.poll_token, {
146
+ pollIntervalMs,
147
+ timeoutMs,
148
+ });
149
+ }
150
+
151
+ createUser({ externalId, idempotencyKey: key } = {}) {
152
+ return this.request("/users", {
153
+ body: { external_id: externalId },
154
+ idempotencyKey: key ?? idempotencyKey("user"),
155
+ method: "POST",
156
+ });
157
+ }
158
+
159
+ listUsers() {
160
+ return this.request("/users");
161
+ }
162
+
163
+ getUser(userId) {
164
+ return this.request(`/users/${encodeURIComponent(userId)}`);
165
+ }
166
+
167
+ rotateUserApiKey(userId, { idempotencyKey: key } = {}) {
168
+ return this.request(`/users/${encodeURIComponent(userId)}/api-key/rotate`, {
169
+ idempotencyKey: key ?? idempotencyKey("rotate"),
170
+ method: "POST",
171
+ });
172
+ }
173
+
174
+ updateUser(userId, status, { idempotencyKey: key } = {}) {
175
+ return this.request(`/users/${encodeURIComponent(userId)}`, {
176
+ body: { status },
177
+ idempotencyKey: key ?? idempotencyKey("user-update"),
178
+ method: "PATCH",
179
+ });
180
+ }
181
+
182
+ listConnections() {
183
+ return this.request("/connect");
184
+ }
185
+
186
+ connect(paymentProvider = "link", { idempotencyKey: key } = {}) {
187
+ return this.request("/connect", {
188
+ body: { payment_provider: paymentProvider },
189
+ idempotencyKey: key ?? idempotencyKey("connect"),
190
+ method: "POST",
191
+ });
192
+ }
193
+
194
+ createCheckout(checkout, { idempotencyKey: key } = {}) {
195
+ return this.request("/checkout", {
196
+ body: checkout,
197
+ idempotencyKey: key ?? idempotencyKey("checkout"),
198
+ method: "POST",
199
+ });
200
+ }
201
+
202
+ getCheckout(checkoutId) {
203
+ return this.request(`/checkout/${encodeURIComponent(checkoutId)}`);
204
+ }
205
+
206
+ cancelCheckout(checkoutId, { idempotencyKey: key } = {}) {
207
+ return this.request(`/checkout/${encodeURIComponent(checkoutId)}/cancel`, {
208
+ idempotencyKey: key ?? idempotencyKey("cancel"),
209
+ method: "POST",
210
+ });
211
+ }
212
+
213
+ sendMessage(checkoutId, content, { idempotencyKey: key } = {}) {
214
+ return this.request(
215
+ `/checkout/${encodeURIComponent(checkoutId)}/messages`,
216
+ {
217
+ body: { content },
218
+ idempotencyKey: key ?? idempotencyKey("message"),
219
+ method: "POST",
220
+ },
221
+ );
222
+ }
223
+
224
+ listMessages(checkoutId) {
225
+ return this.request(`/checkout/${encodeURIComponent(checkoutId)}/messages`);
226
+ }
227
+
228
+ listEvents(checkoutId) {
229
+ return this.request(`/checkout/${encodeURIComponent(checkoutId)}/events`);
230
+ }
231
+
232
+ listArtifacts(checkoutId) {
233
+ return this.request(
234
+ `/checkout/${encodeURIComponent(checkoutId)}/artifacts`,
235
+ );
236
+ }
237
+
238
+ providePaymentDetails(checkoutId, card, { idempotencyKey: key } = {}) {
239
+ return this.request(
240
+ `/checkout/${encodeURIComponent(checkoutId)}/payment-details`,
241
+ {
242
+ body: { card },
243
+ idempotencyKey: key ?? idempotencyKey("payment-details"),
244
+ method: "POST",
245
+ },
246
+ );
247
+ }
248
+
249
+ decidePaymentApproval(
250
+ checkoutId,
251
+ approvalId,
252
+ approved,
253
+ { idempotencyKey: key } = {},
254
+ ) {
255
+ return this.request(
256
+ `/checkout/${encodeURIComponent(checkoutId)}/payment-approval`,
257
+ {
258
+ body: { approval_id: approvalId, approved },
259
+ idempotencyKey: key ?? idempotencyKey("approval"),
260
+ method: "POST",
261
+ },
262
+ );
263
+ }
264
+
265
+ async runCheckout(checkoutRequest, options = {}) {
266
+ const timeoutMs = options.timeoutMs ?? 30 * 60 * 1_000;
267
+ const startedAt = Date.now();
268
+ let checkout = await this.createCheckout(checkoutRequest, {
269
+ idempotencyKey: options.idempotencyKey,
270
+ });
271
+ let progressFingerprint;
272
+ const publishProgress = async (current) => {
273
+ if (typeof options.onProgress !== "function") return;
274
+ const fingerprint = JSON.stringify([
275
+ current.status,
276
+ current.revision,
277
+ current.activity,
278
+ current.intent?.name,
279
+ current.intent?.phase,
280
+ current.intent?.updated_at,
281
+ current.required_input?.type,
282
+ current.approval?.id,
283
+ ]);
284
+ if (fingerprint === progressFingerprint) return;
285
+ progressFingerprint = fingerprint;
286
+ await options.onProgress(current);
287
+ };
288
+ await publishProgress(checkout);
289
+ let handledPaymentRevision;
290
+ let handledApprovalId;
291
+ let handledMessageRevision;
292
+ while (!TERMINAL_STATUSES.has(checkout.status)) {
293
+ if (Date.now() - startedAt >= timeoutMs) {
294
+ throw new ShopstackApiError("Checkout monitoring timed out.", {
295
+ code: "checkout_timeout",
296
+ });
297
+ }
298
+ checkout = await this.getCheckout(checkout.id);
299
+ await publishProgress(checkout);
300
+ if (
301
+ checkout.required_input?.type === "payment_card" &&
302
+ handledPaymentRevision !== checkout.revision
303
+ ) {
304
+ if (typeof options.paymentDetails !== "function") return checkout;
305
+ handledPaymentRevision = checkout.revision;
306
+ const card = await options.paymentDetails(checkout);
307
+ if (card === undefined) return checkout;
308
+ await this.providePaymentDetails(checkout.id, card, {
309
+ idempotencyKey: `payment-details-${checkout.id}-${checkout.revision}`,
310
+ });
311
+ continue;
312
+ }
313
+ if (
314
+ checkout.status === "approval_required" &&
315
+ checkout.approval &&
316
+ handledApprovalId !== checkout.approval.id
317
+ ) {
318
+ if (typeof options.approve !== "function") return checkout;
319
+ handledApprovalId = checkout.approval.id;
320
+ const approved = await options.approve(checkout.approval, checkout);
321
+ if (typeof approved !== "boolean") return checkout;
322
+ await this.decidePaymentApproval(
323
+ checkout.id,
324
+ checkout.approval.id,
325
+ approved,
326
+ { idempotencyKey: `approval-${checkout.approval.id}` },
327
+ );
328
+ if (!approved) return this.getCheckout(checkout.id);
329
+ continue;
330
+ }
331
+ if (
332
+ checkout.status === "help_required" &&
333
+ checkout.required_input === undefined &&
334
+ handledMessageRevision !== checkout.revision
335
+ ) {
336
+ if (typeof options.message !== "function") return checkout;
337
+ handledMessageRevision = checkout.revision;
338
+ const content = await options.message(checkout);
339
+ if (typeof content !== "string" || content.length === 0)
340
+ return checkout;
341
+ await this.sendMessage(checkout.id, content, {
342
+ idempotencyKey: `message-${checkout.id}-${checkout.revision}`,
343
+ });
344
+ continue;
345
+ }
346
+ if (!TERMINAL_STATUSES.has(checkout.status)) {
347
+ const pollIntervalMs =
348
+ options.pollIntervalMs ??
349
+ (checkout.status === "help_required" ||
350
+ checkout.status === "approval_required"
351
+ ? 5_000
352
+ : 2_000);
353
+ await delay(pollIntervalMs);
354
+ }
355
+ }
356
+ return checkout;
357
+ }
358
+ }
@@ -0,0 +1,36 @@
1
+ export interface ShopstackProfile {
2
+ accountId: string;
3
+ apiKey: string;
4
+ keyType: "developer" | "user";
5
+ userId?: string;
6
+ }
7
+
8
+ export interface PendingSignup {
9
+ accountType: "developer" | "personal";
10
+ email: string;
11
+ expiresAt: string;
12
+ id: string;
13
+ pollToken: string;
14
+ profile: string;
15
+ }
16
+
17
+ export class ConfigStore {
18
+ constructor(path?: string);
19
+ activeProfile(): Promise<ShopstackProfile | undefined>;
20
+ saveProfile(
21
+ name: string,
22
+ profile: ShopstackProfile,
23
+ options?: { activate?: boolean },
24
+ ): Promise<ShopstackProfile>;
25
+ useProfile(name: string): Promise<ShopstackProfile>;
26
+ savePendingSignup(signup: PendingSignup): Promise<PendingSignup>;
27
+ pendingSignup(signupId: string): Promise<PendingSignup | undefined>;
28
+ deletePendingSignup(signupId: string): Promise<void>;
29
+ load(): Promise<{
30
+ active: string | null;
31
+ pendingSignups: Record<string, PendingSignup>;
32
+ profiles: Record<string, ShopstackProfile>;
33
+ }>;
34
+ }
35
+
36
+ export function defaultConfigPath(): string;
package/src/config.js ADDED
@@ -0,0 +1,101 @@
1
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+
5
+ export function defaultConfigPath() {
6
+ return (
7
+ process.env.SHOPSTACK_CONFIG_FILE ??
8
+ join(homedir(), ".config", "shopstack", "config.json")
9
+ );
10
+ }
11
+
12
+ export class ConfigStore {
13
+ constructor(path = defaultConfigPath()) {
14
+ this.path = path;
15
+ }
16
+
17
+ async load() {
18
+ try {
19
+ const parsed = JSON.parse(await readFile(this.path, "utf8"));
20
+ if (
21
+ typeof parsed !== "object" ||
22
+ parsed === null ||
23
+ typeof parsed.profiles !== "object" ||
24
+ parsed.profiles === null
25
+ ) {
26
+ throw new Error("Shopstack configuration is invalid.");
27
+ }
28
+ if (
29
+ parsed.pendingSignups !== undefined &&
30
+ (typeof parsed.pendingSignups !== "object" ||
31
+ parsed.pendingSignups === null ||
32
+ Array.isArray(parsed.pendingSignups))
33
+ ) {
34
+ throw new Error("Shopstack configuration is invalid.");
35
+ }
36
+ parsed.pendingSignups ??= {};
37
+ return parsed;
38
+ } catch (error) {
39
+ if (error?.code === "ENOENT") {
40
+ return { active: null, pendingSignups: {}, profiles: {} };
41
+ }
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ async write(config) {
47
+ const directory = dirname(this.path);
48
+ await mkdir(directory, { mode: 0o700, recursive: true });
49
+ const temporary = `${this.path}.${process.pid}.tmp`;
50
+ await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, {
51
+ mode: 0o600,
52
+ });
53
+ await rename(temporary, this.path);
54
+ await chmod(this.path, 0o600);
55
+ }
56
+
57
+ async saveProfile(name, profile, { activate = false } = {}) {
58
+ if (!/^[A-Za-z0-9._-]{1,80}$/u.test(name)) {
59
+ throw new Error("Profile name is invalid.");
60
+ }
61
+ const config = await this.load();
62
+ config.profiles[name] = { ...profile };
63
+ if (activate || config.active === null) config.active = name;
64
+ await this.write(config);
65
+ return config.profiles[name];
66
+ }
67
+
68
+ async useProfile(name) {
69
+ const config = await this.load();
70
+ if (!Object.hasOwn(config.profiles, name)) {
71
+ throw new Error(`Unknown Shopstack profile: ${name}`);
72
+ }
73
+ config.active = name;
74
+ await this.write(config);
75
+ return config.profiles[name];
76
+ }
77
+
78
+ async activeProfile() {
79
+ const config = await this.load();
80
+ const name = config.active;
81
+ return typeof name === "string" ? config.profiles[name] : undefined;
82
+ }
83
+
84
+ async savePendingSignup(signup) {
85
+ const config = await this.load();
86
+ config.pendingSignups[signup.id] = { ...signup };
87
+ await this.write(config);
88
+ return config.pendingSignups[signup.id];
89
+ }
90
+
91
+ async pendingSignup(signupId) {
92
+ const config = await this.load();
93
+ return config.pendingSignups[signupId];
94
+ }
95
+
96
+ async deletePendingSignup(signupId) {
97
+ const config = await this.load();
98
+ delete config.pendingSignups[signupId];
99
+ await this.write(config);
100
+ }
101
+ }
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const message =
4
- "Wow you're fast! Apply for access at https://shopstack.ai";
5
-
6
- console.log(message);