@weirdfingers/boards-auth-supabase 0.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # @weirdfingers/auth-supabase
2
+
3
+ Supabase authentication provider for Boards.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @weirdfingers/auth-supabase @supabase/supabase-js
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { SupabaseAuthProvider } from "@weirdfingers/auth-supabase";
15
+ import { AuthProvider } from "@weirdfingers/boards";
16
+
17
+ const authProvider = new SupabaseAuthProvider({
18
+ url: process.env.NEXT_PUBLIC_SUPABASE_URL!,
19
+ anonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
20
+ tenantId: "my-company", // optional
21
+ });
22
+
23
+ function App() {
24
+ return <AuthProvider provider={authProvider}>{/* Your app */}</AuthProvider>;
25
+ }
26
+ ```
27
+
28
+ ## Configuration
29
+
30
+ See the [Supabase authentication guide](https://docs.weirdfingers.dev/auth/providers/supabase) for detailed setup instructions.
31
+
32
+ ## Features
33
+
34
+ - Email/password authentication
35
+ - Social provider login (Google, GitHub, Discord, etc.)
36
+ - Magic link authentication
37
+ - Password reset
38
+ - Session management
39
+ - TypeScript support
40
+
41
+ ## License
42
+
43
+ MIT
@@ -0,0 +1,126 @@
1
+ import { AuthProviderConfig, BaseAuthProvider, AuthState, User } from '@weirdfingers/boards';
2
+ export { AuthContextValue, AuthProviderConfig, AuthState, User } from '@weirdfingers/boards';
3
+ import { SupabaseClient } from '@supabase/supabase-js';
4
+
5
+ /**
6
+ * Supabase auth provider types.
7
+ */
8
+
9
+ /**
10
+ * Options for Supabase client configuration.
11
+ */
12
+ interface SupabaseAuthOptions {
13
+ debug?: boolean;
14
+ persistSession?: boolean;
15
+ detectSessionInUrl?: boolean;
16
+ headers?: Record<string, string>;
17
+ }
18
+ /**
19
+ * Base configuration shared by all config variants.
20
+ */
21
+ interface SupabaseConfigBase extends AuthProviderConfig {
22
+ /**
23
+ * Optional tenant ID for multi-tenant setups.
24
+ */
25
+ tenantId?: string;
26
+ }
27
+ /**
28
+ * Configuration for creating a new Supabase client.
29
+ */
30
+ interface SupabaseConfigWithCredentials extends SupabaseConfigBase {
31
+ /**
32
+ * Supabase project URL.
33
+ */
34
+ url: string;
35
+ /**
36
+ * Supabase anonymous/public key.
37
+ */
38
+ anonKey: string;
39
+ /**
40
+ * Additional Supabase client options.
41
+ */
42
+ options?: SupabaseAuthOptions;
43
+ /**
44
+ * When using credentials, client should not be provided.
45
+ */
46
+ client?: never;
47
+ }
48
+ /**
49
+ * Configuration for using an existing Supabase client.
50
+ */
51
+ interface SupabaseConfigWithClient extends SupabaseConfigBase {
52
+ /**
53
+ * Pre-configured Supabase client instance.
54
+ */
55
+ client: SupabaseClient;
56
+ /**
57
+ * When using an existing client, credentials should not be provided.
58
+ */
59
+ url?: never;
60
+ anonKey?: never;
61
+ options?: never;
62
+ }
63
+ /**
64
+ * Supabase configuration - either provide credentials to create a new client,
65
+ * or provide an existing client instance.
66
+ */
67
+ type SupabaseConfig = SupabaseConfigWithCredentials | SupabaseConfigWithClient;
68
+ /**
69
+ * Type guard to check if config uses credentials.
70
+ */
71
+ declare function isCredentialsConfig(config: SupabaseConfig): config is SupabaseConfigWithCredentials;
72
+ /**
73
+ * Type guard to check if config uses an existing client.
74
+ */
75
+ declare function isClientConfig(config: SupabaseConfig): config is SupabaseConfigWithClient;
76
+
77
+ /**
78
+ * Supabase authentication provider.
79
+ */
80
+
81
+ declare class SupabaseAuthProvider extends BaseAuthProvider {
82
+ protected config: SupabaseConfig;
83
+ private listeners;
84
+ private currentState;
85
+ private supabase;
86
+ constructor(config: SupabaseConfig);
87
+ initialize(): Promise<void>;
88
+ getAuthState(): Promise<AuthState>;
89
+ signIn(opts?: {
90
+ email?: string;
91
+ password?: string;
92
+ provider?: "google" | "github" | "discord" | "twitter" | "facebook";
93
+ type?: "signup" | "signin" | "magic_link";
94
+ options?: {
95
+ data?: Record<string, unknown>;
96
+ redirectTo?: string;
97
+ shouldCreateUser?: boolean;
98
+ };
99
+ }): Promise<void>;
100
+ signOut(): Promise<void>;
101
+ getToken(): Promise<string | null>;
102
+ getUser(): Promise<User | null>;
103
+ refreshToken(): Promise<string | null>;
104
+ onAuthStateChange(callback: (state: AuthState) => void): () => void;
105
+ destroy(): Promise<void>;
106
+ private handleAuthStateChange;
107
+ private updateState;
108
+ /**
109
+ * Get the tenant ID from config.
110
+ */
111
+ protected getTenantId(): string;
112
+ /**
113
+ * Get the underlying Supabase client for advanced operations.
114
+ */
115
+ getSupabaseClient(): SupabaseClient | null;
116
+ /**
117
+ * Reset password for email.
118
+ */
119
+ resetPassword(email: string, redirectTo?: string): Promise<void>;
120
+ /**
121
+ * Update user password.
122
+ */
123
+ updatePassword(newPassword: string): Promise<void>;
124
+ }
125
+
126
+ export { type SupabaseAuthOptions, SupabaseAuthProvider, type SupabaseConfig, type SupabaseConfigWithClient, type SupabaseConfigWithCredentials, isClientConfig, isCredentialsConfig };
@@ -0,0 +1,126 @@
1
+ import { AuthProviderConfig, BaseAuthProvider, AuthState, User } from '@weirdfingers/boards';
2
+ export { AuthContextValue, AuthProviderConfig, AuthState, User } from '@weirdfingers/boards';
3
+ import { SupabaseClient } from '@supabase/supabase-js';
4
+
5
+ /**
6
+ * Supabase auth provider types.
7
+ */
8
+
9
+ /**
10
+ * Options for Supabase client configuration.
11
+ */
12
+ interface SupabaseAuthOptions {
13
+ debug?: boolean;
14
+ persistSession?: boolean;
15
+ detectSessionInUrl?: boolean;
16
+ headers?: Record<string, string>;
17
+ }
18
+ /**
19
+ * Base configuration shared by all config variants.
20
+ */
21
+ interface SupabaseConfigBase extends AuthProviderConfig {
22
+ /**
23
+ * Optional tenant ID for multi-tenant setups.
24
+ */
25
+ tenantId?: string;
26
+ }
27
+ /**
28
+ * Configuration for creating a new Supabase client.
29
+ */
30
+ interface SupabaseConfigWithCredentials extends SupabaseConfigBase {
31
+ /**
32
+ * Supabase project URL.
33
+ */
34
+ url: string;
35
+ /**
36
+ * Supabase anonymous/public key.
37
+ */
38
+ anonKey: string;
39
+ /**
40
+ * Additional Supabase client options.
41
+ */
42
+ options?: SupabaseAuthOptions;
43
+ /**
44
+ * When using credentials, client should not be provided.
45
+ */
46
+ client?: never;
47
+ }
48
+ /**
49
+ * Configuration for using an existing Supabase client.
50
+ */
51
+ interface SupabaseConfigWithClient extends SupabaseConfigBase {
52
+ /**
53
+ * Pre-configured Supabase client instance.
54
+ */
55
+ client: SupabaseClient;
56
+ /**
57
+ * When using an existing client, credentials should not be provided.
58
+ */
59
+ url?: never;
60
+ anonKey?: never;
61
+ options?: never;
62
+ }
63
+ /**
64
+ * Supabase configuration - either provide credentials to create a new client,
65
+ * or provide an existing client instance.
66
+ */
67
+ type SupabaseConfig = SupabaseConfigWithCredentials | SupabaseConfigWithClient;
68
+ /**
69
+ * Type guard to check if config uses credentials.
70
+ */
71
+ declare function isCredentialsConfig(config: SupabaseConfig): config is SupabaseConfigWithCredentials;
72
+ /**
73
+ * Type guard to check if config uses an existing client.
74
+ */
75
+ declare function isClientConfig(config: SupabaseConfig): config is SupabaseConfigWithClient;
76
+
77
+ /**
78
+ * Supabase authentication provider.
79
+ */
80
+
81
+ declare class SupabaseAuthProvider extends BaseAuthProvider {
82
+ protected config: SupabaseConfig;
83
+ private listeners;
84
+ private currentState;
85
+ private supabase;
86
+ constructor(config: SupabaseConfig);
87
+ initialize(): Promise<void>;
88
+ getAuthState(): Promise<AuthState>;
89
+ signIn(opts?: {
90
+ email?: string;
91
+ password?: string;
92
+ provider?: "google" | "github" | "discord" | "twitter" | "facebook";
93
+ type?: "signup" | "signin" | "magic_link";
94
+ options?: {
95
+ data?: Record<string, unknown>;
96
+ redirectTo?: string;
97
+ shouldCreateUser?: boolean;
98
+ };
99
+ }): Promise<void>;
100
+ signOut(): Promise<void>;
101
+ getToken(): Promise<string | null>;
102
+ getUser(): Promise<User | null>;
103
+ refreshToken(): Promise<string | null>;
104
+ onAuthStateChange(callback: (state: AuthState) => void): () => void;
105
+ destroy(): Promise<void>;
106
+ private handleAuthStateChange;
107
+ private updateState;
108
+ /**
109
+ * Get the tenant ID from config.
110
+ */
111
+ protected getTenantId(): string;
112
+ /**
113
+ * Get the underlying Supabase client for advanced operations.
114
+ */
115
+ getSupabaseClient(): SupabaseClient | null;
116
+ /**
117
+ * Reset password for email.
118
+ */
119
+ resetPassword(email: string, redirectTo?: string): Promise<void>;
120
+ /**
121
+ * Update user password.
122
+ */
123
+ updatePassword(newPassword: string): Promise<void>;
124
+ }
125
+
126
+ export { type SupabaseAuthOptions, SupabaseAuthProvider, type SupabaseConfig, type SupabaseConfigWithClient, type SupabaseConfigWithCredentials, isClientConfig, isCredentialsConfig };
package/dist/index.js ADDED
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ SupabaseAuthProvider: () => SupabaseAuthProvider,
34
+ isClientConfig: () => isClientConfig,
35
+ isCredentialsConfig: () => isCredentialsConfig
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/SupabaseAuthProvider.ts
40
+ var import_boards = require("@weirdfingers/boards");
41
+
42
+ // src/types.ts
43
+ function isCredentialsConfig(config) {
44
+ return "url" in config && "anonKey" in config && !("client" in config && config.client);
45
+ }
46
+ function isClientConfig(config) {
47
+ return "client" in config && config.client !== void 0;
48
+ }
49
+
50
+ // src/SupabaseAuthProvider.ts
51
+ var SupabaseAuthProvider = class extends import_boards.BaseAuthProvider {
52
+ constructor(config) {
53
+ super(config);
54
+ this.listeners = [];
55
+ this.supabase = null;
56
+ this.config = config;
57
+ this.currentState = {
58
+ user: null,
59
+ status: "loading",
60
+ signIn: this.signIn.bind(this),
61
+ signOut: this.signOut.bind(this),
62
+ getToken: this.getToken.bind(this),
63
+ refreshToken: this.refreshToken.bind(this)
64
+ };
65
+ }
66
+ async initialize() {
67
+ try {
68
+ if (isClientConfig(this.config)) {
69
+ this.supabase = this.config.client;
70
+ } else if (isCredentialsConfig(this.config)) {
71
+ const { createClient } = await import("@supabase/supabase-js");
72
+ this.supabase = createClient(this.config.url, this.config.anonKey, {
73
+ auth: {
74
+ persistSession: this.config.options?.persistSession ?? true,
75
+ detectSessionInUrl: this.config.options?.detectSessionInUrl ?? true,
76
+ ...this.config.options?.headers && {
77
+ headers: this.config.options.headers
78
+ }
79
+ }
80
+ });
81
+ } else {
82
+ throw new Error(
83
+ "Invalid configuration: provide either { url, anonKey } or { client }"
84
+ );
85
+ }
86
+ this.supabase.auth.onAuthStateChange(
87
+ (event, session2) => {
88
+ this.handleAuthStateChange(event, session2);
89
+ }
90
+ );
91
+ const {
92
+ data: { session }
93
+ } = await this.supabase.auth.getSession();
94
+ this.handleAuthStateChange("INITIAL_SESSION", session);
95
+ } catch (error) {
96
+ console.error("Failed to initialize Supabase:", error);
97
+ this.updateState({ user: null, status: "unauthenticated" });
98
+ throw error;
99
+ }
100
+ }
101
+ async getAuthState() {
102
+ return this.currentState;
103
+ }
104
+ async signIn(opts = {}) {
105
+ if (!this.supabase) {
106
+ throw new Error("Supabase not initialized");
107
+ }
108
+ this.updateState({ status: "loading" });
109
+ try {
110
+ if (opts.provider) {
111
+ const tenantId = this.getTenantId();
112
+ const { error } = await this.supabase.auth.signInWithOAuth({
113
+ provider: opts.provider,
114
+ options: {
115
+ redirectTo: opts.options?.redirectTo || window.location.origin,
116
+ ...tenantId !== "default" && {
117
+ queryParams: { tenant: tenantId }
118
+ }
119
+ }
120
+ });
121
+ if (error) throw error;
122
+ return;
123
+ }
124
+ if (opts.type === "magic_link" && opts.email) {
125
+ const { error } = await this.supabase.auth.signInWithOtp({
126
+ email: opts.email,
127
+ options: opts.options
128
+ });
129
+ if (error) throw error;
130
+ this.updateState({ status: "unauthenticated" });
131
+ return;
132
+ }
133
+ if (opts.type === "signup" && opts.email && opts.password) {
134
+ const { error } = await this.supabase.auth.signUp({
135
+ email: opts.email,
136
+ password: opts.password,
137
+ options: opts.options
138
+ });
139
+ if (error) throw error;
140
+ return;
141
+ }
142
+ if (opts.email && opts.password) {
143
+ const { error } = await this.supabase.auth.signInWithPassword({
144
+ email: opts.email,
145
+ password: opts.password
146
+ });
147
+ if (error) throw error;
148
+ return;
149
+ }
150
+ throw new Error("Invalid sign in options provided");
151
+ } catch (error) {
152
+ this.updateState({ user: null, status: "unauthenticated" });
153
+ throw error;
154
+ }
155
+ }
156
+ async signOut() {
157
+ if (!this.supabase) {
158
+ throw new Error("Supabase not initialized");
159
+ }
160
+ const { error } = await this.supabase.auth.signOut();
161
+ if (error) throw error;
162
+ }
163
+ async getToken() {
164
+ if (!this.supabase) return null;
165
+ try {
166
+ const {
167
+ data: { session }
168
+ } = await this.supabase.auth.getSession();
169
+ return session?.access_token || null;
170
+ } catch (error) {
171
+ console.error("Failed to get token:", error);
172
+ return null;
173
+ }
174
+ }
175
+ async getUser() {
176
+ return this.currentState.user;
177
+ }
178
+ async refreshToken() {
179
+ if (!this.supabase) return null;
180
+ try {
181
+ const {
182
+ data: { session },
183
+ error
184
+ } = await this.supabase.auth.refreshSession();
185
+ if (error) throw error;
186
+ return session?.access_token || null;
187
+ } catch (_err) {
188
+ return null;
189
+ }
190
+ }
191
+ onAuthStateChange(callback) {
192
+ this.listeners.push(callback);
193
+ return () => {
194
+ const index = this.listeners.indexOf(callback);
195
+ if (index > -1) {
196
+ this.listeners.splice(index, 1);
197
+ }
198
+ };
199
+ }
200
+ async destroy() {
201
+ this.listeners = [];
202
+ }
203
+ handleAuthStateChange(_event, session) {
204
+ if (session) {
205
+ const user = {
206
+ id: session.user.id,
207
+ email: session.user.email ?? "",
208
+ name: session.user.user_metadata?.display_name || session.user.user_metadata?.full_name || session.user.user_metadata?.name || session.user.email?.split("@")[0],
209
+ avatar: session.user.user_metadata?.avatar_url || session.user.user_metadata?.picture,
210
+ metadata: {
211
+ ...session.user.user_metadata,
212
+ provider: "supabase",
213
+ subject: session.user.id
214
+ },
215
+ credits: { balance: 0, reserved: 0 }
216
+ };
217
+ this.updateState({ user, status: "authenticated" });
218
+ } else {
219
+ this.updateState({ user: null, status: "unauthenticated" });
220
+ }
221
+ }
222
+ updateState(updates) {
223
+ this.currentState = { ...this.currentState, ...updates };
224
+ this.listeners.forEach((listener) => listener(this.currentState));
225
+ }
226
+ /**
227
+ * Get the tenant ID from config.
228
+ */
229
+ getTenantId() {
230
+ return this.config.tenantId ?? "default";
231
+ }
232
+ /**
233
+ * Get the underlying Supabase client for advanced operations.
234
+ */
235
+ getSupabaseClient() {
236
+ return this.supabase;
237
+ }
238
+ /**
239
+ * Reset password for email.
240
+ */
241
+ async resetPassword(email, redirectTo) {
242
+ if (!this.supabase) {
243
+ throw new Error("Supabase not initialized");
244
+ }
245
+ const { error } = await this.supabase.auth.resetPasswordForEmail(email, {
246
+ redirectTo: redirectTo || `${window.location.origin}/reset-password`
247
+ });
248
+ if (error) throw error;
249
+ }
250
+ /**
251
+ * Update user password.
252
+ */
253
+ async updatePassword(newPassword) {
254
+ if (!this.supabase) {
255
+ throw new Error("Supabase not initialized");
256
+ }
257
+ const { error } = await this.supabase.auth.updateUser({
258
+ password: newPassword
259
+ });
260
+ if (error) throw error;
261
+ }
262
+ };
263
+ // Annotate the CommonJS export names for ESM import in node:
264
+ 0 && (module.exports = {
265
+ SupabaseAuthProvider,
266
+ isClientConfig,
267
+ isCredentialsConfig
268
+ });
269
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/SupabaseAuthProvider.ts","../src/types.ts"],"sourcesContent":["/**\n * Supabase authentication provider package for Boards.\n *\n * This package provides a tree-shakable Supabase auth provider\n * that can be used independently of the main boards-frontend package.\n */\n\nexport { SupabaseAuthProvider } from './SupabaseAuthProvider';\nexport type {\n SupabaseConfig,\n SupabaseConfigWithCredentials,\n SupabaseConfigWithClient,\n SupabaseAuthOptions,\n} from './types';\nexport { isCredentialsConfig, isClientConfig } from './types';\n\n// Re-export core types for convenience\nexport type {\n AuthState,\n User,\n AuthProviderConfig,\n AuthContextValue\n} from '@weirdfingers/boards';\n","/**\n * Supabase authentication provider.\n */\n\nimport { BaseAuthProvider, AuthState, User } from \"@weirdfingers/boards\";\nimport type { SupabaseClient, Session, AuthChangeEvent } from \"@supabase/supabase-js\";\nimport type { SupabaseConfig } from \"./types\";\nimport { isClientConfig, isCredentialsConfig } from \"./types\";\n\nexport class SupabaseAuthProvider extends BaseAuthProvider {\n protected config: SupabaseConfig;\n private listeners: ((state: AuthState) => void)[] = [];\n private currentState: AuthState;\n private supabase: SupabaseClient | null = null;\n\n constructor(config: SupabaseConfig) {\n super(config);\n this.config = config;\n\n this.currentState = {\n user: null,\n status: \"loading\",\n signIn: this.signIn.bind(this) as AuthState[\"signIn\"],\n signOut: this.signOut.bind(this),\n getToken: this.getToken.bind(this),\n refreshToken: this.refreshToken.bind(this),\n };\n }\n\n async initialize(): Promise<void> {\n try {\n if (isClientConfig(this.config)) {\n // Use the provided client directly\n this.supabase = this.config.client;\n } else if (isCredentialsConfig(this.config)) {\n // Dynamically import Supabase and create a new client\n const { createClient } = await import(\"@supabase/supabase-js\");\n\n this.supabase = createClient(this.config.url, this.config.anonKey, {\n auth: {\n persistSession: this.config.options?.persistSession ?? true,\n detectSessionInUrl: this.config.options?.detectSessionInUrl ?? true,\n ...(this.config.options?.headers && {\n headers: this.config.options.headers,\n }),\n },\n });\n } else {\n throw new Error(\n \"Invalid configuration: provide either { url, anonKey } or { client }\"\n );\n }\n\n // Set up auth state listener\n this.supabase.auth.onAuthStateChange(\n (event: AuthChangeEvent, session: Session | null) => {\n this.handleAuthStateChange(event, session);\n }\n );\n\n // Get initial session\n const {\n data: { session },\n } = await this.supabase.auth.getSession();\n this.handleAuthStateChange(\"INITIAL_SESSION\", session);\n } catch (error) {\n console.error(\"Failed to initialize Supabase:\", error);\n this.updateState({ user: null, status: \"unauthenticated\" });\n throw error;\n }\n }\n\n async getAuthState(): Promise<AuthState> {\n return this.currentState;\n }\n\n async signIn(\n opts: {\n email?: string;\n password?: string;\n provider?: \"google\" | \"github\" | \"discord\" | \"twitter\" | \"facebook\";\n type?: \"signup\" | \"signin\" | \"magic_link\";\n options?: {\n data?: Record<string, unknown>;\n redirectTo?: string;\n shouldCreateUser?: boolean;\n };\n } = {}\n ): Promise<void> {\n if (!this.supabase) {\n throw new Error(\"Supabase not initialized\");\n }\n\n this.updateState({ status: \"loading\" });\n\n try {\n // Social provider login\n if (opts.provider) {\n const tenantId = this.getTenantId();\n const { error } = await this.supabase.auth.signInWithOAuth({\n provider: opts.provider,\n options: {\n redirectTo: opts.options?.redirectTo || window.location.origin,\n ...(tenantId !== \"default\" && {\n queryParams: { tenant: tenantId },\n }),\n },\n });\n\n if (error) throw error;\n return; // OAuth redirects, so we don't update state here\n }\n\n // Magic link\n if (opts.type === \"magic_link\" && opts.email) {\n const { error } = await this.supabase.auth.signInWithOtp({\n email: opts.email,\n options: opts.options,\n });\n\n if (error) throw error;\n this.updateState({ status: \"unauthenticated\" }); // Wait for email click\n return;\n }\n\n // Email/password signup\n if (opts.type === \"signup\" && opts.email && opts.password) {\n const { error } = await this.supabase.auth.signUp({\n email: opts.email,\n password: opts.password,\n options: opts.options,\n });\n\n if (error) throw error;\n return; // May need email confirmation\n }\n\n // Email/password signin (default)\n if (opts.email && opts.password) {\n const { error } = await this.supabase.auth.signInWithPassword({\n email: opts.email,\n password: opts.password,\n });\n\n if (error) throw error;\n return; // State will update via onAuthStateChange\n }\n\n throw new Error(\"Invalid sign in options provided\");\n } catch (error) {\n this.updateState({ user: null, status: \"unauthenticated\" });\n throw error;\n }\n }\n\n async signOut(): Promise<void> {\n if (!this.supabase) {\n throw new Error(\"Supabase not initialized\");\n }\n\n const { error } = await this.supabase.auth.signOut();\n if (error) throw error;\n\n // State will update via onAuthStateChange\n }\n\n async getToken(): Promise<string | null> {\n if (!this.supabase) return null;\n\n try {\n const {\n data: { session },\n } = await this.supabase.auth.getSession();\n return session?.access_token || null;\n } catch (error) {\n console.error(\"Failed to get token:\", error);\n return null;\n }\n }\n\n async getUser(): Promise<User | null> {\n return this.currentState.user;\n }\n\n async refreshToken(): Promise<string | null> {\n if (!this.supabase) return null;\n try {\n const {\n data: { session },\n error,\n } = await this.supabase.auth.refreshSession();\n if (error) throw error;\n return session?.access_token || null;\n } catch (_err) {\n return null;\n }\n }\n\n onAuthStateChange(callback: (state: AuthState) => void): () => void {\n this.listeners.push(callback);\n return () => {\n const index = this.listeners.indexOf(callback);\n if (index > -1) {\n this.listeners.splice(index, 1);\n }\n };\n }\n\n async destroy(): Promise<void> {\n this.listeners = [];\n // Supabase client cleanup is handled automatically\n }\n\n private handleAuthStateChange(\n _event: AuthChangeEvent | \"INITIAL_SESSION\",\n session: Session | null\n ): void {\n if (session) {\n const user: User = {\n id: session.user.id,\n email: session.user.email ?? \"\",\n name:\n session.user.user_metadata?.display_name ||\n session.user.user_metadata?.full_name ||\n session.user.user_metadata?.name ||\n session.user.email?.split(\"@\")[0],\n avatar:\n session.user.user_metadata?.avatar_url ||\n session.user.user_metadata?.picture,\n metadata: {\n ...session.user.user_metadata,\n provider: \"supabase\",\n subject: session.user.id,\n },\n credits: { balance: 0, reserved: 0 },\n };\n\n this.updateState({ user, status: \"authenticated\" });\n } else {\n this.updateState({ user: null, status: \"unauthenticated\" });\n }\n }\n\n private updateState(updates: Partial<AuthState>): void {\n this.currentState = { ...this.currentState, ...updates };\n this.listeners.forEach((listener) => listener(this.currentState));\n }\n\n /**\n * Get the tenant ID from config.\n */\n protected override getTenantId(): string {\n return this.config.tenantId ?? \"default\";\n }\n\n /**\n * Get the underlying Supabase client for advanced operations.\n */\n getSupabaseClient(): SupabaseClient | null {\n return this.supabase;\n }\n\n /**\n * Reset password for email.\n */\n async resetPassword(email: string, redirectTo?: string): Promise<void> {\n if (!this.supabase) {\n throw new Error(\"Supabase not initialized\");\n }\n\n const { error } = await this.supabase.auth.resetPasswordForEmail(email, {\n redirectTo: redirectTo || `${window.location.origin}/reset-password`,\n });\n\n if (error) throw error;\n }\n\n /**\n * Update user password.\n */\n async updatePassword(newPassword: string): Promise<void> {\n if (!this.supabase) {\n throw new Error(\"Supabase not initialized\");\n }\n\n const { error } = await this.supabase.auth.updateUser({\n password: newPassword,\n });\n\n if (error) throw error;\n }\n}\n","/**\n * Supabase auth provider types.\n */\n\nimport type { AuthProviderConfig } from \"@weirdfingers/boards\";\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\n\n/**\n * Options for Supabase client configuration.\n */\nexport interface SupabaseAuthOptions {\n debug?: boolean;\n persistSession?: boolean;\n detectSessionInUrl?: boolean;\n headers?: Record<string, string>;\n}\n\n/**\n * Base configuration shared by all config variants.\n */\ninterface SupabaseConfigBase extends AuthProviderConfig {\n /**\n * Optional tenant ID for multi-tenant setups.\n */\n tenantId?: string;\n}\n\n/**\n * Configuration for creating a new Supabase client.\n */\nexport interface SupabaseConfigWithCredentials extends SupabaseConfigBase {\n /**\n * Supabase project URL.\n */\n url: string;\n\n /**\n * Supabase anonymous/public key.\n */\n anonKey: string;\n\n /**\n * Additional Supabase client options.\n */\n options?: SupabaseAuthOptions;\n\n /**\n * When using credentials, client should not be provided.\n */\n client?: never;\n}\n\n/**\n * Configuration for using an existing Supabase client.\n */\nexport interface SupabaseConfigWithClient extends SupabaseConfigBase {\n /**\n * Pre-configured Supabase client instance.\n */\n client: SupabaseClient;\n\n /**\n * When using an existing client, credentials should not be provided.\n */\n url?: never;\n anonKey?: never;\n options?: never;\n}\n\n/**\n * Supabase configuration - either provide credentials to create a new client,\n * or provide an existing client instance.\n */\nexport type SupabaseConfig = SupabaseConfigWithCredentials | SupabaseConfigWithClient;\n\n/**\n * Type guard to check if config uses credentials.\n */\nexport function isCredentialsConfig(\n config: SupabaseConfig\n): config is SupabaseConfigWithCredentials {\n return \"url\" in config && \"anonKey\" in config && !(\"client\" in config && config.client);\n}\n\n/**\n * Type guard to check if config uses an existing client.\n */\nexport function isClientConfig(config: SupabaseConfig): config is SupabaseConfigWithClient {\n return \"client\" in config && config.client !== undefined;\n}\n"],"mappings":"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","names":["session"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,230 @@
1
+ // src/SupabaseAuthProvider.ts
2
+ import { BaseAuthProvider } from "@weirdfingers/boards";
3
+
4
+ // src/types.ts
5
+ function isCredentialsConfig(config) {
6
+ return "url" in config && "anonKey" in config && !("client" in config && config.client);
7
+ }
8
+ function isClientConfig(config) {
9
+ return "client" in config && config.client !== void 0;
10
+ }
11
+
12
+ // src/SupabaseAuthProvider.ts
13
+ var SupabaseAuthProvider = class extends BaseAuthProvider {
14
+ constructor(config) {
15
+ super(config);
16
+ this.listeners = [];
17
+ this.supabase = null;
18
+ this.config = config;
19
+ this.currentState = {
20
+ user: null,
21
+ status: "loading",
22
+ signIn: this.signIn.bind(this),
23
+ signOut: this.signOut.bind(this),
24
+ getToken: this.getToken.bind(this),
25
+ refreshToken: this.refreshToken.bind(this)
26
+ };
27
+ }
28
+ async initialize() {
29
+ try {
30
+ if (isClientConfig(this.config)) {
31
+ this.supabase = this.config.client;
32
+ } else if (isCredentialsConfig(this.config)) {
33
+ const { createClient } = await import("@supabase/supabase-js");
34
+ this.supabase = createClient(this.config.url, this.config.anonKey, {
35
+ auth: {
36
+ persistSession: this.config.options?.persistSession ?? true,
37
+ detectSessionInUrl: this.config.options?.detectSessionInUrl ?? true,
38
+ ...this.config.options?.headers && {
39
+ headers: this.config.options.headers
40
+ }
41
+ }
42
+ });
43
+ } else {
44
+ throw new Error(
45
+ "Invalid configuration: provide either { url, anonKey } or { client }"
46
+ );
47
+ }
48
+ this.supabase.auth.onAuthStateChange(
49
+ (event, session2) => {
50
+ this.handleAuthStateChange(event, session2);
51
+ }
52
+ );
53
+ const {
54
+ data: { session }
55
+ } = await this.supabase.auth.getSession();
56
+ this.handleAuthStateChange("INITIAL_SESSION", session);
57
+ } catch (error) {
58
+ console.error("Failed to initialize Supabase:", error);
59
+ this.updateState({ user: null, status: "unauthenticated" });
60
+ throw error;
61
+ }
62
+ }
63
+ async getAuthState() {
64
+ return this.currentState;
65
+ }
66
+ async signIn(opts = {}) {
67
+ if (!this.supabase) {
68
+ throw new Error("Supabase not initialized");
69
+ }
70
+ this.updateState({ status: "loading" });
71
+ try {
72
+ if (opts.provider) {
73
+ const tenantId = this.getTenantId();
74
+ const { error } = await this.supabase.auth.signInWithOAuth({
75
+ provider: opts.provider,
76
+ options: {
77
+ redirectTo: opts.options?.redirectTo || window.location.origin,
78
+ ...tenantId !== "default" && {
79
+ queryParams: { tenant: tenantId }
80
+ }
81
+ }
82
+ });
83
+ if (error) throw error;
84
+ return;
85
+ }
86
+ if (opts.type === "magic_link" && opts.email) {
87
+ const { error } = await this.supabase.auth.signInWithOtp({
88
+ email: opts.email,
89
+ options: opts.options
90
+ });
91
+ if (error) throw error;
92
+ this.updateState({ status: "unauthenticated" });
93
+ return;
94
+ }
95
+ if (opts.type === "signup" && opts.email && opts.password) {
96
+ const { error } = await this.supabase.auth.signUp({
97
+ email: opts.email,
98
+ password: opts.password,
99
+ options: opts.options
100
+ });
101
+ if (error) throw error;
102
+ return;
103
+ }
104
+ if (opts.email && opts.password) {
105
+ const { error } = await this.supabase.auth.signInWithPassword({
106
+ email: opts.email,
107
+ password: opts.password
108
+ });
109
+ if (error) throw error;
110
+ return;
111
+ }
112
+ throw new Error("Invalid sign in options provided");
113
+ } catch (error) {
114
+ this.updateState({ user: null, status: "unauthenticated" });
115
+ throw error;
116
+ }
117
+ }
118
+ async signOut() {
119
+ if (!this.supabase) {
120
+ throw new Error("Supabase not initialized");
121
+ }
122
+ const { error } = await this.supabase.auth.signOut();
123
+ if (error) throw error;
124
+ }
125
+ async getToken() {
126
+ if (!this.supabase) return null;
127
+ try {
128
+ const {
129
+ data: { session }
130
+ } = await this.supabase.auth.getSession();
131
+ return session?.access_token || null;
132
+ } catch (error) {
133
+ console.error("Failed to get token:", error);
134
+ return null;
135
+ }
136
+ }
137
+ async getUser() {
138
+ return this.currentState.user;
139
+ }
140
+ async refreshToken() {
141
+ if (!this.supabase) return null;
142
+ try {
143
+ const {
144
+ data: { session },
145
+ error
146
+ } = await this.supabase.auth.refreshSession();
147
+ if (error) throw error;
148
+ return session?.access_token || null;
149
+ } catch (_err) {
150
+ return null;
151
+ }
152
+ }
153
+ onAuthStateChange(callback) {
154
+ this.listeners.push(callback);
155
+ return () => {
156
+ const index = this.listeners.indexOf(callback);
157
+ if (index > -1) {
158
+ this.listeners.splice(index, 1);
159
+ }
160
+ };
161
+ }
162
+ async destroy() {
163
+ this.listeners = [];
164
+ }
165
+ handleAuthStateChange(_event, session) {
166
+ if (session) {
167
+ const user = {
168
+ id: session.user.id,
169
+ email: session.user.email ?? "",
170
+ name: session.user.user_metadata?.display_name || session.user.user_metadata?.full_name || session.user.user_metadata?.name || session.user.email?.split("@")[0],
171
+ avatar: session.user.user_metadata?.avatar_url || session.user.user_metadata?.picture,
172
+ metadata: {
173
+ ...session.user.user_metadata,
174
+ provider: "supabase",
175
+ subject: session.user.id
176
+ },
177
+ credits: { balance: 0, reserved: 0 }
178
+ };
179
+ this.updateState({ user, status: "authenticated" });
180
+ } else {
181
+ this.updateState({ user: null, status: "unauthenticated" });
182
+ }
183
+ }
184
+ updateState(updates) {
185
+ this.currentState = { ...this.currentState, ...updates };
186
+ this.listeners.forEach((listener) => listener(this.currentState));
187
+ }
188
+ /**
189
+ * Get the tenant ID from config.
190
+ */
191
+ getTenantId() {
192
+ return this.config.tenantId ?? "default";
193
+ }
194
+ /**
195
+ * Get the underlying Supabase client for advanced operations.
196
+ */
197
+ getSupabaseClient() {
198
+ return this.supabase;
199
+ }
200
+ /**
201
+ * Reset password for email.
202
+ */
203
+ async resetPassword(email, redirectTo) {
204
+ if (!this.supabase) {
205
+ throw new Error("Supabase not initialized");
206
+ }
207
+ const { error } = await this.supabase.auth.resetPasswordForEmail(email, {
208
+ redirectTo: redirectTo || `${window.location.origin}/reset-password`
209
+ });
210
+ if (error) throw error;
211
+ }
212
+ /**
213
+ * Update user password.
214
+ */
215
+ async updatePassword(newPassword) {
216
+ if (!this.supabase) {
217
+ throw new Error("Supabase not initialized");
218
+ }
219
+ const { error } = await this.supabase.auth.updateUser({
220
+ password: newPassword
221
+ });
222
+ if (error) throw error;
223
+ }
224
+ };
225
+ export {
226
+ SupabaseAuthProvider,
227
+ isClientConfig,
228
+ isCredentialsConfig
229
+ };
230
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/SupabaseAuthProvider.ts","../src/types.ts"],"sourcesContent":["/**\n * Supabase authentication provider.\n */\n\nimport { BaseAuthProvider, AuthState, User } from \"@weirdfingers/boards\";\nimport type { SupabaseClient, Session, AuthChangeEvent } from \"@supabase/supabase-js\";\nimport type { SupabaseConfig } from \"./types\";\nimport { isClientConfig, isCredentialsConfig } from \"./types\";\n\nexport class SupabaseAuthProvider extends BaseAuthProvider {\n protected config: SupabaseConfig;\n private listeners: ((state: AuthState) => void)[] = [];\n private currentState: AuthState;\n private supabase: SupabaseClient | null = null;\n\n constructor(config: SupabaseConfig) {\n super(config);\n this.config = config;\n\n this.currentState = {\n user: null,\n status: \"loading\",\n signIn: this.signIn.bind(this) as AuthState[\"signIn\"],\n signOut: this.signOut.bind(this),\n getToken: this.getToken.bind(this),\n refreshToken: this.refreshToken.bind(this),\n };\n }\n\n async initialize(): Promise<void> {\n try {\n if (isClientConfig(this.config)) {\n // Use the provided client directly\n this.supabase = this.config.client;\n } else if (isCredentialsConfig(this.config)) {\n // Dynamically import Supabase and create a new client\n const { createClient } = await import(\"@supabase/supabase-js\");\n\n this.supabase = createClient(this.config.url, this.config.anonKey, {\n auth: {\n persistSession: this.config.options?.persistSession ?? true,\n detectSessionInUrl: this.config.options?.detectSessionInUrl ?? true,\n ...(this.config.options?.headers && {\n headers: this.config.options.headers,\n }),\n },\n });\n } else {\n throw new Error(\n \"Invalid configuration: provide either { url, anonKey } or { client }\"\n );\n }\n\n // Set up auth state listener\n this.supabase.auth.onAuthStateChange(\n (event: AuthChangeEvent, session: Session | null) => {\n this.handleAuthStateChange(event, session);\n }\n );\n\n // Get initial session\n const {\n data: { session },\n } = await this.supabase.auth.getSession();\n this.handleAuthStateChange(\"INITIAL_SESSION\", session);\n } catch (error) {\n console.error(\"Failed to initialize Supabase:\", error);\n this.updateState({ user: null, status: \"unauthenticated\" });\n throw error;\n }\n }\n\n async getAuthState(): Promise<AuthState> {\n return this.currentState;\n }\n\n async signIn(\n opts: {\n email?: string;\n password?: string;\n provider?: \"google\" | \"github\" | \"discord\" | \"twitter\" | \"facebook\";\n type?: \"signup\" | \"signin\" | \"magic_link\";\n options?: {\n data?: Record<string, unknown>;\n redirectTo?: string;\n shouldCreateUser?: boolean;\n };\n } = {}\n ): Promise<void> {\n if (!this.supabase) {\n throw new Error(\"Supabase not initialized\");\n }\n\n this.updateState({ status: \"loading\" });\n\n try {\n // Social provider login\n if (opts.provider) {\n const tenantId = this.getTenantId();\n const { error } = await this.supabase.auth.signInWithOAuth({\n provider: opts.provider,\n options: {\n redirectTo: opts.options?.redirectTo || window.location.origin,\n ...(tenantId !== \"default\" && {\n queryParams: { tenant: tenantId },\n }),\n },\n });\n\n if (error) throw error;\n return; // OAuth redirects, so we don't update state here\n }\n\n // Magic link\n if (opts.type === \"magic_link\" && opts.email) {\n const { error } = await this.supabase.auth.signInWithOtp({\n email: opts.email,\n options: opts.options,\n });\n\n if (error) throw error;\n this.updateState({ status: \"unauthenticated\" }); // Wait for email click\n return;\n }\n\n // Email/password signup\n if (opts.type === \"signup\" && opts.email && opts.password) {\n const { error } = await this.supabase.auth.signUp({\n email: opts.email,\n password: opts.password,\n options: opts.options,\n });\n\n if (error) throw error;\n return; // May need email confirmation\n }\n\n // Email/password signin (default)\n if (opts.email && opts.password) {\n const { error } = await this.supabase.auth.signInWithPassword({\n email: opts.email,\n password: opts.password,\n });\n\n if (error) throw error;\n return; // State will update via onAuthStateChange\n }\n\n throw new Error(\"Invalid sign in options provided\");\n } catch (error) {\n this.updateState({ user: null, status: \"unauthenticated\" });\n throw error;\n }\n }\n\n async signOut(): Promise<void> {\n if (!this.supabase) {\n throw new Error(\"Supabase not initialized\");\n }\n\n const { error } = await this.supabase.auth.signOut();\n if (error) throw error;\n\n // State will update via onAuthStateChange\n }\n\n async getToken(): Promise<string | null> {\n if (!this.supabase) return null;\n\n try {\n const {\n data: { session },\n } = await this.supabase.auth.getSession();\n return session?.access_token || null;\n } catch (error) {\n console.error(\"Failed to get token:\", error);\n return null;\n }\n }\n\n async getUser(): Promise<User | null> {\n return this.currentState.user;\n }\n\n async refreshToken(): Promise<string | null> {\n if (!this.supabase) return null;\n try {\n const {\n data: { session },\n error,\n } = await this.supabase.auth.refreshSession();\n if (error) throw error;\n return session?.access_token || null;\n } catch (_err) {\n return null;\n }\n }\n\n onAuthStateChange(callback: (state: AuthState) => void): () => void {\n this.listeners.push(callback);\n return () => {\n const index = this.listeners.indexOf(callback);\n if (index > -1) {\n this.listeners.splice(index, 1);\n }\n };\n }\n\n async destroy(): Promise<void> {\n this.listeners = [];\n // Supabase client cleanup is handled automatically\n }\n\n private handleAuthStateChange(\n _event: AuthChangeEvent | \"INITIAL_SESSION\",\n session: Session | null\n ): void {\n if (session) {\n const user: User = {\n id: session.user.id,\n email: session.user.email ?? \"\",\n name:\n session.user.user_metadata?.display_name ||\n session.user.user_metadata?.full_name ||\n session.user.user_metadata?.name ||\n session.user.email?.split(\"@\")[0],\n avatar:\n session.user.user_metadata?.avatar_url ||\n session.user.user_metadata?.picture,\n metadata: {\n ...session.user.user_metadata,\n provider: \"supabase\",\n subject: session.user.id,\n },\n credits: { balance: 0, reserved: 0 },\n };\n\n this.updateState({ user, status: \"authenticated\" });\n } else {\n this.updateState({ user: null, status: \"unauthenticated\" });\n }\n }\n\n private updateState(updates: Partial<AuthState>): void {\n this.currentState = { ...this.currentState, ...updates };\n this.listeners.forEach((listener) => listener(this.currentState));\n }\n\n /**\n * Get the tenant ID from config.\n */\n protected override getTenantId(): string {\n return this.config.tenantId ?? \"default\";\n }\n\n /**\n * Get the underlying Supabase client for advanced operations.\n */\n getSupabaseClient(): SupabaseClient | null {\n return this.supabase;\n }\n\n /**\n * Reset password for email.\n */\n async resetPassword(email: string, redirectTo?: string): Promise<void> {\n if (!this.supabase) {\n throw new Error(\"Supabase not initialized\");\n }\n\n const { error } = await this.supabase.auth.resetPasswordForEmail(email, {\n redirectTo: redirectTo || `${window.location.origin}/reset-password`,\n });\n\n if (error) throw error;\n }\n\n /**\n * Update user password.\n */\n async updatePassword(newPassword: string): Promise<void> {\n if (!this.supabase) {\n throw new Error(\"Supabase not initialized\");\n }\n\n const { error } = await this.supabase.auth.updateUser({\n password: newPassword,\n });\n\n if (error) throw error;\n }\n}\n","/**\n * Supabase auth provider types.\n */\n\nimport type { AuthProviderConfig } from \"@weirdfingers/boards\";\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\n\n/**\n * Options for Supabase client configuration.\n */\nexport interface SupabaseAuthOptions {\n debug?: boolean;\n persistSession?: boolean;\n detectSessionInUrl?: boolean;\n headers?: Record<string, string>;\n}\n\n/**\n * Base configuration shared by all config variants.\n */\ninterface SupabaseConfigBase extends AuthProviderConfig {\n /**\n * Optional tenant ID for multi-tenant setups.\n */\n tenantId?: string;\n}\n\n/**\n * Configuration for creating a new Supabase client.\n */\nexport interface SupabaseConfigWithCredentials extends SupabaseConfigBase {\n /**\n * Supabase project URL.\n */\n url: string;\n\n /**\n * Supabase anonymous/public key.\n */\n anonKey: string;\n\n /**\n * Additional Supabase client options.\n */\n options?: SupabaseAuthOptions;\n\n /**\n * When using credentials, client should not be provided.\n */\n client?: never;\n}\n\n/**\n * Configuration for using an existing Supabase client.\n */\nexport interface SupabaseConfigWithClient extends SupabaseConfigBase {\n /**\n * Pre-configured Supabase client instance.\n */\n client: SupabaseClient;\n\n /**\n * When using an existing client, credentials should not be provided.\n */\n url?: never;\n anonKey?: never;\n options?: never;\n}\n\n/**\n * Supabase configuration - either provide credentials to create a new client,\n * or provide an existing client instance.\n */\nexport type SupabaseConfig = SupabaseConfigWithCredentials | SupabaseConfigWithClient;\n\n/**\n * Type guard to check if config uses credentials.\n */\nexport function isCredentialsConfig(\n config: SupabaseConfig\n): config is SupabaseConfigWithCredentials {\n return \"url\" in config && \"anonKey\" in config && !(\"client\" in config && config.client);\n}\n\n/**\n * Type guard to check if config uses an existing client.\n */\nexport function isClientConfig(config: SupabaseConfig): config is SupabaseConfigWithClient {\n return \"client\" in config && config.client !== undefined;\n}\n"],"mappings":"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","names":["session"]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@weirdfingers/boards-auth-supabase",
3
+ "version": "0.9.13",
4
+ "description": "Supabase authentication provider for Boards",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsup",
10
+ "dev": "tsup --watch",
11
+ "typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
12
+ "lint": "eslint src --ext .ts,.tsx",
13
+ "test": "vitest --run --passWithNoTests"
14
+ },
15
+ "keywords": [
16
+ "boards",
17
+ "authentication",
18
+ "supabase",
19
+ "auth",
20
+ "react"
21
+ ],
22
+ "author": "WeirdFingers",
23
+ "license": "MIT",
24
+ "peerDependencies": {
25
+ "@supabase/supabase-js": "^2.50.2",
26
+ "@weirdfingers/boards": "workspace:*",
27
+ "react": "^18.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@supabase/supabase-js": "^2.50.2",
31
+ "@types/react": "^18.0.0",
32
+ "@typescript-eslint/eslint-plugin": "^8.42.0",
33
+ "@typescript-eslint/parser": "^8.42.0",
34
+ "@weirdfingers/boards": "workspace:*",
35
+ "eslint": "^8.0.0",
36
+ "tsup": "^8.0.0",
37
+ "typescript": "^5.0.0",
38
+ "vitest": "^1.0.0"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "README.md"
43
+ ],
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/weirdfingers/boards.git",
47
+ "directory": "packages/auth-supabase"
48
+ }
49
+ }