@warlock.js/auth 5.15.0 → 5.17.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/esm/commands/auth-cleanup-command.mjs +1 -1
  3. package/esm/commands/auth-purge-never-expiring-command.mjs +1 -1
  4. package/esm/contracts/index.d.mts +1 -1
  5. package/esm/contracts/providers.d.mts +59 -1
  6. package/esm/contracts/providers.d.mts.map +1 -1
  7. package/esm/index.d.mts +11 -2
  8. package/esm/index.mjs +11 -1
  9. package/esm/localization/ar.translations.mjs +16 -0
  10. package/esm/localization/ar.translations.mjs.map +1 -0
  11. package/esm/localization/auth-default-translations.d.mts +10 -0
  12. package/esm/localization/auth-default-translations.d.mts.map +1 -0
  13. package/esm/localization/auth-default-translations.mjs +15 -0
  14. package/esm/localization/auth-default-translations.mjs.map +1 -0
  15. package/esm/localization/auth-translations.type.d.mts +27 -0
  16. package/esm/localization/auth-translations.type.d.mts.map +1 -0
  17. package/esm/localization/en.translations.mjs +16 -0
  18. package/esm/localization/en.translations.mjs.map +1 -0
  19. package/esm/localization/index.mjs +4 -0
  20. package/esm/localization/init.mjs +8 -0
  21. package/esm/localization/init.mjs.map +1 -0
  22. package/esm/localization/register-auth-translations.d.mts +17 -0
  23. package/esm/localization/register-auth-translations.d.mts.map +1 -0
  24. package/esm/localization/register-auth-translations.mjs +29 -0
  25. package/esm/localization/register-auth-translations.mjs.map +1 -0
  26. package/esm/middleware/auth.middleware.d.mts.map +1 -1
  27. package/esm/middleware/auth.middleware.mjs +4 -2
  28. package/esm/middleware/auth.middleware.mjs.map +1 -1
  29. package/esm/middleware/csrf-origin-check.d.mts +9 -0
  30. package/esm/middleware/csrf-origin-check.d.mts.map +1 -1
  31. package/esm/middleware/csrf-origin-check.mjs +12 -56
  32. package/esm/middleware/csrf-origin-check.mjs.map +1 -1
  33. package/esm/middleware/localized-login-path.mjs +48 -0
  34. package/esm/middleware/localized-login-path.mjs.map +1 -0
  35. package/esm/models/access-token/access-token.model.d.mts +7 -0
  36. package/esm/models/access-token/access-token.model.d.mts.map +1 -1
  37. package/esm/models/access-token/access-token.model.mjs +3 -0
  38. package/esm/models/access-token/access-token.model.mjs.map +1 -1
  39. package/esm/models/one-time-token/one-time-token.model.d.mts +6 -0
  40. package/esm/models/one-time-token/one-time-token.model.d.mts.map +1 -1
  41. package/esm/models/one-time-token/one-time-token.model.mjs +3 -0
  42. package/esm/models/one-time-token/one-time-token.model.mjs.map +1 -1
  43. package/esm/models/refresh-token/refresh-token.model.d.mts +7 -0
  44. package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
  45. package/esm/models/refresh-token/refresh-token.model.mjs +3 -0
  46. package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
  47. package/esm/providers/apple-provider.d.mts +74 -0
  48. package/esm/providers/apple-provider.d.mts.map +1 -0
  49. package/esm/providers/apple-provider.mjs +157 -0
  50. package/esm/providers/apple-provider.mjs.map +1 -0
  51. package/esm/providers/discord-provider.d.mts +25 -0
  52. package/esm/providers/discord-provider.d.mts.map +1 -0
  53. package/esm/providers/discord-provider.mjs +84 -0
  54. package/esm/providers/discord-provider.mjs.map +1 -0
  55. package/esm/providers/facebook-provider.d.mts +36 -0
  56. package/esm/providers/facebook-provider.d.mts.map +1 -0
  57. package/esm/providers/facebook-provider.mjs +94 -0
  58. package/esm/providers/facebook-provider.mjs.map +1 -0
  59. package/esm/providers/github-provider.d.mts +31 -0
  60. package/esm/providers/github-provider.d.mts.map +1 -0
  61. package/esm/providers/github-provider.mjs +115 -0
  62. package/esm/providers/github-provider.mjs.map +1 -0
  63. package/esm/providers/index.d.mts +7 -1
  64. package/esm/providers/index.mjs +6 -0
  65. package/esm/providers/linkedin-provider.d.mts +35 -0
  66. package/esm/providers/linkedin-provider.d.mts.map +1 -0
  67. package/esm/providers/linkedin-provider.mjs +108 -0
  68. package/esm/providers/linkedin-provider.mjs.map +1 -0
  69. package/esm/providers/provider-login.d.mts +2 -1
  70. package/esm/providers/provider-login.d.mts.map +1 -1
  71. package/esm/providers/provider-login.mjs +47 -7
  72. package/esm/providers/provider-login.mjs.map +1 -1
  73. package/esm/providers/provider-state-cookie.mjs +35 -16
  74. package/esm/providers/provider-state-cookie.mjs.map +1 -1
  75. package/esm/providers/x-provider.d.mts +35 -0
  76. package/esm/providers/x-provider.d.mts.map +1 -0
  77. package/esm/providers/x-provider.mjs +101 -0
  78. package/esm/providers/x-provider.mjs.map +1 -0
  79. package/esm/services/auth-config.mjs +16 -1
  80. package/esm/services/auth-config.mjs.map +1 -1
  81. package/esm/services/auth.service.mjs +1 -1
  82. package/esm/services/generate-jwt-secret.mjs +1 -1
  83. package/llms-full.txt +1075 -765
  84. package/llms.txt +5 -5
  85. package/package.json +8 -8
  86. package/skills/auth-basics/SKILL.md +16 -1
  87. package/skills/customize-token-storage/SKILL.md +160 -122
  88. package/skills/customize-user-type/SKILL.md +168 -142
  89. package/skills/handle-login-and-logout/SKILL.md +179 -165
  90. package/skills/login-with-providers/SKILL.md +131 -2
  91. package/skills/protect-routes/SKILL.md +308 -249
  92. package/skills/register-user/SKILL.md +15 -1
  93. package/skills/run-auth-commands/SKILL.md +16 -1
  94. package/skills/throttle-login-attempts/SKILL.md +1 -1
