@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.
Files changed (61) hide show
  1. package/dist/auth/index.d.cts +246 -0
  2. package/dist/auth/index.d.ts +246 -0
  3. package/dist/auth/index.js +623 -0
  4. package/dist/auth/index.js.map +1 -0
  5. package/dist/auth/index.mjs +586 -0
  6. package/dist/auth/index.mjs.map +1 -0
  7. package/dist/billing/index.d.cts +58 -0
  8. package/dist/billing/index.d.ts +58 -0
  9. package/dist/billing/index.js +226 -0
  10. package/dist/billing/index.js.map +1 -0
  11. package/dist/billing/index.mjs +196 -0
  12. package/dist/billing/index.mjs.map +1 -0
  13. package/dist/graphql/index.d.cts +17 -0
  14. package/dist/graphql/index.d.ts +17 -0
  15. package/dist/graphql/index.js +67 -0
  16. package/dist/graphql/index.js.map +1 -0
  17. package/dist/graphql/index.mjs +40 -0
  18. package/dist/graphql/index.mjs.map +1 -0
  19. package/dist/index.d.cts +16 -0
  20. package/dist/index.d.ts +16 -0
  21. package/dist/index.js +37 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/index.mjs +11 -0
  24. package/dist/index.mjs.map +1 -0
  25. package/dist/multi-store/index.d.cts +28 -0
  26. package/dist/multi-store/index.d.ts +28 -0
  27. package/dist/multi-store/index.js +181 -0
  28. package/dist/multi-store/index.js.map +1 -0
  29. package/dist/multi-store/index.mjs +152 -0
  30. package/dist/multi-store/index.mjs.map +1 -0
  31. package/dist/performance/index.d.cts +22 -0
  32. package/dist/performance/index.d.ts +22 -0
  33. package/dist/performance/index.js +64 -0
  34. package/dist/performance/index.js.map +1 -0
  35. package/dist/performance/index.mjs +35 -0
  36. package/dist/performance/index.mjs.map +1 -0
  37. package/dist/platform/index.d.cts +16 -0
  38. package/dist/platform/index.d.ts +16 -0
  39. package/dist/platform/index.js +150 -0
  40. package/dist/platform/index.js.map +1 -0
  41. package/dist/platform/index.mjs +121 -0
  42. package/dist/platform/index.mjs.map +1 -0
  43. package/dist/rbac/index.d.cts +38 -0
  44. package/dist/rbac/index.d.ts +38 -0
  45. package/dist/rbac/index.js +56 -0
  46. package/dist/rbac/index.js.map +1 -0
  47. package/dist/rbac/index.mjs +29 -0
  48. package/dist/rbac/index.mjs.map +1 -0
  49. package/dist/security/index.d.cts +26 -0
  50. package/dist/security/index.d.ts +26 -0
  51. package/dist/security/index.js +102 -0
  52. package/dist/security/index.js.map +1 -0
  53. package/dist/security/index.mjs +69 -0
  54. package/dist/security/index.mjs.map +1 -0
  55. package/dist/webhooks/index.d.cts +36 -0
  56. package/dist/webhooks/index.d.ts +36 -0
  57. package/dist/webhooks/index.js +147 -0
  58. package/dist/webhooks/index.js.map +1 -0
  59. package/dist/webhooks/index.mjs +118 -0
  60. package/dist/webhooks/index.mjs.map +1 -0
  61. package/package.json +95 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/auth/index.ts","../../src/auth/token-exchange.ts","../../src/auth/session.ts","../../src/auth/hmac.ts","../../src/auth/oauth.ts"],"sourcesContent":["/**\n * @uniforge/platform-shopify - Authentication\n *\n * Shopify-specific authentication implementation including\n * token exchange, session management, and OAuth flows.\n */\n\n// Token exchange\nexport {\n extractSessionToken,\n extractShop,\n performTokenExchange,\n createShopifyTokenExchangeFn,\n} from './token-exchange';\nexport type {\n PerformTokenExchangeInput,\n ShopifyTokenExchangeFn,\n} from './token-exchange';\n\n// Session manager\nexport { ShopifySessionManager } from './session';\nexport type {\n ShopifyTokenRefreshFn,\n ShopifySessionManagerOptions,\n} from './session';\n\n// HMAC validation\nexport { validateOAuthHmac } from './hmac';\n\n// OAuth flow\nexport { beginOAuth, handleOAuthCallback } from './oauth';\nexport type {\n ShopifyOAuthBeginFn,\n ShopifyOAuthCallbackFn,\n BeginOAuthInput,\n BeginOAuthResult,\n HandleOAuthCallbackInput,\n OAuthCallbackResult,\n} from './oauth';\n","/**\n * Session token extraction and Shopify token exchange.\n *\n * Extracts session tokens from embedded app requests and exchanges\n * them for access tokens via the Shopify API.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type {\n AuthConfig,\n AuthRequest,\n Session,\n} from '@uniforge/platform-core/auth';\nimport { isValidShopDomain } from '@uniforge/platform-core/auth';\nimport { createAuthEvent } from '@uniforge/core/auth';\n\n/**\n * Extract the session token from the Authorization header.\n * Returns null if the header is missing or malformed.\n */\nexport function extractSessionToken(request: AuthRequest): string | null {\n // Try both lower-case and capitalized header names\n const authHeader =\n request.headers['authorization'] ?? request.headers['Authorization'];\n\n if (!authHeader || typeof authHeader !== 'string') {\n return null;\n }\n\n if (!authHeader.startsWith('Bearer ')) {\n return null;\n }\n\n const token = authHeader.slice('Bearer '.length).trim();\n return token.length > 0 ? token : null;\n}\n\n/**\n * Extract the shop domain from the request.\n * Checks query parameter first, then x-shopify-shop-domain header.\n * Validates the domain format before returning.\n */\nexport function extractShop(request: AuthRequest): string | null {\n // Prefer query parameter\n const shopFromQuery = request.query['shop'];\n if (shopFromQuery && isValidShopDomain(shopFromQuery)) {\n return shopFromQuery;\n }\n\n // Fall back to header\n const shopFromHeader = request.headers['x-shopify-shop-domain'];\n if (\n shopFromHeader &&\n typeof shopFromHeader === 'string' &&\n isValidShopDomain(shopFromHeader)\n ) {\n return shopFromHeader;\n }\n\n return null;\n}\n\n/**\n * Minimal interface for the Shopify token exchange function.\n * Allows mocking in tests without depending on the full Shopify API.\n */\nexport interface ShopifyTokenExchangeFn {\n (params: {\n sessionToken: string;\n shop: string;\n requestedTokenType: string;\n }): Promise<{\n session: {\n id: string;\n shop: string;\n state: string;\n isOnline: boolean;\n scope?: string;\n expires?: Date;\n accessToken?: string;\n refreshToken?: string;\n onlineAccessInfo?: any;\n };\n }>;\n}\n\n/** Input parameters for performing a Shopify token exchange. */\nexport interface PerformTokenExchangeInput {\n config: AuthConfig;\n sessionToken: string;\n shop: string;\n tokenType: 'online' | 'offline';\n /** Optional: provide a custom token exchange function (for testing). */\n tokenExchangeFn?: ShopifyTokenExchangeFn;\n}\n\n/**\n * Create a Shopify API client and return its tokenExchange function.\n */\nexport async function createShopifyTokenExchangeFn(\n config: AuthConfig,\n): Promise<ShopifyTokenExchangeFn> {\n const { shopifyApi, LogSeverity } = await import('@shopify/shopify-api');\n await import('@shopify/shopify-api/adapters/node');\n\n const shopify = shopifyApi({\n apiKey: config.apiKey,\n apiSecretKey: config.apiSecretKey,\n scopes: config.scopes,\n hostName: config.hostName,\n apiVersion: config.apiVersion as any,\n isEmbeddedApp: config.isEmbeddedApp ?? true,\n logger: { level: LogSeverity.Error },\n });\n\n return shopify.auth.tokenExchange as ShopifyTokenExchangeFn;\n}\n\n/**\n * Exchange a session token for an access token via Shopify API.\n *\n * Maps the Shopify Session result to UniForge's Session type.\n * Emits auth events on success/failure.\n */\nexport async function performTokenExchange(\n input: PerformTokenExchangeInput,\n): Promise<Session> {\n const { config, sessionToken, shop, tokenType } = input;\n\n if (!sessionToken) {\n throw new Error('Session token is required for token exchange.');\n }\n\n if (!isValidShopDomain(shop)) {\n throw new Error(\n `Invalid shop domain \"${shop}\". Expected format: example.myshopify.com`,\n );\n }\n\n const tokenExchangeFn =\n input.tokenExchangeFn ?? (await createShopifyTokenExchangeFn(config));\n\n const requestedTokenType =\n tokenType === 'online'\n ? 'urn:shopify:params:oauth:token-type:online-access-token'\n : 'urn:shopify:params:oauth:token-type:offline-access-token';\n\n try {\n const { session: shopifySession } = await tokenExchangeFn({\n sessionToken,\n shop,\n requestedTokenType,\n });\n\n const now = new Date();\n const session = mapShopifySession(shopifySession, shop, now);\n\n if (config.eventHandler) {\n const event = createAuthEvent({\n type: 'token_exchange_success',\n shopDomain: shop,\n outcome: 'success',\n sessionId: session.id,\n metadata: {\n tokenType,\n sessionId: session.id,\n },\n });\n await Promise.resolve(config.eventHandler.onAuthEvent(event));\n }\n\n return session;\n } catch (error) {\n if (config.eventHandler) {\n const event = createAuthEvent({\n type: 'token_exchange_failure',\n shopDomain: shop,\n outcome: 'failure',\n metadata: {\n tokenType,\n error: error instanceof Error ? error.message : String(error),\n },\n });\n await Promise.resolve(config.eventHandler.onAuthEvent(event));\n }\n\n throw error;\n }\n}\n\n/**\n * Map a Shopify Session object to a UniForge Session.\n */\nfunction mapShopifySession(\n shopifySession: {\n id: string;\n shop: string;\n state: string;\n isOnline: boolean;\n scope?: string;\n expires?: Date;\n accessToken?: string;\n refreshToken?: string;\n onlineAccessInfo?: any;\n },\n shop: string,\n now: Date,\n): Session {\n const session: Session = {\n id: shopifySession.id,\n shop: shopifySession.shop || shop,\n state: shopifySession.state || randomUUID(),\n isOnline: shopifySession.isOnline,\n scope: shopifySession.scope || '',\n expires: shopifySession.expires ?? null,\n createdAt: now,\n updatedAt: now,\n };\n\n if (shopifySession.accessToken) {\n (session as any).accessToken = shopifySession.accessToken;\n }\n\n if (shopifySession.refreshToken) {\n (session as any).refreshToken = shopifySession.refreshToken;\n }\n\n if (shopifySession.onlineAccessInfo) {\n const info = shopifySession.onlineAccessInfo;\n (session as any).onlineAccessInfo = {\n expiresIn: info.expires_in ?? info.expiresIn ?? 0,\n associatedUserScope:\n info.associated_user_scope ?? info.associatedUserScope ?? '',\n associatedUser: info.associated_user\n ? {\n id: info.associated_user.id,\n firstName: info.associated_user.first_name,\n lastName: info.associated_user.last_name,\n email: info.associated_user.email,\n emailVerified: info.associated_user.email_verified,\n accountOwner: info.associated_user.account_owner,\n locale: info.associated_user.locale,\n collaborator: info.associated_user.collaborator,\n }\n : info.associatedUser,\n };\n }\n\n return session;\n}\n","/**\n * Shopify session manager for embedded app authentication.\n *\n * Manages the lifecycle of Shopify sessions: checks storage for\n * existing valid sessions, performs token exchange when needed,\n * refreshes expiring tokens, and stores results via encrypted\n * session storage.\n */\n\nimport type {\n AuthConfig,\n AuthRequest,\n Session,\n} from '@uniforge/platform-core/auth';\nimport {\n TokenEncryptionServiceImpl,\n EncryptedSessionStorage,\n createAuthEvent,\n} from '@uniforge/core/auth';\nimport {\n extractSessionToken,\n extractShop,\n performTokenExchange,\n} from './token-exchange';\nimport type { ShopifyTokenExchangeFn } from './token-exchange';\n\n/**\n * Injectable function for refreshing a Shopify access token.\n * In production, wraps `shopify.auth.refreshToken()`.\n */\nexport interface ShopifyTokenRefreshFn {\n (params: {\n session: {\n accessToken?: string;\n refreshToken?: string;\n shop: string;\n };\n }): Promise<{\n session: {\n id: string;\n shop: string;\n state: string;\n isOnline: boolean;\n scope?: string;\n expires?: Date;\n accessToken?: string;\n refreshToken?: string;\n };\n }>;\n}\n\n/** Options for configuring the ShopifySessionManager with test doubles. */\nexport interface ShopifySessionManagerOptions {\n /** Optional: provide a custom token exchange function (for testing). */\n tokenExchangeFn?: ShopifyTokenExchangeFn;\n /** Optional: provide a custom token refresh function (for testing). */\n tokenRefreshFn?: ShopifyTokenRefreshFn;\n}\n\nexport class ShopifySessionManager {\n private readonly encryptedStorage: EncryptedSessionStorage;\n private readonly tokenExchangeFn?: ShopifyTokenExchangeFn;\n private readonly tokenRefreshFn?: ShopifyTokenRefreshFn;\n\n constructor(\n private readonly config: AuthConfig,\n options?: ShopifySessionManagerOptions,\n ) {\n const encryption = new TokenEncryptionServiceImpl(config.encryption);\n this.encryptedStorage = new EncryptedSessionStorage(\n config.sessionStorage,\n encryption,\n );\n if (options?.tokenExchangeFn) {\n this.tokenExchangeFn = options.tokenExchangeFn;\n }\n if (options?.tokenRefreshFn) {\n this.tokenRefreshFn = options.tokenRefreshFn;\n }\n }\n\n /**\n * Get an existing valid session or create one via token exchange.\n *\n * For embedded apps, this:\n * 1. Extracts the session token and shop from the request\n * 2. Checks storage for an existing non-expired session\n * 3. If no valid session, performs token exchange with Shopify\n * 4. Stores the new session (encrypted) and returns it\n */\n async getOrCreateSession(request: AuthRequest): Promise<Session> {\n const shop = extractShop(request);\n if (!shop) {\n throw new Error(\n 'Could not extract shop domain from request. Provide shop in query parameter or x-shopify-shop-domain header.',\n );\n }\n\n const sessionToken = extractSessionToken(request);\n if (!sessionToken) {\n throw new Error(\n 'Could not extract session token from Authorization header. Expected: Authorization: Bearer <token>',\n );\n }\n\n // Check for existing offline session\n const sessionId = `offline_${shop}`;\n const existingSession = await this.encryptedStorage.loadSession(sessionId);\n\n if (existingSession && !this.isExpired(existingSession)) {\n return existingSession;\n }\n\n // Perform token exchange\n const exchangeInput: Parameters<typeof performTokenExchange>[0] = {\n config: this.config,\n sessionToken,\n shop,\n tokenType: 'offline',\n };\n if (this.tokenExchangeFn) {\n exchangeInput.tokenExchangeFn = this.tokenExchangeFn;\n }\n const session = await performTokenExchange(exchangeInput);\n\n // Store the session (encrypted)\n await this.encryptedStorage.storeSession(session);\n\n return session;\n }\n\n /**\n * Get an existing valid online session or create one via token exchange.\n */\n async getOrCreateOnlineSession(\n request: AuthRequest,\n ): Promise<Session> {\n const shop = extractShop(request);\n if (!shop) {\n throw new Error(\n 'Could not extract shop domain from request.',\n );\n }\n\n const sessionToken = extractSessionToken(request);\n if (!sessionToken) {\n throw new Error(\n 'Could not extract session token from Authorization header.',\n );\n }\n\n // Perform online token exchange\n const onlineExchangeInput: Parameters<typeof performTokenExchange>[0] = {\n config: this.config,\n sessionToken,\n shop,\n tokenType: 'online',\n };\n if (this.tokenExchangeFn) {\n onlineExchangeInput.tokenExchangeFn = this.tokenExchangeFn;\n }\n const session = await performTokenExchange(onlineExchangeInput);\n\n // Store the session (encrypted)\n await this.encryptedStorage.storeSession(session);\n\n return session;\n }\n\n /**\n * Refresh an expiring session token.\n *\n * Retries up to `config.tokenRefresh.maxRetries` on failure.\n * On success, stores the refreshed session (encrypted) and emits\n * a `token_refreshed` event. On final failure, emits\n * `token_refresh_failed` and throws.\n */\n async refreshSessionToken(session: Session): Promise<Session> {\n const maxRetries = this.config.tokenRefresh?.maxRetries ?? 3;\n const refreshFn =\n this.tokenRefreshFn ?? (await this.createDefaultRefreshFn());\n\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n const refreshInput: {\n accessToken?: string;\n refreshToken?: string;\n shop: string;\n } = { shop: session.shop };\n if (session.accessToken) {\n refreshInput.accessToken = session.accessToken;\n }\n if (session.refreshToken) {\n refreshInput.refreshToken = session.refreshToken;\n }\n const { session: refreshedShopifySession } = await refreshFn({\n session: refreshInput,\n });\n\n const now = new Date();\n const refreshedSession = this.mapRefreshedSession(\n refreshedShopifySession,\n session,\n now,\n );\n\n // Store the refreshed session (encrypted)\n await this.encryptedStorage.storeSession(refreshedSession);\n\n if (this.config.eventHandler) {\n const event = createAuthEvent({\n type: 'token_refreshed',\n shopDomain: session.shop,\n outcome: 'success',\n sessionId: session.id,\n metadata: { attempt: String(attempt + 1) },\n });\n await Promise.resolve(\n this.config.eventHandler.onAuthEvent(event),\n );\n }\n\n return refreshedSession;\n } catch (error) {\n lastError =\n error instanceof Error ? error : new Error(String(error));\n }\n }\n\n // All retries exhausted\n if (this.config.eventHandler) {\n const event = createAuthEvent({\n type: 'token_refresh_failed',\n shopDomain: session.shop,\n outcome: 'failure',\n sessionId: session.id,\n metadata: {\n maxRetries: String(maxRetries),\n error: lastError?.message ?? 'Unknown error',\n },\n });\n await Promise.resolve(this.config.eventHandler.onAuthEvent(event));\n }\n\n throw lastError ?? new Error('Token refresh failed after all retries.');\n }\n\n /**\n * Delete all sessions for a shop (e.g., on app uninstall).\n *\n * Finds all sessions via `findSessionsByShop`, deletes them,\n * and emits `session_deleted` events.\n */\n async cleanupShopSessions(shop: string): Promise<void> {\n const sessions =\n await this.config.sessionStorage.findSessionsByShop(shop);\n\n if (sessions.length === 0) {\n return;\n }\n\n const sessionIds = sessions.map((s) => s.id);\n await this.config.sessionStorage.deleteSessions(sessionIds);\n\n if (this.config.eventHandler) {\n for (const session of sessions) {\n const event = createAuthEvent({\n type: 'session_deleted',\n shopDomain: shop,\n outcome: 'success',\n sessionId: session.id,\n metadata: { reason: 'shop_cleanup' },\n });\n await Promise.resolve(\n this.config.eventHandler.onAuthEvent(event),\n );\n }\n }\n }\n\n private isExpired(session: Session): boolean {\n if (!session.expires) {\n return false; // Non-expiring offline session\n }\n return session.expires.getTime() <= Date.now();\n }\n\n private mapRefreshedSession(\n refreshed: {\n id: string;\n shop: string;\n state: string;\n isOnline: boolean;\n scope?: string;\n expires?: Date;\n accessToken?: string;\n refreshToken?: string;\n },\n original: Session,\n now: Date,\n ): Session {\n const session: Session = {\n id: refreshed.id || original.id,\n shop: refreshed.shop || original.shop,\n state: refreshed.state || original.state,\n isOnline: refreshed.isOnline,\n scope: refreshed.scope || original.scope,\n expires: refreshed.expires ?? original.expires,\n createdAt: original.createdAt,\n updatedAt: now,\n };\n\n if (refreshed.accessToken) {\n (session as any).accessToken = refreshed.accessToken;\n }\n\n if (refreshed.refreshToken) {\n (session as any).refreshToken = refreshed.refreshToken;\n }\n\n return session;\n }\n\n private async createDefaultRefreshFn(): Promise<ShopifyTokenRefreshFn> {\n const { shopifyApi, LogSeverity } = await import('@shopify/shopify-api');\n await import('@shopify/shopify-api/adapters/node');\n\n const shopify = shopifyApi({\n apiKey: this.config.apiKey,\n apiSecretKey: this.config.apiSecretKey,\n scopes: this.config.scopes,\n hostName: this.config.hostName,\n apiVersion: this.config.apiVersion as any,\n isEmbeddedApp: this.config.isEmbeddedApp ?? true,\n logger: { level: LogSeverity.Error },\n });\n\n return async (params) => {\n const result = await (shopify.auth as any).refreshToken(params);\n return result;\n };\n }\n}\n","/**\n * HMAC validation for Shopify OAuth callbacks.\n *\n * Validates the HMAC signature on OAuth callback query parameters\n * using timing-safe comparison to prevent timing attacks.\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\n\n/**\n * Validate the HMAC signature on OAuth callback query parameters.\n *\n * Per Shopify's spec: remove the `hmac` parameter, sort remaining\n * params alphabetically, join as `key=value` with `&`, and compute\n * HMAC-SHA256 with the app's API secret key. Compare using\n * timing-safe equality.\n */\nexport function validateOAuthHmac(\n query: Record<string, string>,\n secret: string,\n): boolean {\n const hmac = query['hmac'];\n if (!hmac) {\n return false;\n }\n\n const entries = Object.entries(query)\n .filter(([key]) => key !== 'hmac')\n .sort(([a], [b]) => a.localeCompare(b));\n\n const message = entries\n .map(([key, value]) => `${key}=${value}`)\n .join('&');\n\n const computed = createHmac('sha256', secret)\n .update(message)\n .digest('hex');\n\n // Use timing-safe comparison to prevent timing attacks\n if (computed.length !== hmac.length) {\n return false;\n }\n\n return timingSafeEqual(\n Buffer.from(computed, 'utf-8'),\n Buffer.from(hmac, 'utf-8'),\n );\n}\n","/**\n * OAuth authorization code flow for non-embedded Shopify apps.\n *\n * Implements the begin (redirect to Shopify) and callback\n * (exchange code for access token) phases of the OAuth flow.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type {\n AuthConfig,\n AuthRequest,\n Session,\n} from '@uniforge/platform-core/auth';\nimport { isValidShopDomain } from '@uniforge/platform-core/auth';\nimport { createAuthEvent } from '@uniforge/core/auth';\nimport {\n TokenEncryptionServiceImpl,\n EncryptedSessionStorage,\n} from '@uniforge/core/auth';\nimport { validateOAuthHmac } from './hmac';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Injectable function for initiating OAuth.\n * In production, wraps `shopify.auth.begin()`.\n */\nexport interface ShopifyOAuthBeginFn {\n (params: {\n shop: string;\n callbackPath: string;\n isOnline: boolean;\n }): Promise<{ redirectUrl: string }>;\n}\n\n/**\n * Injectable function for handling the OAuth callback.\n * In production, wraps `shopify.auth.callback()`.\n */\nexport interface ShopifyOAuthCallbackFn {\n (params: {\n query: Record<string, string>;\n }): Promise<{\n session: {\n id: string;\n shop: string;\n state: string;\n isOnline: boolean;\n scope?: string;\n expires?: Date;\n accessToken?: string;\n refreshToken?: string;\n };\n }>;\n}\n\n/** Input parameters for beginning the OAuth authorization flow. */\nexport interface BeginOAuthInput {\n config: AuthConfig;\n request: AuthRequest;\n callbackPath: string;\n isOnline?: boolean;\n beginFn?: ShopifyOAuthBeginFn;\n}\n\n/** Result from beginning OAuth containing the authorization redirect URL. */\nexport interface BeginOAuthResult {\n redirectUrl: string;\n}\n\n/** Input parameters for handling the OAuth callback from Shopify. */\nexport interface HandleOAuthCallbackInput {\n config: AuthConfig;\n request: AuthRequest;\n callbackFn?: ShopifyOAuthCallbackFn;\n}\n\n/** Result from OAuth callback — either success with session or failure with error details. */\nexport type OAuthCallbackResult =\n | { success: true; session: Session; redirectUrl: string }\n | { success: false; error: { code: string; message: string; statusCode: number } };\n\n// ---------------------------------------------------------------------------\n// Begin OAuth\n// ---------------------------------------------------------------------------\n\n/**\n * Initiate OAuth by redirecting the merchant to Shopify's authorization page.\n */\nexport async function beginOAuth(\n input: BeginOAuthInput,\n): Promise<BeginOAuthResult> {\n const { config, request, callbackPath, isOnline } = input;\n\n const shop = request.query['shop'];\n if (!shop || !isValidShopDomain(shop)) {\n throw new Error(\n 'Could not extract valid shop domain from request. Provide shop in query parameter.',\n );\n }\n\n const beginFn = input.beginFn ?? (await createShopifyOAuthBeginFn(config));\n\n const { redirectUrl } = await beginFn({\n shop,\n callbackPath,\n isOnline: isOnline ?? false,\n });\n\n if (config.eventHandler) {\n const event = createAuthEvent({\n type: 'oauth_begin',\n shopDomain: shop,\n outcome: 'success',\n metadata: { callbackPath },\n });\n await Promise.resolve(config.eventHandler.onAuthEvent(event));\n }\n\n return { redirectUrl };\n}\n\n// ---------------------------------------------------------------------------\n// Handle OAuth Callback\n// ---------------------------------------------------------------------------\n\n/**\n * Handle the OAuth callback from Shopify.\n *\n * Validates HMAC, checks for authorization denial, exchanges the code\n * for an access token, validates scopes, stores the session encrypted,\n * and emits events.\n */\nexport async function handleOAuthCallback(\n input: HandleOAuthCallbackInput,\n): Promise<OAuthCallbackResult> {\n const { config, request } = input;\n const query = request.query;\n const shop = query['shop'] ?? '';\n\n // Check for authorization denial (Shopify sends error param)\n if (query['error']) {\n const errorDesc =\n query['error_description'] ?? `Authorization ${query['error']}`;\n\n if (config.eventHandler) {\n const event = createAuthEvent({\n type: 'oauth_callback_failure',\n shopDomain: shop,\n outcome: 'failure',\n metadata: {\n error: query['error'],\n errorDescription: errorDesc,\n },\n });\n await Promise.resolve(config.eventHandler.onAuthEvent(event));\n }\n\n return {\n success: false,\n error: {\n code: 'ACCESS_DENIED',\n message: errorDesc,\n statusCode: 403,\n },\n };\n }\n\n // Validate HMAC\n if (!validateOAuthHmac(query, config.apiSecretKey)) {\n if (config.eventHandler) {\n const event = createAuthEvent({\n type: 'oauth_callback_failure',\n shopDomain: shop,\n outcome: 'failure',\n metadata: { error: 'HMAC validation failed' },\n });\n await Promise.resolve(config.eventHandler.onAuthEvent(event));\n }\n\n return {\n success: false,\n error: {\n code: 'HMAC_VALIDATION_FAILED',\n message: 'HMAC validation failed on OAuth callback.',\n statusCode: 403,\n },\n };\n }\n\n // Exchange the authorization code for an access token\n try {\n const callbackFn =\n input.callbackFn ?? (await createShopifyOAuthCallbackFn(config));\n\n const { session: shopifySession } = await callbackFn({ query });\n\n const now = new Date();\n const session = mapOAuthSession(shopifySession, shop, now);\n\n // Validate scopes\n const grantedScopes = new Set(\n (session.scope || '').split(',').map((s) => s.trim()),\n );\n const requiredScopes = config.scopes;\n const missingScopes = requiredScopes.filter(\n (scope) => !grantedScopes.has(scope),\n );\n\n if (missingScopes.length > 0) {\n if (config.eventHandler) {\n const event = createAuthEvent({\n type: 'oauth_callback_failure',\n shopDomain: shop,\n outcome: 'failure',\n metadata: {\n error: 'scope_mismatch',\n missingScopes: missingScopes.join(','),\n },\n });\n await Promise.resolve(config.eventHandler.onAuthEvent(event));\n }\n\n return {\n success: false,\n error: {\n code: 'SCOPE_MISMATCH',\n message: `Missing required scopes: ${missingScopes.join(', ')}. Granted: ${session.scope}`,\n statusCode: 403,\n },\n };\n }\n\n // Store the session encrypted\n const encryption = new TokenEncryptionServiceImpl(config.encryption);\n const encryptedStorage = new EncryptedSessionStorage(\n config.sessionStorage,\n encryption,\n );\n await encryptedStorage.storeSession(session);\n\n if (config.eventHandler) {\n const event = createAuthEvent({\n type: 'oauth_callback_success',\n shopDomain: shop,\n outcome: 'success',\n sessionId: session.id,\n metadata: { scope: session.scope },\n });\n await Promise.resolve(config.eventHandler.onAuthEvent(event));\n }\n\n return {\n success: true,\n session,\n redirectUrl: `${config.hostName}/?shop=${shop}`,\n };\n } catch (error) {\n if (config.eventHandler) {\n const event = createAuthEvent({\n type: 'oauth_callback_failure',\n shopDomain: shop,\n outcome: 'failure',\n metadata: {\n error: error instanceof Error ? error.message : String(error),\n },\n });\n await Promise.resolve(config.eventHandler.onAuthEvent(event));\n }\n\n return {\n success: false,\n error: {\n code: 'OAUTH_CALLBACK_FAILED',\n message: error instanceof Error ? error.message : String(error),\n statusCode: 500,\n },\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Shopify API factory functions (production use)\n// ---------------------------------------------------------------------------\n\nasync function createShopifyOAuthBeginFn(\n config: AuthConfig,\n): Promise<ShopifyOAuthBeginFn> {\n // Construct the OAuth authorization URL using config values.\n // The Shopify API's begin() method performs a redirect on the raw response,\n // but our abstraction returns the URL instead so framework adapters can handle it.\n return async (params) => {\n const nonce = randomUUID();\n const scopeString = config.scopes.join(',');\n const redirectUri = `${config.hostName}${params.callbackPath}`;\n const redirectUrl = `https://${params.shop}/admin/oauth/authorize?client_id=${config.apiKey}&scope=${encodeURIComponent(scopeString)}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${nonce}`;\n return { redirectUrl };\n };\n}\n\nasync function createShopifyOAuthCallbackFn(\n config: AuthConfig,\n): Promise<ShopifyOAuthCallbackFn> {\n const { shopifyApi, LogSeverity } = await import('@shopify/shopify-api');\n await import('@shopify/shopify-api/adapters/node');\n\n const shopify = shopifyApi({\n apiKey: config.apiKey,\n apiSecretKey: config.apiSecretKey,\n scopes: config.scopes,\n hostName: config.hostName,\n apiVersion: config.apiVersion as any,\n isEmbeddedApp: config.isEmbeddedApp ?? false,\n logger: { level: LogSeverity.Error },\n });\n\n return async (params) => {\n const result = await (shopify.auth.callback as any)(params);\n return result;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Session mapping\n// ---------------------------------------------------------------------------\n\nfunction mapOAuthSession(\n shopifySession: {\n id: string;\n shop: string;\n state: string;\n isOnline: boolean;\n scope?: string;\n expires?: Date;\n accessToken?: string;\n refreshToken?: string;\n },\n shop: string,\n now: Date,\n): Session {\n const session: Session = {\n id: shopifySession.id,\n shop: shopifySession.shop || shop,\n state: shopifySession.state || randomUUID(),\n isOnline: shopifySession.isOnline,\n scope: shopifySession.scope || '',\n expires: shopifySession.expires ?? null,\n createdAt: now,\n updatedAt: now,\n };\n\n if (shopifySession.accessToken) {\n (session as any).accessToken = shopifySession.accessToken;\n }\n\n if (shopifySession.refreshToken) {\n (session as any).refreshToken = shopifySession.refreshToken;\n }\n\n return session;\n}\n"],"mappings":"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","names":["import_auth","import_auth","import_node_crypto","import_node_crypto","import_auth"]}
