@voltro/plugin-auth-social 0.34.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.
@@ -0,0 +1,666 @@
1
+ import { AuthConfig } from '@voltro/plugin-auth';
2
+ import { Effect } from 'effect';
3
+ import { Predicate } from '@voltro/database';
4
+ import { Subject } from '@voltro/protocol';
5
+ import { TableLike } from '@voltro/database';
6
+ import { UserStore } from '@voltro/plugin-auth';
7
+ import { VoltroPlugin } from '@voltro/protocol';
8
+
9
+ /** Default lifetime we mint. Minutes, not months — see the header. */
10
+ export declare const APPLE_CLIENT_SECRET_DEFAULT_TTL_S = 900;
11
+
12
+ /** Apple's documented ceiling on a client-secret JWT: six months. */
13
+ export declare const APPLE_CLIENT_SECRET_MAX_TTL_S = 15777000;
14
+
15
+ /**
16
+ * Mint (or reuse, within its lifetime) Apple's client-secret JWT for
17
+ * `clientId`.
18
+ *
19
+ * ES256 via `node:crypto` rather than a JWS library: Node's `sign` with
20
+ * `dsaEncoding: 'ieee-p1363'` emits the raw `r || s` pair JOSE requires, which
21
+ * is the only fiddly part of ES256, and doing it here keeps this package free
22
+ * of a signing dependency. (`jose` still verifies the ID token — through
23
+ * `@voltro/protocol/jwt`, which every IdP plugin here shares.)
24
+ */
25
+ export declare const appleClientSecret: (clientId: string, key: AppleClientSecretKey, now?: () => number) => string;
26
+
27
+ /**
28
+ * The material Apple hands you in the developer portal. All three are
29
+ * configuration, never literals in source: `privateKey` is the contents of the
30
+ * downloaded `.p8` (a PKCS#8 PEM) and belongs in an env var.
31
+ */
32
+ export declare interface AppleClientSecretKey {
33
+ /** Apple Developer Team ID (10 chars) — becomes the JWT's `iss`. */
34
+ readonly teamId: string;
35
+ /** The Key ID of the `.p8` — becomes the JWT header's `kid`. */
36
+ readonly keyId: string;
37
+ /** The `.p8` file's contents: a PKCS#8 PEM holding a P-256 private key. */
38
+ readonly privateKey: string;
39
+ /** Lifetime of each minted secret, seconds. Default 900 (15 min); anything
40
+ * above {@link APPLE_CLIENT_SECRET_MAX_TTL_S} is rejected. */
41
+ readonly ttlSeconds?: number;
42
+ }
43
+
44
+ /** The one-time name (and email) Apple posts alongside the FIRST callback. */
45
+ export declare interface AppleUserPayload {
46
+ readonly firstName: string | null;
47
+ readonly lastName: string | null;
48
+ readonly email: string | null;
49
+ }
50
+
51
+ /**
52
+ * Resolve a verified social profile into a session, applying the account-link
53
+ * policy and the subject guards.
54
+ *
55
+ * Returns a refusal rather than failing: every branch here is an expected
56
+ * outcome of a normal login attempt (the account exists unlinked, the address
57
+ * is unverified, a guard vetoed), and modelling those as defects would push
58
+ * callers into `catchAll` for the ordinary path.
59
+ */
60
+ export declare const completeSocialSignIn: (o: SocialSignInOptions) => Effect.Effect<SocialSignInResult>;
61
+
62
+ /** DataStore-backed store — the production choice. Bind it from the plugin's
63
+ * `bindDataStore` hook, or construct it yourself over `ctx.store`. */
64
+ export declare const dataStoreSocialIdentityStore: (store: SocialIdentityDataStore) => SocialIdentityStore;
65
+
66
+ /**
67
+ * Decide what a verified social identity means for local accounts.
68
+ *
69
+ * Pure and total — no store, no clock, no IO — so every branch is directly
70
+ * testable, including the ones that must NEVER fire.
71
+ */
72
+ export declare const decideSocialLink: (input: SocialLinkInput) => SocialLinkDecision;
73
+
74
+ /**
75
+ * Resolve the authenticated GitHub user behind an access token.
76
+ *
77
+ * The `/user/emails` read is best-effort: a token issued without `user:email`
78
+ * (or a GitHub App restricted from it) 403s, and that is a configuration
79
+ * problem for the app to notice — not a reason to fail the login, and above all
80
+ * not a reason to fall back to the unverified profile email. We return
81
+ * `email: null, emailVerified: false` and let the link policy decide.
82
+ */
83
+ export declare const fetchGithubIdentity: (accessToken: string, options: {
84
+ readonly apiBase: string;
85
+ readonly fetchImpl?: typeof fetch;
86
+ }) => Promise<GithubIdentity>;
87
+
88
+ /** What we take from GitHub, before it is folded into a `SocialProfile`. */
89
+ export declare interface GithubIdentity {
90
+ readonly accountId: string;
91
+ readonly login: string;
92
+ readonly name: string | null;
93
+ readonly avatarUrl: string | null;
94
+ readonly email: string | null;
95
+ readonly emailVerified: boolean;
96
+ }
97
+
98
+ /** Is this one of Apple's per-app relay addresses? Used as a fallback when the
99
+ * `is_private_email` claim is absent (older tokens omit it). */
100
+ export declare const isApplePrivateRelayEmail: (email: string | null) => boolean;
101
+
102
+ /** Narrow an arbitrary string to a shipped provider id. */
103
+ export declare const isSocialProviderId: (value: string) => value is SocialProviderId;
104
+
105
+ /**
106
+ * Attach a provider account to a user you have ALREADY authenticated.
107
+ *
108
+ * This is the linking that is always sound, and the escape hatch every app on
109
+ * the default `'never'` policy needs: the proof that the person may attach an
110
+ * identity to this account is that they are holding a session for it. Call it
111
+ * from an authenticated route after `socialCompleteLogin` returns a profile.
112
+ *
113
+ * Idempotent: re-linking the same provider account to the same user returns the
114
+ * existing row. Linking a provider account that belongs to a DIFFERENT user
115
+ * fails — one provider account, one local user, or the identity stops being an
116
+ * identity.
117
+ */
118
+ export declare const linkSocialIdentity: (store: SocialIdentityStore, userId: string, input: Omit<SocialIdentityInput, "userId">) => Effect.Effect<SocialIdentityRecord, Error>;
119
+
120
+ /** In-memory store — tests and single-process dev. Restart wipes it, which for
121
+ * this table means every user's social login unlinks. Not for production. */
122
+ export declare const memorySocialIdentityStore: (seed?: ReadonlyArray<SocialIdentityRecord>) => SocialIdentityStore;
123
+
124
+ /**
125
+ * Parse Apple's `user` form field. Returns `null` for an absent or unparseable
126
+ * value — which is the NORMAL case on every authorization after the first, not
127
+ * an error. A caller that treats a missing name as a failure locks out every
128
+ * returning user.
129
+ */
130
+ export declare const parseAppleUserPayload: (raw: string | undefined | null) => AppleUserPayload | null;
131
+
132
+ /** Test seam — drop the parsed-key and minted-secret caches. */
133
+ export declare const _resetAppleSecretCache: () => void;
134
+
135
+ export declare const SOCIAL_IDENTITY_TABLE = "_voltro_oauth_identities";
136
+
137
+ /** Every shipped provider id. */
138
+ export declare const SOCIAL_PROVIDER_IDS: ReadonlyArray<SocialProviderId>;
139
+
140
+ /** Every shipped provider, in a stable order. */
141
+ export declare const SOCIAL_PROVIDERS: ReadonlyArray<SocialProviderDescriptor>;
142
+
143
+ /**
144
+ * The provider authenticated someone, and we refused to turn that into a
145
+ * session for an EXISTING local account.
146
+ *
147
+ * Not an authentication failure — the provider's answer was fine. It is the
148
+ * account-linking policy declining to attach an unproven identity to an account
149
+ * that already exists. See `linking.ts` for why that is the default.
150
+ */
151
+ export declare class SocialAccountLinkError extends SocialAuthError {
152
+ constructor(code: SocialAuthErrorCode, message: string, status?: number);
153
+ }
154
+
155
+ /**
156
+ * Base class for every social-login failure. `status` is the HTTP status the
157
+ * route layer answers with, so a caller wiring the primitives by hand gets the
158
+ * same status the shipped plugin would produce.
159
+ */
160
+ export declare class SocialAuthError extends Error {
161
+ readonly status: number;
162
+ readonly code: SocialAuthErrorCode;
163
+ constructor(status: number, code: SocialAuthErrorCode, message: string);
164
+ }
165
+
166
+ /** Every failure this package produces. `code` is stable across releases. */
167
+ export declare type SocialAuthErrorCode =
168
+ /** No provider by that id, or the provider is not configured. */
169
+ 'unknown_provider'
170
+ /** The plugin/primitive was called with an unusable configuration. */
171
+ | 'config'
172
+ /** No `code` in the callback, or the request was not a callback at all. */
173
+ | 'missing_code'
174
+ /** The callback's `state` did not match the one minted for this browser. */
175
+ | 'state_mismatch'
176
+ /** The PKCE verifier for this login was not presented back. */
177
+ | 'missing_verifier'
178
+ /** The provider refused the authorization-code exchange. */
179
+ | 'exchange_failed'
180
+ /** The provider's response was well-formed HTTP but not a usable token set. */
181
+ | 'malformed_token_response'
182
+ /** The ID token failed signature / iss / aud / exp verification. */
183
+ | 'id_token_invalid'
184
+ /** The ID token verified but its `nonce` was not the one we minted. */
185
+ | 'nonce_mismatch'
186
+ /** The provider identified the user but gave us no email to work with. */
187
+ | 'no_email'
188
+ /** An account with this email exists and the link policy refused to attach. */
189
+ | 'account_exists_unlinked'
190
+ /** Linking requires a provider-VERIFIED email and this one is not. */
191
+ | 'email_not_verified'
192
+ /** Linking refused because the address is a per-app Apple private relay. */
193
+ | 'private_relay_email'
194
+ /** A post-authentication subject guard vetoed the login. */
195
+ | 'guard_rejected';
196
+
197
+ export declare const socialAuthPlugin: (options: SocialAuthPluginOptions) => VoltroPlugin;
198
+
199
+ export declare interface SocialAuthPluginOptions {
200
+ /** Which providers this app offers. A key with an empty object takes every
201
+ * credential from the environment. */
202
+ readonly providers: Partial<Record<SocialProviderId, SocialProviderConfig>>;
203
+ /** The same user store `authRoutesPlugin` runs on. */
204
+ readonly users: UserStore;
205
+ /**
206
+ * Where identities are recorded. Omit and the plugin builds a
207
+ * DataStore-backed store over the route's boot store — which needs
208
+ * `_voltro_oauth_identities`, contributed here via `extendSchema`.
209
+ */
210
+ readonly identities?: SocialIdentityStore;
211
+ /** The same `AuthConfig` the auth routes plugin runs on: session secret,
212
+ * cookie flags, default tenant, post-authentication subject guards. */
213
+ readonly auth: AuthConfig;
214
+ /**
215
+ * How a social identity may attach to a PRE-EXISTING local account.
216
+ * Default `'never'` — read `linking.ts` before changing it. `'verified-email'`
217
+ * is a deliberate, bounded risk, not a convenience toggle.
218
+ */
219
+ readonly linkPolicy?: SocialLinkPolicy;
220
+ /** Route prefix. Default `/auth/social`. */
221
+ readonly prefix?: string;
222
+ /** Absolute origin used to build the default redirect URIs, e.g.
223
+ * `https://app.example.com`. Falls back to `auth.appBaseUrl`. */
224
+ readonly appBaseUrl?: string;
225
+ /** Where to send the browser after a successful login. Falls back to
226
+ * `auth.successRedirect`, then `/`. */
227
+ readonly successRedirect?: string;
228
+ /**
229
+ * Where to send the browser after a REFUSED login. When set, the refusal's
230
+ * stable `code` is appended as `?social_error=<code>` so the page can explain
231
+ * itself. Omit to answer with a JSON body instead (the default) — useful
232
+ * while wiring, unhelpful to an end user.
233
+ */
234
+ readonly failureRedirect?: string;
235
+ /** How long a started login may take to come back. Default 600s (10 min). */
236
+ readonly stateTtlSeconds?: number;
237
+ /** Name of the short-lived login-state cookie. Default `voltro:oauth`. */
238
+ readonly stateCookieName?: string;
239
+ /** Disambiguates multiple instances of this plugin in one app. */
240
+ readonly name?: string;
241
+ }
242
+
243
+ /** The tables this package contributes, as an array. */
244
+ export declare const socialAuthTables: readonly [TableLike];
245
+
246
+ /**
247
+ * Begin a social login: mint `state`, a PKCE `code_verifier` and an OIDC
248
+ * `nonce`, and build the authorize URL carrying `state` and the S256
249
+ * `code_challenge`.
250
+ *
251
+ * **`state` is not optional and is not caller-supplied.** In the WorkOS
252
+ * package it used to be both, and the shape of that defect is the reason this
253
+ * one has no `state` option at all: an attacker who gets a victim's browser to
254
+ * hit the callback with an authorization code THEY obtained logs the victim
255
+ * into the ATTACKER's account, and `state` is the only thing that stops it.
256
+ * Making it opt-in made "the login works" indistinguishable from "the login is
257
+ * forgeable". An app that wants to carry a post-login return path puts it in
258
+ * its own cookie next to these values — mixing an application payload into a
259
+ * CSRF nonce is precisely what made the nonce guessable.
260
+ *
261
+ * **PKCE (S256) rides along on every provider, including the one that ignores
262
+ * it.** The authorization code travels through the address bar and the referrer
263
+ * chain; without a verifier anyone who captures it before we redeem it can
264
+ * redeem it themselves. Google and Apple enforce the verifier; GitHub's OAuth
265
+ * app flow does not implement RFC 7636 and ignores the parameter. Sending it
266
+ * anyway costs nothing and removes the branch where a future provider addition
267
+ * quietly lands on the no-PKCE side.
268
+ *
269
+ * **`nonce` is minted for OIDC providers and checked against the ID token.**
270
+ * It is what makes a replayed ID token from another login fail here.
271
+ */
272
+ export declare const socialBeginLogin: (o: SocialBeginLoginOptions) => SocialLoginHandoff;
273
+
274
+ export declare interface SocialBeginLoginOptions {
275
+ readonly provider: SocialProviderId;
276
+ /** The OAuth client id issued by the provider. For Apple this is the
277
+ * Services ID (`com.example.web`), not the App ID. */
278
+ readonly clientId: string;
279
+ /** The callback URL the provider redirects (or POSTs) back to. Must be
280
+ * registered with the provider, byte for byte. */
281
+ readonly redirectUri: string;
282
+ /** Override the provider's default scopes. The defaults are the minimum
283
+ * needed to identify the user; ask for more only when you use more. */
284
+ readonly scopes?: ReadonlyArray<string>;
285
+ /** Pre-fill the account chooser (Google `login_hint`, GitHub `login`). */
286
+ readonly loginHint?: string;
287
+ /** Google only: `'consent'` / `'select_account'` / … Passed through. */
288
+ readonly prompt?: string;
289
+ readonly endpoints?: SocialEndpointOverrides;
290
+ /**
291
+ * Inject the random values instead of minting them. TESTS ONLY — a
292
+ * deterministic `state` is a CSRF token an attacker can also predict, and a
293
+ * deterministic `codeVerifier` is no PKCE at all.
294
+ */
295
+ readonly randomness?: {
296
+ readonly state: string;
297
+ readonly codeVerifier: string;
298
+ readonly nonce: string;
299
+ };
300
+ }
301
+
302
+ /**
303
+ * Exchange an authorization code for a verified identity — AFTER checking the
304
+ * CSRF `state`, and carrying the PKCE `code_verifier`.
305
+ *
306
+ * **The state check lives inside this function rather than beside the caller's
307
+ * `if (!code)` deliberately.** A generated `state` that nothing verifies is
308
+ * worse than no state at all: it makes a code review, a screenshot of the
309
+ * authorize URL and a penetration test all read as "CSRF is handled". Making
310
+ * the check part of the only function that can redeem a code means an
311
+ * integration cannot arrive at an identity without having performed it — the
312
+ * loop is closed by the type signature, not by a note in the docs.
313
+ *
314
+ * Both `state` and `nonce` are compared in constant time. Neither is a
315
+ * high-value secret, but both ARE secrets compared against attacker-supplied
316
+ * input, and a `===` on that shape is a habit this repo does not keep.
317
+ */
318
+ export declare const socialCompleteLogin: (o: SocialCompleteLoginOptions) => Promise<SocialProfile>;
319
+
320
+ export declare interface SocialCompleteLoginOptions {
321
+ readonly provider: SocialProviderId;
322
+ readonly clientId: string;
323
+ /** Google / GitHub: the OAuth client secret. Apple: omit and pass
324
+ * {@link SocialCompleteLoginOptions.appleKey} — its secret is a JWT we sign. */
325
+ readonly clientSecret?: string;
326
+ /** Apple only: the `.p8` key material a client secret is minted from. */
327
+ readonly appleKey?: AppleClientSecretKey;
328
+ /** Must be byte-identical to the one used to begin the login. */
329
+ readonly redirectUri: string;
330
+ /** The `code` from the callback. */
331
+ readonly code: string;
332
+ /** The `state` the browser presented back. */
333
+ readonly state: string;
334
+ /** The `state` `socialBeginLogin` minted, read back out of wherever the app
335
+ * stashed it. */
336
+ readonly expectedState: string;
337
+ /** The PKCE verifier `socialBeginLogin` minted. */
338
+ readonly codeVerifier: string;
339
+ /** The OIDC nonce `socialBeginLogin` minted. Required for Google and Apple;
340
+ * meaningless for GitHub, which returns no ID token. */
341
+ readonly expectedNonce?: string;
342
+ /** Apple only: the raw `user` form field. Present on the FIRST authorization
343
+ * and never again — see `apple.ts`. */
344
+ readonly appleUserPayload?: string;
345
+ readonly endpoints?: SocialEndpointOverrides;
346
+ /** Injectable fetch for tests. */
347
+ readonly fetchImpl?: typeof fetch;
348
+ /** Injectable clock (epoch-ms), used when minting Apple's client secret. */
349
+ readonly now?: () => number;
350
+ }
351
+
352
+ /** The plugin or a primitive was configured in a way that cannot work. */
353
+ export declare class SocialConfigError extends SocialAuthError {
354
+ constructor(message: string);
355
+ }
356
+
357
+ /**
358
+ * Per-call endpoint overrides. Exists so the test suite can point a provider at
359
+ * a loopback server that serves a JWKS and a token endpoint — the whole flow is
360
+ * then exercised end to end without a network or a real client secret.
361
+ *
362
+ * NOT an integration seam for pointing production at a different host: an
363
+ * `issuer` override is an override of the thing the ID token is checked
364
+ * against, so a wrong value here silently accepts tokens from somewhere else.
365
+ */
366
+ export declare interface SocialEndpointOverrides {
367
+ readonly authorizeUrl?: string;
368
+ readonly tokenUrl?: string;
369
+ readonly jwksUrl?: string;
370
+ readonly issuer?: string;
371
+ readonly apiBase?: string;
372
+ }
373
+
374
+ /** The provider refused (or mangled) the authorization-code exchange. */
375
+ export declare class SocialExchangeError extends SocialAuthError {
376
+ constructor(code: SocialAuthErrorCode, message: string, status?: number);
377
+ }
378
+
379
+ /**
380
+ * The social-identity table.
381
+ *
382
+ * `[provider, providerAccountId]` is a composite UNIQUE — the DB enforces the
383
+ * one-row-per-provider-account invariant the callback depends on, and the
384
+ * unique constraint doubles as the index backing the lookup, so no separate
385
+ * `.index([...])` on the same columns is needed.
386
+ */
387
+ export declare const socialIdentitiesTable: TableLike;
388
+
389
+ /** The slice of `DataStore` this store needs — narrow so a caller can pass a
390
+ * transactional view, a namespaced view, or a test double. */
391
+ export declare interface SocialIdentityDataStore {
392
+ query(descriptor: {
393
+ readonly table: string;
394
+ readonly predicate?: Predicate;
395
+ readonly limit?: number;
396
+ readonly order?: ReadonlyArray<{
397
+ readonly column: string;
398
+ readonly direction: 'asc' | 'desc';
399
+ }>;
400
+ }): Promise<ReadonlyArray<Readonly<Record<string, unknown>>>>;
401
+ insert(table: string, row: Readonly<Record<string, unknown>>): Promise<Readonly<Record<string, unknown>>>;
402
+ update(table: string, primaryKey: string, patch: Readonly<Record<string, unknown>>): Promise<Readonly<Record<string, unknown>> | null>;
403
+ delete(table: string, primaryKey: string): Promise<boolean>;
404
+ }
405
+
406
+ /** What a caller supplies to link an identity; ids + timestamps are ours. */
407
+ export declare interface SocialIdentityInput {
408
+ readonly userId: string;
409
+ readonly provider: string;
410
+ readonly providerAccountId: string;
411
+ readonly email: string | null;
412
+ readonly emailVerified: boolean;
413
+ }
414
+
415
+ export declare interface SocialIdentityRecord {
416
+ readonly id: string;
417
+ readonly userId: string;
418
+ readonly provider: string;
419
+ readonly providerAccountId: string;
420
+ readonly email: string | null;
421
+ readonly emailVerified: boolean;
422
+ readonly createdAt: Date;
423
+ readonly lastLoginAt: Date | null;
424
+ }
425
+
426
+ /**
427
+ * The identity store contract. Effect-native, mirroring `UserStore` in
428
+ * `@voltro/plugin-auth` so the two compose without adapters.
429
+ */
430
+ export declare interface SocialIdentityStore {
431
+ /** Resolve a provider account to a linked identity, or null. */
432
+ readonly findByProviderAccount: (provider: string, providerAccountId: string) => Effect.Effect<SocialIdentityRecord | null>;
433
+ /** Every provider a user has connected — the account-settings list. */
434
+ readonly listForUser: (userId: string) => Effect.Effect<ReadonlyArray<SocialIdentityRecord>>;
435
+ /** Create the link. */
436
+ readonly insert: (input: SocialIdentityInput) => Effect.Effect<SocialIdentityRecord>;
437
+ /** Stamp `lastLoginAt` on a returning login. */
438
+ readonly touch: (identityId: string, at: Date) => Effect.Effect<void>;
439
+ /** Unlink a provider from a user. Returns whether a row was removed. */
440
+ readonly remove: (userId: string, provider: string) => Effect.Effect<boolean>;
441
+ }
442
+
443
+ /**
444
+ * The ID token did not verify, or verified into claims we refuse.
445
+ *
446
+ * Signature, `iss`, `aud`, `exp`/`iat` and `nonce` all land here. They are ONE
447
+ * class on purpose: which of the five failed is useful in a log (the `message`
448
+ * says) and useless to the caller, who must do the same thing regardless —
449
+ * refuse the login.
450
+ */
451
+ export declare class SocialIdTokenError extends SocialAuthError {
452
+ constructor(code: 'id_token_invalid' | 'nonce_mismatch', message: string);
453
+ }
454
+
455
+ /** What the caller should do with this login. */
456
+ export declare type SocialLinkDecision =
457
+ /** The identity is already linked — sign this user in. */
458
+ {
459
+ readonly kind: 'sign-in';
460
+ readonly userId: string;
461
+ }
462
+ /** No identity, no account with this email — provision a new user. */
463
+ | {
464
+ readonly kind: 'create-user';
465
+ }
466
+ /** No identity, but an account with this email exists and the policy
467
+ * permits attaching to it. */
468
+ | {
469
+ readonly kind: 'link';
470
+ readonly userId: string;
471
+ }
472
+ /** Refuse the login. `error` carries the status + machine-readable code. */
473
+ | {
474
+ readonly kind: 'refuse';
475
+ readonly error: SocialAccountLinkError;
476
+ };
477
+
478
+ export declare interface SocialLinkInput {
479
+ readonly profile: SocialProfile;
480
+ /** The user the `(provider, providerAccountId)` identity row points at, or
481
+ * null when this provider account has never signed in here. */
482
+ readonly linkedUserId: string | null;
483
+ /** The user whose email equals the profile's, or null. Only consulted when
484
+ * `linkedUserId` is null. */
485
+ readonly existingUserIdByEmail: string | null;
486
+ readonly policy: SocialLinkPolicy;
487
+ }
488
+
489
+ /** How a social identity may attach to a PRE-EXISTING local account. */
490
+ export declare type SocialLinkPolicy = 'never' | 'verified-email';
491
+
492
+ /**
493
+ * What a login handoff produces: the URL to send the browser to, plus the three
494
+ * secrets the CALLBACK has to be able to present back.
495
+ *
496
+ * All three must be stashed somewhere the same browser will return them from —
497
+ * a short-lived `HttpOnly` cookie is the usual answer, and what `socialAuthPlugin`
498
+ * does — and handed to `socialCompleteLogin`. There is no server-side store
499
+ * here on purpose: this layer is transport-thin, and where an app keeps a
500
+ * ten-minute nonce is the app's decision.
501
+ */
502
+ export declare interface SocialLoginHandoff {
503
+ readonly provider: SocialProviderId;
504
+ /** The provider's authorize URL to redirect the browser to. */
505
+ readonly url: string;
506
+ /** The CSRF nonce. Compare against the callback's `state` before exchanging. */
507
+ readonly state: string;
508
+ /** The PKCE verifier. Send it with the code exchange. */
509
+ readonly codeVerifier: string;
510
+ /** The OIDC replay nonce, bound into the ID token. Empty string for a
511
+ * provider with no ID token (GitHub) — there is nothing to bind it to. */
512
+ readonly nonce: string;
513
+ /**
514
+ * How the provider will come back. `'form_post'` (Apple) means a CROSS-SITE
515
+ * POST, which a `SameSite=Lax` cookie is not sent on — the caller must store
516
+ * the three values above in a `SameSite=None; Secure` cookie or the callback
517
+ * will look exactly like a state mismatch.
518
+ */
519
+ readonly responseMode: SocialResponseMode;
520
+ }
521
+
522
+ /**
523
+ * A verified identity, normalised across the three providers.
524
+ *
525
+ * `emailVerified` is the field every security decision downstream reads. It is
526
+ * the PROVIDER's assertion that it verified ownership of `email` — never our
527
+ * inference, and never `true` by default. See `linking.ts`.
528
+ */
529
+ export declare interface SocialProfile {
530
+ readonly provider: SocialProviderId;
531
+ /** The provider's stable, opaque id for this account — `sub` on Google and
532
+ * Apple, the numeric account id on GitHub. This, not the email, is what an
533
+ * identity row is keyed by: an email can change hands, this cannot. */
534
+ readonly providerAccountId: string;
535
+ readonly email: string | null;
536
+ readonly emailVerified: boolean;
537
+ /** Apple "Hide My Email": the address is real and verified but is a per-app
538
+ * alias, not the user's address. */
539
+ readonly emailIsPrivateRelay: boolean;
540
+ readonly name: string | null;
541
+ readonly firstName: string | null;
542
+ readonly lastName: string | null;
543
+ readonly avatarUrl: string | null;
544
+ /**
545
+ * True when the name in this profile came from a source that will NOT be
546
+ * present next time — Apple's first-authorization `user` payload. If you want
547
+ * to display the user's name, this is the only callback that will ever carry
548
+ * it: persist it now.
549
+ */
550
+ readonly nameIsFirstAuthorizationOnly: boolean;
551
+ /** The verified ID-token claims (OIDC) or the raw API user (GitHub). Useful
552
+ * for app-specific fields; never re-verified downstream. */
553
+ readonly raw: Readonly<Record<string, unknown>>;
554
+ }
555
+
556
+ /** Resolve a provider descriptor, applying any endpoint overrides. Throws
557
+ * `SocialConfigError` for an unknown id. */
558
+ export declare const socialProvider: (id: string, overrides?: SocialEndpointOverrides) => SocialProviderDescriptor;
559
+
560
+ /** Per-provider credentials + options. Every field may instead come from the
561
+ * environment (see {@link socialAuthPlugin}); nothing here has a default
562
+ * value, because a default credential is a shipped secret. */
563
+ export declare interface SocialProviderConfig {
564
+ readonly clientId?: string;
565
+ /** Google / GitHub. Apple has none — it uses {@link SocialProviderConfig.appleKey}. */
566
+ readonly clientSecret?: string;
567
+ /** Apple only: Team ID + Key ID + the `.p8` contents. */
568
+ readonly appleKey?: AppleClientSecretKey;
569
+ /** Override the provider's default scopes. */
570
+ readonly scopes?: ReadonlyArray<string>;
571
+ /** Override the callback URL. Defaults to
572
+ * `<appBaseUrl><prefix>/<provider>/callback`. Must match the provider's
573
+ * registered redirect byte for byte. */
574
+ readonly redirectUri?: string;
575
+ /** Test seam — point the provider at a loopback server. */
576
+ readonly endpoints?: SocialEndpointOverrides;
577
+ }
578
+
579
+ export declare interface SocialProviderDescriptor {
580
+ readonly id: SocialProviderId;
581
+ /** Human label — logs, and the "Sign in with X" button an app renders. */
582
+ readonly label: string;
583
+ readonly authorizeUrl: string;
584
+ readonly tokenUrl: string;
585
+ /** OIDC providers only: where the ID-token signing keys live. */
586
+ readonly jwksUrl: string | null;
587
+ /** OIDC providers only: the exact `iss` an ID token must carry. */
588
+ readonly issuer: string | null;
589
+ /** Plain-OAuth2 providers only: the API origin identity is read from. */
590
+ readonly apiBase: string | null;
591
+ readonly defaultScopes: ReadonlyArray<string>;
592
+ readonly kind: 'oidc' | 'oauth2';
593
+ readonly responseMode: SocialResponseMode;
594
+ /** Can the provider tell us, per address, that it verified ownership? */
595
+ readonly assertsEmailVerification: boolean;
596
+ /** Does the provider actually implement RFC 7636? We send the challenge
597
+ * either way; this only says whether it is load-bearing there. */
598
+ readonly pkceEnforced: boolean;
599
+ }
600
+
601
+ /** The providers this package ships. */
602
+ export declare type SocialProviderId = 'google' | 'github' | 'apple';
603
+
604
+ /** How the provider hands the callback back to us. Apple POSTs (cross-site,
605
+ * `form_post`) whenever `name` / `email` scopes are requested; the other two
606
+ * redirect with a query string. This changes the cookie the state must live
607
+ * in — see `plugin.ts`. */
608
+ export declare type SocialResponseMode = 'query' | 'form_post';
609
+
610
+ /** A completed social sign-in. */
611
+ export declare interface SocialSignInOk {
612
+ readonly kind: 'ok';
613
+ readonly userId: string;
614
+ readonly subject: Subject;
615
+ /** Attach this to the response. */
616
+ readonly setCookie: string;
617
+ /** A new local user was provisioned by this login. */
618
+ readonly created: boolean;
619
+ /** The identity was attached to a PRE-EXISTING account by policy. */
620
+ readonly linked: boolean;
621
+ /** The profile carried a name that will never be sent again (Apple's
622
+ * first authorization). Persist it now if you want to display it. */
623
+ readonly oneTimeName: {
624
+ readonly firstName: string | null;
625
+ readonly lastName: string | null;
626
+ } | null;
627
+ }
628
+
629
+ export declare interface SocialSignInOptions {
630
+ /** The verified identity from `socialCompleteLogin`. */
631
+ readonly profile: SocialProfile;
632
+ readonly identities: SocialIdentityStore;
633
+ readonly users: UserStore;
634
+ /** The same `AuthConfig` the auth routes plugin runs on — session secret,
635
+ * cookie flags, default tenant, subject guards. */
636
+ readonly config: AuthConfig;
637
+ /** Default `'never'`. Read `linking.ts` before changing it. */
638
+ readonly policy?: SocialLinkPolicy;
639
+ /** Recorded on the session row for the device list. */
640
+ readonly ipAddress?: string | null;
641
+ readonly userAgent?: string | null;
642
+ }
643
+
644
+ /** The login was refused. `code` is stable; `message` is safe to show a user. */
645
+ export declare interface SocialSignInRefused {
646
+ readonly kind: 'refused';
647
+ readonly status: number;
648
+ readonly code: string;
649
+ readonly message: string;
650
+ }
651
+
652
+ export declare type SocialSignInResult = SocialSignInOk | SocialSignInRefused;
653
+
654
+ /**
655
+ * The callback's `state` did not match the value minted for this browser.
656
+ *
657
+ * A distinct class because it is a distinct event: every other error means the
658
+ * provider or the account said no, this one means the request did not come from
659
+ * a login THIS server started. Worth alerting on separately — it is the shape a
660
+ * login-CSRF attempt takes.
661
+ */
662
+ export declare class SocialStateMismatchError extends SocialAuthError {
663
+ constructor(message: string);
664
+ }
665
+
666
+ export { }