shopstack 0.2.1 → 0.2.3
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/LICENSE +21 -0
- package/README.md +23 -7
- package/SKILL.md +22 -1
- package/bin/shopstack +3 -1
- package/package.json +25 -5
- package/src/cli.js +77 -27
- package/src/client.d.ts +32 -6
- package/src/client.js +227 -70
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shopstack
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# shopstack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
JavaScript client and CLI for Shopstack's asynchronous checkout API.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -12,8 +12,8 @@ npm install -g shopstack
|
|
|
12
12
|
|
|
13
13
|
Node.js 18 or newer is required.
|
|
14
14
|
|
|
15
|
-
The
|
|
16
|
-
`https://
|
|
15
|
+
The CLI and client default to Shopstack's production API at
|
|
16
|
+
`https://api.shopstack.ai/v1`. Set
|
|
17
17
|
`SHOPSTACK_API_URL` only when targeting a different Shopstack environment.
|
|
18
18
|
|
|
19
19
|
## Verified signup
|
|
@@ -63,12 +63,14 @@ Configure any stdio MCP client to run:
|
|
|
63
63
|
|
|
64
64
|
The MCP server can start and poll verified signup, manage local profiles,
|
|
65
65
|
create developer-owned users, list/connect Link, and create/poll/message/cancel
|
|
66
|
-
checkouts.
|
|
67
|
-
|
|
66
|
+
checkouts. Checkout creation returns the exact live-view URL as a clickable MCP
|
|
67
|
+
resource; `get_live_view` replaces a lost one-time link for an active checkout.
|
|
68
|
+
It exposes status, activity, and the latest bounded mechanical intent phase. It
|
|
69
|
+
deliberately has no card-input or payment-approval tool.
|
|
68
70
|
|
|
69
71
|
The canonical agent skill ships as `SKILL.md` in this package. After
|
|
70
72
|
publication it is available at
|
|
71
|
-
`https://unpkg.com/shopstack@0.2.
|
|
73
|
+
`https://unpkg.com/shopstack@0.2.3/SKILL.md` with the release-pinned package.
|
|
72
74
|
|
|
73
75
|
## Connections
|
|
74
76
|
|
|
@@ -113,6 +115,16 @@ Then run:
|
|
|
113
115
|
shopstack checkout run --file checkout.json
|
|
114
116
|
```
|
|
115
117
|
|
|
118
|
+
The CLI prints the private owner live-view URL as soon as checkout creation
|
|
119
|
+
succeeds. Do not redirect it into shared logs or expose it to a model. The page
|
|
120
|
+
is view-only unless the model explicitly requests human assistance; while that
|
|
121
|
+
request is active, one owner viewer can use **Take control** and **Return
|
|
122
|
+
control**. **Stop** is always available and never approves payment.
|
|
123
|
+
|
|
124
|
+
Treat the printed URL as opaque and open it exactly as returned, including its
|
|
125
|
+
`#token=...` fragment. If that fragment is lost while the checkout is active,
|
|
126
|
+
run `shopstack checkout view CHECKOUT_ID` to receive a replacement URL. The
|
|
127
|
+
replacement disconnects an older viewer session but does not restart checkout.
|
|
116
128
|
When no provider is selected, the checkout runs normally until the payment
|
|
117
129
|
form, then asks for card details through a no-echo terminal prompt. Card data is
|
|
118
130
|
sent only to the protected payment-details endpoint. The CLI separately shows
|
|
@@ -154,7 +166,11 @@ restart recovery and durable credential storage; never use browser storage.
|
|
|
154
166
|
|
|
155
167
|
`runCheckout` returns at a required input if its corresponding callback is
|
|
156
168
|
omitted. Final payment approval is never inferred from a message or from
|
|
157
|
-
supplying a card.
|
|
169
|
+
supplying a card. While the checkout is active, the CLI and client use a one-time
|
|
170
|
+
checkout-scoped WebSocket capability for change notification and always fetches
|
|
171
|
+
the canonical resource after a signal. It falls back to bounded revision-aware
|
|
172
|
+
HTTP waiting, then to fixed polling when either newer transport is unavailable.
|
|
173
|
+
Neither the API key nor WebSocket capability is placed in the socket URL.
|
|
158
174
|
|
|
159
175
|
Set `SHOPSTACK_API_URL` to target a different Shopstack API and
|
|
160
176
|
`SHOPSTACK_CONFIG_FILE` to relocate CLI profiles.
|
package/SKILL.md
CHANGED
|
@@ -109,12 +109,33 @@ shopstack checkout run --file checkout.json
|
|
|
109
109
|
Or create with MCP, then call `poll_checkout`. Display these authoritative fields to the user when they change:
|
|
110
110
|
|
|
111
111
|
- `status`: one of exactly `queued`, `started`, `help_required`, `approval_required`, `submitting`, `complete`, `failed`, `cancelled`;
|
|
112
|
+
- `presentation_revision`: the monotonic revision for user-visible checkout state;
|
|
112
113
|
- `activity`: bounded present-tense display text;
|
|
113
114
|
- `intent.name`, `intent.phase`, and `intent.updated_at`: bounded mechanical action progress with no arguments or reasoning;
|
|
114
115
|
- `required_input`: a typed handoff such as protected payment-card input;
|
|
115
116
|
- `approval`: the exact amount-bound approval request when present.
|
|
116
117
|
|
|
117
|
-
|
|
118
|
+
The CLI and JavaScript client subscribe with a one-time checkout-scoped
|
|
119
|
+
WebSocket capability. Each valid notification means the presentation revision
|
|
120
|
+
changed; the client then fetches the canonical checkout before displaying or
|
|
121
|
+
acting on it. The notification is not checkout state truth and never grants
|
|
122
|
+
payment authority. The capability belongs in the WebSocket subprotocol, never
|
|
123
|
+
the URL, logs, model context, or messages. If WebSocket delivery is unavailable,
|
|
124
|
+
the client falls back to bounded HTTP waiting and then fixed polling.
|
|
125
|
+
|
|
126
|
+
MCP and manual integrations poll every two seconds for `queued`, `started`, and
|
|
127
|
+
`submitting`; every five seconds for `help_required` and `approval_required`;
|
|
128
|
+
stop at `complete`, `failed`, or `cancelled`.
|
|
129
|
+
|
|
130
|
+
The optional `live_view_url` is a private owner-viewer capability. Show it only
|
|
131
|
+
to the initiating user. Do not store it in browser storage, analytics, logs,
|
|
132
|
+
model context, or ordinary messages.
|
|
133
|
+
|
|
134
|
+
Present `live_view_url` exactly as returned, including its fragment; never
|
|
135
|
+
construct a viewer URL from the checkout ID. MCP returns it as a clickable
|
|
136
|
+
resource link. If it is lost while the checkout is active, call
|
|
137
|
+
`get_live_view` (or `shopstack checkout view CHECKOUT_ID`) once and present the
|
|
138
|
+
replacement verbatim.
|
|
118
139
|
|
|
119
140
|
## Respond at typed boundaries
|
|
120
141
|
|
package/bin/shopstack
CHANGED
|
@@ -5,6 +5,8 @@ import { runCli } from "../src/cli.js";
|
|
|
5
5
|
try {
|
|
6
6
|
await runCli(process.argv.slice(2));
|
|
7
7
|
} catch (error) {
|
|
8
|
-
process.stderr.write(
|
|
8
|
+
process.stderr.write(
|
|
9
|
+
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
10
|
+
);
|
|
9
11
|
process.exitCode = 1;
|
|
10
12
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shopstack",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Shopstack
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Production Shopstack SDK and CLI for agentic checkout.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -23,8 +23,12 @@
|
|
|
23
23
|
"shopstack": "bin/shopstack"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
|
+
"check": "npm run format:check && npm run test:coverage",
|
|
27
|
+
"format:check": "prettier --check .",
|
|
26
28
|
"shopstack": "node ./bin/shopstack",
|
|
27
|
-
"test": "node --test"
|
|
29
|
+
"test": "node --test test",
|
|
30
|
+
"test:coverage": "node --test --experimental-test-coverage test",
|
|
31
|
+
"prepublishOnly": "npm run check"
|
|
28
32
|
},
|
|
29
33
|
"engines": {
|
|
30
34
|
"node": ">=18"
|
|
@@ -33,11 +37,27 @@
|
|
|
33
37
|
"type": "git",
|
|
34
38
|
"url": "git+https://github.com/jimbo132/shopstack-cli.git"
|
|
35
39
|
},
|
|
40
|
+
"homepage": "https://github.com/jimbo132/shopstack-cli#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/jimbo132/shopstack-cli/issues"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"sideEffects": false,
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"ws": "8.21.3"
|
|
50
|
+
},
|
|
36
51
|
"license": "MIT",
|
|
37
52
|
"keywords": [
|
|
38
53
|
"shopstack",
|
|
39
54
|
"cli",
|
|
40
55
|
"checkout",
|
|
41
|
-
"payments"
|
|
42
|
-
|
|
56
|
+
"payments",
|
|
57
|
+
"agentic-commerce",
|
|
58
|
+
"agents"
|
|
59
|
+
],
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"prettier": "3.6.2"
|
|
62
|
+
}
|
|
43
63
|
}
|
package/src/cli.js
CHANGED
|
@@ -4,26 +4,52 @@ import { createInterface } from "node:readline/promises";
|
|
|
4
4
|
import { ShopstackClient } from "./client.js";
|
|
5
5
|
import { ConfigStore } from "./config.js";
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const VERSION = "0.2.2";
|
|
8
|
+
|
|
9
|
+
const HELP = `Shopstack CLI ${VERSION}
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
shopstack <command> [options]
|
|
8
13
|
|
|
9
14
|
Account setup:
|
|
10
15
|
shopstack signup
|
|
16
|
+
# Create or resume an email-verified Personal or Developer profile.
|
|
11
17
|
shopstack signup user --email EMAIL
|
|
18
|
+
# Create a Personal profile without interactive account-type prompts.
|
|
12
19
|
shopstack signup developer --email EMAIL
|
|
20
|
+
# Create a Developer management profile without interactive prompts.
|
|
13
21
|
shopstack signup resume SIGNUP_ID
|
|
22
|
+
# Resume a known pending email-verification flow.
|
|
14
23
|
shopstack users create --external-id ID [--profile NAME]
|
|
24
|
+
# Create an independently scoped user from a Developer profile.
|
|
15
25
|
shopstack profiles list
|
|
26
|
+
# List local profiles without printing their credentials.
|
|
16
27
|
shopstack profiles use NAME
|
|
28
|
+
# Select the profile used by later commands.
|
|
17
29
|
|
|
18
30
|
Payment connection:
|
|
19
31
|
shopstack connect list
|
|
32
|
+
# Inspect payment connections for the active user profile.
|
|
20
33
|
shopstack connect link
|
|
34
|
+
# Connect Link Agentic Wallet in the trusted browser flow.
|
|
21
35
|
|
|
22
36
|
Checkout:
|
|
23
37
|
shopstack checkout create --file checkout.json
|
|
38
|
+
# Create a checkout and return control immediately.
|
|
24
39
|
shopstack checkout run --file checkout.json
|
|
40
|
+
# Create, monitor, and complete a checkout interactively.
|
|
25
41
|
shopstack checkout get CHECKOUT_ID
|
|
42
|
+
# Read the canonical current checkout state.
|
|
43
|
+
shopstack checkout view CHECKOUT_ID
|
|
44
|
+
# Replace and return the active checkout's private owner live-view URL.
|
|
26
45
|
shopstack checkout cancel CHECKOUT_ID
|
|
46
|
+
# Cancel a checkout that has not reached a terminal state.
|
|
47
|
+
|
|
48
|
+
Options:
|
|
49
|
+
-h, --help
|
|
50
|
+
# Show this command reference.
|
|
51
|
+
-v, --version
|
|
52
|
+
# Print the installed Shopstack CLI version.
|
|
27
53
|
`;
|
|
28
54
|
|
|
29
55
|
function parseOptions(args, allowed) {
|
|
@@ -164,11 +190,7 @@ function signupPersistence(configStore, profileName) {
|
|
|
164
190
|
async completePending(state, result) {
|
|
165
191
|
const pendingId = state.attemptId ?? state.signupId ?? state.id;
|
|
166
192
|
if (typeof configStore.completePendingSignup === "function") {
|
|
167
|
-
await configStore.completePendingSignup(
|
|
168
|
-
pendingId,
|
|
169
|
-
profileName,
|
|
170
|
-
result,
|
|
171
|
-
);
|
|
193
|
+
await configStore.completePendingSignup(pendingId, profileName, result);
|
|
172
194
|
return;
|
|
173
195
|
}
|
|
174
196
|
await saveVerifiedSignup(result, profileName, configStore);
|
|
@@ -201,13 +223,17 @@ async function reportSignupProgress(progress, stream) {
|
|
|
201
223
|
stream.write("Signup expired. Start again to receive a new email.\n");
|
|
202
224
|
return;
|
|
203
225
|
case "rate_limited":
|
|
204
|
-
stream.write(
|
|
226
|
+
stream.write(
|
|
227
|
+
"Signup is rate-limited. Retry after the indicated delay.\n",
|
|
228
|
+
);
|
|
205
229
|
return;
|
|
206
230
|
case "conflict":
|
|
207
231
|
stream.write("Signup retry state conflicted and was cleared.\n");
|
|
208
232
|
return;
|
|
209
233
|
case "retryable_failure":
|
|
210
|
-
stream.write(
|
|
234
|
+
stream.write(
|
|
235
|
+
"Signup paused after a retryable failure. Run signup again to resume.\n",
|
|
236
|
+
);
|
|
211
237
|
return;
|
|
212
238
|
default:
|
|
213
239
|
return;
|
|
@@ -216,7 +242,7 @@ async function reportSignupProgress(progress, stream) {
|
|
|
216
242
|
|
|
217
243
|
async function activeClient(dependencies, requiredKind = "user") {
|
|
218
244
|
const profile = await dependencies.configStore.activeProfile();
|
|
219
|
-
const environmentKey =
|
|
245
|
+
const environmentKey = dependencies.env.SHOPSTACK_API_KEY;
|
|
220
246
|
const apiKey = environmentKey || profile?.apiKey;
|
|
221
247
|
const keyType = environmentKey ? requiredKind : profile?.keyType;
|
|
222
248
|
if (!apiKey)
|
|
@@ -227,7 +253,7 @@ async function activeClient(dependencies, requiredKind = "user") {
|
|
|
227
253
|
return {
|
|
228
254
|
client: dependencies.clientFactory({
|
|
229
255
|
apiKey,
|
|
230
|
-
baseUrl:
|
|
256
|
+
baseUrl: dependencies.env.SHOPSTACK_API_URL,
|
|
231
257
|
}),
|
|
232
258
|
profile,
|
|
233
259
|
};
|
|
@@ -244,13 +270,23 @@ export async function runCli(args, supplied = {}) {
|
|
|
244
270
|
stderr: process.stderr,
|
|
245
271
|
stdin: process.stdin,
|
|
246
272
|
stdout: process.stdout,
|
|
273
|
+
env: process.env,
|
|
247
274
|
...supplied,
|
|
248
275
|
};
|
|
249
276
|
const [group, action, ...rest] = args;
|
|
250
|
-
if (
|
|
277
|
+
if (
|
|
278
|
+
group === undefined ||
|
|
279
|
+
group === "help" ||
|
|
280
|
+
group === "--help" ||
|
|
281
|
+
group === "-h"
|
|
282
|
+
) {
|
|
251
283
|
dependencies.stdout.write(HELP);
|
|
252
284
|
return;
|
|
253
285
|
}
|
|
286
|
+
if (group === "--version" || group === "-v") {
|
|
287
|
+
dependencies.stdout.write(`${VERSION}\n`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
254
290
|
|
|
255
291
|
if (group === "signup") {
|
|
256
292
|
if (action === "resume") {
|
|
@@ -259,10 +295,12 @@ export async function runCli(args, supplied = {}) {
|
|
|
259
295
|
}
|
|
260
296
|
const pending = await dependencies.configStore.pendingSignup(rest[0]);
|
|
261
297
|
if (pending === undefined) {
|
|
262
|
-
throw new Error(
|
|
298
|
+
throw new Error(
|
|
299
|
+
"That signup is not present in the local profile store.",
|
|
300
|
+
);
|
|
263
301
|
}
|
|
264
302
|
const client = dependencies.clientFactory({
|
|
265
|
-
baseUrl:
|
|
303
|
+
baseUrl: dependencies.env.SHOPSTACK_API_URL,
|
|
266
304
|
});
|
|
267
305
|
const result = await client.signup({
|
|
268
306
|
accountType: pending.accountType,
|
|
@@ -286,14 +324,18 @@ export async function runCli(args, supplied = {}) {
|
|
|
286
324
|
);
|
|
287
325
|
}
|
|
288
326
|
const signupArgs = action === undefined ? [] : rest;
|
|
289
|
-
const { options, positional } = parseOptions(
|
|
327
|
+
const { options, positional } = parseOptions(
|
|
328
|
+
signupArgs,
|
|
329
|
+
new Set(["email", "profile"]),
|
|
330
|
+
);
|
|
290
331
|
if (positional.length > 0) throw new Error("Unexpected signup argument.");
|
|
291
332
|
const pendingCandidates =
|
|
292
333
|
action === undefined &&
|
|
293
334
|
typeof dependencies.configStore.pendingSignups === "function"
|
|
294
335
|
? await dependencies.configStore.pendingSignups()
|
|
295
336
|
: [];
|
|
296
|
-
const resumable =
|
|
337
|
+
const resumable =
|
|
338
|
+
pendingCandidates.length === 1 ? pendingCandidates[0] : undefined;
|
|
297
339
|
let accountType;
|
|
298
340
|
let email;
|
|
299
341
|
if (resumable !== undefined) {
|
|
@@ -301,12 +343,13 @@ export async function runCli(args, supplied = {}) {
|
|
|
301
343
|
email = resumable.email;
|
|
302
344
|
dependencies.stderr.write("Resuming pending signup.\n");
|
|
303
345
|
} else if (action === undefined) {
|
|
304
|
-
email = String(
|
|
305
|
-
await visiblePrompt("Email: ", dependencies),
|
|
306
|
-
).trim();
|
|
346
|
+
email = String(await visiblePrompt("Email: ", dependencies)).trim();
|
|
307
347
|
if (email.length === 0) throw new Error("Email is required.");
|
|
308
348
|
const selected = String(
|
|
309
|
-
await visiblePrompt(
|
|
349
|
+
await visiblePrompt(
|
|
350
|
+
"Account type (Personal / Developer): ",
|
|
351
|
+
dependencies,
|
|
352
|
+
),
|
|
310
353
|
)
|
|
311
354
|
.trim()
|
|
312
355
|
.toLowerCase();
|
|
@@ -322,7 +365,7 @@ export async function runCli(args, supplied = {}) {
|
|
|
322
365
|
email = required(options, "email");
|
|
323
366
|
}
|
|
324
367
|
const client = dependencies.clientFactory({
|
|
325
|
-
baseUrl:
|
|
368
|
+
baseUrl: dependencies.env.SHOPSTACK_API_URL,
|
|
326
369
|
});
|
|
327
370
|
const profileName =
|
|
328
371
|
options.profile ??
|
|
@@ -373,10 +416,7 @@ export async function runCli(args, supplied = {}) {
|
|
|
373
416
|
},
|
|
374
417
|
{ activate: true },
|
|
375
418
|
);
|
|
376
|
-
writeJson(
|
|
377
|
-
dependencies.stdout,
|
|
378
|
-
sanitizedUserResult(user, externalId),
|
|
379
|
-
);
|
|
419
|
+
writeJson(dependencies.stdout, sanitizedUserResult(user, externalId));
|
|
380
420
|
}
|
|
381
421
|
}
|
|
382
422
|
return;
|
|
@@ -454,6 +494,11 @@ export async function runCli(args, supplied = {}) {
|
|
|
454
494
|
const { client } = await activeClient(dependencies, "user");
|
|
455
495
|
const request = await dependencies.readJsonFile(required(options, "file"));
|
|
456
496
|
const result = await client.runCheckout(request, {
|
|
497
|
+
onCreated: (checkout) => {
|
|
498
|
+
if (typeof checkout.live_view_url === "string") {
|
|
499
|
+
dependencies.stderr.write(`Live view: ${checkout.live_view_url}\n`);
|
|
500
|
+
}
|
|
501
|
+
},
|
|
457
502
|
onProgress: (checkout) => {
|
|
458
503
|
const intent =
|
|
459
504
|
checkout.intent?.name === undefined
|
|
@@ -499,13 +544,18 @@ export async function runCli(args, supplied = {}) {
|
|
|
499
544
|
return;
|
|
500
545
|
}
|
|
501
546
|
|
|
502
|
-
if (
|
|
547
|
+
if (
|
|
548
|
+
group === "checkout" &&
|
|
549
|
+
(action === "get" || action === "view" || action === "cancel")
|
|
550
|
+
) {
|
|
503
551
|
if (rest.length !== 1) throw new Error("A checkout ID is required.");
|
|
504
552
|
const { client } = await activeClient(dependencies, "user");
|
|
505
553
|
const result =
|
|
506
554
|
action === "get"
|
|
507
555
|
? await client.getCheckout(rest[0])
|
|
508
|
-
:
|
|
556
|
+
: action === "view"
|
|
557
|
+
? await client.createLiveView(rest[0])
|
|
558
|
+
: await client.cancelCheckout(rest[0]);
|
|
509
559
|
writeJson(dependencies.stdout, result);
|
|
510
560
|
return;
|
|
511
561
|
}
|
|
@@ -513,4 +563,4 @@ export async function runCli(args, supplied = {}) {
|
|
|
513
563
|
throw new Error("Unknown command. Run `shopstack help`.");
|
|
514
564
|
}
|
|
515
565
|
|
|
516
|
-
export { HELP };
|
|
566
|
+
export { HELP, VERSION };
|
package/src/client.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export interface ShopstackClientOptions {
|
|
|
2
2
|
apiKey?: string;
|
|
3
3
|
baseUrl?: string;
|
|
4
4
|
fetch?: typeof fetch;
|
|
5
|
+
webSocket?: typeof WebSocket;
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export interface PaymentCard {
|
|
@@ -33,6 +34,7 @@ export interface Checkout {
|
|
|
33
34
|
| "failed"
|
|
34
35
|
| "cancelled";
|
|
35
36
|
revision: number;
|
|
37
|
+
presentation_revision: number;
|
|
36
38
|
activity: string;
|
|
37
39
|
intent?: {
|
|
38
40
|
name: string;
|
|
@@ -40,6 +42,7 @@ export interface Checkout {
|
|
|
40
42
|
updated_at: string;
|
|
41
43
|
};
|
|
42
44
|
item_url: string;
|
|
45
|
+
live_view_url?: string;
|
|
43
46
|
required_input?: { type: "payment_card" };
|
|
44
47
|
approval?: PaymentApproval;
|
|
45
48
|
result?: Record<string, unknown>;
|
|
@@ -51,6 +54,7 @@ export interface Checkout {
|
|
|
51
54
|
|
|
52
55
|
export interface RunCheckoutOptions {
|
|
53
56
|
idempotencyKey?: string;
|
|
57
|
+
onCreated?(checkout: Checkout): void | Promise<void>;
|
|
54
58
|
pollIntervalMs?: number;
|
|
55
59
|
timeoutMs?: number;
|
|
56
60
|
onProgress?(checkout: Checkout): void | Promise<void>;
|
|
@@ -66,6 +70,18 @@ export interface RunCheckoutOptions {
|
|
|
66
70
|
): string | Promise<string | undefined> | undefined;
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
export interface CheckoutUpdateSubscription {
|
|
74
|
+
socket_url: string;
|
|
75
|
+
protocol: "shopstack.v1";
|
|
76
|
+
token: string;
|
|
77
|
+
expires_at: string;
|
|
78
|
+
presentation_revision: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface LiveViewCapability {
|
|
82
|
+
live_view_url: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
69
85
|
export interface AccountSignupStarted {
|
|
70
86
|
id: string;
|
|
71
87
|
account_type: "personal" | "developer";
|
|
@@ -93,9 +109,9 @@ export interface SignupOptions {
|
|
|
93
109
|
timeoutMs?: number;
|
|
94
110
|
persistence?: SignupPersistence;
|
|
95
111
|
onProgress?(progress: SignupProgress): void | Promise<void>;
|
|
96
|
-
onVerificationRequired?(
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
onVerificationRequired?(
|
|
113
|
+
signup: Omit<AccountSignupStarted, "poll_token">,
|
|
114
|
+
): void | Promise<void>;
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
export interface PendingSignupState {
|
|
@@ -156,9 +172,7 @@ export class ShopstackClient {
|
|
|
156
172
|
pollAccountSignup(
|
|
157
173
|
signupId: string,
|
|
158
174
|
pollToken: string,
|
|
159
|
-
): Promise<
|
|
160
|
-
Omit<AccountSignupStarted, "poll_token"> | VerifiedAccountSignup
|
|
161
|
-
>;
|
|
175
|
+
): Promise<Omit<AccountSignupStarted, "poll_token"> | VerifiedAccountSignup>;
|
|
162
176
|
waitForAccountSignup(
|
|
163
177
|
signupId: string,
|
|
164
178
|
pollToken: string,
|
|
@@ -190,6 +204,18 @@ export class ShopstackClient {
|
|
|
190
204
|
options?: { idempotencyKey?: string },
|
|
191
205
|
): Promise<Checkout>;
|
|
192
206
|
getCheckout(checkoutId: string): Promise<Checkout>;
|
|
207
|
+
createLiveView(
|
|
208
|
+
checkoutId: string,
|
|
209
|
+
options?: { idempotencyKey?: string },
|
|
210
|
+
): Promise<LiveViewCapability>;
|
|
211
|
+
createCheckoutUpdateSubscription(
|
|
212
|
+
checkoutId: string,
|
|
213
|
+
): Promise<CheckoutUpdateSubscription>;
|
|
214
|
+
waitForCheckoutUpdate(
|
|
215
|
+
checkoutId: string,
|
|
216
|
+
afterRevision: number,
|
|
217
|
+
waitSeconds?: number,
|
|
218
|
+
): Promise<Checkout | undefined>;
|
|
193
219
|
cancelCheckout(
|
|
194
220
|
checkoutId: string,
|
|
195
221
|
options?: { idempotencyKey?: string },
|
package/src/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import WebSocket from "ws";
|
|
2
3
|
|
|
3
4
|
const TERMINAL_STATUSES = new Set(["complete", "failed", "cancelled"]);
|
|
4
5
|
|
|
@@ -30,11 +31,119 @@ function delay(milliseconds) {
|
|
|
30
31
|
: new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
function presentationRevision(checkout) {
|
|
35
|
+
return Number.isInteger(checkout?.presentation_revision)
|
|
36
|
+
? checkout.presentation_revision
|
|
37
|
+
: checkout.revision;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class CheckoutUpdateWatcher {
|
|
41
|
+
constructor(client, checkoutId) {
|
|
42
|
+
this.client = client;
|
|
43
|
+
this.checkoutId = checkoutId;
|
|
44
|
+
this.latestRevision = 0;
|
|
45
|
+
this.waiter = undefined;
|
|
46
|
+
this.socket = undefined;
|
|
47
|
+
this.unavailable = typeof client.WebSocket !== "function";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async connect() {
|
|
51
|
+
if (this.socket || this.unavailable) return;
|
|
52
|
+
try {
|
|
53
|
+
const subscription = await this.client.createCheckoutUpdateSubscription(
|
|
54
|
+
this.checkoutId,
|
|
55
|
+
);
|
|
56
|
+
const socket = new this.client.WebSocket(subscription.socket_url, [
|
|
57
|
+
subscription.protocol,
|
|
58
|
+
subscription.token,
|
|
59
|
+
]);
|
|
60
|
+
this.socket = socket;
|
|
61
|
+
socket.addEventListener("message", (event) => {
|
|
62
|
+
let payload;
|
|
63
|
+
try {
|
|
64
|
+
payload = JSON.parse(String(event.data));
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (
|
|
69
|
+
payload?.type !== "checkout_changed" ||
|
|
70
|
+
!Number.isInteger(payload.presentation_revision) ||
|
|
71
|
+
payload.presentation_revision < 1 ||
|
|
72
|
+
typeof payload.terminal !== "boolean"
|
|
73
|
+
) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.latestRevision = Math.max(
|
|
77
|
+
this.latestRevision,
|
|
78
|
+
payload.presentation_revision,
|
|
79
|
+
);
|
|
80
|
+
this.waiter?.();
|
|
81
|
+
});
|
|
82
|
+
const unavailable = () => {
|
|
83
|
+
this.unavailable = true;
|
|
84
|
+
this.waiter?.();
|
|
85
|
+
};
|
|
86
|
+
socket.addEventListener("error", unavailable);
|
|
87
|
+
socket.addEventListener("close", unavailable);
|
|
88
|
+
} catch {
|
|
89
|
+
this.unavailable = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async wait(afterRevision, timeoutMs) {
|
|
94
|
+
await this.connect();
|
|
95
|
+
if (this.latestRevision > afterRevision) return { kind: "changed" };
|
|
96
|
+
if (!this.unavailable) {
|
|
97
|
+
const changed = await new Promise((resolve) => {
|
|
98
|
+
let settled = false;
|
|
99
|
+
const settle = (value) => {
|
|
100
|
+
if (settled) return;
|
|
101
|
+
settled = true;
|
|
102
|
+
this.waiter = undefined;
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
resolve(value);
|
|
105
|
+
};
|
|
106
|
+
this.waiter = () =>
|
|
107
|
+
settle(
|
|
108
|
+
this.latestRevision > afterRevision ? "changed" : "unavailable",
|
|
109
|
+
);
|
|
110
|
+
const timer = setTimeout(
|
|
111
|
+
() => settle("timeout"),
|
|
112
|
+
Math.max(1, Math.min(timeoutMs, 25_000)),
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
if (changed === "changed") return { kind: "changed" };
|
|
116
|
+
if (changed === "timeout") return { kind: "refresh" };
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const checkout = await this.client.waitForCheckoutUpdate(
|
|
120
|
+
this.checkoutId,
|
|
121
|
+
afterRevision,
|
|
122
|
+
Math.max(1, Math.min(25, Math.ceil(timeoutMs / 1_000))),
|
|
123
|
+
);
|
|
124
|
+
return checkout === undefined
|
|
125
|
+
? { kind: "refresh" }
|
|
126
|
+
: { checkout, kind: "checkout" };
|
|
127
|
+
} catch {
|
|
128
|
+
return { kind: "unavailable" };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
close() {
|
|
133
|
+
try {
|
|
134
|
+
this.socket?.close(1000, "Checkout monitoring complete");
|
|
135
|
+
} catch {
|
|
136
|
+
// Monitoring teardown must not alter the checkout result.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
33
141
|
export class ShopstackClient {
|
|
34
142
|
constructor({
|
|
35
143
|
apiKey,
|
|
36
|
-
baseUrl = "https://
|
|
144
|
+
baseUrl = "https://api.shopstack.ai/v1",
|
|
37
145
|
fetch: fetchImplementation = globalThis.fetch,
|
|
146
|
+
webSocket: webSocketImplementation = WebSocket,
|
|
38
147
|
} = {}) {
|
|
39
148
|
if (typeof baseUrl !== "string" || !/^https?:\/\//u.test(baseUrl)) {
|
|
40
149
|
throw new TypeError("baseUrl must be an HTTP(S) URL");
|
|
@@ -45,6 +154,7 @@ export class ShopstackClient {
|
|
|
45
154
|
this.apiKey = apiKey;
|
|
46
155
|
this.baseUrl = baseUrl.replace(/\/+$/u, "");
|
|
47
156
|
this.fetch = fetchImplementation;
|
|
157
|
+
this.WebSocket = webSocketImplementation;
|
|
48
158
|
}
|
|
49
159
|
|
|
50
160
|
async request(
|
|
@@ -98,7 +208,12 @@ export class ShopstackClient {
|
|
|
98
208
|
return payload;
|
|
99
209
|
}
|
|
100
210
|
|
|
101
|
-
_startAccountSignup({
|
|
211
|
+
_startAccountSignup({
|
|
212
|
+
accountType,
|
|
213
|
+
email,
|
|
214
|
+
idempotencyKey: key,
|
|
215
|
+
recoveryKey,
|
|
216
|
+
}) {
|
|
102
217
|
return this.request("/accounts", {
|
|
103
218
|
body: { account_type: accountType, email },
|
|
104
219
|
idempotencyKey: key,
|
|
@@ -164,16 +279,14 @@ export class ShopstackClient {
|
|
|
164
279
|
},
|
|
165
280
|
};
|
|
166
281
|
const publish = async (state, details = {}) => {
|
|
167
|
-
if (typeof onProgress === "function")
|
|
282
|
+
if (typeof onProgress === "function")
|
|
283
|
+
await onProgress({ state, ...details });
|
|
168
284
|
};
|
|
169
285
|
let pending = await storage.loadPending({
|
|
170
286
|
accountType,
|
|
171
287
|
email: normalizedEmail,
|
|
172
288
|
});
|
|
173
|
-
if (
|
|
174
|
-
pending?.expiresAt &&
|
|
175
|
-
Date.parse(pending.expiresAt) <= Date.now()
|
|
176
|
-
) {
|
|
289
|
+
if (pending?.expiresAt && Date.parse(pending.expiresAt) <= Date.now()) {
|
|
177
290
|
await storage.deletePending(pending);
|
|
178
291
|
await publish("expired", {
|
|
179
292
|
account_type: accountType,
|
|
@@ -331,6 +444,32 @@ export class ShopstackClient {
|
|
|
331
444
|
return this.request(`/checkout/${encodeURIComponent(checkoutId)}`);
|
|
332
445
|
}
|
|
333
446
|
|
|
447
|
+
createLiveView(checkoutId, { idempotencyKey: key } = {}) {
|
|
448
|
+
return this.request(
|
|
449
|
+
`/checkout/${encodeURIComponent(checkoutId)}/live-view`,
|
|
450
|
+
{
|
|
451
|
+
idempotencyKey: key ?? idempotencyKey("live-view"),
|
|
452
|
+
method: "POST",
|
|
453
|
+
},
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
createCheckoutUpdateSubscription(checkoutId) {
|
|
458
|
+
return this.request(`/checkout/${encodeURIComponent(checkoutId)}/updates`, {
|
|
459
|
+
method: "POST",
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
waitForCheckoutUpdate(checkoutId, afterRevision, waitSeconds = 25) {
|
|
464
|
+
const query = new URLSearchParams({
|
|
465
|
+
after: String(afterRevision),
|
|
466
|
+
wait: String(waitSeconds),
|
|
467
|
+
});
|
|
468
|
+
return this.request(
|
|
469
|
+
`/checkout/${encodeURIComponent(checkoutId)}/updates?${query.toString()}`,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
334
473
|
cancelCheckout(checkoutId, { idempotencyKey: key } = {}) {
|
|
335
474
|
return this.request(`/checkout/${encodeURIComponent(checkoutId)}/cancel`, {
|
|
336
475
|
idempotencyKey: key ?? idempotencyKey("cancel"),
|
|
@@ -396,12 +535,16 @@ export class ShopstackClient {
|
|
|
396
535
|
let checkout = await this.createCheckout(checkoutRequest, {
|
|
397
536
|
idempotencyKey: options.idempotencyKey,
|
|
398
537
|
});
|
|
538
|
+
if (typeof options.onCreated === "function") {
|
|
539
|
+
await options.onCreated(checkout);
|
|
540
|
+
}
|
|
399
541
|
let progressFingerprint;
|
|
400
542
|
const publishProgress = async (current) => {
|
|
401
543
|
if (typeof options.onProgress !== "function") return;
|
|
402
544
|
const fingerprint = JSON.stringify([
|
|
403
545
|
current.status,
|
|
404
546
|
current.revision,
|
|
547
|
+
current.presentation_revision,
|
|
405
548
|
current.activity,
|
|
406
549
|
current.intent?.name,
|
|
407
550
|
current.intent?.phase,
|
|
@@ -413,74 +556,88 @@ export class ShopstackClient {
|
|
|
413
556
|
progressFingerprint = fingerprint;
|
|
414
557
|
await options.onProgress(current);
|
|
415
558
|
};
|
|
416
|
-
|
|
559
|
+
const watcher = new CheckoutUpdateWatcher(this, checkout.id);
|
|
417
560
|
let handledPaymentRevision;
|
|
418
561
|
let handledApprovalId;
|
|
419
562
|
let handledMessageRevision;
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
checkout.
|
|
452
|
-
checkout
|
|
453
|
-
|
|
454
|
-
|
|
563
|
+
try {
|
|
564
|
+
while (true) {
|
|
565
|
+
await publishProgress(checkout);
|
|
566
|
+
if (TERMINAL_STATUSES.has(checkout.status)) return checkout;
|
|
567
|
+
const elapsed = Date.now() - startedAt;
|
|
568
|
+
if (elapsed >= timeoutMs) {
|
|
569
|
+
throw new ShopstackApiError("Checkout monitoring timed out.", {
|
|
570
|
+
code: "checkout_timeout",
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
if (
|
|
574
|
+
checkout.required_input?.type === "payment_card" &&
|
|
575
|
+
handledPaymentRevision !== checkout.revision
|
|
576
|
+
) {
|
|
577
|
+
if (typeof options.paymentDetails !== "function") return checkout;
|
|
578
|
+
handledPaymentRevision = checkout.revision;
|
|
579
|
+
const card = await options.paymentDetails(checkout);
|
|
580
|
+
if (card === undefined) return checkout;
|
|
581
|
+
checkout = await this.providePaymentDetails(checkout.id, card, {
|
|
582
|
+
idempotencyKey: `payment-details-${checkout.id}-${checkout.revision}`,
|
|
583
|
+
});
|
|
584
|
+
handledMessageRevision = checkout.revision;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (
|
|
588
|
+
checkout.status === "approval_required" &&
|
|
589
|
+
checkout.approval &&
|
|
590
|
+
handledApprovalId !== checkout.approval.id
|
|
591
|
+
) {
|
|
592
|
+
if (typeof options.approve !== "function") return checkout;
|
|
593
|
+
handledApprovalId = checkout.approval.id;
|
|
594
|
+
const approved = await options.approve(checkout.approval, checkout);
|
|
595
|
+
if (typeof approved !== "boolean") return checkout;
|
|
596
|
+
checkout = await this.decidePaymentApproval(
|
|
597
|
+
checkout.id,
|
|
598
|
+
checkout.approval.id,
|
|
599
|
+
approved,
|
|
600
|
+
{ idempotencyKey: `approval-${checkout.approval.id}` },
|
|
601
|
+
);
|
|
602
|
+
if (!approved) return checkout;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (
|
|
606
|
+
checkout.status === "help_required" &&
|
|
607
|
+
checkout.required_input === undefined &&
|
|
608
|
+
handledMessageRevision !== checkout.revision
|
|
609
|
+
) {
|
|
610
|
+
if (typeof options.message !== "function") return checkout;
|
|
611
|
+
handledMessageRevision = checkout.revision;
|
|
612
|
+
const content = await options.message(checkout);
|
|
613
|
+
if (typeof content !== "string" || content.length === 0)
|
|
614
|
+
return checkout;
|
|
615
|
+
await this.sendMessage(checkout.id, content, {
|
|
616
|
+
idempotencyKey: `message-${checkout.id}-${checkout.revision}`,
|
|
617
|
+
});
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
621
|
+
const update = await watcher.wait(
|
|
622
|
+
presentationRevision(checkout),
|
|
623
|
+
remainingMs,
|
|
455
624
|
);
|
|
456
|
-
if (
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
await this.sendMessage(checkout.id, content, {
|
|
470
|
-
idempotencyKey: `message-${checkout.id}-${checkout.revision}`,
|
|
471
|
-
});
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
if (!TERMINAL_STATUSES.has(checkout.status)) {
|
|
475
|
-
const pollIntervalMs =
|
|
476
|
-
options.pollIntervalMs ??
|
|
477
|
-
(checkout.status === "help_required" ||
|
|
478
|
-
checkout.status === "approval_required"
|
|
479
|
-
? 5_000
|
|
480
|
-
: 2_000);
|
|
481
|
-
await delay(pollIntervalMs);
|
|
625
|
+
if (update.kind === "unavailable") {
|
|
626
|
+
const pollIntervalMs =
|
|
627
|
+
options.pollIntervalMs ??
|
|
628
|
+
(checkout.status === "help_required" ||
|
|
629
|
+
checkout.status === "approval_required"
|
|
630
|
+
? 5_000
|
|
631
|
+
: 2_000);
|
|
632
|
+
await delay(Math.min(pollIntervalMs, remainingMs));
|
|
633
|
+
}
|
|
634
|
+
checkout =
|
|
635
|
+
update.kind === "checkout"
|
|
636
|
+
? update.checkout
|
|
637
|
+
: await this.getCheckout(checkout.id);
|
|
482
638
|
}
|
|
639
|
+
} finally {
|
|
640
|
+
watcher.close();
|
|
483
641
|
}
|
|
484
|
-
return checkout;
|
|
485
642
|
}
|
|
486
643
|
}
|