diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f059e775..ad225a93 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - next workflow_dispatch: jobs: diff --git a/.releaserc.json b/.releaserc.json index d33de4bb..d9a435e6 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -1,4 +1,5 @@ { + "branches": [{ "name": "master" }, { "name": "next", "channel": "next", "prerelease": true }], "plugins": [ [ "semantic-release-plugin-update-version-in-files", diff --git a/package-lock.json b/package-lock.json index 2d5b1614..83774333 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,11 @@ "license": "MIT", "dependencies": { "@supabase/functions-js": "^1.3.3", - "@supabase/gotrue-js": "^1.22.14", + "@supabase/gotrue-js": "^1.23.0-next.1", "@supabase/postgrest-js": "^0.37.2", "@supabase/realtime-js": "^1.7.2", - "@supabase/storage-js": "^1.7.0" + "@supabase/storage-js": "^1.7.0", + "cross-fetch": "^3.1.5" }, "devDependencies": { "@types/jest": "^26.0.13", @@ -918,9 +919,9 @@ } }, "node_modules/@supabase/gotrue-js": { - "version": "1.22.14", - "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.22.14.tgz", - "integrity": "sha512-/LeNV9BFJ9hxzK6KcmJsOF3apxSYd7HlZOD9dd9a3+Ah0cIMmjlFloW/u4vJXN7ko+Ol3EUWJ6iHcgNoT4gvTw==", + "version": "1.23.0-next.1", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.23.0-next.1.tgz", + "integrity": "sha512-f+jRgR+MKfB+JVZKbZ9C9dsN7Ri40Os0wwliWDev6aqipYgSCeeS3I8h7QoZgMWerXIMy/UQMIRluZvOP1zN1g==", "dependencies": { "cross-fetch": "^3.0.6" } @@ -9096,9 +9097,9 @@ } }, "@supabase/gotrue-js": { - "version": "1.22.14", - "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.22.14.tgz", - "integrity": "sha512-/LeNV9BFJ9hxzK6KcmJsOF3apxSYd7HlZOD9dd9a3+Ah0cIMmjlFloW/u4vJXN7ko+Ol3EUWJ6iHcgNoT4gvTw==", + "version": "1.23.0-next.1", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.23.0-next.1.tgz", + "integrity": "sha512-f+jRgR+MKfB+JVZKbZ9C9dsN7Ri40Os0wwliWDev6aqipYgSCeeS3I8h7QoZgMWerXIMy/UQMIRluZvOP1zN1g==", "requires": { "cross-fetch": "^3.0.6" } diff --git a/package.json b/package.json index d25c94f1..2ef4fc31 100644 --- a/package.json +++ b/package.json @@ -38,10 +38,11 @@ }, "dependencies": { "@supabase/functions-js": "^1.3.3", - "@supabase/gotrue-js": "^1.22.14", + "@supabase/gotrue-js": "^1.23.0-next.1", "@supabase/postgrest-js": "^0.37.2", "@supabase/realtime-js": "^1.7.2", - "@supabase/storage-js": "^1.7.0" + "@supabase/storage-js": "^1.7.0", + "cross-fetch": "^3.1.5" }, "devDependencies": { "@types/jest": "^26.0.13", diff --git a/src/SupabaseClient.ts b/src/SupabaseClient.ts index 5e12c835..e22dca42 100644 --- a/src/SupabaseClient.ts +++ b/src/SupabaseClient.ts @@ -1,18 +1,19 @@ -import { DEFAULT_HEADERS, STORAGE_KEY } from './lib/constants' -import { stripTrailingSlash, isBrowser } from './lib/helpers' -import { Fetch, GenericObject, SupabaseClientOptions } from './lib/types' -import { SupabaseAuthClient } from './lib/SupabaseAuthClient' -import { SupabaseQueryBuilder } from './lib/SupabaseQueryBuilder' -import { SupabaseStorageClient } from '@supabase/storage-js' import { FunctionsClient } from '@supabase/functions-js' -import { PostgrestClient } from '@supabase/postgrest-js' import { AuthChangeEvent } from '@supabase/gotrue-js' +import { PostgrestClient } from '@supabase/postgrest-js' import { + RealtimeChannel, RealtimeClient, - RealtimeSubscription, RealtimeClientOptions, - RealtimeChannel, + RealtimeSubscription, } from '@supabase/realtime-js' +import { SupabaseStorageClient } from '@supabase/storage-js' +import { DEFAULT_HEADERS, STORAGE_KEY } from './lib/constants' +import { fetchWithAuth } from './lib/fetch' +import { isBrowser, stripTrailingSlash } from './lib/helpers' +import { SupabaseAuthClient } from './lib/SupabaseAuthClient' +import { SupabaseQueryBuilder } from './lib/SupabaseQueryBuilder' +import { Fetch, SupabaseClientOptions } from './lib/types' const DEFAULT_OPTIONS = { schema: 'public', @@ -89,13 +90,14 @@ export default class SupabaseClient { this.schema = settings.schema this.multiTab = settings.multiTab - this.fetch = settings.fetch this.headers = { ...DEFAULT_HEADERS, ...options?.headers } this.shouldThrowOnError = settings.shouldThrowOnError || false this.auth = this._initSupabaseAuthClient(settings) this.realtime = this._initRealtimeClient({ headers: this.headers, ...settings.realtime }) + this.fetch = fetchWithAuth(this._getAccessToken.bind(this), settings.fetch) + this._listenForAuthEvents() this._listenForMultiTabEvents() @@ -110,7 +112,7 @@ export default class SupabaseClient { */ get functions() { return new FunctionsClient(this.functionsUrl, { - headers: this._getAuthHeaders(), + headers: this.headers, customFetch: this.fetch, }) } @@ -119,7 +121,7 @@ export default class SupabaseClient { * Supabase Storage allows you to manage user-generated content, such as photos or videos. */ get storage() { - return new SupabaseStorageClient(this.storageUrl, this._getAuthHeaders(), this.fetch) + return new SupabaseStorageClient(this.storageUrl, this.headers, this.fetch) } /** @@ -130,7 +132,7 @@ export default class SupabaseClient { from(table: string): SupabaseQueryBuilder { const url = `${this.restUrl}/${table}` return new SupabaseQueryBuilder(url, { - headers: this._getAuthHeaders(), + headers: this.headers, schema: this.schema, realtime: this.realtime, table, @@ -227,6 +229,12 @@ export default class SupabaseClient { return { data: { openSubscriptions: openSubCount }, error } } + private async _getAccessToken() { + const { session } = await this.auth.getSession() + + return session?.access_token ?? null + } + private async _closeSubscription( subscription: RealtimeSubscription | RealtimeChannel ): Promise<{ error: Error | null }> { @@ -297,21 +305,13 @@ export default class SupabaseClient { private _initPostgRESTClient() { return new PostgrestClient(this.restUrl, { - headers: this._getAuthHeaders(), + headers: this.headers, schema: this.schema, fetch: this.fetch, throwOnError: this.shouldThrowOnError, }) } - private _getAuthHeaders(): GenericObject { - const headers: GenericObject = { ...this.headers } - const authBearer = this.auth.session()?.access_token ?? this.supabaseKey - headers['apikey'] = this.supabaseKey - headers['Authorization'] = headers['Authorization'] || `Bearer ${authBearer}` - return headers - } - private _listenForMultiTabEvents() { if (!this.multiTab || !isBrowser() || !window?.addEventListener) { return null diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts new file mode 100644 index 00000000..fc429f45 --- /dev/null +++ b/src/lib/fetch.ts @@ -0,0 +1,42 @@ +import crossFetch, { Headers as CrossFetchHeaders } from 'cross-fetch' + +type Fetch = typeof fetch + +export const resolveFetch = (customFetch?: Fetch): Fetch => { + let _fetch: Fetch + if (customFetch) { + _fetch = customFetch + } else if (typeof fetch === 'undefined') { + _fetch = crossFetch as unknown as Fetch + } else { + _fetch = fetch + } + return (...args) => _fetch(...args) +} + +export const resolveHeadersConstructor = () => { + if (typeof Headers === 'undefined') { + return CrossFetchHeaders + } + + return Headers +} + +export const fetchWithAuth = ( + getAccessToken: () => Promise, + customFetch?: Fetch +): Fetch => { + const fetch = resolveFetch(customFetch) + const HeadersConstructor = resolveHeadersConstructor() + + return async (input, init) => { + const accessToken = await getAccessToken() + let headers = new HeadersConstructor(init?.headers) + + if (!headers.has('Authorization') && accessToken) { + headers.set('Authorization', `Bearer ${accessToken}`) + } + + return fetch(input, { ...init, headers }) + } +} diff --git a/test/client.test.ts b/test/client.test.ts index 2775a507..04046d20 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -1,5 +1,4 @@ import { createClient, SupabaseClient } from '../src/index' -import { DEFAULT_HEADERS } from '../src/lib/constants' const URL = 'http://localhost:3000' const KEY = 'some.fake.key' @@ -17,37 +16,36 @@ test('it should throw an error if no valid params are provided', async () => { }) test('it should not cache Authorization header', async () => { - const checkHeadersSpy = jest.spyOn(SupabaseClient.prototype as any, '_getAuthHeaders') - supabase.auth.setAuth('token1') - supabase.rpc('') // Calling public method `rpc` calls private method _getAuthHeaders which result we want to test - supabase.auth.setAuth('token2') - supabase.rpc('') // Calling public method `rpc` calls private method _getAuthHeaders which result we want to test + supabase.rpc('') + expect(supabase.auth.session()?.access_token).toBe('token1') - expect(checkHeadersSpy.mock.results[0].value).toHaveProperty('Authorization', 'Bearer token1') - expect(checkHeadersSpy.mock.results[1].value).toHaveProperty('Authorization', 'Bearer token2') + supabase.auth.setAuth('token2') + supabase.rpc('') + expect(supabase.auth.session()?.access_token).toBe('token2') }) describe('Custom Headers', () => { test('should have custom header set', () => { const customHeader = { 'X-Test-Header': 'value' } - const checkHeadersSpy = jest.spyOn(SupabaseClient.prototype as any, '_getAuthHeaders') - createClient(URL, KEY, { headers: customHeader }).rpc('') // Calling public method `rpc` calls private method _getAuthHeaders which result we want to test - const getHeaders = checkHeadersSpy.mock.results[0].value + const request = createClient(URL, KEY, { headers: customHeader }).rpc('') + + // @ts-ignore + const getHeaders = request.headers - expect(checkHeadersSpy).toBeCalled() expect(getHeaders).toHaveProperty('X-Test-Header', 'value') }) test('should allow custom Authorization header', () => { const customHeader = { Authorization: 'Bearer custom_token' } supabase.auth.setAuth('override_me') - const checkHeadersSpy = jest.spyOn(SupabaseClient.prototype as any, '_getAuthHeaders') - createClient(URL, KEY, { headers: customHeader }).rpc('') // Calling public method `rpc` calls private method _getAuthHeaders which result we want to test - const getHeaders = checkHeadersSpy.mock.results[0].value - expect(checkHeadersSpy).toBeCalled() + const request = createClient(URL, KEY, { headers: customHeader }).rpc('') + + // @ts-ignore + const getHeaders = request.headers + expect(getHeaders).toHaveProperty('Authorization', 'Bearer custom_token') }) })