diff --git a/.changeset/bind-default-fetch.md b/.changeset/bind-default-fetch.md new file mode 100644 index 0000000..36cc9de --- /dev/null +++ b/.changeset/bind-default-fetch.md @@ -0,0 +1,5 @@ +--- +'@passmint/node': patch +--- + +Bind the default `fetch` to `globalThis` so the SDK works on Cloudflare Workers. The client stored the bare `globalThis.fetch` and invoked it as a method (`this.fetchImpl(...)`); workerd requires `fetch` to be called with its own global as the receiver, so every request threw `TypeError: Illegal invocation` unless a custom `fetch` was passed in options. Node's undici is not receiver-sensitive, which is why tests never caught it. diff --git a/src/client.ts b/src/client.ts index 39dea64..53ebc54 100644 --- a/src/client.ts +++ b/src/client.ts @@ -82,7 +82,12 @@ export class PassmintHttpClient { this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES - this.fetchImpl = options.fetch ?? globalThis.fetch + // Bind the default fetch to its own global: we invoke it as + // `this.fetchImpl(...)`, and receiver-sensitive runtimes (Cloudflare's + // workerd) throw "TypeError: Illegal invocation" when fetch is called + // with any other `this`. Node's undici doesn't care, so only Workers + // users ever saw it. + this.fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis) if (!this.fetchImpl) { throw new PassmintError( 'global fetch is not available; pass a fetch implementation in options.fetch', diff --git a/test/client.test.ts b/test/client.test.ts index af003d7..21d577a 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -54,6 +54,33 @@ describe('PassmintHttpClient constructor', () => { }) }) +describe('default fetch binding', () => { + const originalFetch = globalThis.fetch + + afterEach(() => { + globalThis.fetch = originalFetch + }) + + it('invokes the default global fetch without a foreign receiver (workerd compatibility)', async () => { + // Cloudflare's workerd throws "Illegal invocation" when fetch is called + // with anything other than its own global as `this`. Node's undici does + // not care, so reproduce workerd's strictness in the stub — if the + // client ever stores the default fetch unbound again, this test fails. + globalThis.fetch = async function (this: unknown, ...args: Parameters) { + void args + if (this !== undefined && this !== globalThis) { + throw new TypeError('Illegal invocation') + } + return new Response(JSON.stringify({ ok: true }), { status: 200 }) + } as typeof fetch + + const client = new PassmintHttpClient({ apiKey: 'pmk_test_1', maxRetries: 0 }) + await expect(client.request({ method: 'GET', path: '/v1/me' })).resolves.toEqual({ + ok: true, + }) + }) +}) + describe('VERSION', () => { it('matches package.json (regenerate with: node scripts/sync-version.mjs)', () => { const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))