tinbase 0.16.2 → 0.18.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.
@@ -99,6 +99,7 @@ interface UserRow {
99
99
  phone: string | null;
100
100
  phone_confirmed_at: Date | string | null;
101
101
  is_anonymous: boolean | null;
102
+ email_change: string | null;
102
103
  }
103
104
  /** Routes and services the GoTrue-compatible `/auth/v1/*` endpoints. */
104
105
  export declare class AuthHandler {
@@ -120,18 +121,34 @@ export declare class AuthHandler {
120
121
  * (GoTrue's `over_request_rate_limit`) when exceeded, else null to proceed.
121
122
  */
122
123
  private limit;
124
+ /** GoTrue's 429 for `max_frequency`, with the seconds the caller must wait. */
125
+ private static tooSoon;
123
126
  /**
124
- * Enforce `max_frequency`: refuse a second auth email to the same address
125
- * within the configured window (GoTrue's `over_email_send_rate_limit`).
127
+ * Enforce `max_frequency` the way GoTrue does: against the timestamp that
128
+ * flow last wrote on the user row, not against one budget per address.
126
129
  *
127
- * Keyed by recipient, not by caller, and checked before the account is looked
128
- * up. Both matter: keying by caller lets someone rotate addresses and keep
129
- * mailing one victim, and checking after the lookup would answer differently
130
- * for an address that exists than for one that does not - handing back the
131
- * account enumeration that answering 200 for unknown addresses exists to
132
- * prevent.
130
+ * Which column a flow measures itself by is GoTrue's mapping, quirks
131
+ * included - a magic link is measured by `recovery_sent_at`, the same column
132
+ * password recovery uses, because GoTrue mints both from the recovery token.
133
+ * Sharing that budget is therefore correct rather than an oversight; a
134
+ * confirmation, by contrast, has its own. An app that sets `max_frequency`
135
+ * in config.toml gets the pacing Supabase's docs describe, which is the
136
+ * point of spelling it the same way.
137
+ *
138
+ * An address with no account has no row to measure, so those fall back to an
139
+ * in-memory window keyed the same way. GoTrue simply lets them through,
140
+ * which makes a 429 proof that an account exists - `/recover` answers 200
141
+ * for an address it has never seen precisely so the response cannot be used
142
+ * to enumerate accounts, and a frequency check that only ever fires for real
143
+ * users hands that back. The fallback keeps the two indistinguishable.
133
144
  */
134
145
  private limitEmailFrequency;
146
+ /**
147
+ * Record that this flow has just mailed the address, so the next request is
148
+ * measured from now. Written only after the mail was accepted - a transport
149
+ * that refused it has not spent the window.
150
+ */
151
+ private markEmailSent;
135
152
  /** Stop background timers (rate-limiter sweep). Called on backend close. */
136
153
  stop(): void;
137
154
  /** Dispatch one `/auth/v1/*` request. Any thrown error becomes a 500 `unexpected_failure`. */
@@ -140,6 +157,33 @@ export declare class AuthHandler {
140
157
  private token;
141
158
  private getUser;
142
159
  private updateUser;
160
+ /**
161
+ * Begin an email change: park the new address, mint a token for each side,
162
+ * and mail them.
163
+ *
164
+ * The address does not move until the tokens come back. Writing it straight
165
+ * to `auth.users.email` - which is what this did - means a session is all it
166
+ * takes to walk an account to an address of the holder's choosing, with no
167
+ * confirmation from either end and nothing sent to the address losing it.
168
+ *
169
+ * With `secure_email_change_enabled` (the Supabase default) both ends have
170
+ * to click: the new address proves it exists and is reachable, and the
171
+ * current one gets a say in losing the account. The second token is what
172
+ * makes a stolen session insufficient on its own.
173
+ *
174
+ * Returns a Response only when the change could not be started.
175
+ */
176
+ private startEmailChange;
177
+ /**
178
+ * Park the new address and mint a token per side, without sending anything.
179
+ *
180
+ * Shared with `admin/generate_link`, which needs the same tokens but hands
181
+ * them to the caller. One implementation so the two cannot disagree about
182
+ * how many sides there are or when the previous attempt is invalidated.
183
+ */
184
+ private mintEmailChangeTokens;
185
+ /** One side of an email change, to whichever address that side belongs to. */
186
+ private mailEmailChange;
143
187
  /**
144
188
  * POST /auth/v1/logout[?scope=global|local|others]
145
189
  *
@@ -180,6 +224,21 @@ export declare class AuthHandler {
180
224
  */
181
225
  private issueToken;
182
226
  private sendOtp;
227
+ /**
228
+ * POST /auth/v1/resend - send the confirmation for `type` again.
229
+ *
230
+ * This used to be an alias for /magiclink, which is a different email
231
+ * entirely: it mails a login link measured by `recovery_sent_at`, creates an
232
+ * account for an address it has never seen, and pays no attention to `type`.
233
+ * So a resend for an address with no account signed that person up and sent
234
+ * them a way in, and a resend for a signup was paced by the recovery window.
235
+ *
236
+ * GoTrue answers 200 with an empty body both for an address it does not know
237
+ * and for one already confirmed - the two look identical from outside, which
238
+ * is what stops the endpoint from reporting who has an account. No mail goes
239
+ * out in either case.
240
+ */
241
+ private resend;
183
242
  private sendRecovery;
184
243
  /** Max wrong guesses for a one-time code before its tokens are invalidated. */
185
244
  private static readonly MAX_OTP_ATTEMPTS;
@@ -194,6 +253,25 @@ export declare class AuthHandler {
194
253
  * otp+magiclink rows.
195
254
  */
196
255
  private static redeemTypes;
256
+ /** What a caller is told when one side of an email change is still outstanding. */
257
+ private static readonly EMAIL_CHANGE_PENDING_MSG;
258
+ /**
259
+ * Redeem one side of an email change.
260
+ *
261
+ * Not routed through {@link redeem}, which exists for the login and recovery
262
+ * tokens and does things that are wrong here: it clears every live token for
263
+ * the address and stamps `last_sign_in_at`, and an email change is not a
264
+ * sign-in. It also cannot express the part that matters - that one click may
265
+ * not be enough.
266
+ *
267
+ * The address moves only once no side is left outstanding. With
268
+ * `secure_email_change_enabled` that means both tokens, in either order;
269
+ * with it off there is only ever the new address's. Deciding it by what
270
+ * remains in the table, rather than by counting clicks, is what makes the
271
+ * order irrelevant and a replay harmless - the row is gone the moment it is
272
+ * redeemed.
273
+ */
274
+ private verifyEmailChange;
197
275
  private verifyToken;
198
276
  private verifyLink;
199
277
  private admin;
@@ -212,6 +290,14 @@ export declare class AuthHandler {
212
290
  * is nothing to un-hash and `verifyOtp({ token_hash })` redeems it directly.
213
291
  */
214
292
  private generateLink;
293
+ /**
294
+ * `admin.generateLink({ type: 'email_change_current' | 'email_change_new' })`.
295
+ *
296
+ * Both sides are minted whichever is asked for - they are one change, and
297
+ * minting only the requested half would leave a secure change that can never
298
+ * complete. The caller gets the link for the side it named.
299
+ */
300
+ private generateEmailChangeLink;
215
301
  /**
216
302
  * Append a security event to auth.audit_log_entries (GoTrue-compatible
217
303
  * payload). Best-effort: a logging failure never breaks the request.