@@ -0,0 +1,157 @@
1
+ import { InvalidProviderCallbackError } from "../errors/invalid-provider-callback.error.mjs";
2
+ import { loadOptionalPeer } from "../services/optional-peer.mjs";
3
+ import { pkceChallenge } from "./google-provider.mjs";
4
+
5
+ //#region ../auth/src/providers/apple-provider.ts
6
+ const AUTHORIZATION_ENDPOINT = "https://appleid.apple.com/auth/authorize";
7
+ const TOKEN_ENDPOINT = "https://appleid.apple.com/auth/token";
8
+ const JWKS_URI = "https://appleid.apple.com/auth/keys";
9
+ const ISSUER = "https://appleid.apple.com";
10
+ const DEFAULT_SCOPES = ["name", "email"];
11
+ /** Apple's own recommendation for a per-request client secret: minutes, not the 6-month maximum it allows. */
12
+ const CLIENT_SECRET_TTL_SECONDS = 300;
13
+ /** The `warlock add` feature that installs `jose`. */
14
+ const APPLE_AUTH_FEATURE = "auth-apple";
15
+ /** Apple's key set, created once per process; `jose` caches the fetched JWKS inside it. */
16
+ let appleKeySet;
17
+ /**
18
+ * Apple ("Sign in with Apple") is OpenID Connect over the authorization code
19
+ * flow with PKCE (S256), `state` and `nonce`, the same shape as
20
+ * {@link GoogleProvider} — with two differences that come straight from
21
+ * Apple's own docs (this package has no Apple developer account, so nothing
22
+ * here is proven against a live exchange):
23
+ *
24
+ * 1. Apple has no static client secret. The "secret" the token endpoint wants
25
+ * is a fresh ES256 JWT (`iss` = team id, `sub` = client id, `aud` =
26
+ * `https://appleid.apple.com`, signed with the app's private key and the
27
+ * key id as `kid`) — signed once per callback here rather than cached, to
28
+ * avoid ever presenting a stale one.
29
+ * 2. Requesting `name`/`email` (the default scopes) forces
30
+ * `response_mode=form_post`: Apple POSTs `code`, `state` and, on the
31
+ * user's FIRST authorization only, a `user` field — a JSON string with
32
+ * `{ name: { firstName, lastName }, email } ` that never comes back again
33
+ * on a later login. The callback route must therefore accept a POST body,
34
+ * not just a query string, and `request.input` must read that body the
35
+ * same way it already reads a GET query for the other providers.
36
+ *
37
+ * The id_token is verified with `jose` against Apple's JWKS (signature,
38
+ * `iss`, `aud` = the client id, `exp`), then its `nonce` is compared. Apple is
39
+ * also documented to send `email_verified` (and `is_private_email`) as the
40
+ * STRING `"true"`/`"false"` rather than a JSON boolean on some responses, so
41
+ * both forms are accepted here — treating only the unambiguous `"false"` (or
42
+ * `false`) as unverified. A private-relay address (`@privaterelay.appleid.com`)
43
+ * is still a real, working forwarding address and is accepted as any other.
44
+ */
45
+ var AppleProvider = class {
46
+ constructor(settings) {
47
+ this.settings = settings;
48
+ this.name = "apple";
49
+ this.callbackMode = "form_post";
50
+ }
51
+ authorizationUrl(state) {
52
+ const scopes = this.settings.scopes ?? DEFAULT_SCOPES;
53
+ const url = new URL(AUTHORIZATION_ENDPOINT);
54
+ url.searchParams.set("client_id", this.settings.clientId);
55
+ url.searchParams.set("redirect_uri", this.settings.redirectUri);
56
+ url.searchParams.set("response_type", "code");
57
+ url.searchParams.set("scope", scopes.join(" "));
58
+ url.searchParams.set("state", state.state);
59
+ url.searchParams.set("nonce", state.nonce);
60
+ url.searchParams.set("code_challenge", pkceChallenge(state.codeVerifier));
61
+ url.searchParams.set("code_challenge_method", "S256");
62
+ if (scopes.includes("name") || scopes.includes("email")) url.searchParams.set("response_mode", "form_post");
63
+ return url.toString();
64
+ }
65
+ async handleCallback({ query, expected }) {
66
+ const jose = await loadOptionalPeer("jose", APPLE_AUTH_FEATURE);
67
+ const code = query.code;
68
+ if (typeof code !== "string" || code.length === 0) throw new InvalidProviderCallbackError("missing-code");
69
+ const idToken = await this.exchangeCode(jose, code, expected.codeVerifier);
70
+ const claims = await this.verifyIdToken(jose, idToken);
71
+ if (typeof claims.nonce !== "string" || claims.nonce !== expected.nonce) throw new InvalidProviderCallbackError("nonce-mismatch");
72
+ if (typeof claims.sub !== "string" || claims.sub.length === 0) throw new InvalidProviderCallbackError("missing-subject");
73
+ return {
74
+ provider: this.name,
75
+ providerUserId: claims.sub,
76
+ email: typeof claims.email === "string" ? claims.email : void 0,
77
+ emailVerified: isVerifiedFlag(claims.email_verified),
78
+ name: nameFromUserField(query.user),
79
+ raw: claims
80
+ };
81
+ }
82
+ /** Sign a fresh ES256 client-secret JWT — Apple has no static one. */
83
+ async clientSecret(jose) {
84
+ const key = await jose.importPKCS8(this.settings.privateKey, "ES256");
85
+ const now = Math.floor(Date.now() / 1e3);
86
+ try {
87
+ return await new jose.SignJWT({}).setProtectedHeader({
88
+ alg: "ES256",
89
+ kid: this.settings.keyId
90
+ }).setIssuer(this.settings.teamId).setIssuedAt().setExpirationTime(now + CLIENT_SECRET_TTL_SECONDS).setAudience(ISSUER).setSubject(this.settings.clientId).sign(key);
91
+ } catch (error) {
92
+ throw new InvalidProviderCallbackError("client-secret-signing-failed", error);
93
+ }
94
+ }
95
+ /** Redeem the authorization code (with the PKCE verifier) for an id_token. */
96
+ async exchangeCode(jose, code, codeVerifier) {
97
+ const clientSecret = await this.clientSecret(jose);
98
+ let body;
99
+ try {
100
+ const response = await fetch(TOKEN_ENDPOINT, {
101
+ method: "POST",
102
+ headers: { "content-type": "application/x-www-form-urlencoded" },
103
+ body: new URLSearchParams({
104
+ code,
105
+ client_id: this.settings.clientId,
106
+ client_secret: clientSecret,
107
+ redirect_uri: this.settings.redirectUri,
108
+ grant_type: "authorization_code",
109
+ code_verifier: codeVerifier
110
+ })
111
+ });
112
+ if (!response.ok) throw new Error(`Apple token endpoint answered ${response.status}`);
113
+ body = await response.json();
114
+ } catch (error) {
115
+ throw new InvalidProviderCallbackError("code-exchange-failed", error);
116
+ }
117
+ if (typeof body.id_token !== "string") throw new InvalidProviderCallbackError("missing-id-token");
118
+ return body.id_token;
119
+ }
120
+ /** Verify signature, issuer, audience and expiry; resolve the claims. */
121
+ async verifyIdToken(jose, idToken) {
122
+ appleKeySet ??= jose.createRemoteJWKSet(new URL(JWKS_URI));
123
+ try {
124
+ const { payload } = await jose.jwtVerify(idToken, appleKeySet, {
125
+ issuer: [ISSUER],
126
+ audience: this.settings.clientId
127
+ });
128
+ return payload;
129
+ } catch (error) {
130
+ throw new InvalidProviderCallbackError("invalid-id-token", error);
131
+ }
132
+ }
133
+ };
134
+ /** Apple's `email_verified` arrives as a JSON boolean on some responses, a `"true"`/`"false"` string on others. */
135
+ function isVerifiedFlag(value) {
136
+ return value === true || value === "true";
137
+ }
138
+ /**
139
+ * Apple sends the account name only once, on the FIRST authorization, as a
140
+ * `user` form field holding a JSON string: `{"name":{"firstName","lastName"}}`.
141
+ * Every later login omits it entirely — callers that need the name must save
142
+ * it themselves the first time.
143
+ */
144
+ function nameFromUserField(user) {
145
+ if (typeof user !== "string" || user.length === 0) return void 0;
146
+ try {
147
+ const parsed = JSON.parse(user);
148
+ const name = [parsed.name?.firstName, parsed.name?.lastName].filter((part) => Boolean(part)).join(" ");
149
+ return name.length > 0 ? name : void 0;
150
+ } catch {
151
+ return;
152
+ }
153
+ }
154
+
155
+ //#endregion
156
+ export { APPLE_AUTH_FEATURE, AppleProvider };
157
+ //# sourceMappingURL=apple-provider.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apple-provider.mjs","names":[],"sources":["../../../../../../../auth/src/providers/apple-provider.ts"],"sourcesContent":["import type {\n AppleProviderConfig,\n AuthProvider,\n ProviderAuthorizationState,\n ProviderCallbackParams,\n ProviderProfile,\n} from \"../contracts/providers\";\nimport { InvalidProviderCallbackError } from \"../errors/invalid-provider-callback.error\";\nimport { loadOptionalPeer } from \"../services/optional-peer\";\nimport type { JoseModule } from \"./google-provider\";\nimport { pkceChallenge } from \"./google-provider\";\n\nconst AUTHORIZATION_ENDPOINT = \"https://appleid.apple.com/auth/authorize\";\nconst TOKEN_ENDPOINT = \"https://appleid.apple.com/auth/token\";\nconst JWKS_URI = \"https://appleid.apple.com/auth/keys\";\nconst ISSUER = \"https://appleid.apple.com\";\nconst DEFAULT_SCOPES = [\"name\", \"email\"];\n/** Apple's own recommendation for a per-request client secret: minutes, not the 6-month maximum it allows. */\nconst CLIENT_SECRET_TTL_SECONDS = 5 * 60;\n\n/** The `warlock add` feature that installs `jose`. */\nexport const APPLE_AUTH_FEATURE = \"auth-apple\";\n\ntype AppleSignJwtBuilder = {\n setIssuer: (issuer: string) => AppleSignJwtBuilder;\n setIssuedAt: () => AppleSignJwtBuilder;\n setExpirationTime: (time: number) => AppleSignJwtBuilder;\n setAudience: (audience: string) => AppleSignJwtBuilder;\n setSubject: (subject: string) => AppleSignJwtBuilder;\n sign: (key: unknown) => Promise<string>;\n};\n\n/**\n * The slice of `jose` the Apple provider uses on top of {@link JoseModule} —\n * `importPKCS8` and `SignJWT` sign the ES256 client-secret JWT Apple requires\n * instead of a static client secret.\n */\nexport type AppleJoseModule = JoseModule & {\n importPKCS8: (pkcs8: string, alg: string) => Promise<unknown>;\n SignJWT: new (payload: Record<string, unknown>) => {\n setProtectedHeader: (header: Record<string, unknown>) => AppleSignJwtBuilder;\n };\n};\n\n/** Apple's key set, created once per process; `jose` caches the fetched JWKS inside it. */\nlet appleKeySet: unknown;\n\n/**\n * Apple (\"Sign in with Apple\") is OpenID Connect over the authorization code\n * flow with PKCE (S256), `state` and `nonce`, the same shape as\n * {@link GoogleProvider} — with two differences that come straight from\n * Apple's own docs (this package has no Apple developer account, so nothing\n * here is proven against a live exchange):\n *\n * 1. Apple has no static client secret. The \"secret\" the token endpoint wants\n * is a fresh ES256 JWT (`iss` = team id, `sub` = client id, `aud` =\n * `https://appleid.apple.com`, signed with the app's private key and the\n * key id as `kid`) — signed once per callback here rather than cached, to\n * avoid ever presenting a stale one.\n * 2. Requesting `name`/`email` (the default scopes) forces\n * `response_mode=form_post`: Apple POSTs `code`, `state` and, on the\n * user's FIRST authorization only, a `user` field — a JSON string with\n * `{ name: { firstName, lastName }, email } ` that never comes back again\n * on a later login. The callback route must therefore accept a POST body,\n * not just a query string, and `request.input` must read that body the\n * same way it already reads a GET query for the other providers.\n *\n * The id_token is verified with `jose` against Apple's JWKS (signature,\n * `iss`, `aud` = the client id, `exp`), then its `nonce` is compared. Apple is\n * also documented to send `email_verified` (and `is_private_email`) as the\n * STRING `\"true\"`/`\"false\"` rather than a JSON boolean on some responses, so\n * both forms are accepted here — treating only the unambiguous `\"false\"` (or\n * `false`) as unverified. A private-relay address (`@privaterelay.appleid.com`)\n * is still a real, working forwarding address and is accepted as any other.\n */\nexport class AppleProvider implements AuthProvider {\n public readonly name = \"apple\";\n /** Apple's default scopes (`name`, `email`) force `response_mode=form_post`. */\n public readonly callbackMode = \"form_post\";\n\n public constructor(private readonly settings: AppleProviderConfig) {}\n\n public authorizationUrl(state: ProviderAuthorizationState): string {\n const scopes = this.settings.scopes ?? DEFAULT_SCOPES;\n const url = new URL(AUTHORIZATION_ENDPOINT);\n\n url.searchParams.set(\"client_id\", this.settings.clientId);\n url.searchParams.set(\"redirect_uri\", this.settings.redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", scopes.join(\" \"));\n url.searchParams.set(\"state\", state.state);\n url.searchParams.set(\"nonce\", state.nonce);\n url.searchParams.set(\"code_challenge\", pkceChallenge(state.codeVerifier));\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n\n // Apple requires form_post whenever name/email scopes are requested.\n if (scopes.includes(\"name\") || scopes.includes(\"email\")) {\n url.searchParams.set(\"response_mode\", \"form_post\");\n }\n\n return url.toString();\n }\n\n public async handleCallback({\n query,\n expected,\n }: ProviderCallbackParams): Promise<ProviderProfile> {\n // Load the peer first: a missing SDK fails loudly before any network call.\n const jose = await loadOptionalPeer<AppleJoseModule>(\"jose\", APPLE_AUTH_FEATURE);\n\n const code = query.code;\n\n if (typeof code !== \"string\" || code.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-code\");\n }\n\n const idToken = await this.exchangeCode(jose, code, expected.codeVerifier);\n const claims = await this.verifyIdToken(jose, idToken);\n\n if (typeof claims.nonce !== \"string\" || claims.nonce !== expected.nonce) {\n throw new InvalidProviderCallbackError(\"nonce-mismatch\");\n }\n\n if (typeof claims.sub !== \"string\" || claims.sub.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-subject\");\n }\n\n return {\n provider: this.name,\n providerUserId: claims.sub,\n email: typeof claims.email === \"string\" ? claims.email : undefined,\n emailVerified: isVerifiedFlag(claims.email_verified),\n name: nameFromUserField(query.user),\n raw: claims,\n };\n }\n\n /** Sign a fresh ES256 client-secret JWT — Apple has no static one. */\n private async clientSecret(jose: AppleJoseModule): Promise<string> {\n const key = await jose.importPKCS8(this.settings.privateKey, \"ES256\");\n const now = Math.floor(Date.now() / 1000);\n\n try {\n return await new jose.SignJWT({})\n .setProtectedHeader({ alg: \"ES256\", kid: this.settings.keyId })\n .setIssuer(this.settings.teamId)\n .setIssuedAt()\n .setExpirationTime(now + CLIENT_SECRET_TTL_SECONDS)\n .setAudience(ISSUER)\n .setSubject(this.settings.clientId)\n .sign(key);\n } catch (error) {\n throw new InvalidProviderCallbackError(\"client-secret-signing-failed\", error);\n }\n }\n\n /** Redeem the authorization code (with the PKCE verifier) for an id_token. */\n private async exchangeCode(\n jose: AppleJoseModule,\n code: string,\n codeVerifier: string,\n ): Promise<string> {\n const clientSecret = await this.clientSecret(jose);\n let body: { id_token?: unknown };\n\n try {\n const response = await fetch(TOKEN_ENDPOINT, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n code,\n client_id: this.settings.clientId,\n client_secret: clientSecret,\n redirect_uri: this.settings.redirectUri,\n grant_type: \"authorization_code\",\n code_verifier: codeVerifier,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Apple token endpoint answered ${response.status}`);\n }\n\n body = (await response.json()) as { id_token?: unknown };\n } catch (error) {\n throw new InvalidProviderCallbackError(\"code-exchange-failed\", error);\n }\n\n if (typeof body.id_token !== \"string\") {\n throw new InvalidProviderCallbackError(\"missing-id-token\");\n }\n\n return body.id_token;\n }\n\n /** Verify signature, issuer, audience and expiry; resolve the claims. */\n private async verifyIdToken(\n jose: AppleJoseModule,\n idToken: string,\n ): Promise<Record<string, unknown>> {\n appleKeySet ??= jose.createRemoteJWKSet(new URL(JWKS_URI));\n\n try {\n const { payload } = await jose.jwtVerify(idToken, appleKeySet, {\n issuer: [ISSUER],\n audience: this.settings.clientId,\n });\n\n return payload;\n } catch (error) {\n throw new InvalidProviderCallbackError(\"invalid-id-token\", error);\n }\n }\n}\n\n/** Apple's `email_verified` arrives as a JSON boolean on some responses, a `\"true\"`/`\"false\"` string on others. */\nfunction isVerifiedFlag(value: unknown): boolean {\n return value === true || value === \"true\";\n}\n\n/**\n * Apple sends the account name only once, on the FIRST authorization, as a\n * `user` form field holding a JSON string: `{\"name\":{\"firstName\",\"lastName\"}}`.\n * Every later login omits it entirely — callers that need the name must save\n * it themselves the first time.\n */\nfunction nameFromUserField(user: unknown): string | undefined {\n if (typeof user !== \"string\" || user.length === 0) return undefined;\n\n try {\n const parsed = JSON.parse(user) as { name?: { firstName?: string; lastName?: string } };\n const first = parsed.name?.firstName;\n const last = parsed.name?.lastName;\n const name = [first, last].filter((part): part is string => Boolean(part)).join(\" \");\n\n return name.length > 0 ? name : undefined;\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { AuthProvider, DiscordProviderConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile } from "../contracts/providers.mjs";
2
+
3
+ //#region ../auth/src/providers/discord-provider.d.ts
4
+ /**
5
+ * Discord sign-in: plain OAuth 2 authorization code flow with PKCE (S256).
6
+ * Like GitHub, Discord is not OpenID Connect — there is no `id_token`; the
7
+ * access token is redeemed once for `/users/@me` over a direct HTTPS call.
8
+ * No vendor SDK is involved.
9
+ */
10
+ declare class DiscordProvider implements AuthProvider {
11
+ private readonly settings;
12
+ readonly name = "discord";
13
+ constructor(settings: DiscordProviderConfig);
14
+ authorizationUrl(state: ProviderAuthorizationState): string;
15
+ handleCallback({
16
+ query,
17
+ expected
18
+ }: ProviderCallbackParams): Promise<ProviderProfile>;
19
+ /** Redeem the authorization code (with the PKCE verifier) for an access token. */
20
+ private exchangeCode;
21
+ private fetchUser;
22
+ }
23
+ //#endregion
24
+ export { DiscordProvider };
25
+ //# sourceMappingURL=discord-provider.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-provider.d.mts","names":[],"sources":["../../../../../../../auth/src/providers/discord-provider.ts"],"mappings":";;;;;AA8BA;;;;cAAa,eAAA,YAA2B,YAAA;EAAA,iBAGF,QAAA;EAAA,SAFpB,IAAA;cAEoB,QAAA,EAAU,qBAAA;EAEvC,gBAAA,CAAiB,KAAA,EAAO,0BAAA;EAclB,cAAA;IACX,KAAA;IACA;EAAA,GACC,sBAAA,GAAyB,OAAA,CAAQ,eAAA;EAtBE;EAAA,QAkDxB,YAAA;EAAA,QAiCA,SAAA;AAAA"}
@@ -0,0 +1,84 @@
1
+ import { InvalidProviderCallbackError } from "../errors/invalid-provider-callback.error.mjs";
2
+ import { pkceChallenge } from "./google-provider.mjs";
3
+
4
+ //#region ../auth/src/providers/discord-provider.ts
5
+ const AUTHORIZATION_ENDPOINT = "https://discord.com/oauth2/authorize";
6
+ const TOKEN_ENDPOINT = "https://discord.com/api/oauth2/token";
7
+ const USER_ENDPOINT = "https://discord.com/api/users/@me";
8
+ const DEFAULT_SCOPES = ["identify", "email"];
9
+ /**
10
+ * Discord sign-in: plain OAuth 2 authorization code flow with PKCE (S256).
11
+ * Like GitHub, Discord is not OpenID Connect — there is no `id_token`; the
12
+ * access token is redeemed once for `/users/@me` over a direct HTTPS call.
13
+ * No vendor SDK is involved.
14
+ */
15
+ var DiscordProvider = class {
16
+ constructor(settings) {
17
+ this.settings = settings;
18
+ this.name = "discord";
19
+ }
20
+ authorizationUrl(state) {
21
+ const url = new URL(AUTHORIZATION_ENDPOINT);
22
+ url.searchParams.set("client_id", this.settings.clientId);
23
+ url.searchParams.set("redirect_uri", this.settings.redirectUri);
24
+ url.searchParams.set("response_type", "code");
25
+ url.searchParams.set("scope", (this.settings.scopes ?? DEFAULT_SCOPES).join(" "));
26
+ url.searchParams.set("state", state.state);
27
+ url.searchParams.set("code_challenge", pkceChallenge(state.codeVerifier));
28
+ url.searchParams.set("code_challenge_method", "S256");
29
+ return url.toString();
30
+ }
31
+ async handleCallback({ query, expected }) {
32
+ const code = query.code;
33
+ if (typeof code !== "string" || code.length === 0) throw new InvalidProviderCallbackError("missing-code");
34
+ const accessToken = await this.exchangeCode(code, expected.codeVerifier);
35
+ const user = await this.fetchUser(accessToken);
36
+ if (typeof user.id !== "string" || user.id.length === 0) throw new InvalidProviderCallbackError("missing-subject");
37
+ return {
38
+ provider: this.name,
39
+ providerUserId: user.id,
40
+ email: user.email ?? void 0,
41
+ emailVerified: user.verified === true,
42
+ name: user.global_name ?? user.username,
43
+ avatar: user.avatar ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png` : void 0,
44
+ raw: user
45
+ };
46
+ }
47
+ /** Redeem the authorization code (with the PKCE verifier) for an access token. */
48
+ async exchangeCode(code, codeVerifier) {
49
+ let body;
50
+ try {
51
+ const response = await fetch(TOKEN_ENDPOINT, {
52
+ method: "POST",
53
+ headers: { "content-type": "application/x-www-form-urlencoded" },
54
+ body: new URLSearchParams({
55
+ code,
56
+ client_id: this.settings.clientId,
57
+ client_secret: this.settings.clientSecret,
58
+ redirect_uri: this.settings.redirectUri,
59
+ grant_type: "authorization_code",
60
+ code_verifier: codeVerifier
61
+ })
62
+ });
63
+ if (!response.ok) throw new Error(`Discord token endpoint answered ${response.status}`);
64
+ body = await response.json();
65
+ } catch (error) {
66
+ throw new InvalidProviderCallbackError("code-exchange-failed", error);
67
+ }
68
+ if (typeof body.access_token !== "string" || body.access_token.length === 0) throw new InvalidProviderCallbackError("missing-access-token");
69
+ return body.access_token;
70
+ }
71
+ async fetchUser(accessToken) {
72
+ try {
73
+ const response = await fetch(USER_ENDPOINT, { headers: { authorization: `Bearer ${accessToken}` } });
74
+ if (!response.ok) throw new Error(`Discord user endpoint answered ${response.status}`);
75
+ return await response.json();
76
+ } catch (error) {
77
+ throw new InvalidProviderCallbackError("profile-fetch-failed", error);
78
+ }
79
+ }
80
+ };
81
+
82
+ //#endregion
83
+ export { DiscordProvider };
84
+ //# sourceMappingURL=discord-provider.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-provider.mjs","names":[],"sources":["../../../../../../../auth/src/providers/discord-provider.ts"],"sourcesContent":["import type {\n AuthProvider,\n DiscordProviderConfig,\n ProviderAuthorizationState,\n ProviderCallbackParams,\n ProviderProfile,\n} from \"../contracts/providers\";\nimport { InvalidProviderCallbackError } from \"../errors/invalid-provider-callback.error\";\nimport { pkceChallenge } from \"./google-provider\";\n\nconst AUTHORIZATION_ENDPOINT = \"https://discord.com/oauth2/authorize\";\nconst TOKEN_ENDPOINT = \"https://discord.com/api/oauth2/token\";\nconst USER_ENDPOINT = \"https://discord.com/api/users/@me\";\nconst DEFAULT_SCOPES = [\"identify\", \"email\"];\n\ntype DiscordUser = {\n id: string;\n username: string;\n global_name?: string | null;\n avatar?: string | null;\n email?: string | null;\n verified?: boolean;\n};\n\n/**\n * Discord sign-in: plain OAuth 2 authorization code flow with PKCE (S256).\n * Like GitHub, Discord is not OpenID Connect — there is no `id_token`; the\n * access token is redeemed once for `/users/@me` over a direct HTTPS call.\n * No vendor SDK is involved.\n */\nexport class DiscordProvider implements AuthProvider {\n public readonly name = \"discord\";\n\n public constructor(private readonly settings: DiscordProviderConfig) {}\n\n public authorizationUrl(state: ProviderAuthorizationState): string {\n const url = new URL(AUTHORIZATION_ENDPOINT);\n\n url.searchParams.set(\"client_id\", this.settings.clientId);\n url.searchParams.set(\"redirect_uri\", this.settings.redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", (this.settings.scopes ?? DEFAULT_SCOPES).join(\" \"));\n url.searchParams.set(\"state\", state.state);\n url.searchParams.set(\"code_challenge\", pkceChallenge(state.codeVerifier));\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n\n return url.toString();\n }\n\n public async handleCallback({\n query,\n expected,\n }: ProviderCallbackParams): Promise<ProviderProfile> {\n const code = query.code;\n\n if (typeof code !== \"string\" || code.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-code\");\n }\n\n const accessToken = await this.exchangeCode(code, expected.codeVerifier);\n const user = await this.fetchUser(accessToken);\n\n if (typeof user.id !== \"string\" || user.id.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-subject\");\n }\n\n return {\n provider: this.name,\n providerUserId: user.id,\n email: user.email ?? undefined,\n emailVerified: user.verified === true,\n name: user.global_name ?? user.username,\n avatar: user.avatar\n ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`\n : undefined,\n raw: user,\n };\n }\n\n /** Redeem the authorization code (with the PKCE verifier) for an access token. */\n private async exchangeCode(code: string, codeVerifier: string): Promise<string> {\n let body: { access_token?: unknown };\n\n try {\n const response = await fetch(TOKEN_ENDPOINT, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n code,\n client_id: this.settings.clientId,\n client_secret: this.settings.clientSecret,\n redirect_uri: this.settings.redirectUri,\n grant_type: \"authorization_code\",\n code_verifier: codeVerifier,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Discord token endpoint answered ${response.status}`);\n }\n\n body = (await response.json()) as { access_token?: unknown };\n } catch (error) {\n throw new InvalidProviderCallbackError(\"code-exchange-failed\", error);\n }\n\n if (typeof body.access_token !== \"string\" || body.access_token.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-access-token\");\n }\n\n return body.access_token;\n }\n\n private async fetchUser(accessToken: string): Promise<DiscordUser> {\n try {\n const response = await fetch(USER_ENDPOINT, {\n headers: { authorization: `Bearer ${accessToken}` },\n });\n\n if (!response.ok) {\n throw new Error(`Discord user endpoint answered ${response.status}`);\n }\n\n return (await response.json()) as DiscordUser;\n } catch (error) {\n throw new InvalidProviderCallbackError(\"profile-fetch-failed\", error);\n }\n }\n}\n"],"mappings":";;;;AAUA,MAAM,yBAAyB;AAC/B,MAAM,iBAAiB;AACvB,MAAM,gBAAgB;AACtB,MAAM,iBAAiB,CAAC,YAAY,OAAO;;;;;;;AAiB3C,IAAa,kBAAb,MAAqD;CAGnD,AAAO,YAAY,AAAiB,UAAiC;EAAjC;cAFb;CAE+C;CAEtE,AAAO,iBAAiB,OAA2C;EACjE,MAAM,MAAM,IAAI,IAAI,sBAAsB;EAE1C,IAAI,aAAa,IAAI,aAAa,KAAK,SAAS,QAAQ;EACxD,IAAI,aAAa,IAAI,gBAAgB,KAAK,SAAS,WAAW;EAC9D,IAAI,aAAa,IAAI,iBAAiB,MAAM;EAC5C,IAAI,aAAa,IAAI,UAAU,KAAK,SAAS,UAAU,eAAc,CAAE,KAAK,GAAG,CAAC;EAChF,IAAI,aAAa,IAAI,SAAS,MAAM,KAAK;EACzC,IAAI,aAAa,IAAI,kBAAkB,cAAc,MAAM,YAAY,CAAC;EACxE,IAAI,aAAa,IAAI,yBAAyB,MAAM;EAEpD,OAAO,IAAI,SAAS;CACtB;CAEA,MAAa,eAAe,EAC1B,OACA,YACmD;EACnD,MAAM,OAAO,MAAM;EAEnB,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAC9C,MAAM,IAAI,6BAA6B,cAAc;EAGvD,MAAM,cAAc,MAAM,KAAK,aAAa,MAAM,SAAS,YAAY;EACvE,MAAM,OAAO,MAAM,KAAK,UAAU,WAAW;EAE7C,IAAI,OAAO,KAAK,OAAO,YAAY,KAAK,GAAG,WAAW,GACpD,MAAM,IAAI,6BAA6B,iBAAiB;EAG1D,OAAO;GACL,UAAU,KAAK;GACf,gBAAgB,KAAK;GACrB,OAAO,KAAK,SAAS;GACrB,eAAe,KAAK,aAAa;GACjC,MAAM,KAAK,eAAe,KAAK;GAC/B,QAAQ,KAAK,SACT,sCAAsC,KAAK,GAAG,GAAG,KAAK,OAAO,QAC7D;GACJ,KAAK;EACP;CACF;;CAGA,MAAc,aAAa,MAAc,cAAuC;EAC9E,IAAI;EAEJ,IAAI;GACF,MAAM,WAAW,MAAM,MAAM,gBAAgB;IAC3C,QAAQ;IACR,SAAS,EAAE,gBAAgB,oCAAoC;IAC/D,MAAM,IAAI,gBAAgB;KACxB;KACA,WAAW,KAAK,SAAS;KACzB,eAAe,KAAK,SAAS;KAC7B,cAAc,KAAK,SAAS;KAC5B,YAAY;KACZ,eAAe;IACjB,CAAC;GACH,CAAC;GAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,mCAAmC,SAAS,QAAQ;GAGtE,OAAQ,MAAM,SAAS,KAAK;EAC9B,SAAS,OAAO;GACd,MAAM,IAAI,6BAA6B,wBAAwB,KAAK;EACtE;EAEA,IAAI,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,WAAW,GACxE,MAAM,IAAI,6BAA6B,sBAAsB;EAG/D,OAAO,KAAK;CACd;CAEA,MAAc,UAAU,aAA2C;EACjE,IAAI;GACF,MAAM,WAAW,MAAM,MAAM,eAAe,EAC1C,SAAS,EAAE,eAAe,UAAU,cAAc,EACpD,CAAC;GAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,kCAAkC,SAAS,QAAQ;GAGrE,OAAQ,MAAM,SAAS,KAAK;EAC9B,SAAS,OAAO;GACd,MAAM,IAAI,6BAA6B,wBAAwB,KAAK;EACtE;CACF;AACF"}
@@ -0,0 +1,36 @@
1
+ import { AuthProvider, FacebookProviderConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile } from "../contracts/providers.mjs";
2
+
3
+ //#region ../auth/src/providers/facebook-provider.d.ts
4
+ /**
5
+ * Facebook sign-in: plain OAuth 2 authorization code flow with PKCE (S256).
6
+ * Facebook is not OpenID Connect — there is no `id_token`. Unlike every other
7
+ * built-in provider, Facebook's token endpoint is redeemed with a `GET`
8
+ * request (query-string parameters, not a POST body) — that shape comes
9
+ * straight from Facebook's Graph API docs, not from a live exchange (this
10
+ * package has no Facebook test app). The profile is then read once from
11
+ * `Graph /me?fields=id,name,email,picture`, over a direct HTTPS call; no
12
+ * Facebook SDK is involved.
13
+ *
14
+ * Facebook only ever returns a confirmed address for `email` — there is no
15
+ * separate verified flag — so an email present on the profile is treated as
16
+ * verified. An app without `email` permission granted, or one that simply has
17
+ * no email on file, gets `email: undefined`, which `resolveProviderUser`
18
+ * already rejects as an unverified/missing email, the same path GitHub's
19
+ * unverified addresses take.
20
+ */
21
+ declare class FacebookProvider implements AuthProvider {
22
+ private readonly settings;
23
+ readonly name = "facebook";
24
+ constructor(settings: FacebookProviderConfig);
25
+ authorizationUrl(state: ProviderAuthorizationState): string;
26
+ handleCallback({
27
+ query,
28
+ expected
29
+ }: ProviderCallbackParams): Promise<ProviderProfile>;
30
+ /** Redeem the authorization code (with the PKCE verifier) for an access token. */
31
+ private exchangeCode;
32
+ private fetchUser;
33
+ }
34
+ //#endregion
35
+ export { FacebookProvider };
36
+ //# sourceMappingURL=facebook-provider.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"facebook-provider.d.mts","names":[],"sources":["../../../../../../../auth/src/providers/facebook-provider.ts"],"mappings":";;;;;AAuCA;;;;;;;;;;;;;;;cAAa,gBAAA,YAA4B,YAAA;EAAA,iBAGH,QAAA;EAAA,SAFpB,IAAA;cAEoB,QAAA,EAAU,sBAAA;EAEvC,gBAAA,CAAiB,KAAA,EAAO,0BAAA;EAclB,cAAA;IACX,KAAA;IACA;EAAA,GACC,sBAAA,GAAyB,OAAA,CAAQ,eAAA;EAjBZ;EAAA,QA4CV,YAAA;EAAA,QAkCA,SAAA;AAAA"}
@@ -0,0 +1,94 @@
1
+ import { InvalidProviderCallbackError } from "../errors/invalid-provider-callback.error.mjs";
2
+ import { pkceChallenge } from "./google-provider.mjs";
3
+
4
+ //#region ../auth/src/providers/facebook-provider.ts
5
+ const AUTHORIZATION_ENDPOINT = "https://www.facebook.com/v19.0/dialog/oauth";
6
+ const TOKEN_ENDPOINT = "https://graph.facebook.com/v19.0/oauth/access_token";
7
+ const USER_ENDPOINT = "https://graph.facebook.com/v19.0/me";
8
+ const DEFAULT_SCOPES = ["email", "public_profile"];
9
+ /**
10
+ * Facebook sign-in: plain OAuth 2 authorization code flow with PKCE (S256).
11
+ * Facebook is not OpenID Connect — there is no `id_token`. Unlike every other
12
+ * built-in provider, Facebook's token endpoint is redeemed with a `GET`
13
+ * request (query-string parameters, not a POST body) — that shape comes
14
+ * straight from Facebook's Graph API docs, not from a live exchange (this
15
+ * package has no Facebook test app). The profile is then read once from
16
+ * `Graph /me?fields=id,name,email,picture`, over a direct HTTPS call; no
17
+ * Facebook SDK is involved.
18
+ *
19
+ * Facebook only ever returns a confirmed address for `email` — there is no
20
+ * separate verified flag — so an email present on the profile is treated as
21
+ * verified. An app without `email` permission granted, or one that simply has
22
+ * no email on file, gets `email: undefined`, which `resolveProviderUser`
23
+ * already rejects as an unverified/missing email, the same path GitHub's
24
+ * unverified addresses take.
25
+ */
26
+ var FacebookProvider = class {
27
+ constructor(settings) {
28
+ this.settings = settings;
29
+ this.name = "facebook";
30
+ }
31
+ authorizationUrl(state) {
32
+ const url = new URL(AUTHORIZATION_ENDPOINT);
33
+ url.searchParams.set("client_id", this.settings.clientId);
34
+ url.searchParams.set("redirect_uri", this.settings.redirectUri);
35
+ url.searchParams.set("response_type", "code");
36
+ url.searchParams.set("scope", (this.settings.scopes ?? DEFAULT_SCOPES).join(" "));
37
+ url.searchParams.set("state", state.state);
38
+ url.searchParams.set("code_challenge", pkceChallenge(state.codeVerifier));
39
+ url.searchParams.set("code_challenge_method", "S256");
40
+ return url.toString();
41
+ }
42
+ async handleCallback({ query, expected }) {
43
+ const code = query.code;
44
+ if (typeof code !== "string" || code.length === 0) throw new InvalidProviderCallbackError("missing-code");
45
+ const accessToken = await this.exchangeCode(code, expected.codeVerifier);
46
+ const user = await this.fetchUser(accessToken);
47
+ if (typeof user.id !== "string" || user.id.length === 0) throw new InvalidProviderCallbackError("missing-subject");
48
+ return {
49
+ provider: this.name,
50
+ providerUserId: user.id,
51
+ email: user.email,
52
+ emailVerified: typeof user.email === "string" && user.email.length > 0,
53
+ name: user.name,
54
+ avatar: user.picture?.data?.url,
55
+ raw: user
56
+ };
57
+ }
58
+ /** Redeem the authorization code (with the PKCE verifier) for an access token. */
59
+ async exchangeCode(code, codeVerifier) {
60
+ let body;
61
+ try {
62
+ const url = new URL(TOKEN_ENDPOINT);
63
+ url.searchParams.set("client_id", this.settings.clientId);
64
+ url.searchParams.set("client_secret", this.settings.clientSecret);
65
+ url.searchParams.set("redirect_uri", this.settings.redirectUri);
66
+ url.searchParams.set("code", code);
67
+ url.searchParams.set("code_verifier", codeVerifier);
68
+ const response = await fetch(url.toString(), { headers: { accept: "application/json" } });
69
+ if (!response.ok) throw new Error(`Facebook token endpoint answered ${response.status}`);
70
+ body = await response.json();
71
+ } catch (error) {
72
+ throw new InvalidProviderCallbackError("code-exchange-failed", error);
73
+ }
74
+ if (body.error) throw new InvalidProviderCallbackError("code-exchange-failed", body.error);
75
+ if (typeof body.access_token !== "string" || body.access_token.length === 0) throw new InvalidProviderCallbackError("missing-access-token");
76
+ return body.access_token;
77
+ }
78
+ async fetchUser(accessToken) {
79
+ try {
80
+ const url = new URL(USER_ENDPOINT);
81
+ url.searchParams.set("fields", "id,name,email,picture");
82
+ url.searchParams.set("access_token", accessToken);
83
+ const response = await fetch(url.toString(), { headers: { accept: "application/json" } });
84
+ if (!response.ok) throw new Error(`Facebook /me endpoint answered ${response.status}`);
85
+ return await response.json();
86
+ } catch (error) {
87
+ throw new InvalidProviderCallbackError("profile-fetch-failed", error);
88
+ }
89
+ }
90
+ };
91
+
92
+ //#endregion
93
+ export { FacebookProvider };
94
+ //# sourceMappingURL=facebook-provider.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"facebook-provider.mjs","names":[],"sources":["../../../../../../../auth/src/providers/facebook-provider.ts"],"sourcesContent":["import type {\n AuthProvider,\n FacebookProviderConfig,\n ProviderAuthorizationState,\n ProviderCallbackParams,\n ProviderProfile,\n} from \"../contracts/providers\";\nimport { InvalidProviderCallbackError } from \"../errors/invalid-provider-callback.error\";\nimport { pkceChallenge } from \"./google-provider\";\n\nconst AUTHORIZATION_ENDPOINT = \"https://www.facebook.com/v19.0/dialog/oauth\";\nconst TOKEN_ENDPOINT = \"https://graph.facebook.com/v19.0/oauth/access_token\";\nconst USER_ENDPOINT = \"https://graph.facebook.com/v19.0/me\";\nconst DEFAULT_SCOPES = [\"email\", \"public_profile\"];\n\ntype FacebookUser = {\n id: string;\n name?: string;\n email?: string;\n picture?: { data?: { url?: string } };\n};\n\n/**\n * Facebook sign-in: plain OAuth 2 authorization code flow with PKCE (S256).\n * Facebook is not OpenID Connect — there is no `id_token`. Unlike every other\n * built-in provider, Facebook's token endpoint is redeemed with a `GET`\n * request (query-string parameters, not a POST body) — that shape comes\n * straight from Facebook's Graph API docs, not from a live exchange (this\n * package has no Facebook test app). The profile is then read once from\n * `Graph /me?fields=id,name,email,picture`, over a direct HTTPS call; no\n * Facebook SDK is involved.\n *\n * Facebook only ever returns a confirmed address for `email` — there is no\n * separate verified flag — so an email present on the profile is treated as\n * verified. An app without `email` permission granted, or one that simply has\n * no email on file, gets `email: undefined`, which `resolveProviderUser`\n * already rejects as an unverified/missing email, the same path GitHub's\n * unverified addresses take.\n */\nexport class FacebookProvider implements AuthProvider {\n public readonly name = \"facebook\";\n\n public constructor(private readonly settings: FacebookProviderConfig) {}\n\n public authorizationUrl(state: ProviderAuthorizationState): string {\n const url = new URL(AUTHORIZATION_ENDPOINT);\n\n url.searchParams.set(\"client_id\", this.settings.clientId);\n url.searchParams.set(\"redirect_uri\", this.settings.redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", (this.settings.scopes ?? DEFAULT_SCOPES).join(\" \"));\n url.searchParams.set(\"state\", state.state);\n url.searchParams.set(\"code_challenge\", pkceChallenge(state.codeVerifier));\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n\n return url.toString();\n }\n\n public async handleCallback({\n query,\n expected,\n }: ProviderCallbackParams): Promise<ProviderProfile> {\n const code = query.code;\n\n if (typeof code !== \"string\" || code.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-code\");\n }\n\n const accessToken = await this.exchangeCode(code, expected.codeVerifier);\n const user = await this.fetchUser(accessToken);\n\n if (typeof user.id !== \"string\" || user.id.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-subject\");\n }\n\n return {\n provider: this.name,\n providerUserId: user.id,\n email: user.email,\n // Facebook does not report a verification flag; a returned address is confirmed.\n emailVerified: typeof user.email === \"string\" && user.email.length > 0,\n name: user.name,\n avatar: user.picture?.data?.url,\n raw: user,\n };\n }\n\n /** Redeem the authorization code (with the PKCE verifier) for an access token. */\n private async exchangeCode(code: string, codeVerifier: string): Promise<string> {\n let body: { access_token?: unknown; error?: unknown };\n\n try {\n const url = new URL(TOKEN_ENDPOINT);\n\n url.searchParams.set(\"client_id\", this.settings.clientId);\n url.searchParams.set(\"client_secret\", this.settings.clientSecret);\n url.searchParams.set(\"redirect_uri\", this.settings.redirectUri);\n url.searchParams.set(\"code\", code);\n url.searchParams.set(\"code_verifier\", codeVerifier);\n\n const response = await fetch(url.toString(), { headers: { accept: \"application/json\" } });\n\n if (!response.ok) {\n throw new Error(`Facebook token endpoint answered ${response.status}`);\n }\n\n body = (await response.json()) as { access_token?: unknown; error?: unknown };\n } catch (error) {\n throw new InvalidProviderCallbackError(\"code-exchange-failed\", error);\n }\n\n if (body.error) {\n throw new InvalidProviderCallbackError(\"code-exchange-failed\", body.error);\n }\n\n if (typeof body.access_token !== \"string\" || body.access_token.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-access-token\");\n }\n\n return body.access_token;\n }\n\n private async fetchUser(accessToken: string): Promise<FacebookUser> {\n try {\n const url = new URL(USER_ENDPOINT);\n\n url.searchParams.set(\"fields\", \"id,name,email,picture\");\n url.searchParams.set(\"access_token\", accessToken);\n\n const response = await fetch(url.toString(), { headers: { accept: \"application/json\" } });\n\n if (!response.ok) {\n throw new Error(`Facebook /me endpoint answered ${response.status}`);\n }\n\n return (await response.json()) as FacebookUser;\n } catch (error) {\n throw new InvalidProviderCallbackError(\"profile-fetch-failed\", error);\n }\n }\n}\n"],"mappings":";;;;AAUA,MAAM,yBAAyB;AAC/B,MAAM,iBAAiB;AACvB,MAAM,gBAAgB;AACtB,MAAM,iBAAiB,CAAC,SAAS,gBAAgB;;;;;;;;;;;;;;;;;;AA0BjD,IAAa,mBAAb,MAAsD;CAGpD,AAAO,YAAY,AAAiB,UAAkC;EAAlC;cAFb;CAEgD;CAEvE,AAAO,iBAAiB,OAA2C;EACjE,MAAM,MAAM,IAAI,IAAI,sBAAsB;EAE1C,IAAI,aAAa,IAAI,aAAa,KAAK,SAAS,QAAQ;EACxD,IAAI,aAAa,IAAI,gBAAgB,KAAK,SAAS,WAAW;EAC9D,IAAI,aAAa,IAAI,iBAAiB,MAAM;EAC5C,IAAI,aAAa,IAAI,UAAU,KAAK,SAAS,UAAU,eAAc,CAAE,KAAK,GAAG,CAAC;EAChF,IAAI,aAAa,IAAI,SAAS,MAAM,KAAK;EACzC,IAAI,aAAa,IAAI,kBAAkB,cAAc,MAAM,YAAY,CAAC;EACxE,IAAI,aAAa,IAAI,yBAAyB,MAAM;EAEpD,OAAO,IAAI,SAAS;CACtB;CAEA,MAAa,eAAe,EAC1B,OACA,YACmD;EACnD,MAAM,OAAO,MAAM;EAEnB,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAC9C,MAAM,IAAI,6BAA6B,cAAc;EAGvD,MAAM,cAAc,MAAM,KAAK,aAAa,MAAM,SAAS,YAAY;EACvE,MAAM,OAAO,MAAM,KAAK,UAAU,WAAW;EAE7C,IAAI,OAAO,KAAK,OAAO,YAAY,KAAK,GAAG,WAAW,GACpD,MAAM,IAAI,6BAA6B,iBAAiB;EAG1D,OAAO;GACL,UAAU,KAAK;GACf,gBAAgB,KAAK;GACrB,OAAO,KAAK;GAEZ,eAAe,OAAO,KAAK,UAAU,YAAY,KAAK,MAAM,SAAS;GACrE,MAAM,KAAK;GACX,QAAQ,KAAK,SAAS,MAAM;GAC5B,KAAK;EACP;CACF;;CAGA,MAAc,aAAa,MAAc,cAAuC;EAC9E,IAAI;EAEJ,IAAI;GACF,MAAM,MAAM,IAAI,IAAI,cAAc;GAElC,IAAI,aAAa,IAAI,aAAa,KAAK,SAAS,QAAQ;GACxD,IAAI,aAAa,IAAI,iBAAiB,KAAK,SAAS,YAAY;GAChE,IAAI,aAAa,IAAI,gBAAgB,KAAK,SAAS,WAAW;GAC9D,IAAI,aAAa,IAAI,QAAQ,IAAI;GACjC,IAAI,aAAa,IAAI,iBAAiB,YAAY;GAElD,MAAM,WAAW,MAAM,MAAM,IAAI,SAAS,GAAG,EAAE,SAAS,EAAE,QAAQ,mBAAmB,EAAE,CAAC;GAExF,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,oCAAoC,SAAS,QAAQ;GAGvE,OAAQ,MAAM,SAAS,KAAK;EAC9B,SAAS,OAAO;GACd,MAAM,IAAI,6BAA6B,wBAAwB,KAAK;EACtE;EAEA,IAAI,KAAK,OACP,MAAM,IAAI,6BAA6B,wBAAwB,KAAK,KAAK;EAG3E,IAAI,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,WAAW,GACxE,MAAM,IAAI,6BAA6B,sBAAsB;EAG/D,OAAO,KAAK;CACd;CAEA,MAAc,UAAU,aAA4C;EAClE,IAAI;GACF,MAAM,MAAM,IAAI,IAAI,aAAa;GAEjC,IAAI,aAAa,IAAI,UAAU,uBAAuB;GACtD,IAAI,aAAa,IAAI,gBAAgB,WAAW;GAEhD,MAAM,WAAW,MAAM,MAAM,IAAI,SAAS,GAAG,EAAE,SAAS,EAAE,QAAQ,mBAAmB,EAAE,CAAC;GAExF,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,kCAAkC,SAAS,QAAQ;GAGrE,OAAQ,MAAM,SAAS,KAAK;EAC9B,SAAS,OAAO;GACd,MAAM,IAAI,6BAA6B,wBAAwB,KAAK;EACtE;CACF;AACF"}
@@ -0,0 +1,31 @@
1
+ import { AuthProvider, GitHubProviderConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile } from "../contracts/providers.mjs";
2
+
3
+ //#region ../auth/src/providers/github-provider.d.ts
4
+ /**
5
+ * GitHub sign-in: plain OAuth 2 authorization code flow with PKCE (S256).
6
+ * GitHub is not OpenID Connect — there is no `id_token` and nothing to verify
7
+ * against a JWKS. The access token is redeemed once for the profile, over a
8
+ * direct HTTPS call to GitHub's API; no vendor SDK is involved.
9
+ *
10
+ * The account email is fetched separately from `/user/emails`, because
11
+ * `/user.email` is `null` unless the user made it public — the emails
12
+ * endpoint is also where GitHub reports whether an address is verified.
13
+ */
14
+ declare class GitHubProvider implements AuthProvider {
15
+ private readonly settings;
16
+ readonly name = "github";
17
+ constructor(settings: GitHubProviderConfig);
18
+ authorizationUrl(state: ProviderAuthorizationState): string;
19
+ handleCallback({
20
+ query,
21
+ expected
22
+ }: ProviderCallbackParams): Promise<ProviderProfile>;
23
+ /** Redeem the authorization code (with the PKCE verifier) for an access token. */
24
+ private exchangeCode;
25
+ private fetchUser;
26
+ /** The primary, verified email — `undefined` when none is both primary and verified. */
27
+ private fetchPrimaryEmail;
28
+ }
29
+ //#endregion
30
+ export { GitHubProvider };
31
+ //# sourceMappingURL=github-provider.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-provider.d.mts","names":[],"sources":["../../../../../../../auth/src/providers/github-provider.ts"],"mappings":";;;;;AA0CA;;;;;;;;cAAa,cAAA,YAA0B,YAAA;EAAA,iBAGD,QAAA;EAAA,SAFpB,IAAA;cAEoB,QAAA,EAAU,oBAAA;EAEvC,gBAAA,CAAiB,KAAA,EAAO,0BAAA;EAclB,cAAA;IACX,KAAA;IACA;EAAA,GACC,sBAAA,GAAyB,OAAA,CAAQ,eAAA;EAnBA;EAAA,QA8CtB,YAAA;EAAA,QAwCA,SAAA;EAtFgC;EAAA,QA2GhC,iBAAA;AAAA"}
@@ -0,0 +1,115 @@
1
+ import { InvalidProviderCallbackError } from "../errors/invalid-provider-callback.error.mjs";
2
+ import { pkceChallenge } from "./google-provider.mjs";
3
+
4
+ //#region ../auth/src/providers/github-provider.ts
5
+ const AUTHORIZATION_ENDPOINT = "https://github.com/login/oauth/authorize";
6
+ const TOKEN_ENDPOINT = "https://github.com/login/oauth/access_token";
7
+ const USER_ENDPOINT = "https://api.github.com/user";
8
+ const EMAILS_ENDPOINT = "https://api.github.com/user/emails";
9
+ const DEFAULT_SCOPES = ["read:user", "user:email"];
10
+ /** GitHub requires a `User-Agent`; identify the framework, not the app. */
11
+ const USER_AGENT = "warlock.js-auth";
12
+ /**
13
+ * GitHub sign-in: plain OAuth 2 authorization code flow with PKCE (S256).
14
+ * GitHub is not OpenID Connect — there is no `id_token` and nothing to verify
15
+ * against a JWKS. The access token is redeemed once for the profile, over a
16
+ * direct HTTPS call to GitHub's API; no vendor SDK is involved.
17
+ *
18
+ * The account email is fetched separately from `/user/emails`, because
19
+ * `/user.email` is `null` unless the user made it public — the emails
20
+ * endpoint is also where GitHub reports whether an address is verified.
21
+ */
22
+ var GitHubProvider = class {
23
+ constructor(settings) {
24
+ this.settings = settings;
25
+ this.name = "github";
26
+ }
27
+ authorizationUrl(state) {
28
+ const url = new URL(AUTHORIZATION_ENDPOINT);
29
+ url.searchParams.set("client_id", this.settings.clientId);
30
+ url.searchParams.set("redirect_uri", this.settings.redirectUri);
31
+ url.searchParams.set("response_type", "code");
32
+ url.searchParams.set("scope", (this.settings.scopes ?? DEFAULT_SCOPES).join(" "));
33
+ url.searchParams.set("state", state.state);
34
+ url.searchParams.set("code_challenge", pkceChallenge(state.codeVerifier));
35
+ url.searchParams.set("code_challenge_method", "S256");
36
+ return url.toString();
37
+ }
38
+ async handleCallback({ query, expected }) {
39
+ const code = query.code;
40
+ if (typeof code !== "string" || code.length === 0) throw new InvalidProviderCallbackError("missing-code");
41
+ const accessToken = await this.exchangeCode(code, expected.codeVerifier);
42
+ const user = await this.fetchUser(accessToken);
43
+ const email = await this.fetchPrimaryEmail(accessToken);
44
+ if (typeof user.id !== "number") throw new InvalidProviderCallbackError("missing-subject");
45
+ return {
46
+ provider: this.name,
47
+ providerUserId: String(user.id),
48
+ email: email?.email,
49
+ emailVerified: email?.verified === true,
50
+ name: user.name ?? user.login,
51
+ avatar: user.avatar_url,
52
+ raw: user
53
+ };
54
+ }
55
+ /** Redeem the authorization code (with the PKCE verifier) for an access token. */
56
+ async exchangeCode(code, codeVerifier) {
57
+ let body;
58
+ try {
59
+ const response = await fetch(TOKEN_ENDPOINT, {
60
+ method: "POST",
61
+ headers: {
62
+ "content-type": "application/x-www-form-urlencoded",
63
+ accept: "application/json"
64
+ },
65
+ body: new URLSearchParams({
66
+ code,
67
+ client_id: this.settings.clientId,
68
+ client_secret: this.settings.clientSecret,
69
+ redirect_uri: this.settings.redirectUri,
70
+ code_verifier: codeVerifier
71
+ })
72
+ });
73
+ if (!response.ok) throw new Error(`GitHub token endpoint answered ${response.status}`);
74
+ body = await response.json();
75
+ } catch (error) {
76
+ throw new InvalidProviderCallbackError("code-exchange-failed", error);
77
+ }
78
+ if (body.error) throw new InvalidProviderCallbackError("code-exchange-failed", body.error);
79
+ if (typeof body.access_token !== "string" || body.access_token.length === 0) throw new InvalidProviderCallbackError("missing-access-token");
80
+ return body.access_token;
81
+ }
82
+ async fetchUser(accessToken) {
83
+ try {
84
+ const response = await fetch(USER_ENDPOINT, { headers: {
85
+ authorization: `Bearer ${accessToken}`,
86
+ accept: "application/vnd.github+json",
87
+ "user-agent": USER_AGENT
88
+ } });
89
+ if (!response.ok) throw new Error(`GitHub user endpoint answered ${response.status}`);
90
+ return await response.json();
91
+ } catch (error) {
92
+ throw new InvalidProviderCallbackError("profile-fetch-failed", error);
93
+ }
94
+ }
95
+ /** The primary, verified email — `undefined` when none is both primary and verified. */
96
+ async fetchPrimaryEmail(accessToken) {
97
+ let emails;
98
+ try {
99
+ const response = await fetch(EMAILS_ENDPOINT, { headers: {
100
+ authorization: `Bearer ${accessToken}`,
101
+ accept: "application/vnd.github+json",
102
+ "user-agent": USER_AGENT
103
+ } });
104
+ if (!response.ok) throw new Error(`GitHub emails endpoint answered ${response.status}`);
105
+ emails = await response.json();
106
+ } catch (error) {
107
+ throw new InvalidProviderCallbackError("profile-fetch-failed", error);
108
+ }
109
+ return emails.find((email) => email.primary && email.verified);
110
+ }
111
+ };
112
+
113
+ //#endregion
114
+ export { GitHubProvider };
115
+ //# sourceMappingURL=github-provider.mjs.map