@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/token-exchange.ts","../../src/auth/session.ts","../../src/auth/hmac.ts","../../src/auth/oauth.ts"],"sourcesContent":["/**\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":["createAuthEvent","createAuthEvent","randomUUID","isValidShopDomain","createAuthEvent","TokenEncryptionServiceImpl","EncryptedSessionStorage","isValidShopDomain","createAuthEvent","TokenEncryptionServiceImpl","EncryptedSessionStorage","randomUUID"]}
@@ -0,0 +1,58 @@
1
+ import { GraphQLClient } from '@uniforge/platform-core/graphql';
2
+ import { BillingService, PlanPricing, Subscription, UsageRecord } from '@uniforge/platform-core/billing';
3
+ import { WebhookHandler } from '@uniforge/platform-core/webhooks';
4
+
5
+ /**
6
+ * Shopify billing service.
7
+ *
8
+ * Wraps the core BillingService with Shopify GraphQL mutations
9
+ * for creating subscriptions and usage records.
10
+ */
11
+
12
+ /** Configuration for the Shopify billing service. */
13
+ interface ShopifyBillingServiceConfig {
14
+ graphqlClient: GraphQLClient;
15
+ billingService: BillingService;
16
+ }
17
+ /** Create a Shopify billing service that wraps BillingService with GraphQL operations. */
18
+ declare function createShopifyBillingService(config: ShopifyBillingServiceConfig): {
19
+ createSubscription(input: {
20
+ shopDomain: string;
21
+ planName: string;
22
+ returnUrl: string;
23
+ lineItems: PlanPricing[];
24
+ trialDays?: number;
25
+ }): Promise<{
26
+ subscription: Subscription;
27
+ confirmationUrl: string;
28
+ }>;
29
+ createUsageRecord(input: {
30
+ subscriptionLineItemId: string;
31
+ amount: string;
32
+ currencyCode: string;
33
+ description: string;
34
+ subscriptionId: string;
35
+ idempotencyKey?: string;
36
+ }): Promise<UsageRecord>;
37
+ getSubscription(id: string): Promise<Subscription | null>;
38
+ getActiveSubscription(shopDomain: string): Promise<Subscription | null>;
39
+ cancelSubscription(id: string): Promise<Subscription>;
40
+ };
41
+
42
+ /**
43
+ * Shopify subscription webhook handler.
44
+ *
45
+ * Handles APP_SUBSCRIPTIONS_UPDATE webhooks to sync subscription
46
+ * status changes from Shopify into the local database.
47
+ */
48
+
49
+ /** Create a webhook handler for APP_SUBSCRIPTIONS_UPDATE events. */
50
+ declare function createSubscriptionWebhookHandler(billingService: BillingService): WebhookHandler;
51
+
52
+ /**
53
+ * Shopify billing GraphQL mutations.
54
+ */
55
+ declare const APP_SUBSCRIPTION_CREATE_MUTATION = "\n mutation AppSubscriptionCreate($name: String!, $lineItems: [AppSubscriptionLineItemInput!]!, $returnUrl: URL!, $trialDays: Int) {\n appSubscriptionCreate(name: $name, lineItems: $lineItems, returnUrl: $returnUrl, trialDays: $trialDays) {\n appSubscription {\n id\n name\n status\n lineItems {\n id\n plan {\n pricingDetails {\n ... on AppRecurringPricing {\n price { amount currencyCode }\n interval\n }\n ... on AppUsagePricing {\n balanceUsed { amount currencyCode }\n cappedAmount { amount currencyCode }\n terms\n }\n }\n }\n }\n confirmationUrl\n }\n userErrors {\n field\n message\n }\n }\n }\n";
56
+ declare const APP_USAGE_RECORD_CREATE_MUTATION = "\n mutation AppUsageRecordCreate($subscriptionLineItemId: ID!, $price: MoneyInput!, $description: String!) {\n appUsageRecordCreate(subscriptionLineItemId: $subscriptionLineItemId, price: $price, description: $description) {\n appUsageRecord {\n id\n }\n userErrors {\n field\n message\n }\n }\n }\n";
57
+
58
+ export { APP_SUBSCRIPTION_CREATE_MUTATION, APP_USAGE_RECORD_CREATE_MUTATION, type ShopifyBillingServiceConfig, createShopifyBillingService, createSubscriptionWebhookHandler };
@@ -0,0 +1,58 @@
1
+ import { GraphQLClient } from '@uniforge/platform-core/graphql';
2
+ import { BillingService, PlanPricing, Subscription, UsageRecord } from '@uniforge/platform-core/billing';
3
+ import { WebhookHandler } from '@uniforge/platform-core/webhooks';
4
+
5
+ /**
6
+ * Shopify billing service.
7
+ *
8
+ * Wraps the core BillingService with Shopify GraphQL mutations
9
+ * for creating subscriptions and usage records.
10
+ */
11
+
12
+ /** Configuration for the Shopify billing service. */
13
+ interface ShopifyBillingServiceConfig {
14
+ graphqlClient: GraphQLClient;
15
+ billingService: BillingService;
16
+ }
17
+ /** Create a Shopify billing service that wraps BillingService with GraphQL operations. */
18
+ declare function createShopifyBillingService(config: ShopifyBillingServiceConfig): {
19
+ createSubscription(input: {
20
+ shopDomain: string;
21
+ planName: string;
22
+ returnUrl: string;
23
+ lineItems: PlanPricing[];
24
+ trialDays?: number;
25
+ }): Promise<{
26
+ subscription: Subscription;
27
+ confirmationUrl: string;
28
+ }>;
29
+ createUsageRecord(input: {
30
+ subscriptionLineItemId: string;
31
+ amount: string;
32
+ currencyCode: string;
33
+ description: string;
34
+ subscriptionId: string;
35
+ idempotencyKey?: string;
36
+ }): Promise<UsageRecord>;
37
+ getSubscription(id: string): Promise<Subscription | null>;
38
+ getActiveSubscription(shopDomain: string): Promise<Subscription | null>;
39
+ cancelSubscription(id: string): Promise<Subscription>;
40
+ };
41
+
42
+ /**
43
+ * Shopify subscription webhook handler.
44
+ *
45
+ * Handles APP_SUBSCRIPTIONS_UPDATE webhooks to sync subscription
46
+ * status changes from Shopify into the local database.
47
+ */
48
+
49
+ /** Create a webhook handler for APP_SUBSCRIPTIONS_UPDATE events. */
50
+ declare function createSubscriptionWebhookHandler(billingService: BillingService): WebhookHandler;
51
+
52
+ /**
53
+ * Shopify billing GraphQL mutations.
54
+ */
55
+ declare const APP_SUBSCRIPTION_CREATE_MUTATION = "\n mutation AppSubscriptionCreate($name: String!, $lineItems: [AppSubscriptionLineItemInput!]!, $returnUrl: URL!, $trialDays: Int) {\n appSubscriptionCreate(name: $name, lineItems: $lineItems, returnUrl: $returnUrl, trialDays: $trialDays) {\n appSubscription {\n id\n name\n status\n lineItems {\n id\n plan {\n pricingDetails {\n ... on AppRecurringPricing {\n price { amount currencyCode }\n interval\n }\n ... on AppUsagePricing {\n balanceUsed { amount currencyCode }\n cappedAmount { amount currencyCode }\n terms\n }\n }\n }\n }\n confirmationUrl\n }\n userErrors {\n field\n message\n }\n }\n }\n";
56
+ declare const APP_USAGE_RECORD_CREATE_MUTATION = "\n mutation AppUsageRecordCreate($subscriptionLineItemId: ID!, $price: MoneyInput!, $description: String!) {\n appUsageRecordCreate(subscriptionLineItemId: $subscriptionLineItemId, price: $price, description: $description) {\n appUsageRecord {\n id\n }\n userErrors {\n field\n message\n }\n }\n }\n";
57
+
58
+ export { APP_SUBSCRIPTION_CREATE_MUTATION, APP_USAGE_RECORD_CREATE_MUTATION, type ShopifyBillingServiceConfig, createShopifyBillingService, createSubscriptionWebhookHandler };
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/billing/index.ts
21
+ var billing_exports = {};
22
+ __export(billing_exports, {
23
+ APP_SUBSCRIPTION_CREATE_MUTATION: () => APP_SUBSCRIPTION_CREATE_MUTATION,
24
+ APP_USAGE_RECORD_CREATE_MUTATION: () => APP_USAGE_RECORD_CREATE_MUTATION,
25
+ createShopifyBillingService: () => createShopifyBillingService,
26
+ createSubscriptionWebhookHandler: () => createSubscriptionWebhookHandler
27
+ });
28
+ module.exports = __toCommonJS(billing_exports);
29
+
30
+ // src/billing/mutations.ts
31
+ var APP_SUBSCRIPTION_CREATE_MUTATION = `
32
+ mutation AppSubscriptionCreate($name: String!, $lineItems: [AppSubscriptionLineItemInput!]!, $returnUrl: URL!, $trialDays: Int) {
33
+ appSubscriptionCreate(name: $name, lineItems: $lineItems, returnUrl: $returnUrl, trialDays: $trialDays) {
34
+ appSubscription {
35
+ id
36
+ name
37
+ status
38
+ lineItems {
39
+ id
40
+ plan {
41
+ pricingDetails {
42
+ ... on AppRecurringPricing {
43
+ price { amount currencyCode }
44
+ interval
45
+ }
46
+ ... on AppUsagePricing {
47
+ balanceUsed { amount currencyCode }
48
+ cappedAmount { amount currencyCode }
49
+ terms
50
+ }
51
+ }
52
+ }
53
+ }
54
+ confirmationUrl
55
+ }
56
+ userErrors {
57
+ field
58
+ message
59
+ }
60
+ }
61
+ }
62
+ `;
63
+ var APP_USAGE_RECORD_CREATE_MUTATION = `
64
+ mutation AppUsageRecordCreate($subscriptionLineItemId: ID!, $price: MoneyInput!, $description: String!) {
65
+ appUsageRecordCreate(subscriptionLineItemId: $subscriptionLineItemId, price: $price, description: $description) {
66
+ appUsageRecord {
67
+ id
68
+ }
69
+ userErrors {
70
+ field
71
+ message
72
+ }
73
+ }
74
+ }
75
+ `;
76
+
77
+ // src/billing/service.ts
78
+ function mapLineItemToShopifyInput(item) {
79
+ if (item.type === "recurring") {
80
+ return {
81
+ plan: {
82
+ appRecurringPricingDetails: {
83
+ price: {
84
+ amount: item.price.amount,
85
+ currencyCode: item.price.currencyCode
86
+ },
87
+ interval: item.interval
88
+ }
89
+ }
90
+ };
91
+ }
92
+ return {
93
+ plan: {
94
+ appUsagePricingDetails: {
95
+ cappedAmount: {
96
+ amount: item.cappedAmount.amount,
97
+ currencyCode: item.cappedAmount.currencyCode
98
+ },
99
+ terms: item.terms
100
+ }
101
+ }
102
+ };
103
+ }
104
+ function createShopifyBillingService(config) {
105
+ const { graphqlClient, billingService } = config;
106
+ return {
107
+ async createSubscription(input) {
108
+ const variables = {
109
+ name: input.planName,
110
+ lineItems: input.lineItems.map(mapLineItemToShopifyInput),
111
+ returnUrl: input.returnUrl
112
+ };
113
+ if (input.trialDays !== void 0) {
114
+ variables["trialDays"] = input.trialDays;
115
+ }
116
+ const response = await graphqlClient.mutate(
117
+ APP_SUBSCRIPTION_CREATE_MUTATION,
118
+ variables
119
+ );
120
+ if (response.errors?.length) {
121
+ throw new Error(
122
+ `GraphQL errors: ${response.errors.map((e) => e.message).join(", ")}`
123
+ );
124
+ }
125
+ const result = response.data.appSubscriptionCreate;
126
+ if (result.userErrors.length > 0) {
127
+ throw new Error(
128
+ `Shopify billing error: ${result.userErrors.map((e) => e.message).join(", ")}`
129
+ );
130
+ }
131
+ const appSubscription = result.appSubscription;
132
+ const shopifySubscriptionId = appSubscription.id;
133
+ const { confirmationUrl } = appSubscription;
134
+ const subscriptionInput = {
135
+ shopDomain: input.shopDomain,
136
+ planName: input.planName,
137
+ returnUrl: input.returnUrl,
138
+ shopifySubscriptionId,
139
+ confirmationUrl
140
+ };
141
+ if (input.trialDays !== void 0) {
142
+ subscriptionInput.trialDays = input.trialDays;
143
+ }
144
+ const subscription = await billingService.createSubscription(subscriptionInput);
145
+ return { subscription, confirmationUrl };
146
+ },
147
+ async createUsageRecord(input) {
148
+ const response = await graphqlClient.mutate(
149
+ APP_USAGE_RECORD_CREATE_MUTATION,
150
+ {
151
+ subscriptionLineItemId: input.subscriptionLineItemId,
152
+ price: {
153
+ amount: input.amount,
154
+ currencyCode: input.currencyCode
155
+ },
156
+ description: input.description
157
+ }
158
+ );
159
+ if (response.errors?.length) {
160
+ throw new Error(
161
+ `GraphQL errors: ${response.errors.map((e) => e.message).join(", ")}`
162
+ );
163
+ }
164
+ const result = response.data.appUsageRecordCreate;
165
+ if (result.userErrors.length > 0) {
166
+ throw new Error(
167
+ `Shopify usage record error: ${result.userErrors.map((e) => e.message).join(", ")}`
168
+ );
169
+ }
170
+ const usageRecordInput = {
171
+ subscriptionId: input.subscriptionId,
172
+ description: input.description,
173
+ amount: input.amount,
174
+ currencyCode: input.currencyCode
175
+ };
176
+ if (input.idempotencyKey !== void 0) {
177
+ usageRecordInput.idempotencyKey = input.idempotencyKey;
178
+ }
179
+ return billingService.createUsageRecord(usageRecordInput);
180
+ },
181
+ getSubscription(id) {
182
+ return billingService.getSubscription(id);
183
+ },
184
+ getActiveSubscription(shopDomain) {
185
+ return billingService.getActiveSubscription(shopDomain);
186
+ },
187
+ cancelSubscription(id) {
188
+ return billingService.cancelSubscription(id);
189
+ }
190
+ };
191
+ }
192
+
193
+ // src/billing/webhook-handler.ts
194
+ function createSubscriptionWebhookHandler(billingService) {
195
+ return {
196
+ async handle(payload) {
197
+ try {
198
+ const data = payload.payload;
199
+ const subscription = data.app_subscription;
200
+ if (!subscription?.admin_graphql_api_id || !subscription?.status) {
201
+ return { success: false, error: "Invalid webhook payload: missing subscription data" };
202
+ }
203
+ const shopifyId = subscription.admin_graphql_api_id;
204
+ const status = subscription.status;
205
+ const updated = await billingService.updateSubscriptionByShopifyId(shopifyId, { status });
206
+ if (!updated) {
207
+ return { success: false, error: `Subscription not found for Shopify ID: ${shopifyId}` };
208
+ }
209
+ return { success: true };
210
+ } catch (error) {
211
+ return {
212
+ success: false,
213
+ error: error instanceof Error ? error.message : String(error)
214
+ };
215
+ }
216
+ }
217
+ };
218
+ }
219
+ // Annotate the CommonJS export names for ESM import in node:
220
+ 0 && (module.exports = {
221
+ APP_SUBSCRIPTION_CREATE_MUTATION,
222
+ APP_USAGE_RECORD_CREATE_MUTATION,
223
+ createShopifyBillingService,
224
+ createSubscriptionWebhookHandler
225
+ });
226
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/billing/index.ts","../../src/billing/mutations.ts","../../src/billing/service.ts","../../src/billing/webhook-handler.ts"],"sourcesContent":["/**\n * @uniforge/platform-shopify/billing\n *\n * Shopify billing adapter — subscription creation via GraphQL,\n * usage record tracking, and subscription webhook handling.\n */\n\nexport {\n createShopifyBillingService,\n type ShopifyBillingServiceConfig,\n} from './service.js';\nexport { createSubscriptionWebhookHandler } from './webhook-handler.js';\nexport {\n APP_SUBSCRIPTION_CREATE_MUTATION,\n APP_USAGE_RECORD_CREATE_MUTATION,\n} from './mutations.js';\n","/**\n * Shopify billing GraphQL mutations.\n */\n\nexport const APP_SUBSCRIPTION_CREATE_MUTATION = `\n mutation AppSubscriptionCreate($name: String!, $lineItems: [AppSubscriptionLineItemInput!]!, $returnUrl: URL!, $trialDays: Int) {\n appSubscriptionCreate(name: $name, lineItems: $lineItems, returnUrl: $returnUrl, trialDays: $trialDays) {\n appSubscription {\n id\n name\n status\n lineItems {\n id\n plan {\n pricingDetails {\n ... on AppRecurringPricing {\n price { amount currencyCode }\n interval\n }\n ... on AppUsagePricing {\n balanceUsed { amount currencyCode }\n cappedAmount { amount currencyCode }\n terms\n }\n }\n }\n }\n confirmationUrl\n }\n userErrors {\n field\n message\n }\n }\n }\n`;\n\nexport const APP_USAGE_RECORD_CREATE_MUTATION = `\n mutation AppUsageRecordCreate($subscriptionLineItemId: ID!, $price: MoneyInput!, $description: String!) {\n appUsageRecordCreate(subscriptionLineItemId: $subscriptionLineItemId, price: $price, description: $description) {\n appUsageRecord {\n id\n }\n userErrors {\n field\n message\n }\n }\n }\n`;\n","/**\n * Shopify billing service.\n *\n * Wraps the core BillingService with Shopify GraphQL mutations\n * for creating subscriptions and usage records.\n */\n\nimport type { GraphQLClient } from '@uniforge/platform-core/graphql';\nimport type {\n BillingService,\n Subscription,\n UsageRecord,\n PlanPricing,\n} from '@uniforge/platform-core/billing';\nimport {\n APP_SUBSCRIPTION_CREATE_MUTATION,\n APP_USAGE_RECORD_CREATE_MUTATION,\n} from './mutations.js';\n\n/** Configuration for the Shopify billing service. */\nexport interface ShopifyBillingServiceConfig {\n graphqlClient: GraphQLClient;\n billingService: BillingService;\n}\n\ninterface AppSubscriptionCreateResponse {\n appSubscriptionCreate: {\n appSubscription: {\n id: string;\n name: string;\n status: string;\n lineItems: Array<{ id: string }>;\n confirmationUrl: string;\n } | null;\n userErrors: Array<{ field: string[]; message: string }>;\n };\n}\n\ninterface AppUsageRecordCreateResponse {\n appUsageRecordCreate: {\n appUsageRecord: { id: string } | null;\n userErrors: Array<{ field: string[]; message: string }>;\n };\n}\n\nfunction mapLineItemToShopifyInput(item: PlanPricing): Record<string, unknown> {\n if (item.type === 'recurring') {\n return {\n plan: {\n appRecurringPricingDetails: {\n price: {\n amount: item.price.amount,\n currencyCode: item.price.currencyCode,\n },\n interval: item.interval,\n },\n },\n };\n }\n\n return {\n plan: {\n appUsagePricingDetails: {\n cappedAmount: {\n amount: item.cappedAmount.amount,\n currencyCode: item.cappedAmount.currencyCode,\n },\n terms: item.terms,\n },\n },\n };\n}\n\n/** Create a Shopify billing service that wraps BillingService with GraphQL operations. */\nexport function createShopifyBillingService(config: ShopifyBillingServiceConfig) {\n const { graphqlClient, billingService } = config;\n\n return {\n async createSubscription(input: {\n shopDomain: string;\n planName: string;\n returnUrl: string;\n lineItems: PlanPricing[];\n trialDays?: number;\n }): Promise<{ subscription: Subscription; confirmationUrl: string }> {\n const variables: Record<string, unknown> = {\n name: input.planName,\n lineItems: input.lineItems.map(mapLineItemToShopifyInput),\n returnUrl: input.returnUrl,\n };\n if (input.trialDays !== undefined) {\n variables['trialDays'] = input.trialDays;\n }\n\n const response = await graphqlClient.mutate<AppSubscriptionCreateResponse>(\n APP_SUBSCRIPTION_CREATE_MUTATION,\n variables,\n );\n\n if (response.errors?.length) {\n throw new Error(\n `GraphQL errors: ${response.errors.map((e) => e.message).join(', ')}`,\n );\n }\n\n const result = response.data!.appSubscriptionCreate;\n if (result.userErrors.length > 0) {\n throw new Error(\n `Shopify billing error: ${result.userErrors.map((e) => e.message).join(', ')}`,\n );\n }\n\n const appSubscription = result.appSubscription!;\n const shopifySubscriptionId = appSubscription.id;\n const { confirmationUrl } = appSubscription;\n\n const subscriptionInput: Parameters<typeof billingService.createSubscription>[0] = {\n shopDomain: input.shopDomain,\n planName: input.planName,\n returnUrl: input.returnUrl,\n shopifySubscriptionId,\n confirmationUrl,\n };\n if (input.trialDays !== undefined) {\n subscriptionInput.trialDays = input.trialDays;\n }\n\n const subscription = await billingService.createSubscription(subscriptionInput);\n\n return { subscription, confirmationUrl };\n },\n\n async createUsageRecord(input: {\n subscriptionLineItemId: string;\n amount: string;\n currencyCode: string;\n description: string;\n subscriptionId: string;\n idempotencyKey?: string;\n }): Promise<UsageRecord> {\n const response = await graphqlClient.mutate<AppUsageRecordCreateResponse>(\n APP_USAGE_RECORD_CREATE_MUTATION,\n {\n subscriptionLineItemId: input.subscriptionLineItemId,\n price: {\n amount: input.amount,\n currencyCode: input.currencyCode,\n },\n description: input.description,\n },\n );\n\n if (response.errors?.length) {\n throw new Error(\n `GraphQL errors: ${response.errors.map((e) => e.message).join(', ')}`,\n );\n }\n\n const result = response.data!.appUsageRecordCreate;\n if (result.userErrors.length > 0) {\n throw new Error(\n `Shopify usage record error: ${result.userErrors.map((e) => e.message).join(', ')}`,\n );\n }\n\n const usageRecordInput: Parameters<typeof billingService.createUsageRecord>[0] = {\n subscriptionId: input.subscriptionId,\n description: input.description,\n amount: input.amount,\n currencyCode: input.currencyCode,\n };\n if (input.idempotencyKey !== undefined) {\n usageRecordInput.idempotencyKey = input.idempotencyKey;\n }\n\n return billingService.createUsageRecord(usageRecordInput);\n },\n\n getSubscription(id: string) {\n return billingService.getSubscription(id);\n },\n\n getActiveSubscription(shopDomain: string) {\n return billingService.getActiveSubscription(shopDomain);\n },\n\n cancelSubscription(id: string) {\n return billingService.cancelSubscription(id);\n },\n };\n}\n","/**\n * Shopify subscription webhook handler.\n *\n * Handles APP_SUBSCRIPTIONS_UPDATE webhooks to sync subscription\n * status changes from Shopify into the local database.\n */\n\nimport type { BillingService, SubscriptionStatus } from '@uniforge/platform-core/billing';\nimport type {\n WebhookHandler,\n WebhookPayload,\n WebhookHandlerResult,\n} from '@uniforge/platform-core/webhooks';\n\ninterface AppSubscriptionUpdatePayload {\n app_subscription: {\n admin_graphql_api_id: string;\n name: string;\n status: string;\n };\n}\n\n/** Create a webhook handler for APP_SUBSCRIPTIONS_UPDATE events. */\nexport function createSubscriptionWebhookHandler(\n billingService: BillingService,\n): WebhookHandler {\n return {\n async handle(payload: WebhookPayload): Promise<WebhookHandlerResult> {\n try {\n const data = payload.payload as AppSubscriptionUpdatePayload;\n const subscription = data.app_subscription;\n\n if (!subscription?.admin_graphql_api_id || !subscription?.status) {\n return { success: false, error: 'Invalid webhook payload: missing subscription data' };\n }\n\n const shopifyId = subscription.admin_graphql_api_id;\n const status = subscription.status as SubscriptionStatus;\n\n const updated = await billingService.updateSubscriptionByShopifyId(shopifyId, { status });\n\n if (!updated) {\n return { success: false, error: `Subscription not found for Shopify ID: ${shopifyId}` };\n }\n\n return { success: true };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,196 @@
1
+ // src/billing/mutations.ts
2
+ var APP_SUBSCRIPTION_CREATE_MUTATION = `
3
+ mutation AppSubscriptionCreate($name: String!, $lineItems: [AppSubscriptionLineItemInput!]!, $returnUrl: URL!, $trialDays: Int) {
4
+ appSubscriptionCreate(name: $name, lineItems: $lineItems, returnUrl: $returnUrl, trialDays: $trialDays) {
5
+ appSubscription {
6
+ id
7
+ name
8
+ status
9
+ lineItems {
10
+ id
11
+ plan {
12
+ pricingDetails {
13
+ ... on AppRecurringPricing {
14
+ price { amount currencyCode }
15
+ interval
16
+ }
17
+ ... on AppUsagePricing {
18
+ balanceUsed { amount currencyCode }
19
+ cappedAmount { amount currencyCode }
20
+ terms
21
+ }
22
+ }
23
+ }
24
+ }
25
+ confirmationUrl
26
+ }
27
+ userErrors {
28
+ field
29
+ message
30
+ }
31
+ }
32
+ }
33
+ `;
34
+ var APP_USAGE_RECORD_CREATE_MUTATION = `
35
+ mutation AppUsageRecordCreate($subscriptionLineItemId: ID!, $price: MoneyInput!, $description: String!) {
36
+ appUsageRecordCreate(subscriptionLineItemId: $subscriptionLineItemId, price: $price, description: $description) {
37
+ appUsageRecord {
38
+ id
39
+ }
40
+ userErrors {
41
+ field
42
+ message
43
+ }
44
+ }
45
+ }
46
+ `;
47
+
48
+ // src/billing/service.ts
49
+ function mapLineItemToShopifyInput(item) {
50
+ if (item.type === "recurring") {
51
+ return {
52
+ plan: {
53
+ appRecurringPricingDetails: {
54
+ price: {
55
+ amount: item.price.amount,
56
+ currencyCode: item.price.currencyCode
57
+ },
58
+ interval: item.interval
59
+ }
60
+ }
61
+ };
62
+ }
63
+ return {
64
+ plan: {
65
+ appUsagePricingDetails: {
66
+ cappedAmount: {
67
+ amount: item.cappedAmount.amount,
68
+ currencyCode: item.cappedAmount.currencyCode
69
+ },
70
+ terms: item.terms
71
+ }
72
+ }
73
+ };
74
+ }
75
+ function createShopifyBillingService(config) {
76
+ const { graphqlClient, billingService } = config;
77
+ return {
78
+ async createSubscription(input) {
79
+ const variables = {
80
+ name: input.planName,
81
+ lineItems: input.lineItems.map(mapLineItemToShopifyInput),
82
+ returnUrl: input.returnUrl
83
+ };
84
+ if (input.trialDays !== void 0) {
85
+ variables["trialDays"] = input.trialDays;
86
+ }
87
+ const response = await graphqlClient.mutate(
88
+ APP_SUBSCRIPTION_CREATE_MUTATION,
89
+ variables
90
+ );
91
+ if (response.errors?.length) {
92
+ throw new Error(
93
+ `GraphQL errors: ${response.errors.map((e) => e.message).join(", ")}`
94
+ );
95
+ }
96
+ const result = response.data.appSubscriptionCreate;
97
+ if (result.userErrors.length > 0) {
98
+ throw new Error(
99
+ `Shopify billing error: ${result.userErrors.map((e) => e.message).join(", ")}`
100
+ );
101
+ }
102
+ const appSubscription = result.appSubscription;
103
+ const shopifySubscriptionId = appSubscription.id;
104
+ const { confirmationUrl } = appSubscription;
105
+ const subscriptionInput = {
106
+ shopDomain: input.shopDomain,
107
+ planName: input.planName,
108
+ returnUrl: input.returnUrl,
109
+ shopifySubscriptionId,
110
+ confirmationUrl
111
+ };
112
+ if (input.trialDays !== void 0) {
113
+ subscriptionInput.trialDays = input.trialDays;
114
+ }
115
+ const subscription = await billingService.createSubscription(subscriptionInput);
116
+ return { subscription, confirmationUrl };
117
+ },
118
+ async createUsageRecord(input) {
119
+ const response = await graphqlClient.mutate(
120
+ APP_USAGE_RECORD_CREATE_MUTATION,
121
+ {
122
+ subscriptionLineItemId: input.subscriptionLineItemId,
123
+ price: {
124
+ amount: input.amount,
125
+ currencyCode: input.currencyCode
126
+ },
127
+ description: input.description
128
+ }
129
+ );
130
+ if (response.errors?.length) {
131
+ throw new Error(
132
+ `GraphQL errors: ${response.errors.map((e) => e.message).join(", ")}`
133
+ );
134
+ }
135
+ const result = response.data.appUsageRecordCreate;
136
+ if (result.userErrors.length > 0) {
137
+ throw new Error(
138
+ `Shopify usage record error: ${result.userErrors.map((e) => e.message).join(", ")}`
139
+ );
140
+ }
141
+ const usageRecordInput = {
142
+ subscriptionId: input.subscriptionId,
143
+ description: input.description,
144
+ amount: input.amount,
145
+ currencyCode: input.currencyCode
146
+ };
147
+ if (input.idempotencyKey !== void 0) {
148
+ usageRecordInput.idempotencyKey = input.idempotencyKey;
149
+ }
150
+ return billingService.createUsageRecord(usageRecordInput);
151
+ },
152
+ getSubscription(id) {
153
+ return billingService.getSubscription(id);
154
+ },
155
+ getActiveSubscription(shopDomain) {
156
+ return billingService.getActiveSubscription(shopDomain);
157
+ },
158
+ cancelSubscription(id) {
159
+ return billingService.cancelSubscription(id);
160
+ }
161
+ };
162
+ }
163
+
164
+ // src/billing/webhook-handler.ts
165
+ function createSubscriptionWebhookHandler(billingService) {
166
+ return {
167
+ async handle(payload) {
168
+ try {
169
+ const data = payload.payload;
170
+ const subscription = data.app_subscription;
171
+ if (!subscription?.admin_graphql_api_id || !subscription?.status) {
172
+ return { success: false, error: "Invalid webhook payload: missing subscription data" };
173
+ }
174
+ const shopifyId = subscription.admin_graphql_api_id;
175
+ const status = subscription.status;
176
+ const updated = await billingService.updateSubscriptionByShopifyId(shopifyId, { status });
177
+ if (!updated) {
178
+ return { success: false, error: `Subscription not found for Shopify ID: ${shopifyId}` };
179
+ }
180
+ return { success: true };
181
+ } catch (error) {
182
+ return {
183
+ success: false,
184
+ error: error instanceof Error ? error.message : String(error)
185
+ };
186
+ }
187
+ }
188
+ };
189
+ }
190
+ export {
191
+ APP_SUBSCRIPTION_CREATE_MUTATION,
192
+ APP_USAGE_RECORD_CREATE_MUTATION,
193
+ createShopifyBillingService,
194
+ createSubscriptionWebhookHandler
195
+ };
196
+ //# sourceMappingURL=index.mjs.map