From 8bd079362944d31e02e1fa859d94f992b28666ff Mon Sep 17 00:00:00 2001 From: priosshrsth Date: Sun, 29 Mar 2026 10:26:25 +0000 Subject: [PATCH] feat: bundle SDK, add isMarketplaceApp flag, per-instance Proxy - Set unbundle: false to inline @assembly-js/node-sdk into dist - Add src/sdk-init.ts with per-instance Proxy that sets OpenAPI.HEADERS before every method call (fixes multi-instance singleton issue) - Add isMarketplaceApp option to AssemblyKitOptions - Remove @assembly-js/node-sdk from peerDependencies (bundled as devDep) - Replace AssemblyAPI type with AssemblySDK from sdk-init - Remove ASSEMBLY_ENV=local hack from constructor Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 1 - pnpm-lock.yaml | 42 ++++++++++++++++++++---------- src/client/base-resource.ts | 8 +++--- src/client/index.ts | 29 +++++++++++---------- src/client/options.ts | 16 ++++++++---- src/sdk-init.ts | 51 +++++++++++++++++++++++++++++++++++++ vite.config.ts | 2 +- 7 files changed, 111 insertions(+), 38 deletions(-) create mode 100644 src/sdk-init.ts diff --git a/package.json b/package.json index 6a85ce1..0132061 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ }, "peerDependencies": { "@assembly-js/app-bridge": "^1.1.1", - "@assembly-js/node-sdk": "^3.19.1", "pino": ">=9", "pino-pretty": ">=10", "react": ">=18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4201f31..8bffbd8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,7 +21,7 @@ importers: version: 1.1.1 '@assembly-js/node-sdk': specifier: ^3.19.1 - version: 3.19.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 3.19.1(react-dom@18.3.1(react@19.2.4))(react@19.2.4) '@types/node': specifier: ^25.5.0 version: 25.5.0 @@ -822,6 +822,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} @@ -899,6 +902,10 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1027,10 +1034,10 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} - react-dom@19.2.4: - resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: - react: ^19.2.4 + react: ^18.3.1 react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} @@ -1049,8 +1056,8 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} @@ -1239,10 +1246,10 @@ snapshots: '@assembly-js/app-bridge@1.1.1': {} - '@assembly-js/node-sdk@3.19.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@assembly-js/node-sdk@3.19.1(react-dom@18.3.1(react@19.2.4))(react@19.2.4)': dependencies: isomorphic-fetch: 3.0.0 - next: 14.2.35(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 14.2.35(react-dom@18.3.1(react@19.2.4))(react@19.2.4) transitivePeerDependencies: - '@babel/core' - '@opentelemetry/api' @@ -1714,6 +1721,8 @@ snapshots: joycon@3.1.1: {} + js-tokens@4.0.0: {} + jsonc-parser@3.3.1: {} lightningcss-android-arm64@1.32.0: @@ -1765,13 +1774,17 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + minimist@1.2.8: {} mrmime@2.0.1: {} nanoid@3.3.11: {} - next@14.2.35(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@14.2.35(react-dom@18.3.1(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 14.2.35 '@swc/helpers': 0.5.5 @@ -1780,7 +1793,7 @@ snapshots: graceful-fs: 4.2.11 postcss: 8.4.31 react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react-dom: 18.3.1(react@19.2.4) styled-jsx: 5.1.1(react@19.2.4) optionalDependencies: '@next/swc-darwin-arm64': 14.2.33 @@ -1941,10 +1954,11 @@ snapshots: quick-format-unescaped@4.0.4: {} - react-dom@19.2.4(react@19.2.4): + react-dom@18.3.1(react@19.2.4): dependencies: + loose-envify: 1.4.0 react: 19.2.4 - scheduler: 0.27.0 + scheduler: 0.23.2 react@19.2.4: {} @@ -1973,7 +1987,9 @@ snapshots: safe-stable-stringify@2.5.0: {} - scheduler@0.27.0: {} + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 secure-json-parse@4.1.0: {} diff --git a/src/client/base-resource.ts b/src/client/base-resource.ts index dbac110..6da496d 100644 --- a/src/client/base-resource.ts +++ b/src/client/base-resource.ts @@ -1,5 +1,5 @@ -import type { AssemblyAPI } from "@assembly-js/node-sdk"; import pRetry from "p-retry"; +import type { AssemblySDK } from "src/sdk-init"; import type { z } from "zod"; import type { RetryOptions } from "./options"; @@ -17,7 +17,7 @@ const isRetryableError = (error: unknown): boolean => { * Wraps an SDK instance with a Proxy that retries every method call * on 429/5xx errors using p-retry. */ -const withRetry = (sdk: AssemblyAPI, retry: RetryOptions): AssemblyAPI => +const withRetry = (sdk: AssemblySDK, retry: RetryOptions): AssemblySDK => new Proxy(sdk, { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver); @@ -36,10 +36,10 @@ const withRetry = (sdk: AssemblyAPI, retry: RetryOptions): AssemblyAPI => }); export abstract class BaseResource { - protected readonly sdk: AssemblyAPI; + protected readonly sdk: AssemblySDK; protected readonly validateResponses: boolean; - constructor(sdk: AssemblyAPI, validateResponses: boolean, retry: RetryOptions | false) { + constructor(sdk: AssemblySDK, validateResponses: boolean, retry: RetryOptions | false) { this.sdk = retry === false ? sdk : withRetry(sdk, retry); this.validateResponses = validateResponses; } diff --git a/src/client/index.ts b/src/client/index.ts index 938590f..b678eda 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,5 +1,5 @@ -import { assemblyApi } from "@assembly-js/node-sdk"; import { AssemblyNoTokenError } from "src/errors/no-token"; +import { initSdk } from "src/sdk-init"; import type { TokenPayload } from "src/schemas/shared/token"; import { AssemblyToken } from "src/token/assembly-token"; import type { ClientTokenPayload, InternalUserTokenPayload } from "src/token/assembly-token"; @@ -99,34 +99,35 @@ export class AssemblyKit { readonly workspace: WorkspaceResource; constructor(options: AssemblyKitOptions) { - if (!options.token && !options.workspaceId) { + const isMarketplace: boolean = options.isMarketplaceApp ?? false; + + if (isMarketplace && !options.token) { throw new AssemblyNoTokenError({ - message: "Either `token` or `workspaceId` must be provided.", + message: "Marketplace apps require a `token`. Provide an encrypted token from Assembly.", + }); + } + if (!isMarketplace && !options.token && !options.workspaceId) { + throw new AssemblyNoTokenError({ + message: "`workspaceId` is required when `token` is not provided.", }); } this.currentToken = options.token; if (options.token) { - // Token mode: decrypt token to get payload + let the underlying SDK build the compound key. this.token = new AssemblyToken({ token: options.token, apiKey: options.apiKey }); this.payload = this.token.payload; } else { - // Workspace-only mode: no token to decrypt. this.token = undefined; this.payload = undefined; } - // When no token is provided, set ASSEMBLY_ENV=local so the underlying SDK - // accepts the raw apiKey, then pass the compound key (workspaceId/apiKey) - // as the apiKey directly. - let sdkApiKey: string = options.apiKey; - if (!options.token) { - process.env.ASSEMBLY_ENV = "local"; - sdkApiKey = `${options.workspaceId}/${options.apiKey}`; - } + // Build compound key ourselves — no ASSEMBLY_ENV hack, no upstream init.js. + const compoundKey: string = this.token + ? this.token.buildCompoundKey({ apiKey: options.apiKey }) + : `${options.workspaceId}/${options.apiKey}`; - const sdk = assemblyApi({ apiKey: sdkApiKey, token: options.token }); + const sdk = initSdk({ compoundKey }); const validate = options.validateResponses ?? true; const retry = options.retry === false ? false : { ...DEFAULT_RETRY, ...options.retry }; diff --git a/src/client/options.ts b/src/client/options.ts index c804d63..2344bae 100644 --- a/src/client/options.ts +++ b/src/client/options.ts @@ -20,17 +20,23 @@ export const DEFAULT_RETRY: RetryOptions = { /** * Options for `createAssemblyKit()`. * - * At least one of `token` or `workspaceId` must be provided at runtime. - * If both are provided, `token` takes precedence (workspaceId is ignored). - * If only `workspaceId` is provided, the compound key is built as `workspaceId/apiKey`. + * - `isMarketplaceApp: true` → `token` is required (marketplace apps receive it from the platform). + * - `isMarketplaceApp: false` (default) → `workspaceId` is required; `token` is optional. + * - When both `token` and `workspaceId` are provided, `token` takes precedence. */ export interface AssemblyKitOptions { /** Assembly API key. */ apiKey: string; - /** Encrypted token from Assembly. Takes precedence over `workspaceId` when provided. */ + /** Encrypted token from Assembly. Required when `isMarketplaceApp` is `true`. */ token?: string; - /** Workspace ID. Required when `token` is not provided. Ignored when `token` is present. */ + /** Workspace ID. Required when `isMarketplaceApp` is `false` and `token` is not provided. */ workspaceId?: string; + /** + * When `true`, `token` is required at construction time (marketplace apps). + * When `false` (default), `workspaceId` is required instead. + * @default false + */ + isMarketplaceApp?: boolean; /** Retry configuration, or `false` to disable retry entirely. */ retry?: RetryOptions | false; /** When true, all responses are validated through Zod schemas. Default: true. */ diff --git a/src/sdk-init.ts b/src/sdk-init.ts new file mode 100644 index 0000000..6a3315a --- /dev/null +++ b/src/sdk-init.ts @@ -0,0 +1,51 @@ +/** + * Custom SDK initializer that wraps `@assembly-js/node-sdk`. + * + * Calls `assemblyApi()` once with a dummy key (under ASSEMBLY_ENV=local) to + * obtain the `DefaultService` reference + `sendWebhook`. Then wraps it in a + * Proxy that sets `OpenAPI.HEADERS` to this instance's compound key before + * every method call — making multiple `createAssemblyKit()` instances safe + * in the same process. + * + * With `unbundle: false` in vite.config.ts, the SDK is inlined into + * assembly-kit's dist. Consumers never need to install `@assembly-js/node-sdk`. + */ + +import { type AssemblyAPI, OpenAPI, assemblyApi } from "@assembly-js/node-sdk"; + +const SDK_VERSION: string = "3.19.1"; + +/** The underlying SDK type returned by `assemblyApi()`. */ +export type AssemblySDK = AssemblyAPI; + +// Obtain the DefaultService reference once. We pass a placeholder key and +// set ASSEMBLY_ENV=local so init.js skips token decryption. The headers it +// sets are immediately overwritten per-instance via the Proxy. +const _prevEnv: string | undefined = process.env.ASSEMBLY_ENV; +process.env.ASSEMBLY_ENV = "local"; +const _baseSdk: AssemblyAPI = assemblyApi({ apiKey: "__placeholder__" }); +process.env.ASSEMBLY_ENV = _prevEnv; + +/** + * Create a per-instance SDK wrapper that sets `OpenAPI.HEADERS` to this + * instance's compound key before every method call. + * + * JS is single-threaded, so the header is always correct when the fetch fires. + */ +export function initSdk({ compoundKey }: { compoundKey: string }): AssemblySDK { + const headers: Record = { + "X-API-Key": compoundKey, + "X-Assembly-SDK-Version": SDK_VERSION, + }; + + return new Proxy(_baseSdk, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + if (typeof value !== "function") return value; + return (...args: unknown[]) => { + OpenAPI.HEADERS = headers; + return (value as (...a: unknown[]) => unknown).apply(target, args); + }; + }, + }); +} diff --git a/vite.config.ts b/vite.config.ts index 7a26ef2..a1b3114 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -21,7 +21,7 @@ export default defineConfig({ "src/schemas/index.ts", "src/token/index.ts", ], - unbundle: true, + unbundle: false, exports: true, sourcemap: true, dts: {