tinbase 0.10.0 → 0.10.2
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 +10 -9
- package/dist/admin/api.d.ts +62 -1
- package/dist/admin/api.js +221 -10
- package/dist/admin/api.js.map +1 -1
- package/dist/admin/ui.d.ts +1 -1
- package/dist/admin/ui.js +1548 -16
- package/dist/admin/ui.js.map +1 -1
- package/dist/auth/handler.d.ts +52 -5
- package/dist/auth/handler.js +147 -36
- package/dist/auth/handler.js.map +1 -1
- package/dist/auth/inbox.d.ts +4 -2
- package/dist/auth/inbox.js +1 -0
- package/dist/auth/inbox.js.map +1 -1
- package/dist/auth/oauth.d.ts +30 -4
- package/dist/auth/oauth.js +22 -6
- package/dist/auth/oauth.js.map +1 -1
- package/dist/auth/password.d.ts +5 -0
- package/dist/auth/password.js +7 -0
- package/dist/auth/password.js.map +1 -1
- package/dist/auth/qr.d.ts +1 -1
- package/dist/auth/qr.js +1 -1
- package/dist/auth/rate-limit.d.ts +33 -0
- package/dist/auth/rate-limit.js +63 -0
- package/dist/auth/rate-limit.js.map +1 -0
- package/dist/auth/redirect.d.ts +20 -0
- package/dist/auth/redirect.js +79 -0
- package/dist/auth/redirect.js.map +1 -0
- package/dist/auth/settings.d.ts +55 -0
- package/dist/auth/settings.js +116 -0
- package/dist/auth/settings.js.map +1 -0
- package/dist/auth/totp.js +1 -1
- package/dist/cli.js +43 -21
- package/dist/cli.js.map +1 -1
- package/dist/cron/service.d.ts +15 -2
- package/dist/cron/service.js +19 -2
- package/dist/cron/service.js.map +1 -1
- package/dist/db/bootstrap.d.ts +13 -6
- package/dist/db/bootstrap.js +26 -5
- package/dist/db/bootstrap.js.map +1 -1
- package/dist/db/database.d.ts +61 -3
- package/dist/db/database.js +72 -9
- package/dist/db/database.js.map +1 -1
- package/dist/db/emulated.d.ts +39 -2
- package/dist/db/emulated.js +39 -23
- package/dist/db/emulated.js.map +1 -1
- package/dist/db/engine.d.ts +19 -5
- package/dist/db/engine.js +4 -2
- package/dist/db/engine.js.map +1 -1
- package/dist/db/inspect.d.ts +6 -1
- package/dist/db/inspect.js +2 -1
- package/dist/db/inspect.js.map +1 -1
- package/dist/db/pglite-engine.d.ts +7 -1
- package/dist/db/pglite-engine.js +7 -1
- package/dist/db/pglite-engine.js.map +1 -1
- package/dist/db/pgmem-engine.d.ts +9 -3
- package/dist/db/pgmem-engine.js +13 -7
- package/dist/db/pgmem-engine.js.map +1 -1
- package/dist/db/schema-diff.d.ts +9 -1
- package/dist/db/schema-diff.js +19 -5
- package/dist/db/schema-diff.js.map +1 -1
- package/dist/db/sql-compat.d.ts +2 -2
- package/dist/db/sql-compat.js +2 -2
- package/dist/functions/deno-shim.d.ts +6 -17
- package/dist/functions/deno-shim.js +42 -10
- package/dist/functions/deno-shim.js.map +1 -1
- package/dist/functions/handler.d.ts +9 -4
- package/dist/functions/handler.js +9 -1
- package/dist/functions/handler.js.map +1 -1
- package/dist/gen-types.d.ts +6 -1
- package/dist/gen-types.js +53 -41
- package/dist/gen-types.js.map +1 -1
- package/dist/index.d.ts +33 -3
- package/dist/index.js +158 -28
- package/dist/index.js.map +1 -1
- package/dist/jwt.d.ts +4 -1
- package/dist/jwt.js +4 -2
- package/dist/jwt.js.map +1 -1
- package/dist/log-buffer.d.ts +7 -1
- package/dist/log-buffer.js +3 -0
- package/dist/log-buffer.js.map +1 -1
- package/dist/net/service.d.ts +35 -6
- package/dist/net/service.js +72 -17
- package/dist/net/service.js.map +1 -1
- package/dist/node/bun-server.d.ts +2 -1
- package/dist/node/bun-server.js +11 -1
- package/dist/node/bun-server.js.map +1 -1
- package/dist/node/bundle-function.d.ts +1 -1
- package/dist/node/bundle-function.js +1 -1
- package/dist/node/config-toml.d.ts +31 -0
- package/dist/node/config-toml.js +186 -0
- package/dist/node/config-toml.js.map +1 -0
- package/dist/node/db-diff.d.ts +13 -1
- package/dist/node/db-diff.js +3 -1
- package/dist/node/db-diff.js.map +1 -1
- package/dist/node/fs-driver.d.ts +16 -1
- package/dist/node/fs-driver.js +20 -1
- package/dist/node/fs-driver.js.map +1 -1
- package/dist/node/index.d.ts +2 -2
- package/dist/node/index.js +1 -1
- package/dist/node/index.js.map +1 -1
- package/dist/node/load-config.d.ts +80 -0
- package/dist/node/load-config.js +230 -0
- package/dist/node/load-config.js.map +1 -0
- package/dist/node/load-functions.d.ts +17 -2
- package/dist/node/load-functions.js +17 -4
- package/dist/node/load-functions.js.map +1 -1
- package/dist/node/native/engine.d.ts +7 -0
- package/dist/node/native/engine.js +58 -7
- package/dist/node/native/engine.js.map +1 -1
- package/dist/node/native/wire.d.ts +15 -1
- package/dist/node/native/wire.js +32 -10
- package/dist/node/native/wire.js.map +1 -1
- package/dist/node/project.d.ts +15 -1
- package/dist/node/project.js +19 -7
- package/dist/node/project.js.map +1 -1
- package/dist/node/server-shared.d.ts +4 -1
- package/dist/node/server.d.ts +11 -0
- package/dist/node/server.js +11 -0
- package/dist/node/server.js.map +1 -1
- package/dist/node/ws.d.ts +26 -1
- package/dist/node/ws.js +26 -3
- package/dist/node/ws.js.map +1 -1
- package/dist/realtime/engine.d.ts +6 -0
- package/dist/realtime/engine.js +45 -34
- package/dist/realtime/engine.js.map +1 -1
- package/dist/rest/build.d.ts +37 -1
- package/dist/rest/build.js +48 -8
- package/dist/rest/build.js.map +1 -1
- package/dist/rest/errors.d.ts +8 -0
- package/dist/rest/errors.js +12 -1
- package/dist/rest/errors.js.map +1 -1
- package/dist/rest/handler.d.ts +17 -1
- package/dist/rest/handler.js +48 -5
- package/dist/rest/handler.js.map +1 -1
- package/dist/rest/parse.d.ts +24 -1
- package/dist/rest/parse.js +7 -1
- package/dist/rest/parse.js.map +1 -1
- package/dist/retention/service.d.ts +3 -0
- package/dist/retention/service.js +3 -1
- package/dist/retention/service.js.map +1 -1
- package/dist/security.d.ts +22 -0
- package/dist/security.js +47 -0
- package/dist/security.js.map +1 -0
- package/dist/storage/driver.d.ts +8 -1
- package/dist/storage/driver.js +4 -1
- package/dist/storage/driver.js.map +1 -1
- package/dist/storage/handler.d.ts +10 -1
- package/dist/storage/handler.js +93 -12
- package/dist/storage/handler.js.map +1 -1
- package/dist/types.d.ts +77 -1
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/dist/webhooks/service.d.ts +25 -2
- package/dist/webhooks/service.js +21 -3
- package/dist/webhooks/service.js.map +1 -1
- package/package.json +5 -3
package/dist/admin/ui.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/admin/ui.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG
|
|
1
|
+
{"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/admin/ui.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4jDzB,CAAA"}
|
package/dist/auth/handler.d.ts
CHANGED
|
@@ -1,18 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GoTrue-compatible auth endpoints (/auth/v1/*)
|
|
2
|
+
* GoTrue-compatible auth endpoints (/auth/v1/*) - the subset supabase-js
|
|
3
3
|
* uses for email/password auth, sessions, and admin user management.
|
|
4
4
|
*/
|
|
5
5
|
import type { Database } from '../db/database.js';
|
|
6
|
-
import type
|
|
6
|
+
import { type Mailer, type RequestContext } from '../types.js';
|
|
7
7
|
import { type OAuthProviderConfig } from './oauth.js';
|
|
8
|
+
import { type AuthSettings } from './settings.js';
|
|
9
|
+
import { RateLimiter } from './rate-limit.js';
|
|
10
|
+
/** Construction-time config for {@link AuthHandler}. */
|
|
8
11
|
export interface AuthConfig {
|
|
12
|
+
/** HS256 secret used to sign and verify access tokens */
|
|
9
13
|
jwtSecret: string;
|
|
14
|
+
/** public base URL of this instance; used as issuer and default redirect target */
|
|
10
15
|
siteUrl: string;
|
|
16
|
+
/** Access-token lifetime in seconds. */
|
|
11
17
|
jwtExpiry: number;
|
|
18
|
+
/** Force sign-out after this many seconds (config.toml auth.sessions.timebox). Caps session lifetime. */
|
|
19
|
+
sessionTimeboxSeconds?: number;
|
|
20
|
+
/** sends outgoing auth email (magic links, OTP codes, recovery) */
|
|
12
21
|
mailer: Mailer;
|
|
22
|
+
/** OAuth providers to enable, keyed by provider name (google, github, …) */
|
|
13
23
|
oauthProviders?: Record<string, OAuthProviderConfig>;
|
|
14
24
|
/** injectable fetch for the OAuth provider calls (tests use a mock provider) */
|
|
15
25
|
oauthFetch?: typeof fetch;
|
|
26
|
+
/**
|
|
27
|
+
* Additional redirect targets allowed beyond the site URL's own origin
|
|
28
|
+
* (GoTrue's URI_ALLOW_LIST). Entries may use `*`/`**` globs. A `redirect_to`
|
|
29
|
+
* that matches neither the site origin nor an entry falls back to the site URL.
|
|
30
|
+
*/
|
|
31
|
+
uriAllowList?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* Enforce the redirect allowlist strictly. Off for local dev (any well-formed
|
|
34
|
+
* URL is honored, like `supabase start`); the backend turns it on when
|
|
35
|
+
* network-exposed so redirects can't leave the allowed origins.
|
|
36
|
+
*/
|
|
37
|
+
enforceRedirectAllowList?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Runtime-mutable toggles (signups, anonymous users, autoconfirm…). The
|
|
40
|
+
* admin API mutates this same object in place, so changes apply instantly.
|
|
41
|
+
*/
|
|
42
|
+
settings?: AuthSettings;
|
|
43
|
+
/**
|
|
44
|
+
* Rate limiter for login/signup/OTP/recovery. Defaults to a fresh in-memory
|
|
45
|
+
* limiter with GoTrue-shaped windows; pass `null` to disable (e.g. tests).
|
|
46
|
+
*/
|
|
47
|
+
rateLimiter?: RateLimiter | null;
|
|
16
48
|
}
|
|
17
49
|
interface UserRow {
|
|
18
50
|
id: string;
|
|
@@ -30,11 +62,23 @@ interface UserRow {
|
|
|
30
62
|
phone_confirmed_at: Date | string | null;
|
|
31
63
|
is_anonymous: boolean | null;
|
|
32
64
|
}
|
|
65
|
+
/** Routes and services the GoTrue-compatible `/auth/v1/*` endpoints. */
|
|
33
66
|
export declare class AuthHandler {
|
|
34
67
|
private db;
|
|
35
68
|
private config;
|
|
36
69
|
private oauth;
|
|
70
|
+
/** Shared, runtime-mutable settings - read on every request, never copied. */
|
|
71
|
+
private settings;
|
|
72
|
+
private rateLimiter;
|
|
37
73
|
constructor(db: Database, config: AuthConfig);
|
|
74
|
+
/**
|
|
75
|
+
* Enforce the rate limit for `action`, keyed by client address. Returns a 429
|
|
76
|
+
* (GoTrue's `over_request_rate_limit`) when exceeded, else null to proceed.
|
|
77
|
+
*/
|
|
78
|
+
private limit;
|
|
79
|
+
/** Stop background timers (rate-limiter sweep). Called on backend close. */
|
|
80
|
+
stop(): void;
|
|
81
|
+
/** Dispatch one `/auth/v1/*` request. Any thrown error becomes a 500 `unexpected_failure`. */
|
|
38
82
|
handle(req: Request, ctx: RequestContext, url: URL): Promise<Response>;
|
|
39
83
|
private signup;
|
|
40
84
|
private token;
|
|
@@ -58,7 +102,7 @@ export declare class AuthHandler {
|
|
|
58
102
|
/**
|
|
59
103
|
* Export everything held about one user across the auth schema, for a GDPR
|
|
60
104
|
* right-of-access / portability request. Credentials (password hash, MFA
|
|
61
|
-
* secrets, raw token values) are deliberately omitted
|
|
105
|
+
* secrets, raw token values) are deliberately omitted - they are not personal
|
|
62
106
|
* data to hand back and exporting them would leak secrets.
|
|
63
107
|
*/
|
|
64
108
|
private exportUser;
|
|
@@ -69,7 +113,7 @@ export declare class AuthHandler {
|
|
|
69
113
|
* doesn't exist and a summary of what was erased.
|
|
70
114
|
*
|
|
71
115
|
* Note: storage.objects.owner has no FK to auth.users, so object rows/bytes
|
|
72
|
-
* owned by the user are not removed here
|
|
116
|
+
* owned by the user are not removed here - see COMPLIANCE.md for the
|
|
73
117
|
* storage-erasure step the operator must run.
|
|
74
118
|
*/
|
|
75
119
|
private eraseUser;
|
|
@@ -78,8 +122,11 @@ export declare class AuthHandler {
|
|
|
78
122
|
private verifyFactor;
|
|
79
123
|
private unenrollFactor;
|
|
80
124
|
private getUserFactors;
|
|
125
|
+
/** GoTrue-shaped identities for a user (linked providers), from auth.identities. */
|
|
126
|
+
private getUserIdentities;
|
|
81
127
|
private userFromBearer;
|
|
82
|
-
|
|
128
|
+
/** Shape a user row into the GoTrue user object supabase-js expects. */
|
|
129
|
+
userJson(u: UserRow, factors?: Record<string, unknown>[], identities?: Record<string, unknown>[]): Record<string, unknown>;
|
|
83
130
|
/** Session tokens for a bare user id (used by the OAuth implicit callback). */
|
|
84
131
|
private sessionTokensFor;
|
|
85
132
|
private sessionFor;
|
package/dist/auth/handler.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { randomToken, signJwt, verifyJwt } from '../jwt.js';
|
|
2
|
+
import { TINBASE_VERSION } from '../types.js';
|
|
2
3
|
import { OAuthService } from './oauth.js';
|
|
3
4
|
import { hashPassword, verifyPassword } from './password.js';
|
|
4
5
|
import { qrSvgDataUri } from './qr.js';
|
|
6
|
+
import { DEFAULT_AUTH_SETTINGS } from './settings.js';
|
|
7
|
+
import { resolveRedirect } from './redirect.js';
|
|
8
|
+
import { RateLimiter } from './rate-limit.js';
|
|
5
9
|
import { generateTotpSecret, otpauthUri, verifyTotp } from './totp.js';
|
|
6
10
|
function authError(status, errorCode, msg) {
|
|
7
11
|
return json(status, { code: status, error_code: errorCode, msg });
|
|
8
12
|
}
|
|
13
|
+
/** A cryptographically-random numeric OTP of `length` digits (6-10). */
|
|
14
|
+
function randomOtp(length) {
|
|
15
|
+
const n = Math.max(6, Math.min(10, Math.floor(length)));
|
|
16
|
+
const buf = new Uint32Array(n);
|
|
17
|
+
crypto.getRandomValues(buf);
|
|
18
|
+
let code = '';
|
|
19
|
+
for (let i = 0; i < n; i++)
|
|
20
|
+
code += String(buf[i] % 10);
|
|
21
|
+
return code;
|
|
22
|
+
}
|
|
9
23
|
function json(status, body) {
|
|
10
24
|
return new Response(status === 204 ? null : JSON.stringify(body), {
|
|
11
25
|
status,
|
|
@@ -17,33 +31,64 @@ function iso(v) {
|
|
|
17
31
|
return null;
|
|
18
32
|
return v instanceof Date ? v.toISOString() : new Date(v).toISOString();
|
|
19
33
|
}
|
|
34
|
+
/** Routes and services the GoTrue-compatible `/auth/v1/*` endpoints. */
|
|
20
35
|
export class AuthHandler {
|
|
21
36
|
db;
|
|
22
37
|
config;
|
|
23
38
|
oauth;
|
|
39
|
+
/** Shared, runtime-mutable settings - read on every request, never copied. */
|
|
40
|
+
settings;
|
|
41
|
+
rateLimiter;
|
|
24
42
|
constructor(db, config) {
|
|
25
43
|
this.db = db;
|
|
26
44
|
this.config = config;
|
|
27
|
-
this.oauth = new OAuthService(db, config.siteUrl, config.oauthProviders ?? {}, config.oauthFetch ?? fetch);
|
|
45
|
+
this.oauth = new OAuthService(db, config.siteUrl, config.oauthProviders ?? {}, config.oauthFetch ?? fetch, config.uriAllowList, config.enforceRedirectAllowList);
|
|
46
|
+
this.settings = config.settings ?? { ...DEFAULT_AUTH_SETTINGS };
|
|
47
|
+
this.rateLimiter = config.rateLimiter === undefined ? new RateLimiter() : config.rateLimiter;
|
|
28
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Enforce the rate limit for `action`, keyed by client address. Returns a 429
|
|
51
|
+
* (GoTrue's `over_request_rate_limit`) when exceeded, else null to proceed.
|
|
52
|
+
*/
|
|
53
|
+
limit(action, req) {
|
|
54
|
+
if (!this.rateLimiter)
|
|
55
|
+
return null;
|
|
56
|
+
const client = req.headers.get('x-tinbase-remote-addr') ?? 'local';
|
|
57
|
+
const retryAfter = this.rateLimiter.check(action, client);
|
|
58
|
+
if (retryAfter === null)
|
|
59
|
+
return null;
|
|
60
|
+
return new Response(JSON.stringify({ code: 429, error_code: 'over_request_rate_limit', msg: 'Request rate limit reached' }), { status: 429, headers: { 'content-type': 'application/json; charset=utf-8', 'retry-after': String(retryAfter) } });
|
|
61
|
+
}
|
|
62
|
+
/** Stop background timers (rate-limiter sweep). Called on backend close. */
|
|
63
|
+
stop() {
|
|
64
|
+
this.rateLimiter?.stop();
|
|
65
|
+
}
|
|
66
|
+
/** Dispatch one `/auth/v1/*` request. Any thrown error becomes a 500 `unexpected_failure`. */
|
|
29
67
|
async handle(req, ctx, url) {
|
|
30
68
|
const path = url.pathname.replace(/^\/auth\/v1\/?/, '').replace(/\/+$/, '');
|
|
31
69
|
const method = req.method.toUpperCase();
|
|
32
70
|
try {
|
|
33
71
|
if (path === 'health')
|
|
34
|
-
return json(200, { name: 'tinbase-auth', version:
|
|
72
|
+
return json(200, { name: 'tinbase-auth', version: TINBASE_VERSION, description: 'GoTrue-compatible auth' });
|
|
35
73
|
if (path === 'settings') {
|
|
74
|
+
const providers = Object.keys(this.config.oauthProviders ?? {});
|
|
36
75
|
return json(200, {
|
|
37
|
-
external: {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
76
|
+
external: {
|
|
77
|
+
email: true,
|
|
78
|
+
phone: false,
|
|
79
|
+
anonymous_users: this.settings.anonymousUsers,
|
|
80
|
+
...Object.fromEntries(providers.map((p) => [p, !this.settings.disabledProviders.includes(p)])),
|
|
81
|
+
},
|
|
82
|
+
disable_signup: this.settings.disableSignup,
|
|
83
|
+
autoconfirm: this.settings.autoconfirm,
|
|
84
|
+
mailer_autoconfirm: this.settings.autoconfirm,
|
|
85
|
+
minimum_password_length: this.settings.minPasswordLength,
|
|
41
86
|
});
|
|
42
87
|
}
|
|
43
88
|
if (path === 'signup' && method === 'POST')
|
|
44
|
-
return await this.signup(req);
|
|
89
|
+
return this.limit('signup', req) ?? (await this.signup(req));
|
|
45
90
|
if (path === 'token' && method === 'POST')
|
|
46
|
-
return await this.token(req, url);
|
|
91
|
+
return this.limit('token', req) ?? (await this.token(req, url));
|
|
47
92
|
if (path === 'user' && method === 'GET')
|
|
48
93
|
return await this.getUser(req);
|
|
49
94
|
if (path === 'user' && method === 'PUT')
|
|
@@ -51,11 +96,11 @@ export class AuthHandler {
|
|
|
51
96
|
if (path === 'logout' && method === 'POST')
|
|
52
97
|
return await this.logout(req);
|
|
53
98
|
if (path === 'otp' && method === 'POST')
|
|
54
|
-
return await this.sendOtp(req);
|
|
99
|
+
return this.limit('otp', req) ?? (await this.sendOtp(req));
|
|
55
100
|
if (path === 'recover' && method === 'POST')
|
|
56
|
-
return await this.sendRecovery(req);
|
|
101
|
+
return this.limit('recover', req) ?? (await this.sendRecovery(req));
|
|
57
102
|
if (['magiclink', 'resend'].includes(path) && method === 'POST')
|
|
58
|
-
return await this.sendOtp(req);
|
|
103
|
+
return this.limit('otp', req) ?? (await this.sendOtp(req));
|
|
59
104
|
if (path === 'verify' && method === 'POST')
|
|
60
105
|
return await this.verifyToken(req);
|
|
61
106
|
if (path === 'verify' && method === 'GET')
|
|
@@ -68,8 +113,13 @@ export class AuthHandler {
|
|
|
68
113
|
return await this.verifyFactor(req, path.split('/')[1]);
|
|
69
114
|
if (/^factors\/[^/]+$/.test(path) && method === 'DELETE')
|
|
70
115
|
return await this.unenrollFactor(req, path.split('/')[1]);
|
|
71
|
-
if (path === 'authorize' && method === 'GET')
|
|
116
|
+
if (path === 'authorize' && method === 'GET') {
|
|
117
|
+
const provider = url.searchParams.get('provider') ?? '';
|
|
118
|
+
if (provider && this.settings.disabledProviders.includes(provider)) {
|
|
119
|
+
return authError(422, 'provider_disabled', `Sign-ins with ${provider} are disabled`);
|
|
120
|
+
}
|
|
72
121
|
return await this.oauth.authorize(url);
|
|
122
|
+
}
|
|
73
123
|
if (path === 'callback' && (method === 'GET' || method === 'POST')) {
|
|
74
124
|
return await this.oauth.callback(url, (userId) => this.sessionTokensFor(userId));
|
|
75
125
|
}
|
|
@@ -87,16 +137,22 @@ export class AuthHandler {
|
|
|
87
137
|
const body = (await req.json().catch(() => ({})));
|
|
88
138
|
if (!body.email && !body.password) {
|
|
89
139
|
// supabase.auth.signInAnonymously()
|
|
140
|
+
if (!this.settings.anonymousUsers) {
|
|
141
|
+
return authError(422, 'anonymous_provider_disabled', 'Anonymous sign-ins are disabled');
|
|
142
|
+
}
|
|
90
143
|
const res = await this.db.query(`insert into auth.users (aud, role, raw_app_meta_data, raw_user_meta_data, is_anonymous, last_sign_in_at)
|
|
91
144
|
values ('authenticated', 'authenticated', '{}', $1, true, now())
|
|
92
145
|
returning *`, [JSON.stringify(body.data ?? {})]);
|
|
93
146
|
return json(200, await this.sessionFor(res.rows[0]));
|
|
94
147
|
}
|
|
148
|
+
if (this.settings.disableSignup) {
|
|
149
|
+
return authError(422, 'signup_disabled', 'Signups not allowed for this instance');
|
|
150
|
+
}
|
|
95
151
|
if (!body.email || !body.password) {
|
|
96
152
|
return authError(400, 'validation_failed', 'Signup requires a valid email and password');
|
|
97
153
|
}
|
|
98
|
-
if (body.password.length <
|
|
99
|
-
return authError(422, 'weak_password',
|
|
154
|
+
if (body.password.length < this.settings.minPasswordLength) {
|
|
155
|
+
return authError(422, 'weak_password', `Password should be at least ${this.settings.minPasswordLength} characters.`);
|
|
100
156
|
}
|
|
101
157
|
const email = body.email.toLowerCase().trim();
|
|
102
158
|
const existing = await this.db.query(`select id from auth.users where email = $1`, [email]);
|
|
@@ -104,14 +160,26 @@ export class AuthHandler {
|
|
|
104
160
|
return authError(422, 'user_already_exists', 'User already registered');
|
|
105
161
|
}
|
|
106
162
|
const hashed = await hashPassword(body.password);
|
|
163
|
+
const autoconfirm = this.settings.autoconfirm;
|
|
107
164
|
const res = await this.db.query(`insert into auth.users
|
|
108
165
|
(aud, role, email, encrypted_password, email_confirmed_at, last_sign_in_at,
|
|
109
166
|
raw_app_meta_data, raw_user_meta_data)
|
|
110
|
-
values ('authenticated', 'authenticated', $1, $2, now(), now(),
|
|
167
|
+
values ('authenticated', 'authenticated', $1, $2, case when $4 then now() else null end, now(),
|
|
111
168
|
'{"provider":"email","providers":["email"]}', $3)
|
|
112
|
-
returning *`, [email, hashed, JSON.stringify(body.data ?? {})]);
|
|
169
|
+
returning *`, [email, hashed, JSON.stringify(body.data ?? {}), autoconfirm]);
|
|
113
170
|
const newUser = res.rows[0];
|
|
171
|
+
// GoTrue records an `email` identity at signup, so user.identities reflects
|
|
172
|
+
// the email provider (getUser returned [] before this). Mirrors the shape
|
|
173
|
+
// written on anonymous-to-email upgrade.
|
|
174
|
+
await this.db.query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
|
|
175
|
+
values ($1, 'email', $2, $3)
|
|
176
|
+
on conflict (provider, provider_id) do nothing`, [newUser.id, newUser.id, JSON.stringify({ sub: newUser.id, email: newUser.email })]);
|
|
114
177
|
await this.audit('user_signedup', { actorId: newUser.id, actorEmail: email });
|
|
178
|
+
if (!autoconfirm) {
|
|
179
|
+
// confirmation required: email a verification link/code; no session yet
|
|
180
|
+
await this.issueToken(email, 'otp', false, 'confirm');
|
|
181
|
+
return json(200, this.userJson(newUser));
|
|
182
|
+
}
|
|
115
183
|
return json(200, await this.sessionFor(newUser));
|
|
116
184
|
}
|
|
117
185
|
async token(req, url) {
|
|
@@ -125,6 +193,10 @@ export class AuthHandler {
|
|
|
125
193
|
await this.audit('login_failed', { actorEmail: email, traits: { grant_type: 'password' } });
|
|
126
194
|
return authError(400, 'invalid_credentials', 'Invalid login credentials');
|
|
127
195
|
}
|
|
196
|
+
// when confirmation is required, unverified accounts cannot sign in yet
|
|
197
|
+
if (!this.settings.autoconfirm && !user.email_confirmed_at) {
|
|
198
|
+
return authError(400, 'email_not_confirmed', 'Email not confirmed');
|
|
199
|
+
}
|
|
128
200
|
await this.db.query(`update auth.users set last_sign_in_at = now() where id = $1`, [user.id]);
|
|
129
201
|
await this.audit('login', { actorId: user.id, actorEmail: user.email, traits: { grant_type: 'password' } });
|
|
130
202
|
return json(200, await this.sessionFor(user));
|
|
@@ -161,7 +233,7 @@ export class AuthHandler {
|
|
|
161
233
|
const user = await this.userFromBearer(req);
|
|
162
234
|
if (!user)
|
|
163
235
|
return authError(401, 'no_authorization', 'Invalid or expired token');
|
|
164
|
-
return json(200, this.userJson(user, await this.getUserFactors(user.id)));
|
|
236
|
+
return json(200, this.userJson(user, await this.getUserFactors(user.id), await this.getUserIdentities(user.id)));
|
|
165
237
|
}
|
|
166
238
|
async updateUser(req) {
|
|
167
239
|
const user = await this.userFromBearer(req);
|
|
@@ -172,7 +244,7 @@ export class AuthHandler {
|
|
|
172
244
|
const params = [];
|
|
173
245
|
// Upgrading an anonymous user to a permanent one: adding an email (and
|
|
174
246
|
// usually a password) keeps the same id + data, flips is_anonymous off, and
|
|
175
|
-
// records an email identity
|
|
247
|
+
// records an email identity - matching supabase.auth.updateUser({ email }).
|
|
176
248
|
const upgradingAnon = (user.is_anonymous ?? false) && !!body.email;
|
|
177
249
|
if (body.email) {
|
|
178
250
|
const email = body.email.toLowerCase().trim();
|
|
@@ -184,8 +256,8 @@ export class AuthHandler {
|
|
|
184
256
|
sets.push(`email = $${params.length}, email_confirmed_at = now()`);
|
|
185
257
|
}
|
|
186
258
|
if (body.password) {
|
|
187
|
-
if (body.password.length <
|
|
188
|
-
return authError(422, 'weak_password',
|
|
259
|
+
if (body.password.length < this.settings.minPasswordLength) {
|
|
260
|
+
return authError(422, 'weak_password', `Password should be at least ${this.settings.minPasswordLength} characters.`);
|
|
189
261
|
}
|
|
190
262
|
params.push(await hashPassword(body.password));
|
|
191
263
|
sets.push(`encrypted_password = $${params.length}`);
|
|
@@ -220,31 +292,36 @@ export class AuthHandler {
|
|
|
220
292
|
return new Response(null, { status: 204 });
|
|
221
293
|
}
|
|
222
294
|
// ── OTP / magic links / recovery ──────────────────────────────────────
|
|
223
|
-
async issueToken(email, tokenType, createUser) {
|
|
295
|
+
async issueToken(email, tokenType, createUser, flavor = 'login') {
|
|
224
296
|
const normalized = email.toLowerCase().trim();
|
|
225
297
|
let res = await this.db.query(`select * from auth.users where email = $1`, [normalized]);
|
|
226
298
|
let user = res.rows[0];
|
|
227
299
|
if (!user) {
|
|
228
300
|
if (!createUser)
|
|
229
301
|
return authError(422, 'otp_disabled', 'Signups not allowed for otp');
|
|
302
|
+
if (this.settings.disableSignup)
|
|
303
|
+
return authError(422, 'signup_disabled', 'Signups not allowed for this instance');
|
|
230
304
|
res = await this.db.query(`insert into auth.users (aud, role, email, raw_app_meta_data, raw_user_meta_data)
|
|
231
305
|
values ('authenticated', 'authenticated', $1, '{"provider":"email","providers":["email"]}', '{}')
|
|
232
306
|
returning *`, [normalized]);
|
|
233
307
|
user = res.rows[0];
|
|
234
308
|
}
|
|
235
|
-
const code =
|
|
309
|
+
const code = randomOtp(this.settings.otpLength);
|
|
236
310
|
const linkToken = randomToken(24);
|
|
311
|
+
const expiry = `${this.settings.otpExpirySeconds} seconds`;
|
|
237
312
|
await this.db.query(`delete from auth.one_time_tokens where email = $1 and token_type = $2`, [normalized, tokenType]);
|
|
238
313
|
await this.db.query(`insert into auth.one_time_tokens (user_id, email, token_type, token, expires_at)
|
|
239
|
-
values ($1, $2, $3, $4, now() + interval
|
|
314
|
+
values ($1, $2, $3, $4, now() + $7::interval), ($1, $2, $5, $6, now() + $7::interval)`, [user.id, normalized, tokenType, code, tokenType === 'otp' ? 'magiclink' : tokenType, linkToken, expiry]);
|
|
240
315
|
const kind = tokenType === 'otp' ? 'magiclink' : tokenType;
|
|
241
316
|
const link = `${this.config.siteUrl}/auth/v1/verify?token=${linkToken}&type=${kind}`;
|
|
242
317
|
await this.config.mailer.send({
|
|
243
318
|
to: normalized,
|
|
244
|
-
subject: tokenType === '
|
|
245
|
-
text: tokenType === '
|
|
246
|
-
? `
|
|
247
|
-
:
|
|
319
|
+
subject: tokenType === 'recovery' ? 'Reset your password' : flavor === 'confirm' ? 'Confirm your email' : 'Your login code',
|
|
320
|
+
text: tokenType === 'recovery'
|
|
321
|
+
? `Reset your password with this link: ${link}\n\nOr use code ${code}`
|
|
322
|
+
: flavor === 'confirm'
|
|
323
|
+
? `Confirm your email address with this link: ${link}\n\nOr enter the code ${code}`
|
|
324
|
+
: `Your one-time code is ${code}\n\nOr sign in with this link: ${link}`,
|
|
248
325
|
});
|
|
249
326
|
return json(200, {});
|
|
250
327
|
}
|
|
@@ -291,7 +368,7 @@ export class AuthHandler {
|
|
|
291
368
|
if (!body.token)
|
|
292
369
|
return authError(400, 'validation_failed', 'token is required');
|
|
293
370
|
// A recovery (password-reset) token must be redeemed with type=recovery
|
|
294
|
-
// explicitly
|
|
371
|
+
// explicitly - never fold it into the default set, or a guessed login OTP
|
|
295
372
|
// could mint a recovery session.
|
|
296
373
|
const types = body.type === 'recovery' ? ['recovery'] : body.type === 'magiclink' ? ['magiclink'] : ['otp', 'magiclink'];
|
|
297
374
|
const user = await this.redeem(body.token, types, body.email);
|
|
@@ -302,7 +379,10 @@ export class AuthHandler {
|
|
|
302
379
|
async verifyLink(url) {
|
|
303
380
|
const token = url.searchParams.get('token') ?? '';
|
|
304
381
|
const type = url.searchParams.get('type') ?? 'magiclink';
|
|
305
|
-
|
|
382
|
+
// Never redirect (with the freshly minted session tokens) to an origin the
|
|
383
|
+
// operator hasn't allowed - a crafted magic-link would otherwise exfiltrate
|
|
384
|
+
// the session. Unknown targets fall back to the site URL.
|
|
385
|
+
const redirectTo = resolveRedirect(url.searchParams.get('redirect_to'), this.config.siteUrl, this.config.uriAllowList, this.config.enforceRedirectAllowList);
|
|
306
386
|
const user = await this.redeem(token, [type]);
|
|
307
387
|
if (!user) {
|
|
308
388
|
return new Response(null, { status: 303, headers: { location: `${redirectTo}#error=access_denied&error_code=otp_expired` } });
|
|
@@ -413,7 +493,7 @@ export class AuthHandler {
|
|
|
413
493
|
/**
|
|
414
494
|
* Export everything held about one user across the auth schema, for a GDPR
|
|
415
495
|
* right-of-access / portability request. Credentials (password hash, MFA
|
|
416
|
-
* secrets, raw token values) are deliberately omitted
|
|
496
|
+
* secrets, raw token values) are deliberately omitted - they are not personal
|
|
417
497
|
* data to hand back and exporting them would leak secrets.
|
|
418
498
|
*/
|
|
419
499
|
async exportUser(userId) {
|
|
@@ -455,7 +535,7 @@ export class AuthHandler {
|
|
|
455
535
|
* doesn't exist and a summary of what was erased.
|
|
456
536
|
*
|
|
457
537
|
* Note: storage.objects.owner has no FK to auth.users, so object rows/bytes
|
|
458
|
-
* owned by the user are not removed here
|
|
538
|
+
* owned by the user are not removed here - see COMPLIANCE.md for the
|
|
459
539
|
* storage-erasure step the operator must run.
|
|
460
540
|
*/
|
|
461
541
|
async eraseUser(userId) {
|
|
@@ -484,6 +564,13 @@ export class AuthHandler {
|
|
|
484
564
|
if (factorType !== 'totp') {
|
|
485
565
|
return authError(422, 'validation_failed', 'Only the totp factor type is supported');
|
|
486
566
|
}
|
|
567
|
+
if (!this.settings.totpEnrollEnabled) {
|
|
568
|
+
return authError(422, 'mfa_totp_enroll_disabled', 'TOTP enrollment is disabled');
|
|
569
|
+
}
|
|
570
|
+
const enrolled = await this.db.query(`select count(*)::int as n from auth.mfa_factors where user_id = $1`, [user.id]);
|
|
571
|
+
if (enrolled.rows[0].n >= this.settings.maxEnrolledFactors) {
|
|
572
|
+
return authError(422, 'too_many_enrolled_mfa_factors', 'Maximum number of enrolled MFA factors reached');
|
|
573
|
+
}
|
|
487
574
|
const friendlyName = body.friendly_name ?? null;
|
|
488
575
|
if (friendlyName) {
|
|
489
576
|
const dup = await this.db.query(`select 1 from auth.mfa_factors where user_id = $1 and friendly_name = $2`, [user.id, friendlyName]);
|
|
@@ -517,6 +604,9 @@ export class AuthHandler {
|
|
|
517
604
|
const user = await this.userFromBearer(req);
|
|
518
605
|
if (!user)
|
|
519
606
|
return authError(401, 'no_authorization', 'This endpoint requires a Bearer token');
|
|
607
|
+
if (!this.settings.totpVerifyEnabled) {
|
|
608
|
+
return authError(422, 'mfa_totp_verify_disabled', 'TOTP verification is disabled');
|
|
609
|
+
}
|
|
520
610
|
const fr = await this.db.query(`select id from auth.mfa_factors where id = $1 and user_id = $2`, [factorId, user.id]);
|
|
521
611
|
if (fr.rows.length === 0)
|
|
522
612
|
return authError(404, 'mfa_factor_not_found', 'MFA factor not found');
|
|
@@ -587,6 +677,21 @@ export class AuthHandler {
|
|
|
587
677
|
updated_at: iso(f.updated_at),
|
|
588
678
|
}));
|
|
589
679
|
}
|
|
680
|
+
/** GoTrue-shaped identities for a user (linked providers), from auth.identities. */
|
|
681
|
+
async getUserIdentities(userId) {
|
|
682
|
+
const res = await this.db.query(`select id, provider_id, user_id, identity_data, provider, created_at, updated_at, last_sign_in_at
|
|
683
|
+
from auth.identities where user_id = $1 order by created_at`, [userId]);
|
|
684
|
+
return res.rows.map((r) => ({
|
|
685
|
+
identity_id: r.id,
|
|
686
|
+
id: r.provider_id,
|
|
687
|
+
user_id: r.user_id,
|
|
688
|
+
identity_data: r.identity_data ?? {},
|
|
689
|
+
provider: r.provider,
|
|
690
|
+
last_sign_in_at: iso(r.last_sign_in_at),
|
|
691
|
+
created_at: iso(r.created_at),
|
|
692
|
+
updated_at: iso(r.updated_at),
|
|
693
|
+
}));
|
|
694
|
+
}
|
|
590
695
|
// ── helpers ───────────────────────────────────────────────────────────
|
|
591
696
|
async userFromBearer(req) {
|
|
592
697
|
const authz = req.headers.get('authorization') ?? '';
|
|
@@ -598,7 +703,8 @@ export class AuthHandler {
|
|
|
598
703
|
const res = await this.db.query(`select * from auth.users where id = $1`, [claims.sub]);
|
|
599
704
|
return res.rows[0] ?? null;
|
|
600
705
|
}
|
|
601
|
-
|
|
706
|
+
/** Shape a user row into the GoTrue user object supabase-js expects. */
|
|
707
|
+
userJson(u, factors = [], identities = []) {
|
|
602
708
|
return {
|
|
603
709
|
id: u.id,
|
|
604
710
|
aud: u.aud ?? 'authenticated',
|
|
@@ -610,7 +716,7 @@ export class AuthHandler {
|
|
|
610
716
|
last_sign_in_at: iso(u.last_sign_in_at),
|
|
611
717
|
app_metadata: u.raw_app_meta_data ?? {},
|
|
612
718
|
user_metadata: u.raw_user_meta_data ?? {},
|
|
613
|
-
identities
|
|
719
|
+
identities,
|
|
614
720
|
factors,
|
|
615
721
|
created_at: iso(u.created_at),
|
|
616
722
|
updated_at: iso(u.updated_at),
|
|
@@ -625,7 +731,12 @@ export class AuthHandler {
|
|
|
625
731
|
}
|
|
626
732
|
async sessionFor(user, parentToken, opts) {
|
|
627
733
|
const now = Math.floor(Date.now() / 1000);
|
|
628
|
-
|
|
734
|
+
// Cap the access-token lifetime at the session timebox so a timeboxed
|
|
735
|
+
// session can't outlive its absolute deadline (config.toml auth.sessions.timebox).
|
|
736
|
+
const lifetime = this.config.sessionTimeboxSeconds
|
|
737
|
+
? Math.min(this.config.jwtExpiry, this.config.sessionTimeboxSeconds)
|
|
738
|
+
: this.config.jwtExpiry;
|
|
739
|
+
const expiresAt = now + lifetime;
|
|
629
740
|
const sessionId = crypto.randomUUID();
|
|
630
741
|
const claims = {
|
|
631
742
|
iss: `${this.config.siteUrl}/auth/v1`,
|
|
@@ -649,10 +760,10 @@ export class AuthHandler {
|
|
|
649
760
|
return {
|
|
650
761
|
access_token: accessToken,
|
|
651
762
|
token_type: 'bearer',
|
|
652
|
-
expires_in:
|
|
763
|
+
expires_in: lifetime,
|
|
653
764
|
expires_at: expiresAt,
|
|
654
765
|
refresh_token: refreshToken,
|
|
655
|
-
user: this.userJson(user, await this.getUserFactors(user.id)),
|
|
766
|
+
user: this.userJson(user, await this.getUserFactors(user.id), await this.getUserIdentities(user.id)),
|
|
656
767
|
};
|
|
657
768
|
}
|
|
658
769
|
}
|