@supabase/server 0.2.0 → 1.0.0
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 +93 -92
- package/dist/adapters/h3/index.cjs +3 -3
- package/dist/adapters/h3/index.d.cts +3 -3
- package/dist/adapters/h3/index.d.mts +3 -3
- package/dist/adapters/h3/index.mjs +3 -3
- package/dist/adapters/hono/index.cjs +2 -2
- package/dist/adapters/hono/index.d.cts +2 -2
- package/dist/adapters/hono/index.d.mts +2 -2
- package/dist/adapters/hono/index.mjs +2 -2
- package/dist/core/index.cjs +1 -1
- package/dist/core/index.d.cts +25 -9
- package/dist/core/index.d.mts +25 -9
- package/dist/core/index.mjs +1 -1
- package/dist/{create-supabase-context-C_8SbO5w.cjs → create-supabase-context-B-2NDJhL.cjs} +10 -9
- package/dist/{create-supabase-context-DXD5rxi1.mjs → create-supabase-context-BBZtr3D2.mjs} +10 -9
- package/dist/{errors-Dyj5Cjt6.d.cts → errors-0dbzn5gA.d.mts} +1 -1
- package/dist/{errors-m42mkqhD.d.mts → errors-CZFEYnV_.d.cts} +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +5 -5
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +3 -3
- package/dist/{types-DKe8uOwI.d.mts → types-B2yXZjmG.d.mts} +40 -23
- package/dist/{types-DqhOaSlC.d.cts → types-u7fYLtzC.d.cts} +40 -23
- package/dist/{verify-auth-C4zqDlfj.cjs → verify-auth-BKZK83Y8.cjs} +66 -34
- package/dist/{verify-auth-CxFZy9rl.mjs → verify-auth-CZQd36s0.mjs} +66 -34
- package/docs/adapters/h3.md +180 -0
- package/docs/{hono-adapter.md → adapters/hono.md} +14 -25
- package/docs/api-reference.md +28 -15
- package/docs/auth-modes.md +38 -34
- package/docs/core-primitives.md +13 -13
- package/docs/environment-variables.md +17 -17
- package/docs/error-handling.md +4 -4
- package/docs/getting-started.md +17 -17
- package/docs/security.md +15 -15
- package/docs/ssr-frameworks.md +148 -172
- package/docs/typescript-generics.md +6 -6
- package/package.json +5 -3
- package/skills/supabase-server/SKILL.md +51 -44
|
@@ -58,7 +58,7 @@ const EnvErrorMap = {
|
|
|
58
58
|
* ```ts
|
|
59
59
|
* import { AuthError, createSupabaseContext } from '@supabase/server'
|
|
60
60
|
*
|
|
61
|
-
* const { data: ctx, error } = await createSupabaseContext(request, {
|
|
61
|
+
* const { data: ctx, error } = await createSupabaseContext(request, { auth: 'user' })
|
|
62
62
|
* if (error) {
|
|
63
63
|
* // error is an AuthError
|
|
64
64
|
* return Response.json(
|
|
@@ -249,7 +249,7 @@ function createAdminClient(options) {
|
|
|
249
249
|
*
|
|
250
250
|
* @example
|
|
251
251
|
* ```ts
|
|
252
|
-
* const { data: auth } = await verifyAuth(request, {
|
|
252
|
+
* const { data: auth } = await verifyAuth(request, { auth: 'user' })
|
|
253
253
|
* const supabase = createContextClient({
|
|
254
254
|
* auth: { token: auth.token, keyName: auth.keyName },
|
|
255
255
|
* })
|
|
@@ -320,6 +320,37 @@ function extractCredentials(request) {
|
|
|
320
320
|
};
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/core/utils/deprecation.ts
|
|
325
|
+
let allowDeprecationWarned = false;
|
|
326
|
+
/**
|
|
327
|
+
* Emits a one-time deprecation warning when the legacy `allow` option is used
|
|
328
|
+
* instead of `auth`. The warning fires at most once per process to avoid
|
|
329
|
+
* spamming logs in long-running servers.
|
|
330
|
+
*
|
|
331
|
+
* @internal
|
|
332
|
+
*/
|
|
333
|
+
function warnAllowDeprecated() {
|
|
334
|
+
if (allowDeprecationWarned) return;
|
|
335
|
+
allowDeprecationWarned = true;
|
|
336
|
+
console.warn("[@supabase/server] The `allow` option is deprecated and will be removed in a future major release. Use `auth` instead — e.g. `{ auth: \"user\" }` instead of `{ allow: \"user\" }`.");
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Resolves the auth mode from `auth` (preferred) or `allow` (deprecated),
|
|
340
|
+
* falling back to `"user"` when neither is provided. Emits a one-time
|
|
341
|
+
* deprecation warning when `allow` is used without `auth`.
|
|
342
|
+
*
|
|
343
|
+
* @internal
|
|
344
|
+
*/
|
|
345
|
+
function resolveAuthOption(options) {
|
|
346
|
+
if (options.auth !== void 0) return options.auth;
|
|
347
|
+
if (options.allow !== void 0) {
|
|
348
|
+
warnAllowDeprecated();
|
|
349
|
+
return options.allow;
|
|
350
|
+
}
|
|
351
|
+
return "user";
|
|
352
|
+
}
|
|
353
|
+
|
|
323
354
|
//#endregion
|
|
324
355
|
//#region src/core/utils/timing-safe-equal.ts
|
|
325
356
|
const encoder = new TextEncoder();
|
|
@@ -347,19 +378,19 @@ async function timingSafeEqual(a, b) {
|
|
|
347
378
|
//#endregion
|
|
348
379
|
//#region src/core/verify-credentials.ts
|
|
349
380
|
/**
|
|
350
|
-
* Parses an {@link
|
|
381
|
+
* Parses an {@link AuthModeWithKey} string into its base mode and optional key name.
|
|
351
382
|
*
|
|
352
383
|
* @example
|
|
353
384
|
* ```
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
385
|
+
* parseAuthMode('user') → { base: 'user', keyName: null }
|
|
386
|
+
* parseAuthMode('publishable:web') → { base: 'publishable', keyName: 'web' }
|
|
387
|
+
* parseAuthMode('secret:*') → { base: 'secret', keyName: '*' }
|
|
357
388
|
* ```
|
|
358
389
|
*
|
|
359
390
|
* @internal
|
|
360
391
|
*/
|
|
361
|
-
function
|
|
362
|
-
if (mode === "
|
|
392
|
+
function parseAuthMode(mode) {
|
|
393
|
+
if (mode === "none" || mode === "publishable" || mode === "secret" || mode === "user") return {
|
|
363
394
|
base: mode,
|
|
364
395
|
keyName: null
|
|
365
396
|
};
|
|
@@ -379,13 +410,13 @@ function parseAllowMode(mode) {
|
|
|
379
410
|
* Converts raw {@link JWTClaims} (snake_case) to a normalized {@link UserClaims} (camelCase).
|
|
380
411
|
* @internal
|
|
381
412
|
*/
|
|
382
|
-
function
|
|
413
|
+
function jwtClaimsToUserClaims(jwtClaims) {
|
|
383
414
|
return {
|
|
384
|
-
id:
|
|
385
|
-
role:
|
|
386
|
-
email:
|
|
387
|
-
appMetadata:
|
|
388
|
-
userMetadata:
|
|
415
|
+
id: jwtClaims.sub,
|
|
416
|
+
role: jwtClaims.role,
|
|
417
|
+
email: jwtClaims.email,
|
|
418
|
+
appMetadata: jwtClaims.app_metadata,
|
|
419
|
+
userMetadata: jwtClaims.user_metadata
|
|
389
420
|
};
|
|
390
421
|
}
|
|
391
422
|
const INVALID = Symbol("invalid");
|
|
@@ -400,34 +431,34 @@ const INVALID = Symbol("invalid");
|
|
|
400
431
|
* @internal
|
|
401
432
|
*/
|
|
402
433
|
async function tryMode(mode, credentials, env) {
|
|
403
|
-
const { base, keyName } =
|
|
434
|
+
const { base, keyName } = parseAuthMode(mode);
|
|
404
435
|
switch (base) {
|
|
405
|
-
case "
|
|
406
|
-
|
|
436
|
+
case "none": return {
|
|
437
|
+
authMode: "none",
|
|
407
438
|
token: null,
|
|
408
439
|
userClaims: null,
|
|
409
|
-
|
|
440
|
+
jwtClaims: null,
|
|
410
441
|
keyName: null
|
|
411
442
|
};
|
|
412
|
-
case "
|
|
443
|
+
case "publishable": {
|
|
413
444
|
if (!credentials.apikey) return null;
|
|
414
445
|
const keys = env.publishableKeys;
|
|
415
446
|
if (keyName === "*") {
|
|
416
447
|
for (const [name, value] of Object.entries(keys)) if (await timingSafeEqual(credentials.apikey, value)) return {
|
|
417
|
-
|
|
448
|
+
authMode: "publishable",
|
|
418
449
|
token: null,
|
|
419
450
|
userClaims: null,
|
|
420
|
-
|
|
451
|
+
jwtClaims: null,
|
|
421
452
|
keyName: name
|
|
422
453
|
};
|
|
423
454
|
} else {
|
|
424
455
|
const name = keyName ?? "default";
|
|
425
456
|
const value = keys[name];
|
|
426
457
|
if (value && await timingSafeEqual(credentials.apikey, value)) return {
|
|
427
|
-
|
|
458
|
+
authMode: "publishable",
|
|
428
459
|
token: null,
|
|
429
460
|
userClaims: null,
|
|
430
|
-
|
|
461
|
+
jwtClaims: null,
|
|
431
462
|
keyName: name
|
|
432
463
|
};
|
|
433
464
|
}
|
|
@@ -438,20 +469,20 @@ async function tryMode(mode, credentials, env) {
|
|
|
438
469
|
const keys = env.secretKeys;
|
|
439
470
|
if (keyName === "*") {
|
|
440
471
|
for (const [name, value] of Object.entries(keys)) if (await timingSafeEqual(credentials.apikey, value)) return {
|
|
441
|
-
|
|
472
|
+
authMode: "secret",
|
|
442
473
|
token: null,
|
|
443
474
|
userClaims: null,
|
|
444
|
-
|
|
475
|
+
jwtClaims: null,
|
|
445
476
|
keyName: name
|
|
446
477
|
};
|
|
447
478
|
} else {
|
|
448
479
|
const name = keyName ?? "default";
|
|
449
480
|
const value = keys[name];
|
|
450
481
|
if (value && await timingSafeEqual(credentials.apikey, value)) return {
|
|
451
|
-
|
|
482
|
+
authMode: "secret",
|
|
452
483
|
token: null,
|
|
453
484
|
userClaims: null,
|
|
454
|
-
|
|
485
|
+
jwtClaims: null,
|
|
455
486
|
keyName: name
|
|
456
487
|
};
|
|
457
488
|
}
|
|
@@ -464,12 +495,12 @@ async function tryMode(mode, credentials, env) {
|
|
|
464
495
|
const jwkSet = (0, jose.createLocalJWKSet)(env.jwks);
|
|
465
496
|
const { payload } = await (0, jose.jwtVerify)(credentials.token, jwkSet);
|
|
466
497
|
if (typeof payload.sub !== "string") return INVALID;
|
|
467
|
-
const
|
|
498
|
+
const jwtClaims = payload;
|
|
468
499
|
return {
|
|
469
|
-
|
|
500
|
+
authMode: "user",
|
|
470
501
|
token: credentials.token,
|
|
471
|
-
userClaims:
|
|
472
|
-
|
|
502
|
+
userClaims: jwtClaimsToUserClaims(jwtClaims),
|
|
503
|
+
jwtClaims,
|
|
473
504
|
keyName: null
|
|
474
505
|
};
|
|
475
506
|
} catch {
|
|
@@ -495,7 +526,7 @@ async function tryMode(mode, credentials, env) {
|
|
|
495
526
|
* ```ts
|
|
496
527
|
* const credentials = extractCredentials(request)
|
|
497
528
|
* const { data: auth, error } = await verifyCredentials(credentials, {
|
|
498
|
-
*
|
|
529
|
+
* auth: ['user', 'publishable'],
|
|
499
530
|
* })
|
|
500
531
|
* if (error) {
|
|
501
532
|
* return Response.json({ message: error.message }, { status: error.status })
|
|
@@ -508,7 +539,8 @@ async function verifyCredentials(credentials, options) {
|
|
|
508
539
|
data: null,
|
|
509
540
|
error: new AuthError(envError.message, envError.code, 500)
|
|
510
541
|
};
|
|
511
|
-
const
|
|
542
|
+
const resolved = resolveAuthOption(options);
|
|
543
|
+
const modes = Array.isArray(resolved) ? resolved : [resolved];
|
|
512
544
|
for (const mode of modes) {
|
|
513
545
|
const result = await tryMode(mode, credentials, env);
|
|
514
546
|
if (result === INVALID) return {
|
|
@@ -547,7 +579,7 @@ async function verifyCredentials(credentials, options) {
|
|
|
547
579
|
* import { verifyAuth } from '@supabase/server/core'
|
|
548
580
|
*
|
|
549
581
|
* const { data: auth, error } = await verifyAuth(request, {
|
|
550
|
-
*
|
|
582
|
+
* auth: 'user',
|
|
551
583
|
* })
|
|
552
584
|
*
|
|
553
585
|
* if (error) {
|
|
@@ -58,7 +58,7 @@ const EnvErrorMap = {
|
|
|
58
58
|
* ```ts
|
|
59
59
|
* import { AuthError, createSupabaseContext } from '@supabase/server'
|
|
60
60
|
*
|
|
61
|
-
* const { data: ctx, error } = await createSupabaseContext(request, {
|
|
61
|
+
* const { data: ctx, error } = await createSupabaseContext(request, { auth: 'user' })
|
|
62
62
|
* if (error) {
|
|
63
63
|
* // error is an AuthError
|
|
64
64
|
* return Response.json(
|
|
@@ -249,7 +249,7 @@ function createAdminClient(options) {
|
|
|
249
249
|
*
|
|
250
250
|
* @example
|
|
251
251
|
* ```ts
|
|
252
|
-
* const { data: auth } = await verifyAuth(request, {
|
|
252
|
+
* const { data: auth } = await verifyAuth(request, { auth: 'user' })
|
|
253
253
|
* const supabase = createContextClient({
|
|
254
254
|
* auth: { token: auth.token, keyName: auth.keyName },
|
|
255
255
|
* })
|
|
@@ -320,6 +320,37 @@ function extractCredentials(request) {
|
|
|
320
320
|
};
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/core/utils/deprecation.ts
|
|
325
|
+
let allowDeprecationWarned = false;
|
|
326
|
+
/**
|
|
327
|
+
* Emits a one-time deprecation warning when the legacy `allow` option is used
|
|
328
|
+
* instead of `auth`. The warning fires at most once per process to avoid
|
|
329
|
+
* spamming logs in long-running servers.
|
|
330
|
+
*
|
|
331
|
+
* @internal
|
|
332
|
+
*/
|
|
333
|
+
function warnAllowDeprecated() {
|
|
334
|
+
if (allowDeprecationWarned) return;
|
|
335
|
+
allowDeprecationWarned = true;
|
|
336
|
+
console.warn("[@supabase/server] The `allow` option is deprecated and will be removed in a future major release. Use `auth` instead — e.g. `{ auth: \"user\" }` instead of `{ allow: \"user\" }`.");
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Resolves the auth mode from `auth` (preferred) or `allow` (deprecated),
|
|
340
|
+
* falling back to `"user"` when neither is provided. Emits a one-time
|
|
341
|
+
* deprecation warning when `allow` is used without `auth`.
|
|
342
|
+
*
|
|
343
|
+
* @internal
|
|
344
|
+
*/
|
|
345
|
+
function resolveAuthOption(options) {
|
|
346
|
+
if (options.auth !== void 0) return options.auth;
|
|
347
|
+
if (options.allow !== void 0) {
|
|
348
|
+
warnAllowDeprecated();
|
|
349
|
+
return options.allow;
|
|
350
|
+
}
|
|
351
|
+
return "user";
|
|
352
|
+
}
|
|
353
|
+
|
|
323
354
|
//#endregion
|
|
324
355
|
//#region src/core/utils/timing-safe-equal.ts
|
|
325
356
|
const encoder = new TextEncoder();
|
|
@@ -347,19 +378,19 @@ async function timingSafeEqual(a, b) {
|
|
|
347
378
|
//#endregion
|
|
348
379
|
//#region src/core/verify-credentials.ts
|
|
349
380
|
/**
|
|
350
|
-
* Parses an {@link
|
|
381
|
+
* Parses an {@link AuthModeWithKey} string into its base mode and optional key name.
|
|
351
382
|
*
|
|
352
383
|
* @example
|
|
353
384
|
* ```
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
385
|
+
* parseAuthMode('user') → { base: 'user', keyName: null }
|
|
386
|
+
* parseAuthMode('publishable:web') → { base: 'publishable', keyName: 'web' }
|
|
387
|
+
* parseAuthMode('secret:*') → { base: 'secret', keyName: '*' }
|
|
357
388
|
* ```
|
|
358
389
|
*
|
|
359
390
|
* @internal
|
|
360
391
|
*/
|
|
361
|
-
function
|
|
362
|
-
if (mode === "
|
|
392
|
+
function parseAuthMode(mode) {
|
|
393
|
+
if (mode === "none" || mode === "publishable" || mode === "secret" || mode === "user") return {
|
|
363
394
|
base: mode,
|
|
364
395
|
keyName: null
|
|
365
396
|
};
|
|
@@ -379,13 +410,13 @@ function parseAllowMode(mode) {
|
|
|
379
410
|
* Converts raw {@link JWTClaims} (snake_case) to a normalized {@link UserClaims} (camelCase).
|
|
380
411
|
* @internal
|
|
381
412
|
*/
|
|
382
|
-
function
|
|
413
|
+
function jwtClaimsToUserClaims(jwtClaims) {
|
|
383
414
|
return {
|
|
384
|
-
id:
|
|
385
|
-
role:
|
|
386
|
-
email:
|
|
387
|
-
appMetadata:
|
|
388
|
-
userMetadata:
|
|
415
|
+
id: jwtClaims.sub,
|
|
416
|
+
role: jwtClaims.role,
|
|
417
|
+
email: jwtClaims.email,
|
|
418
|
+
appMetadata: jwtClaims.app_metadata,
|
|
419
|
+
userMetadata: jwtClaims.user_metadata
|
|
389
420
|
};
|
|
390
421
|
}
|
|
391
422
|
const INVALID = Symbol("invalid");
|
|
@@ -400,34 +431,34 @@ const INVALID = Symbol("invalid");
|
|
|
400
431
|
* @internal
|
|
401
432
|
*/
|
|
402
433
|
async function tryMode(mode, credentials, env) {
|
|
403
|
-
const { base, keyName } =
|
|
434
|
+
const { base, keyName } = parseAuthMode(mode);
|
|
404
435
|
switch (base) {
|
|
405
|
-
case "
|
|
406
|
-
|
|
436
|
+
case "none": return {
|
|
437
|
+
authMode: "none",
|
|
407
438
|
token: null,
|
|
408
439
|
userClaims: null,
|
|
409
|
-
|
|
440
|
+
jwtClaims: null,
|
|
410
441
|
keyName: null
|
|
411
442
|
};
|
|
412
|
-
case "
|
|
443
|
+
case "publishable": {
|
|
413
444
|
if (!credentials.apikey) return null;
|
|
414
445
|
const keys = env.publishableKeys;
|
|
415
446
|
if (keyName === "*") {
|
|
416
447
|
for (const [name, value] of Object.entries(keys)) if (await timingSafeEqual(credentials.apikey, value)) return {
|
|
417
|
-
|
|
448
|
+
authMode: "publishable",
|
|
418
449
|
token: null,
|
|
419
450
|
userClaims: null,
|
|
420
|
-
|
|
451
|
+
jwtClaims: null,
|
|
421
452
|
keyName: name
|
|
422
453
|
};
|
|
423
454
|
} else {
|
|
424
455
|
const name = keyName ?? "default";
|
|
425
456
|
const value = keys[name];
|
|
426
457
|
if (value && await timingSafeEqual(credentials.apikey, value)) return {
|
|
427
|
-
|
|
458
|
+
authMode: "publishable",
|
|
428
459
|
token: null,
|
|
429
460
|
userClaims: null,
|
|
430
|
-
|
|
461
|
+
jwtClaims: null,
|
|
431
462
|
keyName: name
|
|
432
463
|
};
|
|
433
464
|
}
|
|
@@ -438,20 +469,20 @@ async function tryMode(mode, credentials, env) {
|
|
|
438
469
|
const keys = env.secretKeys;
|
|
439
470
|
if (keyName === "*") {
|
|
440
471
|
for (const [name, value] of Object.entries(keys)) if (await timingSafeEqual(credentials.apikey, value)) return {
|
|
441
|
-
|
|
472
|
+
authMode: "secret",
|
|
442
473
|
token: null,
|
|
443
474
|
userClaims: null,
|
|
444
|
-
|
|
475
|
+
jwtClaims: null,
|
|
445
476
|
keyName: name
|
|
446
477
|
};
|
|
447
478
|
} else {
|
|
448
479
|
const name = keyName ?? "default";
|
|
449
480
|
const value = keys[name];
|
|
450
481
|
if (value && await timingSafeEqual(credentials.apikey, value)) return {
|
|
451
|
-
|
|
482
|
+
authMode: "secret",
|
|
452
483
|
token: null,
|
|
453
484
|
userClaims: null,
|
|
454
|
-
|
|
485
|
+
jwtClaims: null,
|
|
455
486
|
keyName: name
|
|
456
487
|
};
|
|
457
488
|
}
|
|
@@ -464,12 +495,12 @@ async function tryMode(mode, credentials, env) {
|
|
|
464
495
|
const jwkSet = createLocalJWKSet(env.jwks);
|
|
465
496
|
const { payload } = await jwtVerify(credentials.token, jwkSet);
|
|
466
497
|
if (typeof payload.sub !== "string") return INVALID;
|
|
467
|
-
const
|
|
498
|
+
const jwtClaims = payload;
|
|
468
499
|
return {
|
|
469
|
-
|
|
500
|
+
authMode: "user",
|
|
470
501
|
token: credentials.token,
|
|
471
|
-
userClaims:
|
|
472
|
-
|
|
502
|
+
userClaims: jwtClaimsToUserClaims(jwtClaims),
|
|
503
|
+
jwtClaims,
|
|
473
504
|
keyName: null
|
|
474
505
|
};
|
|
475
506
|
} catch {
|
|
@@ -495,7 +526,7 @@ async function tryMode(mode, credentials, env) {
|
|
|
495
526
|
* ```ts
|
|
496
527
|
* const credentials = extractCredentials(request)
|
|
497
528
|
* const { data: auth, error } = await verifyCredentials(credentials, {
|
|
498
|
-
*
|
|
529
|
+
* auth: ['user', 'publishable'],
|
|
499
530
|
* })
|
|
500
531
|
* if (error) {
|
|
501
532
|
* return Response.json({ message: error.message }, { status: error.status })
|
|
@@ -508,7 +539,8 @@ async function verifyCredentials(credentials, options) {
|
|
|
508
539
|
data: null,
|
|
509
540
|
error: new AuthError(envError.message, envError.code, 500)
|
|
510
541
|
};
|
|
511
|
-
const
|
|
542
|
+
const resolved = resolveAuthOption(options);
|
|
543
|
+
const modes = Array.isArray(resolved) ? resolved : [resolved];
|
|
512
544
|
for (const mode of modes) {
|
|
513
545
|
const result = await tryMode(mode, credentials, env);
|
|
514
546
|
if (result === INVALID) return {
|
|
@@ -547,7 +579,7 @@ async function verifyCredentials(credentials, options) {
|
|
|
547
579
|
* import { verifyAuth } from '@supabase/server/core'
|
|
548
580
|
*
|
|
549
581
|
* const { data: auth, error } = await verifyAuth(request, {
|
|
550
|
-
*
|
|
582
|
+
* auth: 'user',
|
|
551
583
|
* })
|
|
552
584
|
*
|
|
553
585
|
* if (error) {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# H3 / Nuxt Adapter
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
Install H3 as a peer dependency:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add h3
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The adapter exports its own `withSupabase` that returns H3 middleware instead of a fetch handler. Works with standalone H3 servers and Nuxt server routes (which run on H3 under the hood).
|
|
12
|
+
|
|
13
|
+
## Basic app with auth
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { H3 } from 'h3'
|
|
17
|
+
import { withSupabase } from '@supabase/server/adapters/h3'
|
|
18
|
+
|
|
19
|
+
const app = new H3()
|
|
20
|
+
|
|
21
|
+
// Apply auth to all routes
|
|
22
|
+
app.use(withSupabase({ auth: 'user' }))
|
|
23
|
+
|
|
24
|
+
app.get('/todos', async (event) => {
|
|
25
|
+
const { supabase } = event.context.supabaseContext
|
|
26
|
+
const { data } = await supabase.from('todos').select()
|
|
27
|
+
return data
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
app.get('/profile', async (event) => {
|
|
31
|
+
const { supabase, userClaims } = event.context.supabaseContext
|
|
32
|
+
const { data } = await supabase
|
|
33
|
+
.from('profiles')
|
|
34
|
+
.select()
|
|
35
|
+
.eq('id', userClaims!.id)
|
|
36
|
+
return data
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
export default { fetch: app.fetch }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The context is stored in `event.context.supabaseContext` and contains the same `SupabaseContext` fields as the main `withSupabase` wrapper: `supabase`, `supabaseAdmin`, `userClaims`, `jwtClaims`, and `authMode`.
|
|
43
|
+
|
|
44
|
+
## Per-route auth
|
|
45
|
+
|
|
46
|
+
Apply different auth modes to different routes by attaching the middleware to specific handlers:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { H3 } from 'h3'
|
|
50
|
+
import { withSupabase } from '@supabase/server/adapters/h3'
|
|
51
|
+
|
|
52
|
+
const app = new H3()
|
|
53
|
+
|
|
54
|
+
// Public route — no auth
|
|
55
|
+
app.get('/health', () => ({ status: 'ok' }))
|
|
56
|
+
|
|
57
|
+
// User-authenticated route
|
|
58
|
+
app.get('/todos', withSupabase({ auth: 'user' }), async (event) => {
|
|
59
|
+
const { supabase } = event.context.supabaseContext
|
|
60
|
+
const { data } = await supabase.from('todos').select()
|
|
61
|
+
return data
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Secret-key-protected admin route
|
|
65
|
+
app.post('/admin/sync', withSupabase({ auth: 'secret' }), async (event) => {
|
|
66
|
+
const { supabaseAdmin } = event.context.supabaseContext
|
|
67
|
+
const { data } = await supabaseAdmin
|
|
68
|
+
.from('audit_log')
|
|
69
|
+
.insert({ action: 'sync' })
|
|
70
|
+
return data
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Dual auth — users or services
|
|
74
|
+
app.get(
|
|
75
|
+
'/reports',
|
|
76
|
+
withSupabase({ auth: ['user', 'secret'] }),
|
|
77
|
+
async (event) => {
|
|
78
|
+
const { authMode } = event.context.supabaseContext
|
|
79
|
+
return { authMode }
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
export default { fetch: app.fetch }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Nuxt: file-based routes
|
|
87
|
+
|
|
88
|
+
Use `defineHandler` to attach auth to a single Nuxt server route:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// server/api/games.get.ts
|
|
92
|
+
import { defineHandler } from 'h3'
|
|
93
|
+
import { withSupabase } from '@supabase/server/adapters/h3'
|
|
94
|
+
|
|
95
|
+
export default defineHandler({
|
|
96
|
+
middleware: [withSupabase({ auth: 'user' })],
|
|
97
|
+
handler: async (event) => {
|
|
98
|
+
const { supabase } = event.context.supabaseContext
|
|
99
|
+
return supabase.from('favorite_games').select()
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Nuxt: app-wide auth
|
|
105
|
+
|
|
106
|
+
Register as a server middleware to apply auth to every route:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// server/middleware/supabase.ts
|
|
110
|
+
import { withSupabase } from '@supabase/server/adapters/h3'
|
|
111
|
+
|
|
112
|
+
export default withSupabase({ auth: 'user' })
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Skip behavior
|
|
116
|
+
|
|
117
|
+
If a previous middleware already set `event.context.supabaseContext`, subsequent `withSupabase` calls skip auth. This enables a pattern where route-level middleware overrides the app-wide default:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const app = new H3()
|
|
121
|
+
|
|
122
|
+
// App-wide: require user auth
|
|
123
|
+
app.use(withSupabase({ auth: 'user' }))
|
|
124
|
+
|
|
125
|
+
// This route needs secret auth instead.
|
|
126
|
+
// The route-level middleware runs first, sets the context,
|
|
127
|
+
// and the app-wide middleware skips.
|
|
128
|
+
app.post('/webhook', withSupabase({ auth: 'secret' }), async (event) => {
|
|
129
|
+
const { supabaseAdmin } = event.context.supabaseContext
|
|
130
|
+
// ...
|
|
131
|
+
})
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## CORS
|
|
135
|
+
|
|
136
|
+
The H3 adapter does not handle CORS — the `cors` option is excluded from its config type. Use H3's built-in CORS utilities:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { H3, handleCors } from 'h3'
|
|
140
|
+
import { withSupabase } from '@supabase/server/adapters/h3'
|
|
141
|
+
|
|
142
|
+
const app = new H3()
|
|
143
|
+
|
|
144
|
+
app.use((event) => handleCors(event, { origin: '*' }))
|
|
145
|
+
app.use(withSupabase({ auth: 'user' }))
|
|
146
|
+
|
|
147
|
+
app.get('/todos', async (event) => {
|
|
148
|
+
const { supabase } = event.context.supabaseContext
|
|
149
|
+
const { data } = await supabase.from('todos').select()
|
|
150
|
+
return data
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
export default { fetch: app.fetch }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Environment overrides
|
|
157
|
+
|
|
158
|
+
Pass `env` to override auto-detected environment variables, same as the main wrapper:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
app.use(
|
|
162
|
+
withSupabase({
|
|
163
|
+
auth: 'user',
|
|
164
|
+
env: { url: 'http://localhost:54321' },
|
|
165
|
+
}),
|
|
166
|
+
)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Supabase client options
|
|
170
|
+
|
|
171
|
+
Forward options to the underlying `createClient()` calls:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
app.use(
|
|
175
|
+
withSupabase({
|
|
176
|
+
auth: 'user',
|
|
177
|
+
supabaseOptions: { db: { schema: 'api' } },
|
|
178
|
+
}),
|
|
179
|
+
)
|
|
180
|
+
```
|