shopstack 0.1.0 → 0.2.1
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 +145 -10
- package/SKILL.md +128 -0
- package/bin/shopstack +8 -1
- package/package.json +28 -6
- package/src/cli.js +516 -0
- package/src/client.d.ts +220 -0
- package/src/client.js +486 -0
- package/src/config.d.ts +55 -0
- package/src/config.js +153 -0
- package/scripts/postinstall.js +0 -6
package/src/client.d.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
export interface ShopstackClientOptions {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
fetch?: typeof fetch;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface PaymentCard {
|
|
8
|
+
number: string;
|
|
9
|
+
exp_month: number;
|
|
10
|
+
exp_year: number;
|
|
11
|
+
cvc: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PaymentApproval {
|
|
15
|
+
id: string;
|
|
16
|
+
subtotal: string | null;
|
|
17
|
+
shipping: string | null;
|
|
18
|
+
tax: string | null;
|
|
19
|
+
total: string;
|
|
20
|
+
currency: string;
|
|
21
|
+
expires_at: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Checkout {
|
|
25
|
+
id: string;
|
|
26
|
+
status:
|
|
27
|
+
| "queued"
|
|
28
|
+
| "started"
|
|
29
|
+
| "help_required"
|
|
30
|
+
| "approval_required"
|
|
31
|
+
| "submitting"
|
|
32
|
+
| "complete"
|
|
33
|
+
| "failed"
|
|
34
|
+
| "cancelled";
|
|
35
|
+
revision: number;
|
|
36
|
+
activity: string;
|
|
37
|
+
intent?: {
|
|
38
|
+
name: string;
|
|
39
|
+
phase: "selected" | "completed" | "uncertain" | "failed";
|
|
40
|
+
updated_at: string;
|
|
41
|
+
};
|
|
42
|
+
item_url: string;
|
|
43
|
+
required_input?: { type: "payment_card" };
|
|
44
|
+
approval?: PaymentApproval;
|
|
45
|
+
result?: Record<string, unknown>;
|
|
46
|
+
failure?: Record<string, unknown>;
|
|
47
|
+
created_at: string;
|
|
48
|
+
updated_at: string;
|
|
49
|
+
expires_at: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RunCheckoutOptions {
|
|
53
|
+
idempotencyKey?: string;
|
|
54
|
+
pollIntervalMs?: number;
|
|
55
|
+
timeoutMs?: number;
|
|
56
|
+
onProgress?(checkout: Checkout): void | Promise<void>;
|
|
57
|
+
paymentDetails?(
|
|
58
|
+
checkout: Checkout,
|
|
59
|
+
): PaymentCard | Promise<PaymentCard | undefined> | undefined;
|
|
60
|
+
approve?(
|
|
61
|
+
approval: PaymentApproval,
|
|
62
|
+
checkout: Checkout,
|
|
63
|
+
): boolean | Promise<boolean | undefined> | undefined;
|
|
64
|
+
message?(
|
|
65
|
+
checkout: Checkout,
|
|
66
|
+
): string | Promise<string | undefined> | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface AccountSignupStarted {
|
|
70
|
+
id: string;
|
|
71
|
+
account_type: "personal" | "developer";
|
|
72
|
+
email: string;
|
|
73
|
+
status: "verification_required";
|
|
74
|
+
created_at: string;
|
|
75
|
+
expires_at: string;
|
|
76
|
+
poll_token: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface VerifiedAccountSignup {
|
|
80
|
+
id: string;
|
|
81
|
+
status: "verified";
|
|
82
|
+
verified_at: string;
|
|
83
|
+
account: Record<string, unknown>;
|
|
84
|
+
api_key: string;
|
|
85
|
+
key_type: "user" | "developer";
|
|
86
|
+
user?: Record<string, unknown>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface SignupOptions {
|
|
90
|
+
accountType: "personal" | "developer";
|
|
91
|
+
email: string;
|
|
92
|
+
pollIntervalMs?: number;
|
|
93
|
+
timeoutMs?: number;
|
|
94
|
+
persistence?: SignupPersistence;
|
|
95
|
+
onProgress?(progress: SignupProgress): void | Promise<void>;
|
|
96
|
+
onVerificationRequired?(signup: Omit<AccountSignupStarted, "poll_token">):
|
|
97
|
+
| void
|
|
98
|
+
| Promise<void>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface PendingSignupState {
|
|
102
|
+
accountType: "personal" | "developer";
|
|
103
|
+
attemptId: string;
|
|
104
|
+
email: string;
|
|
105
|
+
expiresAt?: string;
|
|
106
|
+
idempotencyKey: string;
|
|
107
|
+
pollToken?: string;
|
|
108
|
+
recoveryKey: string;
|
|
109
|
+
signupId?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface SignupPersistence {
|
|
113
|
+
loadPending(input: {
|
|
114
|
+
accountType: "personal" | "developer";
|
|
115
|
+
email: string;
|
|
116
|
+
}): Promise<PendingSignupState | undefined>;
|
|
117
|
+
savePending(state: PendingSignupState): Promise<void>;
|
|
118
|
+
completePending(
|
|
119
|
+
state: PendingSignupState,
|
|
120
|
+
result: VerifiedAccountSignup,
|
|
121
|
+
): Promise<void>;
|
|
122
|
+
deletePending(state: PendingSignupState): Promise<void>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface SignupProgress {
|
|
126
|
+
state:
|
|
127
|
+
| "pending"
|
|
128
|
+
| "email_sent"
|
|
129
|
+
| "waiting"
|
|
130
|
+
| "verified"
|
|
131
|
+
| "expired"
|
|
132
|
+
| "rate_limited"
|
|
133
|
+
| "conflict"
|
|
134
|
+
| "retryable_failure"
|
|
135
|
+
| "complete";
|
|
136
|
+
account_type?: "personal" | "developer";
|
|
137
|
+
email?: string;
|
|
138
|
+
expires_at?: string;
|
|
139
|
+
key_type?: "user" | "developer";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export type CompletedAccountSignup = Omit<VerifiedAccountSignup, "api_key">;
|
|
143
|
+
|
|
144
|
+
export class ShopstackApiError extends Error {
|
|
145
|
+
code: string;
|
|
146
|
+
requestId?: string;
|
|
147
|
+
status?: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export class ShopstackClient {
|
|
151
|
+
constructor(options?: ShopstackClientOptions);
|
|
152
|
+
startAccountSignup(input: {
|
|
153
|
+
accountType: "personal" | "developer";
|
|
154
|
+
email: string;
|
|
155
|
+
}): Promise<AccountSignupStarted>;
|
|
156
|
+
pollAccountSignup(
|
|
157
|
+
signupId: string,
|
|
158
|
+
pollToken: string,
|
|
159
|
+
): Promise<
|
|
160
|
+
Omit<AccountSignupStarted, "poll_token"> | VerifiedAccountSignup
|
|
161
|
+
>;
|
|
162
|
+
waitForAccountSignup(
|
|
163
|
+
signupId: string,
|
|
164
|
+
pollToken: string,
|
|
165
|
+
options?: { pollIntervalMs?: number; timeoutMs?: number },
|
|
166
|
+
): Promise<VerifiedAccountSignup>;
|
|
167
|
+
signup(input: SignupOptions): Promise<CompletedAccountSignup>;
|
|
168
|
+
createUser(input: {
|
|
169
|
+
externalId: string;
|
|
170
|
+
idempotencyKey?: string;
|
|
171
|
+
}): Promise<Record<string, unknown>>;
|
|
172
|
+
listUsers(): Promise<Record<string, unknown>>;
|
|
173
|
+
getUser(userId: string): Promise<Record<string, unknown>>;
|
|
174
|
+
rotateUserApiKey(
|
|
175
|
+
userId: string,
|
|
176
|
+
options?: { idempotencyKey?: string },
|
|
177
|
+
): Promise<Record<string, unknown>>;
|
|
178
|
+
updateUser(
|
|
179
|
+
userId: string,
|
|
180
|
+
status: "active" | "suspended",
|
|
181
|
+
options?: { idempotencyKey?: string },
|
|
182
|
+
): Promise<Record<string, unknown>>;
|
|
183
|
+
listConnections(): Promise<Record<string, unknown>>;
|
|
184
|
+
connect(
|
|
185
|
+
paymentProvider?: "link",
|
|
186
|
+
options?: { idempotencyKey?: string },
|
|
187
|
+
): Promise<Record<string, unknown>>;
|
|
188
|
+
createCheckout(
|
|
189
|
+
checkout: Record<string, unknown>,
|
|
190
|
+
options?: { idempotencyKey?: string },
|
|
191
|
+
): Promise<Checkout>;
|
|
192
|
+
getCheckout(checkoutId: string): Promise<Checkout>;
|
|
193
|
+
cancelCheckout(
|
|
194
|
+
checkoutId: string,
|
|
195
|
+
options?: { idempotencyKey?: string },
|
|
196
|
+
): Promise<Checkout>;
|
|
197
|
+
sendMessage(
|
|
198
|
+
checkoutId: string,
|
|
199
|
+
content: string,
|
|
200
|
+
options?: { idempotencyKey?: string },
|
|
201
|
+
): Promise<Record<string, unknown>>;
|
|
202
|
+
listMessages(checkoutId: string): Promise<Record<string, unknown>>;
|
|
203
|
+
listEvents(checkoutId: string): Promise<Record<string, unknown>>;
|
|
204
|
+
listArtifacts(checkoutId: string): Promise<Record<string, unknown>>;
|
|
205
|
+
providePaymentDetails(
|
|
206
|
+
checkoutId: string,
|
|
207
|
+
card: PaymentCard,
|
|
208
|
+
options?: { idempotencyKey?: string },
|
|
209
|
+
): Promise<Checkout>;
|
|
210
|
+
decidePaymentApproval(
|
|
211
|
+
checkoutId: string,
|
|
212
|
+
approvalId: string,
|
|
213
|
+
approved: boolean,
|
|
214
|
+
options?: { idempotencyKey?: string },
|
|
215
|
+
): Promise<Checkout>;
|
|
216
|
+
runCheckout(
|
|
217
|
+
checkout: Record<string, unknown>,
|
|
218
|
+
options?: RunCheckoutOptions,
|
|
219
|
+
): Promise<Checkout>;
|
|
220
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const TERMINAL_STATUSES = new Set(["complete", "failed", "cancelled"]);
|
|
4
|
+
|
|
5
|
+
export class ShopstackApiError extends Error {
|
|
6
|
+
constructor(message, { code = "api_error", requestId, status } = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "ShopstackApiError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.requestId = requestId;
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function idempotencyKey(prefix) {
|
|
16
|
+
return `${prefix}-${randomUUID()}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function signupRecoveryKey() {
|
|
20
|
+
return `signup_recovery_${randomBytes(32).toString("base64url")}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function signupAttemptKey(accountType, email) {
|
|
24
|
+
return `${accountType}\u0000${email.trim().toLowerCase()}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function delay(milliseconds) {
|
|
28
|
+
return milliseconds <= 0
|
|
29
|
+
? Promise.resolve()
|
|
30
|
+
: new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class ShopstackClient {
|
|
34
|
+
constructor({
|
|
35
|
+
apiKey,
|
|
36
|
+
baseUrl = "https://shopstack-staging.shopstack.workers.dev/v1",
|
|
37
|
+
fetch: fetchImplementation = globalThis.fetch,
|
|
38
|
+
} = {}) {
|
|
39
|
+
if (typeof baseUrl !== "string" || !/^https?:\/\//u.test(baseUrl)) {
|
|
40
|
+
throw new TypeError("baseUrl must be an HTTP(S) URL");
|
|
41
|
+
}
|
|
42
|
+
if (typeof fetchImplementation !== "function") {
|
|
43
|
+
throw new TypeError("a fetch implementation is required");
|
|
44
|
+
}
|
|
45
|
+
this.apiKey = apiKey;
|
|
46
|
+
this.baseUrl = baseUrl.replace(/\/+$/u, "");
|
|
47
|
+
this.fetch = fetchImplementation;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async request(
|
|
51
|
+
path,
|
|
52
|
+
{
|
|
53
|
+
bearerToken,
|
|
54
|
+
body,
|
|
55
|
+
idempotencyKey: mutationKey,
|
|
56
|
+
method = "GET",
|
|
57
|
+
signupRecoveryKey: recoveryKey,
|
|
58
|
+
} = {},
|
|
59
|
+
) {
|
|
60
|
+
const headers = new Headers({ Accept: "application/json" });
|
|
61
|
+
const authorization = bearerToken ?? this.apiKey;
|
|
62
|
+
if (authorization) {
|
|
63
|
+
headers.set("Authorization", `Bearer ${authorization}`);
|
|
64
|
+
}
|
|
65
|
+
if (body !== undefined) headers.set("Content-Type", "application/json");
|
|
66
|
+
if (mutationKey) headers.set("Idempotency-Key", mutationKey);
|
|
67
|
+
if (recoveryKey) headers.set("Signup-Recovery-Key", recoveryKey);
|
|
68
|
+
const response = await this.fetch(`${this.baseUrl}${path}`, {
|
|
69
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
70
|
+
headers,
|
|
71
|
+
method,
|
|
72
|
+
});
|
|
73
|
+
const raw = await response.text();
|
|
74
|
+
let payload;
|
|
75
|
+
try {
|
|
76
|
+
payload = raw ? JSON.parse(raw) : undefined;
|
|
77
|
+
} catch {
|
|
78
|
+
throw new ShopstackApiError("Shopstack returned invalid JSON.", {
|
|
79
|
+
status: response.status,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
const error = payload?.error;
|
|
84
|
+
throw new ShopstackApiError(
|
|
85
|
+
typeof error?.message === "string"
|
|
86
|
+
? error.message
|
|
87
|
+
: `Shopstack request failed with status ${response.status}.`,
|
|
88
|
+
{
|
|
89
|
+
code: typeof error?.code === "string" ? error.code : "api_error",
|
|
90
|
+
requestId:
|
|
91
|
+
typeof error?.request_id === "string"
|
|
92
|
+
? error.request_id
|
|
93
|
+
: undefined,
|
|
94
|
+
status: response.status,
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return payload;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_startAccountSignup({ accountType, email, idempotencyKey: key, recoveryKey }) {
|
|
102
|
+
return this.request("/accounts", {
|
|
103
|
+
body: { account_type: accountType, email },
|
|
104
|
+
idempotencyKey: key,
|
|
105
|
+
method: "POST",
|
|
106
|
+
signupRecoveryKey: recoveryKey,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
startAccountSignup({ accountType, email } = {}) {
|
|
111
|
+
return this._startAccountSignup({
|
|
112
|
+
accountType,
|
|
113
|
+
email,
|
|
114
|
+
idempotencyKey: idempotencyKey("account"),
|
|
115
|
+
recoveryKey: signupRecoveryKey(),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
pollAccountSignup(signupId, pollToken) {
|
|
120
|
+
return this.request(`/accounts/${encodeURIComponent(signupId)}`, {
|
|
121
|
+
bearerToken: pollToken,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async waitForAccountSignup(
|
|
126
|
+
signupId,
|
|
127
|
+
pollToken,
|
|
128
|
+
{ pollIntervalMs = 2_000, timeoutMs = 15 * 60 * 1_000 } = {},
|
|
129
|
+
) {
|
|
130
|
+
const startedAt = Date.now();
|
|
131
|
+
while (true) {
|
|
132
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
133
|
+
throw new ShopstackApiError("Email verification timed out.", {
|
|
134
|
+
code: "signup_timeout",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const current = await this.pollAccountSignup(signupId, pollToken);
|
|
138
|
+
if (current.status === "verified") return current;
|
|
139
|
+
await delay(pollIntervalMs);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async signup({
|
|
144
|
+
accountType,
|
|
145
|
+
email,
|
|
146
|
+
onProgress,
|
|
147
|
+
onVerificationRequired,
|
|
148
|
+
persistence,
|
|
149
|
+
pollIntervalMs = 2_000,
|
|
150
|
+
timeoutMs = 15 * 60 * 1_000,
|
|
151
|
+
} = {}) {
|
|
152
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
153
|
+
const memoryKey = signupAttemptKey(accountType, normalizedEmail);
|
|
154
|
+
this._pendingSignups ??= new Map();
|
|
155
|
+
const storage = persistence ?? {
|
|
156
|
+
completePending: async (state, result) => {
|
|
157
|
+
this.apiKey = result.api_key;
|
|
158
|
+
this._pendingSignups.delete(memoryKey);
|
|
159
|
+
},
|
|
160
|
+
deletePending: async () => this._pendingSignups.delete(memoryKey),
|
|
161
|
+
loadPending: async () => this._pendingSignups.get(memoryKey),
|
|
162
|
+
savePending: async (state) => {
|
|
163
|
+
this._pendingSignups.set(memoryKey, structuredClone(state));
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
const publish = async (state, details = {}) => {
|
|
167
|
+
if (typeof onProgress === "function") await onProgress({ state, ...details });
|
|
168
|
+
};
|
|
169
|
+
let pending = await storage.loadPending({
|
|
170
|
+
accountType,
|
|
171
|
+
email: normalizedEmail,
|
|
172
|
+
});
|
|
173
|
+
if (
|
|
174
|
+
pending?.expiresAt &&
|
|
175
|
+
Date.parse(pending.expiresAt) <= Date.now()
|
|
176
|
+
) {
|
|
177
|
+
await storage.deletePending(pending);
|
|
178
|
+
await publish("expired", {
|
|
179
|
+
account_type: accountType,
|
|
180
|
+
email: normalizedEmail,
|
|
181
|
+
});
|
|
182
|
+
pending = undefined;
|
|
183
|
+
}
|
|
184
|
+
pending ??= {
|
|
185
|
+
accountType,
|
|
186
|
+
attemptId: `attempt-${randomUUID()}`,
|
|
187
|
+
email: normalizedEmail,
|
|
188
|
+
idempotencyKey: idempotencyKey("account"),
|
|
189
|
+
recoveryKey: signupRecoveryKey(),
|
|
190
|
+
};
|
|
191
|
+
await storage.savePending(pending);
|
|
192
|
+
await publish("pending", {
|
|
193
|
+
account_type: accountType,
|
|
194
|
+
email: normalizedEmail,
|
|
195
|
+
});
|
|
196
|
+
try {
|
|
197
|
+
if (!pending.signupId || !pending.pollToken) {
|
|
198
|
+
const started = await this._startAccountSignup({
|
|
199
|
+
accountType,
|
|
200
|
+
email: normalizedEmail,
|
|
201
|
+
idempotencyKey: pending.idempotencyKey,
|
|
202
|
+
recoveryKey: pending.recoveryKey,
|
|
203
|
+
});
|
|
204
|
+
pending = {
|
|
205
|
+
...pending,
|
|
206
|
+
expiresAt: started.expires_at,
|
|
207
|
+
pollToken: started.poll_token,
|
|
208
|
+
signupId: started.id,
|
|
209
|
+
};
|
|
210
|
+
await storage.savePending(pending);
|
|
211
|
+
if (typeof onVerificationRequired === "function") {
|
|
212
|
+
await onVerificationRequired({
|
|
213
|
+
account_type: started.account_type,
|
|
214
|
+
email: started.email,
|
|
215
|
+
expires_at: started.expires_at,
|
|
216
|
+
id: started.id,
|
|
217
|
+
status: started.status,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
await publish("email_sent", {
|
|
221
|
+
account_type: accountType,
|
|
222
|
+
email: normalizedEmail,
|
|
223
|
+
expires_at: started.expires_at,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
await publish("waiting", {
|
|
227
|
+
account_type: accountType,
|
|
228
|
+
email: normalizedEmail,
|
|
229
|
+
expires_at: pending.expiresAt,
|
|
230
|
+
});
|
|
231
|
+
const result = await this.waitForAccountSignup(
|
|
232
|
+
pending.signupId,
|
|
233
|
+
pending.pollToken,
|
|
234
|
+
{ pollIntervalMs, timeoutMs },
|
|
235
|
+
);
|
|
236
|
+
await storage.completePending(pending, result);
|
|
237
|
+
await publish("verified", {
|
|
238
|
+
account_type: accountType,
|
|
239
|
+
email: normalizedEmail,
|
|
240
|
+
});
|
|
241
|
+
await publish("complete", {
|
|
242
|
+
account_type: accountType,
|
|
243
|
+
email: normalizedEmail,
|
|
244
|
+
key_type: result.key_type,
|
|
245
|
+
});
|
|
246
|
+
const { api_key: _credential, ...completed } = result;
|
|
247
|
+
return completed;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (error?.code === "signup_expired") {
|
|
250
|
+
await storage.deletePending(pending);
|
|
251
|
+
await publish("expired", {
|
|
252
|
+
account_type: accountType,
|
|
253
|
+
email: normalizedEmail,
|
|
254
|
+
});
|
|
255
|
+
} else if (error?.code === "rate_limited") {
|
|
256
|
+
await publish("rate_limited", {
|
|
257
|
+
account_type: accountType,
|
|
258
|
+
email: normalizedEmail,
|
|
259
|
+
});
|
|
260
|
+
} else if (
|
|
261
|
+
error?.code === "idempotency_conflict" ||
|
|
262
|
+
error?.code === "invalid_signup_recovery"
|
|
263
|
+
) {
|
|
264
|
+
await storage.deletePending(pending);
|
|
265
|
+
await publish("conflict", {
|
|
266
|
+
account_type: accountType,
|
|
267
|
+
email: normalizedEmail,
|
|
268
|
+
});
|
|
269
|
+
} else {
|
|
270
|
+
await publish("retryable_failure", {
|
|
271
|
+
account_type: accountType,
|
|
272
|
+
email: normalizedEmail,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
createUser({ externalId, idempotencyKey: key } = {}) {
|
|
280
|
+
return this.request("/users", {
|
|
281
|
+
body: { external_id: externalId },
|
|
282
|
+
idempotencyKey: key ?? idempotencyKey("user"),
|
|
283
|
+
method: "POST",
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
listUsers() {
|
|
288
|
+
return this.request("/users");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
getUser(userId) {
|
|
292
|
+
return this.request(`/users/${encodeURIComponent(userId)}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
rotateUserApiKey(userId, { idempotencyKey: key } = {}) {
|
|
296
|
+
return this.request(`/users/${encodeURIComponent(userId)}/api-key/rotate`, {
|
|
297
|
+
idempotencyKey: key ?? idempotencyKey("rotate"),
|
|
298
|
+
method: "POST",
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
updateUser(userId, status, { idempotencyKey: key } = {}) {
|
|
303
|
+
return this.request(`/users/${encodeURIComponent(userId)}`, {
|
|
304
|
+
body: { status },
|
|
305
|
+
idempotencyKey: key ?? idempotencyKey("user-update"),
|
|
306
|
+
method: "PATCH",
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
listConnections() {
|
|
311
|
+
return this.request("/connect");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
connect(paymentProvider = "link", { idempotencyKey: key } = {}) {
|
|
315
|
+
return this.request("/connect", {
|
|
316
|
+
body: { payment_provider: paymentProvider },
|
|
317
|
+
idempotencyKey: key ?? idempotencyKey("connect"),
|
|
318
|
+
method: "POST",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
createCheckout(checkout, { idempotencyKey: key } = {}) {
|
|
323
|
+
return this.request("/checkout", {
|
|
324
|
+
body: checkout,
|
|
325
|
+
idempotencyKey: key ?? idempotencyKey("checkout"),
|
|
326
|
+
method: "POST",
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
getCheckout(checkoutId) {
|
|
331
|
+
return this.request(`/checkout/${encodeURIComponent(checkoutId)}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
cancelCheckout(checkoutId, { idempotencyKey: key } = {}) {
|
|
335
|
+
return this.request(`/checkout/${encodeURIComponent(checkoutId)}/cancel`, {
|
|
336
|
+
idempotencyKey: key ?? idempotencyKey("cancel"),
|
|
337
|
+
method: "POST",
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
sendMessage(checkoutId, content, { idempotencyKey: key } = {}) {
|
|
342
|
+
return this.request(
|
|
343
|
+
`/checkout/${encodeURIComponent(checkoutId)}/messages`,
|
|
344
|
+
{
|
|
345
|
+
body: { content },
|
|
346
|
+
idempotencyKey: key ?? idempotencyKey("message"),
|
|
347
|
+
method: "POST",
|
|
348
|
+
},
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
listMessages(checkoutId) {
|
|
353
|
+
return this.request(`/checkout/${encodeURIComponent(checkoutId)}/messages`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
listEvents(checkoutId) {
|
|
357
|
+
return this.request(`/checkout/${encodeURIComponent(checkoutId)}/events`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
listArtifacts(checkoutId) {
|
|
361
|
+
return this.request(
|
|
362
|
+
`/checkout/${encodeURIComponent(checkoutId)}/artifacts`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
providePaymentDetails(checkoutId, card, { idempotencyKey: key } = {}) {
|
|
367
|
+
return this.request(
|
|
368
|
+
`/checkout/${encodeURIComponent(checkoutId)}/payment-details`,
|
|
369
|
+
{
|
|
370
|
+
body: { card },
|
|
371
|
+
idempotencyKey: key ?? idempotencyKey("payment-details"),
|
|
372
|
+
method: "POST",
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
decidePaymentApproval(
|
|
378
|
+
checkoutId,
|
|
379
|
+
approvalId,
|
|
380
|
+
approved,
|
|
381
|
+
{ idempotencyKey: key } = {},
|
|
382
|
+
) {
|
|
383
|
+
return this.request(
|
|
384
|
+
`/checkout/${encodeURIComponent(checkoutId)}/payment-approval`,
|
|
385
|
+
{
|
|
386
|
+
body: { approval_id: approvalId, approved },
|
|
387
|
+
idempotencyKey: key ?? idempotencyKey("approval"),
|
|
388
|
+
method: "POST",
|
|
389
|
+
},
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async runCheckout(checkoutRequest, options = {}) {
|
|
394
|
+
const timeoutMs = options.timeoutMs ?? 30 * 60 * 1_000;
|
|
395
|
+
const startedAt = Date.now();
|
|
396
|
+
let checkout = await this.createCheckout(checkoutRequest, {
|
|
397
|
+
idempotencyKey: options.idempotencyKey,
|
|
398
|
+
});
|
|
399
|
+
let progressFingerprint;
|
|
400
|
+
const publishProgress = async (current) => {
|
|
401
|
+
if (typeof options.onProgress !== "function") return;
|
|
402
|
+
const fingerprint = JSON.stringify([
|
|
403
|
+
current.status,
|
|
404
|
+
current.revision,
|
|
405
|
+
current.activity,
|
|
406
|
+
current.intent?.name,
|
|
407
|
+
current.intent?.phase,
|
|
408
|
+
current.intent?.updated_at,
|
|
409
|
+
current.required_input?.type,
|
|
410
|
+
current.approval?.id,
|
|
411
|
+
]);
|
|
412
|
+
if (fingerprint === progressFingerprint) return;
|
|
413
|
+
progressFingerprint = fingerprint;
|
|
414
|
+
await options.onProgress(current);
|
|
415
|
+
};
|
|
416
|
+
await publishProgress(checkout);
|
|
417
|
+
let handledPaymentRevision;
|
|
418
|
+
let handledApprovalId;
|
|
419
|
+
let handledMessageRevision;
|
|
420
|
+
while (!TERMINAL_STATUSES.has(checkout.status)) {
|
|
421
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
422
|
+
throw new ShopstackApiError("Checkout monitoring timed out.", {
|
|
423
|
+
code: "checkout_timeout",
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
checkout = await this.getCheckout(checkout.id);
|
|
427
|
+
await publishProgress(checkout);
|
|
428
|
+
if (
|
|
429
|
+
checkout.required_input?.type === "payment_card" &&
|
|
430
|
+
handledPaymentRevision !== checkout.revision
|
|
431
|
+
) {
|
|
432
|
+
if (typeof options.paymentDetails !== "function") return checkout;
|
|
433
|
+
handledPaymentRevision = checkout.revision;
|
|
434
|
+
const card = await options.paymentDetails(checkout);
|
|
435
|
+
if (card === undefined) return checkout;
|
|
436
|
+
await this.providePaymentDetails(checkout.id, card, {
|
|
437
|
+
idempotencyKey: `payment-details-${checkout.id}-${checkout.revision}`,
|
|
438
|
+
});
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (
|
|
442
|
+
checkout.status === "approval_required" &&
|
|
443
|
+
checkout.approval &&
|
|
444
|
+
handledApprovalId !== checkout.approval.id
|
|
445
|
+
) {
|
|
446
|
+
if (typeof options.approve !== "function") return checkout;
|
|
447
|
+
handledApprovalId = checkout.approval.id;
|
|
448
|
+
const approved = await options.approve(checkout.approval, checkout);
|
|
449
|
+
if (typeof approved !== "boolean") return checkout;
|
|
450
|
+
await this.decidePaymentApproval(
|
|
451
|
+
checkout.id,
|
|
452
|
+
checkout.approval.id,
|
|
453
|
+
approved,
|
|
454
|
+
{ idempotencyKey: `approval-${checkout.approval.id}` },
|
|
455
|
+
);
|
|
456
|
+
if (!approved) return this.getCheckout(checkout.id);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (
|
|
460
|
+
checkout.status === "help_required" &&
|
|
461
|
+
checkout.required_input === undefined &&
|
|
462
|
+
handledMessageRevision !== checkout.revision
|
|
463
|
+
) {
|
|
464
|
+
if (typeof options.message !== "function") return checkout;
|
|
465
|
+
handledMessageRevision = checkout.revision;
|
|
466
|
+
const content = await options.message(checkout);
|
|
467
|
+
if (typeof content !== "string" || content.length === 0)
|
|
468
|
+
return checkout;
|
|
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);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return checkout;
|
|
485
|
+
}
|
|
486
|
+
}
|