diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..733169b --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +# TEMPORARY FIX: suppress sending tokenId to auth header +yarn patch-assembly-node-sdk + +yarn drizzle-kit migrate +next build \ No newline at end of file diff --git a/lib-patches/assembly-js-node-sdk.js b/lib-patches/assembly-js-node-sdk.js new file mode 100644 index 0000000..79e1650 --- /dev/null +++ b/lib-patches/assembly-js-node-sdk.js @@ -0,0 +1,141 @@ +import { DefaultService as Assembly, OpenAPI } from '../codegen/api' +export { OpenAPI } +import { request as __request } from '../codegen/api/core/request' +import { decryptAES128BitToken, generate128BitKey } from '../utils/crypto' +// SDK version for tracking compatibility +// TODO: Restore dynamic version reading after fixing build +let SDK_VERSION = '3.19.1' +const sdk = Assembly +// Helper functions to check env vars at runtime (supports both new and old names) +function getIsDebug() { + let _a + return !!((_a = process.env.ASSEMBLY_DEBUG) !== null && _a !== void 0 + ? _a + : process.env.COPILOT_DEBUG) +} +function getEnvMode() { + let _a + return (_a = process.env.ASSEMBLY_ENV) !== null && _a !== void 0 + ? _a + : process.env.COPILOT_ENV +} +// Exported for testing purposes only +export function processToken(token) { + try { + const json = JSON.parse(token) + // workspaceId is the only required field + if (!('workspaceId' in json)) { + throw new Error('Missing required field in token payload: workspaceId') + } + // Note: We intentionally do NOT validate that all keys are from a known list. + // This allows the backend to add new fields (like tokenId, expiresAt) without + // breaking older SDK versions. Unknown fields are simply ignored. + const areAllValuesValid = Object.values(json).every( + (val) => typeof val === 'string', + ) + if (!areAllValuesValid) { + throw new Error('Invalid values in token payload.') + } + const result = { + companyId: json.companyId, + clientId: json.clientId, + internalUserId: json.internalUserId, + workspaceId: json.workspaceId, + notificationId: json.notificationId, + baseUrl: json.baseUrl, + tokenId: json.tokenId, + } + return result + } catch (e) { + if (getIsDebug()) { + console.error(e) + } + return null + } +} +// Primary function (new name) +export function assemblyApi({ apiKey, token: tokenString }) { + const isDebug = getIsDebug() + const envMode = getEnvMode() + let key = ['local', '__SECRET_STAGING__'].includes( + envMode !== null && envMode !== void 0 ? envMode : '', + ) + ? apiKey + : undefined + if (isDebug) { + console.log('Debugging the assemblyApi init script.') + console.log({ env: envMode }) + } + if (tokenString) { + if (isDebug) { + console.log({ tokenString, apiKey }) + } + try { + const decipherKey = generate128BitKey(apiKey) + const decryptedPayload = decryptAES128BitToken(decipherKey, tokenString) + if (isDebug) { + console.log('Decrypted Payload:', decryptedPayload) + } + const payload = processToken(decryptedPayload) + if (!payload) { + throw new Error('Invalid token payload.') + } + if (isDebug) { + console.log('Payload:', payload) + } + if (payload.baseUrl) { + OpenAPI.BASE = payload.baseUrl + } + sdk.getTokenPayload = () => new Promise((resolve) => resolve(payload)) + // Build the key: workspaceId/apiKey or workspaceId/apiKey/tokenId if tokenId is present + key = payload.tokenId + ? `${payload.workspaceId}/${apiKey}/${payload.tokenId}` + : `${payload.workspaceId}/${apiKey}` + } catch (error) { + console.error(error) + } + } + if (!key) { + console.warn( + 'We were unable to authorize the SDK. If you are working in a local development environment, set the ASSEMBLY_ENV environment variable to "local" (COPILOT_ENV also works).', + ) + throw new Error('Unable to authorize Assembly SDK.') + } + + // TEMPORARY FIX: suppress sending tokenId to auth header if ASSEMBLY_SUPPRESS_TOKEN_ID is set + const suppressTokenId = !!process.env.ASSEMBLY_SUPPRESS_TOKEN_ID + + if (suppressTokenId) { + const [org, project] = key.split('/') + + if (!org || !project) { + throw new Error(`Invalid auth header`) + } + + key = `${org}/${project}` + // disable SDK version to prevent expiry logic from triggering (?) + SDK_VERSION = undefined + } + + if (isDebug) { + console.log(`Authorizing with key: ${key}`) + } + + OpenAPI.HEADERS = { + 'X-API-Key': key, + 'X-Assembly-SDK-Version': SDK_VERSION, + } + sdk.sendWebhook = (event, payload) => { + return __request(OpenAPI, { + method: 'POST', + url: '/v1/webhooks/{event}', + path: { event }, + body: payload, + mediaType: 'application/json', + }) + } + return sdk +} +/** @deprecated Use `assemblyApi` instead. Will be removed in v5.0.0. */ +export const copilotApi = assemblyApi +//# sourceMappingURL=init.js.map diff --git a/package.json b/package.json index 087a574..2eea42f 100644 --- a/package.json +++ b/package.json @@ -16,15 +16,16 @@ "lint-staged": "npx lint-staged", "prepare": "husky", "supabase:dev": "supabase start --ignore-health-check", - "cmd:rename-qb-accounts": "tsx src/cmd/renameQbAccount/index.ts" + "cmd:rename-qb-accounts": "tsx src/cmd/renameQbAccount/index.ts", + "patch-assembly-node-sdk": "cp ./lib-patches/assembly-js-node-sdk.js ./node_modules/@assembly-js/node-sdk/dist/api/init.js" }, "dependencies": { + "@assembly-js/node-sdk": "^3.19.1", "@sentry/nextjs": "^9.13.0", "@supabase/supabase-js": "^2.49.4", "@trigger.dev/sdk": "4.3.0", "bottleneck": "^2.19.5", "copilot-design-system": "^2.0.10", - "copilot-node-sdk": "^3.16.0", "dayjs": "^1.11.13", "deep-equal": "^2.2.3", "drizzle-orm": "^0.42.0", @@ -88,4 +89,4 @@ "yarn prettier:fix" ] } -} +} \ No newline at end of file diff --git a/src/utils/copilotAPI.ts b/src/utils/copilotAPI.ts index d2b1078..e1c8393 100644 --- a/src/utils/copilotAPI.ts +++ b/src/utils/copilotAPI.ts @@ -44,18 +44,18 @@ import { WorkspaceResponseSchema, } from '@/type/common' import Bottleneck from 'bottleneck' -import type { CopilotAPI as SDK } from 'copilot-node-sdk' -import { copilotApi } from 'copilot-node-sdk' +import type { AssemblyAPI as SDK } from '@assembly-js/node-sdk' +import { assemblyApi } from '@assembly-js/node-sdk' import { z } from 'zod' import { API_DOMAIN } from '@/constant/domains' import httpStatus from 'http-status' import { MAX_INVOICE_LIST_LIMIT } from '@/app/api/core/constants/limit' export class CopilotAPI { - copilot: SDK + assemblyPromise: SDK constructor(private token: string) { - this.copilot = copilotApi({ apiKey, token }) + this.assemblyPromise = assemblyApi({ apiKey, token }) } private async manualFetch( @@ -88,7 +88,8 @@ export class CopilotAPI { // Get Token Payload from copilot request token async _getTokenPayload(): Promise { - const getTokenPayload = this.copilot.getTokenPayload + const assembly = await this.assemblyPromise + const getTokenPayload = assembly.getTokenPayload if (!getTokenPayload) { console.error( `CopilotAPI#getTokenPayload | Could not parse token payload for token ${this.token}`, @@ -100,22 +101,25 @@ export class CopilotAPI { } async _me(): Promise { + const assembly = await this.assemblyPromise + console.info('CopilotAPI#me | token =', this.token) const tokenPayload = await this.getTokenPayload() const id = tokenPayload?.internalUserId || tokenPayload?.clientId if (!tokenPayload || !id) return null const retrieveCurrentUserInfo = tokenPayload.internalUserId - ? this.copilot.retrieveInternalUser - : this.copilot.retrieveClient + ? assembly.retrieveInternalUser + : assembly.retrieveClient const currentUserInfo = await retrieveCurrentUserInfo({ id }) return MeResponseSchema.parse(currentUserInfo) } async _getWorkspace(): Promise { + const assembly = await this.assemblyPromise console.info('CopilotAPI#getWorkspace | token =', this.token) - return WorkspaceResponseSchema.parse(await this.copilot.retrieveWorkspace()) + return WorkspaceResponseSchema.parse(await assembly.retrieveWorkspace()) } async _getClientTokenPayload(): Promise { @@ -138,9 +142,11 @@ export class CopilotAPI { requestBody: ClientRequest, sendInvite: boolean = false, ): Promise { + const assembly = await this.assemblyPromise + console.info('CopilotAPI#createClient | token =', this.token) return ClientResponseSchema.parse( - await this.copilot.createClient({ sendInvite, requestBody }), + await assembly.createClient({ sendInvite, requestBody }), ) } @@ -149,11 +155,10 @@ export class CopilotAPI { * Error handling: if copilot throws NOT FOUND error or BAD REQUEST error, return undefined. This is done as we don't want to terminate the process */ async _getClient(id: string): Promise { + const assembly = await this.assemblyPromise try { console.info('CopilotAPI#getClient | token =', this.token) - return ClientResponseSchema.parse( - await this.copilot.retrieveClient({ id }), - ) + return ClientResponseSchema.parse(await assembly.retrieveClient({ id })) } catch (error: unknown) { if ( typeof error === 'object' && @@ -180,9 +185,10 @@ export class CopilotAPI { * Error handling: if copilot throws NOT FOUND error or BAD REQUEST error, return undefined. This is done as we don't want to terminate the process */ async _getClients(args: CopilotListArgs & { companyId?: string } = {}) { + const assembly = await this.assemblyPromise try { console.info('CopilotAPI#getClients | token =', this.token) - return ClientsResponseSchema.parse(await this.copilot.listClients(args)) + return ClientsResponseSchema.parse(await assembly.listClients(args)) } catch (error: unknown) { if ( typeof error === 'object' && @@ -208,21 +214,25 @@ export class CopilotAPI { id: string, requestBody: ClientRequest, ): Promise { + const assembly = await this.assemblyPromise console.info('CopilotAPI#updateClient | token =', this.token) return ClientResponseSchema.parse( - await this.copilot.updateClient({ id, requestBody }), + await assembly.updateClient({ id, requestBody }), ) } async _deleteClient(id: string) { + const assembly = await this.assemblyPromise console.info('CopilotAPI#deleteClient | token =', this.token) - return await this.copilot.deleteClient({ id }) + return await assembly.deleteClient({ id }) } async _createCompany(requestBody: CompanyCreateRequest) { + const assembly = await this.assemblyPromise + console.info('CopilotAPI#createCompany | token =', this.token) return CompanyResponseSchema.parse( - await this.copilot.createCompany({ requestBody }), + await assembly.createCompany({ requestBody }), ) } @@ -231,11 +241,11 @@ export class CopilotAPI { * Error handling: if copilot throws NOT FOUND error or BAD REQUEST error, return undefined. This is done as we don't want to terminate the process */ async _getCompany(id: string): Promise { + const assembly = await this.assemblyPromise + try { console.info('CopilotAPI#getCompany | token =', this.token) - return CompanyResponseSchema.parse( - await this.copilot.retrieveCompany({ id }), - ) + return CompanyResponseSchema.parse(await assembly.retrieveCompany({ id })) } catch (error: unknown) { if ( typeof error === 'object' && @@ -258,8 +268,10 @@ export class CopilotAPI { } async _getCompanies(args: CopilotListArgs = {}): Promise { + const assembly = await this.assemblyPromise + console.info('CopilotAPI#getCompanies | token =', this.token) - return CompaniesResponseSchema.parse(await this.copilot.listCompanies(args)) + return CompaniesResponseSchema.parse(await assembly.listCompanies(args)) } async _getCompanyClients(companyId: string): Promise { @@ -268,43 +280,48 @@ export class CopilotAPI { } async _getCustomFields(): Promise { + const assembly = await this.assemblyPromise + console.info('CopilotAPI#getCustomFields | token =', this.token) - return CustomFieldResponseSchema.parse( - await this.copilot.listCustomFields({}), - ) + return CustomFieldResponseSchema.parse(await assembly.listCustomFields({})) } async _getInternalUsers( args: CopilotListArgs = {}, ): Promise { + const assembly = await this.assemblyPromise + console.info('CopilotAPI#getInternalUsers | token =', this.token) return InternalUsersResponseSchema.parse( - await this.copilot.listInternalUsers(args), + await assembly.listInternalUsers(args), ) } async _getInternalUser(id: string): Promise { + const assembly = await this.assemblyPromise console.info('CopilotAPI#getInternalUser | token =', this.token) return InternalUsersSchema.parse( - await this.copilot.retrieveInternalUser({ id }), + await assembly.retrieveInternalUser({ id }), ) } async _createNotification( requestBody: NotificationRequestBody, ): Promise { + const assembly = await this.assemblyPromise console.info('CopilotAPI#createNotification | token =', this.token) console.info('CopilotAPI#createNotification | requestBody =', requestBody) return NotificationCreatedResponseSchema.parse( - await this.copilot.createNotification({ + await assembly.createNotification({ requestBody, }), ) } async _markNotificationAsRead(id: string): Promise { + const assembly = await this.assemblyPromise console.info('CopilotAPI#markNotificationAsRead | token =', this.token) - await this.copilot.markNotificationRead({ id }) + await assembly.markNotificationRead({ id }) } async _bulkMarkNotificationsAsRead(notificationIds: string[]): Promise { @@ -331,8 +348,10 @@ export class CopilotAPI { } async _deleteNotification(id: string): Promise { + const assembly = await this.assemblyPromise + console.info('CopilotAPI#deleteNotification | token =', this.token) - await this.copilot.deleteNotification({ id }) + await assembly.deleteNotification({ id }) } async _bulkDeleteNotifications(notificationIds: string[]): Promise { @@ -379,10 +398,10 @@ export class CopilotAPI { } async _getProduct(id: string): Promise { + const assembly = await this.assemblyPromise + console.info('CopilotAPI#getProduct | token =', this.token) - return ProductResponseSchema.parse( - await this.copilot.retrieveProduct({ id }), - ) + return ProductResponseSchema.parse(await assembly.retrieveProduct({ id })) } async _getProducts( @@ -390,15 +409,18 @@ export class CopilotAPI { nextToken?: string, limit?: number, ): Promise { + const assembly = await this.assemblyPromise + console.info('CopilotAPI#getProducts | token =', this.token) return ProductsResponseSchema.parse( - await this.copilot.listProducts({ name, nextToken, limit }), + await assembly.listProducts({ name, nextToken, limit }), ) } async _getPrice(id: string): Promise { + const assembly = await this.assemblyPromise console.info('CopilotAPI#getPrice | token =', this.token) - return PriceResponseSchema.parse(await this.copilot.retrievePrice({ id })) + return PriceResponseSchema.parse(await assembly.retrievePrice({ id })) } async _getPrices( @@ -406,17 +428,19 @@ export class CopilotAPI { nextToken?: string, limit?: string, ): Promise { + const assembly = await this.assemblyPromise + console.info('CopilotAPI#getPrices | token =', this.token) return PricesResponseSchema.parse( - await this.copilot.listPrices({ productId, nextToken, limit }), + await assembly.listPrices({ productId, nextToken, limit }), ) } async _getInvoice(id: string): Promise { + const assembly = await this.assemblyPromise + console.info('CopilotAPI#getInvoice | token =', this.token) - return InvoiceResponseSchema.parse( - await this.copilot.retrieveInvoice({ id }), - ) + return InvoiceResponseSchema.parse(await assembly.retrieveInvoice({ id })) } async _getInvoices( @@ -436,9 +460,11 @@ export class CopilotAPI { } async _getPayments(invoiceId: string): Promise { + const assembly = await this.assemblyPromise + console.info('CopilotAPI#getPayments | token =', this.token) return PaymentsResponseSchema.parse( - await this.copilot.listPayments({ invoiceId }), + await assembly.listPayments({ invoiceId }), ) } diff --git a/yarn.lock b/yarn.lock index a4aa734..0b7c62c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,6 +29,16 @@ __metadata: languageName: node linkType: hard +"@assembly-js/node-sdk@npm:^3.19.1": + version: 3.19.1 + resolution: "@assembly-js/node-sdk@npm:3.19.1" + dependencies: + isomorphic-fetch: "npm:^3.0.0" + next: "npm:^14.0.2" + checksum: 10c0/8385b54c6426f7867db8c308c96f5e535aeab59ff1150fc96809d080a1004792920d0390e8a7d26775e5480a8a10f2b7f7db1d29c1e42267a9a03e12e8418785 + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.27.1": version: 7.27.1 resolution: "@babel/code-frame@npm:7.27.1" @@ -5200,16 +5210,6 @@ __metadata: languageName: node linkType: hard -"copilot-node-sdk@npm:^3.16.0": - version: 3.17.1 - resolution: "copilot-node-sdk@npm:3.17.1" - dependencies: - isomorphic-fetch: "npm:^3.0.0" - next: "npm:^14.0.2" - checksum: 10c0/976739b359a36402e249e5d3b7182c0271eb3d0c6d8479ca81b8f733b263fe1edf14fffff82e89df8679de4ecc73c58478c4006de9ab99fd8f4e642a7c343c80 - languageName: node - linkType: hard - "copy-anything@npm:^4": version: 4.0.5 resolution: "copy-anything@npm:4.0.5" @@ -5220,12 +5220,12 @@ __metadata: linkType: hard "cors@npm:~2.8.5": - version: 2.8.5 - resolution: "cors@npm:2.8.5" + version: 2.8.6 + resolution: "cors@npm:2.8.6" dependencies: object-assign: "npm:^4" vary: "npm:^1" - checksum: 10c0/373702b7999409922da80de4a61938aabba6929aea5b6fd9096fefb9e8342f626c0ebd7507b0e8b0b311380744cc985f27edebc0a26e0ddb784b54e1085de761 + checksum: 10c0/ab2bc57b8af8ef8476682a59647f7c55c1a7d406b559ac06119aa1c5f70b96d35036864d197b24cf86e228e4547231088f1f94ca05061dbb14d89cc0bc9d4cab languageName: node linkType: hard @@ -5313,6 +5313,7 @@ __metadata: version: 0.0.0-use.local resolution: "custom-app-base@workspace:." dependencies: + "@assembly-js/node-sdk": "npm:^3.19.1" "@eslint/eslintrc": "npm:^3.3.1" "@eslint/js": "npm:^9.24.0" "@ngrok/ngrok": "npm:^1.4.1" @@ -5331,7 +5332,6 @@ __metadata: autoprefixer: "npm:^10.4.0" bottleneck: "npm:^2.19.5" copilot-design-system: "npm:^2.0.10" - copilot-node-sdk: "npm:^3.16.0" dayjs: "npm:^1.11.13" deep-equal: "npm:^2.2.3" dotenv: "npm:^16.4.5"