@uniforge/platform-shopify 0.1.0-alpha.2
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/auth/index.d.cts +246 -0
- package/dist/auth/index.d.ts +246 -0
- package/dist/auth/index.js +623 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/index.mjs +586 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/billing/index.d.cts +58 -0
- package/dist/billing/index.d.ts +58 -0
- package/dist/billing/index.js +226 -0
- package/dist/billing/index.js.map +1 -0
- package/dist/billing/index.mjs +196 -0
- package/dist/billing/index.mjs.map +1 -0
- package/dist/graphql/index.d.cts +17 -0
- package/dist/graphql/index.d.ts +17 -0
- package/dist/graphql/index.js +67 -0
- package/dist/graphql/index.js.map +1 -0
- package/dist/graphql/index.mjs +40 -0
- package/dist/graphql/index.mjs.map +1 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +11 -0
- package/dist/index.mjs.map +1 -0
- package/dist/multi-store/index.d.cts +28 -0
- package/dist/multi-store/index.d.ts +28 -0
- package/dist/multi-store/index.js +181 -0
- package/dist/multi-store/index.js.map +1 -0
- package/dist/multi-store/index.mjs +152 -0
- package/dist/multi-store/index.mjs.map +1 -0
- package/dist/performance/index.d.cts +22 -0
- package/dist/performance/index.d.ts +22 -0
- package/dist/performance/index.js +64 -0
- package/dist/performance/index.js.map +1 -0
- package/dist/performance/index.mjs +35 -0
- package/dist/performance/index.mjs.map +1 -0
- package/dist/platform/index.d.cts +16 -0
- package/dist/platform/index.d.ts +16 -0
- package/dist/platform/index.js +150 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/index.mjs +121 -0
- package/dist/platform/index.mjs.map +1 -0
- package/dist/rbac/index.d.cts +38 -0
- package/dist/rbac/index.d.ts +38 -0
- package/dist/rbac/index.js +56 -0
- package/dist/rbac/index.js.map +1 -0
- package/dist/rbac/index.mjs +29 -0
- package/dist/rbac/index.mjs.map +1 -0
- package/dist/security/index.d.cts +26 -0
- package/dist/security/index.d.ts +26 -0
- package/dist/security/index.js +102 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/index.mjs +69 -0
- package/dist/security/index.mjs.map +1 -0
- package/dist/webhooks/index.d.cts +36 -0
- package/dist/webhooks/index.d.ts +36 -0
- package/dist/webhooks/index.js +147 -0
- package/dist/webhooks/index.js.map +1 -0
- package/dist/webhooks/index.mjs +118 -0
- package/dist/webhooks/index.mjs.map +1 -0
- package/package.json +95 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/auth/index.ts
|
|
31
|
+
var auth_exports = {};
|
|
32
|
+
__export(auth_exports, {
|
|
33
|
+
ShopifySessionManager: () => ShopifySessionManager,
|
|
34
|
+
beginOAuth: () => beginOAuth,
|
|
35
|
+
createShopifyTokenExchangeFn: () => createShopifyTokenExchangeFn,
|
|
36
|
+
extractSessionToken: () => extractSessionToken,
|
|
37
|
+
extractShop: () => extractShop,
|
|
38
|
+
handleOAuthCallback: () => handleOAuthCallback,
|
|
39
|
+
performTokenExchange: () => performTokenExchange,
|
|
40
|
+
validateOAuthHmac: () => validateOAuthHmac
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(auth_exports);
|
|
43
|
+
|
|
44
|
+
// src/auth/token-exchange.ts
|
|
45
|
+
var import_node_crypto = require("crypto");
|
|
46
|
+
var import_auth = require("@uniforge/platform-core/auth");
|
|
47
|
+
var import_auth2 = require("@uniforge/core/auth");
|
|
48
|
+
function extractSessionToken(request) {
|
|
49
|
+
const authHeader = request.headers["authorization"] ?? request.headers["Authorization"];
|
|
50
|
+
if (!authHeader || typeof authHeader !== "string") {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (!authHeader.startsWith("Bearer ")) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const token = authHeader.slice("Bearer ".length).trim();
|
|
57
|
+
return token.length > 0 ? token : null;
|
|
58
|
+
}
|
|
59
|
+
function extractShop(request) {
|
|
60
|
+
const shopFromQuery = request.query["shop"];
|
|
61
|
+
if (shopFromQuery && (0, import_auth.isValidShopDomain)(shopFromQuery)) {
|
|
62
|
+
return shopFromQuery;
|
|
63
|
+
}
|
|
64
|
+
const shopFromHeader = request.headers["x-shopify-shop-domain"];
|
|
65
|
+
if (shopFromHeader && typeof shopFromHeader === "string" && (0, import_auth.isValidShopDomain)(shopFromHeader)) {
|
|
66
|
+
return shopFromHeader;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
async function createShopifyTokenExchangeFn(config) {
|
|
71
|
+
const { shopifyApi, LogSeverity } = await import("@shopify/shopify-api");
|
|
72
|
+
await import("@shopify/shopify-api/adapters/node");
|
|
73
|
+
const shopify = shopifyApi({
|
|
74
|
+
apiKey: config.apiKey,
|
|
75
|
+
apiSecretKey: config.apiSecretKey,
|
|
76
|
+
scopes: config.scopes,
|
|
77
|
+
hostName: config.hostName,
|
|
78
|
+
apiVersion: config.apiVersion,
|
|
79
|
+
isEmbeddedApp: config.isEmbeddedApp ?? true,
|
|
80
|
+
logger: { level: LogSeverity.Error }
|
|
81
|
+
});
|
|
82
|
+
return shopify.auth.tokenExchange;
|
|
83
|
+
}
|
|
84
|
+
async function performTokenExchange(input) {
|
|
85
|
+
const { config, sessionToken, shop, tokenType } = input;
|
|
86
|
+
if (!sessionToken) {
|
|
87
|
+
throw new Error("Session token is required for token exchange.");
|
|
88
|
+
}
|
|
89
|
+
if (!(0, import_auth.isValidShopDomain)(shop)) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Invalid shop domain "${shop}". Expected format: example.myshopify.com`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const tokenExchangeFn = input.tokenExchangeFn ?? await createShopifyTokenExchangeFn(config);
|
|
95
|
+
const requestedTokenType = tokenType === "online" ? "urn:shopify:params:oauth:token-type:online-access-token" : "urn:shopify:params:oauth:token-type:offline-access-token";
|
|
96
|
+
try {
|
|
97
|
+
const { session: shopifySession } = await tokenExchangeFn({
|
|
98
|
+
sessionToken,
|
|
99
|
+
shop,
|
|
100
|
+
requestedTokenType
|
|
101
|
+
});
|
|
102
|
+
const now = /* @__PURE__ */ new Date();
|
|
103
|
+
const session = mapShopifySession(shopifySession, shop, now);
|
|
104
|
+
if (config.eventHandler) {
|
|
105
|
+
const event = (0, import_auth2.createAuthEvent)({
|
|
106
|
+
type: "token_exchange_success",
|
|
107
|
+
shopDomain: shop,
|
|
108
|
+
outcome: "success",
|
|
109
|
+
sessionId: session.id,
|
|
110
|
+
metadata: {
|
|
111
|
+
tokenType,
|
|
112
|
+
sessionId: session.id
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
await Promise.resolve(config.eventHandler.onAuthEvent(event));
|
|
116
|
+
}
|
|
117
|
+
return session;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (config.eventHandler) {
|
|
120
|
+
const event = (0, import_auth2.createAuthEvent)({
|
|
121
|
+
type: "token_exchange_failure",
|
|
122
|
+
shopDomain: shop,
|
|
123
|
+
outcome: "failure",
|
|
124
|
+
metadata: {
|
|
125
|
+
tokenType,
|
|
126
|
+
error: error instanceof Error ? error.message : String(error)
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
await Promise.resolve(config.eventHandler.onAuthEvent(event));
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function mapShopifySession(shopifySession, shop, now) {
|
|
135
|
+
const session = {
|
|
136
|
+
id: shopifySession.id,
|
|
137
|
+
shop: shopifySession.shop || shop,
|
|
138
|
+
state: shopifySession.state || (0, import_node_crypto.randomUUID)(),
|
|
139
|
+
isOnline: shopifySession.isOnline,
|
|
140
|
+
scope: shopifySession.scope || "",
|
|
141
|
+
expires: shopifySession.expires ?? null,
|
|
142
|
+
createdAt: now,
|
|
143
|
+
updatedAt: now
|
|
144
|
+
};
|
|
145
|
+
if (shopifySession.accessToken) {
|
|
146
|
+
session.accessToken = shopifySession.accessToken;
|
|
147
|
+
}
|
|
148
|
+
if (shopifySession.refreshToken) {
|
|
149
|
+
session.refreshToken = shopifySession.refreshToken;
|
|
150
|
+
}
|
|
151
|
+
if (shopifySession.onlineAccessInfo) {
|
|
152
|
+
const info = shopifySession.onlineAccessInfo;
|
|
153
|
+
session.onlineAccessInfo = {
|
|
154
|
+
expiresIn: info.expires_in ?? info.expiresIn ?? 0,
|
|
155
|
+
associatedUserScope: info.associated_user_scope ?? info.associatedUserScope ?? "",
|
|
156
|
+
associatedUser: info.associated_user ? {
|
|
157
|
+
id: info.associated_user.id,
|
|
158
|
+
firstName: info.associated_user.first_name,
|
|
159
|
+
lastName: info.associated_user.last_name,
|
|
160
|
+
email: info.associated_user.email,
|
|
161
|
+
emailVerified: info.associated_user.email_verified,
|
|
162
|
+
accountOwner: info.associated_user.account_owner,
|
|
163
|
+
locale: info.associated_user.locale,
|
|
164
|
+
collaborator: info.associated_user.collaborator
|
|
165
|
+
} : info.associatedUser
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return session;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/auth/session.ts
|
|
172
|
+
var import_auth3 = require("@uniforge/core/auth");
|
|
173
|
+
var ShopifySessionManager = class {
|
|
174
|
+
constructor(config, options) {
|
|
175
|
+
this.config = config;
|
|
176
|
+
const encryption = new import_auth3.TokenEncryptionServiceImpl(config.encryption);
|
|
177
|
+
this.encryptedStorage = new import_auth3.EncryptedSessionStorage(
|
|
178
|
+
config.sessionStorage,
|
|
179
|
+
encryption
|
|
180
|
+
);
|
|
181
|
+
if (options?.tokenExchangeFn) {
|
|
182
|
+
this.tokenExchangeFn = options.tokenExchangeFn;
|
|
183
|
+
}
|
|
184
|
+
if (options?.tokenRefreshFn) {
|
|
185
|
+
this.tokenRefreshFn = options.tokenRefreshFn;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
encryptedStorage;
|
|
189
|
+
tokenExchangeFn;
|
|
190
|
+
tokenRefreshFn;
|
|
191
|
+
/**
|
|
192
|
+
* Get an existing valid session or create one via token exchange.
|
|
193
|
+
*
|
|
194
|
+
* For embedded apps, this:
|
|
195
|
+
* 1. Extracts the session token and shop from the request
|
|
196
|
+
* 2. Checks storage for an existing non-expired session
|
|
197
|
+
* 3. If no valid session, performs token exchange with Shopify
|
|
198
|
+
* 4. Stores the new session (encrypted) and returns it
|
|
199
|
+
*/
|
|
200
|
+
async getOrCreateSession(request) {
|
|
201
|
+
const shop = extractShop(request);
|
|
202
|
+
if (!shop) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
"Could not extract shop domain from request. Provide shop in query parameter or x-shopify-shop-domain header."
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const sessionToken = extractSessionToken(request);
|
|
208
|
+
if (!sessionToken) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
"Could not extract session token from Authorization header. Expected: Authorization: Bearer <token>"
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const sessionId = `offline_${shop}`;
|
|
214
|
+
const existingSession = await this.encryptedStorage.loadSession(sessionId);
|
|
215
|
+
if (existingSession && !this.isExpired(existingSession)) {
|
|
216
|
+
return existingSession;
|
|
217
|
+
}
|
|
218
|
+
const exchangeInput = {
|
|
219
|
+
config: this.config,
|
|
220
|
+
sessionToken,
|
|
221
|
+
shop,
|
|
222
|
+
tokenType: "offline"
|
|
223
|
+
};
|
|
224
|
+
if (this.tokenExchangeFn) {
|
|
225
|
+
exchangeInput.tokenExchangeFn = this.tokenExchangeFn;
|
|
226
|
+
}
|
|
227
|
+
const session = await performTokenExchange(exchangeInput);
|
|
228
|
+
await this.encryptedStorage.storeSession(session);
|
|
229
|
+
return session;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get an existing valid online session or create one via token exchange.
|
|
233
|
+
*/
|
|
234
|
+
async getOrCreateOnlineSession(request) {
|
|
235
|
+
const shop = extractShop(request);
|
|
236
|
+
if (!shop) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
"Could not extract shop domain from request."
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
const sessionToken = extractSessionToken(request);
|
|
242
|
+
if (!sessionToken) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
"Could not extract session token from Authorization header."
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const onlineExchangeInput = {
|
|
248
|
+
config: this.config,
|
|
249
|
+
sessionToken,
|
|
250
|
+
shop,
|
|
251
|
+
tokenType: "online"
|
|
252
|
+
};
|
|
253
|
+
if (this.tokenExchangeFn) {
|
|
254
|
+
onlineExchangeInput.tokenExchangeFn = this.tokenExchangeFn;
|
|
255
|
+
}
|
|
256
|
+
const session = await performTokenExchange(onlineExchangeInput);
|
|
257
|
+
await this.encryptedStorage.storeSession(session);
|
|
258
|
+
return session;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Refresh an expiring session token.
|
|
262
|
+
*
|
|
263
|
+
* Retries up to `config.tokenRefresh.maxRetries` on failure.
|
|
264
|
+
* On success, stores the refreshed session (encrypted) and emits
|
|
265
|
+
* a `token_refreshed` event. On final failure, emits
|
|
266
|
+
* `token_refresh_failed` and throws.
|
|
267
|
+
*/
|
|
268
|
+
async refreshSessionToken(session) {
|
|
269
|
+
const maxRetries = this.config.tokenRefresh?.maxRetries ?? 3;
|
|
270
|
+
const refreshFn = this.tokenRefreshFn ?? await this.createDefaultRefreshFn();
|
|
271
|
+
let lastError;
|
|
272
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
273
|
+
try {
|
|
274
|
+
const refreshInput = { shop: session.shop };
|
|
275
|
+
if (session.accessToken) {
|
|
276
|
+
refreshInput.accessToken = session.accessToken;
|
|
277
|
+
}
|
|
278
|
+
if (session.refreshToken) {
|
|
279
|
+
refreshInput.refreshToken = session.refreshToken;
|
|
280
|
+
}
|
|
281
|
+
const { session: refreshedShopifySession } = await refreshFn({
|
|
282
|
+
session: refreshInput
|
|
283
|
+
});
|
|
284
|
+
const now = /* @__PURE__ */ new Date();
|
|
285
|
+
const refreshedSession = this.mapRefreshedSession(
|
|
286
|
+
refreshedShopifySession,
|
|
287
|
+
session,
|
|
288
|
+
now
|
|
289
|
+
);
|
|
290
|
+
await this.encryptedStorage.storeSession(refreshedSession);
|
|
291
|
+
if (this.config.eventHandler) {
|
|
292
|
+
const event = (0, import_auth3.createAuthEvent)({
|
|
293
|
+
type: "token_refreshed",
|
|
294
|
+
shopDomain: session.shop,
|
|
295
|
+
outcome: "success",
|
|
296
|
+
sessionId: session.id,
|
|
297
|
+
metadata: { attempt: String(attempt + 1) }
|
|
298
|
+
});
|
|
299
|
+
await Promise.resolve(
|
|
300
|
+
this.config.eventHandler.onAuthEvent(event)
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return refreshedSession;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (this.config.eventHandler) {
|
|
309
|
+
const event = (0, import_auth3.createAuthEvent)({
|
|
310
|
+
type: "token_refresh_failed",
|
|
311
|
+
shopDomain: session.shop,
|
|
312
|
+
outcome: "failure",
|
|
313
|
+
sessionId: session.id,
|
|
314
|
+
metadata: {
|
|
315
|
+
maxRetries: String(maxRetries),
|
|
316
|
+
error: lastError?.message ?? "Unknown error"
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
await Promise.resolve(this.config.eventHandler.onAuthEvent(event));
|
|
320
|
+
}
|
|
321
|
+
throw lastError ?? new Error("Token refresh failed after all retries.");
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Delete all sessions for a shop (e.g., on app uninstall).
|
|
325
|
+
*
|
|
326
|
+
* Finds all sessions via `findSessionsByShop`, deletes them,
|
|
327
|
+
* and emits `session_deleted` events.
|
|
328
|
+
*/
|
|
329
|
+
async cleanupShopSessions(shop) {
|
|
330
|
+
const sessions = await this.config.sessionStorage.findSessionsByShop(shop);
|
|
331
|
+
if (sessions.length === 0) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const sessionIds = sessions.map((s) => s.id);
|
|
335
|
+
await this.config.sessionStorage.deleteSessions(sessionIds);
|
|
336
|
+
if (this.config.eventHandler) {
|
|
337
|
+
for (const session of sessions) {
|
|
338
|
+
const event = (0, import_auth3.createAuthEvent)({
|
|
339
|
+
type: "session_deleted",
|
|
340
|
+
shopDomain: shop,
|
|
341
|
+
outcome: "success",
|
|
342
|
+
sessionId: session.id,
|
|
343
|
+
metadata: { reason: "shop_cleanup" }
|
|
344
|
+
});
|
|
345
|
+
await Promise.resolve(
|
|
346
|
+
this.config.eventHandler.onAuthEvent(event)
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
isExpired(session) {
|
|
352
|
+
if (!session.expires) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
return session.expires.getTime() <= Date.now();
|
|
356
|
+
}
|
|
357
|
+
mapRefreshedSession(refreshed, original, now) {
|
|
358
|
+
const session = {
|
|
359
|
+
id: refreshed.id || original.id,
|
|
360
|
+
shop: refreshed.shop || original.shop,
|
|
361
|
+
state: refreshed.state || original.state,
|
|
362
|
+
isOnline: refreshed.isOnline,
|
|
363
|
+
scope: refreshed.scope || original.scope,
|
|
364
|
+
expires: refreshed.expires ?? original.expires,
|
|
365
|
+
createdAt: original.createdAt,
|
|
366
|
+
updatedAt: now
|
|
367
|
+
};
|
|
368
|
+
if (refreshed.accessToken) {
|
|
369
|
+
session.accessToken = refreshed.accessToken;
|
|
370
|
+
}
|
|
371
|
+
if (refreshed.refreshToken) {
|
|
372
|
+
session.refreshToken = refreshed.refreshToken;
|
|
373
|
+
}
|
|
374
|
+
return session;
|
|
375
|
+
}
|
|
376
|
+
async createDefaultRefreshFn() {
|
|
377
|
+
const { shopifyApi, LogSeverity } = await import("@shopify/shopify-api");
|
|
378
|
+
await import("@shopify/shopify-api/adapters/node");
|
|
379
|
+
const shopify = shopifyApi({
|
|
380
|
+
apiKey: this.config.apiKey,
|
|
381
|
+
apiSecretKey: this.config.apiSecretKey,
|
|
382
|
+
scopes: this.config.scopes,
|
|
383
|
+
hostName: this.config.hostName,
|
|
384
|
+
apiVersion: this.config.apiVersion,
|
|
385
|
+
isEmbeddedApp: this.config.isEmbeddedApp ?? true,
|
|
386
|
+
logger: { level: LogSeverity.Error }
|
|
387
|
+
});
|
|
388
|
+
return async (params) => {
|
|
389
|
+
const result = await shopify.auth.refreshToken(params);
|
|
390
|
+
return result;
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/auth/hmac.ts
|
|
396
|
+
var import_node_crypto2 = require("crypto");
|
|
397
|
+
function validateOAuthHmac(query, secret) {
|
|
398
|
+
const hmac = query["hmac"];
|
|
399
|
+
if (!hmac) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
const entries = Object.entries(query).filter(([key]) => key !== "hmac").sort(([a], [b]) => a.localeCompare(b));
|
|
403
|
+
const message = entries.map(([key, value]) => `${key}=${value}`).join("&");
|
|
404
|
+
const computed = (0, import_node_crypto2.createHmac)("sha256", secret).update(message).digest("hex");
|
|
405
|
+
if (computed.length !== hmac.length) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
return (0, import_node_crypto2.timingSafeEqual)(
|
|
409
|
+
Buffer.from(computed, "utf-8"),
|
|
410
|
+
Buffer.from(hmac, "utf-8")
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/auth/oauth.ts
|
|
415
|
+
var import_node_crypto3 = require("crypto");
|
|
416
|
+
var import_auth4 = require("@uniforge/platform-core/auth");
|
|
417
|
+
var import_auth5 = require("@uniforge/core/auth");
|
|
418
|
+
var import_auth6 = require("@uniforge/core/auth");
|
|
419
|
+
async function beginOAuth(input) {
|
|
420
|
+
const { config, request, callbackPath, isOnline } = input;
|
|
421
|
+
const shop = request.query["shop"];
|
|
422
|
+
if (!shop || !(0, import_auth4.isValidShopDomain)(shop)) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
"Could not extract valid shop domain from request. Provide shop in query parameter."
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
const beginFn = input.beginFn ?? await createShopifyOAuthBeginFn(config);
|
|
428
|
+
const { redirectUrl } = await beginFn({
|
|
429
|
+
shop,
|
|
430
|
+
callbackPath,
|
|
431
|
+
isOnline: isOnline ?? false
|
|
432
|
+
});
|
|
433
|
+
if (config.eventHandler) {
|
|
434
|
+
const event = (0, import_auth5.createAuthEvent)({
|
|
435
|
+
type: "oauth_begin",
|
|
436
|
+
shopDomain: shop,
|
|
437
|
+
outcome: "success",
|
|
438
|
+
metadata: { callbackPath }
|
|
439
|
+
});
|
|
440
|
+
await Promise.resolve(config.eventHandler.onAuthEvent(event));
|
|
441
|
+
}
|
|
442
|
+
return { redirectUrl };
|
|
443
|
+
}
|
|
444
|
+
async function handleOAuthCallback(input) {
|
|
445
|
+
const { config, request } = input;
|
|
446
|
+
const query = request.query;
|
|
447
|
+
const shop = query["shop"] ?? "";
|
|
448
|
+
if (query["error"]) {
|
|
449
|
+
const errorDesc = query["error_description"] ?? `Authorization ${query["error"]}`;
|
|
450
|
+
if (config.eventHandler) {
|
|
451
|
+
const event = (0, import_auth5.createAuthEvent)({
|
|
452
|
+
type: "oauth_callback_failure",
|
|
453
|
+
shopDomain: shop,
|
|
454
|
+
outcome: "failure",
|
|
455
|
+
metadata: {
|
|
456
|
+
error: query["error"],
|
|
457
|
+
errorDescription: errorDesc
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
await Promise.resolve(config.eventHandler.onAuthEvent(event));
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
success: false,
|
|
464
|
+
error: {
|
|
465
|
+
code: "ACCESS_DENIED",
|
|
466
|
+
message: errorDesc,
|
|
467
|
+
statusCode: 403
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
if (!validateOAuthHmac(query, config.apiSecretKey)) {
|
|
472
|
+
if (config.eventHandler) {
|
|
473
|
+
const event = (0, import_auth5.createAuthEvent)({
|
|
474
|
+
type: "oauth_callback_failure",
|
|
475
|
+
shopDomain: shop,
|
|
476
|
+
outcome: "failure",
|
|
477
|
+
metadata: { error: "HMAC validation failed" }
|
|
478
|
+
});
|
|
479
|
+
await Promise.resolve(config.eventHandler.onAuthEvent(event));
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
success: false,
|
|
483
|
+
error: {
|
|
484
|
+
code: "HMAC_VALIDATION_FAILED",
|
|
485
|
+
message: "HMAC validation failed on OAuth callback.",
|
|
486
|
+
statusCode: 403
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const callbackFn = input.callbackFn ?? await createShopifyOAuthCallbackFn(config);
|
|
492
|
+
const { session: shopifySession } = await callbackFn({ query });
|
|
493
|
+
const now = /* @__PURE__ */ new Date();
|
|
494
|
+
const session = mapOAuthSession(shopifySession, shop, now);
|
|
495
|
+
const grantedScopes = new Set(
|
|
496
|
+
(session.scope || "").split(",").map((s) => s.trim())
|
|
497
|
+
);
|
|
498
|
+
const requiredScopes = config.scopes;
|
|
499
|
+
const missingScopes = requiredScopes.filter(
|
|
500
|
+
(scope) => !grantedScopes.has(scope)
|
|
501
|
+
);
|
|
502
|
+
if (missingScopes.length > 0) {
|
|
503
|
+
if (config.eventHandler) {
|
|
504
|
+
const event = (0, import_auth5.createAuthEvent)({
|
|
505
|
+
type: "oauth_callback_failure",
|
|
506
|
+
shopDomain: shop,
|
|
507
|
+
outcome: "failure",
|
|
508
|
+
metadata: {
|
|
509
|
+
error: "scope_mismatch",
|
|
510
|
+
missingScopes: missingScopes.join(",")
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
await Promise.resolve(config.eventHandler.onAuthEvent(event));
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
success: false,
|
|
517
|
+
error: {
|
|
518
|
+
code: "SCOPE_MISMATCH",
|
|
519
|
+
message: `Missing required scopes: ${missingScopes.join(", ")}. Granted: ${session.scope}`,
|
|
520
|
+
statusCode: 403
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
const encryption = new import_auth6.TokenEncryptionServiceImpl(config.encryption);
|
|
525
|
+
const encryptedStorage = new import_auth6.EncryptedSessionStorage(
|
|
526
|
+
config.sessionStorage,
|
|
527
|
+
encryption
|
|
528
|
+
);
|
|
529
|
+
await encryptedStorage.storeSession(session);
|
|
530
|
+
if (config.eventHandler) {
|
|
531
|
+
const event = (0, import_auth5.createAuthEvent)({
|
|
532
|
+
type: "oauth_callback_success",
|
|
533
|
+
shopDomain: shop,
|
|
534
|
+
outcome: "success",
|
|
535
|
+
sessionId: session.id,
|
|
536
|
+
metadata: { scope: session.scope }
|
|
537
|
+
});
|
|
538
|
+
await Promise.resolve(config.eventHandler.onAuthEvent(event));
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
success: true,
|
|
542
|
+
session,
|
|
543
|
+
redirectUrl: `${config.hostName}/?shop=${shop}`
|
|
544
|
+
};
|
|
545
|
+
} catch (error) {
|
|
546
|
+
if (config.eventHandler) {
|
|
547
|
+
const event = (0, import_auth5.createAuthEvent)({
|
|
548
|
+
type: "oauth_callback_failure",
|
|
549
|
+
shopDomain: shop,
|
|
550
|
+
outcome: "failure",
|
|
551
|
+
metadata: {
|
|
552
|
+
error: error instanceof Error ? error.message : String(error)
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
await Promise.resolve(config.eventHandler.onAuthEvent(event));
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
success: false,
|
|
559
|
+
error: {
|
|
560
|
+
code: "OAUTH_CALLBACK_FAILED",
|
|
561
|
+
message: error instanceof Error ? error.message : String(error),
|
|
562
|
+
statusCode: 500
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async function createShopifyOAuthBeginFn(config) {
|
|
568
|
+
return async (params) => {
|
|
569
|
+
const nonce = (0, import_node_crypto3.randomUUID)();
|
|
570
|
+
const scopeString = config.scopes.join(",");
|
|
571
|
+
const redirectUri = `${config.hostName}${params.callbackPath}`;
|
|
572
|
+
const redirectUrl = `https://${params.shop}/admin/oauth/authorize?client_id=${config.apiKey}&scope=${encodeURIComponent(scopeString)}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${nonce}`;
|
|
573
|
+
return { redirectUrl };
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
async function createShopifyOAuthCallbackFn(config) {
|
|
577
|
+
const { shopifyApi, LogSeverity } = await import("@shopify/shopify-api");
|
|
578
|
+
await import("@shopify/shopify-api/adapters/node");
|
|
579
|
+
const shopify = shopifyApi({
|
|
580
|
+
apiKey: config.apiKey,
|
|
581
|
+
apiSecretKey: config.apiSecretKey,
|
|
582
|
+
scopes: config.scopes,
|
|
583
|
+
hostName: config.hostName,
|
|
584
|
+
apiVersion: config.apiVersion,
|
|
585
|
+
isEmbeddedApp: config.isEmbeddedApp ?? false,
|
|
586
|
+
logger: { level: LogSeverity.Error }
|
|
587
|
+
});
|
|
588
|
+
return async (params) => {
|
|
589
|
+
const result = await shopify.auth.callback(params);
|
|
590
|
+
return result;
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function mapOAuthSession(shopifySession, shop, now) {
|
|
594
|
+
const session = {
|
|
595
|
+
id: shopifySession.id,
|
|
596
|
+
shop: shopifySession.shop || shop,
|
|
597
|
+
state: shopifySession.state || (0, import_node_crypto3.randomUUID)(),
|
|
598
|
+
isOnline: shopifySession.isOnline,
|
|
599
|
+
scope: shopifySession.scope || "",
|
|
600
|
+
expires: shopifySession.expires ?? null,
|
|
601
|
+
createdAt: now,
|
|
602
|
+
updatedAt: now
|
|
603
|
+
};
|
|
604
|
+
if (shopifySession.accessToken) {
|
|
605
|
+
session.accessToken = shopifySession.accessToken;
|
|
606
|
+
}
|
|
607
|
+
if (shopifySession.refreshToken) {
|
|
608
|
+
session.refreshToken = shopifySession.refreshToken;
|
|
609
|
+
}
|
|
610
|
+
return session;
|
|
611
|
+
}
|
|
612
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
613
|
+
0 && (module.exports = {
|
|
614
|
+
ShopifySessionManager,
|
|
615
|
+
beginOAuth,
|
|
616
|
+
createShopifyTokenExchangeFn,
|
|
617
|
+
extractSessionToken,
|
|
618
|
+
extractShop,
|
|
619
|
+
handleOAuthCallback,
|
|
620
|
+
performTokenExchange,
|
|
621
|
+
validateOAuthHmac
|
|
622
|
+
});
|
|
623
|
+
//# sourceMappingURL=index.js.map
|