shopstack 0.2.5 → 0.2.6
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 +16 -19
- package/SKILL.md +11 -10
- package/package.json +1 -1
- package/src/cli.js +55 -143
- package/src/client.d.ts +25 -25
- package/src/client.js +32 -37
- package/src/config.d.ts +10 -10
- package/src/config.js +34 -31
package/README.md
CHANGED
|
@@ -16,30 +16,27 @@ The CLI and client default to Shopstack's production API at
|
|
|
16
16
|
`https://api.shopstack.ai/v1`. Set
|
|
17
17
|
`SHOPSTACK_API_URL` only when targeting a different Shopstack environment.
|
|
18
18
|
|
|
19
|
-
## Verified
|
|
19
|
+
## Verified login
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
shopstack
|
|
22
|
+
shopstack login [--email EMAIL] [--profile NAME] [--account-type personal|developer]
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
The command asks for email
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
The command asks for an email when `--email` is absent. It uses the `default`
|
|
26
|
+
profile and Personal account preference unless flags select other values. An
|
|
27
|
+
existing account keeps its current account type. A new email creates the
|
|
28
|
+
requested account type. The command waits for up to 15 minutes while you open
|
|
29
|
+
the verification email. It does not issue a new API key until the link is
|
|
30
|
+
consumed. The CLI privately persists all retry and polling capabilities before
|
|
31
|
+
the first request, then retrieves a new key after verification and stores it in
|
|
30
32
|
`~/.config/shopstack/config.json` with file mode `0600`; it never prints the
|
|
31
33
|
key, recovery capability, or polling capability. If the command is interrupted,
|
|
32
|
-
rerun `shopstack
|
|
33
|
-
The explicit helper also remains available:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
shopstack signup resume sup_...
|
|
37
|
-
```
|
|
34
|
+
rerun `shopstack login`; the matching pending attempt resumes automatically.
|
|
38
35
|
|
|
39
36
|
## Developer account and users
|
|
40
37
|
|
|
41
38
|
```bash
|
|
42
|
-
shopstack
|
|
39
|
+
shopstack login
|
|
43
40
|
shopstack users create --external-id customer-123 --profile customer-123
|
|
44
41
|
```
|
|
45
42
|
|
|
@@ -61,7 +58,7 @@ Configure any stdio MCP client to run:
|
|
|
61
58
|
}
|
|
62
59
|
```
|
|
63
60
|
|
|
64
|
-
The MCP server
|
|
61
|
+
The MCP server has one verified login tool, manages local profiles,
|
|
65
62
|
create developer-owned users, list/connect Link, and create/poll/message/cancel
|
|
66
63
|
checkouts. Checkout creation returns the exact live-view URL as a clickable MCP
|
|
67
64
|
resource; `get_live_view` replaces a lost one-time link for an active checkout.
|
|
@@ -147,11 +144,11 @@ Card values are never accepted as command-line flags.
|
|
|
147
144
|
import { ShopstackClient } from "shopstack";
|
|
148
145
|
|
|
149
146
|
const onboarding = new ShopstackClient();
|
|
150
|
-
const account = await onboarding.
|
|
147
|
+
const account = await onboarding.login({
|
|
151
148
|
accountType: "developer",
|
|
152
149
|
email: "developer@example.com",
|
|
153
150
|
onProgress: ({ state }) => console.log(state),
|
|
154
|
-
persistence:
|
|
151
|
+
persistence: secureLoginPersistence,
|
|
155
152
|
});
|
|
156
153
|
|
|
157
154
|
const shopstack = new ShopstackClient({
|
|
@@ -165,8 +162,8 @@ const result = await shopstack.runCheckout(checkoutRequest, {
|
|
|
165
162
|
});
|
|
166
163
|
```
|
|
167
164
|
|
|
168
|
-
`
|
|
169
|
-
identity only after the
|
|
165
|
+
`login` generates retry state internally and returns the authenticated account
|
|
166
|
+
identity only after the new credential is stored. It never returns the API
|
|
170
167
|
key, recovery key, or polling token. Pass a secure persistence adapter for
|
|
171
168
|
restart recovery and durable credential storage; never use browser storage.
|
|
172
169
|
|
package/SKILL.md
CHANGED
|
@@ -38,21 +38,22 @@ Never ask the model to print or relay a Shopstack API key. The CLI and MCP serve
|
|
|
38
38
|
- A developer management key creates and manages users but cannot run a user's checkout.
|
|
39
39
|
- Every developer-owned user receives an independent user API key and profile.
|
|
40
40
|
|
|
41
|
-
Start verified CLI
|
|
41
|
+
Start verified CLI login:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
shopstack
|
|
44
|
+
shopstack login [--email EMAIL] [--profile NAME] [--account-type personal|developer]
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
The command prompts for email
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
The command prompts only for a missing email. Personal and `default` are the
|
|
48
|
+
default account preference and profile. An existing account keeps its current
|
|
49
|
+
type. A new email creates the requested account type. The user must open the
|
|
50
|
+
time-limited verification link. The CLI generates and privately stores its
|
|
51
|
+
retry and polling capabilities before the request, polls login, saves the new
|
|
52
|
+
key with mode `0600`, and prints none of those credentials. Rerunning
|
|
53
|
+
`shopstack login` automatically resumes the matching pending attempt after an
|
|
54
|
+
interruption.
|
|
54
55
|
|
|
55
|
-
After developer
|
|
56
|
+
After developer login, create an end user:
|
|
56
57
|
|
|
57
58
|
```bash
|
|
58
59
|
shopstack users create --external-id customer-123 --profile customer-123
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -17,14 +17,8 @@ Usage:
|
|
|
17
17
|
shopstack <command> [options]
|
|
18
18
|
|
|
19
19
|
Account setup:
|
|
20
|
-
shopstack
|
|
21
|
-
#
|
|
22
|
-
shopstack signup user --email EMAIL
|
|
23
|
-
# Create a Personal profile without interactive account-type prompts.
|
|
24
|
-
shopstack signup developer --email EMAIL
|
|
25
|
-
# Create a Developer management profile without interactive prompts.
|
|
26
|
-
shopstack signup resume SIGNUP_ID
|
|
27
|
-
# Resume a known pending email-verification flow.
|
|
20
|
+
shopstack login [--email EMAIL] [--profile NAME] [--account-type personal|developer]
|
|
21
|
+
# Log in, or create the requested account type when the email is new.
|
|
28
22
|
shopstack users create --external-id ID [--profile NAME]
|
|
29
23
|
# Create an independently scoped user from a Developer profile.
|
|
30
24
|
shopstack profiles list
|
|
@@ -86,11 +80,6 @@ function required(options, name) {
|
|
|
86
80
|
return value;
|
|
87
81
|
}
|
|
88
82
|
|
|
89
|
-
function isAffirmative(value) {
|
|
90
|
-
const answer = String(value).trim().toLowerCase();
|
|
91
|
-
return answer === "y" || answer === "yes";
|
|
92
|
-
}
|
|
93
|
-
|
|
94
83
|
function writeJson(stream, value) {
|
|
95
84
|
stream.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
96
85
|
}
|
|
@@ -240,7 +229,7 @@ function sanitizedUserResult(result, profile) {
|
|
|
240
229
|
return { api_key_saved: true, profile, user };
|
|
241
230
|
}
|
|
242
231
|
|
|
243
|
-
async function
|
|
232
|
+
async function saveVerifiedLogin(result, profileName, configStore) {
|
|
244
233
|
await configStore.saveProfile(
|
|
245
234
|
profileName,
|
|
246
235
|
{
|
|
@@ -253,34 +242,34 @@ async function saveVerifiedSignup(result, profileName, configStore) {
|
|
|
253
242
|
);
|
|
254
243
|
}
|
|
255
244
|
|
|
256
|
-
function
|
|
245
|
+
function loginPersistence(configStore, profileName) {
|
|
257
246
|
return {
|
|
258
247
|
async loadPending(input) {
|
|
259
|
-
return typeof configStore.
|
|
260
|
-
? configStore.
|
|
248
|
+
return typeof configStore.findPendingLogin === "function"
|
|
249
|
+
? configStore.findPendingLogin(input)
|
|
261
250
|
: undefined;
|
|
262
251
|
},
|
|
263
252
|
async savePending(state) {
|
|
264
|
-
await configStore.
|
|
253
|
+
await configStore.savePendingLogin({ ...state, profile: profileName });
|
|
265
254
|
},
|
|
266
255
|
async completePending(state, result) {
|
|
267
|
-
const pendingId = state.attemptId ?? state.
|
|
268
|
-
if (typeof configStore.
|
|
269
|
-
await configStore.
|
|
256
|
+
const pendingId = state.attemptId ?? state.loginId ?? state.id;
|
|
257
|
+
if (typeof configStore.completePendingLogin === "function") {
|
|
258
|
+
await configStore.completePendingLogin(pendingId, profileName, result);
|
|
270
259
|
return;
|
|
271
260
|
}
|
|
272
|
-
await
|
|
273
|
-
await configStore.
|
|
261
|
+
await saveVerifiedLogin(result, profileName, configStore);
|
|
262
|
+
await configStore.deletePendingLogin(pendingId);
|
|
274
263
|
},
|
|
275
264
|
async deletePending(state) {
|
|
276
|
-
await configStore.
|
|
277
|
-
state.attemptId ?? state.
|
|
265
|
+
await configStore.deletePendingLogin(
|
|
266
|
+
state.attemptId ?? state.loginId ?? state.id,
|
|
278
267
|
);
|
|
279
268
|
},
|
|
280
269
|
};
|
|
281
270
|
}
|
|
282
271
|
|
|
283
|
-
async function
|
|
272
|
+
async function reportLoginProgress(progress, stream) {
|
|
284
273
|
switch (progress.state) {
|
|
285
274
|
case "email_sent":
|
|
286
275
|
stream.write("Verification email sent.\n");
|
|
@@ -292,23 +281,21 @@ async function reportSignupProgress(progress, stream) {
|
|
|
292
281
|
stream.write("✓ Email verified\n");
|
|
293
282
|
return;
|
|
294
283
|
case "complete":
|
|
295
|
-
stream.write("✓ Shopstack
|
|
284
|
+
stream.write("✓ Shopstack login complete\n");
|
|
296
285
|
stream.write("✓ Credentials saved securely\n");
|
|
297
286
|
return;
|
|
298
287
|
case "expired":
|
|
299
|
-
stream.write("
|
|
288
|
+
stream.write("Login expired. Start again to receive a new email.\n");
|
|
300
289
|
return;
|
|
301
290
|
case "rate_limited":
|
|
302
|
-
stream.write(
|
|
303
|
-
"Signup is rate-limited. Retry after the indicated delay.\n",
|
|
304
|
-
);
|
|
291
|
+
stream.write("Login is rate-limited. Retry after the indicated delay.\n");
|
|
305
292
|
return;
|
|
306
293
|
case "conflict":
|
|
307
|
-
stream.write("
|
|
294
|
+
stream.write("Login retry state conflicted and was cleared.\n");
|
|
308
295
|
return;
|
|
309
296
|
case "retryable_failure":
|
|
310
297
|
stream.write(
|
|
311
|
-
"
|
|
298
|
+
"Login paused after a retryable failure. Run login again to resume.\n",
|
|
312
299
|
);
|
|
313
300
|
return;
|
|
314
301
|
default:
|
|
@@ -321,8 +308,7 @@ async function activeClient(dependencies, requiredKind = "user") {
|
|
|
321
308
|
const environmentKey = dependencies.env.SHOPSTACK_API_KEY;
|
|
322
309
|
const apiKey = environmentKey || profile?.apiKey;
|
|
323
310
|
const keyType = environmentKey ? requiredKind : profile?.keyType;
|
|
324
|
-
if (!apiKey)
|
|
325
|
-
throw new Error("No active Shopstack API key. Run signup first.");
|
|
311
|
+
if (!apiKey) throw new Error("No active Shopstack API key. Run login first.");
|
|
326
312
|
if (requiredKind && keyType !== requiredKind) {
|
|
327
313
|
throw new Error(`This command requires an active ${requiredKind} profile.`);
|
|
328
314
|
}
|
|
@@ -368,136 +354,62 @@ export async function runCli(args, supplied = {}) {
|
|
|
368
354
|
return;
|
|
369
355
|
}
|
|
370
356
|
|
|
371
|
-
if (group === "
|
|
372
|
-
if (action === "resume") {
|
|
373
|
-
if (rest.length !== 1) {
|
|
374
|
-
throw new Error("Use `shopstack signup resume SIGNUP_ID`.");
|
|
375
|
-
}
|
|
376
|
-
const pending = await dependencies.configStore.pendingSignup(rest[0]);
|
|
377
|
-
if (pending === undefined) {
|
|
378
|
-
throw new Error(
|
|
379
|
-
"That signup is not present in the local profile store.",
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
const client = dependencies.clientFactory({
|
|
383
|
-
baseUrl: dependencies.env.SHOPSTACK_API_URL,
|
|
384
|
-
});
|
|
385
|
-
const result = await client.signup({
|
|
386
|
-
accountType: pending.accountType,
|
|
387
|
-
email: pending.email,
|
|
388
|
-
onProgress: (progress) =>
|
|
389
|
-
reportSignupProgress(progress, dependencies.stderr),
|
|
390
|
-
persistence: signupPersistence(
|
|
391
|
-
dependencies.configStore,
|
|
392
|
-
pending.profile,
|
|
393
|
-
),
|
|
394
|
-
});
|
|
395
|
-
writeJson(dependencies.stdout, {
|
|
396
|
-
...sanitizedAccountResult(result),
|
|
397
|
-
profile: pending.profile,
|
|
398
|
-
});
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
if (action !== undefined && action !== "user" && action !== "developer") {
|
|
402
|
-
throw new Error(
|
|
403
|
-
"Use `shopstack signup user` or `shopstack signup developer`.",
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
const signupArgs = action === undefined ? [] : rest;
|
|
357
|
+
if (group === "login") {
|
|
407
358
|
const { options, positional } = parseOptions(
|
|
408
|
-
|
|
409
|
-
new Set(["email", "profile"]),
|
|
359
|
+
args.slice(1),
|
|
360
|
+
new Set(["account-type", "email", "profile"]),
|
|
410
361
|
);
|
|
411
|
-
if (positional.length > 0) throw new Error("Unexpected
|
|
362
|
+
if (positional.length > 0) throw new Error("Unexpected login argument.");
|
|
363
|
+
const profileName = options.profile ?? "default";
|
|
364
|
+
const requestedAccountType = options["account-type"] ?? "personal";
|
|
365
|
+
if (
|
|
366
|
+
requestedAccountType !== "personal" &&
|
|
367
|
+
requestedAccountType !== "developer"
|
|
368
|
+
) {
|
|
369
|
+
throw new Error("Account type must be personal or developer.");
|
|
370
|
+
}
|
|
412
371
|
const pendingCandidates =
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
? await dependencies.configStore.pendingSignups()
|
|
372
|
+
typeof dependencies.configStore.pendingLogins === "function"
|
|
373
|
+
? await dependencies.configStore.pendingLogins()
|
|
416
374
|
: [];
|
|
375
|
+
const matchingPending = pendingCandidates.filter(
|
|
376
|
+
(pending) =>
|
|
377
|
+
pending.profile === profileName &&
|
|
378
|
+
(options.email === undefined ||
|
|
379
|
+
pending.email.trim().toLowerCase() ===
|
|
380
|
+
options.email.trim().toLowerCase()) &&
|
|
381
|
+
(options["account-type"] === undefined ||
|
|
382
|
+
pending.accountType === requestedAccountType),
|
|
383
|
+
);
|
|
417
384
|
const resumable =
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
let email;
|
|
385
|
+
matchingPending.length === 1 ? matchingPending[0] : undefined;
|
|
386
|
+
const accountType = resumable?.accountType ?? requestedAccountType;
|
|
387
|
+
let email = resumable?.email ?? options.email;
|
|
421
388
|
if (resumable !== undefined) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
} else if (action === undefined) {
|
|
389
|
+
dependencies.stderr.write("Resuming pending login.\n");
|
|
390
|
+
}
|
|
391
|
+
if (email === undefined) {
|
|
426
392
|
email = String(await visiblePrompt("Email: ", dependencies)).trim();
|
|
427
|
-
if (email.length === 0) throw new Error("Email is required.");
|
|
428
|
-
const selected = String(
|
|
429
|
-
await visiblePrompt(
|
|
430
|
-
"Account type (Personal / Developer): ",
|
|
431
|
-
dependencies,
|
|
432
|
-
),
|
|
433
|
-
)
|
|
434
|
-
.trim()
|
|
435
|
-
.toLowerCase();
|
|
436
|
-
if (selected === "personal" || selected === "user") {
|
|
437
|
-
accountType = "personal";
|
|
438
|
-
} else if (selected === "developer") {
|
|
439
|
-
accountType = "developer";
|
|
440
|
-
} else {
|
|
441
|
-
throw new Error("Account type must be Personal or Developer.");
|
|
442
|
-
}
|
|
443
|
-
} else {
|
|
444
|
-
accountType = action === "user" ? "personal" : "developer";
|
|
445
|
-
email = required(options, "email");
|
|
446
393
|
}
|
|
394
|
+
if (email.length === 0) throw new Error("Email is required.");
|
|
447
395
|
const client = dependencies.clientFactory({
|
|
448
396
|
baseUrl: dependencies.env.SHOPSTACK_API_URL,
|
|
449
397
|
});
|
|
450
|
-
const
|
|
451
|
-
options.profile ??
|
|
452
|
-
resumable?.profile ??
|
|
453
|
-
(accountType === "developer" ? "developer" : "default");
|
|
454
|
-
const result = await client.signup({
|
|
398
|
+
const result = await client.login({
|
|
455
399
|
accountType,
|
|
456
400
|
email,
|
|
457
401
|
onProgress: (progress) =>
|
|
458
|
-
|
|
459
|
-
persistence:
|
|
402
|
+
reportLoginProgress(progress, dependencies.stderr),
|
|
403
|
+
persistence: loginPersistence(dependencies.configStore, profileName),
|
|
460
404
|
});
|
|
461
405
|
writeJson(dependencies.stdout, {
|
|
462
406
|
...sanitizedAccountResult(result),
|
|
463
407
|
profile: profileName,
|
|
464
408
|
});
|
|
465
|
-
if (
|
|
409
|
+
if (result.key_type === "developer") {
|
|
466
410
|
dependencies.stderr.write(
|
|
467
411
|
"The developer management credential cannot run user checkouts. Create a user profile with `shopstack users create --external-id ID`.\n",
|
|
468
412
|
);
|
|
469
|
-
if (
|
|
470
|
-
action === undefined &&
|
|
471
|
-
isAffirmative(
|
|
472
|
-
await visiblePrompt(
|
|
473
|
-
"Create the first developer-owned user now? (y/N): ",
|
|
474
|
-
dependencies,
|
|
475
|
-
),
|
|
476
|
-
)
|
|
477
|
-
) {
|
|
478
|
-
const externalId = String(
|
|
479
|
-
await visiblePrompt("User external ID: ", dependencies),
|
|
480
|
-
).trim();
|
|
481
|
-
if (externalId.length === 0) {
|
|
482
|
-
throw new Error("User external ID is required.");
|
|
483
|
-
}
|
|
484
|
-
const { client: developerClient, profile } = await activeClient(
|
|
485
|
-
dependencies,
|
|
486
|
-
"developer",
|
|
487
|
-
);
|
|
488
|
-
const user = await developerClient.createUser({ externalId });
|
|
489
|
-
await dependencies.configStore.saveProfile(
|
|
490
|
-
externalId,
|
|
491
|
-
{
|
|
492
|
-
accountId: profile.accountId,
|
|
493
|
-
apiKey: user.api_key,
|
|
494
|
-
keyType: "user",
|
|
495
|
-
userId: user.id,
|
|
496
|
-
},
|
|
497
|
-
{ activate: true },
|
|
498
|
-
);
|
|
499
|
-
writeJson(dependencies.stdout, sanitizedUserResult(user, externalId));
|
|
500
|
-
}
|
|
501
413
|
}
|
|
502
414
|
return;
|
|
503
415
|
}
|
package/src/client.d.ts
CHANGED
|
@@ -82,7 +82,7 @@ export interface LiveViewCapability {
|
|
|
82
82
|
live_view_url: string;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export interface
|
|
85
|
+
export interface AccountLoginStarted {
|
|
86
86
|
id: string;
|
|
87
87
|
account_type: "personal" | "developer";
|
|
88
88
|
email: string;
|
|
@@ -92,7 +92,7 @@ export interface AccountSignupStarted {
|
|
|
92
92
|
poll_token: string;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export interface
|
|
95
|
+
export interface VerifiedAccountLogin {
|
|
96
96
|
id: string;
|
|
97
97
|
status: "verified";
|
|
98
98
|
verified_at: string;
|
|
@@ -102,19 +102,19 @@ export interface VerifiedAccountSignup {
|
|
|
102
102
|
user?: Record<string, unknown>;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
export interface
|
|
105
|
+
export interface LoginOptions {
|
|
106
106
|
accountType: "personal" | "developer";
|
|
107
107
|
email: string;
|
|
108
108
|
pollIntervalMs?: number;
|
|
109
109
|
timeoutMs?: number;
|
|
110
|
-
persistence?:
|
|
111
|
-
onProgress?(progress:
|
|
110
|
+
persistence?: LoginPersistence;
|
|
111
|
+
onProgress?(progress: LoginProgress): void | Promise<void>;
|
|
112
112
|
onVerificationRequired?(
|
|
113
|
-
|
|
113
|
+
login: Omit<AccountLoginStarted, "poll_token">,
|
|
114
114
|
): void | Promise<void>;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
export interface
|
|
117
|
+
export interface PendingLoginState {
|
|
118
118
|
accountType: "personal" | "developer";
|
|
119
119
|
attemptId: string;
|
|
120
120
|
email: string;
|
|
@@ -122,23 +122,23 @@ export interface PendingSignupState {
|
|
|
122
122
|
idempotencyKey: string;
|
|
123
123
|
pollToken?: string;
|
|
124
124
|
recoveryKey: string;
|
|
125
|
-
|
|
125
|
+
loginId?: string;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
export interface
|
|
128
|
+
export interface LoginPersistence {
|
|
129
129
|
loadPending(input: {
|
|
130
130
|
accountType: "personal" | "developer";
|
|
131
131
|
email: string;
|
|
132
|
-
}): Promise<
|
|
133
|
-
savePending(state:
|
|
132
|
+
}): Promise<PendingLoginState | undefined>;
|
|
133
|
+
savePending(state: PendingLoginState): Promise<void>;
|
|
134
134
|
completePending(
|
|
135
|
-
state:
|
|
136
|
-
result:
|
|
135
|
+
state: PendingLoginState,
|
|
136
|
+
result: VerifiedAccountLogin,
|
|
137
137
|
): Promise<void>;
|
|
138
|
-
deletePending(state:
|
|
138
|
+
deletePending(state: PendingLoginState): Promise<void>;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
export interface
|
|
141
|
+
export interface LoginProgress {
|
|
142
142
|
state:
|
|
143
143
|
| "pending"
|
|
144
144
|
| "email_sent"
|
|
@@ -155,7 +155,7 @@ export interface SignupProgress {
|
|
|
155
155
|
key_type?: "user" | "developer";
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
export type
|
|
158
|
+
export type CompletedAccountLogin = Omit<VerifiedAccountLogin, "api_key">;
|
|
159
159
|
|
|
160
160
|
export class ShopstackApiError extends Error {
|
|
161
161
|
code: string;
|
|
@@ -165,20 +165,20 @@ export class ShopstackApiError extends Error {
|
|
|
165
165
|
|
|
166
166
|
export class ShopstackClient {
|
|
167
167
|
constructor(options?: ShopstackClientOptions);
|
|
168
|
-
|
|
168
|
+
startAccountLogin(input: {
|
|
169
169
|
accountType: "personal" | "developer";
|
|
170
170
|
email: string;
|
|
171
|
-
}): Promise<
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
}): Promise<AccountLoginStarted>;
|
|
172
|
+
pollAccountLogin(
|
|
173
|
+
loginId: string,
|
|
174
174
|
pollToken: string,
|
|
175
|
-
): Promise<Omit<
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
): Promise<Omit<AccountLoginStarted, "poll_token"> | VerifiedAccountLogin>;
|
|
176
|
+
waitForAccountLogin(
|
|
177
|
+
loginId: string,
|
|
178
178
|
pollToken: string,
|
|
179
179
|
options?: { pollIntervalMs?: number; timeoutMs?: number },
|
|
180
|
-
): Promise<
|
|
181
|
-
|
|
180
|
+
): Promise<VerifiedAccountLogin>;
|
|
181
|
+
login(input: LoginOptions): Promise<CompletedAccountLogin>;
|
|
182
182
|
createUser(input: {
|
|
183
183
|
externalId: string;
|
|
184
184
|
idempotencyKey?: string;
|
package/src/client.js
CHANGED
|
@@ -17,11 +17,11 @@ function idempotencyKey(prefix) {
|
|
|
17
17
|
return `${prefix}-${randomUUID()}`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function
|
|
21
|
-
return `
|
|
20
|
+
function loginRecoveryKey() {
|
|
21
|
+
return `login_recovery_${randomBytes(32).toString("base64url")}`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function
|
|
24
|
+
function loginAttemptKey(accountType, email) {
|
|
25
25
|
return `${accountType}\u0000${email.trim().toLowerCase()}`;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -164,7 +164,7 @@ export class ShopstackClient {
|
|
|
164
164
|
body,
|
|
165
165
|
idempotencyKey: mutationKey,
|
|
166
166
|
method = "GET",
|
|
167
|
-
|
|
167
|
+
loginRecoveryKey: recoveryKey,
|
|
168
168
|
} = {},
|
|
169
169
|
) {
|
|
170
170
|
const headers = new Headers({ Accept: "application/json" });
|
|
@@ -174,7 +174,7 @@ export class ShopstackClient {
|
|
|
174
174
|
}
|
|
175
175
|
if (body !== undefined) headers.set("Content-Type", "application/json");
|
|
176
176
|
if (mutationKey) headers.set("Idempotency-Key", mutationKey);
|
|
177
|
-
if (recoveryKey) headers.set("
|
|
177
|
+
if (recoveryKey) headers.set("Login-Recovery-Key", recoveryKey);
|
|
178
178
|
const response = await this.fetch(`${this.baseUrl}${path}`, {
|
|
179
179
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
180
180
|
headers,
|
|
@@ -208,37 +208,32 @@ export class ShopstackClient {
|
|
|
208
208
|
return payload;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
email,
|
|
214
|
-
idempotencyKey: key,
|
|
215
|
-
recoveryKey,
|
|
216
|
-
}) {
|
|
217
|
-
return this.request("/accounts", {
|
|
211
|
+
_startAccountLogin({ accountType, email, idempotencyKey: key, recoveryKey }) {
|
|
212
|
+
return this.request("/login", {
|
|
218
213
|
body: { account_type: accountType, email },
|
|
219
214
|
idempotencyKey: key,
|
|
220
215
|
method: "POST",
|
|
221
|
-
|
|
216
|
+
loginRecoveryKey: recoveryKey,
|
|
222
217
|
});
|
|
223
218
|
}
|
|
224
219
|
|
|
225
|
-
|
|
226
|
-
return this.
|
|
220
|
+
startAccountLogin({ accountType, email } = {}) {
|
|
221
|
+
return this._startAccountLogin({
|
|
227
222
|
accountType,
|
|
228
223
|
email,
|
|
229
224
|
idempotencyKey: idempotencyKey("account"),
|
|
230
|
-
recoveryKey:
|
|
225
|
+
recoveryKey: loginRecoveryKey(),
|
|
231
226
|
});
|
|
232
227
|
}
|
|
233
228
|
|
|
234
|
-
|
|
235
|
-
return this.request(`/
|
|
229
|
+
pollAccountLogin(loginId, pollToken) {
|
|
230
|
+
return this.request(`/login/${encodeURIComponent(loginId)}`, {
|
|
236
231
|
bearerToken: pollToken,
|
|
237
232
|
});
|
|
238
233
|
}
|
|
239
234
|
|
|
240
|
-
async
|
|
241
|
-
|
|
235
|
+
async waitForAccountLogin(
|
|
236
|
+
loginId,
|
|
242
237
|
pollToken,
|
|
243
238
|
{ pollIntervalMs = 2_000, timeoutMs = 15 * 60 * 1_000 } = {},
|
|
244
239
|
) {
|
|
@@ -246,16 +241,16 @@ export class ShopstackClient {
|
|
|
246
241
|
while (true) {
|
|
247
242
|
if (Date.now() - startedAt >= timeoutMs) {
|
|
248
243
|
throw new ShopstackApiError("Email verification timed out.", {
|
|
249
|
-
code: "
|
|
244
|
+
code: "login_timeout",
|
|
250
245
|
});
|
|
251
246
|
}
|
|
252
|
-
const current = await this.
|
|
247
|
+
const current = await this.pollAccountLogin(loginId, pollToken);
|
|
253
248
|
if (current.status === "verified") return current;
|
|
254
249
|
await delay(pollIntervalMs);
|
|
255
250
|
}
|
|
256
251
|
}
|
|
257
252
|
|
|
258
|
-
async
|
|
253
|
+
async login({
|
|
259
254
|
accountType,
|
|
260
255
|
email,
|
|
261
256
|
onProgress,
|
|
@@ -265,17 +260,17 @@ export class ShopstackClient {
|
|
|
265
260
|
timeoutMs = 15 * 60 * 1_000,
|
|
266
261
|
} = {}) {
|
|
267
262
|
const normalizedEmail = email.trim().toLowerCase();
|
|
268
|
-
const memoryKey =
|
|
269
|
-
this.
|
|
263
|
+
const memoryKey = loginAttemptKey(accountType, normalizedEmail);
|
|
264
|
+
this._pendingLogins ??= new Map();
|
|
270
265
|
const storage = persistence ?? {
|
|
271
266
|
completePending: async (state, result) => {
|
|
272
267
|
this.apiKey = result.api_key;
|
|
273
|
-
this.
|
|
268
|
+
this._pendingLogins.delete(memoryKey);
|
|
274
269
|
},
|
|
275
|
-
deletePending: async () => this.
|
|
276
|
-
loadPending: async () => this.
|
|
270
|
+
deletePending: async () => this._pendingLogins.delete(memoryKey),
|
|
271
|
+
loadPending: async () => this._pendingLogins.get(memoryKey),
|
|
277
272
|
savePending: async (state) => {
|
|
278
|
-
this.
|
|
273
|
+
this._pendingLogins.set(memoryKey, structuredClone(state));
|
|
279
274
|
},
|
|
280
275
|
};
|
|
281
276
|
const publish = async (state, details = {}) => {
|
|
@@ -299,7 +294,7 @@ export class ShopstackClient {
|
|
|
299
294
|
attemptId: `attempt-${randomUUID()}`,
|
|
300
295
|
email: normalizedEmail,
|
|
301
296
|
idempotencyKey: idempotencyKey("account"),
|
|
302
|
-
recoveryKey:
|
|
297
|
+
recoveryKey: loginRecoveryKey(),
|
|
303
298
|
};
|
|
304
299
|
await storage.savePending(pending);
|
|
305
300
|
await publish("pending", {
|
|
@@ -307,8 +302,8 @@ export class ShopstackClient {
|
|
|
307
302
|
email: normalizedEmail,
|
|
308
303
|
});
|
|
309
304
|
try {
|
|
310
|
-
if (!pending.
|
|
311
|
-
const started = await this.
|
|
305
|
+
if (!pending.loginId || !pending.pollToken) {
|
|
306
|
+
const started = await this._startAccountLogin({
|
|
312
307
|
accountType,
|
|
313
308
|
email: normalizedEmail,
|
|
314
309
|
idempotencyKey: pending.idempotencyKey,
|
|
@@ -318,7 +313,7 @@ export class ShopstackClient {
|
|
|
318
313
|
...pending,
|
|
319
314
|
expiresAt: started.expires_at,
|
|
320
315
|
pollToken: started.poll_token,
|
|
321
|
-
|
|
316
|
+
loginId: started.id,
|
|
322
317
|
};
|
|
323
318
|
await storage.savePending(pending);
|
|
324
319
|
if (typeof onVerificationRequired === "function") {
|
|
@@ -341,8 +336,8 @@ export class ShopstackClient {
|
|
|
341
336
|
email: normalizedEmail,
|
|
342
337
|
expires_at: pending.expiresAt,
|
|
343
338
|
});
|
|
344
|
-
const result = await this.
|
|
345
|
-
pending.
|
|
339
|
+
const result = await this.waitForAccountLogin(
|
|
340
|
+
pending.loginId,
|
|
346
341
|
pending.pollToken,
|
|
347
342
|
{ pollIntervalMs, timeoutMs },
|
|
348
343
|
);
|
|
@@ -359,7 +354,7 @@ export class ShopstackClient {
|
|
|
359
354
|
const { api_key: _credential, ...completed } = result;
|
|
360
355
|
return completed;
|
|
361
356
|
} catch (error) {
|
|
362
|
-
if (error?.code === "
|
|
357
|
+
if (error?.code === "login_expired") {
|
|
363
358
|
await storage.deletePending(pending);
|
|
364
359
|
await publish("expired", {
|
|
365
360
|
account_type: accountType,
|
|
@@ -372,7 +367,7 @@ export class ShopstackClient {
|
|
|
372
367
|
});
|
|
373
368
|
} else if (
|
|
374
369
|
error?.code === "idempotency_conflict" ||
|
|
375
|
-
error?.code === "
|
|
370
|
+
error?.code === "invalid_login_recovery"
|
|
376
371
|
) {
|
|
377
372
|
await storage.deletePending(pending);
|
|
378
373
|
await publish("conflict", {
|
package/src/config.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface ShopstackProfile {
|
|
|
5
5
|
userId?: string;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export interface
|
|
8
|
+
export interface PendingLogin {
|
|
9
9
|
accountType: "developer" | "personal";
|
|
10
10
|
attemptId?: string;
|
|
11
11
|
email: string;
|
|
@@ -15,7 +15,7 @@ export interface PendingSignup {
|
|
|
15
15
|
pollToken?: string;
|
|
16
16
|
profile: string;
|
|
17
17
|
recoveryKey?: string;
|
|
18
|
-
|
|
18
|
+
loginId?: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export class ConfigStore {
|
|
@@ -27,14 +27,14 @@ export class ConfigStore {
|
|
|
27
27
|
options?: { activate?: boolean },
|
|
28
28
|
): Promise<ShopstackProfile>;
|
|
29
29
|
useProfile(name: string): Promise<ShopstackProfile>;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
savePendingLogin(login: PendingLogin): Promise<PendingLogin>;
|
|
31
|
+
pendingLogin(loginId: string): Promise<PendingLogin | undefined>;
|
|
32
|
+
pendingLogins(): Promise<PendingLogin[]>;
|
|
33
|
+
findPendingLogin(input: {
|
|
34
34
|
accountType: "developer" | "personal";
|
|
35
35
|
email: string;
|
|
36
|
-
}): Promise<
|
|
37
|
-
|
|
36
|
+
}): Promise<PendingLogin | undefined>;
|
|
37
|
+
completePendingLogin(
|
|
38
38
|
attemptId: string,
|
|
39
39
|
profileName: string,
|
|
40
40
|
result: {
|
|
@@ -44,10 +44,10 @@ export class ConfigStore {
|
|
|
44
44
|
user?: { id?: string };
|
|
45
45
|
},
|
|
46
46
|
): Promise<ShopstackProfile>;
|
|
47
|
-
|
|
47
|
+
deletePendingLogin(loginId: string): Promise<void>;
|
|
48
48
|
load(): Promise<{
|
|
49
49
|
active: string | null;
|
|
50
|
-
|
|
50
|
+
pendingLogins: Record<string, PendingLogin>;
|
|
51
51
|
profiles: Record<string, ShopstackProfile>;
|
|
52
52
|
}>;
|
|
53
53
|
}
|
package/src/config.js
CHANGED
|
@@ -26,18 +26,21 @@ export class ConfigStore {
|
|
|
26
26
|
throw new Error("Shopstack configuration is invalid.");
|
|
27
27
|
}
|
|
28
28
|
if (
|
|
29
|
-
parsed.
|
|
30
|
-
(typeof parsed.
|
|
31
|
-
parsed.
|
|
32
|
-
Array.isArray(parsed.
|
|
29
|
+
parsed.pendingLogins !== undefined &&
|
|
30
|
+
(typeof parsed.pendingLogins !== "object" ||
|
|
31
|
+
parsed.pendingLogins === null ||
|
|
32
|
+
Array.isArray(parsed.pendingLogins))
|
|
33
33
|
) {
|
|
34
34
|
throw new Error("Shopstack configuration is invalid.");
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
return {
|
|
37
|
+
active: parsed.active ?? null,
|
|
38
|
+
pendingLogins: parsed.pendingLogins ?? {},
|
|
39
|
+
profiles: parsed.profiles,
|
|
40
|
+
};
|
|
38
41
|
} catch (error) {
|
|
39
42
|
if (error?.code === "ENOENT") {
|
|
40
|
-
return { active: null,
|
|
43
|
+
return { active: null, pendingLogins: {}, profiles: {} };
|
|
41
44
|
}
|
|
42
45
|
throw error;
|
|
43
46
|
}
|
|
@@ -82,50 +85,50 @@ export class ConfigStore {
|
|
|
82
85
|
return typeof name === "string" ? config.profiles[name] : undefined;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
async
|
|
88
|
+
async savePendingLogin(login) {
|
|
86
89
|
const config = await this.load();
|
|
87
|
-
const key =
|
|
90
|
+
const key = login.attemptId ?? login.id;
|
|
88
91
|
if (typeof key !== "string" || key.length === 0) {
|
|
89
|
-
throw new Error("Pending
|
|
92
|
+
throw new Error("Pending login identity is invalid.");
|
|
90
93
|
}
|
|
91
|
-
config.
|
|
94
|
+
config.pendingLogins[key] = { ...login };
|
|
92
95
|
await this.write(config);
|
|
93
|
-
return config.
|
|
96
|
+
return config.pendingLogins[key];
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
async
|
|
99
|
+
async pendingLogin(loginId) {
|
|
97
100
|
const config = await this.load();
|
|
98
101
|
return (
|
|
99
|
-
config.
|
|
100
|
-
Object.values(config.
|
|
101
|
-
(pending) => pending.id ===
|
|
102
|
+
config.pendingLogins[loginId] ??
|
|
103
|
+
Object.values(config.pendingLogins).find(
|
|
104
|
+
(pending) => pending.id === loginId || pending.loginId === loginId,
|
|
102
105
|
)
|
|
103
106
|
);
|
|
104
107
|
}
|
|
105
108
|
|
|
106
|
-
async
|
|
107
|
-
return Object.values((await this.load()).
|
|
109
|
+
async pendingLogins() {
|
|
110
|
+
return Object.values((await this.load()).pendingLogins);
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
async
|
|
113
|
+
async findPendingLogin({ accountType, email }) {
|
|
111
114
|
const normalizedEmail = email.trim().toLowerCase();
|
|
112
|
-
return Object.values((await this.load()).
|
|
115
|
+
return Object.values((await this.load()).pendingLogins).find(
|
|
113
116
|
(pending) =>
|
|
114
117
|
pending.accountType === accountType &&
|
|
115
118
|
pending.email.trim().toLowerCase() === normalizedEmail,
|
|
116
119
|
);
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
async
|
|
122
|
+
async completePendingLogin(attemptId, profileName, result) {
|
|
120
123
|
const config = await this.load();
|
|
121
|
-
const entry = Object.entries(config.
|
|
124
|
+
const entry = Object.entries(config.pendingLogins).find(
|
|
122
125
|
([key, pending]) =>
|
|
123
126
|
key === attemptId ||
|
|
124
127
|
pending.id === attemptId ||
|
|
125
|
-
pending.
|
|
128
|
+
pending.loginId === attemptId,
|
|
126
129
|
);
|
|
127
130
|
if (entry === undefined) {
|
|
128
|
-
throw new Error("Pending
|
|
131
|
+
throw new Error("Pending login is unavailable.");
|
|
129
132
|
}
|
|
130
133
|
config.profiles[profileName] = {
|
|
131
134
|
accountId: result.account.id,
|
|
@@ -134,20 +137,20 @@ export class ConfigStore {
|
|
|
134
137
|
...(result.user?.id === undefined ? {} : { userId: result.user.id }),
|
|
135
138
|
};
|
|
136
139
|
config.active = profileName;
|
|
137
|
-
delete config.
|
|
140
|
+
delete config.pendingLogins[entry[0]];
|
|
138
141
|
await this.write(config);
|
|
139
142
|
return config.profiles[profileName];
|
|
140
143
|
}
|
|
141
144
|
|
|
142
|
-
async
|
|
145
|
+
async deletePendingLogin(loginId) {
|
|
143
146
|
const config = await this.load();
|
|
144
|
-
const entry = Object.entries(config.
|
|
147
|
+
const entry = Object.entries(config.pendingLogins).find(
|
|
145
148
|
([key, pending]) =>
|
|
146
|
-
key ===
|
|
147
|
-
pending.id ===
|
|
148
|
-
pending.
|
|
149
|
+
key === loginId ||
|
|
150
|
+
pending.id === loginId ||
|
|
151
|
+
pending.loginId === loginId,
|
|
149
152
|
);
|
|
150
|
-
if (entry !== undefined) delete config.
|
|
153
|
+
if (entry !== undefined) delete config.pendingLogins[entry[0]];
|
|
151
154
|
await this.write(config);
|
|
152
155
|
}
|
|
153
156
|
}
|