Skip to content

OUT-4067: migrate to assembly-js node sdk - #74

Merged
SandipBajracharya merged 4 commits into
mainfrom
OUT-4067
Aug 24, 2026
Merged

OUT-4067: migrate to assembly-js node sdk#74
SandipBajracharya merged 4 commits into
mainfrom
OUT-4067

Conversation

@SandipBajracharya

Copy link
Copy Markdown
Collaborator

Summary

Replaces the deprecated copilot-node-sdk with @assembly-js/node-sdk.

The new SDK enforces request-token expiry, which breaks long-running syncs (invoice sync especially). To avoid that, CopilotAPI is scoped by the workspace API key (workspaceId/apiKey) and passes no request token — the workspace key does not expire. The request token is only used briefly at auth time to decode the workspace payload.

Changes

  • CopilotAPI: instantiate @assembly-js/node-sdk scoped by workspaceId/apiKey; create the client lazily and reuse it per instance.
  • Auth: decode the token via getAssemblyTokenPayload (retried, returns null on failure → typed CopilotInvalidTokenError); removed the now-dead constructor try/catch and the redundant second decode on the home page (use user.portalId).
  • Notifications: deliveryTargets and email header/subject/title are now required, matching the SDK's types.
  • Removed the unused copilot-node-sdk dependency.

Cross-portal safety

The SDK isolates per-request credentials via AsyncLocalStorage, so concurrent portals can't bleed state on shared serverless instances (Fluid Compute) or persistent workers (trigger.dev). Every code path — webhook, home page, and the batch retry — derives workspaceId from its own token; nothing is shared across portals.

New regression tests lock this in:

  • each portal builds a CopilotAPI scoped to its own workspaceId;
  • failed-sync retries hit each portal's own Xero tenant, never another's (verified to fail when a shared-workspace leak is injected).

Testing

  • pnpm typecheck
  • pnpm lint
  • pnpm test ✅ 63/63 (6× clean full-suite runs — no flakiness)
  • pnpm build

🤖 Generated with Claude Code

SandipBajracharya and others added 2 commits August 19, 2026 21:28
Replace deprecated copilot-node-sdk with @assembly-js/node-sdk.

- Scope CopilotAPI by workspace key (workspaceId/apiKey) with no request
  token, so long invoice syncs don't fail on token expiry
- Create the SDK client lazily and reuse it per instance
- Decode the request token via getAssemblyTokenPayload; retry and return
  null on failure so auth maps it to a typed error
- Drop the dead constructor try/catch and the redundant token decode in
  the home page (use user.portalId)
- Require deliveryTargets/email fields on notifications to match the SDK

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Mock the Assembly SDK to decode tokens (or fall back to the seeded
  portal), keeping its broken ESM build out of the test graph
- Assert each portal builds a CopilotAPI scoped to its own workspaceId
- Assert failed-sync retries hit each portal's own Xero tenant, never
  another portal's

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 19, 2026

Copy link
Copy Markdown

OUT-4067

@supabase

supabase Bot commented Aug 19, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project pkdwtcdqcefmlgxmcwmc because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
xero-integration Ready Ready Preview Aug 24, 2026 8:40am

Request Review

@greptile-apps

greptile-apps Bot commented Aug 19, 2026

Copy link
Copy Markdown

Greptile Summary

The PR migrates the Copilot integration from the deprecated SDK to @assembly-js/node-sdk and scopes API clients using workspace credentials instead of expiring request tokens.

  • Lazily creates and reuses a workspace-scoped Assembly client.
  • Decodes request tokens during authentication to establish workspace identity.
  • Updates home-page, service, and failed-sync paths to use the authenticated portal ID.
  • Aligns notification types with the new SDK and adds cross-portal isolation tests.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
src/lib/copilot/CopilotAPI.ts Replaces the legacy token-scoped SDK client with a lazy, memoized Assembly client scoped by workspace credentials.
src/lib/copilot/models/User.model.ts Authenticates by decoding the request token first and constructs the API client from the resulting workspace ID.
src/lib/copilot/utils.ts Adds retried Assembly token-payload decoding with typed validation and invalid-token handling.
src/config/server.env.ts Validates and normalizes the SDK environment mode required for tokenless workspace authorization.
src/features/failed-syncs/lib/RetryFailedSyncs.service.ts Scopes legacy failed-sync product lookups to the workspace decoded from each record's token.
test/integration/crossPortalIsolation.test.ts Adds regression coverage showing that authentication and failed-sync retries preserve portal and Xero-tenant isolation.

Sequence Diagram

sequenceDiagram
  participant R as Request or Retry
  participant U as User.authenticate
  participant A as Assembly SDK
  participant C as CopilotAPI
  participant X as Workspace API
  R->>U: Request token
  U->>A: Decode token payload
  A-->>U: workspaceId and internalUserId
  U->>C: Construct with workspaceId
  C->>A: Lazily initialize workspace-scoped client
  C->>X: Perform workspace operation
  X-->>C: Validated resource response
Loading

Reviews (3): Last reviewed commit: "refactor(OUT-4067): fold env-mode preced..." | Re-trigger Greptile

The workspace-scoped keyless auth path (new CopilotAPI(portalId)) only
authorizes when ASSEMBLY_ENV/COPILOT_ENV is a keyless mode; otherwise the
SDK throws at the first API call. Validate it at boot so a misconfigured
runtime fails fast with a clear message instead of 500ing every request.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@SandipBajracharya

Copy link
Copy Markdown
Collaborator Author

@greptileai review PR

@SandipBajracharya SandipBajracharya changed the title feat(OUT-4067): migrate to assembly-js node sdk OUT-4067: migrate to assembly-js node sdk Aug 24, 2026
Comment thread src/config/server.env.ts Outdated
Comment on lines +19 to +20
ASSEMBLY_ENV: z.string().optional(),
COPILOT_ENV: z.string().optional(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just use ASSEMBLY_ENV here. and use z.enum or nativeEnum to limit the values. No need for superRefine.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — collapsed it to a single ASSEMBLY_ENV field with z.enum and dropped the superRefine.

Comment thread src/config/server.env.ts Outdated
}
})

const env = ServerEnvSchema.parse(process.env)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const env = ServerEnvSchema.parse(process.env)
const env = ServerEnvSchema.parse({...process.env, ASSEMBLY_ENV: process.env.ASSEMBLY_ENV || process.env.COPILOT_ENV})

I suggest we set the priority based on the code in assembly sdk. Otherwise relying on two envs might cause some discrepancy.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied this. One tweak: went with ?? instead of || to match the SDK’s getEnvMode exactly — with || an empty-string ASSEMBLY_ENV would silently fall back to COPILOT_ENV here while the SDK would still reject it.

Merge ASSEMBLY_ENV/COPILOT_ENV once at parse time (matching the SDK's
getEnvMode) and validate the single value with z.enum, replacing the
cross-field superRefine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@SandipBajracharya

Copy link
Copy Markdown
Collaborator Author

@greptileai review changes

@priosshrsth priosshrsth left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@SandipBajracharya
SandipBajracharya merged commit 80d47cc into main Aug 24, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants