@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.
package/dist/index.js ADDED
@@ -0,0 +1,721 @@
1
+ import { Effect as e } from "effect";
2
+ import { definePlugin as t } from "@voltro/protocol";
3
+ import { buildSetCookie as n, readCookie as r, timingSafeStringEqual as i } from "@voltro/protocol/session";
4
+ import { createHash as a, createPrivateKey as o, randomBytes as s, sign as c } from "node:crypto";
5
+ import { JwtVerifyError as l, verifyJwt as u } from "@voltro/protocol/jwt";
6
+ import { issueUserSession as d, newAuthId as f, runSubjectGuards as p } from "@voltro/plugin-auth";
7
+ import { and as m, boolean as h, eq as g, generateId as _, id as v, table as y, text as b, timestamp as x } from "@voltro/database";
8
+ //#region src/pkce.ts
9
+ var S = () => s(32).toString("base64url"), ee = (e) => a("sha256").update(e, "ascii").digest().toString("base64url"), C = class extends Error {
10
+ status;
11
+ code;
12
+ constructor(e, t, n) {
13
+ super(n), this.status = e, this.code = t, this.name = "SocialAuthError";
14
+ }
15
+ }, w = class extends C {
16
+ constructor(e) {
17
+ super(400, "state_mismatch", e), this.name = "SocialStateMismatchError";
18
+ }
19
+ }, T = class extends C {
20
+ constructor(e, t, n = 502) {
21
+ super(n, e, t), this.name = "SocialExchangeError";
22
+ }
23
+ }, E = class extends C {
24
+ constructor(e, t) {
25
+ super(401, e, t), this.name = "SocialIdTokenError";
26
+ }
27
+ }, D = class extends C {
28
+ constructor(e) {
29
+ super(500, "config", e), this.name = "SocialConfigError";
30
+ }
31
+ }, O = class extends C {
32
+ constructor(e, t, n = 409) {
33
+ super(n, e, t), this.name = "SocialAccountLinkError";
34
+ }
35
+ }, te = {
36
+ id: "google",
37
+ label: "Google",
38
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
39
+ tokenUrl: "https://oauth2.googleapis.com/token",
40
+ jwksUrl: "https://www.googleapis.com/oauth2/v3/certs",
41
+ issuer: "https://accounts.google.com",
42
+ apiBase: null,
43
+ defaultScopes: [
44
+ "openid",
45
+ "email",
46
+ "profile"
47
+ ],
48
+ kind: "oidc",
49
+ responseMode: "query",
50
+ assertsEmailVerification: !0,
51
+ pkceEnforced: !0
52
+ }, ne = {
53
+ id: "github",
54
+ label: "GitHub",
55
+ authorizeUrl: "https://github.com/login/oauth/authorize",
56
+ tokenUrl: "https://github.com/login/oauth/access_token",
57
+ jwksUrl: null,
58
+ issuer: null,
59
+ apiBase: "https://api.github.com",
60
+ defaultScopes: ["read:user", "user:email"],
61
+ kind: "oauth2",
62
+ responseMode: "query",
63
+ assertsEmailVerification: !0,
64
+ pkceEnforced: !1
65
+ }, k = {
66
+ id: "apple",
67
+ label: "Apple",
68
+ authorizeUrl: "https://appleid.apple.com/auth/authorize",
69
+ tokenUrl: "https://appleid.apple.com/auth/token",
70
+ jwksUrl: "https://appleid.apple.com/auth/keys",
71
+ issuer: "https://appleid.apple.com",
72
+ apiBase: null,
73
+ defaultScopes: ["name", "email"],
74
+ kind: "oidc",
75
+ responseMode: "form_post",
76
+ assertsEmailVerification: !0,
77
+ pkceEnforced: !0
78
+ }, re = {
79
+ google: te,
80
+ github: ne,
81
+ apple: k
82
+ }, ie = [
83
+ te,
84
+ ne,
85
+ k
86
+ ], A = [
87
+ "google",
88
+ "github",
89
+ "apple"
90
+ ], j = (e) => e === "google" || e === "github" || e === "apple", M = (e, t = {}) => {
91
+ if (!j(e)) throw new D(`unknown social provider '${e}' — expected one of ${A.join(", ")}`);
92
+ return {
93
+ ...re[e],
94
+ ...t.authorizeUrl === void 0 ? {} : { authorizeUrl: t.authorizeUrl },
95
+ ...t.tokenUrl === void 0 ? {} : { tokenUrl: t.tokenUrl },
96
+ ...t.jwksUrl === void 0 ? {} : { jwksUrl: t.jwksUrl },
97
+ ...t.issuer === void 0 ? {} : { issuer: t.issuer },
98
+ ...t.apiBase === void 0 ? {} : { apiBase: t.apiBase }
99
+ };
100
+ }, N = (e) => {
101
+ if (!e.clientId) throw new D("socialBeginLogin: `clientId` is required");
102
+ if (!e.redirectUri) throw new D("socialBeginLogin: `redirectUri` is required");
103
+ let t = M(e.provider, e.endpoints ?? {}), n = e.randomness?.state ?? S(), r = e.randomness?.codeVerifier ?? S(), i = t.kind === "oidc" ? e.randomness?.nonce ?? S() : "", a = e.scopes ?? t.defaultScopes, o = new URLSearchParams({
104
+ client_id: e.clientId,
105
+ redirect_uri: e.redirectUri,
106
+ response_type: "code",
107
+ scope: a.join(" "),
108
+ state: n,
109
+ code_challenge: ee(r),
110
+ code_challenge_method: "S256"
111
+ });
112
+ return i && o.set("nonce", i), t.responseMode === "form_post" && o.set("response_mode", "form_post"), e.prompt && o.set("prompt", e.prompt), e.loginHint && o.set(t.id === "github" ? "login" : "login_hint", e.loginHint), {
113
+ provider: t.id,
114
+ url: `${t.authorizeUrl}?${o.toString()}`,
115
+ state: n,
116
+ codeVerifier: r,
117
+ nonce: i,
118
+ responseMode: t.responseMode
119
+ };
120
+ }, P = 15777e3, ae = 900, oe = "https://appleid.apple.com", F = (e) => (typeof e == "string" ? Buffer.from(e, "utf8") : e).toString("base64url"), I = /* @__PURE__ */ new Map(), se = (e) => {
121
+ let t = I.get(e);
122
+ if (t) return t;
123
+ let n;
124
+ try {
125
+ n = o({ key: e });
126
+ } catch (e) {
127
+ throw new D(`apple: could not parse the .p8 private key — ${e.message}`);
128
+ }
129
+ if (n.asymmetricKeyType !== "ec") throw new D(`apple: the .p8 key must be an EC (P-256) key, got '${String(n.asymmetricKeyType)}'`);
130
+ let r = n.asymmetricKeyDetails?.namedCurve;
131
+ if (r !== void 0 && r !== "prime256v1") throw new D(`apple: ES256 requires the P-256 curve (prime256v1), got '${r}'`);
132
+ return I.set(e, n), n;
133
+ }, L = /* @__PURE__ */ new Map(), ce = () => {
134
+ I.clear(), L.clear();
135
+ }, R = (e, t, n = Date.now) => {
136
+ if (!e) throw new D("apple: `clientId` (Services ID) is required");
137
+ if (!t.teamId) throw new D("apple: `teamId` is required");
138
+ if (!t.keyId) throw new D("apple: `keyId` is required");
139
+ if (!t.privateKey) throw new D("apple: `privateKey` (.p8 contents) is required");
140
+ let r = t.ttlSeconds ?? 900;
141
+ if (!Number.isFinite(r) || r <= 0) throw new D("apple: `ttlSeconds` must be a positive number");
142
+ if (r > 15777e3) throw new D(`apple: \`ttlSeconds\` may not exceed ${P} (six months, Apple's cap) — got ${r}`);
143
+ let i = n(), a = `${t.teamId}\u0000${t.keyId}\u0000${e}\u0000${r}`, o = L.get(a);
144
+ if (o && o.expiresAtMs - i > 6e4) return o.secret;
145
+ let s = Math.floor(i / 1e3), l = s + r, u = `${F(JSON.stringify({
146
+ alg: "ES256",
147
+ kid: t.keyId,
148
+ typ: "JWT"
149
+ }))}.${F(JSON.stringify({
150
+ iss: t.teamId,
151
+ iat: s,
152
+ exp: l,
153
+ aud: oe,
154
+ sub: e
155
+ }))}`, d = `${u}.${c("sha256", Buffer.from(u, "ascii"), {
156
+ key: se(t.privateKey),
157
+ dsaEncoding: "ieee-p1363"
158
+ }).toString("base64url")}`;
159
+ return L.set(a, {
160
+ secret: d,
161
+ expiresAtMs: l * 1e3
162
+ }), d;
163
+ }, z = (e) => {
164
+ if (!e) return null;
165
+ let t;
166
+ try {
167
+ t = JSON.parse(e);
168
+ } catch {
169
+ return null;
170
+ }
171
+ if (typeof t != "object" || !t) return null;
172
+ let n = t, r = typeof n.name == "object" && n.name !== null ? n.name : {}, i = (e) => typeof e == "string" && e.length > 0 ? e : null, a = {
173
+ firstName: i(r.firstName),
174
+ lastName: i(r.lastName),
175
+ email: i(n.email)
176
+ };
177
+ return a.firstName === null && a.lastName === null && a.email === null ? null : a;
178
+ }, B = (e) => e !== null && e.toLowerCase().endsWith("@privaterelay.appleid.com"), V = (e) => ({
179
+ authorization: `Bearer ${e}`,
180
+ accept: "application/vnd.github+json",
181
+ "x-github-api-version": "2022-11-28",
182
+ "user-agent": "voltro-plugin-auth-social"
183
+ }), H = async (e, t) => {
184
+ let n = t.fetchImpl ?? fetch, r = await n(`${t.apiBase}/user`, { headers: V(e) });
185
+ if (!r.ok) {
186
+ let e = await r.text().catch(() => "");
187
+ throw new T("exchange_failed", `github: GET /user failed (${r.status}): ${e.slice(0, 200)}`);
188
+ }
189
+ let i = await r.json();
190
+ if (i.id === void 0 || i.id === null || !i.login) throw new T("malformed_token_response", "github: GET /user response is missing id/login");
191
+ let a = null, o = !1, s = await n(`${t.apiBase}/user/emails`, { headers: V(e) });
192
+ if (s.ok) {
193
+ let e = await s.json();
194
+ if (Array.isArray(e)) {
195
+ let t = e.find((e) => e.primary === !0 && typeof e.email == "string");
196
+ t?.email && (a = t.email, o = t.verified === !0);
197
+ }
198
+ }
199
+ return {
200
+ accountId: String(i.id),
201
+ login: i.login,
202
+ name: typeof i.name == "string" && i.name.length > 0 ? i.name : null,
203
+ avatarUrl: typeof i.avatar_url == "string" && i.avatar_url.length > 0 ? i.avatar_url : null,
204
+ email: a,
205
+ emailVerified: o
206
+ };
207
+ }, U = (e) => e === !0 || e === "true", W = (e) => typeof e == "string" && e.length > 0 ? e : null, G = async (e) => {
208
+ let t = M(e.provider, e.endpoints ?? {});
209
+ if (!e.code) throw new C(400, "missing_code", "socialCompleteLogin: missing `code`");
210
+ if (!e.expectedState) throw new w("socialCompleteLogin: no `expectedState` — the login was never started by this server, or the state cookie expired. Refusing to exchange the code.");
211
+ if (!e.state || !i(e.state, e.expectedState)) throw new w("socialCompleteLogin: `state` does not match the value minted for this browser (possible login CSRF).");
212
+ if (!e.codeVerifier) throw new C(400, "missing_verifier", "socialCompleteLogin: missing `codeVerifier` (PKCE)");
213
+ if (t.kind === "oidc" && !e.expectedNonce) throw new D(`socialCompleteLogin: ${t.id} returns an ID token, so \`expectedNonce\` is required — without it a token captured from another login replays here.`);
214
+ let n = t.id === "apple" ? R(e.clientId, e.appleKey ?? (() => {
215
+ throw new D("apple: `appleKey` (teamId + keyId + .p8) is required");
216
+ })(), e.now ?? Date.now) : e.clientSecret ?? (() => {
217
+ throw new D(`${t.id}: \`clientSecret\` is required`);
218
+ })(), r = e.fetchImpl ?? fetch, a = new URLSearchParams({
219
+ grant_type: "authorization_code",
220
+ client_id: e.clientId,
221
+ client_secret: n,
222
+ code: e.code,
223
+ redirect_uri: e.redirectUri,
224
+ code_verifier: e.codeVerifier
225
+ }), o = await r(t.tokenUrl, {
226
+ method: "POST",
227
+ headers: {
228
+ "content-type": "application/x-www-form-urlencoded",
229
+ accept: "application/json"
230
+ },
231
+ body: a.toString()
232
+ });
233
+ if (!o.ok) {
234
+ let e = await o.text().catch(() => "");
235
+ throw new T("exchange_failed", `${t.id}: token exchange failed (${o.status}): ${e.slice(0, 200)}`);
236
+ }
237
+ let s;
238
+ try {
239
+ s = await o.json();
240
+ } catch {
241
+ throw new T("malformed_token_response", `${t.id}: token endpoint returned a body that is not JSON`);
242
+ }
243
+ if (s.error) throw new T("exchange_failed", `${t.id}: token exchange refused: ${s.error}${s.error_description ? ` (${s.error_description})` : ""}`);
244
+ if (t.kind === "oauth2") {
245
+ if (!s.access_token) throw new T("malformed_token_response", `${t.id}: token response carries no access_token`);
246
+ if (!t.apiBase) throw new D(`${t.id}: no apiBase configured`);
247
+ let n = await H(s.access_token, {
248
+ apiBase: t.apiBase,
249
+ ...e.fetchImpl === void 0 ? {} : { fetchImpl: e.fetchImpl }
250
+ });
251
+ return {
252
+ provider: t.id,
253
+ providerAccountId: n.accountId,
254
+ email: n.email,
255
+ emailVerified: n.emailVerified,
256
+ emailIsPrivateRelay: !1,
257
+ name: n.name ?? n.login,
258
+ firstName: null,
259
+ lastName: null,
260
+ avatarUrl: n.avatarUrl,
261
+ nameIsFirstAuthorizationOnly: !1,
262
+ raw: { ...n }
263
+ };
264
+ }
265
+ if (!s.id_token) throw new T("malformed_token_response", `${t.id}: token response carries no id_token`);
266
+ if (!t.jwksUrl || !t.issuer) throw new D(`${t.id}: no JWKS/issuer configured`);
267
+ let c;
268
+ try {
269
+ c = await u(s.id_token, t.jwksUrl, {
270
+ issuer: t.issuer,
271
+ audience: e.clientId
272
+ });
273
+ } catch (e) {
274
+ throw e instanceof l ? new E("id_token_invalid", `${t.id}: id_token ${e.code}: ${e.message}`) : new E("id_token_invalid", `${t.id}: id_token verification failed: ${e.message}`);
275
+ }
276
+ let d = W(c.nonce);
277
+ if (!d || !i(d, e.expectedNonce ?? "")) throw new E("nonce_mismatch", `${t.id}: id_token nonce does not match the value minted for this login (possible replay).`);
278
+ let f = W(c.sub);
279
+ if (!f) throw new E("id_token_invalid", `${t.id}: id_token has no sub claim`);
280
+ let p = W(c.email), m = U(c.email_verified);
281
+ if (t.id === "apple") {
282
+ let t = z(e.appleUserPayload), n = U(c.is_private_email) || B(p), r = t?.firstName ?? null, i = t?.lastName ?? null, a = [r, i].filter((e) => e !== null).join(" ");
283
+ return {
284
+ provider: "apple",
285
+ providerAccountId: f,
286
+ email: p ?? t?.email ?? null,
287
+ emailVerified: m,
288
+ emailIsPrivateRelay: n,
289
+ name: a.length > 0 ? a : null,
290
+ firstName: r,
291
+ lastName: i,
292
+ avatarUrl: null,
293
+ nameIsFirstAuthorizationOnly: t !== null,
294
+ raw: c
295
+ };
296
+ }
297
+ return {
298
+ provider: t.id,
299
+ providerAccountId: f,
300
+ email: p,
301
+ emailVerified: m,
302
+ emailIsPrivateRelay: !1,
303
+ name: W(c.name),
304
+ firstName: W(c.given_name),
305
+ lastName: W(c.family_name),
306
+ avatarUrl: W(c.picture),
307
+ nameIsFirstAuthorizationOnly: !1,
308
+ raw: c
309
+ };
310
+ }, K = (e) => {
311
+ let { profile: t, linkedUserId: n, existingUserIdByEmail: r, policy: i } = e;
312
+ return n === null ? t.email === null ? {
313
+ kind: "refuse",
314
+ error: new O("no_email", `${t.provider} did not return an email address for this account. Add and verify an email with the provider, or sign in another way.`, 400)
315
+ } : r === null ? { kind: "create-user" } : i === "never" ? {
316
+ kind: "refuse",
317
+ error: new O("account_exists_unlinked", `An account already exists for ${t.email}. Sign in with your existing method, then connect ${t.provider} from your account settings.`)
318
+ } : t.emailVerified ? t.emailIsPrivateRelay ? {
319
+ kind: "refuse",
320
+ error: new O("private_relay_email", "This is an Apple private-relay address, which is unique to this app and cannot identify an existing account. Sign in with your existing method, then connect Apple from your account settings.")
321
+ } : {
322
+ kind: "link",
323
+ userId: r
324
+ } : {
325
+ kind: "refuse",
326
+ error: new O("email_not_verified", `${t.provider} did not verify ownership of ${t.email}, so it cannot be attached to the existing account with that address.`)
327
+ } : {
328
+ kind: "sign-in",
329
+ userId: n
330
+ };
331
+ }, q = (e) => ({
332
+ kind: "refused",
333
+ status: e.status,
334
+ code: e.code,
335
+ message: e.message
336
+ }), J = (t) => e.gen(function* () {
337
+ let { profile: n, identities: r, users: i, config: a } = t, o = t.policy ?? "never", s = yield* r.findByProviderAccount(n.provider, n.providerAccountId), c = n.email === null ? null : yield* i.findByEmail(n.email), l = K({
338
+ profile: n,
339
+ linkedUserId: s?.userId ?? null,
340
+ existingUserIdByEmail: c?.id ?? null,
341
+ policy: o
342
+ });
343
+ if (l.kind === "refuse") return q(l.error);
344
+ let u = null, m = !1, h = !1;
345
+ if (l.kind === "create-user") {
346
+ let t = n.email, r = yield* i.insert({
347
+ id: f("user"),
348
+ email: t,
349
+ passwordHash: null,
350
+ tenantId: a.defaultTenantId
351
+ }).pipe(e.catchTag("UserAlreadyExistsError", (t) => e.succeed(new O("account_exists_unlinked", `An account already exists for ${t.email}. Sign in with your existing method, then connect ${n.provider} from your account settings.`))));
352
+ if (r instanceof O) return q(r);
353
+ u = r, m = !0;
354
+ } else {
355
+ if (h = l.kind === "link", u = yield* i.findById(l.userId), !u) return q(new O("account_exists_unlinked", "The account this login is linked to no longer exists.", 409));
356
+ let e = a.subjectGuards ?? [];
357
+ if (e.length > 0) {
358
+ let t = yield* p(e, u);
359
+ if (!t.ok) return {
360
+ kind: "refused",
361
+ status: 403,
362
+ code: t.code,
363
+ message: t.message
364
+ };
365
+ }
366
+ }
367
+ s === null ? yield* r.insert({
368
+ userId: u.id,
369
+ provider: n.provider,
370
+ providerAccountId: n.providerAccountId,
371
+ email: n.email,
372
+ emailVerified: n.emailVerified
373
+ }) : yield* r.touch(s.id, /* @__PURE__ */ new Date());
374
+ let g = yield* d(u, i, a, {
375
+ ipAddress: t.ipAddress ?? null,
376
+ userAgent: t.userAgent ?? null
377
+ });
378
+ return {
379
+ kind: "ok",
380
+ userId: u.id,
381
+ subject: g.subject,
382
+ setCookie: g.setCookie,
383
+ created: m,
384
+ linked: h,
385
+ oneTimeName: n.nameIsFirstAuthorizationOnly ? {
386
+ firstName: n.firstName,
387
+ lastName: n.lastName
388
+ } : null
389
+ };
390
+ }), Y = "_voltro_oauth_identities", le = y(Y, {
391
+ id: v({ prefix: "oauthid" }),
392
+ userId: b(),
393
+ provider: b(),
394
+ providerAccountId: b(),
395
+ email: b().nullable(),
396
+ emailVerified: h().default(!1),
397
+ createdAt: x().default("now"),
398
+ lastLoginAt: x().nullable()
399
+ }).unique("byOauthProviderAccount", ["provider", "providerAccountId"]).index(["userId"]), ue = [le], de = () => _({
400
+ kind: "typeid",
401
+ prefix: "oauthid"
402
+ }, "oauthid"), fe = (t = []) => {
403
+ let n = [...t];
404
+ return {
405
+ findByProviderAccount: (t, r) => e.sync(() => n.find((e) => e.provider === t && e.providerAccountId === r) ?? null),
406
+ listForUser: (t) => e.sync(() => n.filter((e) => e.userId === t)),
407
+ insert: (t) => e.sync(() => {
408
+ let e = {
409
+ ...t,
410
+ id: de(),
411
+ createdAt: /* @__PURE__ */ new Date(),
412
+ lastLoginAt: null
413
+ };
414
+ return n.push(e), e;
415
+ }),
416
+ touch: (t, r) => e.sync(() => {
417
+ let e = n.findIndex((e) => e.id === t);
418
+ e >= 0 && (n[e] = {
419
+ ...n[e],
420
+ lastLoginAt: r
421
+ });
422
+ }),
423
+ remove: (t, r) => e.sync(() => {
424
+ let e = n.findIndex((e) => e.userId === t && e.provider === r);
425
+ return e < 0 ? !1 : (n.splice(e, 1), !0);
426
+ })
427
+ };
428
+ }, pe = (e) => {
429
+ if (e instanceof Date) return e;
430
+ if (typeof e == "string" || typeof e == "number") {
431
+ let t = new Date(e);
432
+ return Number.isNaN(t.getTime()) ? null : t;
433
+ }
434
+ return null;
435
+ }, X = (e) => ({
436
+ id: String(e.id ?? ""),
437
+ userId: String(e.userId ?? ""),
438
+ provider: String(e.provider ?? ""),
439
+ providerAccountId: String(e.providerAccountId ?? ""),
440
+ email: typeof e.email == "string" && e.email.length > 0 ? e.email : null,
441
+ emailVerified: e.emailVerified === !0 || e.emailVerified === 1,
442
+ createdAt: pe(e.createdAt) ?? /* @__PURE__ */ new Date(0),
443
+ lastLoginAt: pe(e.lastLoginAt)
444
+ }), me = (t) => ({
445
+ findByProviderAccount: (n, r) => e.promise(async () => {
446
+ let e = (await t.query({
447
+ table: Y,
448
+ predicate: m(g("provider", n), g("providerAccountId", r)),
449
+ limit: 1
450
+ }))[0];
451
+ return e ? X(e) : null;
452
+ }),
453
+ listForUser: (n) => e.promise(async () => (await t.query({
454
+ table: Y,
455
+ predicate: g("userId", n),
456
+ order: [{
457
+ column: "createdAt",
458
+ direction: "asc"
459
+ }]
460
+ })).map(X)),
461
+ insert: (n) => e.promise(async () => X(await t.insert(Y, {
462
+ id: de(),
463
+ userId: n.userId,
464
+ provider: n.provider,
465
+ providerAccountId: n.providerAccountId,
466
+ email: n.email,
467
+ emailVerified: n.emailVerified,
468
+ createdAt: /* @__PURE__ */ new Date(),
469
+ lastLoginAt: null
470
+ }))),
471
+ touch: (n, r) => e.promise(async () => {
472
+ await t.update(Y, n, { lastLoginAt: r });
473
+ }),
474
+ remove: (n, r) => e.promise(async () => {
475
+ let e = (await t.query({
476
+ table: Y,
477
+ predicate: m(g("userId", n), g("provider", r)),
478
+ limit: 1
479
+ }))[0];
480
+ return e ? t.delete(Y, String(e.id ?? "")) : !1;
481
+ })
482
+ }), he = (t, n, r) => e.gen(function* () {
483
+ let i = yield* t.findByProviderAccount(r.provider, r.providerAccountId);
484
+ return i ? i.userId === n ? i : yield* e.fail(/* @__PURE__ */ Error(`linkSocialIdentity: this ${r.provider} account is already linked to another user`)) : yield* t.insert({
485
+ ...r,
486
+ userId: n
487
+ });
488
+ }), ge = "/auth/social", _e = 600, ve = "voltro:oauth", Z = {
489
+ google: [{
490
+ name: "VOLTRO_GOOGLE_CLIENT_ID",
491
+ secret: !1,
492
+ description: "Google OAuth client id (…apps.googleusercontent.com)."
493
+ }, {
494
+ name: "VOLTRO_GOOGLE_CLIENT_SECRET",
495
+ secret: !0,
496
+ description: "Google OAuth client secret."
497
+ }],
498
+ github: [{
499
+ name: "VOLTRO_GITHUB_CLIENT_ID",
500
+ secret: !1,
501
+ description: "GitHub OAuth app client id."
502
+ }, {
503
+ name: "VOLTRO_GITHUB_CLIENT_SECRET",
504
+ secret: !0,
505
+ description: "GitHub OAuth app client secret."
506
+ }],
507
+ apple: [
508
+ {
509
+ name: "VOLTRO_APPLE_CLIENT_ID",
510
+ secret: !1,
511
+ description: "Apple Services ID, e.g. com.example.web."
512
+ },
513
+ {
514
+ name: "VOLTRO_APPLE_TEAM_ID",
515
+ secret: !1,
516
+ description: "Apple Developer Team ID (10 chars)."
517
+ },
518
+ {
519
+ name: "VOLTRO_APPLE_KEY_ID",
520
+ secret: !1,
521
+ description: "Key ID of the Sign-in-with-Apple .p8 key."
522
+ },
523
+ {
524
+ name: "VOLTRO_APPLE_PRIVATE_KEY",
525
+ secret: !0,
526
+ description: "Contents of the .p8 file (PKCS#8 PEM). Apple signs no static secret — this key mints one per exchange."
527
+ }
528
+ ]
529
+ }, Q = (e, t) => ({
530
+ status: e,
531
+ body: JSON.stringify(t),
532
+ contentType: "application/json; charset=utf-8"
533
+ }), $ = (e, t) => ({
534
+ status: 302,
535
+ body: "",
536
+ contentType: "text/plain; charset=utf-8",
537
+ headers: {
538
+ location: e,
539
+ ...t === void 0 ? {} : { "set-cookie": t }
540
+ }
541
+ }), ye = (e) => Buffer.from(JSON.stringify(e), "utf8").toString("base64url"), be = (e) => {
542
+ if (!e) return null;
543
+ try {
544
+ let t = JSON.parse(Buffer.from(e, "base64url").toString("utf8"));
545
+ return typeof t.p != "string" || typeof t.s != "string" || typeof t.v != "string" || typeof t.n != "string" ? null : {
546
+ p: t.p,
547
+ s: t.s,
548
+ v: t.v,
549
+ n: t.n
550
+ };
551
+ } catch {
552
+ return null;
553
+ }
554
+ }, xe = (e) => e.method === "POST" && (e.headers["content-type"] ?? "").includes("application/x-www-form-urlencoded") ? new URLSearchParams(Buffer.from(e.rawBody).toString("utf8")) : new URLSearchParams(e.query), Se = (i) => {
555
+ let a = i.prefix ?? ge, o = i.stateTtlSeconds ?? _e, s = i.stateCookieName ?? ve, c = i.linkPolicy ?? "never", l = i.auth.cookieSecure ?? !0, u = Object.keys(i.providers).filter(j);
556
+ if (u.length === 0) throw new D("socialAuthPlugin: `providers` is empty — enable at least one of google / github / apple");
557
+ let d = (i.appBaseUrl ?? i.auth.appBaseUrl ?? "").replace(/\/+$/, ""), f = (e) => {
558
+ let t = i.providers[e] ?? {}, n = process.env, r = t.clientId ?? n[Z[e][0].name] ?? "";
559
+ if (!r) throw new D(`socialAuthPlugin: ${e} has no client id — pass \`providers.${e}.clientId\` or set ${Z[e][0].name}`);
560
+ let o = t.redirectUri ?? `${d}${a}/${e}/callback`;
561
+ if (!o.startsWith("http")) throw new D(`socialAuthPlugin: ${e} needs an absolute redirect URI — set \`appBaseUrl\` (or \`providers.${e}.redirectUri\`)`);
562
+ let s = {
563
+ id: e,
564
+ clientId: r,
565
+ redirectUri: o,
566
+ endpoints: t.endpoints ?? {},
567
+ ...t.scopes === void 0 ? {} : { scopes: t.scopes }
568
+ };
569
+ if (e === "apple") {
570
+ let e = t.appleKey ?? (n.VOLTRO_APPLE_TEAM_ID && n.VOLTRO_APPLE_KEY_ID && n.VOLTRO_APPLE_PRIVATE_KEY ? {
571
+ teamId: n.VOLTRO_APPLE_TEAM_ID,
572
+ keyId: n.VOLTRO_APPLE_KEY_ID,
573
+ privateKey: n.VOLTRO_APPLE_PRIVATE_KEY.replace(/\\n/g, "\n")
574
+ } : void 0);
575
+ if (!e) throw new D("socialAuthPlugin: apple needs `appleKey` (teamId + keyId + .p8) or VOLTRO_APPLE_TEAM_ID / VOLTRO_APPLE_KEY_ID / VOLTRO_APPLE_PRIVATE_KEY");
576
+ return {
577
+ ...s,
578
+ appleKey: e
579
+ };
580
+ }
581
+ let c = t.clientSecret ?? n[Z[e][1].name] ?? "";
582
+ if (!c) throw new D(`socialAuthPlugin: ${e} has no client secret — pass \`providers.${e}.clientSecret\` or set ${Z[e][1].name}`);
583
+ return {
584
+ ...s,
585
+ clientSecret: c
586
+ };
587
+ }, p = i.identities ?? null, m = (e) => {
588
+ if (p) return p;
589
+ let t = e.store;
590
+ if (!t) throw new D("socialAuthPlugin: no identity store — pass `identities`, or run on an app with a DataStore");
591
+ return p = me(t), p;
592
+ }, h = (e) => {
593
+ if (!e.startsWith(`${a}/`)) return null;
594
+ let [t, ...n] = e.slice(a.length + 1).split("/");
595
+ return !t || !j(t) || !u.includes(t) ? null : n.length === 0 ? {
596
+ id: t,
597
+ isCallback: !1
598
+ } : n.length === 1 && n[0] === "callback" ? {
599
+ id: t,
600
+ isCallback: !0
601
+ } : null;
602
+ }, g = (e, t, r) => {
603
+ if (!i.failureRedirect) return Q(e, {
604
+ error: t,
605
+ message: r
606
+ });
607
+ let o = i.failureRedirect.includes("?") ? "&" : "?";
608
+ return $(`${i.failureRedirect}${o}social_error=${encodeURIComponent(t)}`, n(s, "", {
609
+ maxAgeSeconds: 0,
610
+ path: a,
611
+ secure: l
612
+ }));
613
+ }, _ = (e, t) => {
614
+ let r = f(t), c = M(t, r.endpoints), u = N({
615
+ provider: t,
616
+ clientId: r.clientId,
617
+ redirectUri: r.redirectUri,
618
+ ...r.scopes === void 0 ? {} : { scopes: r.scopes },
619
+ endpoints: r.endpoints,
620
+ ...new URLSearchParams(e.query).get("login_hint") ? { loginHint: new URLSearchParams(e.query).get("login_hint") } : {}
621
+ }), d = n(s, ye({
622
+ p: t,
623
+ s: u.state,
624
+ v: u.codeVerifier,
625
+ n: u.nonce
626
+ }), {
627
+ maxAgeSeconds: o,
628
+ httpOnly: !0,
629
+ sameSite: c.responseMode === "form_post" ? "none" : "lax",
630
+ secure: c.responseMode === "form_post" || l,
631
+ ...i.auth.cookieDomain ? { domain: i.auth.cookieDomain } : {},
632
+ path: a
633
+ });
634
+ return $(u.url, d);
635
+ }, v = async (t, o) => {
636
+ let u = xe(t), d = u.get("error");
637
+ if (d) return g(400, "exchange_failed", `${o} refused the authorization: ${d}`);
638
+ let p = be(r(t.headers.cookie, s)), h = p && p.p === o ? p : null, _ = f(o), v = await G({
639
+ provider: o,
640
+ clientId: _.clientId,
641
+ ..._.clientSecret === void 0 ? {} : { clientSecret: _.clientSecret },
642
+ ..._.appleKey === void 0 ? {} : { appleKey: _.appleKey },
643
+ redirectUri: _.redirectUri,
644
+ code: u.get("code") ?? "",
645
+ state: u.get("state") ?? "",
646
+ expectedState: h?.s ?? "",
647
+ codeVerifier: h?.v ?? "",
648
+ expectedNonce: h?.n ?? "",
649
+ ...u.get("user") ? { appleUserPayload: u.get("user") } : {},
650
+ endpoints: _.endpoints
651
+ }), y = await e.runPromise(J({
652
+ profile: v,
653
+ identities: m(t),
654
+ users: i.users,
655
+ config: i.auth,
656
+ policy: c,
657
+ ipAddress: t.remoteAddr ?? null,
658
+ userAgent: t.headers["user-agent"] ?? null
659
+ }));
660
+ if (y.kind === "refused") return g(y.status, y.code, y.message);
661
+ let b = n(s, "", {
662
+ maxAgeSeconds: 0,
663
+ path: a,
664
+ secure: l
665
+ });
666
+ return {
667
+ status: 302,
668
+ body: "",
669
+ contentType: "text/plain; charset=utf-8",
670
+ headers: {
671
+ location: i.successRedirect ?? i.auth.successRedirect ?? "/",
672
+ "set-cookie": `${b}\n${y.setCookie}`
673
+ }
674
+ };
675
+ }, y = {
676
+ method: "*",
677
+ path: a,
678
+ handle: async (e) => {
679
+ let t = h(e.path);
680
+ if (!t) return Q(404, { error: "not_found" });
681
+ try {
682
+ return t.isCallback ? e.method !== "GET" && e.method !== "POST" ? Q(405, { error: "method_not_allowed" }) : await v(e, t.id) : e.method === "GET" ? _(e, t.id) : Q(405, { error: "method_not_allowed" });
683
+ } catch (e) {
684
+ if (e instanceof C) return g(e.status, e.code, e.message);
685
+ throw e;
686
+ }
687
+ }
688
+ }, b = [
689
+ "store:write",
690
+ "network:outbound:oauth2.googleapis.com",
691
+ "network:outbound:www.googleapis.com",
692
+ "network:outbound:github.com",
693
+ "network:outbound:api.github.com",
694
+ "network:outbound:appleid.apple.com"
695
+ ], x = u.flatMap((e) => Z[e].map((t) => ({
696
+ name: t.name,
697
+ required: i.providers[e]?.clientId === void 0,
698
+ secret: t.secret,
699
+ description: t.description
700
+ })));
701
+ return t({
702
+ name: i.name ? `@voltro/plugin-auth-social#${i.name}` : "@voltro/plugin-auth-social",
703
+ description: "Social login — Sign in with Google / GitHub / Apple. Mandatory PKCE (S256) + state, JWKS-verified ID tokens, a deliberate account-linking policy, and sessions issued through @voltro/plugin-auth.",
704
+ permissions: b,
705
+ declaredEnv: x,
706
+ extendSchema: { tables: ue },
707
+ httpRoutes: [y],
708
+ onActivate: (t) => e.sync(() => {
709
+ for (let e of u) f(e);
710
+ t.logger.info("social auth active", {
711
+ prefix: a,
712
+ providers: u.join(","),
713
+ linkPolicy: c,
714
+ stateTtlSeconds: o
715
+ });
716
+ }),
717
+ onDeactivate: () => e.void
718
+ });
719
+ };
720
+ //#endregion
721
+ export { ae as APPLE_CLIENT_SECRET_DEFAULT_TTL_S, P as APPLE_CLIENT_SECRET_MAX_TTL_S, Y as SOCIAL_IDENTITY_TABLE, ie as SOCIAL_PROVIDERS, A as SOCIAL_PROVIDER_IDS, O as SocialAccountLinkError, C as SocialAuthError, D as SocialConfigError, T as SocialExchangeError, E as SocialIdTokenError, w as SocialStateMismatchError, ce as _resetAppleSecretCache, R as appleClientSecret, J as completeSocialSignIn, me as dataStoreSocialIdentityStore, K as decideSocialLink, H as fetchGithubIdentity, B as isApplePrivateRelayEmail, j as isSocialProviderId, he as linkSocialIdentity, fe as memorySocialIdentityStore, z as parseAppleUserPayload, Se as socialAuthPlugin, ue as socialAuthTables, N as socialBeginLogin, G as socialCompleteLogin, le as socialIdentitiesTable, M as socialProvider };