@@ -0,0 +1,586 @@
1
+ // src/auth/token-exchange.ts
2
+ import { randomUUID } from "crypto";
3
+ import { isValidShopDomain } from "@uniforge/platform-core/auth";
4
+ import { createAuthEvent } from "@uniforge/core/auth";
5
+ function extractSessionToken(request) {
6
+ const authHeader = request.headers["authorization"] ?? request.headers["Authorization"];
7
+ if (!authHeader || typeof authHeader !== "string") {
8
+ return null;
9
+ }
10
+ if (!authHeader.startsWith("Bearer ")) {
11
+ return null;
12
+ }
13
+ const token = authHeader.slice("Bearer ".length).trim();
14
+ return token.length > 0 ? token : null;
15
+ }
16
+ function extractShop(request) {
17
+ const shopFromQuery = request.query["shop"];
18
+ if (shopFromQuery && isValidShopDomain(shopFromQuery)) {
19
+ return shopFromQuery;
20
+ }
21
+ const shopFromHeader = request.headers["x-shopify-shop-domain"];
22
+ if (shopFromHeader && typeof shopFromHeader === "string" && isValidShopDomain(shopFromHeader)) {
23
+ return shopFromHeader;
24
+ }
25
+ return null;
26
+ }
27
+ async function createShopifyTokenExchangeFn(config) {
28
+ const { shopifyApi, LogSeverity } = await import("@shopify/shopify-api");
29
+ await import("@shopify/shopify-api/adapters/node");
30
+ const shopify = shopifyApi({
31
+ apiKey: config.apiKey,
32
+ apiSecretKey: config.apiSecretKey,
33
+ scopes: config.scopes,
34
+ hostName: config.hostName,
35
+ apiVersion: config.apiVersion,
36
+ isEmbeddedApp: config.isEmbeddedApp ?? true,
37
+ logger: { level: LogSeverity.Error }
38
+ });
39
+ return shopify.auth.tokenExchange;
40
+ }
41
+ async function performTokenExchange(input) {
42
+ const { config, sessionToken, shop, tokenType } = input;
43
+ if (!sessionToken) {
44
+ throw new Error("Session token is required for token exchange.");
45
+ }
46
+ if (!isValidShopDomain(shop)) {
47
+ throw new Error(
48
+ `Invalid shop domain "${shop}". Expected format: example.myshopify.com`
49
+ );
50
+ }
51
+ const tokenExchangeFn = input.tokenExchangeFn ?? await createShopifyTokenExchangeFn(config);
52
+ const requestedTokenType = tokenType === "online" ? "urn:shopify:params:oauth:token-type:online-access-token" : "urn:shopify:params:oauth:token-type:offline-access-token";
53
+ try {
54
+ const { session: shopifySession } = await tokenExchangeFn({
55
+ sessionToken,
56
+ shop,
57
+ requestedTokenType
58
+ });
59
+ const now = /* @__PURE__ */ new Date();
60
+ const session = mapShopifySession(shopifySession, shop, now);
61
+ if (config.eventHandler) {
62
+ const event = createAuthEvent({
63
+ type: "token_exchange_success",
64
+ shopDomain: shop,
65
+ outcome: "success",
66
+ sessionId: session.id,
67
+ metadata: {
68
+ tokenType,
69
+ sessionId: session.id
70
+ }
71
+ });
72
+ await Promise.resolve(config.eventHandler.onAuthEvent(event));
73
+ }
74
+ return session;
75
+ } catch (error) {
76
+ if (config.eventHandler) {
77
+ const event = createAuthEvent({
78
+ type: "token_exchange_failure",
79
+ shopDomain: shop,
80
+ outcome: "failure",
81
+ metadata: {
82
+ tokenType,
83
+ error: error instanceof Error ? error.message : String(error)
84
+ }
85
+ });
86
+ await Promise.resolve(config.eventHandler.onAuthEvent(event));
87
+ }
88
+ throw error;
89
+ }
90
+ }
91
+ function mapShopifySession(shopifySession, shop, now) {
92
+ const session = {
93
+ id: shopifySession.id,
94
+ shop: shopifySession.shop || shop,
95
+ state: shopifySession.state || randomUUID(),
96
+ isOnline: shopifySession.isOnline,
97
+ scope: shopifySession.scope || "",
98
+ expires: shopifySession.expires ?? null,
99
+ createdAt: now,
100
+ updatedAt: now
101
+ };
102
+ if (shopifySession.accessToken) {
103
+ session.accessToken = shopifySession.accessToken;
104
+ }
105
+ if (shopifySession.refreshToken) {
106
+ session.refreshToken = shopifySession.refreshToken;
107
+ }
108
+ if (shopifySession.onlineAccessInfo) {
109
+ const info = shopifySession.onlineAccessInfo;
110
+ session.onlineAccessInfo = {
111
+ expiresIn: info.expires_in ?? info.expiresIn ?? 0,
112
+ associatedUserScope: info.associated_user_scope ?? info.associatedUserScope ?? "",
113
+ associatedUser: info.associated_user ? {
114
+ id: info.associated_user.id,
115
+ firstName: info.associated_user.first_name,
116
+ lastName: info.associated_user.last_name,
117
+ email: info.associated_user.email,
118
+ emailVerified: info.associated_user.email_verified,
119
+ accountOwner: info.associated_user.account_owner,
120
+ locale: info.associated_user.locale,
121
+ collaborator: info.associated_user.collaborator
122
+ } : info.associatedUser
123
+ };
124
+ }
125
+ return session;
126
+ }
127
+
128
+ // src/auth/session.ts
129
+ import {
130
+ TokenEncryptionServiceImpl,
131
+ EncryptedSessionStorage,
132
+ createAuthEvent as createAuthEvent2
133
+ } from "@uniforge/core/auth";
134
+ var ShopifySessionManager = class {
135
+ constructor(config, options) {
136
+ this.config = config;
137
+ const encryption = new TokenEncryptionServiceImpl(config.encryption);
138
+ this.encryptedStorage = new EncryptedSessionStorage(
139
+ config.sessionStorage,
140
+ encryption
141
+ );
142
+ if (options?.tokenExchangeFn) {
143
+ this.tokenExchangeFn = options.tokenExchangeFn;
144
+ }
145
+ if (options?.tokenRefreshFn) {
146
+ this.tokenRefreshFn = options.tokenRefreshFn;
147
+ }
148
+ }
149
+ encryptedStorage;
150
+ tokenExchangeFn;
151
+ tokenRefreshFn;
152
+ /**
153
+ * Get an existing valid session or create one via token exchange.
154
+ *
155
+ * For embedded apps, this:
156
+ * 1. Extracts the session token and shop from the request
157
+ * 2. Checks storage for an existing non-expired session
158
+ * 3. If no valid session, performs token exchange with Shopify
159
+ * 4. Stores the new session (encrypted) and returns it
160
+ */
161
+ async getOrCreateSession(request) {
162
+ const shop = extractShop(request);
163
+ if (!shop) {
164
+ throw new Error(
165
+ "Could not extract shop domain from request. Provide shop in query parameter or x-shopify-shop-domain header."
166
+ );
167
+ }
168
+ const sessionToken = extractSessionToken(request);
169
+ if (!sessionToken) {
170
+ throw new Error(
171
+ "Could not extract session token from Authorization header. Expected: Authorization: Bearer <token>"
172
+ );
173
+ }
174
+ const sessionId = `offline_${shop}`;
175
+ const existingSession = await this.encryptedStorage.loadSession(sessionId);
176
+ if (existingSession && !this.isExpired(existingSession)) {
177
+ return existingSession;
178
+ }
179
+ const exchangeInput = {
180
+ config: this.config,
181
+ sessionToken,
182
+ shop,
183
+ tokenType: "offline"
184
+ };
185
+ if (this.tokenExchangeFn) {
186
+ exchangeInput.tokenExchangeFn = this.tokenExchangeFn;
187
+ }
188
+ const session = await performTokenExchange(exchangeInput);
189
+ await this.encryptedStorage.storeSession(session);
190
+ return session;
191
+ }
192
+ /**
193
+ * Get an existing valid online session or create one via token exchange.
194
+ */
195
+ async getOrCreateOnlineSession(request) {
196
+ const shop = extractShop(request);
197
+ if (!shop) {
198
+ throw new Error(
199
+ "Could not extract shop domain from request."
200
+ );
201
+ }
202
+ const sessionToken = extractSessionToken(request);
203
+ if (!sessionToken) {
204
+ throw new Error(
205
+ "Could not extract session token from Authorization header."
206
+ );
207
+ }
208
+ const onlineExchangeInput = {
209
+ config: this.config,
210
+ sessionToken,
211
+ shop,
212
+ tokenType: "online"
213
+ };
214
+ if (this.tokenExchangeFn) {
215
+ onlineExchangeInput.tokenExchangeFn = this.tokenExchangeFn;
216
+ }
217
+ const session = await performTokenExchange(onlineExchangeInput);
218
+ await this.encryptedStorage.storeSession(session);
219
+ return session;
220
+ }
221
+ /**
222
+ * Refresh an expiring session token.
223
+ *
224
+ * Retries up to `config.tokenRefresh.maxRetries` on failure.
225
+ * On success, stores the refreshed session (encrypted) and emits
226
+ * a `token_refreshed` event. On final failure, emits
227
+ * `token_refresh_failed` and throws.
228
+ */
229
+ async refreshSessionToken(session) {
230
+ const maxRetries = this.config.tokenRefresh?.maxRetries ?? 3;
231
+ const refreshFn = this.tokenRefreshFn ?? await this.createDefaultRefreshFn();
232
+ let lastError;
233
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
234
+ try {
235
+ const refreshInput = { shop: session.shop };
236
+ if (session.accessToken) {
237
+ refreshInput.accessToken = session.accessToken;
238
+ }
239
+ if (session.refreshToken) {
240
+ refreshInput.refreshToken = session.refreshToken;
241
+ }
242
+ const { session: refreshedShopifySession } = await refreshFn({
243
+ session: refreshInput
244
+ });
245
+ const now = /* @__PURE__ */ new Date();
246
+ const refreshedSession = this.mapRefreshedSession(
247
+ refreshedShopifySession,
248
+ session,
249
+ now
250
+ );
251
+ await this.encryptedStorage.storeSession(refreshedSession);
252
+ if (this.config.eventHandler) {
253
+ const event = createAuthEvent2({
254
+ type: "token_refreshed",
255
+ shopDomain: session.shop,
256
+ outcome: "success",
257
+ sessionId: session.id,
258
+ metadata: { attempt: String(attempt + 1) }
259
+ });
260
+ await Promise.resolve(
261
+ this.config.eventHandler.onAuthEvent(event)
262
+ );
263
+ }
264
+ return refreshedSession;
265
+ } catch (error) {
266
+ lastError = error instanceof Error ? error : new Error(String(error));
267
+ }
268
+ }
269
+ if (this.config.eventHandler) {
270
+ const event = createAuthEvent2({
271
+ type: "token_refresh_failed",
272
+ shopDomain: session.shop,
273
+ outcome: "failure",
274
+ sessionId: session.id,
275
+ metadata: {
276
+ maxRetries: String(maxRetries),
277
+ error: lastError?.message ?? "Unknown error"
278
+ }
279
+ });
280
+ await Promise.resolve(this.config.eventHandler.onAuthEvent(event));
281
+ }
282
+ throw lastError ?? new Error("Token refresh failed after all retries.");
283
+ }
284
+ /**
285
+ * Delete all sessions for a shop (e.g., on app uninstall).
286
+ *
287
+ * Finds all sessions via `findSessionsByShop`, deletes them,
288
+ * and emits `session_deleted` events.
289
+ */
290
+ async cleanupShopSessions(shop) {
291
+ const sessions = await this.config.sessionStorage.findSessionsByShop(shop);
292
+ if (sessions.length === 0) {
293
+ return;
294
+ }
295
+ const sessionIds = sessions.map((s) => s.id);
296
+ await this.config.sessionStorage.deleteSessions(sessionIds);
297
+ if (this.config.eventHandler) {
298
+ for (const session of sessions) {
299
+ const event = createAuthEvent2({
300
+ type: "session_deleted",
301
+ shopDomain: shop,
302
+ outcome: "success",
303
+ sessionId: session.id,
304
+ metadata: { reason: "shop_cleanup" }
305
+ });
306
+ await Promise.resolve(
307
+ this.config.eventHandler.onAuthEvent(event)
308
+ );
309
+ }
310
+ }
311
+ }
312
+ isExpired(session) {
313
+ if (!session.expires) {
314
+ return false;
315
+ }
316
+ return session.expires.getTime() <= Date.now();
317
+ }
318
+ mapRefreshedSession(refreshed, original, now) {
319
+ const session = {
320
+ id: refreshed.id || original.id,
321
+ shop: refreshed.shop || original.shop,
322
+ state: refreshed.state || original.state,
323
+ isOnline: refreshed.isOnline,
324
+ scope: refreshed.scope || original.scope,
325
+ expires: refreshed.expires ?? original.expires,
326
+ createdAt: original.createdAt,
327
+ updatedAt: now
328
+ };
329
+ if (refreshed.accessToken) {
330
+ session.accessToken = refreshed.accessToken;
331
+ }
332
+ if (refreshed.refreshToken) {
333
+ session.refreshToken = refreshed.refreshToken;
334
+ }
335
+ return session;
336
+ }
337
+ async createDefaultRefreshFn() {
338
+ const { shopifyApi, LogSeverity } = await import("@shopify/shopify-api");
339
+ await import("@shopify/shopify-api/adapters/node");
340
+ const shopify = shopifyApi({
341
+ apiKey: this.config.apiKey,
342
+ apiSecretKey: this.config.apiSecretKey,
343
+ scopes: this.config.scopes,
344
+ hostName: this.config.hostName,
345
+ apiVersion: this.config.apiVersion,
346
+ isEmbeddedApp: this.config.isEmbeddedApp ?? true,
347
+ logger: { level: LogSeverity.Error }
348
+ });
349
+ return async (params) => {
350
+ const result = await shopify.auth.refreshToken(params);
351
+ return result;
352
+ };
353
+ }
354
+ };
355
+
356
+ // src/auth/hmac.ts
357
+ import { createHmac, timingSafeEqual } from "crypto";
358
+ function validateOAuthHmac(query, secret) {
359
+ const hmac = query["hmac"];
360
+ if (!hmac) {
361
+ return false;
362
+ }
363
+ const entries = Object.entries(query).filter(([key]) => key !== "hmac").sort(([a], [b]) => a.localeCompare(b));
364
+ const message = entries.map(([key, value]) => `${key}=${value}`).join("&");
365
+ const computed = createHmac("sha256", secret).update(message).digest("hex");
366
+ if (computed.length !== hmac.length) {
367
+ return false;
368
+ }
369
+ return timingSafeEqual(
370
+ Buffer.from(computed, "utf-8"),
371
+ Buffer.from(hmac, "utf-8")
372
+ );
373
+ }
374
+
375
+ // src/auth/oauth.ts
376
+ import { randomUUID as randomUUID2 } from "crypto";
377
+ import { isValidShopDomain as isValidShopDomain2 } from "@uniforge/platform-core/auth";
378
+ import { createAuthEvent as createAuthEvent3 } from "@uniforge/core/auth";
379
+ import {
380
+ TokenEncryptionServiceImpl as TokenEncryptionServiceImpl2,
381
+ EncryptedSessionStorage as EncryptedSessionStorage2
382
+ } from "@uniforge/core/auth";
383
+ async function beginOAuth(input) {
384
+ const { config, request, callbackPath, isOnline } = input;
385
+ const shop = request.query["shop"];
386
+ if (!shop || !isValidShopDomain2(shop)) {
387
+ throw new Error(
388
+ "Could not extract valid shop domain from request. Provide shop in query parameter."
389
+ );
390
+ }
391
+ const beginFn = input.beginFn ?? await createShopifyOAuthBeginFn(config);
392
+ const { redirectUrl } = await beginFn({
393
+ shop,
394
+ callbackPath,
395
+ isOnline: isOnline ?? false
396
+ });
397
+ if (config.eventHandler) {
398
+ const event = createAuthEvent3({
399
+ type: "oauth_begin",
400
+ shopDomain: shop,
401
+ outcome: "success",
402
+ metadata: { callbackPath }
403
+ });
404
+ await Promise.resolve(config.eventHandler.onAuthEvent(event));
405
+ }
406
+ return { redirectUrl };
407
+ }
408
+ async function handleOAuthCallback(input) {
409
+ const { config, request } = input;
410
+ const query = request.query;
411
+ const shop = query["shop"] ?? "";
412
+ if (query["error"]) {
413
+ const errorDesc = query["error_description"] ?? `Authorization ${query["error"]}`;
414
+ if (config.eventHandler) {
415
+ const event = createAuthEvent3({
416
+ type: "oauth_callback_failure",
417
+ shopDomain: shop,
418
+ outcome: "failure",
419
+ metadata: {
420
+ error: query["error"],
421
+ errorDescription: errorDesc
422
+ }
423
+ });
424
+ await Promise.resolve(config.eventHandler.onAuthEvent(event));
425
+ }
426
+ return {
427
+ success: false,
428
+ error: {
429
+ code: "ACCESS_DENIED",
430
+ message: errorDesc,
431
+ statusCode: 403
432
+ }
433
+ };
434
+ }
435
+ if (!validateOAuthHmac(query, config.apiSecretKey)) {
436
+ if (config.eventHandler) {
437
+ const event = createAuthEvent3({
438
+ type: "oauth_callback_failure",
439
+ shopDomain: shop,
440
+ outcome: "failure",
441
+ metadata: { error: "HMAC validation failed" }
442
+ });
443
+ await Promise.resolve(config.eventHandler.onAuthEvent(event));
444
+ }
445
+ return {
446
+ success: false,
447
+ error: {
448
+ code: "HMAC_VALIDATION_FAILED",
449
+ message: "HMAC validation failed on OAuth callback.",
450
+ statusCode: 403
451
+ }
452
+ };
453
+ }
454
+ try {
455
+ const callbackFn = input.callbackFn ?? await createShopifyOAuthCallbackFn(config);
456
+ const { session: shopifySession } = await callbackFn({ query });
457
+ const now = /* @__PURE__ */ new Date();
458
+ const session = mapOAuthSession(shopifySession, shop, now);
459
+ const grantedScopes = new Set(
460
+ (session.scope || "").split(",").map((s) => s.trim())
461
+ );
462
+ const requiredScopes = config.scopes;
463
+ const missingScopes = requiredScopes.filter(
464
+ (scope) => !grantedScopes.has(scope)
465
+ );
466
+ if (missingScopes.length > 0) {
467
+ if (config.eventHandler) {
468
+ const event = createAuthEvent3({
469
+ type: "oauth_callback_failure",
470
+ shopDomain: shop,
471
+ outcome: "failure",
472
+ metadata: {
473
+ error: "scope_mismatch",
474
+ missingScopes: missingScopes.join(",")
475
+ }
476
+ });
477
+ await Promise.resolve(config.eventHandler.onAuthEvent(event));
478
+ }
479
+ return {
480
+ success: false,
481
+ error: {
482
+ code: "SCOPE_MISMATCH",
483
+ message: `Missing required scopes: ${missingScopes.join(", ")}. Granted: ${session.scope}`,
484
+ statusCode: 403
485
+ }
486
+ };
487
+ }
488
+ const encryption = new TokenEncryptionServiceImpl2(config.encryption);
489
+ const encryptedStorage = new EncryptedSessionStorage2(
490
+ config.sessionStorage,
491
+ encryption
492
+ );
493
+ await encryptedStorage.storeSession(session);
494
+ if (config.eventHandler) {
495
+ const event = createAuthEvent3({
496
+ type: "oauth_callback_success",
497
+ shopDomain: shop,
498
+ outcome: "success",
499
+ sessionId: session.id,
500
+ metadata: { scope: session.scope }
501
+ });
502
+ await Promise.resolve(config.eventHandler.onAuthEvent(event));
503
+ }
504
+ return {
505
+ success: true,
506
+ session,
507
+ redirectUrl: `${config.hostName}/?shop=${shop}`
508
+ };
509
+ } catch (error) {
510
+ if (config.eventHandler) {
511
+ const event = createAuthEvent3({
512
+ type: "oauth_callback_failure",
513
+ shopDomain: shop,
514
+ outcome: "failure",
515
+ metadata: {
516
+ error: error instanceof Error ? error.message : String(error)
517
+ }
518
+ });
519
+ await Promise.resolve(config.eventHandler.onAuthEvent(event));
520
+ }
521
+ return {
522
+ success: false,
523
+ error: {
524
+ code: "OAUTH_CALLBACK_FAILED",
525
+ message: error instanceof Error ? error.message : String(error),
526
+ statusCode: 500
527
+ }
528
+ };
529
+ }
530
+ }
531
+ async function createShopifyOAuthBeginFn(config) {
532
+ return async (params) => {
533
+ const nonce = randomUUID2();
534
+ const scopeString = config.scopes.join(",");
535
+ const redirectUri = `${config.hostName}${params.callbackPath}`;
536
+ const redirectUrl = `https://${params.shop}/admin/oauth/authorize?client_id=${config.apiKey}&scope=${encodeURIComponent(scopeString)}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${nonce}`;
537
+ return { redirectUrl };
538
+ };
539
+ }
540
+ async function createShopifyOAuthCallbackFn(config) {
541
+ const { shopifyApi, LogSeverity } = await import("@shopify/shopify-api");
542
+ await import("@shopify/shopify-api/adapters/node");
543
+ const shopify = shopifyApi({
544
+ apiKey: config.apiKey,
545
+ apiSecretKey: config.apiSecretKey,
546
+ scopes: config.scopes,
547
+ hostName: config.hostName,
548
+ apiVersion: config.apiVersion,
549
+ isEmbeddedApp: config.isEmbeddedApp ?? false,
550
+ logger: { level: LogSeverity.Error }
551
+ });
552
+ return async (params) => {
553
+ const result = await shopify.auth.callback(params);
554
+ return result;
555
+ };
556
+ }
557
+ function mapOAuthSession(shopifySession, shop, now) {
558
+ const session = {
559
+ id: shopifySession.id,
560
+ shop: shopifySession.shop || shop,
561
+ state: shopifySession.state || randomUUID2(),
562
+ isOnline: shopifySession.isOnline,
563
+ scope: shopifySession.scope || "",
564
+ expires: shopifySession.expires ?? null,
565
+ createdAt: now,
566
+ updatedAt: now
567
+ };
568
+ if (shopifySession.accessToken) {
569
+ session.accessToken = shopifySession.accessToken;
570
+ }
571
+ if (shopifySession.refreshToken) {
572
+ session.refreshToken = shopifySession.refreshToken;
573
+ }
574
+ return session;
575
+ }
576
+ export {
577
+ ShopifySessionManager,
578
+ beginOAuth,
579
+ createShopifyTokenExchangeFn,
580
+ extractSessionToken,
581
+ extractShop,
582
+ handleOAuthCallback,
583
+ performTokenExchange,
584
+ validateOAuthHmac
585
+ };
586
+ //# sourceMappingURL=index.mjs.map