diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..c7dee4e4
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,160 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What this app is
+
+A multi-tenant Next.js (App Router) service that synchronizes Copilot / Assembly workspaces with QuickBooks Online (QBO). It runs on Vercel, persists state in Postgres (Supabase in prod, Drizzle ORM throughout), and reacts to Copilot webhooks (`invoice.created/updated/paid/voided/deleted`, `product.updated`, `price.created`, `payment.succeeded`) by mirroring those entities into the corresponding QBO realm.
+
+A "portal" is one Copilot/Assembly workspace bonded to one QuickBooks realm. Almost every table is keyed by `portalId`; almost every service derives `this.user.workspaceId` from the request token and scopes everything to that portal.
+
+## Common commands
+
+Package manager is **Yarn 4 (Berry)**, Node **22.14.0** (`.nvmrc`).
+
+```bash
+yarn install # install
+yarn dev # Next dev (Turbopack)
+yarn build # next build (CI uses build.sh which also runs drizzle-kit migrate)
+yarn lint:check # ESLint over src/ and test/
+yarn prettier:check # Prettier check
+yarn lint:fix # ESLint --fix
+yarn prettier:fix # Prettier write
+
+# Tests (Vitest, two projects defined in vitest.config.ts)
+yarn test # both: unit then integration (groupOrder enforces this)
+yarn test:watch # watch
+yarn test:coverage # v8 coverage
+npx vitest run --project unit # only unit
+npx vitest run --project integration # only integration
+npx vitest run test/integration/quickbooks/priceCreated/happyPath.test.ts # single file
+npx vitest run -t 'happy path' # by test-name pattern
+
+# Trigger.dev (background task runtime)
+yarn trigger:dev # local dev worker
+yarn trigger:deploy # deploy tasks
+
+# DB migrations (Drizzle Kit, schema lives at src/db/schema/)
+npx drizzle-kit generate # create new migration from schema changes
+npx drizzle-kit migrate # apply pending migrations to DATABASE_URL
+
+# One-off operational scripts (tsx, see src/cmd/*)
+yarn cmd:rename-qb-accounts
+yarn cmd:backfill-product-info
+yarn cmd:sync-missed-invoices
+yarn cmd:sync-missed-products
+```
+
+Husky `pre-commit` runs `lint-staged` (eslint --fix + prettier --write on `src/**/*.{ts,tsx}`). CI (`.github/workflows/test.yml`) runs `yarn test` on PRs; `.github/workflows/lint.yml` runs lint+prettier on every push. CI assumes the testcontainers Postgres image is available (Docker is preinstalled on `ubuntu-latest`).
+
+## Architecture
+
+### Request → handler shape
+
+Every API route follows the same skeleton:
+
+```
+src/app/api///
+ route.ts # exports { POST/GET } = withErrorHandler(controllerFn); sets maxDuration
+ .controller.ts # auth + Sentry scope + parse + delegate to service
+ .service.ts # extends BaseService; orchestrates DB + external APIs
+```
+
+Controllers call `authenticate(req)` (`src/app/api/core/utils/authenticate.ts`), which reads `?token=…`, asks Copilot to decrypt it, and returns a `User` (`src/app/api/core/models/User.model.ts`). `User` carries `workspaceId` (= portalId), role, and the lazily-attached `qbConnection` (service-item / client-fee refs).
+
+`withErrorHandler` (`src/app/api/core/utils/withErrorHandler.ts`) is the **only** error path. It maps `ZodError` / `APIError` / `CopilotApiError` / `RetryableError` / Intuit OAuth + Axios errors to HTTP responses and forwards categorized exceptions to Sentry. Don't add try/catch in route handlers — throw and let this wrapper format.
+
+### BaseService and the DB singleton
+
+Services extend `BaseService` (`src/app/api/core/services/base.service.ts`), which holds:
+
+- `this.db` — the **module-level Drizzle singleton** from `src/db/index.ts` (`DBClient.getInstance()`); `casing: 'snake_case'`.
+- `this.user` — the authenticated `User` for the request.
+- `setTransaction(tx)` / `unsetTransaction()` — swap `this.db` for a transaction handle inside a `db.transaction(...)` callback, then restore.
+
+**Pitfall (known, see `memory/project_unsetTransaction_bug.md`):** `unsetTransaction()` is sometimes called inside the transaction callback or skipped on error paths — across `BaseService` subclasses this leaves the singleton pointed at a closed tx. When introducing or modifying transactional code, audit that `setTransaction` / `unsetTransaction` are paired in `try/finally` and that nested service calls share the tx handle.
+
+The DB singleton is also why test helpers (`test/helpers/seed.ts`, `test/helpers/testDb.ts`) import `@/db` directly — see `docs/why-test-helpers-use-the-app-db-singleton.md`. Don't introduce a separate test-only Drizzle client; tests must read what the app writes.
+
+### Webhook flow (the central path)
+
+`POST /api/quickbooks/webhook` → `WebhookService.handleWebhookEvent` (`src/app/api/quickbooks/webhook/webhook.service.ts`) is a switch on `payload.eventType` that dispatches to `InvoiceService` / `ProductService` / `PaymentService`. A few things to know before changing it:
+
+1. **Idempotency is enforced via `qb_sync_logs` claim rows.** `SyncLogService.claimWebhookEvent({ copilotId, entityType, eventType, … })` returns `{ claimed: false }` if a row already exists; handlers exit early. Any new webhook handler must call `claimWebhookEvent` before doing real work or duplicate processing will leak into QBO.
+2. **`qb_sync_logs.quickbooks_id` is polymorphic.** Its meaning depends on `(entityType, eventType)` — for `INVOICE/PAID` it stores the QBO **Payment** ID, not the Invoice ID. See `memory/project_qb_sync_logs_semantics.md`.
+3. **Pre-claim sleeps for ordering.** `INVOICE_UPDATED` / `INVOICE_VOIDED` / `PAYMENT_SUCCEEDED` sleep before `claimWebhookEvent` so a companion event (e.g., `INVOICE_CREATED`) can claim first. The `delayMs` lives in the handler, not the caller — keep it that way; moving the sleep after the claim re-opens the race.
+4. **Setting flags gate handlers.** `PRICE_CREATED` / `PRODUCT_UPDATED` no-op when `createNewProductFlag` is false; `PAYMENT_SUCCEEDED` no-ops when `absorbedFeeFlag` is false or there's no platform-paid fee. Read `qb_settings` via `SettingService` rather than passing flags around.
+5. **There's a known TOCTOU race on `claimWebhookEvent`** — accepted, parked, will be addressed with an advisory lock + dedupe job, not a rewrite. See `memory/project_qb_sync_logs_toctou_parked.md`.
+
+### Token refresh
+
+QBO access tokens expire in ~1h, refresh tokens in ~100 days. `src/utils/intuitAPI.ts` sends authenticated requests; `src/utils/tokenRefresh.ts` (`getValidQbTokens`) refreshes when stale. The `vercel.json` cron `/api/quickbooks/refresh-tokens` runs daily at 06:00 UTC to keep refresh tokens warm. There's a known silent-401 bug — expired tokens cause `null` returns from `getFetchWithHeader/postFetchWithHeaders`; the planned fix is auto-refresh inside those helpers (design at `docs/intuit-api-token-refresh.md`, summary in `memory/project_intuit_api_token_refresh.md`).
+
+### Background work
+
+- **Vercel crons** (`vercel.json`):
+ - `/api/quickbooks/cron` every 12h — kicks off `processResyncForFailedRecords` (Trigger.dev task) to retry failed sync logs. Auth via `Bearer ${CRON_SECRET}`.
+ - `/api/quickbooks/refresh-tokens` daily 06:00 UTC.
+- **Trigger.dev** tasks live in `src/trigger/` (config at `trigger.config.ts`, runtime: node, default 3 retries, `maxDuration: 3600s`). Sentry source maps are uploaded only when `VERCEL_ENV === 'production'`.
+
+## Multi-tenancy invariant
+
+Every `WHERE` clause that touches a portal-scoped table needs `portalId = this.user.workspaceId`. Forgetting this leaks one tenant's data into another. The unique indexes on `qb_sync_logs` and `qb_invoice_sync` (see migrations 20260427100328 / 20260427055352) enforce some of this at the DB level, but most of it is service-layer discipline.
+
+## Database & schema
+
+- Drizzle schemas in `src/db/schema/*` registered in `src/db/schema/index.ts`. Relations in `relation.ts`.
+- Migrations in `src/db/migrations/` (prefix `supabase`, generated by drizzle-kit). The `init.sql` (20250701) defines all enums; subsequent files alter.
+- Custom column helpers in `src/db/helper/column.helper.ts` (`timestamps`) and enum bridge in `drizzle.helper.ts` (`enumToPgEnum`).
+- `qb_payments` table exists but is currently unused (reserved for future) — no rows in prod. See `memory/project_qb_payments_unused.md`.
+- Type-safe Zod schemas come from `drizzle-zod` (`createInsertSchema` / `createSelectSchema`); reuse those rather than hand-rolling Zod for DB rows.
+
+## Testing
+
+- Two Vitest **projects** in `vitest.config.ts` — `unit` (mock-heavy, isolated) and `integration` (real Postgres via testcontainers). Run order is enforced via `sequence.groupOrder` (unit=0, integration=1).
+- Integration project is configured **`pool: 'forks'` + `fileParallelism: false` + `isolate: false`** so all integration tests share one Postgres container _and_ one app DB connection. Don't change these without reading `docs/vitest-gotchas.md` and `docs/why-test-helpers-use-the-app-db-singleton.md`.
+- `.env.test` is loaded by `test/integration/globalSetup.ts` with `override: true` so a developer's local `.env` can't leak into tests. `DATABASE_URL` is intentionally **not** in `.env.test` — globalSetup sets it from the container's URI before any worker imports `src/config`.
+- Module mocks for integration are in `test/integration/setup.ts` — `@/utils/copilotAPI`, `@/utils/intuitAPI`, and `@sentry/nextjs` must be mocked with **explicit factories** (and Intuit/Copilot mock implementations must use `function`, not `=>`, because the code does `new IntuitAPI(...)`). See `docs/vitest-gotchas.md` items 1–3.
+- Test helpers in `test/helpers/`: `seed.ts` (`seedHealthyPortal`, `TEST_PORTAL_ID`, etc.), `webhook.ts` (`postWebhook` via `next-test-api-route-handler`), `testDb.ts` (`truncateAllTestTables`).
+- Test-data philosophy in `docs/test-data-dos-and-donts.md`: static fixtures for the thing under test, factories with explicit overrides for single-dimension variants, **no faker** in fixtures or assertions.
+
+## Path aliases
+
+```
+@/* → src/*
+@test/* → test/*
+```
+
+Configured in `tsconfig.json` and propagated to Vitest via `vite-tsconfig-paths` (per-project in `vitest.config.ts`).
+
+## Style notes
+
+- Prettier: single quotes, no semis, trailing comma all (`.prettierrc`).
+- ESLint: `next/core-web-vitals` + TypeScript; `prefer-const` and `no-var` are errors; unused-var underscore prefix is exempt; `@typescript-eslint/no-explicit-any` is disabled (the codebase uses `any` deliberately at framework boundaries).
+- Tailwind v4 + `copilot-design-system`. UI surface is small (settings dashboard + OAuth callback) — most work happens in the API/service layer.
+- The `docs/` folder is **gitignored** (per `.gitignore`) and used for local decision notes — design docs, post-mortems, comparison tables. Save non-trivial tradeoff discussions there rather than in code comments or commit messages.
+
+## Things to read before non-trivial changes
+
+- `docs/testcontainers-vs-local-supabase.md` — why integration tests use testcontainers, not the local Supabase stack.
+- `docs/why-test-helpers-use-the-app-db-singleton.md` — why test helpers import `@/db` and what would break if you opened a separate client.
+- `docs/vitest-gotchas.md` — the five real traps already hit in this project.
+- `docs/test-data-dos-and-donts.md` — the test-data rules.
+- `docs/intuit-api-token-refresh.md` — design for the silent-401 fix.
+
+## What this repo doesn't have
+
+- No design system / shared component library — UI is a thin dashboard, mostly settings forms.
+- No GraphQL, no tRPC — plain Next.js Route Handlers + service classes.
+- No DI container — `BaseService` reads `db` from a module singleton; tests work _with_ that constraint, not around it.
+- No existing CLAUDE.md until this one.
+
+## Code quality
+
+- Do not use let unless absolutely necessary. Use const instead.
+- Always keep the comments short, on point and easy to understand with easy wordings. This is must.
+- Follow DRY, KISS, SOLID, YAGNI principles.
+
+## Note
+
+- After a successful implementation, the changes will be reviewed by the team lead and greptileAI in github.
\ No newline at end of file
diff --git a/lib-patches/assembly-js-node-sdk.js b/lib-patches/assembly-js-node-sdk.js
deleted file mode 100644
index ad80e204..00000000
--- a/lib-patches/assembly-js-node-sdk.js
+++ /dev/null
@@ -1,138 +0,0 @@
-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
- 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
- // TEMPORARY FIX end
-
- 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/lib-patches/copilot-node-sdk.js b/lib-patches/copilot-node-sdk.js
deleted file mode 100644
index 25b22dbd..00000000
--- a/lib-patches/copilot-node-sdk.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import { DefaultService as Copilot, OpenAPI } from '../codegen/api'
-import { decryptAES128BitToken, generate128BitKey } from '../utils/crypto'
-const sdk = Copilot
-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')
- }
- const areAllKeysValid = Object.keys(json).every((key) =>
- [
- 'workspaceId',
- 'companyId',
- 'clientId',
- 'internalUserId',
- 'notificationId',
- // patched below keys for SDK to work and not throw error
- 'baseUrl',
- 'instanceId',
- ].includes(key),
- )
- if (!areAllKeysValid) {
- throw new Error('Invalid keys in token payload.')
- }
- 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,
- }
- return result
- } catch (e) {
- if (process.env.COPILOT_DEBUG) {
- console.error(e)
- }
- return null
- }
-}
-export function copilotApi({ apiKey, token: tokenString }) {
- var _a
- let key = ['local', '__SECRET_STAGING__'].includes(
- (_a = process.env.COPILOT_ENV) !== null && _a !== void 0 ? _a : '',
- )
- ? apiKey
- : undefined
- if (process.env.COPILOT_DEBUG) {
- console.log('Debugging the copilotApi init script.')
- console.log({ env: process.env.COPILOT_ENV })
- }
- if (tokenString) {
- if (process.env.COPILOT_DEBUG) {
- console.log({ tokenString, apiKey })
- }
- try {
- const decipherKey = generate128BitKey(apiKey)
- const decryptedPayload = decryptAES128BitToken(decipherKey, tokenString)
- if (process.env.COPILOT_DEBUG) {
- console.log('Decrypted Payload:', decryptedPayload)
- }
- const payload = processToken(decryptedPayload)
- if (!payload) {
- throw new Error('Invalid token payload.')
- }
- if (process.env.COPILOT_DEBUG) {
- console.log('Payload:', payload)
- }
- sdk.getTokenPayload = () => new Promise((resolve) => resolve(payload))
- key = `${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 COPILOT_ENV environment variable to "local".',
- )
- throw new Error('Unable to authorize Copilot SDK.')
- }
- if (process.env.COPILOT_DEBUG) {
- console.log(`Authorizing with key: ${key}`)
- }
- OpenAPI.HEADERS = {
- 'X-API-Key': key,
- }
- return sdk
-}
-//# sourceMappingURL=init.js.map
diff --git a/package.json b/package.json
index 21f3db10..0fa49708 100644
--- a/package.json
+++ b/package.json
@@ -18,8 +18,6 @@
"prepare": "husky",
"supabase:dev": "supabase start --ignore-health-check",
"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",
- "patch-copilot-node-sdk": "cp ./lib-patches/copilot-node-sdk.js ./node_modules/copilot-node-sdk/dist/api/init.js",
"cmd:backfill-product-info": "tsx src/cmd/backfillProductInfo/index.ts",
"cmd:sync-missed-invoices": "tsx src/cmd/syncMissedInvoices/index.ts",
"cmd:sync-missed-products": "tsx src/cmd/syncMissedProducts/index.ts",
@@ -28,12 +26,12 @@
"test:coverage": "vitest run --coverage"
},
"dependencies": {
+ "@assembly-js/node-sdk": "^4.2.2",
"@sentry/nextjs": "^9.13.0",
"@supabase/supabase-js": "^2.49.4",
"@trigger.dev/sdk": "4.4.4",
"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",
@@ -103,4 +101,4 @@
"prettier --write"
]
}
-}
\ No newline at end of file
+}
diff --git a/scripts/build.sh b/scripts/build.sh
index 599f2cc9..48db73e4 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -1,22 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
-# TEMPORARY FIX: suppress sending tokenId to auth header
-# yarn patch-assembly-node-sdk
-
echo "👷 Running build script for environment: ${VERCEL_ENV:-unknown}"
-if [ "${VERCEL_ENV:-}" != "production" ]; then
- echo "[1/3] Running copilot-node-sdk patch 🧑🏻🔧"
- yarn patch-copilot-node-sdk
-else
- echo "[1/3] Skipping copilot-node-sdk patch (production)"
-fi
+# The @assembly-js/node-sdk (v4) needs no patching — it is per-request scoped
+# and carries no client-side token-expiry logic, so no SDK file swap here.
-echo "[2/3] Running drizzle-kit migrate"
+echo "[1/2] Running drizzle-kit migrate"
yarn drizzle-kit migrate
-echo "[3/3] Running next build"
+echo "[2/2] Running next build"
next build
echo "🥳 Build completed! 🎉🎉"
diff --git a/src/action/copilot.action.ts b/src/action/copilot.action.ts
deleted file mode 100644
index d63efcf9..00000000
--- a/src/action/copilot.action.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Token } from '@/type/common'
-import { CopilotAPI } from '@/utils/copilotAPI'
-
-export async function getTokenPayload(token: string): Promise {
- const copilotClient = new CopilotAPI(token)
- const payload = await copilotClient.getTokenPayload()
- return payload as Token
-}
diff --git a/src/app/(home)/Home.tsx b/src/app/(home)/Home.tsx
index 65863af8..ab4ea09f 100644
--- a/src/app/(home)/Home.tsx
+++ b/src/app/(home)/Home.tsx
@@ -1,4 +1,3 @@
-import { getTokenPayload } from '@/action/copilot.action'
import {
checkPortalConnection,
reconnectIfCta,
@@ -9,6 +8,7 @@ import { SyncLogService } from '@/app/api/quickbooks/syncLog/syncLog.service'
import { AppProvider } from '@/app/context/AppContext'
import { SilentError } from '@/components/template/SilentError'
import { getWorkspaceInfo } from '@/db/service/token.service'
+import { getAssemblyTokenPayload } from '@/utils/assemblyTokenPayload'
import { z } from 'zod'
export default async function Main({
@@ -27,7 +27,7 @@ export default async function Main({
return
}
- const tokenPayload = await getTokenPayload(token)
+ const tokenPayload = await getAssemblyTokenPayload(token)
if (!tokenPayload) {
return
}
@@ -41,7 +41,7 @@ export default async function Main({
const [portalConnection, workspace, latestSuccessLog] = await Promise.all([
checkPortalConnection(tokenPayload.workspaceId),
- getWorkspaceInfo(token),
+ getWorkspaceInfo(tokenPayload.workspaceId),
syncLogService.getLatestSyncSuccessLog().catch((err) => {
console.error('Home#getLatestSyncSuccessLog | Error =', err)
return null
diff --git a/src/app/api/core/utils/authenticate.ts b/src/app/api/core/utils/authenticate.ts
index f08630ef..ed97615c 100644
--- a/src/app/api/core/utils/authenticate.ts
+++ b/src/app/api/core/utils/authenticate.ts
@@ -1,24 +1,22 @@
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { getAssemblyTokenPayload } from '@/utils/assemblyTokenPayload'
import { NextRequest } from 'next/server'
import User from '@/app/api/core/models/User.model'
import { z } from 'zod'
-import { TokenSchema } from '@/type/common'
import APIError from '@/app/api/core/exceptions/api'
import httpStatus from 'http-status'
import { withRetry } from '@/app/api/core/utils/withRetry'
export const _authenticateWithToken = async (token: string): Promise => {
- const copilotClient = new CopilotAPI(token)
- const payload = TokenSchema.safeParse(await copilotClient.getTokenPayload())
+ const tokenPayload = await getAssemblyTokenPayload(token)
- if (!payload.success) {
+ if (!tokenPayload) {
throw new APIError(httpStatus.UNAUTHORIZED, 'Failed to authenticate token')
}
// Access to IU and webhook events.
if (
- !payload.data.internalUserId &&
- (payload.data.clientId || payload.data.companyId)
+ !tokenPayload.internalUserId &&
+ (tokenPayload.clientId || tokenPayload.companyId)
) {
throw new APIError(
httpStatus.UNAUTHORIZED,
@@ -26,7 +24,7 @@ export const _authenticateWithToken = async (token: string): Promise => {
)
}
- return new User(token, payload.data)
+ return new User(token, tokenPayload)
}
export const authenticateWithToken = (...args: unknown[]) =>
withRetry(_authenticateWithToken, args)
@@ -35,7 +33,7 @@ export const authenticateWithToken = (...args: unknown[]) =>
* Token parser and authentication util
*
* `authenticate` takes in the current request, parses the "token" searchParam from it,
- * uses `CopilotAPI` to check if the user token is valid
+ * uses `getAssemblyTokenPayload` to check if the user token is valid
* and finally returns an instance of `User` that is associated with this request
*/
const authenticate = async (req: NextRequest) => {
diff --git a/src/app/api/core/utils/withErrorHandler.ts b/src/app/api/core/utils/withErrorHandler.ts
index 472262af..61bb6fe3 100644
--- a/src/app/api/core/utils/withErrorHandler.ts
+++ b/src/app/api/core/utils/withErrorHandler.ts
@@ -37,7 +37,7 @@ type RequestHandler = (req: NextRequest, params: any) => Promise
* });
*
* @throws {ZodError} Captures and handles validation errors and responds with status 400 and the issue detail.
- * @throws {CopilotApiError} Captures and handles CopilotAPI errors, uses the error status, and message if available.
+ * @throws {CopilotApiError} Captures and handles AssemblyAPI errors, uses the error status, and message if available.
* @throws {APIError} Captures and handles APIError
* @throws {AxiosError} Captures and handles AxiosError (Specially from Intuit SDK)
*/
diff --git a/src/app/api/core/utils/withRetry.ts b/src/app/api/core/utils/withRetry.ts
index 65ce290b..e91998d8 100644
--- a/src/app/api/core/utils/withRetry.ts
+++ b/src/app/api/core/utils/withRetry.ts
@@ -159,7 +159,7 @@ export const withRetry = async (
factor: 4,
onFailedAttempt: (error: FailedAttemptError) => {
console.warn(
- `CopilotAPI#withRetry | Attempt ${error.attemptNumber} failed. There are ${error.retriesLeft} retries left. Error:`,
+ `AssemblyAPI#withRetry | Attempt ${error.attemptNumber} failed. There are ${error.retriesLeft} retries left. Error:`,
error,
)
},
diff --git a/src/app/api/notification/notification.service.ts b/src/app/api/notification/notification.service.ts
index adb51974..98c965c5 100644
--- a/src/app/api/notification/notification.service.ts
+++ b/src/app/api/notification/notification.service.ts
@@ -9,7 +9,7 @@ import {
getInProductNotificationDetail,
} from '@/app/api/notification/notification.helper'
import { InternalUsersResponse } from '@/type/common'
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { AssemblyAPI } from '@/utils/assemblyAPI'
import CustomLogger from '@/utils/logger'
import { captureException, captureMessage } from '@sentry/nextjs'
@@ -41,7 +41,7 @@ export class NotificationService extends BaseService {
this.user.token,
)
try {
- const copilot = new CopilotAPI(this.user.token)
+ const copilot = new AssemblyAPI(this.user.workspaceId)
// 1. get all parties that gets notification
const parties = await this.getAllParties(copilot, action)
@@ -102,7 +102,7 @@ export class NotificationService extends BaseService {
}
async getAllParties(
- copilot: CopilotAPI,
+ copilot: AssemblyAPI,
action: NotificationActions,
): Promise {
if (IU_RECIPIENT_ACTIONS.has(action)) {
diff --git a/src/app/api/quickbooks/cron/cron.service.ts b/src/app/api/quickbooks/cron/cron.service.ts
index 3c159537..22d4923a 100644
--- a/src/app/api/quickbooks/cron/cron.service.ts
+++ b/src/app/api/quickbooks/cron/cron.service.ts
@@ -7,7 +7,7 @@ import { copilotAPIKey } from '@/config'
import { db } from '@/db'
import { QBSyncLog } from '@/db/schema/qbSyncLogs'
import { getAllActivePortalConnections } from '@/db/service/token.service'
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { getAssemblyTokenPayload } from '@/utils/assemblyTokenPayload'
import { encodePayload } from '@/utils/crypto'
import CustomLogger from '@/utils/logger'
import * as Sentry from '@sentry/nextjs'
@@ -29,8 +29,7 @@ export default class CronService {
const token = encodePayload(copilotAPIKey, payload)
// check if token is valid or not
- const copilot = new CopilotAPI(token)
- const tokenPayload = await copilot.getTokenPayload()
+ const tokenPayload = await getAssemblyTokenPayload(token)
CustomLogger.info({
obj: { copilotApiCronToken: token, tokenPayload },
message:
diff --git a/src/app/api/quickbooks/customer/customer.service.ts b/src/app/api/quickbooks/customer/customer.service.ts
index 4ab6d92f..1cebfc31 100644
--- a/src/app/api/quickbooks/customer/customer.service.ts
+++ b/src/app/api/quickbooks/customer/customer.service.ts
@@ -13,7 +13,7 @@ import {
import { CompanyResponse, WhereClause } from '@/type/common'
import { QBCustomerCreatePayloadType } from '@/type/dto/intuitAPI.dto'
import { InvoiceCreatedResponseType } from '@/type/dto/webhook.dto'
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { AssemblyAPI } from '@/utils/assemblyAPI'
import IntuitAPI from '@/utils/intuitAPI'
import { addSyncBreadcrumb } from '@/utils/sentry'
import { replaceSpecialCharsForQB } from '@/utils/string'
@@ -145,7 +145,7 @@ export class CustomerService extends BaseService {
)
}
- const copilot = new CopilotAPI(this.user.token)
+ const copilot = new AssemblyAPI(this.user.workspaceId)
let client
// get client and company info from copilot
diff --git a/src/app/api/quickbooks/invoice/invoice.service.ts b/src/app/api/quickbooks/invoice/invoice.service.ts
index a9ebeaef..13d985f9 100644
--- a/src/app/api/quickbooks/invoice/invoice.service.ts
+++ b/src/app/api/quickbooks/invoice/invoice.service.ts
@@ -50,7 +50,7 @@ import {
InvoiceResponseType,
} from '@/type/dto/webhook.dto'
import { bottleneck } from '@/utils/bottleneck'
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { AssemblyAPI } from '@/utils/assemblyAPI'
import IntuitAPI, { IntuitAPITokensType } from '@/utils/intuitAPI'
import dayjs from 'dayjs'
import { and, eq, isNull } from 'drizzle-orm'
@@ -74,12 +74,12 @@ type InvoiceItemRefAndDescriptionType = {
}
export class InvoiceService extends BaseService {
- private copilot: CopilotAPI
+ private copilot: AssemblyAPI
private syncLogService: SyncLogService
constructor(user: User) {
super(user)
- this.copilot = new CopilotAPI(user.token)
+ this.copilot = new AssemblyAPI(user.workspaceId)
this.syncLogService = new SyncLogService(user)
}
diff --git a/src/app/api/quickbooks/product/product.service.ts b/src/app/api/quickbooks/product/product.service.ts
index 7f4c0854..cf191e9b 100644
--- a/src/app/api/quickbooks/product/product.service.ts
+++ b/src/app/api/quickbooks/product/product.service.ts
@@ -19,7 +19,7 @@ import {
ProductCreatedResponseType,
ProductUpdatedResponseType,
} from '@/type/dto/webhook.dto'
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { AssemblyAPI } from '@/utils/assemblyAPI'
import IntuitAPI, { IntuitAPITokensType } from '@/utils/intuitAPI'
import {
and,
@@ -327,7 +327,7 @@ export class ProductService extends BaseService {
* their own UnitPrice, so the table is product-to-item only.
*/
async getProductsForMapping(): Promise {
- const copilot = new CopilotAPI(this.user.token)
+ const copilot = new AssemblyAPI(this.user.workspaceId)
const products = await copilot.getProducts({
limit: MAX_PRODUCT_LIST_LIMIT,
})
diff --git a/src/app/api/quickbooks/sync/sync.service.ts b/src/app/api/quickbooks/sync/sync.service.ts
index efd93239..9e6129c9 100644
--- a/src/app/api/quickbooks/sync/sync.service.ts
+++ b/src/app/api/quickbooks/sync/sync.service.ts
@@ -3,7 +3,7 @@ import { SyncLogService } from '@/app/api/quickbooks/syncLog/syncLog.service'
import { InvoiceService } from '@/app/api/quickbooks/invoice/invoice.service'
import { AuthService } from '@/app/api/quickbooks/auth/auth.service'
import IntuitAPI, { IntuitAPITokensType } from '@/utils/intuitAPI'
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { AssemblyAPI } from '@/utils/assemblyAPI'
import {
FailedRecordCategoryType,
EntityType,
@@ -43,7 +43,7 @@ export class SyncService extends BaseService {
record: QBSyncLogSelectSchemaType,
qbTokenInfo: IntuitAPITokensType,
) {
- const copilotApi = new CopilotAPI(this.user.token)
+ const copilotApi = new AssemblyAPI(this.user.workspaceId)
try {
// get invoice from Copilot API
@@ -74,7 +74,7 @@ export class SyncService extends BaseService {
record: QBSyncLogSelectSchemaType,
qbTokenInfo: IntuitAPITokensType,
) {
- const copilotApi = new CopilotAPI(this.user.token)
+ const copilotApi = new AssemblyAPI(this.user.workspaceId)
try {
// get invoice from Copilot API
@@ -312,7 +312,7 @@ export class SyncService extends BaseService {
qbTokenInfo: IntuitAPITokensType,
) {
const productService = new ProductService(this.user)
- const copilotApi = new CopilotAPI(this.user.token)
+ const copilotApi = new AssemblyAPI(this.user.workspaceId)
const productResponse = await copilotApi.getProduct(
z.string().parse(record.copilotId),
@@ -345,7 +345,7 @@ export class SyncService extends BaseService {
qbTokenInfo: IntuitAPITokensType,
) {
const productService = new ProductService(this.user)
- const copilotApi = new CopilotAPI(this.user.token)
+ const copilotApi = new AssemblyAPI(this.user.workspaceId)
try {
const product = await copilotApi.getProduct(record.copilotId)
diff --git a/src/app/api/quickbooks/webhook/webhook.service.ts b/src/app/api/quickbooks/webhook/webhook.service.ts
index 343f56b6..19127f3c 100644
--- a/src/app/api/quickbooks/webhook/webhook.service.ts
+++ b/src/app/api/quickbooks/webhook/webhook.service.ts
@@ -20,7 +20,7 @@ import {
WebhookEventResponseType,
} from '@/type/dto/webhook.dto'
import { validateAccessToken } from '@/utils/auth'
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { AssemblyAPI } from '@/utils/assemblyAPI'
import { ErrorMessageAndCode, getMessageAndCodeFromError } from '@/utils/error'
import { IntuitAPITokensType } from '@/utils/intuitAPI'
import CustomLogger from '@/utils/logger'
@@ -509,7 +509,7 @@ export class WebhookService extends BaseService {
return
}
- const copilotApp = new CopilotAPI(this.user.token)
+ const copilotApp = new AssemblyAPI(this.user.workspaceId)
const invoice = await copilotApp.getInvoice(
parsedPaymentSucceedResource.data.invoiceId,
)
diff --git a/src/cmd/backfillProductInfo/backfillProductInfo.service.ts b/src/cmd/backfillProductInfo/backfillProductInfo.service.ts
index bda0a91c..3bc567ab 100644
--- a/src/cmd/backfillProductInfo/backfillProductInfo.service.ts
+++ b/src/cmd/backfillProductInfo/backfillProductInfo.service.ts
@@ -9,7 +9,7 @@ import {
QBProductSync,
} from '@/db/schema/qbProductSync'
import { StatusableError } from '@/type/CopilotApiError'
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { AssemblyAPI } from '@/utils/assemblyAPI'
import IntuitAPI from '@/utils/intuitAPI'
import { eq, isNotNull } from 'drizzle-orm'
import { convert } from 'html-to-text'
@@ -38,7 +38,7 @@ export class BackfillProductInfoService extends BaseService {
}
// 2. get all products from assembly
- const copilotApi = new CopilotAPI(this.user.token)
+ const copilotApi = new AssemblyAPI(this.user.workspaceId)
const assemblyProducts = (
await copilotApi.getProducts({ limit: MAX_PRODUCT_LIST_LIMIT })
)?.data
diff --git a/src/cmd/backfillProductInfo/index.ts b/src/cmd/backfillProductInfo/index.ts
index ae8e759a..da838af1 100644
--- a/src/cmd/backfillProductInfo/index.ts
+++ b/src/cmd/backfillProductInfo/index.ts
@@ -4,7 +4,7 @@ import { BackfillProductInfoService } from '@/cmd/backfillProductInfo/backfillPr
import { copilotAPIKey } from '@/config'
import { PortalConnectionWithSettingType } from '@/db/schema/qbPortalConnections'
import { getAllActivePortalConnections } from '@/db/service/token.service'
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { getAssemblyTokenPayload } from '@/utils/assemblyTokenPayload'
import { encodePayload } from '@/utils/crypto'
import CustomLogger from '@/utils/logger'
@@ -54,8 +54,7 @@ async function initiateProcess(connection: PortalConnectionWithSettingType) {
}
const token = encodePayload(copilotAPIKey, payload)
- const copilot = new CopilotAPI(token)
- const tokenPayload = await copilot.getTokenPayload()
+ const tokenPayload = await getAssemblyTokenPayload(token)
CustomLogger.info({
obj: { copilotApiCronToken: token, tokenPayload },
message:
diff --git a/src/cmd/syncMissedInvoices/index.ts b/src/cmd/syncMissedInvoices/index.ts
index 2eb1c6e2..4e1de97a 100644
--- a/src/cmd/syncMissedInvoices/index.ts
+++ b/src/cmd/syncMissedInvoices/index.ts
@@ -4,7 +4,7 @@ import { SyncMissedInvoicesService } from '@/cmd/syncMissedInvoices/syncMissedIn
import { copilotAPIKey } from '@/config'
import { PortalConnectionWithSettingType } from '@/db/schema/qbPortalConnections'
import { getAllActivePortalConnections } from '@/db/service/token.service'
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { getAssemblyTokenPayload } from '@/utils/assemblyTokenPayload'
import { encodePayload } from '@/utils/crypto'
import CustomLogger from '@/utils/logger'
@@ -53,8 +53,7 @@ async function initiateProcess(connection: PortalConnectionWithSettingType) {
}
const token = encodePayload(copilotAPIKey, payload)
- const copilot = new CopilotAPI(token)
- const tokenPayload = await copilot.getTokenPayload()
+ const tokenPayload = await getAssemblyTokenPayload(token)
CustomLogger.info({
obj: { copilotApiCronToken: token, tokenPayload },
message:
diff --git a/src/cmd/syncMissedInvoices/syncMissedInvoices.service.ts b/src/cmd/syncMissedInvoices/syncMissedInvoices.service.ts
index 46b6e170..fece8d08 100644
--- a/src/cmd/syncMissedInvoices/syncMissedInvoices.service.ts
+++ b/src/cmd/syncMissedInvoices/syncMissedInvoices.service.ts
@@ -6,7 +6,7 @@ import { AuthService } from '@/app/api/quickbooks/auth/auth.service'
import { InvoiceService } from '@/app/api/quickbooks/invoice/invoice.service'
import { QBSyncLog } from '@/db/schema/qbSyncLogs'
import { StatusableError } from '@/type/CopilotApiError'
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { AssemblyAPI } from '@/utils/assemblyAPI'
import CustomLogger from '@/utils/logger'
import { and, eq, gte, or, sql } from 'drizzle-orm'
import httpStatus from 'http-status'
@@ -32,7 +32,7 @@ export class SyncMissedInvoicesService extends BaseService {
)
// 2. Fetch all invoices from Copilot for this portal (single API call)
- const copilotApi = new CopilotAPI(this.user.token)
+ const copilotApi = new AssemblyAPI(this.user.workspaceId)
const allInvoices = await copilotApi.getInvoices(this.user.workspaceId)
const allPayments = await copilotApi.getPayments()
diff --git a/src/cmd/syncMissedProducts/index.ts b/src/cmd/syncMissedProducts/index.ts
index 610903ad..60536005 100644
--- a/src/cmd/syncMissedProducts/index.ts
+++ b/src/cmd/syncMissedProducts/index.ts
@@ -4,7 +4,7 @@ import { SyncMissedProductsService } from '@/cmd/syncMissedProducts/syncMissedPr
import { copilotAPIKey } from '@/config'
import { PortalConnectionWithSettingType } from '@/db/schema/qbPortalConnections'
import { getAllActivePortalConnections } from '@/db/service/token.service'
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { getAssemblyTokenPayload } from '@/utils/assemblyTokenPayload'
import { encodePayload } from '@/utils/crypto'
import CustomLogger from '@/utils/logger'
@@ -61,8 +61,7 @@ async function initiateProcess(connection: PortalConnectionWithSettingType) {
}
const token = encodePayload(copilotAPIKey, payload)
- const copilot = new CopilotAPI(token)
- const tokenPayload = await copilot.getTokenPayload()
+ const tokenPayload = await getAssemblyTokenPayload(token)
CustomLogger.info({
obj: { copilotApiCronToken: token, tokenPayload },
message:
diff --git a/src/cmd/syncMissedProducts/syncMissedProducts.service.ts b/src/cmd/syncMissedProducts/syncMissedProducts.service.ts
index e8d0e56a..911bc23c 100644
--- a/src/cmd/syncMissedProducts/syncMissedProducts.service.ts
+++ b/src/cmd/syncMissedProducts/syncMissedProducts.service.ts
@@ -3,7 +3,7 @@ import { BaseService } from '@/app/api/core/services/base.service'
import { withRetry } from '@/app/api/core/utils/withRetry'
import { AuthService } from '@/app/api/quickbooks/auth/auth.service'
import { StatusableError } from '@/type/CopilotApiError'
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { AssemblyAPI } from '@/utils/assemblyAPI'
import { and, eq } from 'drizzle-orm'
import httpStatus from 'http-status'
import { MAX_PRODUCT_LIST_LIMIT } from '@/app/api/core/constants/limit'
@@ -26,7 +26,7 @@ export class SyncMissedProductsService extends BaseService {
)
// 1. Get all the products for the portal
- const copilotApi = new CopilotAPI(this.user.token)
+ const copilotApi = new AssemblyAPI(this.user.workspaceId)
const allProducts = await copilotApi.getProducts({
limit: MAX_PRODUCT_LIST_LIMIT,
})
diff --git a/src/db/service/token.service.ts b/src/db/service/token.service.ts
index 38d986f6..cb56b22d 100644
--- a/src/db/service/token.service.ts
+++ b/src/db/service/token.service.ts
@@ -8,7 +8,7 @@ import {
} from '@/db/schema/qbPortalConnections'
import { QBSetting, QBSettingsSelectSchemaType } from '@/db/schema/qbSettings'
import { WorkspaceResponse } from '@/type/common'
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { AssemblyAPI } from '@/utils/assemblyAPI'
import { IntuitAPITokensType } from '@/utils/intuitAPI'
import { and, asc, eq, isNotNull, isNull, sql } from 'drizzle-orm'
import httpStatus from 'http-status'
@@ -101,9 +101,9 @@ export const getPortalSettings = async (
}
export const getWorkspaceInfo = async (
- token: string,
+ workspaceId: string,
): Promise => {
- return await new CopilotAPI(token).getWorkspace()
+ return await new AssemblyAPI(workspaceId).getWorkspace()
}
export const getPortalTokens = async (
diff --git a/src/helper/fetch.helper.ts b/src/helper/fetch.helper.ts
index 9b35cf46..64a3e2c2 100644
--- a/src/helper/fetch.helper.ts
+++ b/src/helper/fetch.helper.ts
@@ -35,7 +35,7 @@ const extractUpstreamDetail = (body: unknown): string | undefined => {
return undefined
}
-// Exported so other clients with their own fetch wrappers (e.g. CopilotAPI's
+// Exported so other clients with their own fetch wrappers (e.g. AssemblyAPI's
// manualFetch) can produce consistently-shaped HttpFetchError instances
// without duplicating the JSON/text body-parsing logic.
export const buildHttpFetchError = async (
diff --git a/src/type/common.ts b/src/type/common.ts
index 165f3cba..7c15b4fa 100644
--- a/src/type/common.ts
+++ b/src/type/common.ts
@@ -154,7 +154,7 @@ export const ClientRequestSchema = z.object({
familyName: z.string(),
email: z.string().email(),
companyId: z.string().uuid().optional(),
- // NOTE: customFields can also be passed as a JSON object, but CopilotAPI has its type defined to stringified JSON
+ // NOTE: customFields can also be passed as a JSON object, but AssemblyAPI has its type defined to stringified JSON
customFields: z.string().optional(),
})
export type ClientRequest = z.infer
@@ -183,24 +183,22 @@ export type InternalUsersResponse = z.infer
export const NotificationRequestBodySchema = z.object({
senderId: z.string(),
recipientId: z.string(),
- deliveryTargets: z
- .object({
- inProduct: z
- .object({
- title: z.string(),
- body: z.string().optional(),
- })
- .optional(),
- email: z
- .object({
- subject: z.string().optional(),
- header: z.string().optional(),
- title: z.string().optional(),
- body: z.string().optional(),
- })
- .optional(),
- })
- .optional(),
+ deliveryTargets: z.object({
+ inProduct: z
+ .object({
+ title: z.string(),
+ body: z.string().optional(),
+ })
+ .optional(),
+ email: z
+ .object({
+ subject: z.string(),
+ header: z.string(),
+ title: z.string(),
+ body: z.string().optional(),
+ })
+ .optional(),
+ }),
})
export type NotificationRequestBody = z.infer<
diff --git a/src/utils/copilotAPI.ts b/src/utils/assemblyAPI.ts
similarity index 62%
rename from src/utils/copilotAPI.ts
rename to src/utils/assemblyAPI.ts
index 5c065e03..59730f69 100644
--- a/src/utils/copilotAPI.ts
+++ b/src/utils/assemblyAPI.ts
@@ -10,8 +10,6 @@ import {
ClientResponse,
ClientResponseSchema,
ClientsResponseSchema,
- ClientToken,
- ClientTokenSchema,
CompaniesResponse,
CompaniesResponseSchema,
CompanyCreateRequest,
@@ -26,10 +24,6 @@ import {
InternalUsersSchema,
InvoiceResponse,
InvoiceResponseSchema,
- IUToken,
- IUTokenSchema,
- MeResponse,
- MeResponseSchema,
NotificationCreatedResponse,
NotificationCreatedResponseSchema,
NotificationRequestBody,
@@ -37,20 +31,16 @@ import {
PaymentsResponseSchema,
PriceResponse,
PriceResponseSchema,
- PricesResponse,
- PricesResponseSchema,
ProductResponse,
ProductResponseSchema,
ProductsResponse,
ProductsResponseSchema,
- Token,
- TokenSchema,
WorkspaceResponse,
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'
@@ -59,11 +49,11 @@ import {
MAX_INVOICE_LIST_LIMIT,
} from '@/app/api/core/constants/limit'
-export class CopilotAPI {
- copilot: SDK
+export class AssemblyAPI {
+ assembly: Promise
- constructor(private token: string) {
- this.copilot = copilotApi({ apiKey, token })
+ constructor(private workspaceId: string) {
+ this.assembly = assemblyApi({ apiKey: `${this.workspaceId}/${apiKey}` })
}
private async manualFetch(
@@ -79,7 +69,7 @@ export class CopilotAPI {
}
console.info(
- `CopilotAPI#manualFetch | url = ${url}, apiKey = ${apiKey}, workspaceId = ${workspaceId}`,
+ `AssemblyAPI#manualFetch | url = ${url}, apiKey = ${apiKey}, workspaceId = ${workspaceId}`,
)
const resp = await fetch(url, {
@@ -95,64 +85,27 @@ export class CopilotAPI {
return await resp.json()
}
- // NOTE: Any method prefixed with _ is a API method that doesn't implement retry & delay
- // NOTE: Any normal API method name implements `withRetry` with default config
-
- // Get Token Payload from copilot request token
- async _getTokenPayload(): Promise {
- const getTokenPayload = this.copilot.getTokenPayload
- if (!getTokenPayload) {
- console.error(
- `CopilotAPI#getTokenPayload | Could not parse token payload for token ${this.token}`,
- )
- return null
- }
-
- return TokenSchema.parse(await getTokenPayload())
+ private getSDK() {
+ return this.assembly
}
- async _me(): Promise {
- 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
- const currentUserInfo = await retrieveCurrentUserInfo({ id })
-
- return MeResponseSchema.parse(currentUserInfo)
- }
+ // NOTE: Any method prefixed with _ is a API method that doesn't implement retry & delay
+ // NOTE: Any normal API method name implements `withRetry` with default config
async _getWorkspace(): Promise {
- console.info('CopilotAPI#getWorkspace | token =', this.token)
- return WorkspaceResponseSchema.parse(await this.copilot.retrieveWorkspace())
- }
-
- async _getClientTokenPayload(): Promise {
- console.info('CopilotAPI#getClientTokenPayload | token =', this.token)
- const tokenPayload = await this.getTokenPayload()
- if (!tokenPayload) return null
-
- return ClientTokenSchema.parse(tokenPayload)
- }
-
- async _getIUTokenPayload(): Promise {
- console.info('CopilotAPI#getIUTokenPayload | token =', this.token)
- const tokenPayload = await this.getTokenPayload()
- if (!tokenPayload) return null
-
- return IUTokenSchema.parse(tokenPayload)
+ console.info('AssemblyAPI#getWorkspace | workspaceId =', this.workspaceId)
+ const sdk = await this.getSDK()
+ return WorkspaceResponseSchema.parse(await sdk.retrieveWorkspace())
}
async _createClient(
requestBody: ClientRequest,
sendInvite: boolean = false,
): Promise {
- console.info('CopilotAPI#createClient | token =', this.token)
+ console.info('AssemblyAPI#createClient | workspaceId =', this.workspaceId)
+ const sdk = await this.getSDK()
return ClientResponseSchema.parse(
- await this.copilot.createClient({ sendInvite, requestBody }),
+ await sdk.createClient({ sendInvite, requestBody }),
)
}
@@ -161,11 +114,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 sdk = await this.getSDK()
try {
- console.info('CopilotAPI#getClient | token =', this.token)
- return ClientResponseSchema.parse(
- await this.copilot.retrieveClient({ id }),
- )
+ console.info('AssemblyAPI#getClient | workspaceId =', this.workspaceId)
+ return ClientResponseSchema.parse(await sdk.retrieveClient({ id }))
} catch (error: unknown) {
if (
typeof error === 'object' &&
@@ -179,7 +131,7 @@ export class CopilotAPI {
error.status === httpStatus.NOT_FOUND
) {
const errorBody = (error as { body: any }).body
- console.info('CopilotAPI#getClient | message =', errorBody.message)
+ console.info('AssemblyAPI#getClient | message =', errorBody.message)
return
}
}
@@ -192,9 +144,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 sdk = await this.getSDK()
try {
- console.info('CopilotAPI#getClients | token =', this.token)
- return ClientsResponseSchema.parse(await this.copilot.listClients(args))
+ console.info('AssemblyAPI#getClients | workspaceId =', this.workspaceId)
+ return ClientsResponseSchema.parse(await sdk.listClients(args))
} catch (error: unknown) {
if (
typeof error === 'object' &&
@@ -208,7 +161,7 @@ export class CopilotAPI {
error.status === httpStatus.NOT_FOUND
) {
const errorBody = (error as { body: any }).body
- console.info('CopilotAPI#getClients | message =', errorBody.message)
+ console.info('AssemblyAPI#getClients | message =', errorBody.message)
return
}
}
@@ -220,22 +173,23 @@ export class CopilotAPI {
id: string,
requestBody: ClientRequest,
): Promise {
- console.info('CopilotAPI#updateClient | token =', this.token)
+ console.info('AssemblyAPI#updateClient | workspaceId =', this.workspaceId)
+ const sdk = await this.getSDK()
return ClientResponseSchema.parse(
- await this.copilot.updateClient({ id, requestBody }),
+ await sdk.updateClient({ id, requestBody }),
)
}
async _deleteClient(id: string) {
- console.info('CopilotAPI#deleteClient | token =', this.token)
- return await this.copilot.deleteClient({ id })
+ console.info('AssemblyAPI#deleteClient | workspaceId =', this.workspaceId)
+ const sdk = await this.getSDK()
+ return await sdk.deleteClient({ id })
}
async _createCompany(requestBody: CompanyCreateRequest) {
- console.info('CopilotAPI#createCompany | token =', this.token)
- return CompanyResponseSchema.parse(
- await this.copilot.createCompany({ requestBody }),
- )
+ console.info('AssemblyAPI#createCompany | token =', this.workspaceId)
+ const sdk = await this.getSDK()
+ return CompanyResponseSchema.parse(await sdk.createCompany({ requestBody }))
}
/**
@@ -244,10 +198,9 @@ export class CopilotAPI {
*/
async _getCompany(id: string): Promise {
try {
- console.info('CopilotAPI#getCompany | token =', this.token)
- return CompanyResponseSchema.parse(
- await this.copilot.retrieveCompany({ id }),
- )
+ console.info('AssemblyAPI#getCompany | token =', this.workspaceId)
+ const sdk = await this.getSDK()
+ return CompanyResponseSchema.parse(await sdk.retrieveCompany({ id }))
} catch (error: unknown) {
if (
typeof error === 'object' &&
@@ -261,7 +214,7 @@ export class CopilotAPI {
error.status === httpStatus.NOT_FOUND
) {
const errorBody = (error as { body: any }).body
- console.info('CopilotAPI#getCompany | message =', errorBody.message)
+ console.info('AssemblyAPI#getCompany | message =', errorBody.message)
return
}
}
@@ -270,57 +223,63 @@ export class CopilotAPI {
}
async _getCompanies(args: CopilotListArgs = {}): Promise {
- console.info('CopilotAPI#getCompanies | token =', this.token)
- return CompaniesResponseSchema.parse(await this.copilot.listCompanies(args))
+ console.info('AssemblyAPI#getCompanies | token =', this.workspaceId)
+ const sdk = await this.getSDK()
+ return CompaniesResponseSchema.parse(await sdk.listCompanies(args))
}
async _getCompanyClients(companyId: string): Promise {
- console.info('CopilotAPI#getCompanyClients | token =', this.token)
+ console.info('AssemblyAPI#getCompanyClients | token =', this.workspaceId)
return (await this.getClients({ limit: 10000, companyId }))?.data || []
}
async _getCustomFields(): Promise {
- console.info('CopilotAPI#getCustomFields | token =', this.token)
- return CustomFieldResponseSchema.parse(
- await this.copilot.listCustomFields(),
- )
+ console.info('AssemblyAPI#getCustomFields | token =', this.workspaceId)
+ const sdk = await this.getSDK()
+ return CustomFieldResponseSchema.parse(await sdk.listCustomFields({}))
}
async _getInternalUsers(
args: CopilotListArgs = {},
): Promise {
- console.info('CopilotAPI#getInternalUsers | token =', this.token)
- return InternalUsersResponseSchema.parse(
- await this.copilot.listInternalUsers(args),
- )
+ console.info('AssemblyAPI#getInternalUsers | token =', this.workspaceId)
+ const sdk = await this.getSDK()
+ return InternalUsersResponseSchema.parse(await sdk.listInternalUsers(args))
}
async _getInternalUser(id: string): Promise {
- console.info('CopilotAPI#getInternalUser | token =', this.token)
- return InternalUsersSchema.parse(
- await this.copilot.retrieveInternalUser({ id }),
- )
+ console.info('AssemblyAPI#getInternalUser | token =', this.workspaceId)
+ const sdk = await this.getSDK()
+ return InternalUsersSchema.parse(await sdk.retrieveInternalUser({ id }))
}
async _createNotification(
requestBody: NotificationRequestBody,
): Promise {
- console.info('CopilotAPI#createNotification | token =', this.token)
- console.info('CopilotAPI#createNotification | requestBody =', requestBody)
+ console.info('AssemblyAPI#createNotification | token =', this.workspaceId)
+ console.info('AssemblyAPI#createNotification | requestBody =', requestBody)
+ const sdk = await this.getSDK()
return NotificationCreatedResponseSchema.parse(
- await this.copilot.createNotification({
+ await sdk.createNotification({
requestBody,
}),
)
}
async _markNotificationAsRead(id: string): Promise {
- console.info('CopilotAPI#markNotificationAsRead | token =', this.token)
- await this.copilot.markNotificationRead({ id })
+ console.info(
+ 'AssemblyAPI#markNotificationAsRead | token =',
+ this.workspaceId,
+ )
+ const sdk = await this.getSDK()
+ await sdk.markNotificationRead({ id })
}
async _bulkMarkNotificationsAsRead(notificationIds: string[]): Promise {
- console.info('CopilotAPI#markNotificationAsRead | token =', this.token)
+ console.info(
+ 'AssemblyAPI#markNotificationAsRead | token =',
+ this.workspaceId,
+ )
const markAsReadPromises = []
const bottleneck = new Bottleneck({ minTime: 250, maxConcurrent: 2 })
@@ -343,12 +302,13 @@ export class CopilotAPI {
}
async _deleteNotification(id: string): Promise {
- console.info('CopilotAPI#deleteNotification | token =', this.token)
- await this.copilot.deleteNotification({ id })
+ console.info('AssemblyAPI#deleteNotification | token =', this.workspaceId)
+ const sdk = await this.getSDK()
+ await sdk.deleteNotification({ id })
}
async _bulkDeleteNotifications(notificationIds: string[]): Promise {
- console.info('CopilotAPI#deleteNotification | token =', this.token)
+ console.info('AssemblyAPI#deleteNotification | token =', this.workspaceId)
const deletePromises = []
const bottleneck = new Bottleneck({ minTime: 250, maxConcurrent: 2 })
for (const notification of notificationIds) {
@@ -391,10 +351,9 @@ export class CopilotAPI {
}
async _getProduct(id: string): Promise {
- console.info('CopilotAPI#getProduct | token =', this.token)
- return ProductResponseSchema.parse(
- await this.copilot.retrieveProduct({ id }),
- )
+ console.info('AssemblyAPI#getProduct | token =', this.workspaceId)
+ const sdk = await this.getSDK()
+ return ProductResponseSchema.parse(await sdk.retrieveProduct({ id }))
}
async _getProducts({
@@ -406,39 +365,29 @@ export class CopilotAPI {
nextToken?: string
limit?: number
}): Promise {
- console.info('CopilotAPI#getProducts | token =', this.token)
+ console.info('AssemblyAPI#getProducts | token =', this.workspaceId)
+ const sdk = await this.getSDK()
return ProductsResponseSchema.parse(
- await this.copilot.listProducts({ name, nextToken, limit }),
+ await sdk.listProducts({ name, nextToken, limit }),
)
}
async _getPrice(id: string): Promise {
- console.info('CopilotAPI#getPrice | token =', this.token)
- return PriceResponseSchema.parse(await this.copilot.retrievePrice({ id }))
- }
-
- async _getPrices(
- productId?: string,
- nextToken?: string,
- limit?: string,
- ): Promise {
- console.info('CopilotAPI#getPrices | token =', this.token)
- return PricesResponseSchema.parse(
- await this.copilot.listPrices({ productId, nextToken, limit }),
- )
+ console.info('AssemblyAPI#getPrice | token =', this.workspaceId)
+ const sdk = await this.getSDK()
+ return PriceResponseSchema.parse(await sdk.retrievePrice({ id }))
}
async _getInvoice(id: string): Promise {
- console.info('CopilotAPI#getInvoice | token =', this.token)
- return InvoiceResponseSchema.parse(
- await this.copilot.retrieveInvoice({ id }),
- )
+ console.info('AssemblyAPI#getInvoice | token =', this.workspaceId)
+ const sdk = await this.getSDK()
+ return InvoiceResponseSchema.parse(await sdk.retrieveInvoice({ id }))
}
async _getInvoices(
workspaceId?: string,
): Promise {
- console.info('CopilotAPI#getInvoices | token =', this.token)
+ console.info('AssemblyAPI#getInvoices | token =', this.workspaceId)
const data = await this.manualFetch(
'invoices',
{
@@ -447,18 +396,19 @@ export class CopilotAPI {
workspaceId,
)
- console.info(`CopilotAPI#getInvoices | data length = ${data.data?.length}`)
+ console.info(`AssemblyAPI#getInvoices | data length = ${data.data?.length}`)
return z.array(InvoiceResponseSchema).parse(data.data)
}
async _getPayments(
invoiceId?: string,
): Promise {
- console.info('CopilotAPI#getPayments | token =', this.token)
+ console.info('AssemblyAPI#getPayments | token =', this.workspaceId)
+ const sdk = await this.getSDK()
return PaymentsResponseSchema.parse(
- await this.copilot.listPayments({
+ await sdk.listPayments({
invoiceId,
- limit: MAX_ASSEMBLY_RESOURCE_LIST_LIMIT.toString(),
+ limit: MAX_ASSEMBLY_RESOURCE_LIST_LIMIT,
}),
)
}
@@ -470,11 +420,7 @@ export class CopilotAPI {
}
// Methods wrapped with retry
- getTokenPayload = this.wrapWithRetry(this._getTokenPayload)
- me = this.wrapWithRetry(this._me)
getWorkspace = this.wrapWithRetry(this._getWorkspace)
- getClientTokenPayload = this.wrapWithRetry(this._getClientTokenPayload)
- getIUTokenPayload = this.wrapWithRetry(this._getIUTokenPayload)
createClient = this.wrapWithRetry(this._createClient)
getClient = this.wrapWithRetry(this._getClient)
getClients = this.wrapWithRetry(this._getClients)
@@ -497,7 +443,6 @@ export class CopilotAPI {
getProduct = this.wrapWithRetry(this._getProduct)
getProducts = this.wrapWithRetry(this._getProducts)
getPrice = this.wrapWithRetry(this._getPrice)
- getPrices = this.wrapWithRetry(this._getPrices)
getInvoice = this.wrapWithRetry(this._getInvoice)
getInvoices = this.wrapWithRetry(this._getInvoices)
getPayments = this.wrapWithRetry(this._getPayments)
diff --git a/src/utils/assemblyTokenPayload.ts b/src/utils/assemblyTokenPayload.ts
new file mode 100644
index 00000000..36ccf166
--- /dev/null
+++ b/src/utils/assemblyTokenPayload.ts
@@ -0,0 +1,17 @@
+import { copilotAPIKey } from '@/config'
+import { TokenSchema } from '@/type/common'
+import { assemblyApi } from '@assembly-js/node-sdk'
+
+// Decodes a request token into its payload. Kept separate from AssemblyAPI
+// (workspace-scoped) so the token-scoped SDK stays out of the wholesale-mocked
+// client — the auth boundary is the only caller.
+export async function getAssemblyTokenPayload(token: string) {
+ const sdk = await assemblyApi({ apiKey: copilotAPIKey, token })
+ if (!sdk.getTokenPayload) {
+ console.error(
+ `getAssemblyTokenPayload | Could not parse token payload for token ${token}`,
+ )
+ return null
+ }
+ return TokenSchema.parse(await sdk.getTokenPayload())
+}
diff --git a/test/diagrams/test-flow.md b/test/diagrams/test-flow.md
index 37257acc..372758a7 100644
--- a/test/diagrams/test-flow.md
+++ b/test/diagrams/test-flow.md
@@ -26,7 +26,7 @@ flowchart TD
C --> D["Per-file vi.mock(...) at top of file
(every external boundary stubbed)"]
D --> D1["vi.mock @/db
(no real Postgres at all)"]
- D --> D2["vi.mock @/utils/copilotAPI"]
+ D --> D2["vi.mock @/utils/assemblyAPI"]
D --> D3["vi.mock @/utils/intuitAPI"]
D --> D4["vi.mock @sentry/nextjs"]
D --> D5["vi.mock @/utils/logger / sleep / auth"]
@@ -81,7 +81,7 @@ flowchart LR
C4["dotenv loads .env.test
(override: true)"]
D["Single forked worker boots
(inherits env vars)"]
E["setupFiles → test/integration/setup.ts"]
- E1["vi.mock @/utils/copilotAPI"]
+ E1["vi.mock @/utils/assemblyAPI"]
E2["vi.mock @/utils/intuitAPI"]
E3["vi.mock @/utils/intuit
(pinned on globalThis)"]
E4["vi.mock @sentry/nextjs"]
@@ -111,7 +111,7 @@ flowchart LR
J["Next.js route
src/app/api/quickbooks/webhook/route.ts"]
K["Controller → WebhookService
→ Invoice/Product/Payment services"]
L1[("Postgres in Testcontainer
via @/db Drizzle singleton")]
- L2["MOCKED CopilotAPI"]
+ L2["MOCKED AssemblyAPI"]
L3["MOCKED IntuitAPI"]
M["Response"]
N["expect(status / DB rows / mock.calls)"]
diff --git a/test/helpers/invoiceCreatedTestSetup.ts b/test/helpers/invoiceCreatedTestSetup.ts
index fdcbdac7..88ea8ef4 100644
--- a/test/helpers/invoiceCreatedTestSetup.ts
+++ b/test/helpers/invoiceCreatedTestSetup.ts
@@ -2,14 +2,14 @@ import { beforeEach, afterEach, vi } from 'vitest'
import { truncateAllTestTables } from '@test/helpers/testDb'
import {
installMockApis,
- type MockCopilotAPI,
+ type MockAssemblyAPI,
type MockIntuitAPI,
} from '@test/helpers/mocks'
type InstallOpts = Parameters[0]
export interface InvoiceCreatedTestHandle {
- copilot: MockCopilotAPI
+ copilot: MockAssemblyAPI
intuit: MockIntuitAPI
}
diff --git a/test/helpers/invoiceDeletedTestSetup.ts b/test/helpers/invoiceDeletedTestSetup.ts
index 105d0b20..abf70507 100644
--- a/test/helpers/invoiceDeletedTestSetup.ts
+++ b/test/helpers/invoiceDeletedTestSetup.ts
@@ -2,14 +2,14 @@ import { beforeEach, afterEach, vi } from 'vitest'
import { truncateAllTestTables } from '@test/helpers/testDb'
import {
installMockApis,
- type MockCopilotAPI,
+ type MockAssemblyAPI,
type MockIntuitAPI,
} from '@test/helpers/mocks'
type InstallOpts = Parameters[0]
export interface InvoiceDeletedTestHandle {
- copilot: MockCopilotAPI
+ copilot: MockAssemblyAPI
intuit: MockIntuitAPI
}
diff --git a/test/helpers/invoicePaidTestSetup.ts b/test/helpers/invoicePaidTestSetup.ts
index 0b2608e5..e76e21d6 100644
--- a/test/helpers/invoicePaidTestSetup.ts
+++ b/test/helpers/invoicePaidTestSetup.ts
@@ -2,14 +2,14 @@ import { beforeEach, afterEach, vi } from 'vitest'
import { truncateAllTestTables } from '@test/helpers/testDb'
import {
installMockApis,
- type MockCopilotAPI,
+ type MockAssemblyAPI,
type MockIntuitAPI,
} from '@test/helpers/mocks'
type InstallOpts = Parameters[0]
export interface InvoicePaidTestHandle {
- copilot: MockCopilotAPI
+ copilot: MockAssemblyAPI
intuit: MockIntuitAPI
}
diff --git a/test/helpers/invoiceVoidedTestSetup.ts b/test/helpers/invoiceVoidedTestSetup.ts
index dd3d7553..65cef3f3 100644
--- a/test/helpers/invoiceVoidedTestSetup.ts
+++ b/test/helpers/invoiceVoidedTestSetup.ts
@@ -2,14 +2,14 @@ import { beforeEach, afterEach, vi } from 'vitest'
import { truncateAllTestTables } from '@test/helpers/testDb'
import {
installMockApis,
- type MockCopilotAPI,
+ type MockAssemblyAPI,
type MockIntuitAPI,
} from '@test/helpers/mocks'
type InstallOpts = Parameters[0]
export interface InvoiceVoidedTestHandle {
- copilot: MockCopilotAPI
+ copilot: MockAssemblyAPI
intuit: MockIntuitAPI
}
diff --git a/test/helpers/mocks.ts b/test/helpers/mocks.ts
index b11d0d79..a93798a7 100644
--- a/test/helpers/mocks.ts
+++ b/test/helpers/mocks.ts
@@ -1,10 +1,8 @@
import { vi, type Mock } from 'vitest'
-import { CopilotAPI } from '@/utils/copilotAPI'
+import { AssemblyAPI } from '@/utils/assemblyAPI'
import IntuitAPI from '@/utils/intuitAPI'
import {
TEST_INCOME_ACCOUNT_REF,
- TEST_INTERNAL_USER_ID,
- TEST_PORTAL_ID,
TEST_COPILOT_INVOICE_ID,
TEST_INVOICE_NUMBER,
TEST_QB_PURCHASE_ID,
@@ -22,22 +20,18 @@ type MockMethodOverrides = {
: never]?: Mock
}
-type CopilotAPIOverrides = MockMethodOverrides
+type AssemblyAPIOverrides = MockMethodOverrides
type IntuitAPIOverrides = MockMethodOverrides
/**
- * Factory for a mocked CopilotAPI instance.
+ * Factory for a mocked AssemblyAPI instance.
*
- * Tests mock the CopilotAPI module with `vi.mock('@/utils/copilotAPI')`, then
- * wire each `new CopilotAPI(token)` call to an object produced by this factory.
+ * Tests mock the AssemblyAPI module with `vi.mock('@/utils/assemblyAPI')`, then
+ * wire each `new AssemblyAPI(token)` call to an object produced by this factory.
* Override any method via the `overrides` arg to tailor behavior per test.
*/
-export function createMockCopilotAPI(overrides: CopilotAPIOverrides = {}) {
+export function createMockAssemblyAPI(overrides: AssemblyAPIOverrides = {}) {
return {
- getTokenPayload: vi.fn().mockResolvedValue({
- workspaceId: TEST_PORTAL_ID,
- internalUserId: TEST_INTERNAL_USER_ID,
- }),
getProduct: vi.fn().mockResolvedValue({
id: '2cf93cf0-45fa-485f-b584-03c2c38a3999',
name: 'Test Product',
@@ -162,31 +156,31 @@ export function createMockIntuitAPI(overrides: IntuitAPIOverrides = {}) {
}
}
-export type MockCopilotAPI = ReturnType
+export type MockAssemblyAPI = ReturnType
export type MockIntuitAPI = ReturnType
/**
- * Wires the module-mocked CopilotAPI + IntuitAPI to shared instances and
+ * Wires the module-mocked AssemblyAPI + IntuitAPI to shared instances and
* returns them so tests can assert on calls. Uses `function` (not arrow) so
* the mock is callable with `new`.
*
- * Caveat: one request may `new CopilotAPI(...)` several times (auth +
+ * Caveat: one request may `new AssemblyAPI(...)` several times (auth +
* invoice flow) — all share this instance, so call counts sum across sites.
*/
export function installMockApis(
opts: {
- copilot?: MockCopilotAPI
+ copilot?: MockAssemblyAPI
intuit?: MockIntuitAPI
} = {},
-): { copilot: MockCopilotAPI; intuit: MockIntuitAPI } {
- const copilot = opts.copilot ?? createMockCopilotAPI()
+): { copilot: MockAssemblyAPI; intuit: MockIntuitAPI } {
+ const copilot = opts.copilot ?? createMockAssemblyAPI()
const intuit = opts.intuit ?? createMockIntuitAPI()
- vi.mocked(CopilotAPI).mockImplementation(function (
+ vi.mocked(AssemblyAPI).mockImplementation(function (
this: unknown,
- ): CopilotAPI {
- return copilot as unknown as CopilotAPI
- } as unknown as typeof CopilotAPI)
+ ): AssemblyAPI {
+ return copilot as unknown as AssemblyAPI
+ } as unknown as typeof AssemblyAPI)
vi.mocked(IntuitAPI).mockImplementation(function (this: unknown): IntuitAPI {
return intuit as unknown as IntuitAPI
diff --git a/test/helpers/paymentSucceededTestSetup.ts b/test/helpers/paymentSucceededTestSetup.ts
index 2a7b5c0a..7653544a 100644
--- a/test/helpers/paymentSucceededTestSetup.ts
+++ b/test/helpers/paymentSucceededTestSetup.ts
@@ -2,14 +2,14 @@ import { beforeEach, afterEach, vi } from 'vitest'
import { truncateAllTestTables } from '@test/helpers/testDb'
import {
installMockApis,
- type MockCopilotAPI,
+ type MockAssemblyAPI,
type MockIntuitAPI,
} from '@test/helpers/mocks'
type InstallOpts = Parameters[0]
export interface PaymentSucceededTestHandle {
- copilot: MockCopilotAPI
+ copilot: MockAssemblyAPI
intuit: MockIntuitAPI
}
diff --git a/test/helpers/productCreatedTestSetup.ts b/test/helpers/productCreatedTestSetup.ts
index 81a3432c..f4e3bc2c 100644
--- a/test/helpers/productCreatedTestSetup.ts
+++ b/test/helpers/productCreatedTestSetup.ts
@@ -2,14 +2,14 @@ import { beforeEach, afterEach, vi } from 'vitest'
import { truncateAllTestTables } from '@test/helpers/testDb'
import {
installMockApis,
- type MockCopilotAPI,
+ type MockAssemblyAPI,
type MockIntuitAPI,
} from '@test/helpers/mocks'
type InstallOpts = Parameters[0]
export interface ProductCreatedTestHandle {
- copilot: MockCopilotAPI
+ copilot: MockAssemblyAPI
intuit: MockIntuitAPI
}
diff --git a/test/integration/quickbooks/accounts/updateAccountRefs.test.ts b/test/integration/quickbooks/accounts/updateAccountRefs.test.ts
index 3d571cc1..8ef956c0 100644
--- a/test/integration/quickbooks/accounts/updateAccountRefs.test.ts
+++ b/test/integration/quickbooks/accounts/updateAccountRefs.test.ts
@@ -105,7 +105,7 @@ describe('PATCH /api/quickbooks/accounts', () => {
})
it("cannot modify another portal's row (tenant isolation)", async () => {
- // CopilotAPI mock always decrypts to TEST_PORTAL_ID, so this verifies the
+ // AssemblyAPI mock always decrypts to TEST_PORTAL_ID, so this verifies the
// WHERE-clause scope works — not that a foreign token would be rejected.
await seedHealthyPortal()
const OTHER = 'other-portal-99999999'
diff --git a/test/integration/quickbooks/invoiceCreated/useCompanyNameFlag.test.ts b/test/integration/quickbooks/invoiceCreated/useCompanyNameFlag.test.ts
index 2b515dec..ef63b186 100644
--- a/test/integration/quickbooks/invoiceCreated/useCompanyNameFlag.test.ts
+++ b/test/integration/quickbooks/invoiceCreated/useCompanyNameFlag.test.ts
@@ -5,13 +5,13 @@ import { QBCustomers } from '@/db/schema/qbCustomers'
import invoiceCreatedPayload from '@test/fixtures/invoiceCreated.webhook'
import { seedHealthyPortal, seedProductSync } from '@test/helpers/seed'
-import { createMockCopilotAPI } from '@test/helpers/mocks'
+import { createMockAssemblyAPI } from '@test/helpers/mocks'
import { setupInvoiceCreatedTest } from '@test/helpers/invoiceCreatedTestSetup'
import { postWebhook } from '@test/helpers/webhook'
describe('POST /api/quickbooks/webhook — invoice.created (invoice belongs to a company and the "use company name" setting is on)', () => {
const apis = setupInvoiceCreatedTest(() => ({
- copilot: createMockCopilotAPI({
+ copilot: createMockAssemblyAPI({
// Payload has companyId but no clientId, so any client lookup is wrong.
getClient: vi.fn().mockResolvedValue(undefined),
getCompany: vi.fn().mockResolvedValue({
diff --git a/test/integration/quickbooks/paymentSucceeded/copilotInvoiceNotFound.test.ts b/test/integration/quickbooks/paymentSucceeded/copilotInvoiceNotFound.test.ts
index 37bfd7f5..1ffe8bc6 100644
--- a/test/integration/quickbooks/paymentSucceeded/copilotInvoiceNotFound.test.ts
+++ b/test/integration/quickbooks/paymentSucceeded/copilotInvoiceNotFound.test.ts
@@ -11,13 +11,13 @@ import {
seedQBInvoiceSync,
TEST_COPILOT_PAYMENT_ID,
} from '@test/helpers/seed'
-import { createMockCopilotAPI } from '@test/helpers/mocks'
+import { createMockAssemblyAPI } from '@test/helpers/mocks'
import { setupPaymentSucceededTest } from '@test/helpers/paymentSucceededTestSetup'
import { postWebhook } from '@test/helpers/webhook'
describe('POST /api/quickbooks/webhook — payment.succeeded (Copilot returns no invoice)', () => {
const apis = setupPaymentSucceededTest(() => ({
- copilot: createMockCopilotAPI({
+ copilot: createMockAssemblyAPI({
getInvoice: vi.fn().mockResolvedValue(undefined),
}),
}))
diff --git a/test/integration/setup.ts b/test/integration/setup.ts
index 2e210acd..7e4f179d 100644
--- a/test/integration/setup.ts
+++ b/test/integration/setup.ts
@@ -1,14 +1,15 @@
import { vi } from 'vitest'
+import { TEST_INTERNAL_USER_ID, TEST_PORTAL_ID } from '@test/helpers/seed'
/**
* Shared module mocks for all integration tests.
*
* Loaded via `setupFiles` in vitest.config.ts (integration project). Each
* test file still configures per-test behavior in beforeEach via
- * `vi.mocked(CopilotAPI).mockImplementation(...)`.
+ * `vi.mocked(AssemblyAPI).mockImplementation(...)`.
*
* Why here instead of per-file:
- * - Explicit factory for CopilotAPI/IntuitAPI avoids evaluating the real
+ * - Explicit factory for AssemblyAPI/IntuitAPI avoids evaluating the real
* modules (copilot-node-sdk has an ESM directory-import that breaks).
* - Sentry has to be stubbed because withRetry.ts calls
* `scope.addEventProcessor(...)` inside Sentry.withScope.
@@ -18,18 +19,28 @@ import { vi } from 'vitest'
*/
type MockSingletons = {
- CopilotAPI?: ReturnType
+ AssemblyAPI?: ReturnType
IntuitAPI?: ReturnType
}
const g = globalThis as typeof globalThis & {
__qbsync_test_mocks?: MockSingletons
}
g.__qbsync_test_mocks ??= {}
-g.__qbsync_test_mocks.CopilotAPI ??= vi.fn()
+g.__qbsync_test_mocks.AssemblyAPI ??= vi.fn()
g.__qbsync_test_mocks.IntuitAPI ??= vi.fn()
-vi.mock('@/utils/copilotAPI', () => ({
- CopilotAPI: g.__qbsync_test_mocks!.CopilotAPI!,
+vi.mock('@/utils/assemblyAPI', () => ({
+ AssemblyAPI: g.__qbsync_test_mocks!.AssemblyAPI!,
+}))
+
+// Token decode lives in a separate module (getAssemblyTokenPayload) so it stays
+// out of the wholesale-mocked AssemblyAPI. Plain fn — not vi.fn — so it
+// survives the clearAllMocks/restoreAllMocks that per-test setup helpers call.
+vi.mock('@/utils/assemblyTokenPayload', () => ({
+ getAssemblyTokenPayload: async () => ({
+ workspaceId: TEST_PORTAL_ID,
+ internalUserId: TEST_INTERNAL_USER_ID,
+ }),
}))
vi.mock('@/utils/intuitAPI', () => ({
diff --git a/test/unit/api/quickbooks/token/updateAccountRefs.test.ts b/test/unit/api/quickbooks/token/updateAccountRefs.test.ts
index 3a61e94e..640001f4 100644
--- a/test/unit/api/quickbooks/token/updateAccountRefs.test.ts
+++ b/test/unit/api/quickbooks/token/updateAccountRefs.test.ts
@@ -14,15 +14,15 @@ vi.mock('@/utils/logger', () => ({
default: { info: vi.fn(), error: vi.fn() },
}))
-// Stub IntuitAPI + CopilotAPI so importing TokenService doesn't transitively
+// Stub IntuitAPI + AssemblyAPI so importing TokenService doesn't transitively
// load copilot-node-sdk (which has an ESM directory-import that breaks under
// vitest). See docs/vitest-gotchas.md.
vi.mock('@/utils/intuitAPI', () => ({
default: vi.fn(),
IntuitAPIErrorMessage: '#IntuitAPIErrorMessage#',
}))
-vi.mock('@/utils/copilotAPI', () => ({
- CopilotAPI: vi.fn(),
+vi.mock('@/utils/assemblyAPI', () => ({
+ AssemblyAPI: vi.fn(),
}))
import { TokenService } from '@/app/api/quickbooks/token/token.service'
diff --git a/test/unit/api/quickbooks/webhook/handleInvoiceCreated.test.ts b/test/unit/api/quickbooks/webhook/handleInvoiceCreated.test.ts
index 7e0a9803..9f45954c 100644
--- a/test/unit/api/quickbooks/webhook/handleInvoiceCreated.test.ts
+++ b/test/unit/api/quickbooks/webhook/handleInvoiceCreated.test.ts
@@ -35,11 +35,11 @@ vi.mock('@/utils/auth', () => ({
refreshTokenExpireMessage: 'Refresh token is expired',
}))
-// `@/utils/copilotAPI` pulls in `copilot-node-sdk`, which has an ESM directory
+// `@/utils/assemblyAPI` pulls in `copilot-node-sdk`, which has an ESM directory
// import that breaks under Vitest's resolver. We don't exercise it here, so
// stub it out completely.
-vi.mock('@/utils/copilotAPI', () => ({
- CopilotAPI: vi.fn(),
+vi.mock('@/utils/assemblyAPI', () => ({
+ AssemblyAPI: vi.fn(),
}))
// `@/utils/intuitAPI` is imported transitively by `@/utils/error`
@@ -60,7 +60,7 @@ vi.mock('@/db', () => ({
// `new SyncLogService(...)` requires a constructable mock — `vi.fn()` arrow
// implementations aren't constructors, so we use a plain `function` factory.
// See `docs/vitest-gotchas.md` and the same pattern in
-// `test/integration/setup.ts` for IntuitAPI/CopilotAPI.
+// `test/integration/setup.ts` for IntuitAPI/AssemblyAPI.
const claimWebhookEvent = vi.fn()
const updateOrCreateQBSyncLog = vi.fn()
vi.mock('@/app/api/quickbooks/syncLog/syncLog.service', () => ({
diff --git a/test/unit/app/api/quickbooks/invoice/invoice.service.docNumber.test.ts b/test/unit/app/api/quickbooks/invoice/invoice.service.docNumber.test.ts
index a9b20aef..0611e939 100644
--- a/test/unit/app/api/quickbooks/invoice/invoice.service.docNumber.test.ts
+++ b/test/unit/app/api/quickbooks/invoice/invoice.service.docNumber.test.ts
@@ -32,8 +32,8 @@ vi.mock('@/utils/logger', () => ({
default: { info: vi.fn(), error: vi.fn() },
}))
-vi.mock('@/utils/copilotAPI', () => ({
- CopilotAPI: vi.fn(),
+vi.mock('@/utils/assemblyAPI', () => ({
+ AssemblyAPI: vi.fn(),
}))
vi.mock('@/utils/intuitAPI', () => ({
diff --git a/test/unit/quickbooks/syncErrorNotifier.test.ts b/test/unit/quickbooks/syncErrorNotifier.test.ts
index deb6c752..fd248ca9 100644
--- a/test/unit/quickbooks/syncErrorNotifier.test.ts
+++ b/test/unit/quickbooks/syncErrorNotifier.test.ts
@@ -4,7 +4,7 @@ import { NotificationActions } from '@/app/api/core/types/notification'
import { QBSyncLogSelectSchemaType } from '@/db/schema/qbSyncLogs'
// Stub Sentry + logger before importing the SUT — its transitive imports pull
-// in CopilotAPI/IntuitAPI which try to construct real SDK clients at import time.
+// in AssemblyAPI/IntuitAPI which try to construct real SDK clients at import time.
vi.mock('@sentry/nextjs', () => ({
withScope: vi.fn(),
captureMessage: vi.fn(),
@@ -69,6 +69,7 @@ const baseLog: QBSyncLogSelectSchemaType = {
errorMessage: 'Closed accounting period',
errorCode: String(QBOErrorCodes.CLOSED_PERIOD),
category: 'qb_api_error' as never,
+ shouldRetry: false,
attempt: 0,
createdAt: new Date(),
updatedAt: new Date(),
diff --git a/test/unit/utils/assemblyTokenPayload.test.ts b/test/unit/utils/assemblyTokenPayload.test.ts
new file mode 100644
index 00000000..e7e2af85
--- /dev/null
+++ b/test/unit/utils/assemblyTokenPayload.test.ts
@@ -0,0 +1,75 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+
+// Simulate the real @assembly-js/node-sdk v4 contract: assemblyApi() returns a
+// fresh, per-call client whose getTokenPayload() resolves the payload derived
+// from THIS call's token (captured in a closure), never shared global state.
+// A staggered delay inside getTokenPayload forces concurrent calls to resolve
+// out of order — so any cross-call state leak in getAssemblyTokenPayload would
+// surface as a mismatched workspaceId.
+vi.mock('@assembly-js/node-sdk', () => ({
+ assemblyApi: vi.fn(async ({ token }: { token: string }) => {
+ const payload = JSON.parse(token) as {
+ workspaceId: string
+ delayMs: number
+ }
+ return {
+ getTokenPayload: async () => {
+ await new Promise((resolve) => setTimeout(resolve, payload.delayMs))
+ return { workspaceId: payload.workspaceId }
+ },
+ }
+ }),
+}))
+
+import { assemblyApi } from '@assembly-js/node-sdk'
+import { getAssemblyTokenPayload } from '@/utils/assemblyTokenPayload'
+
+// In this test a "token" is just a JSON string carrying the workspace it belongs
+// to and how long its decode should take.
+const makeToken = (workspaceId: string, delayMs: number) =>
+ JSON.stringify({ workspaceId, delayMs })
+
+describe('getAssemblyTokenPayload — concurrency isolation', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('returns the matching workspaceId for a single call', async () => {
+ const payload = await getAssemblyTokenPayload(makeToken('ws-solo', 0))
+ expect(payload?.workspaceId).toBe('ws-solo')
+ })
+
+ it('keeps each workspace isolated across interleaved concurrent calls', async () => {
+ // 20 different workspaces, fired together. Earlier calls are given LONGER
+ // delays so completion order is the reverse of call order — maximising
+ // interleaving at both await points inside getTokenPayload.
+ const count = 20
+ const workspaceIds = Array.from({ length: count }, (_, i) => `ws-${i}`)
+
+ const results = await Promise.all(
+ workspaceIds.map((workspaceId, i) =>
+ getAssemblyTokenPayload(makeToken(workspaceId, (count - i) * 2)),
+ ),
+ )
+
+ // Every call must return its OWN workspace — no bleed between requests.
+ results.forEach((payload, i) => {
+ expect(payload?.workspaceId).toBe(workspaceIds[i])
+ })
+ })
+
+ it('builds one independent SDK per call (no shared client)', async () => {
+ await Promise.all([
+ getAssemblyTokenPayload(makeToken('ws-a', 6)),
+ getAssemblyTokenPayload(makeToken('ws-b', 3)),
+ getAssemblyTokenPayload(makeToken('ws-c', 0)),
+ ])
+
+ // assemblyApi is invoked once per call, each with that call's own token.
+ expect(assemblyApi).toHaveBeenCalledTimes(3)
+ const tokensSeen = (
+ assemblyApi as unknown as ReturnType
+ ).mock.calls.map(([arg]) => JSON.parse(arg.token).workspaceId)
+ expect(tokensSeen.sort()).toEqual(['ws-a', 'ws-b', 'ws-c'])
+ })
+})
diff --git a/yarn.lock b/yarn.lock
index 0953a0ca..df0ac9c4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -29,6 +29,13 @@ __metadata:
languageName: node
linkType: hard
+"@assembly-js/node-sdk@npm:^4.2.2":
+ version: 4.2.2
+ resolution: "@assembly-js/node-sdk@npm:4.2.2"
+ checksum: 10c0/e2f84680e5f5e7cbec5f95b2a17119896ce53f2db8b957a575dc57164826fd182105962383f9572435538c73e9f895b8df261c02b616c32171529a45fff95963
+ 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"
@@ -1730,13 +1737,6 @@ __metadata:
languageName: node
linkType: hard
-"@next/env@npm:14.1.3":
- version: 14.1.3
- resolution: "@next/env@npm:14.1.3"
- checksum: 10c0/928dba385f1ed3346880845662f87fe386e42ee0c798ccbd7c13a31e2cb509153d42e6c8a4a73ad9c6d647ac9cb6a570316c8ddcaccc9ec201f7519ec383dc02
- languageName: node
- linkType: hard
-
"@next/env@npm:15.5.18":
version: 15.5.18
resolution: "@next/env@npm:15.5.18"
@@ -1753,13 +1753,6 @@ __metadata:
languageName: node
linkType: hard
-"@next/swc-darwin-arm64@npm:14.1.3":
- version: 14.1.3
- resolution: "@next/swc-darwin-arm64@npm:14.1.3"
- conditions: os=darwin & cpu=arm64
- languageName: node
- linkType: hard
-
"@next/swc-darwin-arm64@npm:15.5.18":
version: 15.5.18
resolution: "@next/swc-darwin-arm64@npm:15.5.18"
@@ -1767,13 +1760,6 @@ __metadata:
languageName: node
linkType: hard
-"@next/swc-darwin-x64@npm:14.1.3":
- version: 14.1.3
- resolution: "@next/swc-darwin-x64@npm:14.1.3"
- conditions: os=darwin & cpu=x64
- languageName: node
- linkType: hard
-
"@next/swc-darwin-x64@npm:15.5.18":
version: 15.5.18
resolution: "@next/swc-darwin-x64@npm:15.5.18"
@@ -1781,13 +1767,6 @@ __metadata:
languageName: node
linkType: hard
-"@next/swc-linux-arm64-gnu@npm:14.1.3":
- version: 14.1.3
- resolution: "@next/swc-linux-arm64-gnu@npm:14.1.3"
- conditions: os=linux & cpu=arm64 & libc=glibc
- languageName: node
- linkType: hard
-
"@next/swc-linux-arm64-gnu@npm:15.5.18":
version: 15.5.18
resolution: "@next/swc-linux-arm64-gnu@npm:15.5.18"
@@ -1795,13 +1774,6 @@ __metadata:
languageName: node
linkType: hard
-"@next/swc-linux-arm64-musl@npm:14.1.3":
- version: 14.1.3
- resolution: "@next/swc-linux-arm64-musl@npm:14.1.3"
- conditions: os=linux & cpu=arm64 & libc=musl
- languageName: node
- linkType: hard
-
"@next/swc-linux-arm64-musl@npm:15.5.18":
version: 15.5.18
resolution: "@next/swc-linux-arm64-musl@npm:15.5.18"
@@ -1809,13 +1781,6 @@ __metadata:
languageName: node
linkType: hard
-"@next/swc-linux-x64-gnu@npm:14.1.3":
- version: 14.1.3
- resolution: "@next/swc-linux-x64-gnu@npm:14.1.3"
- conditions: os=linux & cpu=x64 & libc=glibc
- languageName: node
- linkType: hard
-
"@next/swc-linux-x64-gnu@npm:15.5.18":
version: 15.5.18
resolution: "@next/swc-linux-x64-gnu@npm:15.5.18"
@@ -1823,13 +1788,6 @@ __metadata:
languageName: node
linkType: hard
-"@next/swc-linux-x64-musl@npm:14.1.3":
- version: 14.1.3
- resolution: "@next/swc-linux-x64-musl@npm:14.1.3"
- conditions: os=linux & cpu=x64 & libc=musl
- languageName: node
- linkType: hard
-
"@next/swc-linux-x64-musl@npm:15.5.18":
version: 15.5.18
resolution: "@next/swc-linux-x64-musl@npm:15.5.18"
@@ -1837,13 +1795,6 @@ __metadata:
languageName: node
linkType: hard
-"@next/swc-win32-arm64-msvc@npm:14.1.3":
- version: 14.1.3
- resolution: "@next/swc-win32-arm64-msvc@npm:14.1.3"
- conditions: os=win32 & cpu=arm64
- languageName: node
- linkType: hard
-
"@next/swc-win32-arm64-msvc@npm:15.5.18":
version: 15.5.18
resolution: "@next/swc-win32-arm64-msvc@npm:15.5.18"
@@ -1851,20 +1802,6 @@ __metadata:
languageName: node
linkType: hard
-"@next/swc-win32-ia32-msvc@npm:14.1.3":
- version: 14.1.3
- resolution: "@next/swc-win32-ia32-msvc@npm:14.1.3"
- conditions: os=win32 & cpu=ia32
- languageName: node
- linkType: hard
-
-"@next/swc-win32-x64-msvc@npm:14.1.3":
- version: 14.1.3
- resolution: "@next/swc-win32-x64-msvc@npm:14.1.3"
- conditions: os=win32 & cpu=x64
- languageName: node
- linkType: hard
-
"@next/swc-win32-x64-msvc@npm:15.5.18":
version: 15.5.18
resolution: "@next/swc-win32-x64-msvc@npm:15.5.18"
@@ -3935,15 +3872,6 @@ __metadata:
languageName: node
linkType: hard
-"@swc/helpers@npm:0.5.2":
- version: 0.5.2
- resolution: "@swc/helpers@npm:0.5.2"
- dependencies:
- tslib: "npm:^2.4.0"
- checksum: 10c0/b6fa49bcf6c00571d0eb7837b163f8609960d4d77538160585e27ed167361e9776bd6e5eb9646ffac2fb4d43c58df9ca50dab9d96ab097e6591bc82a75fd1164
- languageName: node
- linkType: hard
-
"@tailwindcss/node@npm:4.1.5":
version: 4.1.5
resolution: "@tailwindcss/node@npm:4.1.5"
@@ -5570,15 +5498,6 @@ __metadata:
languageName: node
linkType: hard
-"busboy@npm:1.6.0":
- version: 1.6.0
- resolution: "busboy@npm:1.6.0"
- dependencies:
- streamsearch: "npm:^1.1.0"
- checksum: 10c0/fa7e836a2b82699b6e074393428b91ae579d4f9e21f5ac468e1b459a244341d722d2d22d10920cdd849743dbece6dca11d72de939fb75a7448825cf2babfba1f
- languageName: node
- linkType: hard
-
"byline@npm:^5.0.0":
version: 5.0.0
resolution: "byline@npm:5.0.0"
@@ -6075,16 +5994,6 @@ __metadata:
languageName: node
linkType: hard
-"copilot-node-sdk@npm:~3.16.0":
- version: 3.16.0
- resolution: "copilot-node-sdk@npm:3.16.0"
- dependencies:
- isomorphic-fetch: "npm:^3.0.0"
- next: "npm:^14.0.2"
- checksum: 10c0/6ac9cbdf0c61d78bfb758f13eca7fc9b290e1d48d32acf22499a3d196f650acc1f1f022c61eac30848acb63db052ed03e29210fb0f4f9309676bebb4c8d604c4
- languageName: node
- linkType: hard
-
"core-js@npm:^3.49.0":
version: 3.49.0
resolution: "core-js@npm:3.49.0"
@@ -6223,6 +6132,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "custom-app-base@workspace:."
dependencies:
+ "@assembly-js/node-sdk": "npm:^4.2.2"
"@eslint/eslintrc": "npm:^3.3.1"
"@eslint/js": "npm:^9.24.0"
"@ngrok/ngrok": "npm:^1.4.1"
@@ -6243,7 +6153,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"
@@ -8648,7 +8557,7 @@ __metadata:
languageName: node
linkType: hard
-"graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
+"graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
version: 4.2.11
resolution: "graceful-fs@npm:4.2.11"
checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2
@@ -9524,16 +9433,6 @@ __metadata:
languageName: node
linkType: hard
-"isomorphic-fetch@npm:^3.0.0":
- version: 3.0.0
- resolution: "isomorphic-fetch@npm:3.0.0"
- dependencies:
- node-fetch: "npm:^2.6.1"
- whatwg-fetch: "npm:^3.4.1"
- checksum: 10c0/511b1135c6d18125a07de661091f5e7403b7640060355d2d704ce081e019bc1862da849482d079ce5e2559b8976d3de7709566063aec1b908369c0b98a2b075b
- languageName: node
- linkType: hard
-
"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.2":
version: 3.2.2
resolution: "istanbul-lib-coverage@npm:3.2.2"
@@ -10778,61 +10677,6 @@ __metadata:
languageName: node
linkType: hard
-"next@npm:^14.0.2":
- version: 14.1.3
- resolution: "next@npm:14.1.3"
- dependencies:
- "@next/env": "npm:14.1.3"
- "@next/swc-darwin-arm64": "npm:14.1.3"
- "@next/swc-darwin-x64": "npm:14.1.3"
- "@next/swc-linux-arm64-gnu": "npm:14.1.3"
- "@next/swc-linux-arm64-musl": "npm:14.1.3"
- "@next/swc-linux-x64-gnu": "npm:14.1.3"
- "@next/swc-linux-x64-musl": "npm:14.1.3"
- "@next/swc-win32-arm64-msvc": "npm:14.1.3"
- "@next/swc-win32-ia32-msvc": "npm:14.1.3"
- "@next/swc-win32-x64-msvc": "npm:14.1.3"
- "@swc/helpers": "npm:0.5.2"
- busboy: "npm:1.6.0"
- caniuse-lite: "npm:^1.0.30001579"
- graceful-fs: "npm:^4.2.11"
- postcss: "npm:8.4.31"
- styled-jsx: "npm:5.1.1"
- peerDependencies:
- "@opentelemetry/api": ^1.1.0
- react: ^18.2.0
- react-dom: ^18.2.0
- sass: ^1.3.0
- dependenciesMeta:
- "@next/swc-darwin-arm64":
- optional: true
- "@next/swc-darwin-x64":
- optional: true
- "@next/swc-linux-arm64-gnu":
- optional: true
- "@next/swc-linux-arm64-musl":
- optional: true
- "@next/swc-linux-x64-gnu":
- optional: true
- "@next/swc-linux-x64-musl":
- optional: true
- "@next/swc-win32-arm64-msvc":
- optional: true
- "@next/swc-win32-ia32-msvc":
- optional: true
- "@next/swc-win32-x64-msvc":
- optional: true
- peerDependenciesMeta:
- "@opentelemetry/api":
- optional: true
- sass:
- optional: true
- bin:
- next: dist/bin/next
- checksum: 10c0/b723955669b40b49761220b582e46ee0fb472b01b67fb9b6ceabc9a191a252bba253a65d72b284fcac1001c85d39bd6816db71e78e97b2221137ed15776c35bd
- languageName: node
- linkType: hard
-
"node-domexception@npm:^1.0.0":
version: 1.0.0
resolution: "node-domexception@npm:1.0.0"
@@ -10847,7 +10691,7 @@ __metadata:
languageName: node
linkType: hard
-"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7":
+"node-fetch@npm:^2.6.7":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
@@ -13113,13 +12957,6 @@ __metadata:
languageName: node
linkType: hard
-"streamsearch@npm:^1.1.0":
- version: 1.1.0
- resolution: "streamsearch@npm:1.1.0"
- checksum: 10c0/fbd9aecc2621364384d157f7e59426f4bfd385e8b424b5aaa79c83a6f5a1c8fd2e4e3289e95de1eb3511cb96bb333d6281a9919fafce760e4edb35b2cd2facab
- languageName: node
- linkType: hard
-
"streamx@npm:^2.12.5, streamx@npm:^2.15.0, streamx@npm:^2.25.0":
version: 2.25.0
resolution: "streamx@npm:2.25.0"
@@ -13369,22 +13206,6 @@ __metadata:
languageName: node
linkType: hard
-"styled-jsx@npm:5.1.1":
- version: 5.1.1
- resolution: "styled-jsx@npm:5.1.1"
- dependencies:
- client-only: "npm:0.0.1"
- peerDependencies:
- react: ">= 16.8.0 || 17.x.x || ^18.0.0-0"
- peerDependenciesMeta:
- "@babel/core":
- optional: true
- babel-plugin-macros:
- optional: true
- checksum: 10c0/42655cdadfa5388f8a48bb282d6b450df7d7b8cf066ac37038bd0499d3c9f084815ebd9ff9dfa12a218fd4441338851db79603498d7557207009c1cf4d609835
- languageName: node
- linkType: hard
-
"styled-jsx@npm:5.1.6":
version: 5.1.6
resolution: "styled-jsx@npm:5.1.6"
@@ -14342,13 +14163,6 @@ __metadata:
languageName: node
linkType: hard
-"whatwg-fetch@npm:^3.4.1":
- version: 3.6.20
- resolution: "whatwg-fetch@npm:3.6.20"
- checksum: 10c0/fa972dd14091321d38f36a4d062298df58c2248393ef9e8b154493c347c62e2756e25be29c16277396046d6eaa4b11bd174f34e6403fff6aaca9fb30fa1ff46d
- languageName: node
- linkType: hard
-
"whatwg-url@npm:^5.0.0":
version: 5.0.0
resolution: "whatwg-url@npm:5.0.0"