shopstack 0.2.4 → 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 +21 -21
- package/SKILL.md +16 -10
- package/package.json +1 -1
- package/src/cli.js +134 -144
- 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.
|
|
@@ -70,7 +67,7 @@ deliberately has no card-input or payment-approval tool.
|
|
|
70
67
|
|
|
71
68
|
The canonical agent skill ships as `SKILL.md` in this package. After
|
|
72
69
|
publication it is available at
|
|
73
|
-
`https://unpkg.com/shopstack@0.2.
|
|
70
|
+
`https://unpkg.com/shopstack@0.2.5/SKILL.md` with the release-pinned package.
|
|
74
71
|
|
|
75
72
|
## Connections
|
|
76
73
|
|
|
@@ -80,7 +77,10 @@ shopstack connect link
|
|
|
80
77
|
```
|
|
81
78
|
|
|
82
79
|
Link is currently the only persistent payment provider. Connecting it is
|
|
83
|
-
optional.
|
|
80
|
+
optional. `shopstack connect link` opens the HTTPS Link setup page, displays the
|
|
81
|
+
confirmation phrase, and checks the connection every five seconds for up to
|
|
82
|
+
five minutes. It prints `Link connected` when the connection is ready. If a
|
|
83
|
+
browser cannot open automatically, use the exact URL printed in the terminal.
|
|
84
84
|
|
|
85
85
|
## Run a checkout
|
|
86
86
|
|
|
@@ -144,11 +144,11 @@ Card values are never accepted as command-line flags.
|
|
|
144
144
|
import { ShopstackClient } from "shopstack";
|
|
145
145
|
|
|
146
146
|
const onboarding = new ShopstackClient();
|
|
147
|
-
const account = await onboarding.
|
|
147
|
+
const account = await onboarding.login({
|
|
148
148
|
accountType: "developer",
|
|
149
149
|
email: "developer@example.com",
|
|
150
150
|
onProgress: ({ state }) => console.log(state),
|
|
151
|
-
persistence:
|
|
151
|
+
persistence: secureLoginPersistence,
|
|
152
152
|
});
|
|
153
153
|
|
|
154
154
|
const shopstack = new ShopstackClient({
|
|
@@ -162,8 +162,8 @@ const result = await shopstack.runCheckout(checkoutRequest, {
|
|
|
162
162
|
});
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
`
|
|
166
|
-
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
|
|
167
167
|
key, recovery key, or polling token. Pass a secure persistence adapter for
|
|
168
168
|
restart recovery and durable credential storage; never use browser storage.
|
|
169
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
|
|
@@ -69,6 +70,11 @@ shopstack connect list
|
|
|
69
70
|
shopstack connect link
|
|
70
71
|
```
|
|
71
72
|
|
|
73
|
+
The Link command opens the HTTPS setup page, displays the confirmation phrase,
|
|
74
|
+
and checks every five seconds for up to five minutes. Continue only after it
|
|
75
|
+
prints `Link connected` and reports `checkout_ready: true`. If the browser does
|
|
76
|
+
not open automatically, open the exact URL printed in the terminal.
|
|
77
|
+
|
|
72
78
|
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.
|
|
73
79
|
|
|
74
80
|
Never put card data in MCP arguments, natural-language messages, model output, logs, or ordinary CLI flags.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { createInterface } from "node:readline/promises";
|
|
3
4
|
|
|
@@ -7,6 +8,8 @@ import { ConfigStore } from "./config.js";
|
|
|
7
8
|
const VERSION = JSON.parse(
|
|
8
9
|
await readFile(new URL("../package.json", import.meta.url), "utf8"),
|
|
9
10
|
).version;
|
|
11
|
+
const LINK_POLL_ATTEMPTS = 60;
|
|
12
|
+
const LINK_POLL_INTERVAL_MS = 5_000;
|
|
10
13
|
|
|
11
14
|
const HELP = `Shopstack CLI ${VERSION}
|
|
12
15
|
|
|
@@ -14,14 +17,8 @@ Usage:
|
|
|
14
17
|
shopstack <command> [options]
|
|
15
18
|
|
|
16
19
|
Account setup:
|
|
17
|
-
shopstack
|
|
18
|
-
#
|
|
19
|
-
shopstack signup user --email EMAIL
|
|
20
|
-
# Create a Personal profile without interactive account-type prompts.
|
|
21
|
-
shopstack signup developer --email EMAIL
|
|
22
|
-
# Create a Developer management profile without interactive prompts.
|
|
23
|
-
shopstack signup resume SIGNUP_ID
|
|
24
|
-
# 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.
|
|
25
22
|
shopstack users create --external-id ID [--profile NAME]
|
|
26
23
|
# Create an independently scoped user from a Developer profile.
|
|
27
24
|
shopstack profiles list
|
|
@@ -83,15 +80,81 @@ function required(options, name) {
|
|
|
83
80
|
return value;
|
|
84
81
|
}
|
|
85
82
|
|
|
86
|
-
function isAffirmative(value) {
|
|
87
|
-
const answer = String(value).trim().toLowerCase();
|
|
88
|
-
return answer === "y" || answer === "yes";
|
|
89
|
-
}
|
|
90
|
-
|
|
91
83
|
function writeJson(stream, value) {
|
|
92
84
|
stream.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
93
85
|
}
|
|
94
86
|
|
|
87
|
+
function delay(milliseconds) {
|
|
88
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function openExternal(url) {
|
|
92
|
+
const parsed = new URL(url);
|
|
93
|
+
if (parsed.protocol !== "https:") {
|
|
94
|
+
throw new Error("Only HTTPS connection URLs can be opened.");
|
|
95
|
+
}
|
|
96
|
+
const [command, args] =
|
|
97
|
+
process.platform === "darwin"
|
|
98
|
+
? ["open", [url]]
|
|
99
|
+
: process.platform === "win32"
|
|
100
|
+
? ["cmd.exe", ["/d", "/s", "/c", "start", "", url]]
|
|
101
|
+
: ["xdg-open", [url]];
|
|
102
|
+
await new Promise((resolve, reject) => {
|
|
103
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
104
|
+
child.once("error", reject);
|
|
105
|
+
child.once("spawn", () => {
|
|
106
|
+
child.unref();
|
|
107
|
+
resolve();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function connectLink(client, dependencies) {
|
|
113
|
+
let result = await client.connect("link");
|
|
114
|
+
if (
|
|
115
|
+
result.connection_status !== "action_required" ||
|
|
116
|
+
typeof result.connect_url !== "string"
|
|
117
|
+
) {
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
dependencies.stderr.write(`Open Link: ${result.connect_url}\n`);
|
|
122
|
+
if (typeof result.phrase === "string") {
|
|
123
|
+
dependencies.stderr.write(`Confirmation phrase: ${result.phrase}\n`);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
await dependencies.openExternal(result.connect_url);
|
|
127
|
+
} catch {
|
|
128
|
+
dependencies.stderr.write(
|
|
129
|
+
"The browser could not open automatically. Open the URL shown above.\n",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
dependencies.stderr.write("Waiting for Link confirmation...\n");
|
|
133
|
+
|
|
134
|
+
for (let attempt = 0; attempt < dependencies.linkPollAttempts; attempt += 1) {
|
|
135
|
+
await dependencies.delay(dependencies.linkPollIntervalMs);
|
|
136
|
+
result = await client.connect("link");
|
|
137
|
+
if (
|
|
138
|
+
result.connection_status === "active" &&
|
|
139
|
+
result.checkout_ready === true
|
|
140
|
+
) {
|
|
141
|
+
dependencies.stderr.write("✓ Link connected\n");
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
if (
|
|
145
|
+
result.connection_status !== "action_required" &&
|
|
146
|
+
result.connection_status !== "connecting"
|
|
147
|
+
) {
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
dependencies.stderr.write(
|
|
153
|
+
"Link setup is still pending. Run `shopstack connect link` to resume.\n",
|
|
154
|
+
);
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
95
158
|
async function readJsonFile(path) {
|
|
96
159
|
return JSON.parse(await readFile(path, "utf8"));
|
|
97
160
|
}
|
|
@@ -166,7 +229,7 @@ function sanitizedUserResult(result, profile) {
|
|
|
166
229
|
return { api_key_saved: true, profile, user };
|
|
167
230
|
}
|
|
168
231
|
|
|
169
|
-
async function
|
|
232
|
+
async function saveVerifiedLogin(result, profileName, configStore) {
|
|
170
233
|
await configStore.saveProfile(
|
|
171
234
|
profileName,
|
|
172
235
|
{
|
|
@@ -179,34 +242,34 @@ async function saveVerifiedSignup(result, profileName, configStore) {
|
|
|
179
242
|
);
|
|
180
243
|
}
|
|
181
244
|
|
|
182
|
-
function
|
|
245
|
+
function loginPersistence(configStore, profileName) {
|
|
183
246
|
return {
|
|
184
247
|
async loadPending(input) {
|
|
185
|
-
return typeof configStore.
|
|
186
|
-
? configStore.
|
|
248
|
+
return typeof configStore.findPendingLogin === "function"
|
|
249
|
+
? configStore.findPendingLogin(input)
|
|
187
250
|
: undefined;
|
|
188
251
|
},
|
|
189
252
|
async savePending(state) {
|
|
190
|
-
await configStore.
|
|
253
|
+
await configStore.savePendingLogin({ ...state, profile: profileName });
|
|
191
254
|
},
|
|
192
255
|
async completePending(state, result) {
|
|
193
|
-
const pendingId = state.attemptId ?? state.
|
|
194
|
-
if (typeof configStore.
|
|
195
|
-
await configStore.
|
|
256
|
+
const pendingId = state.attemptId ?? state.loginId ?? state.id;
|
|
257
|
+
if (typeof configStore.completePendingLogin === "function") {
|
|
258
|
+
await configStore.completePendingLogin(pendingId, profileName, result);
|
|
196
259
|
return;
|
|
197
260
|
}
|
|
198
|
-
await
|
|
199
|
-
await configStore.
|
|
261
|
+
await saveVerifiedLogin(result, profileName, configStore);
|
|
262
|
+
await configStore.deletePendingLogin(pendingId);
|
|
200
263
|
},
|
|
201
264
|
async deletePending(state) {
|
|
202
|
-
await configStore.
|
|
203
|
-
state.attemptId ?? state.
|
|
265
|
+
await configStore.deletePendingLogin(
|
|
266
|
+
state.attemptId ?? state.loginId ?? state.id,
|
|
204
267
|
);
|
|
205
268
|
},
|
|
206
269
|
};
|
|
207
270
|
}
|
|
208
271
|
|
|
209
|
-
async function
|
|
272
|
+
async function reportLoginProgress(progress, stream) {
|
|
210
273
|
switch (progress.state) {
|
|
211
274
|
case "email_sent":
|
|
212
275
|
stream.write("Verification email sent.\n");
|
|
@@ -218,23 +281,21 @@ async function reportSignupProgress(progress, stream) {
|
|
|
218
281
|
stream.write("✓ Email verified\n");
|
|
219
282
|
return;
|
|
220
283
|
case "complete":
|
|
221
|
-
stream.write("✓ Shopstack
|
|
284
|
+
stream.write("✓ Shopstack login complete\n");
|
|
222
285
|
stream.write("✓ Credentials saved securely\n");
|
|
223
286
|
return;
|
|
224
287
|
case "expired":
|
|
225
|
-
stream.write("
|
|
288
|
+
stream.write("Login expired. Start again to receive a new email.\n");
|
|
226
289
|
return;
|
|
227
290
|
case "rate_limited":
|
|
228
|
-
stream.write(
|
|
229
|
-
"Signup is rate-limited. Retry after the indicated delay.\n",
|
|
230
|
-
);
|
|
291
|
+
stream.write("Login is rate-limited. Retry after the indicated delay.\n");
|
|
231
292
|
return;
|
|
232
293
|
case "conflict":
|
|
233
|
-
stream.write("
|
|
294
|
+
stream.write("Login retry state conflicted and was cleared.\n");
|
|
234
295
|
return;
|
|
235
296
|
case "retryable_failure":
|
|
236
297
|
stream.write(
|
|
237
|
-
"
|
|
298
|
+
"Login paused after a retryable failure. Run login again to resume.\n",
|
|
238
299
|
);
|
|
239
300
|
return;
|
|
240
301
|
default:
|
|
@@ -247,8 +308,7 @@ async function activeClient(dependencies, requiredKind = "user") {
|
|
|
247
308
|
const environmentKey = dependencies.env.SHOPSTACK_API_KEY;
|
|
248
309
|
const apiKey = environmentKey || profile?.apiKey;
|
|
249
310
|
const keyType = environmentKey ? requiredKind : profile?.keyType;
|
|
250
|
-
if (!apiKey)
|
|
251
|
-
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.");
|
|
252
312
|
if (requiredKind && keyType !== requiredKind) {
|
|
253
313
|
throw new Error(`This command requires an active ${requiredKind} profile.`);
|
|
254
314
|
}
|
|
@@ -266,6 +326,10 @@ export async function runCli(args, supplied = {}) {
|
|
|
266
326
|
clientFactory: (options) => new ShopstackClient(options),
|
|
267
327
|
configStore: new ConfigStore(),
|
|
268
328
|
confirm: undefined,
|
|
329
|
+
delay,
|
|
330
|
+
linkPollAttempts: LINK_POLL_ATTEMPTS,
|
|
331
|
+
linkPollIntervalMs: LINK_POLL_INTERVAL_MS,
|
|
332
|
+
openExternal,
|
|
269
333
|
prompt: undefined,
|
|
270
334
|
readJsonFile,
|
|
271
335
|
secretPrompt: undefined,
|
|
@@ -290,136 +354,62 @@ export async function runCli(args, supplied = {}) {
|
|
|
290
354
|
return;
|
|
291
355
|
}
|
|
292
356
|
|
|
293
|
-
if (group === "
|
|
294
|
-
if (action === "resume") {
|
|
295
|
-
if (rest.length !== 1) {
|
|
296
|
-
throw new Error("Use `shopstack signup resume SIGNUP_ID`.");
|
|
297
|
-
}
|
|
298
|
-
const pending = await dependencies.configStore.pendingSignup(rest[0]);
|
|
299
|
-
if (pending === undefined) {
|
|
300
|
-
throw new Error(
|
|
301
|
-
"That signup is not present in the local profile store.",
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
const client = dependencies.clientFactory({
|
|
305
|
-
baseUrl: dependencies.env.SHOPSTACK_API_URL,
|
|
306
|
-
});
|
|
307
|
-
const result = await client.signup({
|
|
308
|
-
accountType: pending.accountType,
|
|
309
|
-
email: pending.email,
|
|
310
|
-
onProgress: (progress) =>
|
|
311
|
-
reportSignupProgress(progress, dependencies.stderr),
|
|
312
|
-
persistence: signupPersistence(
|
|
313
|
-
dependencies.configStore,
|
|
314
|
-
pending.profile,
|
|
315
|
-
),
|
|
316
|
-
});
|
|
317
|
-
writeJson(dependencies.stdout, {
|
|
318
|
-
...sanitizedAccountResult(result),
|
|
319
|
-
profile: pending.profile,
|
|
320
|
-
});
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
if (action !== undefined && action !== "user" && action !== "developer") {
|
|
324
|
-
throw new Error(
|
|
325
|
-
"Use `shopstack signup user` or `shopstack signup developer`.",
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
const signupArgs = action === undefined ? [] : rest;
|
|
357
|
+
if (group === "login") {
|
|
329
358
|
const { options, positional } = parseOptions(
|
|
330
|
-
|
|
331
|
-
new Set(["email", "profile"]),
|
|
359
|
+
args.slice(1),
|
|
360
|
+
new Set(["account-type", "email", "profile"]),
|
|
332
361
|
);
|
|
333
|
-
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
|
+
}
|
|
334
371
|
const pendingCandidates =
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
? await dependencies.configStore.pendingSignups()
|
|
372
|
+
typeof dependencies.configStore.pendingLogins === "function"
|
|
373
|
+
? await dependencies.configStore.pendingLogins()
|
|
338
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
|
+
);
|
|
339
384
|
const resumable =
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
let email;
|
|
385
|
+
matchingPending.length === 1 ? matchingPending[0] : undefined;
|
|
386
|
+
const accountType = resumable?.accountType ?? requestedAccountType;
|
|
387
|
+
let email = resumable?.email ?? options.email;
|
|
343
388
|
if (resumable !== undefined) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
} else if (action === undefined) {
|
|
389
|
+
dependencies.stderr.write("Resuming pending login.\n");
|
|
390
|
+
}
|
|
391
|
+
if (email === undefined) {
|
|
348
392
|
email = String(await visiblePrompt("Email: ", dependencies)).trim();
|
|
349
|
-
if (email.length === 0) throw new Error("Email is required.");
|
|
350
|
-
const selected = String(
|
|
351
|
-
await visiblePrompt(
|
|
352
|
-
"Account type (Personal / Developer): ",
|
|
353
|
-
dependencies,
|
|
354
|
-
),
|
|
355
|
-
)
|
|
356
|
-
.trim()
|
|
357
|
-
.toLowerCase();
|
|
358
|
-
if (selected === "personal" || selected === "user") {
|
|
359
|
-
accountType = "personal";
|
|
360
|
-
} else if (selected === "developer") {
|
|
361
|
-
accountType = "developer";
|
|
362
|
-
} else {
|
|
363
|
-
throw new Error("Account type must be Personal or Developer.");
|
|
364
|
-
}
|
|
365
|
-
} else {
|
|
366
|
-
accountType = action === "user" ? "personal" : "developer";
|
|
367
|
-
email = required(options, "email");
|
|
368
393
|
}
|
|
394
|
+
if (email.length === 0) throw new Error("Email is required.");
|
|
369
395
|
const client = dependencies.clientFactory({
|
|
370
396
|
baseUrl: dependencies.env.SHOPSTACK_API_URL,
|
|
371
397
|
});
|
|
372
|
-
const
|
|
373
|
-
options.profile ??
|
|
374
|
-
resumable?.profile ??
|
|
375
|
-
(accountType === "developer" ? "developer" : "default");
|
|
376
|
-
const result = await client.signup({
|
|
398
|
+
const result = await client.login({
|
|
377
399
|
accountType,
|
|
378
400
|
email,
|
|
379
401
|
onProgress: (progress) =>
|
|
380
|
-
|
|
381
|
-
persistence:
|
|
402
|
+
reportLoginProgress(progress, dependencies.stderr),
|
|
403
|
+
persistence: loginPersistence(dependencies.configStore, profileName),
|
|
382
404
|
});
|
|
383
405
|
writeJson(dependencies.stdout, {
|
|
384
406
|
...sanitizedAccountResult(result),
|
|
385
407
|
profile: profileName,
|
|
386
408
|
});
|
|
387
|
-
if (
|
|
409
|
+
if (result.key_type === "developer") {
|
|
388
410
|
dependencies.stderr.write(
|
|
389
411
|
"The developer management credential cannot run user checkouts. Create a user profile with `shopstack users create --external-id ID`.\n",
|
|
390
412
|
);
|
|
391
|
-
if (
|
|
392
|
-
action === undefined &&
|
|
393
|
-
isAffirmative(
|
|
394
|
-
await visiblePrompt(
|
|
395
|
-
"Create the first developer-owned user now? (y/N): ",
|
|
396
|
-
dependencies,
|
|
397
|
-
),
|
|
398
|
-
)
|
|
399
|
-
) {
|
|
400
|
-
const externalId = String(
|
|
401
|
-
await visiblePrompt("User external ID: ", dependencies),
|
|
402
|
-
).trim();
|
|
403
|
-
if (externalId.length === 0) {
|
|
404
|
-
throw new Error("User external ID is required.");
|
|
405
|
-
}
|
|
406
|
-
const { client: developerClient, profile } = await activeClient(
|
|
407
|
-
dependencies,
|
|
408
|
-
"developer",
|
|
409
|
-
);
|
|
410
|
-
const user = await developerClient.createUser({ externalId });
|
|
411
|
-
await dependencies.configStore.saveProfile(
|
|
412
|
-
externalId,
|
|
413
|
-
{
|
|
414
|
-
accountId: profile.accountId,
|
|
415
|
-
apiKey: user.api_key,
|
|
416
|
-
keyType: "user",
|
|
417
|
-
userId: user.id,
|
|
418
|
-
},
|
|
419
|
-
{ activate: true },
|
|
420
|
-
);
|
|
421
|
-
writeJson(dependencies.stdout, sanitizedUserResult(user, externalId));
|
|
422
|
-
}
|
|
423
413
|
}
|
|
424
414
|
return;
|
|
425
415
|
}
|
|
@@ -474,7 +464,7 @@ export async function runCli(args, supplied = {}) {
|
|
|
474
464
|
const result =
|
|
475
465
|
action === "list"
|
|
476
466
|
? await client.listConnections()
|
|
477
|
-
: await client
|
|
467
|
+
: await connectLink(client, dependencies);
|
|
478
468
|
writeJson(dependencies.stdout, result);
|
|
479
469
|
return;
|
|
480
470
|
}
|
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
|
}
|