@modainteract/moda-interact-shared 5.1.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-PZZJFC2L.js +325 -0
- package/dist/chunk-PZZJFC2L.js.map +1 -0
- package/dist/keyring-BLXjNChq.d.ts +10 -0
- package/dist/security/credentials/node.d.ts +3 -10
- package/dist/security/credentials/node.js +11 -312
- package/dist/security/credentials/node.js.map +1 -1
- package/dist/shopify/node.d.ts +301 -1
- package/dist/shopify/node.js +480 -1
- package/dist/shopify/node.js.map +1 -1
- package/package.json +1 -1
package/dist/shopify/node.js
CHANGED
|
@@ -1,10 +1,485 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decryptCredential
|
|
3
|
+
} from "../chunk-PZZJFC2L.js";
|
|
1
4
|
import {
|
|
2
5
|
ShopifyWhatsAppConsentSyncJobV1Schema
|
|
3
6
|
} from "../chunk-G7FCWVBU.js";
|
|
4
7
|
import "../chunk-QG53EVA6.js";
|
|
8
|
+
import "../chunk-FTUTJH7W.js";
|
|
9
|
+
import "../chunk-AUS3WQIW.js";
|
|
5
10
|
|
|
6
11
|
// src/shopify/node.ts
|
|
7
12
|
import { createHash } from "crypto";
|
|
13
|
+
|
|
14
|
+
// src/shopify/offline-token-provider.ts
|
|
15
|
+
import { Buffer } from "buffer";
|
|
16
|
+
import { randomUUID } from "crypto";
|
|
17
|
+
import { performance } from "perf_hooks";
|
|
18
|
+
var ERROR_MESSAGES = {
|
|
19
|
+
SESSION_MISSING: "Shopify offline session is unavailable.",
|
|
20
|
+
SESSION_IDENTITY_CONFLICT: "Shopify offline session identity is inconsistent.",
|
|
21
|
+
INVALID_SESSION: "Shopify offline session is invalid.",
|
|
22
|
+
REFRESH_TOKEN_EXPIRED: "Shopify offline refresh token has expired.",
|
|
23
|
+
OAUTH_CONFIG_UNAVAILABLE: "Shopify OAuth configuration is unavailable.",
|
|
24
|
+
LOCK_TIMEOUT: "Shopify offline session is busy; retry the request.",
|
|
25
|
+
CANCELLED: "Shopify offline session request was cancelled.",
|
|
26
|
+
DEADLINE: "Shopify offline session operation exceeded its time limit.",
|
|
27
|
+
REFRESH_REJECTED: "Shopify rejected the offline refresh request.",
|
|
28
|
+
REFRESH_RESPONSE_INVALID: "Shopify returned an invalid offline refresh response.",
|
|
29
|
+
REFRESH_OUTCOME_UNKNOWN: "Shopify refresh outcome is unknown; retry in a later request.",
|
|
30
|
+
PERSISTENCE_UNAVAILABLE: "Shopify offline session could not be persisted."
|
|
31
|
+
};
|
|
32
|
+
var RETRYABLE = /* @__PURE__ */ new Set([
|
|
33
|
+
"LOCK_TIMEOUT",
|
|
34
|
+
"DEADLINE",
|
|
35
|
+
"PERSISTENCE_UNAVAILABLE",
|
|
36
|
+
"REFRESH_OUTCOME_UNKNOWN"
|
|
37
|
+
]);
|
|
38
|
+
var ShopifyOfflineTokenError = class extends Error {
|
|
39
|
+
code;
|
|
40
|
+
retryable;
|
|
41
|
+
constructor(code) {
|
|
42
|
+
super(ERROR_MESSAGES[code]);
|
|
43
|
+
this.name = "ShopifyOfflineTokenError";
|
|
44
|
+
this.code = code;
|
|
45
|
+
this.retryable = RETRYABLE.has(code);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var CallbackFailure = class extends Error {
|
|
49
|
+
constructor(value) {
|
|
50
|
+
super("Shopify offline callback failed.");
|
|
51
|
+
this.value = value;
|
|
52
|
+
}
|
|
53
|
+
value;
|
|
54
|
+
};
|
|
55
|
+
var TX_OPTIONS = { maxWait: 5e3, timeout: 2e4, isolationLevel: "ReadCommitted" };
|
|
56
|
+
var ACCESS_REFRESH_SKEW_MS = 32e4;
|
|
57
|
+
var REFRESH_NETWORK_BUDGET_MS = 8e3;
|
|
58
|
+
var OPERATION_BUDGET_MS = 15e3;
|
|
59
|
+
var RESPONSE_LIMIT = 16384;
|
|
60
|
+
var CONTROL = /[\u0000-\u001f\u007f]/;
|
|
61
|
+
var SESSION_SELECT = { id: true, shop: true, isOnline: true, accessToken: true, refreshToken: true, scope: true, expires: true, refreshTokenExpires: true };
|
|
62
|
+
function fail(code) {
|
|
63
|
+
throw new ShopifyOfflineTokenError(code);
|
|
64
|
+
}
|
|
65
|
+
function isRecord(value) {
|
|
66
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
function validToken(value) {
|
|
69
|
+
return typeof value === "string" && value.length > 0 && value.length <= 4096 && value.trim() === value && !/[\r\n\u0000]/.test(value);
|
|
70
|
+
}
|
|
71
|
+
function validScope(value) {
|
|
72
|
+
return typeof value === "string" && value.length <= 4096 && !CONTROL.test(value);
|
|
73
|
+
}
|
|
74
|
+
function validDate(value) {
|
|
75
|
+
return value === null || value instanceof Date && Number.isFinite(value.getTime());
|
|
76
|
+
}
|
|
77
|
+
function mapDatabaseError(error) {
|
|
78
|
+
const code = isRecord(error) && typeof error.code === "string" ? error.code : "";
|
|
79
|
+
const message = error instanceof Error ? error.message : "";
|
|
80
|
+
if (code === "55P03" || /lock timeout/i.test(message)) return new ShopifyOfflineTokenError("LOCK_TIMEOUT");
|
|
81
|
+
if (code === "57014" || /statement timeout|statement timed out/i.test(message)) return new ShopifyOfflineTokenError("DEADLINE");
|
|
82
|
+
return new ShopifyOfflineTokenError("PERSISTENCE_UNAVAILABLE");
|
|
83
|
+
}
|
|
84
|
+
function createDeadline(signal) {
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
let expired = false;
|
|
87
|
+
const timer = setTimeout(() => {
|
|
88
|
+
expired = true;
|
|
89
|
+
controller.abort();
|
|
90
|
+
}, OPERATION_BUDGET_MS);
|
|
91
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
92
|
+
if (signal?.aborted) onAbort();
|
|
93
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
94
|
+
return {
|
|
95
|
+
signal: controller.signal,
|
|
96
|
+
dispose() {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
signal?.removeEventListener("abort", onAbort);
|
|
99
|
+
},
|
|
100
|
+
wait(work) {
|
|
101
|
+
if (controller.signal.aborted) this.check();
|
|
102
|
+
let onDeadline;
|
|
103
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
104
|
+
onDeadline = () => reject(new Error("operation aborted"));
|
|
105
|
+
controller.signal.addEventListener("abort", onDeadline, { once: true });
|
|
106
|
+
});
|
|
107
|
+
return Promise.race([work, aborted]).finally(() => {
|
|
108
|
+
if (onDeadline) controller.signal.removeEventListener("abort", onDeadline);
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
check() {
|
|
112
|
+
if (signal?.aborted) fail("CANCELLED");
|
|
113
|
+
if (expired) fail("DEADLINE");
|
|
114
|
+
if (controller.signal.aborted) fail("DEADLINE");
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function readBoundedBody(response, signal) {
|
|
119
|
+
if (!response.body) return "";
|
|
120
|
+
const reader = response.body.getReader();
|
|
121
|
+
const chunks = [];
|
|
122
|
+
let size = 0;
|
|
123
|
+
try {
|
|
124
|
+
while (true) {
|
|
125
|
+
if (signal.aborted) throw new Error("refresh stream aborted");
|
|
126
|
+
const { done, value } = await reader.read();
|
|
127
|
+
if (done) break;
|
|
128
|
+
size += value.byteLength;
|
|
129
|
+
if (size > RESPONSE_LIMIT) {
|
|
130
|
+
await reader.cancel().catch(() => void 0);
|
|
131
|
+
fail("REFRESH_OUTCOME_UNKNOWN");
|
|
132
|
+
}
|
|
133
|
+
chunks.push(value);
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
reader.releaseLock();
|
|
137
|
+
}
|
|
138
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
139
|
+
}
|
|
140
|
+
function snapshot(row, shopId, shop) {
|
|
141
|
+
if (!validToken(row.accessToken) || !validScope(row.scope) && row.scope !== null || !validDate(row.expires) || !validDate(row.refreshTokenExpires)) fail("INVALID_SESSION");
|
|
142
|
+
return Object.freeze({ id: row.id, shopId, shop, accessToken: row.accessToken, scope: row.scope, expires: row.expires, refreshTokenExpires: row.refreshTokenExpires });
|
|
143
|
+
}
|
|
144
|
+
function createShopifyOfflineTokenProvider(input) {
|
|
145
|
+
const now = input.now ?? Date.now;
|
|
146
|
+
function notify(event) {
|
|
147
|
+
try {
|
|
148
|
+
void Promise.resolve(input.onEvent?.(Object.freeze(event))).catch(() => void 0);
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function resolveShop(shopId, deadline) {
|
|
153
|
+
deadline.check();
|
|
154
|
+
if (typeof shopId !== "string" || !shopId.trim()) fail("SESSION_MISSING");
|
|
155
|
+
let shop;
|
|
156
|
+
try {
|
|
157
|
+
shop = await deadline.wait(input.prisma.shop.findUnique({ where: { id: shopId }, select: { id: true, domain: true, platform: true, status: true } }));
|
|
158
|
+
} catch (error) {
|
|
159
|
+
deadline.check();
|
|
160
|
+
throw mapDatabaseError(error);
|
|
161
|
+
}
|
|
162
|
+
deadline.check();
|
|
163
|
+
if (!shop) fail("SESSION_MISSING");
|
|
164
|
+
if (shop.platform !== "SHOPIFY" || shop.status !== "ACTIVE") fail("INVALID_SESSION");
|
|
165
|
+
if (!shop.domain) fail("SESSION_IDENTITY_CONFLICT");
|
|
166
|
+
return shop;
|
|
167
|
+
}
|
|
168
|
+
async function runLocked(shop, shopId, deadline, work, outcome = { remoteSucceeded: false }) {
|
|
169
|
+
try {
|
|
170
|
+
deadline.check();
|
|
171
|
+
const result = await deadline.wait(input.prisma.$transaction(async (tx) => {
|
|
172
|
+
await tx.$executeRawUnsafe("SET LOCAL lock_timeout = '5s'");
|
|
173
|
+
await tx.$executeRawUnsafe("SET LOCAL statement_timeout = '18s'");
|
|
174
|
+
await tx.$queryRawUnsafe("SELECT pg_advisory_xact_lock(hashtextextended($1, 0)) IS NULL", `moda:shopify-offline:v1:${shop.domain}`);
|
|
175
|
+
deadline.check();
|
|
176
|
+
const currentShop = await tx.shop.findUnique({ where: { id: shopId }, select: { id: true, domain: true, platform: true, status: true } });
|
|
177
|
+
if (!currentShop) fail("SESSION_MISSING");
|
|
178
|
+
if (currentShop.platform !== "SHOPIFY" || currentShop.status !== "ACTIVE") fail("INVALID_SESSION");
|
|
179
|
+
if (currentShop.domain !== shop.domain) fail("SESSION_IDENTITY_CONFLICT");
|
|
180
|
+
const sessions = await tx.session.findMany({ where: { shop: currentShop.domain, isOnline: false }, select: SESSION_SELECT, take: 2 });
|
|
181
|
+
if (sessions.length === 0) fail("SESSION_MISSING");
|
|
182
|
+
if (sessions.length !== 1) fail("SESSION_IDENTITY_CONFLICT");
|
|
183
|
+
const row = sessions[0];
|
|
184
|
+
if (!row.id || row.shop !== currentShop.domain || row.isOnline !== false) fail("SESSION_IDENTITY_CONFLICT");
|
|
185
|
+
await tx.$queryRawUnsafe('SELECT "id" FROM "shopify"."Session" WHERE "id" = $1 FOR UPDATE', row.id);
|
|
186
|
+
const reread = await tx.session.findUnique({ where: { id: row.id }, select: SESSION_SELECT });
|
|
187
|
+
if (!reread || reread.id !== row.id || reread.shop !== currentShop.domain || reread.isOnline !== false) fail("SESSION_IDENTITY_CONFLICT");
|
|
188
|
+
const context = Object.freeze({ transaction: tx, shopId, shop: currentShop.domain, sessionId: reread.id, signal: deadline.signal });
|
|
189
|
+
deadline.check();
|
|
190
|
+
let value;
|
|
191
|
+
try {
|
|
192
|
+
value = await deadline.wait(work(context));
|
|
193
|
+
} catch (error) {
|
|
194
|
+
deadline.check();
|
|
195
|
+
throw new CallbackFailure(error);
|
|
196
|
+
}
|
|
197
|
+
deadline.check();
|
|
198
|
+
return value;
|
|
199
|
+
}, TX_OPTIONS));
|
|
200
|
+
return result;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (error instanceof CallbackFailure) throw error.value;
|
|
203
|
+
if (deadline.signal.aborted) deadline.check();
|
|
204
|
+
if (outcome.remoteSucceeded && (error instanceof ShopifyOfflineTokenError ? error.code === "LOCK_TIMEOUT" || error.code === "DEADLINE" || error.code === "PERSISTENCE_UNAVAILABLE" : true)) fail("REFRESH_OUTCOME_UNKNOWN");
|
|
205
|
+
if (error instanceof ShopifyOfflineTokenError) throw error;
|
|
206
|
+
deadline.check();
|
|
207
|
+
throw mapDatabaseError(error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function getSession(shopId, options) {
|
|
211
|
+
const deadline = createDeadline(options?.signal);
|
|
212
|
+
let attempt;
|
|
213
|
+
const outcome = { remoteSucceeded: false };
|
|
214
|
+
function emit(event, result, reasonCode) {
|
|
215
|
+
if (!attempt) return;
|
|
216
|
+
notify({
|
|
217
|
+
event,
|
|
218
|
+
operationId: attempt.operationId,
|
|
219
|
+
shopId: attempt.shopId,
|
|
220
|
+
source: "SHARED",
|
|
221
|
+
durationMs: Math.min(6e4, Math.max(0, Math.round(performance.now() - attempt.startedAt))),
|
|
222
|
+
outcome: result,
|
|
223
|
+
reasonCode
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
deadline.check();
|
|
228
|
+
const shop = await resolveShop(shopId, deadline);
|
|
229
|
+
deadline.check();
|
|
230
|
+
const result = await runLocked(shop, shopId, deadline, async (context) => {
|
|
231
|
+
let row = await context.transaction.session.findUnique({ where: { id: context.sessionId }, select: SESSION_SELECT });
|
|
232
|
+
if (!row) fail("SESSION_IDENTITY_CONFLICT");
|
|
233
|
+
const currentTime = now();
|
|
234
|
+
if (!Number.isFinite(currentTime)) fail("INVALID_SESSION");
|
|
235
|
+
if (row.expires === null && row.refreshToken === null) return snapshot(row, shopId, context.shop);
|
|
236
|
+
if (row.expires === null) fail("INVALID_SESSION");
|
|
237
|
+
const expiresAt = row.expires?.getTime() ?? 0;
|
|
238
|
+
if (expiresAt - currentTime > ACCESS_REFRESH_SKEW_MS) return snapshot(row, shopId, context.shop);
|
|
239
|
+
if (!validToken(row.refreshToken)) fail("INVALID_SESSION");
|
|
240
|
+
if (!validDate(row.refreshTokenExpires) || row.refreshTokenExpires === null) fail("INVALID_SESSION");
|
|
241
|
+
if (row.refreshTokenExpires.getTime() <= currentTime) fail("REFRESH_TOKEN_EXPIRED");
|
|
242
|
+
let credentials;
|
|
243
|
+
try {
|
|
244
|
+
credentials = await input.readOAuth();
|
|
245
|
+
} catch {
|
|
246
|
+
fail("OAUTH_CONFIG_UNAVAILABLE");
|
|
247
|
+
}
|
|
248
|
+
if (options?.signal?.aborted) fail("CANCELLED");
|
|
249
|
+
if (context.signal.aborted) fail("DEADLINE");
|
|
250
|
+
if (!validToken(credentials.apiKey) || !validToken(credentials.apiSecret)) fail("OAUTH_CONFIG_UNAVAILABLE");
|
|
251
|
+
const requestStartedAt = now();
|
|
252
|
+
const refreshController = new AbortController();
|
|
253
|
+
const refreshTimeout = setTimeout(() => refreshController.abort(), REFRESH_NETWORK_BUDGET_MS);
|
|
254
|
+
const onAbort = () => refreshController.abort();
|
|
255
|
+
context.signal.addEventListener("abort", onAbort, { once: true });
|
|
256
|
+
let response;
|
|
257
|
+
try {
|
|
258
|
+
const form = new URLSearchParams({ client_id: credentials.apiKey, client_secret: credentials.apiSecret, grant_type: "refresh_token", refresh_token: row.refreshToken });
|
|
259
|
+
attempt = { operationId: randomUUID(), shopId: context.shopId, startedAt: performance.now() };
|
|
260
|
+
emit("shopify.offline_refresh.started", "STARTED", null);
|
|
261
|
+
response = await input.fetch(`https://${context.shop}/admin/oauth/access_token`, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
redirect: "error",
|
|
264
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
265
|
+
body: form,
|
|
266
|
+
signal: refreshController.signal
|
|
267
|
+
});
|
|
268
|
+
} catch {
|
|
269
|
+
clearTimeout(refreshTimeout);
|
|
270
|
+
context.signal.removeEventListener("abort", onAbort);
|
|
271
|
+
if (options?.signal?.aborted) fail("CANCELLED");
|
|
272
|
+
if (context.signal.aborted) fail("REFRESH_OUTCOME_UNKNOWN");
|
|
273
|
+
fail("REFRESH_OUTCOME_UNKNOWN");
|
|
274
|
+
}
|
|
275
|
+
if (response.status >= 400 && response.status < 500) fail("REFRESH_REJECTED");
|
|
276
|
+
if (!response.ok) {
|
|
277
|
+
outcome.remoteSucceeded = true;
|
|
278
|
+
fail("REFRESH_OUTCOME_UNKNOWN");
|
|
279
|
+
}
|
|
280
|
+
outcome.remoteSucceeded = true;
|
|
281
|
+
let raw;
|
|
282
|
+
try {
|
|
283
|
+
raw = await readBoundedBody(response, refreshController.signal);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
clearTimeout(refreshTimeout);
|
|
286
|
+
context.signal.removeEventListener("abort", onAbort);
|
|
287
|
+
if (options?.signal?.aborted) fail("CANCELLED");
|
|
288
|
+
if (context.signal.aborted) fail("REFRESH_OUTCOME_UNKNOWN");
|
|
289
|
+
fail("REFRESH_OUTCOME_UNKNOWN");
|
|
290
|
+
} finally {
|
|
291
|
+
clearTimeout(refreshTimeout);
|
|
292
|
+
context.signal.removeEventListener("abort", onAbort);
|
|
293
|
+
}
|
|
294
|
+
let payload;
|
|
295
|
+
try {
|
|
296
|
+
payload = JSON.parse(raw);
|
|
297
|
+
} catch {
|
|
298
|
+
fail("REFRESH_OUTCOME_UNKNOWN");
|
|
299
|
+
}
|
|
300
|
+
if (!isRecord(payload) || !validToken(payload.access_token) || !validToken(payload.refresh_token) || !Number.isSafeInteger(payload.expires_in) || payload.expires_in <= 0 || payload.expires_in > 86400 || !Number.isSafeInteger(payload.refresh_token_expires_in) || payload.refresh_token_expires_in <= 0 || payload.refresh_token_expires_in > 7776e3 || payload.expires_in <= 320 || payload.scope !== void 0 && !validScope(payload.scope)) fail("REFRESH_OUTCOME_UNKNOWN");
|
|
301
|
+
const expires = requestStartedAt + payload.expires_in * 1e3;
|
|
302
|
+
const refreshTokenExpires = requestStartedAt + payload.refresh_token_expires_in * 1e3;
|
|
303
|
+
if (!Number.isFinite(expires) || !Number.isFinite(refreshTokenExpires) || Number.isNaN(new Date(expires).getTime()) || Number.isNaN(new Date(refreshTokenExpires).getTime())) fail("REFRESH_OUTCOME_UNKNOWN");
|
|
304
|
+
const scope = payload.scope === void 0 ? row.scope : payload.scope;
|
|
305
|
+
try {
|
|
306
|
+
row = await context.transaction.session.update({ where: { id: row.id }, data: {
|
|
307
|
+
accessToken: payload.access_token,
|
|
308
|
+
expires: new Date(expires),
|
|
309
|
+
refreshToken: payload.refresh_token,
|
|
310
|
+
refreshTokenExpires: new Date(refreshTokenExpires),
|
|
311
|
+
scope
|
|
312
|
+
}, select: SESSION_SELECT });
|
|
313
|
+
} catch {
|
|
314
|
+
outcome.remoteSucceeded = true;
|
|
315
|
+
throw new ShopifyOfflineTokenError("REFRESH_OUTCOME_UNKNOWN");
|
|
316
|
+
}
|
|
317
|
+
outcome.remoteSucceeded = true;
|
|
318
|
+
return snapshot(row, shopId, context.shop);
|
|
319
|
+
}, outcome);
|
|
320
|
+
emit("shopify.offline_refresh.committed", "COMMITTED", null);
|
|
321
|
+
return result;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
let failure = error;
|
|
324
|
+
try {
|
|
325
|
+
deadline.check();
|
|
326
|
+
} catch (deadlineError) {
|
|
327
|
+
failure = deadlineError;
|
|
328
|
+
}
|
|
329
|
+
const code = failure instanceof ShopifyOfflineTokenError ? failure.code : "PERSISTENCE_UNAVAILABLE";
|
|
330
|
+
const ambiguous = outcome.remoteSucceeded && code !== "REFRESH_REJECTED" && code !== "CANCELLED";
|
|
331
|
+
const reason = ambiguous ? "REFRESH_OUTCOME_UNKNOWN" : code;
|
|
332
|
+
emit("shopify.offline_refresh.failed", reason === "REFRESH_REJECTED" ? "REJECTED" : reason === "CANCELLED" ? "CANCELLED" : reason === "REFRESH_OUTCOME_UNKNOWN" ? "UNKNOWN" : "UNAVAILABLE", reason);
|
|
333
|
+
throw failure;
|
|
334
|
+
} finally {
|
|
335
|
+
deadline.dispose();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function withSessionLock(shopId, work, options) {
|
|
339
|
+
const deadline = createDeadline(options?.signal);
|
|
340
|
+
try {
|
|
341
|
+
deadline.check();
|
|
342
|
+
const shop = await resolveShop(shopId, deadline);
|
|
343
|
+
deadline.check();
|
|
344
|
+
return await runLocked(shop, shopId, deadline, work);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
deadline.check();
|
|
347
|
+
throw error;
|
|
348
|
+
} finally {
|
|
349
|
+
deadline.dispose();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function withSdkInstallationSessionLock(integrationDomain, work) {
|
|
353
|
+
if (typeof integrationDomain !== "string" || !integrationDomain.trim()) fail("INVALID_SESSION");
|
|
354
|
+
const deadline = createDeadline();
|
|
355
|
+
try {
|
|
356
|
+
return await input.prisma.$transaction(async (tx) => {
|
|
357
|
+
await tx.$executeRawUnsafe("SET LOCAL lock_timeout = '5s'");
|
|
358
|
+
await tx.$executeRawUnsafe("SET LOCAL statement_timeout = '18s'");
|
|
359
|
+
await tx.$queryRawUnsafe("SELECT pg_advisory_xact_lock(hashtextextended($1, 0)) IS NULL", `moda:shopify-offline:v1:${integrationDomain}`);
|
|
360
|
+
let value;
|
|
361
|
+
try {
|
|
362
|
+
value = await deadline.wait(work(Object.freeze({ transaction: tx, shopId: null, shop: integrationDomain, sessionId: null, signal: deadline.signal })));
|
|
363
|
+
} catch (error) {
|
|
364
|
+
deadline.check();
|
|
365
|
+
throw new CallbackFailure(error);
|
|
366
|
+
}
|
|
367
|
+
deadline.check();
|
|
368
|
+
return value;
|
|
369
|
+
}, TX_OPTIONS);
|
|
370
|
+
} catch (error) {
|
|
371
|
+
if (error instanceof CallbackFailure) throw error.value;
|
|
372
|
+
if (error instanceof ShopifyOfflineTokenError) throw error;
|
|
373
|
+
if (deadline.signal.aborted) deadline.check();
|
|
374
|
+
throw mapDatabaseError(error);
|
|
375
|
+
} finally {
|
|
376
|
+
deadline.dispose();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return Object.freeze({
|
|
380
|
+
withSdkInstallationSessionLock,
|
|
381
|
+
getAccessToken: async (shopId, options) => (await getSession(shopId, options)).accessToken,
|
|
382
|
+
getSession,
|
|
383
|
+
withSessionLock
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/shopify/oauth-configuration.ts
|
|
388
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
389
|
+
var CONFIGURATION_KEY = "shopify.oauth";
|
|
390
|
+
var CONFIGURATION_ENVIRONMENTS = /* @__PURE__ */ new Set(["local", "test", "development", "staging", "production"]);
|
|
391
|
+
var ENVELOPE_KEYS = ["kind", "schemaVersion", "algorithm", "wrappingKeyId", "nonce", "ciphertext", "authTag"];
|
|
392
|
+
var RESERVED_KEY_IDS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
393
|
+
var KEY_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
394
|
+
var BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
395
|
+
var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
|
|
396
|
+
var ERROR_MESSAGES2 = {
|
|
397
|
+
INVALID_ENVIRONMENT: "Shopify OAuth configuration is unavailable.",
|
|
398
|
+
CONFIGURATION_UNAVAILABLE: "Shopify OAuth configuration is unavailable.",
|
|
399
|
+
CONFIGURATION_INVALID: "Shopify OAuth configuration is invalid."
|
|
400
|
+
};
|
|
401
|
+
var ShopifyOAuthConfigurationError = class extends Error {
|
|
402
|
+
code;
|
|
403
|
+
constructor(code) {
|
|
404
|
+
super(ERROR_MESSAGES2[code]);
|
|
405
|
+
this.name = "ShopifyOAuthConfigurationError";
|
|
406
|
+
this.code = code;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
function configurationFailure(code) {
|
|
410
|
+
throw new ShopifyOAuthConfigurationError(code);
|
|
411
|
+
}
|
|
412
|
+
function isRecord2(value) {
|
|
413
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
414
|
+
}
|
|
415
|
+
function hasExactKeys(value, expected) {
|
|
416
|
+
const actual = Object.keys(value);
|
|
417
|
+
return actual.length === expected.length && actual.every((key) => expected.includes(key));
|
|
418
|
+
}
|
|
419
|
+
function canonicalBase64(value, exactLength) {
|
|
420
|
+
if (typeof value !== "string" || value.length === 0 || !BASE64_PATTERN.test(value)) return null;
|
|
421
|
+
const decoded = Buffer2.from(value, "base64");
|
|
422
|
+
if (decoded.length < 1 || decoded.length > 8192 || decoded.toString("base64") !== value) return null;
|
|
423
|
+
if (exactLength !== null && decoded.length !== exactLength) return null;
|
|
424
|
+
return Uint8Array.from(decoded);
|
|
425
|
+
}
|
|
426
|
+
function parseSealedEnvelope(value) {
|
|
427
|
+
if (!isRecord2(value) || !hasExactKeys(value, ENVELOPE_KEYS) || value.kind !== "sealed" || value.schemaVersion !== 1 || value.algorithm !== "AES-256-GCM" || typeof value.wrappingKeyId !== "string" || !KEY_ID_PATTERN.test(value.wrappingKeyId) || RESERVED_KEY_IDS.has(value.wrappingKeyId)) {
|
|
428
|
+
return configurationFailure("CONFIGURATION_INVALID");
|
|
429
|
+
}
|
|
430
|
+
const nonce = canonicalBase64(value.nonce, 12);
|
|
431
|
+
const ciphertext = canonicalBase64(value.ciphertext, null);
|
|
432
|
+
const authTag = canonicalBase64(value.authTag, 16);
|
|
433
|
+
if (!nonce || !ciphertext || !authTag) return configurationFailure("CONFIGURATION_INVALID");
|
|
434
|
+
return { keyId: value.wrappingKeyId, nonce, ciphertext, authTag };
|
|
435
|
+
}
|
|
436
|
+
function validCredential(value) {
|
|
437
|
+
return typeof value === "string" && value.length >= 1 && value.length <= 4096 && value.trim() === value && !CONTROL_CHARACTERS.test(value);
|
|
438
|
+
}
|
|
439
|
+
function parseCredentials(value) {
|
|
440
|
+
if (!isRecord2(value) || !hasExactKeys(value, ["apiKey", "apiSecret"]) || !validCredential(value.apiKey) || !validCredential(value.apiSecret)) {
|
|
441
|
+
return configurationFailure("CONFIGURATION_INVALID");
|
|
442
|
+
}
|
|
443
|
+
return Object.freeze({ apiKey: value.apiKey, apiSecret: value.apiSecret });
|
|
444
|
+
}
|
|
445
|
+
async function readShopifyOAuthConfiguration(input) {
|
|
446
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || typeof input.environment !== "string" || !CONFIGURATION_ENVIRONMENTS.has(input.environment)) {
|
|
447
|
+
return configurationFailure("INVALID_ENVIRONMENT");
|
|
448
|
+
}
|
|
449
|
+
let row;
|
|
450
|
+
try {
|
|
451
|
+
row = await input.prisma.platformConfiguration.findUnique({
|
|
452
|
+
where: { environment_key: { environment: input.environment, key: CONFIGURATION_KEY } },
|
|
453
|
+
select: { environment: true, key: true, version: true, value: true }
|
|
454
|
+
});
|
|
455
|
+
} catch {
|
|
456
|
+
return configurationFailure("CONFIGURATION_UNAVAILABLE");
|
|
457
|
+
}
|
|
458
|
+
if (!row) return configurationFailure("CONFIGURATION_UNAVAILABLE");
|
|
459
|
+
if (row.environment !== input.environment || row.key !== CONFIGURATION_KEY || !Number.isSafeInteger(row.version) || row.version < 1 || row.version > 2147483647) {
|
|
460
|
+
return configurationFailure("CONFIGURATION_INVALID");
|
|
461
|
+
}
|
|
462
|
+
if (isRecord2(row.value) && hasExactKeys(row.value, ["kind", "schemaVersion"]) && row.value.kind === "disabled" && row.value.schemaVersion === 1) {
|
|
463
|
+
return configurationFailure("CONFIGURATION_UNAVAILABLE");
|
|
464
|
+
}
|
|
465
|
+
let plaintext;
|
|
466
|
+
try {
|
|
467
|
+
const envelope = parseSealedEnvelope(row.value);
|
|
468
|
+
const aad = JSON.stringify(["moda-platform-config-v1", input.environment, CONFIGURATION_KEY, row.version, 1]);
|
|
469
|
+
plaintext = decryptCredential({ keyring: input.unsealKeyring, envelope, aad });
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (error instanceof ShopifyOAuthConfigurationError) throw error;
|
|
472
|
+
return configurationFailure("CONFIGURATION_INVALID");
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
return parseCredentials(JSON.parse(plaintext));
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (error instanceof ShopifyOAuthConfigurationError) throw error;
|
|
478
|
+
return configurationFailure("CONFIGURATION_INVALID");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/shopify/node.ts
|
|
8
483
|
function createShopifyWebhookJobDigest(prefix, first, second) {
|
|
9
484
|
if (typeof first !== "string" || first.length === 0) {
|
|
10
485
|
throw new Error(`${prefix}: first input must be a non-empty string`);
|
|
@@ -48,11 +523,15 @@ function createShopifyWhatsAppConsentSyncJobId(input) {
|
|
|
48
523
|
return `shopify-whatsapp-consent-sync-${digest}`;
|
|
49
524
|
}
|
|
50
525
|
export {
|
|
526
|
+
ShopifyOAuthConfigurationError,
|
|
527
|
+
ShopifyOfflineTokenError,
|
|
51
528
|
createPendingRecoveryCandidateJobId,
|
|
52
529
|
createShopifyCheckoutJobId,
|
|
53
530
|
createShopifyDiscountSyncJobId,
|
|
531
|
+
createShopifyOfflineTokenProvider,
|
|
54
532
|
createShopifyOrderJobId,
|
|
55
533
|
createShopifyWebhookJobId,
|
|
56
|
-
createShopifyWhatsAppConsentSyncJobId
|
|
534
|
+
createShopifyWhatsAppConsentSyncJobId,
|
|
535
|
+
readShopifyOAuthConfiguration
|
|
57
536
|
};
|
|
58
537
|
//# sourceMappingURL=node.js.map
|
package/dist/shopify/node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shopify/node.ts"],"sourcesContent":["import { ShopifyWhatsAppConsentSyncJobV1Schema, type ShopifyWhatsAppConsentSyncJobV1 } from \"./whatsapp-consent-sync.schema.js\";\nimport { createHash } from \"node:crypto\";\n\nfunction createShopifyWebhookJobDigest(prefix: string, first: string, second: string): string {\n if (typeof first !== \"string\" || first.length === 0) {\n throw new Error(`${prefix}: first input must be a non-empty string`);\n }\n\n if (typeof second !== \"string\" || second.length === 0) {\n throw new Error(`${prefix}: second input must be a non-empty string`);\n }\n\n const digest = createHash(\"sha256\").update(`${first}:${second}`).digest(\"hex\");\n return `${prefix}-${digest}`;\n}\n\n/**\n * Legacy webhook delivery job id used by earlier receipt/outbox flows.\n * Prefer the resource-scoped helpers below for current queue publication.\n */\nexport function createShopifyWebhookJobId(appKey: string, deliveryId: string): string {\n return createShopifyWebhookJobDigest(\"shopify\", appKey, deliveryId);\n}\n\nexport function createShopifyCheckoutJobId(shopId: string, checkoutToken: string): string {\n return createShopifyWebhookJobDigest(\"checkout\", shopId, checkoutToken);\n}\n\nexport function createShopifyOrderJobId(shopId: string, orderGid: string): string {\n return createShopifyWebhookJobDigest(\"order-created\", shopId, orderGid);\n}\n\nexport function createPendingRecoveryCandidateJobId(\n shopId: string,\n checkoutToken: string,\n): string {\n return createShopifyWebhookJobDigest(\"pending-recovery\", shopId, checkoutToken);\n}\n\nexport function createShopifyDiscountSyncJobId(event: {\n shopId: string;\n reason: \"SUBSCRIPTION_ACTIVATED\" | \"REINSTALL_RECONCILED\" | \"SCOPES_UPDATED\" | \"ADMIN_REQUESTED\" | \"DISCOUNT_WEBHOOK\";\n requestedAt: string;\n deliveryId?: string | null;\n}): string {\n if (typeof event.shopId !== \"string\" || event.shopId.trim().length === 0) {\n throw new Error(\"createShopifyDiscountSyncJobId: shopId must be a non-empty string\");\n }\n\n if (event.reason === \"DISCOUNT_WEBHOOK\") {\n if (typeof event.deliveryId !== \"string\" || event.deliveryId.trim().length === 0) {\n throw new Error(\"createShopifyDiscountSyncJobId: deliveryId is required for DISCOUNT_WEBHOOK\");\n }\n return createShopifyWebhookJobDigest(\"discount-sync\", event.shopId, event.deliveryId);\n }\n\n if (typeof event.requestedAt !== \"string\" || event.requestedAt.trim().length === 0) {\n throw new Error(\"createShopifyDiscountSyncJobId: requestedAt must be a non-empty ISO datetime string\");\n }\n\n return createShopifyWebhookJobDigest(\"discount-sync\", `${event.shopId}:${event.reason}`, event.requestedAt);\n}\n\n/**\n * One deduplication identity per authenticated shop + webhook delivery.\n * Length-safe JSON tuple serialization avoids `a:b` vs `a`/`b:c` collisions;\n * the job ID never reveals the customer ID, delivery ID or phone.\n */\nexport function createShopifyWhatsAppConsentSyncJobId(\n input: ShopifyWhatsAppConsentSyncJobV1,\n): string {\n const job = ShopifyWhatsAppConsentSyncJobV1Schema.parse(input);\n const digest = createHash(\"sha256\")\n .update(JSON.stringify([job.shopId, job.deliveryId]))\n .digest(\"hex\");\n return `shopify-whatsapp-consent-sync-${digest}`;\n}\n"],"mappings":";;;;;;AACA,SAAS,kBAAkB;AAE3B,SAAS,8BAA8B,QAAgB,OAAe,QAAwB;AAC5F,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAAG;AACnD,UAAM,IAAI,MAAM,GAAG,MAAM,0CAA0C;AAAA,EACrE;AAEA,MAAI,OAAO,WAAW,YAAY,OAAO,WAAW,GAAG;AACrD,UAAM,IAAI,MAAM,GAAG,MAAM,2CAA2C;AAAA,EACtE;AAEA,QAAM,SAAS,WAAW,QAAQ,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,EAAE,EAAE,OAAO,KAAK;AAC7E,SAAO,GAAG,MAAM,IAAI,MAAM;AAC5B;AAMO,SAAS,0BAA0B,QAAgB,YAA4B;AACpF,SAAO,8BAA8B,WAAW,QAAQ,UAAU;AACpE;AAEO,SAAS,2BAA2B,QAAgB,eAA+B;AACxF,SAAO,8BAA8B,YAAY,QAAQ,aAAa;AACxE;AAEO,SAAS,wBAAwB,QAAgB,UAA0B;AAChF,SAAO,8BAA8B,iBAAiB,QAAQ,QAAQ;AACxE;AAEO,SAAS,oCACd,QACA,eACQ;AACR,SAAO,8BAA8B,oBAAoB,QAAQ,aAAa;AAChF;AAEO,SAAS,+BAA+B,OAKpC;AACT,MAAI,OAAO,MAAM,WAAW,YAAY,MAAM,OAAO,KAAK,EAAE,WAAW,GAAG;AACxE,UAAM,IAAI,MAAM,mEAAmE;AAAA,EACrF;AAEA,MAAI,MAAM,WAAW,oBAAoB;AACvC,QAAI,OAAO,MAAM,eAAe,YAAY,MAAM,WAAW,KAAK,EAAE,WAAW,GAAG;AAChF,YAAM,IAAI,MAAM,6EAA6E;AAAA,IAC/F;AACA,WAAO,8BAA8B,iBAAiB,MAAM,QAAQ,MAAM,UAAU;AAAA,EACtF;AAEA,MAAI,OAAO,MAAM,gBAAgB,YAAY,MAAM,YAAY,KAAK,EAAE,WAAW,GAAG;AAClF,UAAM,IAAI,MAAM,qFAAqF;AAAA,EACvG;AAEA,SAAO,8BAA8B,iBAAiB,GAAG,MAAM,MAAM,IAAI,MAAM,MAAM,IAAI,MAAM,WAAW;AAC5G;AAOO,SAAS,sCACd,OACQ;AACR,QAAM,MAAM,sCAAsC,MAAM,KAAK;AAC7D,QAAM,SAAS,WAAW,QAAQ,EAC/B,OAAO,KAAK,UAAU,CAAC,IAAI,QAAQ,IAAI,UAAU,CAAC,CAAC,EACnD,OAAO,KAAK;AACf,SAAO,iCAAiC,MAAM;AAChD;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shopify/node.ts","../../src/shopify/offline-token-provider.ts","../../src/shopify/oauth-configuration.ts"],"sourcesContent":["import { ShopifyWhatsAppConsentSyncJobV1Schema, type ShopifyWhatsAppConsentSyncJobV1 } from \"./whatsapp-consent-sync.schema.js\";\nimport { createHash } from \"node:crypto\";\n\nexport {\n ShopifyOfflineTokenError,\n createShopifyOfflineTokenProvider,\n type ShopifyOfflineTokenErrorCode,\n type ShopifyOfflineRefreshEvent,\n type ShopifyOfflineDatabase,\n type ShopifyOfflineTransaction,\n type ShopifyOfflineSessionDelegate,\n type ShopifyOfflineSessionRecord,\n type ShopifyOfflineSessionWhere,\n type ShopifyOfflineSessionScalarWhere,\n type ShopifyOfflineSessionUniqueWhere,\n type ShopifyOfflineSessionSelect,\n type ShopifyOfflineSessionData,\n type ShopifyOfflineSessionCreateData,\n type ShopifyOfflineStringFilter,\n type ShopifyOfflineBooleanFilter,\n type ShopifyOfflineNullableStringFilter,\n type ShopifyOfflineNullableBooleanFilter,\n type ShopifyOfflineDateFilter,\n type ShopifyOfflineBigIntFilter,\n type ShopifyOfflineShopRecord,\n type OfflineCredentialSnapshot,\n type LockedSessionContext,\n type SdkInstallationSessionContext,\n type ShopifyOfflineTokenProvider,\n} from \"./offline-token-provider.js\";\nexport {\n ShopifyOAuthConfigurationError,\n readShopifyOAuthConfiguration,\n type ShopifyOAuthConfigurationErrorCode,\n type ShopifyOAuthConfigurationPrisma,\n type ShopifyOAuthConfigurationRow,\n type ShopifyOAuthCredentials,\n} from \"./oauth-configuration.js\";\n\nfunction createShopifyWebhookJobDigest(prefix: string, first: string, second: string): string {\n if (typeof first !== \"string\" || first.length === 0) {\n throw new Error(`${prefix}: first input must be a non-empty string`);\n }\n\n if (typeof second !== \"string\" || second.length === 0) {\n throw new Error(`${prefix}: second input must be a non-empty string`);\n }\n\n const digest = createHash(\"sha256\").update(`${first}:${second}`).digest(\"hex\");\n return `${prefix}-${digest}`;\n}\n\n/**\n * Legacy webhook delivery job id used by earlier receipt/outbox flows.\n * Prefer the resource-scoped helpers below for current queue publication.\n */\nexport function createShopifyWebhookJobId(appKey: string, deliveryId: string): string {\n return createShopifyWebhookJobDigest(\"shopify\", appKey, deliveryId);\n}\n\nexport function createShopifyCheckoutJobId(shopId: string, checkoutToken: string): string {\n return createShopifyWebhookJobDigest(\"checkout\", shopId, checkoutToken);\n}\n\nexport function createShopifyOrderJobId(shopId: string, orderGid: string): string {\n return createShopifyWebhookJobDigest(\"order-created\", shopId, orderGid);\n}\n\nexport function createPendingRecoveryCandidateJobId(\n shopId: string,\n checkoutToken: string,\n): string {\n return createShopifyWebhookJobDigest(\"pending-recovery\", shopId, checkoutToken);\n}\n\nexport function createShopifyDiscountSyncJobId(event: {\n shopId: string;\n reason: \"SUBSCRIPTION_ACTIVATED\" | \"REINSTALL_RECONCILED\" | \"SCOPES_UPDATED\" | \"ADMIN_REQUESTED\" | \"DISCOUNT_WEBHOOK\";\n requestedAt: string;\n deliveryId?: string | null;\n}): string {\n if (typeof event.shopId !== \"string\" || event.shopId.trim().length === 0) {\n throw new Error(\"createShopifyDiscountSyncJobId: shopId must be a non-empty string\");\n }\n\n if (event.reason === \"DISCOUNT_WEBHOOK\") {\n if (typeof event.deliveryId !== \"string\" || event.deliveryId.trim().length === 0) {\n throw new Error(\"createShopifyDiscountSyncJobId: deliveryId is required for DISCOUNT_WEBHOOK\");\n }\n return createShopifyWebhookJobDigest(\"discount-sync\", event.shopId, event.deliveryId);\n }\n\n if (typeof event.requestedAt !== \"string\" || event.requestedAt.trim().length === 0) {\n throw new Error(\"createShopifyDiscountSyncJobId: requestedAt must be a non-empty ISO datetime string\");\n }\n\n return createShopifyWebhookJobDigest(\"discount-sync\", `${event.shopId}:${event.reason}`, event.requestedAt);\n}\n\n/**\n * One deduplication identity per authenticated shop + webhook delivery.\n * Length-safe JSON tuple serialization avoids `a:b` vs `a`/`b:c` collisions;\n * the job ID never reveals the customer ID, delivery ID or phone.\n */\nexport function createShopifyWhatsAppConsentSyncJobId(\n input: ShopifyWhatsAppConsentSyncJobV1,\n): string {\n const job = ShopifyWhatsAppConsentSyncJobV1Schema.parse(input);\n const digest = createHash(\"sha256\")\n .update(JSON.stringify([job.shopId, job.deliveryId]))\n .digest(\"hex\");\n return `shopify-whatsapp-consent-sync-${digest}`;\n}\n","import { Buffer } from \"node:buffer\";\nimport { randomUUID } from \"node:crypto\";\nimport { performance } from \"node:perf_hooks\";\n\nexport type ShopifyOfflineTokenErrorCode =\n | \"SESSION_MISSING\" | \"SESSION_IDENTITY_CONFLICT\" | \"INVALID_SESSION\"\n | \"REFRESH_TOKEN_EXPIRED\" | \"OAUTH_CONFIG_UNAVAILABLE\" | \"LOCK_TIMEOUT\"\n | \"CANCELLED\" | \"DEADLINE\" | \"REFRESH_REJECTED\"\n | \"REFRESH_RESPONSE_INVALID\" | \"REFRESH_OUTCOME_UNKNOWN\" | \"PERSISTENCE_UNAVAILABLE\";\n\n/** Bounded refresh lifecycle metadata. Never includes provider credentials or errors. */\nexport type ShopifyOfflineRefreshEvent = Readonly<{\n event: \"shopify.offline_refresh.started\" | \"shopify.offline_refresh.committed\" | \"shopify.offline_refresh.failed\";\n operationId: string;\n shopId: string | null;\n source: \"SHARED\" | \"SDK\";\n durationMs: number;\n outcome: \"STARTED\" | \"COMMITTED\" | \"REJECTED\" | \"UNKNOWN\" | \"CANCELLED\" | \"UNAVAILABLE\";\n reasonCode: ShopifyOfflineTokenErrorCode | null;\n}>;\n\nconst ERROR_MESSAGES: Record<ShopifyOfflineTokenErrorCode, string> = {\n SESSION_MISSING: \"Shopify offline session is unavailable.\",\n SESSION_IDENTITY_CONFLICT: \"Shopify offline session identity is inconsistent.\",\n INVALID_SESSION: \"Shopify offline session is invalid.\",\n REFRESH_TOKEN_EXPIRED: \"Shopify offline refresh token has expired.\",\n OAUTH_CONFIG_UNAVAILABLE: \"Shopify OAuth configuration is unavailable.\",\n LOCK_TIMEOUT: \"Shopify offline session is busy; retry the request.\",\n CANCELLED: \"Shopify offline session request was cancelled.\",\n DEADLINE: \"Shopify offline session operation exceeded its time limit.\",\n REFRESH_REJECTED: \"Shopify rejected the offline refresh request.\",\n REFRESH_RESPONSE_INVALID: \"Shopify returned an invalid offline refresh response.\",\n REFRESH_OUTCOME_UNKNOWN: \"Shopify refresh outcome is unknown; retry in a later request.\",\n PERSISTENCE_UNAVAILABLE: \"Shopify offline session could not be persisted.\",\n};\n\nconst RETRYABLE = new Set<ShopifyOfflineTokenErrorCode>([\n \"LOCK_TIMEOUT\", \"DEADLINE\", \"PERSISTENCE_UNAVAILABLE\", \"REFRESH_OUTCOME_UNKNOWN\",\n]);\n\nexport class ShopifyOfflineTokenError extends Error {\n readonly code: ShopifyOfflineTokenErrorCode;\n readonly retryable: boolean;\n\n constructor(code: ShopifyOfflineTokenErrorCode) {\n super(ERROR_MESSAGES[code]);\n this.name = \"ShopifyOfflineTokenError\";\n this.code = code;\n this.retryable = RETRYABLE.has(code);\n }\n}\n\nclass CallbackFailure extends Error {\n constructor(readonly value: unknown) { super(\"Shopify offline callback failed.\"); }\n}\n\nexport type ShopifyOfflineSessionRecord = Readonly<{\n id: string; shop: string; isOnline: boolean; accessToken: string | null; refreshToken: string | null;\n scope: string | null; expires: Date | null; refreshTokenExpires: Date | null;\n}>;\nexport type ShopifyOfflineShopRecord = Readonly<{\n id: string; domain: string; platform: string; status: string;\n}>;\n\nexport type ShopifyOfflineStringFilter = string | Readonly<{\n equals?: string; in?: string[]; notIn?: string[]; lt?: string; lte?: string; gt?: string; gte?: string;\n contains?: string; startsWith?: string; endsWith?: string; mode?: \"default\" | \"insensitive\";\n not?: string | ShopifyOfflineStringFilter;\n}>;\nexport type ShopifyOfflineBooleanFilter = boolean | Readonly<{ equals?: boolean; not?: boolean | ShopifyOfflineBooleanFilter }>;\nexport type ShopifyOfflineNullableStringFilter = string | null | Readonly<{\n equals?: string | null; in?: string[] | null; notIn?: string[] | null; lt?: string; lte?: string; gt?: string; gte?: string;\n contains?: string; startsWith?: string; endsWith?: string; mode?: \"default\" | \"insensitive\";\n not?: string | null | ShopifyOfflineNullableStringFilter;\n}>;\nexport type ShopifyOfflineNullableBooleanFilter = boolean | null | Readonly<{\n equals?: boolean | null; not?: boolean | null | ShopifyOfflineNullableBooleanFilter;\n}>;\nexport type ShopifyOfflineDateFilter = Date | string | null | Readonly<{\n equals?: Date | string | null; in?: Date[] | string[] | null; notIn?: Date[] | string[] | null;\n lt?: Date | string; lte?: Date | string; gt?: Date | string; gte?: Date | string;\n not?: Date | string | null | ShopifyOfflineDateFilter;\n}>;\nexport type ShopifyOfflineBigIntFilter = bigint | number | null | Readonly<{\n equals?: bigint | number | null; in?: bigint[] | number[] | null; notIn?: bigint[] | number[] | null;\n lt?: bigint | number; lte?: bigint | number; gt?: bigint | number; gte?: bigint | number;\n not?: bigint | number | null | ShopifyOfflineBigIntFilter;\n}>;\nexport type ShopifyOfflineSessionScalarWhere = {\n id?: ShopifyOfflineStringFilter;\n shop?: ShopifyOfflineStringFilter;\n state?: ShopifyOfflineStringFilter;\n isOnline?: ShopifyOfflineBooleanFilter;\n scope?: ShopifyOfflineNullableStringFilter;\n expires?: ShopifyOfflineDateFilter;\n accessToken?: ShopifyOfflineStringFilter;\n userId?: ShopifyOfflineBigIntFilter;\n firstName?: ShopifyOfflineNullableStringFilter;\n lastName?: ShopifyOfflineNullableStringFilter;\n email?: ShopifyOfflineNullableStringFilter;\n accountOwner?: ShopifyOfflineBooleanFilter;\n locale?: ShopifyOfflineNullableStringFilter;\n collaborator?: ShopifyOfflineNullableBooleanFilter;\n emailVerified?: ShopifyOfflineNullableBooleanFilter;\n refreshToken?: ShopifyOfflineNullableStringFilter;\n refreshTokenExpires?: ShopifyOfflineDateFilter;\n};\nexport type ShopifyOfflineSessionWhere = ShopifyOfflineSessionScalarWhere & {\n AND?: ShopifyOfflineSessionScalarWhere | ShopifyOfflineSessionScalarWhere[];\n OR?: ShopifyOfflineSessionScalarWhere[];\n NOT?: ShopifyOfflineSessionScalarWhere | ShopifyOfflineSessionScalarWhere[];\n};\nexport type ShopifyOfflineSessionUniqueWhere = Readonly<{ id: string }>;\nexport type ShopifyOfflineSessionSelect = Readonly<Partial<Record<\n \"id\" | \"shop\" | \"state\" | \"isOnline\" | \"scope\" | \"expires\" | \"accessToken\" | \"userId\"\n | \"firstName\" | \"lastName\" | \"email\" | \"accountOwner\" | \"locale\" | \"collaborator\"\n | \"emailVerified\" | \"refreshToken\" | \"refreshTokenExpires\",\n true\n>>>;\nexport type ShopifyOfflineSessionData = Readonly<{\n id?: string;\n shop?: string;\n state?: string;\n isOnline?: boolean;\n scope?: string | null;\n expires?: Date | null;\n accessToken?: string;\n userId?: bigint | null;\n firstName?: string | null;\n lastName?: string | null;\n email?: string | null;\n accountOwner?: boolean;\n locale?: string | null;\n collaborator?: boolean | null;\n emailVerified?: boolean | null;\n refreshToken?: string | null;\n refreshTokenExpires?: Date | null;\n}>;\nexport type ShopifyOfflineSessionCreateData = ShopifyOfflineSessionData & Readonly<{\n id: string;\n shop: string;\n state: string;\n accessToken: string;\n}>;\nexport type ShopifyOfflineSessionDelegate = Readonly<{\n findUnique(args: Readonly<{ where: ShopifyOfflineSessionUniqueWhere; select?: ShopifyOfflineSessionSelect }>): Promise<ShopifyOfflineSessionRecord | null>;\n findMany(args: Readonly<{ where: ShopifyOfflineSessionWhere; select?: ShopifyOfflineSessionSelect; take?: number }>): Promise<ShopifyOfflineSessionRecord[]>;\n create(args: Readonly<{ data: ShopifyOfflineSessionCreateData; select?: ShopifyOfflineSessionSelect }>): Promise<ShopifyOfflineSessionRecord>;\n update(args: Readonly<{ where: ShopifyOfflineSessionUniqueWhere; data: ShopifyOfflineSessionData; select?: ShopifyOfflineSessionSelect }>): Promise<ShopifyOfflineSessionRecord>;\n upsert(args: Readonly<{ where: ShopifyOfflineSessionUniqueWhere; create: ShopifyOfflineSessionCreateData; update: ShopifyOfflineSessionData; select?: ShopifyOfflineSessionSelect }>): Promise<ShopifyOfflineSessionRecord>;\n delete(args: Readonly<{ where: ShopifyOfflineSessionUniqueWhere; select?: ShopifyOfflineSessionSelect }>): Promise<ShopifyOfflineSessionRecord>;\n deleteMany(args: Readonly<{ where?: ShopifyOfflineSessionWhere }>): Promise<{ count: number }>;\n updateMany(args: Readonly<{ where?: ShopifyOfflineSessionWhere; data: ShopifyOfflineSessionData }>): Promise<{ count: number }>;\n}>;\n\nexport type ShopifyOfflineTransaction = Readonly<{\n $queryRawUnsafe<T = unknown>(sql: string, ...values: unknown[]): Promise<T>;\n $executeRawUnsafe(sql: string, ...values: unknown[]): Promise<number>;\n session: ShopifyOfflineSessionDelegate;\n shop: Readonly<{\n findUnique(args: Readonly<{ where: Readonly<{ id: string }>; select: Readonly<{ id: true; domain: true; platform: true; status: true }> }>): Promise<ShopifyOfflineShopRecord | null>;\n }>;\n}>;\n\nexport type ShopifyOfflineDatabase = Readonly<{\n $transaction<T>(\n work: (transaction: ShopifyOfflineTransaction) => Promise<T>,\n options: Readonly<{ maxWait: 5000; timeout: 20000; isolationLevel: \"ReadCommitted\" }>,\n ): Promise<T>;\n shop: ShopifyOfflineTransaction[\"shop\"];\n}>;\n\nexport type OfflineCredentialSnapshot = Readonly<{\n id: string; shopId: string; shop: string; accessToken: string; scope: string | null;\n expires: Date | null; refreshTokenExpires: Date | null;\n}>;\nexport type LockedSessionContext = Readonly<{\n transaction: ShopifyOfflineTransaction; shopId: string; shop: string; sessionId: string; signal: AbortSignal;\n}>;\nexport type SdkInstallationSessionContext = Readonly<{\n transaction: ShopifyOfflineTransaction; shopId: null; shop: string; sessionId: string | null; signal: AbortSignal;\n}>;\nexport type ShopifyOfflineTokenProvider = Readonly<{\n withSdkInstallationSessionLock<T>(integrationDomain: string, work: (context: SdkInstallationSessionContext) => Promise<T>): Promise<T>;\n getAccessToken(shopId: string, options?: Readonly<{ signal?: AbortSignal }>): Promise<string>;\n getSession(shopId: string, options?: Readonly<{ signal?: AbortSignal }>): Promise<OfflineCredentialSnapshot>;\n withSessionLock<T>(shopId: string, work: (context: LockedSessionContext) => Promise<T>, options?: Readonly<{ signal?: AbortSignal }>): Promise<T>;\n}>;\n\ntype PersistedShop = ShopifyOfflineShopRecord;\nconst TX_OPTIONS = { maxWait: 5000, timeout: 20000, isolationLevel: \"ReadCommitted\" } as const;\nconst ACCESS_REFRESH_SKEW_MS = 320_000;\nconst REFRESH_NETWORK_BUDGET_MS = 8_000;\nconst OPERATION_BUDGET_MS = 15_000;\nconst RESPONSE_LIMIT = 16_384;\nconst CONTROL = /[\\u0000-\\u001f\\u007f]/;\nconst SESSION_SELECT = { id: true, shop: true, isOnline: true, accessToken: true, refreshToken: true, scope: true, expires: true, refreshTokenExpires: true } as const;\n\nfunction fail(code: ShopifyOfflineTokenErrorCode): never { throw new ShopifyOfflineTokenError(code); }\nfunction isRecord(value: unknown): value is Record<string, unknown> { return typeof value === \"object\" && value !== null && !Array.isArray(value); }\nfunction validToken(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0 && value.length <= 4096 && value.trim() === value && !/[\\r\\n\\u0000]/.test(value);\n}\nfunction validScope(value: unknown): value is string { return typeof value === \"string\" && value.length <= 4096 && !CONTROL.test(value); }\nfunction validDate(value: unknown): value is Date | null { return value === null || (value instanceof Date && Number.isFinite(value.getTime())); }\nfunction mapDatabaseError(error: unknown): ShopifyOfflineTokenError {\n const code = isRecord(error) && typeof error.code === \"string\" ? error.code : \"\";\n const message = error instanceof Error ? error.message : \"\";\n if (code === \"55P03\" || /lock timeout/i.test(message)) return new ShopifyOfflineTokenError(\"LOCK_TIMEOUT\");\n if (code === \"57014\" || /statement timeout|statement timed out/i.test(message)) return new ShopifyOfflineTokenError(\"DEADLINE\");\n return new ShopifyOfflineTokenError(\"PERSISTENCE_UNAVAILABLE\");\n}\n\nfunction createDeadline(signal?: AbortSignal): Readonly<{ signal: AbortSignal; dispose(): void; check(): void; wait<T>(work: Promise<T>): Promise<T> }> {\n const controller = new AbortController();\n let expired = false;\n const timer = setTimeout(() => { expired = true; controller.abort(); }, OPERATION_BUDGET_MS);\n const onAbort = () => controller.abort(signal?.reason);\n if (signal?.aborted) onAbort();\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n return {\n signal: controller.signal,\n dispose() { clearTimeout(timer); signal?.removeEventListener(\"abort\", onAbort); },\n wait<T>(work: Promise<T>): Promise<T> {\n if (controller.signal.aborted) this.check();\n let onDeadline: (() => void) | undefined;\n const aborted = new Promise<never>((_resolve, reject) => {\n onDeadline = () => reject(new Error(\"operation aborted\"));\n controller.signal.addEventListener(\"abort\", onDeadline, { once: true });\n });\n return Promise.race([work, aborted]).finally(() => {\n if (onDeadline) controller.signal.removeEventListener(\"abort\", onDeadline);\n });\n },\n check() {\n if (signal?.aborted) fail(\"CANCELLED\");\n if (expired) fail(\"DEADLINE\");\n if (controller.signal.aborted) fail(\"DEADLINE\");\n },\n };\n}\n\nasync function readBoundedBody(response: Response, signal: AbortSignal): Promise<string> {\n if (!response.body) return \"\";\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n try {\n while (true) {\n if (signal.aborted) throw new Error(\"refresh stream aborted\");\n const { done, value } = await reader.read();\n if (done) break;\n size += value.byteLength;\n if (size > RESPONSE_LIMIT) {\n await reader.cancel().catch(() => undefined);\n fail(\"REFRESH_OUTCOME_UNKNOWN\");\n }\n chunks.push(value);\n }\n } finally { reader.releaseLock(); }\n return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString(\"utf8\");\n}\n\nfunction snapshot(row: ShopifyOfflineSessionRecord, shopId: string, shop: string): OfflineCredentialSnapshot {\n if (!validToken(row.accessToken) || !validScope(row.scope) && row.scope !== null\n || !validDate(row.expires) || !validDate(row.refreshTokenExpires)) fail(\"INVALID_SESSION\");\n return Object.freeze({ id: row.id, shopId, shop, accessToken: row.accessToken, scope: row.scope, expires: row.expires, refreshTokenExpires: row.refreshTokenExpires });\n}\n\nexport function createShopifyOfflineTokenProvider(input: Readonly<{\n prisma: ShopifyOfflineDatabase;\n readOAuth: () => Promise<Readonly<{ apiKey: string; apiSecret: string }>>;\n fetch: typeof fetch;\n now?: () => number;\n onEvent?: (event: ShopifyOfflineRefreshEvent) => void;\n}>): ShopifyOfflineTokenProvider {\n const now = input.now ?? Date.now;\n function notify(event: ShopifyOfflineRefreshEvent): void {\n try {\n // Promise resolution also isolates rejected promises/thenables from adapters.\n void Promise.resolve(input.onEvent?.(Object.freeze(event))).catch(() => undefined);\n } catch { /* Observers never affect credential or transaction outcomes. */ }\n }\n\n async function resolveShop(shopId: string, deadline: ReturnType<typeof createDeadline>): Promise<PersistedShop> {\n deadline.check();\n if (typeof shopId !== \"string\" || !shopId.trim()) fail(\"SESSION_MISSING\");\n let shop: PersistedShop | null;\n try {\n shop = await deadline.wait(input.prisma.shop.findUnique({ where: { id: shopId }, select: { id: true, domain: true, platform: true, status: true } }));\n } catch (error) {\n deadline.check();\n throw mapDatabaseError(error);\n }\n deadline.check();\n if (!shop) fail(\"SESSION_MISSING\");\n if (shop.platform !== \"SHOPIFY\" || shop.status !== \"ACTIVE\") fail(\"INVALID_SESSION\");\n if (!shop.domain) fail(\"SESSION_IDENTITY_CONFLICT\");\n return shop;\n }\n\n async function runLocked<T>(shop: PersistedShop, shopId: string, deadline: ReturnType<typeof createDeadline>, work: (context: LockedSessionContext) => Promise<T>, outcome: { remoteSucceeded: boolean } = { remoteSucceeded: false }): Promise<T> {\n try {\n deadline.check();\n const result = await deadline.wait(input.prisma.$transaction(async (tx) => {\n await tx.$executeRawUnsafe(\"SET LOCAL lock_timeout = '5s'\");\n await tx.$executeRawUnsafe(\"SET LOCAL statement_timeout = '18s'\");\n await tx.$queryRawUnsafe(\"SELECT pg_advisory_xact_lock(hashtextextended($1, 0)) IS NULL\", `moda:shopify-offline:v1:${shop.domain}`);\n deadline.check();\n\n const currentShop = await tx.shop.findUnique({ where: { id: shopId }, select: { id: true, domain: true, platform: true, status: true } });\n if (!currentShop) fail(\"SESSION_MISSING\");\n if (currentShop.platform !== \"SHOPIFY\" || currentShop.status !== \"ACTIVE\") fail(\"INVALID_SESSION\");\n if (currentShop.domain !== shop.domain) fail(\"SESSION_IDENTITY_CONFLICT\");\n\n const sessions = await tx.session.findMany({ where: { shop: currentShop.domain, isOnline: false }, select: SESSION_SELECT, take: 2 });\n if (sessions.length === 0) fail(\"SESSION_MISSING\");\n if (sessions.length !== 1) fail(\"SESSION_IDENTITY_CONFLICT\");\n const row = sessions[0]!;\n if (!row.id || row.shop !== currentShop.domain || row.isOnline !== false) fail(\"SESSION_IDENTITY_CONFLICT\");\n await tx.$queryRawUnsafe('SELECT \"id\" FROM \"shopify\".\"Session\" WHERE \"id\" = $1 FOR UPDATE', row.id);\n const reread = await tx.session.findUnique({ where: { id: row.id }, select: SESSION_SELECT });\n if (!reread || reread.id !== row.id || reread.shop !== currentShop.domain || reread.isOnline !== false) fail(\"SESSION_IDENTITY_CONFLICT\");\n\n const context: LockedSessionContext = Object.freeze({ transaction: tx, shopId, shop: currentShop.domain, sessionId: reread.id, signal: deadline.signal });\n deadline.check();\n let value: T;\n try { value = await deadline.wait(work(context)); }\n catch (error) { deadline.check(); throw new CallbackFailure(error); }\n deadline.check();\n return value;\n }, TX_OPTIONS));\n return result;\n } catch (error) {\n if (error instanceof CallbackFailure) throw error.value;\n if (deadline.signal.aborted) deadline.check();\n if (outcome.remoteSucceeded && (error instanceof ShopifyOfflineTokenError\n ? error.code === \"LOCK_TIMEOUT\" || error.code === \"DEADLINE\" || error.code === \"PERSISTENCE_UNAVAILABLE\"\n : true)) fail(\"REFRESH_OUTCOME_UNKNOWN\");\n if (error instanceof ShopifyOfflineTokenError) throw error;\n deadline.check();\n throw mapDatabaseError(error);\n }\n }\n\n async function getSession(shopId: string, options?: Readonly<{ signal?: AbortSignal }>): Promise<OfflineCredentialSnapshot> {\n const deadline = createDeadline(options?.signal);\n let attempt: { operationId: string; shopId: string; startedAt: number } | undefined;\n const outcome = { remoteSucceeded: false };\n function emit(event: ShopifyOfflineRefreshEvent[\"event\"], result: ShopifyOfflineRefreshEvent[\"outcome\"], reasonCode: ShopifyOfflineTokenErrorCode | null): void {\n if (!attempt) return;\n notify({ event, operationId: attempt.operationId, shopId: attempt.shopId, source: \"SHARED\",\n durationMs: Math.min(60_000, Math.max(0, Math.round(performance.now() - attempt.startedAt))), outcome: result, reasonCode });\n }\n try {\n deadline.check();\n const shop = await resolveShop(shopId, deadline);\n deadline.check();\n const result = await runLocked(shop, shopId, deadline, async (context) => {\n let row = await context.transaction.session.findUnique({ where: { id: context.sessionId }, select: SESSION_SELECT });\n if (!row) fail(\"SESSION_IDENTITY_CONFLICT\");\n const currentTime = now();\n if (!Number.isFinite(currentTime)) fail(\"INVALID_SESSION\");\n if (row.expires === null && row.refreshToken === null) return snapshot(row, shopId, context.shop);\n if (row.expires === null) fail(\"INVALID_SESSION\");\n const expiresAt = row.expires?.getTime() ?? 0;\n if (expiresAt - currentTime > ACCESS_REFRESH_SKEW_MS) return snapshot(row, shopId, context.shop);\n if (!validToken(row.refreshToken)) fail(\"INVALID_SESSION\");\n if (!validDate(row.refreshTokenExpires) || row.refreshTokenExpires === null) fail(\"INVALID_SESSION\");\n if (row.refreshTokenExpires.getTime() <= currentTime) fail(\"REFRESH_TOKEN_EXPIRED\");\n\n let credentials: Readonly<{ apiKey: string; apiSecret: string }>;\n try { credentials = await input.readOAuth(); }\n catch { fail(\"OAUTH_CONFIG_UNAVAILABLE\"); }\n if (options?.signal?.aborted) fail(\"CANCELLED\");\n if (context.signal.aborted) fail(\"DEADLINE\");\n if (!validToken(credentials.apiKey) || !validToken(credentials.apiSecret)) fail(\"OAUTH_CONFIG_UNAVAILABLE\");\n const requestStartedAt = now();\n const refreshController = new AbortController();\n const refreshTimeout = setTimeout(() => refreshController.abort(), REFRESH_NETWORK_BUDGET_MS);\n const onAbort = () => refreshController.abort();\n context.signal.addEventListener(\"abort\", onAbort, { once: true });\n let response: Response;\n try {\n const form = new URLSearchParams({ client_id: credentials.apiKey, client_secret: credentials.apiSecret, grant_type: \"refresh_token\", refresh_token: row.refreshToken });\n attempt = { operationId: randomUUID(), shopId: context.shopId, startedAt: performance.now() };\n emit(\"shopify.offline_refresh.started\", \"STARTED\", null);\n response = await input.fetch(`https://${context.shop}/admin/oauth/access_token`, {\n method: \"POST\", redirect: \"error\", headers: { \"content-type\": \"application/x-www-form-urlencoded\" }, body: form, signal: refreshController.signal,\n });\n } catch {\n clearTimeout(refreshTimeout);\n context.signal.removeEventListener(\"abort\", onAbort);\n if (options?.signal?.aborted) fail(\"CANCELLED\");\n if (context.signal.aborted) fail(\"REFRESH_OUTCOME_UNKNOWN\");\n fail(\"REFRESH_OUTCOME_UNKNOWN\");\n }\n if (response.status >= 400 && response.status < 500) fail(\"REFRESH_REJECTED\");\n if (!response.ok) { outcome.remoteSucceeded = true; fail(\"REFRESH_OUTCOME_UNKNOWN\"); }\n outcome.remoteSucceeded = true;\n let raw: string;\n try { raw = await readBoundedBody(response, refreshController.signal); }\n catch (error) {\n clearTimeout(refreshTimeout);\n context.signal.removeEventListener(\"abort\", onAbort);\n if (options?.signal?.aborted) fail(\"CANCELLED\");\n if (context.signal.aborted) fail(\"REFRESH_OUTCOME_UNKNOWN\");\n fail(\"REFRESH_OUTCOME_UNKNOWN\");\n } finally {\n clearTimeout(refreshTimeout);\n context.signal.removeEventListener(\"abort\", onAbort);\n }\n let payload: unknown;\n try { payload = JSON.parse(raw) as unknown; } catch { fail(\"REFRESH_OUTCOME_UNKNOWN\"); }\n if (!isRecord(payload) || !validToken(payload.access_token) || !validToken(payload.refresh_token)\n || !Number.isSafeInteger(payload.expires_in) || (payload.expires_in as number) <= 0 || (payload.expires_in as number) > 86_400\n || !Number.isSafeInteger(payload.refresh_token_expires_in) || (payload.refresh_token_expires_in as number) <= 0 || (payload.refresh_token_expires_in as number) > 7_776_000\n || (payload.expires_in as number) <= 320\n || (payload.scope !== undefined && !validScope(payload.scope))) fail(\"REFRESH_OUTCOME_UNKNOWN\");\n const expires = requestStartedAt + (payload.expires_in as number) * 1000;\n const refreshTokenExpires = requestStartedAt + (payload.refresh_token_expires_in as number) * 1000;\n if (!Number.isFinite(expires) || !Number.isFinite(refreshTokenExpires) || Number.isNaN(new Date(expires).getTime()) || Number.isNaN(new Date(refreshTokenExpires).getTime())) fail(\"REFRESH_OUTCOME_UNKNOWN\");\n const scope = payload.scope === undefined ? row.scope : payload.scope as string;\n try {\n row = await context.transaction.session.update({ where: { id: row.id }, data: {\n accessToken: payload.access_token, expires: new Date(expires), refreshToken: payload.refresh_token,\n refreshTokenExpires: new Date(refreshTokenExpires), scope,\n }, select: SESSION_SELECT });\n } catch { outcome.remoteSucceeded = true; throw new ShopifyOfflineTokenError(\"REFRESH_OUTCOME_UNKNOWN\"); }\n outcome.remoteSucceeded = true;\n return snapshot(row, shopId, context.shop);\n }, outcome);\n emit(\"shopify.offline_refresh.committed\", \"COMMITTED\", null);\n return result;\n } catch (error) {\n let failure = error;\n try { deadline.check(); } catch (deadlineError) { failure = deadlineError; }\n const code = failure instanceof ShopifyOfflineTokenError ? failure.code : \"PERSISTENCE_UNAVAILABLE\";\n const ambiguous = outcome.remoteSucceeded && code !== \"REFRESH_REJECTED\" && code !== \"CANCELLED\";\n const reason = ambiguous ? \"REFRESH_OUTCOME_UNKNOWN\" : code;\n emit(\"shopify.offline_refresh.failed\", reason === \"REFRESH_REJECTED\" ? \"REJECTED\"\n : reason === \"CANCELLED\" ? \"CANCELLED\" : reason === \"REFRESH_OUTCOME_UNKNOWN\" ? \"UNKNOWN\" : \"UNAVAILABLE\", reason);\n throw failure;\n } finally { deadline.dispose(); }\n }\n\n async function withSessionLock<T>(shopId: string, work: (context: LockedSessionContext) => Promise<T>, options?: Readonly<{ signal?: AbortSignal }>): Promise<T> {\n const deadline = createDeadline(options?.signal);\n try {\n deadline.check();\n const shop = await resolveShop(shopId, deadline);\n deadline.check();\n return await runLocked(shop, shopId, deadline, work);\n } catch (error) {\n deadline.check();\n throw error;\n } finally { deadline.dispose(); }\n }\n\n async function withSdkInstallationSessionLock<T>(integrationDomain: string, work: (context: SdkInstallationSessionContext) => Promise<T>): Promise<T> {\n if (typeof integrationDomain !== \"string\" || !integrationDomain.trim()) fail(\"INVALID_SESSION\");\n const deadline = createDeadline();\n try {\n return await input.prisma.$transaction(async (tx) => {\n await tx.$executeRawUnsafe(\"SET LOCAL lock_timeout = '5s'\");\n await tx.$executeRawUnsafe(\"SET LOCAL statement_timeout = '18s'\");\n await tx.$queryRawUnsafe(\"SELECT pg_advisory_xact_lock(hashtextextended($1, 0)) IS NULL\", `moda:shopify-offline:v1:${integrationDomain}`);\n let value: T;\n try { value = await deadline.wait(work(Object.freeze({ transaction: tx, shopId: null, shop: integrationDomain, sessionId: null, signal: deadline.signal }))); }\n catch (error) { deadline.check(); throw new CallbackFailure(error); }\n deadline.check();\n return value;\n }, TX_OPTIONS);\n } catch (error) {\n if (error instanceof CallbackFailure) throw error.value;\n if (error instanceof ShopifyOfflineTokenError) throw error;\n if (deadline.signal.aborted) deadline.check();\n throw mapDatabaseError(error);\n } finally { deadline.dispose(); }\n }\n\n return Object.freeze({\n withSdkInstallationSessionLock,\n getAccessToken: async (shopId: string, options?: Readonly<{ signal?: AbortSignal }>) => (await getSession(shopId, options)).accessToken,\n getSession,\n withSessionLock,\n });\n}\n","import { Buffer } from \"node:buffer\";\nimport {\n decryptCredential,\n type CredentialEnvelope,\n type CredentialKeyring,\n} from \"../security/credentials/node.js\";\n\nconst CONFIGURATION_KEY = \"shopify.oauth\" as const;\nconst CONFIGURATION_ENVIRONMENTS = new Set([\"local\", \"test\", \"development\", \"staging\", \"production\"]);\nconst ENVELOPE_KEYS = [\"kind\", \"schemaVersion\", \"algorithm\", \"wrappingKeyId\", \"nonce\", \"ciphertext\", \"authTag\"] as const;\nconst RESERVED_KEY_IDS = new Set([\"__proto__\", \"prototype\", \"constructor\"]);\nconst KEY_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;\nconst BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;\nconst CONTROL_CHARACTERS = /[\\u0000-\\u001f\\u007f]/;\n\nexport type ShopifyOAuthConfigurationErrorCode =\n | \"INVALID_ENVIRONMENT\"\n | \"CONFIGURATION_UNAVAILABLE\"\n | \"CONFIGURATION_INVALID\";\n\nconst ERROR_MESSAGES: Record<ShopifyOAuthConfigurationErrorCode, string> = {\n INVALID_ENVIRONMENT: \"Shopify OAuth configuration is unavailable.\",\n CONFIGURATION_UNAVAILABLE: \"Shopify OAuth configuration is unavailable.\",\n CONFIGURATION_INVALID: \"Shopify OAuth configuration is invalid.\",\n};\n\n/** Safe configuration boundary error; never contains a row, key, or decrypt cause. */\nexport class ShopifyOAuthConfigurationError extends Error {\n readonly code: ShopifyOAuthConfigurationErrorCode;\n\n constructor(code: ShopifyOAuthConfigurationErrorCode) {\n super(ERROR_MESSAGES[code]);\n this.name = \"ShopifyOAuthConfigurationError\";\n this.code = code;\n }\n}\n\nexport type ShopifyOAuthConfigurationRow = Readonly<{\n environment: string;\n key: string;\n version: number;\n value: unknown;\n}>;\n\nexport type ShopifyOAuthConfigurationPrisma = Readonly<{\n platformConfiguration: Readonly<{\n findUnique(args: Readonly<{\n where: Readonly<{ environment_key: Readonly<{ environment: string; key: typeof CONFIGURATION_KEY }> }>;\n select: Readonly<{ environment: true; key: true; version: true; value: true }>;\n }>): Promise<ShopifyOAuthConfigurationRow | null>;\n }>;\n}>;\n\nexport type ShopifyOAuthCredentials = Readonly<{\n apiKey: string;\n apiSecret: string;\n}>;\n\nfunction configurationFailure(code: ShopifyOAuthConfigurationErrorCode): never {\n throw new ShopifyOAuthConfigurationError(code);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasExactKeys(value: Record<string, unknown>, expected: readonly string[]): boolean {\n const actual = Object.keys(value);\n return actual.length === expected.length && actual.every((key) => expected.includes(key));\n}\n\nfunction canonicalBase64(value: unknown, exactLength: number | null): Uint8Array | null {\n if (typeof value !== \"string\" || value.length === 0 || !BASE64_PATTERN.test(value)) return null;\n const decoded = Buffer.from(value, \"base64\");\n if (decoded.length < 1 || decoded.length > 8192 || decoded.toString(\"base64\") !== value) return null;\n if (exactLength !== null && decoded.length !== exactLength) return null;\n return Uint8Array.from(decoded);\n}\n\nfunction parseSealedEnvelope(value: unknown): CredentialEnvelope {\n if (!isRecord(value) || !hasExactKeys(value, ENVELOPE_KEYS)\n || value.kind !== \"sealed\"\n || value.schemaVersion !== 1\n || value.algorithm !== \"AES-256-GCM\"\n || typeof value.wrappingKeyId !== \"string\"\n || !KEY_ID_PATTERN.test(value.wrappingKeyId)\n || RESERVED_KEY_IDS.has(value.wrappingKeyId)) {\n return configurationFailure(\"CONFIGURATION_INVALID\");\n }\n\n const nonce = canonicalBase64(value.nonce, 12);\n const ciphertext = canonicalBase64(value.ciphertext, null);\n const authTag = canonicalBase64(value.authTag, 16);\n if (!nonce || !ciphertext || !authTag) return configurationFailure(\"CONFIGURATION_INVALID\");\n return { keyId: value.wrappingKeyId, nonce, ciphertext, authTag };\n}\n\nfunction validCredential(value: unknown): value is string {\n return typeof value === \"string\" && value.length >= 1 && value.length <= 4096\n && value.trim() === value && !CONTROL_CHARACTERS.test(value);\n}\n\nfunction parseCredentials(value: unknown): ShopifyOAuthCredentials {\n if (!isRecord(value) || !hasExactKeys(value, [\"apiKey\", \"apiSecret\"])\n || !validCredential(value.apiKey) || !validCredential(value.apiSecret)) {\n return configurationFailure(\"CONFIGURATION_INVALID\");\n }\n return Object.freeze({ apiKey: value.apiKey, apiSecret: value.apiSecret });\n}\n\n/**\n * Read and unseal the existing platform `shopify.oauth` row. Environment identity\n * is deployment configuration supplied by the caller, never request data.\n */\nexport async function readShopifyOAuthConfiguration(input: Readonly<{\n prisma: ShopifyOAuthConfigurationPrisma;\n environment: string;\n unsealKeyring: CredentialKeyring;\n}>): Promise<ShopifyOAuthCredentials> {\n if (!input || typeof input !== \"object\" || Array.isArray(input)\n || typeof input.environment !== \"string\" || !CONFIGURATION_ENVIRONMENTS.has(input.environment)) {\n return configurationFailure(\"INVALID_ENVIRONMENT\");\n }\n\n let row: ShopifyOAuthConfigurationRow | null;\n try {\n row = await input.prisma.platformConfiguration.findUnique({\n where: { environment_key: { environment: input.environment, key: CONFIGURATION_KEY } },\n select: { environment: true, key: true, version: true, value: true },\n });\n } catch {\n return configurationFailure(\"CONFIGURATION_UNAVAILABLE\");\n }\n\n if (!row) return configurationFailure(\"CONFIGURATION_UNAVAILABLE\");\n if (row.environment !== input.environment || row.key !== CONFIGURATION_KEY\n || !Number.isSafeInteger(row.version) || row.version < 1 || row.version > 2_147_483_647) {\n return configurationFailure(\"CONFIGURATION_INVALID\");\n }\n if (isRecord(row.value) && hasExactKeys(row.value, [\"kind\", \"schemaVersion\"])\n && row.value.kind === \"disabled\" && row.value.schemaVersion === 1) {\n return configurationFailure(\"CONFIGURATION_UNAVAILABLE\");\n }\n\n let plaintext: string;\n try {\n const envelope = parseSealedEnvelope(row.value);\n const aad = JSON.stringify([\"moda-platform-config-v1\", input.environment, CONFIGURATION_KEY, row.version, 1]);\n plaintext = decryptCredential({ keyring: input.unsealKeyring, envelope, aad });\n } catch (error) {\n if (error instanceof ShopifyOAuthConfigurationError) throw error;\n return configurationFailure(\"CONFIGURATION_INVALID\");\n }\n\n try {\n return parseCredentials(JSON.parse(plaintext) as unknown);\n } catch (error) {\n if (error instanceof ShopifyOAuthConfigurationError) throw error;\n return configurationFailure(\"CONFIGURATION_INVALID\");\n }\n}\n"],"mappings":";;;;;;;;;;;AACA,SAAS,kBAAkB;;;ACD3B,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB;AAmB5B,IAAM,iBAA+D;AAAA,EACnE,iBAAiB;AAAA,EACjB,2BAA2B;AAAA,EAC3B,iBAAiB;AAAA,EACjB,uBAAuB;AAAA,EACvB,0BAA0B;AAAA,EAC1B,cAAc;AAAA,EACd,WAAW;AAAA,EACX,UAAU;AAAA,EACV,kBAAkB;AAAA,EAClB,0BAA0B;AAAA,EAC1B,yBAAyB;AAAA,EACzB,yBAAyB;AAC3B;AAEA,IAAM,YAAY,oBAAI,IAAkC;AAAA,EACtD;AAAA,EAAgB;AAAA,EAAY;AAAA,EAA2B;AACzD,CAAC;AAEM,IAAM,2BAAN,cAAuC,MAAM;AAAA,EACzC;AAAA,EACA;AAAA,EAET,YAAY,MAAoC;AAC9C,UAAM,eAAe,IAAI,CAAC;AAC1B,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,YAAY,UAAU,IAAI,IAAI;AAAA,EACrC;AACF;AAEA,IAAM,kBAAN,cAA8B,MAAM;AAAA,EAClC,YAAqB,OAAgB;AAAE,UAAM,kCAAkC;AAA1D;AAAA,EAA6D;AAAA,EAA7D;AACvB;AAwIA,IAAM,aAAa,EAAE,SAAS,KAAM,SAAS,KAAO,gBAAgB,gBAAgB;AACpF,IAAM,yBAAyB;AAC/B,IAAM,4BAA4B;AAClC,IAAM,sBAAsB;AAC5B,IAAM,iBAAiB;AACvB,IAAM,UAAU;AAChB,IAAM,iBAAiB,EAAE,IAAI,MAAM,MAAM,MAAM,UAAU,MAAM,aAAa,MAAM,cAAc,MAAM,OAAO,MAAM,SAAS,MAAM,qBAAqB,KAAK;AAE5J,SAAS,KAAK,MAA2C;AAAE,QAAM,IAAI,yBAAyB,IAAI;AAAG;AACrG,SAAS,SAAS,OAAkD;AAAE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAAG;AACnJ,SAAS,WAAW,OAAiC;AACnD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,KAAK,MAAM,UAAU,QAAQ,MAAM,KAAK,MAAM,SAAS,CAAC,eAAe,KAAK,KAAK;AACtI;AACA,SAAS,WAAW,OAAiC;AAAE,SAAO,OAAO,UAAU,YAAY,MAAM,UAAU,QAAQ,CAAC,QAAQ,KAAK,KAAK;AAAG;AACzI,SAAS,UAAU,OAAsC;AAAE,SAAO,UAAU,QAAS,iBAAiB,QAAQ,OAAO,SAAS,MAAM,QAAQ,CAAC;AAAI;AACjJ,SAAS,iBAAiB,OAA0C;AAClE,QAAM,OAAO,SAAS,KAAK,KAAK,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO;AAC9E,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,MAAI,SAAS,WAAW,gBAAgB,KAAK,OAAO,EAAG,QAAO,IAAI,yBAAyB,cAAc;AACzG,MAAI,SAAS,WAAW,yCAAyC,KAAK,OAAO,EAAG,QAAO,IAAI,yBAAyB,UAAU;AAC9H,SAAO,IAAI,yBAAyB,yBAAyB;AAC/D;AAEA,SAAS,eAAe,QAAgI;AACtJ,QAAM,aAAa,IAAI,gBAAgB;AACvC,MAAI,UAAU;AACd,QAAM,QAAQ,WAAW,MAAM;AAAE,cAAU;AAAM,eAAW,MAAM;AAAA,EAAG,GAAG,mBAAmB;AAC3F,QAAM,UAAU,MAAM,WAAW,MAAM,QAAQ,MAAM;AACrD,MAAI,QAAQ,QAAS,SAAQ;AAC7B,UAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AACzD,SAAO;AAAA,IACL,QAAQ,WAAW;AAAA,IACnB,UAAU;AAAE,mBAAa,KAAK;AAAG,cAAQ,oBAAoB,SAAS,OAAO;AAAA,IAAG;AAAA,IAChF,KAAQ,MAA8B;AACpC,UAAI,WAAW,OAAO,QAAS,MAAK,MAAM;AAC1C,UAAI;AACJ,YAAM,UAAU,IAAI,QAAe,CAAC,UAAU,WAAW;AACvD,qBAAa,MAAM,OAAO,IAAI,MAAM,mBAAmB,CAAC;AACxD,mBAAW,OAAO,iBAAiB,SAAS,YAAY,EAAE,MAAM,KAAK,CAAC;AAAA,MACxE,CAAC;AACD,aAAO,QAAQ,KAAK,CAAC,MAAM,OAAO,CAAC,EAAE,QAAQ,MAAM;AACjD,YAAI,WAAY,YAAW,OAAO,oBAAoB,SAAS,UAAU;AAAA,MAC3E,CAAC;AAAA,IACH;AAAA,IACA,QAAQ;AACN,UAAI,QAAQ,QAAS,MAAK,WAAW;AACrC,UAAI,QAAS,MAAK,UAAU;AAC5B,UAAI,WAAW,OAAO,QAAS,MAAK,UAAU;AAAA,IAChD;AAAA,EACF;AACF;AAEA,eAAe,gBAAgB,UAAoB,QAAsC;AACvF,MAAI,CAAC,SAAS,KAAM,QAAO;AAC3B,QAAM,SAAS,SAAS,KAAK,UAAU;AACvC,QAAM,SAAuB,CAAC;AAC9B,MAAI,OAAO;AACX,MAAI;AACF,WAAO,MAAM;AACX,UAAI,OAAO,QAAS,OAAM,IAAI,MAAM,wBAAwB;AAC5D,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,UAAI,KAAM;AACV,cAAQ,MAAM;AACd,UAAI,OAAO,gBAAgB;AACzB,cAAM,OAAO,OAAO,EAAE,MAAM,MAAM,MAAS;AAC3C,aAAK,yBAAyB;AAAA,MAChC;AACA,aAAO,KAAK,KAAK;AAAA,IACnB;AAAA,EACF,UAAE;AAAU,WAAO,YAAY;AAAA,EAAG;AAClC,SAAO,OAAO,OAAO,OAAO,IAAI,CAAC,UAAU,OAAO,KAAK,KAAK,CAAC,CAAC,EAAE,SAAS,MAAM;AACjF;AAEA,SAAS,SAAS,KAAkC,QAAgB,MAAyC;AAC3G,MAAI,CAAC,WAAW,IAAI,WAAW,KAAK,CAAC,WAAW,IAAI,KAAK,KAAK,IAAI,UAAU,QACvE,CAAC,UAAU,IAAI,OAAO,KAAK,CAAC,UAAU,IAAI,mBAAmB,EAAG,MAAK,iBAAiB;AAC3F,SAAO,OAAO,OAAO,EAAE,IAAI,IAAI,IAAI,QAAQ,MAAM,aAAa,IAAI,aAAa,OAAO,IAAI,OAAO,SAAS,IAAI,SAAS,qBAAqB,IAAI,oBAAoB,CAAC;AACvK;AAEO,SAAS,kCAAkC,OAMjB;AAC/B,QAAM,MAAM,MAAM,OAAO,KAAK;AAC9B,WAAS,OAAO,OAAyC;AACvD,QAAI;AAEF,WAAK,QAAQ,QAAQ,MAAM,UAAU,OAAO,OAAO,KAAK,CAAC,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,IACnF,QAAQ;AAAA,IAAmE;AAAA,EAC7E;AAEA,iBAAe,YAAY,QAAgB,UAAqE;AAC9G,aAAS,MAAM;AACf,QAAI,OAAO,WAAW,YAAY,CAAC,OAAO,KAAK,EAAG,MAAK,iBAAiB;AACxE,QAAI;AACJ,QAAI;AACF,aAAO,MAAM,SAAS,KAAK,MAAM,OAAO,KAAK,WAAW,EAAE,OAAO,EAAE,IAAI,OAAO,GAAG,QAAQ,EAAE,IAAI,MAAM,QAAQ,MAAM,UAAU,MAAM,QAAQ,KAAK,EAAE,CAAC,CAAC;AAAA,IACtJ,SAAS,OAAO;AACd,eAAS,MAAM;AACf,YAAM,iBAAiB,KAAK;AAAA,IAC9B;AACA,aAAS,MAAM;AACf,QAAI,CAAC,KAAM,MAAK,iBAAiB;AACjC,QAAI,KAAK,aAAa,aAAa,KAAK,WAAW,SAAU,MAAK,iBAAiB;AACnF,QAAI,CAAC,KAAK,OAAQ,MAAK,2BAA2B;AAClD,WAAO;AAAA,EACT;AAEA,iBAAe,UAAa,MAAqB,QAAgB,UAA6C,MAAqD,UAAwC,EAAE,iBAAiB,MAAM,GAAe;AACjP,QAAI;AACF,eAAS,MAAM;AACf,YAAM,SAAS,MAAM,SAAS,KAAK,MAAM,OAAO,aAAa,OAAO,OAAO;AACzE,cAAM,GAAG,kBAAkB,+BAA+B;AAC1D,cAAM,GAAG,kBAAkB,qCAAqC;AAChE,cAAM,GAAG,gBAAgB,iEAAiE,2BAA2B,KAAK,MAAM,EAAE;AAClI,iBAAS,MAAM;AAEf,cAAM,cAAc,MAAM,GAAG,KAAK,WAAW,EAAE,OAAO,EAAE,IAAI,OAAO,GAAG,QAAQ,EAAE,IAAI,MAAM,QAAQ,MAAM,UAAU,MAAM,QAAQ,KAAK,EAAE,CAAC;AACxI,YAAI,CAAC,YAAa,MAAK,iBAAiB;AACxC,YAAI,YAAY,aAAa,aAAa,YAAY,WAAW,SAAU,MAAK,iBAAiB;AACjG,YAAI,YAAY,WAAW,KAAK,OAAQ,MAAK,2BAA2B;AAExE,cAAM,WAAW,MAAM,GAAG,QAAQ,SAAS,EAAE,OAAO,EAAE,MAAM,YAAY,QAAQ,UAAU,MAAM,GAAG,QAAQ,gBAAgB,MAAM,EAAE,CAAC;AACpI,YAAI,SAAS,WAAW,EAAG,MAAK,iBAAiB;AACjD,YAAI,SAAS,WAAW,EAAG,MAAK,2BAA2B;AAC3D,cAAM,MAAM,SAAS,CAAC;AACtB,YAAI,CAAC,IAAI,MAAM,IAAI,SAAS,YAAY,UAAU,IAAI,aAAa,MAAO,MAAK,2BAA2B;AAC1G,cAAM,GAAG,gBAAgB,mEAAmE,IAAI,EAAE;AAClG,cAAM,SAAS,MAAM,GAAG,QAAQ,WAAW,EAAE,OAAO,EAAE,IAAI,IAAI,GAAG,GAAG,QAAQ,eAAe,CAAC;AAC5F,YAAI,CAAC,UAAU,OAAO,OAAO,IAAI,MAAM,OAAO,SAAS,YAAY,UAAU,OAAO,aAAa,MAAO,MAAK,2BAA2B;AAExI,cAAM,UAAgC,OAAO,OAAO,EAAE,aAAa,IAAI,QAAQ,MAAM,YAAY,QAAQ,WAAW,OAAO,IAAI,QAAQ,SAAS,OAAO,CAAC;AACxJ,iBAAS,MAAM;AACf,YAAI;AACJ,YAAI;AAAE,kBAAQ,MAAM,SAAS,KAAK,KAAK,OAAO,CAAC;AAAA,QAAG,SAC3C,OAAO;AAAE,mBAAS,MAAM;AAAG,gBAAM,IAAI,gBAAgB,KAAK;AAAA,QAAG;AACpE,iBAAS,MAAM;AACf,eAAO;AAAA,MACT,GAAG,UAAU,CAAC;AACd,aAAO;AAAA,IACT,SAAS,OAAO;AACd,UAAI,iBAAiB,gBAAiB,OAAM,MAAM;AAClD,UAAI,SAAS,OAAO,QAAS,UAAS,MAAM;AAC5C,UAAI,QAAQ,oBAAoB,iBAAiB,2BAC7C,MAAM,SAAS,kBAAkB,MAAM,SAAS,cAAc,MAAM,SAAS,4BAC7E,MAAO,MAAK,yBAAyB;AACzC,UAAI,iBAAiB,yBAA0B,OAAM;AACrD,eAAS,MAAM;AACf,YAAM,iBAAiB,KAAK;AAAA,IAC9B;AAAA,EACF;AAEA,iBAAe,WAAW,QAAgB,SAAkF;AAC1H,UAAM,WAAW,eAAe,SAAS,MAAM;AAC/C,QAAI;AACJ,UAAM,UAAU,EAAE,iBAAiB,MAAM;AACzC,aAAS,KAAK,OAA4C,QAA+C,YAAuD;AAC9J,UAAI,CAAC,QAAS;AACd,aAAO;AAAA,QAAE;AAAA,QAAO,aAAa,QAAQ;AAAA,QAAa,QAAQ,QAAQ;AAAA,QAAQ,QAAQ;AAAA,QAChF,YAAY,KAAK,IAAI,KAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,YAAY,IAAI,IAAI,QAAQ,SAAS,CAAC,CAAC;AAAA,QAAG,SAAS;AAAA,QAAQ;AAAA,MAAW,CAAC;AAAA,IAC/H;AACA,QAAI;AACF,eAAS,MAAM;AACf,YAAM,OAAO,MAAM,YAAY,QAAQ,QAAQ;AAC/C,eAAS,MAAM;AACf,YAAM,SAAS,MAAM,UAAU,MAAM,QAAQ,UAAU,OAAO,YAAY;AAC1E,YAAI,MAAM,MAAM,QAAQ,YAAY,QAAQ,WAAW,EAAE,OAAO,EAAE,IAAI,QAAQ,UAAU,GAAG,QAAQ,eAAe,CAAC;AACnH,YAAI,CAAC,IAAK,MAAK,2BAA2B;AAC1C,cAAM,cAAc,IAAI;AACxB,YAAI,CAAC,OAAO,SAAS,WAAW,EAAG,MAAK,iBAAiB;AACzD,YAAI,IAAI,YAAY,QAAQ,IAAI,iBAAiB,KAAM,QAAO,SAAS,KAAK,QAAQ,QAAQ,IAAI;AAChG,YAAI,IAAI,YAAY,KAAM,MAAK,iBAAiB;AAChD,cAAM,YAAY,IAAI,SAAS,QAAQ,KAAK;AAC5C,YAAI,YAAY,cAAc,uBAAwB,QAAO,SAAS,KAAK,QAAQ,QAAQ,IAAI;AAC/F,YAAI,CAAC,WAAW,IAAI,YAAY,EAAG,MAAK,iBAAiB;AACzD,YAAI,CAAC,UAAU,IAAI,mBAAmB,KAAK,IAAI,wBAAwB,KAAM,MAAK,iBAAiB;AACnG,YAAI,IAAI,oBAAoB,QAAQ,KAAK,YAAa,MAAK,uBAAuB;AAElF,YAAI;AACJ,YAAI;AAAE,wBAAc,MAAM,MAAM,UAAU;AAAA,QAAG,QACvC;AAAE,eAAK,0BAA0B;AAAA,QAAG;AAC1C,YAAI,SAAS,QAAQ,QAAS,MAAK,WAAW;AAC9C,YAAI,QAAQ,OAAO,QAAS,MAAK,UAAU;AAC3C,YAAI,CAAC,WAAW,YAAY,MAAM,KAAK,CAAC,WAAW,YAAY,SAAS,EAAG,MAAK,0BAA0B;AAC1G,cAAM,mBAAmB,IAAI;AAC7B,cAAM,oBAAoB,IAAI,gBAAgB;AAC9C,cAAM,iBAAiB,WAAW,MAAM,kBAAkB,MAAM,GAAG,yBAAyB;AAC5F,cAAM,UAAU,MAAM,kBAAkB,MAAM;AAC9C,gBAAQ,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAChE,YAAI;AACJ,YAAI;AACF,gBAAM,OAAO,IAAI,gBAAgB,EAAE,WAAW,YAAY,QAAQ,eAAe,YAAY,WAAW,YAAY,iBAAiB,eAAe,IAAI,aAAa,CAAC;AACtK,oBAAU,EAAE,aAAa,WAAW,GAAG,QAAQ,QAAQ,QAAQ,WAAW,YAAY,IAAI,EAAE;AAC5F,eAAK,mCAAmC,WAAW,IAAI;AACvD,qBAAW,MAAM,MAAM,MAAM,WAAW,QAAQ,IAAI,6BAA6B;AAAA,YAC/E,QAAQ;AAAA,YAAQ,UAAU;AAAA,YAAS,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,YAAG,MAAM;AAAA,YAAM,QAAQ,kBAAkB;AAAA,UAC7I,CAAC;AAAA,QACH,QAAQ;AACN,uBAAa,cAAc;AAC3B,kBAAQ,OAAO,oBAAoB,SAAS,OAAO;AACnD,cAAI,SAAS,QAAQ,QAAS,MAAK,WAAW;AAC9C,cAAI,QAAQ,OAAO,QAAS,MAAK,yBAAyB;AAC1D,eAAK,yBAAyB;AAAA,QAChC;AACA,YAAI,SAAS,UAAU,OAAO,SAAS,SAAS,IAAK,MAAK,kBAAkB;AAC5E,YAAI,CAAC,SAAS,IAAI;AAAE,kBAAQ,kBAAkB;AAAM,eAAK,yBAAyB;AAAA,QAAG;AACrF,gBAAQ,kBAAkB;AAC1B,YAAI;AACJ,YAAI;AAAE,gBAAM,MAAM,gBAAgB,UAAU,kBAAkB,MAAM;AAAA,QAAG,SAChE,OAAO;AACZ,uBAAa,cAAc;AAC3B,kBAAQ,OAAO,oBAAoB,SAAS,OAAO;AACnD,cAAI,SAAS,QAAQ,QAAS,MAAK,WAAW;AAC9C,cAAI,QAAQ,OAAO,QAAS,MAAK,yBAAyB;AAC1D,eAAK,yBAAyB;AAAA,QAChC,UAAE;AACA,uBAAa,cAAc;AAC3B,kBAAQ,OAAO,oBAAoB,SAAS,OAAO;AAAA,QACrD;AACA,YAAI;AACJ,YAAI;AAAE,oBAAU,KAAK,MAAM,GAAG;AAAA,QAAc,QAAQ;AAAE,eAAK,yBAAyB;AAAA,QAAG;AACvF,YAAI,CAAC,SAAS,OAAO,KAAK,CAAC,WAAW,QAAQ,YAAY,KAAK,CAAC,WAAW,QAAQ,aAAa,KAC3F,CAAC,OAAO,cAAc,QAAQ,UAAU,KAAM,QAAQ,cAAyB,KAAM,QAAQ,aAAwB,SACrH,CAAC,OAAO,cAAc,QAAQ,wBAAwB,KAAM,QAAQ,4BAAuC,KAAM,QAAQ,2BAAsC,UAC9J,QAAQ,cAAyB,OACjC,QAAQ,UAAU,UAAa,CAAC,WAAW,QAAQ,KAAK,EAAI,MAAK,yBAAyB;AAChG,cAAM,UAAU,mBAAoB,QAAQ,aAAwB;AACpE,cAAM,sBAAsB,mBAAoB,QAAQ,2BAAsC;AAC9F,YAAI,CAAC,OAAO,SAAS,OAAO,KAAK,CAAC,OAAO,SAAS,mBAAmB,KAAK,OAAO,MAAM,IAAI,KAAK,OAAO,EAAE,QAAQ,CAAC,KAAK,OAAO,MAAM,IAAI,KAAK,mBAAmB,EAAE,QAAQ,CAAC,EAAG,MAAK,yBAAyB;AAC5M,cAAM,QAAQ,QAAQ,UAAU,SAAY,IAAI,QAAQ,QAAQ;AAChE,YAAI;AACF,gBAAM,MAAM,QAAQ,YAAY,QAAQ,OAAO,EAAE,OAAO,EAAE,IAAI,IAAI,GAAG,GAAG,MAAM;AAAA,YAC5E,aAAa,QAAQ;AAAA,YAAc,SAAS,IAAI,KAAK,OAAO;AAAA,YAAG,cAAc,QAAQ;AAAA,YACrF,qBAAqB,IAAI,KAAK,mBAAmB;AAAA,YAAG;AAAA,UACtD,GAAG,QAAQ,eAAe,CAAC;AAAA,QAC7B,QAAQ;AAAE,kBAAQ,kBAAkB;AAAM,gBAAM,IAAI,yBAAyB,yBAAyB;AAAA,QAAG;AACzG,gBAAQ,kBAAkB;AAC1B,eAAO,SAAS,KAAK,QAAQ,QAAQ,IAAI;AAAA,MACzC,GAAG,OAAO;AACV,WAAK,qCAAqC,aAAa,IAAI;AAC3D,aAAO;AAAA,IACT,SAAS,OAAO;AACd,UAAI,UAAU;AACd,UAAI;AAAE,iBAAS,MAAM;AAAA,MAAG,SAAS,eAAe;AAAE,kBAAU;AAAA,MAAe;AAC3E,YAAM,OAAO,mBAAmB,2BAA2B,QAAQ,OAAO;AAC1E,YAAM,YAAY,QAAQ,mBAAmB,SAAS,sBAAsB,SAAS;AACrF,YAAM,SAAS,YAAY,4BAA4B;AACvD,WAAK,kCAAkC,WAAW,qBAAqB,aACnE,WAAW,cAAc,cAAc,WAAW,4BAA4B,YAAY,eAAe,MAAM;AACnH,YAAM;AAAA,IACR,UAAE;AAAU,eAAS,QAAQ;AAAA,IAAG;AAAA,EAClC;AAEA,iBAAe,gBAAmB,QAAgB,MAAqD,SAA0D;AAC/J,UAAM,WAAW,eAAe,SAAS,MAAM;AAC/C,QAAI;AACF,eAAS,MAAM;AACf,YAAM,OAAO,MAAM,YAAY,QAAQ,QAAQ;AAC/C,eAAS,MAAM;AACf,aAAO,MAAM,UAAU,MAAM,QAAQ,UAAU,IAAI;AAAA,IACrD,SAAS,OAAO;AACd,eAAS,MAAM;AACf,YAAM;AAAA,IACR,UAAE;AAAU,eAAS,QAAQ;AAAA,IAAG;AAAA,EAClC;AAEA,iBAAe,+BAAkC,mBAA2B,MAA0E;AACpJ,QAAI,OAAO,sBAAsB,YAAY,CAAC,kBAAkB,KAAK,EAAG,MAAK,iBAAiB;AAC9F,UAAM,WAAW,eAAe;AAChC,QAAI;AACF,aAAO,MAAM,MAAM,OAAO,aAAa,OAAO,OAAO;AACnD,cAAM,GAAG,kBAAkB,+BAA+B;AAC1D,cAAM,GAAG,kBAAkB,qCAAqC;AAChE,cAAM,GAAG,gBAAgB,iEAAiE,2BAA2B,iBAAiB,EAAE;AACxI,YAAI;AACJ,YAAI;AAAE,kBAAQ,MAAM,SAAS,KAAK,KAAK,OAAO,OAAO,EAAE,aAAa,IAAI,QAAQ,MAAM,MAAM,mBAAmB,WAAW,MAAM,QAAQ,SAAS,OAAO,CAAC,CAAC,CAAC;AAAA,QAAG,SACvJ,OAAO;AAAE,mBAAS,MAAM;AAAG,gBAAM,IAAI,gBAAgB,KAAK;AAAA,QAAG;AACpE,iBAAS,MAAM;AACf,eAAO;AAAA,MACT,GAAG,UAAU;AAAA,IACf,SAAS,OAAO;AACd,UAAI,iBAAiB,gBAAiB,OAAM,MAAM;AAClD,UAAI,iBAAiB,yBAA0B,OAAM;AACrD,UAAI,SAAS,OAAO,QAAS,UAAS,MAAM;AAC5C,YAAM,iBAAiB,KAAK;AAAA,IAC9B,UAAE;AAAU,eAAS,QAAQ;AAAA,IAAG;AAAA,EAClC;AAEA,SAAO,OAAO,OAAO;AAAA,IACnB;AAAA,IACA,gBAAgB,OAAO,QAAgB,aAAkD,MAAM,WAAW,QAAQ,OAAO,GAAG;AAAA,IAC5H;AAAA,IACA;AAAA,EACF,CAAC;AACH;;;ACveA,SAAS,UAAAA,eAAc;AAOvB,IAAM,oBAAoB;AAC1B,IAAM,6BAA6B,oBAAI,IAAI,CAAC,SAAS,QAAQ,eAAe,WAAW,YAAY,CAAC;AACpG,IAAM,gBAAgB,CAAC,QAAQ,iBAAiB,aAAa,iBAAiB,SAAS,cAAc,SAAS;AAC9G,IAAM,mBAAmB,oBAAI,IAAI,CAAC,aAAa,aAAa,aAAa,CAAC;AAC1E,IAAM,iBAAiB;AACvB,IAAM,iBAAiB;AACvB,IAAM,qBAAqB;AAO3B,IAAMC,kBAAqE;AAAA,EACzE,qBAAqB;AAAA,EACrB,2BAA2B;AAAA,EAC3B,uBAAuB;AACzB;AAGO,IAAM,iCAAN,cAA6C,MAAM;AAAA,EAC/C;AAAA,EAET,YAAY,MAA0C;AACpD,UAAMA,gBAAe,IAAI,CAAC;AAC1B,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAuBA,SAAS,qBAAqB,MAAiD;AAC7E,QAAM,IAAI,+BAA+B,IAAI;AAC/C;AAEA,SAASC,UAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,aAAa,OAAgC,UAAsC;AAC1F,QAAM,SAAS,OAAO,KAAK,KAAK;AAChC,SAAO,OAAO,WAAW,SAAS,UAAU,OAAO,MAAM,CAAC,QAAQ,SAAS,SAAS,GAAG,CAAC;AAC1F;AAEA,SAAS,gBAAgB,OAAgB,aAA+C;AACtF,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,KAAK,CAAC,eAAe,KAAK,KAAK,EAAG,QAAO;AAC3F,QAAM,UAAUC,QAAO,KAAK,OAAO,QAAQ;AAC3C,MAAI,QAAQ,SAAS,KAAK,QAAQ,SAAS,QAAQ,QAAQ,SAAS,QAAQ,MAAM,MAAO,QAAO;AAChG,MAAI,gBAAgB,QAAQ,QAAQ,WAAW,YAAa,QAAO;AACnE,SAAO,WAAW,KAAK,OAAO;AAChC;AAEA,SAAS,oBAAoB,OAAoC;AAC/D,MAAI,CAACD,UAAS,KAAK,KAAK,CAAC,aAAa,OAAO,aAAa,KACrD,MAAM,SAAS,YACf,MAAM,kBAAkB,KACxB,MAAM,cAAc,iBACpB,OAAO,MAAM,kBAAkB,YAC/B,CAAC,eAAe,KAAK,MAAM,aAAa,KACxC,iBAAiB,IAAI,MAAM,aAAa,GAAG;AAC9C,WAAO,qBAAqB,uBAAuB;AAAA,EACrD;AAEA,QAAM,QAAQ,gBAAgB,MAAM,OAAO,EAAE;AAC7C,QAAM,aAAa,gBAAgB,MAAM,YAAY,IAAI;AACzD,QAAM,UAAU,gBAAgB,MAAM,SAAS,EAAE;AACjD,MAAI,CAAC,SAAS,CAAC,cAAc,CAAC,QAAS,QAAO,qBAAqB,uBAAuB;AAC1F,SAAO,EAAE,OAAO,MAAM,eAAe,OAAO,YAAY,QAAQ;AAClE;AAEA,SAAS,gBAAgB,OAAiC;AACxD,SAAO,OAAO,UAAU,YAAY,MAAM,UAAU,KAAK,MAAM,UAAU,QACpE,MAAM,KAAK,MAAM,SAAS,CAAC,mBAAmB,KAAK,KAAK;AAC/D;AAEA,SAAS,iBAAiB,OAAyC;AACjE,MAAI,CAACA,UAAS,KAAK,KAAK,CAAC,aAAa,OAAO,CAAC,UAAU,WAAW,CAAC,KAC/D,CAAC,gBAAgB,MAAM,MAAM,KAAK,CAAC,gBAAgB,MAAM,SAAS,GAAG;AACxE,WAAO,qBAAqB,uBAAuB;AAAA,EACrD;AACA,SAAO,OAAO,OAAO,EAAE,QAAQ,MAAM,QAAQ,WAAW,MAAM,UAAU,CAAC;AAC3E;AAMA,eAAsB,8BAA8B,OAId;AACpC,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,KACzD,OAAO,MAAM,gBAAgB,YAAY,CAAC,2BAA2B,IAAI,MAAM,WAAW,GAAG;AAChG,WAAO,qBAAqB,qBAAqB;AAAA,EACnD;AAEA,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,sBAAsB,WAAW;AAAA,MACxD,OAAO,EAAE,iBAAiB,EAAE,aAAa,MAAM,aAAa,KAAK,kBAAkB,EAAE;AAAA,MACrF,QAAQ,EAAE,aAAa,MAAM,KAAK,MAAM,SAAS,MAAM,OAAO,KAAK;AAAA,IACrE,CAAC;AAAA,EACH,QAAQ;AACN,WAAO,qBAAqB,2BAA2B;AAAA,EACzD;AAEA,MAAI,CAAC,IAAK,QAAO,qBAAqB,2BAA2B;AACjE,MAAI,IAAI,gBAAgB,MAAM,eAAe,IAAI,QAAQ,qBACpD,CAAC,OAAO,cAAc,IAAI,OAAO,KAAK,IAAI,UAAU,KAAK,IAAI,UAAU,YAAe;AACzF,WAAO,qBAAqB,uBAAuB;AAAA,EACrD;AACA,MAAIA,UAAS,IAAI,KAAK,KAAK,aAAa,IAAI,OAAO,CAAC,QAAQ,eAAe,CAAC,KACvE,IAAI,MAAM,SAAS,cAAc,IAAI,MAAM,kBAAkB,GAAG;AACnE,WAAO,qBAAqB,2BAA2B;AAAA,EACzD;AAEA,MAAI;AACJ,MAAI;AACF,UAAM,WAAW,oBAAoB,IAAI,KAAK;AAC9C,UAAM,MAAM,KAAK,UAAU,CAAC,2BAA2B,MAAM,aAAa,mBAAmB,IAAI,SAAS,CAAC,CAAC;AAC5G,gBAAY,kBAAkB,EAAE,SAAS,MAAM,eAAe,UAAU,IAAI,CAAC;AAAA,EAC/E,SAAS,OAAO;AACd,QAAI,iBAAiB,+BAAgC,OAAM;AAC3D,WAAO,qBAAqB,uBAAuB;AAAA,EACrD;AAEA,MAAI;AACF,WAAO,iBAAiB,KAAK,MAAM,SAAS,CAAY;AAAA,EAC1D,SAAS,OAAO;AACd,QAAI,iBAAiB,+BAAgC,OAAM;AAC3D,WAAO,qBAAqB,uBAAuB;AAAA,EACrD;AACF;;;AFzHA,SAAS,8BAA8B,QAAgB,OAAe,QAAwB;AAC5F,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAAG;AACnD,UAAM,IAAI,MAAM,GAAG,MAAM,0CAA0C;AAAA,EACrE;AAEA,MAAI,OAAO,WAAW,YAAY,OAAO,WAAW,GAAG;AACrD,UAAM,IAAI,MAAM,GAAG,MAAM,2CAA2C;AAAA,EACtE;AAEA,QAAM,SAAS,WAAW,QAAQ,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,EAAE,EAAE,OAAO,KAAK;AAC7E,SAAO,GAAG,MAAM,IAAI,MAAM;AAC5B;AAMO,SAAS,0BAA0B,QAAgB,YAA4B;AACpF,SAAO,8BAA8B,WAAW,QAAQ,UAAU;AACpE;AAEO,SAAS,2BAA2B,QAAgB,eAA+B;AACxF,SAAO,8BAA8B,YAAY,QAAQ,aAAa;AACxE;AAEO,SAAS,wBAAwB,QAAgB,UAA0B;AAChF,SAAO,8BAA8B,iBAAiB,QAAQ,QAAQ;AACxE;AAEO,SAAS,oCACd,QACA,eACQ;AACR,SAAO,8BAA8B,oBAAoB,QAAQ,aAAa;AAChF;AAEO,SAAS,+BAA+B,OAKpC;AACT,MAAI,OAAO,MAAM,WAAW,YAAY,MAAM,OAAO,KAAK,EAAE,WAAW,GAAG;AACxE,UAAM,IAAI,MAAM,mEAAmE;AAAA,EACrF;AAEA,MAAI,MAAM,WAAW,oBAAoB;AACvC,QAAI,OAAO,MAAM,eAAe,YAAY,MAAM,WAAW,KAAK,EAAE,WAAW,GAAG;AAChF,YAAM,IAAI,MAAM,6EAA6E;AAAA,IAC/F;AACA,WAAO,8BAA8B,iBAAiB,MAAM,QAAQ,MAAM,UAAU;AAAA,EACtF;AAEA,MAAI,OAAO,MAAM,gBAAgB,YAAY,MAAM,YAAY,KAAK,EAAE,WAAW,GAAG;AAClF,UAAM,IAAI,MAAM,qFAAqF;AAAA,EACvG;AAEA,SAAO,8BAA8B,iBAAiB,GAAG,MAAM,MAAM,IAAI,MAAM,MAAM,IAAI,MAAM,WAAW;AAC5G;AAOO,SAAS,sCACd,OACQ;AACR,QAAM,MAAM,sCAAsC,MAAM,KAAK;AAC7D,QAAM,SAAS,WAAW,QAAQ,EAC/B,OAAO,KAAK,UAAU,CAAC,IAAI,QAAQ,IAAI,UAAU,CAAC,CAAC,EACnD,OAAO,KAAK;AACf,SAAO,iCAAiC,MAAM;AAChD;","names":["Buffer","ERROR_MESSAGES","isRecord","Buffer"]}
|