@voltro/plugin-auth-social 0.86.0 → 0.87.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/plugin.js CHANGED
@@ -4,11 +4,17 @@ import { socialBeginLogin as i } from "./login.js";
4
4
  import { socialCompleteLogin as a } from "./callback.js";
5
5
  import { completeSocialSignIn as o } from "./signIn.js";
6
6
  import { dataStoreSocialIdentityStore as s, socialAuthTables as c } from "./identityStore.js";
7
- import { Effect as l } from "effect";
8
- import { definePlugin as u } from "@voltro/protocol";
9
- import { buildSetCookie as d, readCookie as f } from "@voltro/protocol/session";
7
+ import { completeGuestSocialUpgrade as l } from "./guestUpgrade.js";
8
+ import { decodeGuestUpgradeHandoff as u, encodeGuestUpgradeHandoff as d, guestIdentityOf as f } from "./guestHandoff.js";
9
+ import { isNativeVerifier as p, nativeUpgradeRequestOf as m, openNativeUpgradeCode as h, sealNativeUpgradeCode as g, signNativeUpgradeTicket as ee, verifierMatches as te, verifyNativeUpgradeTicket as ne } from "./nativeGuestUpgrade.js";
10
+ import { verifyPlatformTicket as _ } from "./platform.js";
11
+ import { Effect as v, Result as y } from "effect";
12
+ import { and as b, eq as x } from "@voltro/database";
13
+ import { definePlugin as re } from "@voltro/protocol";
14
+ import { SESSION_HEADER_NAME as S, buildSetCookie as C, readCookie as w } from "@voltro/protocol/session";
15
+ import { CSRF_COOKIE_NAME as T, CSRF_HEADER_NAME as ie, configSessionSecrets as E, dataStoreGuestAuth as ae, dataStoreSessionRegistry as oe, issueCsrfToken as se, parseGuestCredential as D, readSessionFromHeaders as O, signAuthClaims as ce, verifyAuthClaims as k, verifyCsrf as le } from "@voltro/plugin-auth";
10
16
  //#region src/plugin.ts
11
- var p = "/auth/social", m = 600, h = "voltro:oauth", g = {
17
+ var ue = "/auth/social", de = 600, fe = "voltro:oauth", A = {
12
18
  google: [{
13
19
  name: "VOLTRO_GOOGLE_CLIENT_ID",
14
20
  secret: !1,
@@ -48,41 +54,140 @@ var p = "/auth/social", m = 600, h = "voltro:oauth", g = {
48
54
  secret: !0,
49
55
  description: "Contents of the .p8 file (PKCS#8 PEM). Apple signs no static secret — this key mints one per exchange."
50
56
  }
51
- ]
52
- }, _ = (e, t) => ({
57
+ ],
58
+ discord: [{
59
+ name: "VOLTRO_DISCORD_CLIENT_ID",
60
+ secret: !1,
61
+ description: "Discord OAuth application client id."
62
+ }, {
63
+ name: "VOLTRO_DISCORD_CLIENT_SECRET",
64
+ secret: !0,
65
+ description: "Discord OAuth application client secret."
66
+ }],
67
+ twitch: [{
68
+ name: "VOLTRO_TWITCH_CLIENT_ID",
69
+ secret: !1,
70
+ description: "Twitch confidential application client id."
71
+ }, {
72
+ name: "VOLTRO_TWITCH_CLIENT_SECRET",
73
+ secret: !0,
74
+ description: "Twitch confidential application client secret."
75
+ }],
76
+ microsoft: [{
77
+ name: "VOLTRO_MICROSOFT_CLIENT_ID",
78
+ secret: !1,
79
+ description: "Microsoft application client id for personal accounts."
80
+ }, {
81
+ name: "VOLTRO_MICROSOFT_CLIENT_SECRET",
82
+ secret: !0,
83
+ description: "Microsoft application client secret."
84
+ }],
85
+ epic: [{
86
+ name: "VOLTRO_EPIC_CLIENT_ID",
87
+ secret: !1,
88
+ description: "Epic Account Services OAuth client id."
89
+ }, {
90
+ name: "VOLTRO_EPIC_CLIENT_SECRET",
91
+ secret: !0,
92
+ description: "Epic Account Services OAuth client secret."
93
+ }]
94
+ }, j = (e, t) => ({
53
95
  status: e,
54
96
  body: JSON.stringify(t),
55
- contentType: "application/json; charset=utf-8"
56
- }), v = (e, t) => ({
97
+ contentType: "application/json; charset=utf-8",
98
+ headers: { "cache-control": "no-store" }
99
+ }), M = (e, t) => e + (e.includes("?") ? "&" : "?") + new URLSearchParams(t).toString(), N = (e, t) => ({
57
100
  status: 302,
58
101
  body: "",
59
102
  contentType: "text/plain; charset=utf-8",
60
103
  headers: {
104
+ "cache-control": "no-store",
105
+ "referrer-policy": "no-referrer",
61
106
  location: e,
62
107
  ...t === void 0 ? {} : { "set-cookie": t }
63
108
  }
64
- }), y = (e) => Buffer.from(JSON.stringify(e), "utf8").toString("base64url"), b = (e) => {
109
+ }), pe = (e, t) => ce({ ...e }, t.current.secret, "social-login"), me = (e, t) => {
65
110
  if (!e) return null;
66
111
  try {
67
- let t = JSON.parse(Buffer.from(e, "base64url").toString("utf8"));
68
- return typeof t.p != "string" || typeof t.s != "string" || typeof t.v != "string" || typeof t.n != "string" ? null : {
69
- p: t.p,
70
- s: t.s,
71
- v: t.v,
72
- n: t.n
112
+ let n = [t.current, t.previous].filter((e) => e !== void 0).map((t) => k(e, t.secret, "social-login")).find((e) => e !== null);
113
+ if (!n) return null;
114
+ let r = n;
115
+ if (typeof r.p != "string" || typeof r.s != "string" || typeof r.v != "string" || typeof r.n != "string" || typeof r.exp != "number" || typeof r.audience != "string" || typeof r.redirectUri != "string") return null;
116
+ let i = r.guest === void 0 ? void 0 : f(r.guest);
117
+ if (i === null) return null;
118
+ let a = r.native, o = a === void 0 ? void 0 : m(a, typeof a?.r == "string" ? [a.r] : []);
119
+ return o === null || o !== void 0 && i === void 0 ? null : {
120
+ p: r.p,
121
+ s: r.s,
122
+ v: r.v,
123
+ n: r.n,
124
+ exp: r.exp,
125
+ audience: r.audience,
126
+ redirectUri: r.redirectUri,
127
+ ...i === void 0 ? {} : { guest: i },
128
+ ...o === void 0 ? {} : { native: o }
73
129
  };
74
130
  } catch {
75
131
  return null;
76
132
  }
77
- }, x = (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), S = (S) => {
78
- let C = S.prefix ?? p, w = S.stateTtlSeconds ?? m, T = S.stateCookieName ?? h, E = S.linkPolicy ?? "never", D = S.auth.cookieSecure ?? !0, O = Object.keys(S.providers).filter(n);
79
- if (O.length === 0) throw new t("socialAuthPlugin: `providers` is empty — enable at least one of google / github / apple");
80
- let k = (S.appBaseUrl ?? S.auth.appBaseUrl ?? "").replace(/\/+$/, ""), A = (e) => {
81
- let n = S.providers[e] ?? {}, r = process.env, i = n.clientId ?? r[g[e][0].name] ?? "";
82
- if (!i) throw new t(`socialAuthPlugin: ${e} has no client id — pass \`providers.${e}.clientId\` or set ${g[e][0].name}`);
83
- let a = n.redirectUri ?? `${k}${C}/${e}/callback`;
84
- if (!a.startsWith("http")) throw new t(`socialAuthPlugin: ${e} needs an absolute redirect URI — set \`appBaseUrl\` (or \`providers.${e}.redirectUri\`)`);
85
- let o = {
133
+ }, he = (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), P = (k) => {
134
+ let P = k.prefix ?? ue, F = k.stateTtlSeconds ?? de, I = k.stateCookieName ?? fe, L = k.linkPolicy ?? "never", R = k.auth.cookieSecure ?? !0, z = k.providers ?? {}, ge = Object.keys(z);
135
+ if (ge.find((e) => !n(e)) !== void 0) throw new t("socialAuthPlugin: unknown provider configuration");
136
+ let B = ge.filter(n), V = /* @__PURE__ */ new Map();
137
+ for (let e of k.platforms ?? []) {
138
+ if (V.has(e.id)) throw new t(`socialAuthPlugin: platform ${e.id} is configured twice`);
139
+ V.set(e.id, e);
140
+ }
141
+ if (B.length === 0 && V.size === 0) throw new t("socialAuthPlugin: `providers` and `platforms` are empty — enable at least one provider or platform");
142
+ if (!Number.isSafeInteger(F) || F <= 0 || F > 3600) throw new t("socialAuthPlugin: stateTtlSeconds must be an integer from 1 to 3600");
143
+ let H = (k.appBaseUrl ?? k.auth.appBaseUrl ?? "").replace(/\/+$/, ""), U = I + ":upgrade", W = k.guestUpgrade, G = JSON.stringify([
144
+ H,
145
+ P,
146
+ k.name ?? "",
147
+ k.auth.defaultTenantId
148
+ ]);
149
+ if (W) {
150
+ if (W.guest.tenantId !== k.auth.defaultTenantId || k.auth.ensureActor !== void 0 && W.configForTransaction === void 0) throw new t("Guest upgrade requires matching server tenants and transaction-bound actor hooks");
151
+ if (k.identities !== void 0) throw new t("Guest upgrade requires transaction-bound SQL social identities");
152
+ let e = !1;
153
+ try {
154
+ let t = new URL(H), n = new URL(W.confirmationRedirect, t);
155
+ e = /^https?:$/.test(t.protocol) && n.origin === t.origin && !n.username && !n.password && !W.confirmationRedirect.startsWith("//") && !W.confirmationRedirect.includes("\\");
156
+ } catch {}
157
+ if (!e) throw new t("Guest upgrade confirmation must be a same-origin application page");
158
+ for (let e of W.native?.redirectUris ?? ["acme://valid"]) {
159
+ let n = null;
160
+ try {
161
+ n = new URL(e);
162
+ } catch {}
163
+ if (!n || e.includes("*") || n.hash || n.username || n.password || /^(javascript|data|file):$/.test(n.protocol) || n.protocol === "http:" && ![
164
+ "127.0.0.1",
165
+ "[::1]",
166
+ "localhost"
167
+ ].includes(n.hostname)) throw new t("Native guest upgrade redirect URIs must be exact app URIs (custom scheme, https or loopback http)");
168
+ }
169
+ if (W.native && W.native.redirectUris.length === 0) throw new t("Native guest upgrade needs at least one redirect URI");
170
+ }
171
+ let K = (e) => {
172
+ if (!W) throw new t("Guest upgrade is not configured");
173
+ return W.service?.(e) ?? ae(e, W.guest);
174
+ }, q = (e) => C(e, "", {
175
+ maxAgeSeconds: 0,
176
+ path: P,
177
+ secure: R,
178
+ httpOnly: !0,
179
+ sameSite: "lax",
180
+ ...k.auth.cookieDomain ? { domain: k.auth.cookieDomain } : {}
181
+ }), J = (e) => {
182
+ let n = z[e] ?? {}, r = process.env, i = n.clientId ?? r[A[e][0].name] ?? "";
183
+ if (!i) throw new t(`socialAuthPlugin: ${e} has no client id — pass \`providers.${e}.clientId\` or set ${A[e][0].name}`);
184
+ let a = n.redirectUri ?? `${H}${P}/${e}/callback`, o = !1;
185
+ try {
186
+ let e = new URL(a);
187
+ o = (e.protocol === "https:" || e.protocol === "http:") && e.username === "" && e.password === "" && e.hash === "";
188
+ } catch {}
189
+ if (!o) throw new t(`socialAuthPlugin: ${e} needs an absolute redirect URI — set \`appBaseUrl\` (or \`providers.${e}.redirectUri\`)`);
190
+ let s = {
86
191
  id: e,
87
192
  clientId: i,
88
193
  redirectUri: a,
@@ -97,148 +202,540 @@ var p = "/auth/social", m = 600, h = "voltro:oauth", g = {
97
202
  } : void 0);
98
203
  if (!e) throw new t("socialAuthPlugin: apple needs `appleKey` (teamId + keyId + .p8) or VOLTRO_APPLE_TEAM_ID / VOLTRO_APPLE_KEY_ID / VOLTRO_APPLE_PRIVATE_KEY");
99
204
  return {
100
- ...o,
205
+ ...s,
101
206
  appleKey: e
102
207
  };
103
208
  }
104
- let s = n.clientSecret ?? r[g[e][1].name] ?? "";
105
- if (!s) throw new t(`socialAuthPlugin: ${e} has no client secret — pass \`providers.${e}.clientSecret\` or set ${g[e][1].name}`);
209
+ let c = n.clientSecret ?? r[A[e][1].name] ?? "";
210
+ if (!c) throw new t(`socialAuthPlugin: ${e} has no client secret — pass \`providers.${e}.clientSecret\` or set ${A[e][1].name}`);
106
211
  return {
107
- ...o,
108
- clientSecret: s
212
+ ...s,
213
+ clientSecret: c
109
214
  };
110
- }, j = S.identities ?? null, M = (e) => {
111
- if (j) return j;
215
+ }, Y = k.identities ?? null, _e = (e) => {
216
+ if (Y) return Y;
112
217
  let n = e.store;
113
218
  if (!n) throw new t("socialAuthPlugin: no identity store — pass `identities`, or run on an app with a DataStore");
114
- return j = s(n), j;
115
- }, N = (e) => {
116
- if (!e.startsWith(`${C}/`)) return null;
117
- let [t, ...r] = e.slice(C.length + 1).split("/");
118
- return !t || !n(t) || !O.includes(t) ? null : r.length === 0 ? {
219
+ return Y = s(n), Y;
220
+ }, ve = (e) => {
221
+ if (!e.startsWith(`${P}/`)) return null;
222
+ let [t, ...r] = e.slice(P.length + 1).split("/");
223
+ return !t || !n(t) || !B.includes(t) ? null : r.length === 0 ? {
119
224
  id: t,
120
225
  isCallback: !1
121
226
  } : r.length === 1 && r[0] === "callback" ? {
122
227
  id: t,
123
228
  isCallback: !0
124
229
  } : null;
125
- }, P = (e, t, n) => {
126
- if (!S.failureRedirect) return _(e, {
127
- error: t,
128
- message: n
129
- });
130
- let r = S.failureRedirect.includes("?") ? "&" : "?";
131
- return v(`${S.failureRedirect}${r}social_error=${encodeURIComponent(t)}`, d(T, "", {
132
- maxAgeSeconds: 0,
133
- path: C,
134
- secure: D
135
- }));
136
- }, F = (e, t) => {
137
- let n = A(t), a = r(t, n.endpoints), o = i({
230
+ }, X = (e, t, n) => {
231
+ let r = [q(I), ...W ? [q(U)] : []];
232
+ if (!k.failureRedirect) return {
233
+ ...j(e, {
234
+ error: t,
235
+ message: n
236
+ }),
237
+ headers: {
238
+ "cache-control": "no-store",
239
+ "set-cookie": r
240
+ }
241
+ };
242
+ let i = k.failureRedirect.includes("?") ? "&" : "?", a = N(`${k.failureRedirect}${i}social_error=${encodeURIComponent(t)}`);
243
+ return {
244
+ ...a,
245
+ headers: {
246
+ ...a.headers,
247
+ "set-cookie": r
248
+ }
249
+ };
250
+ }, Z = (e, t, n, a) => {
251
+ let o = J(t), s = r(t, o.endpoints), c = i({
138
252
  provider: t,
139
- clientId: n.clientId,
140
- redirectUri: n.redirectUri,
141
- ...n.scopes === void 0 ? {} : { scopes: n.scopes },
142
- endpoints: n.endpoints,
253
+ clientId: o.clientId,
254
+ redirectUri: o.redirectUri,
255
+ ...o.scopes === void 0 ? {} : { scopes: o.scopes },
256
+ endpoints: o.endpoints,
143
257
  ...new URLSearchParams(e.query).get("login_hint") ? { loginHint: new URLSearchParams(e.query).get("login_hint") } : {}
144
- }), s = d(T, y({
258
+ }), l = C(I, pe({
145
259
  p: t,
146
- s: o.state,
147
- v: o.codeVerifier,
148
- n: o.nonce
149
- }), {
150
- maxAgeSeconds: w,
260
+ s: c.state,
261
+ v: c.codeVerifier,
262
+ n: c.nonce,
263
+ exp: Math.floor(Date.now() / 1e3) + F,
264
+ audience: G,
265
+ redirectUri: o.redirectUri,
266
+ ...n === void 0 ? {} : { guest: n },
267
+ ...n !== void 0 && a !== void 0 ? { native: a } : {}
268
+ }, E(k.auth)), {
269
+ maxAgeSeconds: F,
151
270
  httpOnly: !0,
152
- sameSite: a.responseMode === "form_post" ? "none" : "lax",
153
- secure: a.responseMode === "form_post" || D,
154
- ...S.auth.cookieDomain ? { domain: S.auth.cookieDomain } : {},
155
- path: C
156
- });
157
- return v(o.url, s);
158
- }, I = async (e, t) => {
159
- let n = x(e), r = n.get("error");
160
- if (r) return P(400, "exchange_failed", `${t} refused the authorization: ${r}`);
161
- let i = b(f(e.headers.cookie, T)), s = i && i.p === t ? i : null, c = A(t), u = await a({
271
+ sameSite: s.responseMode === "form_post" ? "none" : "lax",
272
+ secure: s.responseMode === "form_post" || R,
273
+ ...k.auth.cookieDomain ? { domain: k.auth.cookieDomain } : {},
274
+ path: P
275
+ }), u = N(c.url, l);
276
+ return W ? {
277
+ ...u,
278
+ headers: {
279
+ ...u.headers,
280
+ "set-cookie": [l, q(U)]
281
+ }
282
+ } : u;
283
+ }, ye = async (e, t) => {
284
+ let n = he(e), r = me(w(e.headers.cookie, I), E(k.auth)), i = J(t), s = r && r.p === t && r.audience === G && r.redirectUri === i.redirectUri ? r : null, c = W?.native && s?.guest && s.native ? s.native : null, l = (e) => {
285
+ let t = N(e);
286
+ return {
287
+ ...t,
288
+ headers: {
289
+ ...t.headers,
290
+ "set-cookie": [q(I)]
291
+ }
292
+ };
293
+ }, u = (e, t, n) => c ? l(M(c.r, {
294
+ error: t,
295
+ state: c.ns
296
+ })) : X(e, t, n);
297
+ if (n.get("error")) return u(400, "exchange_failed", `${t} refused the authorization`);
298
+ if (s?.guest) {
299
+ if (!W || !e.store || s.guest.tenantId !== W.guest.tenantId) return u(403, "credential_rejected", "Guest upgrade was refused.");
300
+ let t = await v.runPromise(v.result(K(e.store).isLive(s.guest)));
301
+ if (y.isFailure(t)) return u(503, "store_unavailable", "Guest upgrade storage is unavailable.");
302
+ if (!t.success) return u(403, "credential_rejected", "Guest upgrade was refused.");
303
+ }
304
+ let f = await a({
162
305
  provider: t,
163
- clientId: c.clientId,
164
- ...c.clientSecret === void 0 ? {} : { clientSecret: c.clientSecret },
165
- ...c.appleKey === void 0 ? {} : { appleKey: c.appleKey },
166
- redirectUri: c.redirectUri,
306
+ clientId: i.clientId,
307
+ ...i.clientSecret === void 0 ? {} : { clientSecret: i.clientSecret },
308
+ ...i.appleKey === void 0 ? {} : { appleKey: i.appleKey },
309
+ redirectUri: i.redirectUri,
167
310
  code: n.get("code") ?? "",
168
311
  state: n.get("state") ?? "",
169
312
  expectedState: s?.s ?? "",
170
313
  codeVerifier: s?.v ?? "",
171
314
  expectedNonce: s?.n ?? "",
172
315
  ...n.get("user") ? { appleUserPayload: n.get("user") } : {},
173
- endpoints: c.endpoints
174
- }), p = await l.runPromise(o({
175
- profile: u,
176
- identities: M(e),
177
- users: S.users,
178
- config: S.auth,
179
- policy: E,
316
+ endpoints: i.endpoints
317
+ });
318
+ if (s?.guest && W) {
319
+ let e = Math.min(s.exp, Math.floor(Date.now() / 1e3) + F), t = d({
320
+ guest: s.guest,
321
+ profile: f,
322
+ exp: e,
323
+ audience: G
324
+ }, E(k.auth));
325
+ if (c) {
326
+ let n = g({
327
+ handoff: t,
328
+ ch: c.ch,
329
+ guestId: s.guest.id,
330
+ exp: Math.min(e, Math.floor(Date.now() / 1e3) + 120)
331
+ }, E(k.auth), G);
332
+ return l(M(c.r, {
333
+ code: n,
334
+ state: c.ns
335
+ }));
336
+ }
337
+ let n = N(W.confirmationRedirect);
338
+ return {
339
+ ...n,
340
+ headers: {
341
+ ...n.headers,
342
+ "set-cookie": [q(I), C(U, t, {
343
+ maxAgeSeconds: Math.max(0, e - Math.floor(Date.now() / 1e3)),
344
+ path: P,
345
+ httpOnly: !0,
346
+ sameSite: "lax",
347
+ secure: R,
348
+ ...k.auth.cookieDomain ? { domain: k.auth.cookieDomain } : {}
349
+ })]
350
+ }
351
+ };
352
+ }
353
+ let p = await v.runPromise(o({
354
+ profile: f,
355
+ identities: _e(e),
356
+ users: k.users,
357
+ config: k.auth,
358
+ policy: L,
180
359
  ipAddress: e.remoteAddr ?? null,
181
360
  userAgent: e.headers["user-agent"] ?? null
182
361
  }));
183
- if (p.kind === "refused") return P(p.status, p.code, p.message);
184
- let m = d(T, "", {
362
+ if (p.kind === "refused") return X(p.status, p.code, p.message);
363
+ let m = C(I, "", {
185
364
  maxAgeSeconds: 0,
186
- path: C,
187
- secure: D
365
+ path: P,
366
+ secure: R
188
367
  });
189
368
  return {
190
369
  status: 302,
191
370
  body: "",
192
371
  contentType: "text/plain; charset=utf-8",
193
372
  headers: {
194
- location: S.successRedirect ?? S.auth.successRedirect ?? "/",
373
+ "cache-control": "no-store",
374
+ "referrer-policy": "no-referrer",
375
+ location: k.successRedirect ?? k.auth.successRedirect ?? "/",
195
376
  "set-cookie": [m, p.setCookie]
196
377
  }
197
378
  };
198
- }, L = {
379
+ }, Q = (e, t) => ({
380
+ ...j(e, t),
381
+ headers: { "cache-control": "no-store" }
382
+ }), be = async (e, t, r) => {
383
+ let i = W?.native;
384
+ if (!W || !i) return j(404, { error: "not_found" });
385
+ let a = async (t) => {
386
+ if (!e.store) return Q(503, { error: "store_unavailable" });
387
+ let n = await v.runPromise(v.result(K(e.store).isLive(t)));
388
+ return y.isFailure(n) ? Q(503, { error: "store_unavailable" }) : n.success ? null : Q(403, { error: "credential_rejected" });
389
+ };
390
+ if (t === "begin") {
391
+ if (e.method !== "GET") return j(405, { error: "method_not_allowed" });
392
+ let t = ne(new URLSearchParams(e.query).get("t"), r, G, i.redirectUris);
393
+ return !t || !n(t.provider) || !B.includes(t.provider) || t.guest.tenantId !== W.guest.tenantId ? Q(403, { error: "upgrade_ticket_rejected" }) : await a(t.guest) ?? Z(e, t.provider, t.guest, t.native);
394
+ }
395
+ if (e.method !== "POST") return j(405, { error: "method_not_allowed" });
396
+ if ((e.headers["content-type"] ?? "").split(";")[0]?.trim() !== "application/json") return j(415, { error: "json_required" });
397
+ if (e.rawBody.byteLength > 8192) return j(413, { error: "body_too_large" });
398
+ let o;
399
+ try {
400
+ o = JSON.parse(new TextDecoder().decode(e.rawBody));
401
+ } catch {
402
+ return Q(400, { error: "invalid_input" });
403
+ }
404
+ if (!o || typeof o != "object" || Array.isArray(o)) return Q(400, { error: "invalid_input" });
405
+ let s = o;
406
+ if (t.startsWith("start/")) {
407
+ let o = t.slice(6);
408
+ if (!n(o) || !B.includes(o)) return j(404, { error: "not_found" });
409
+ let c = e.headers[S], l = typeof W.guest.secret == "function" ? W.guest.secret() : W.guest.secret, u = typeof c == "string" && c ? f(O({ [S]: c }, l)?.subject) : null;
410
+ if (!u || u.tenantId !== W.guest.tenantId) return Q(403, { error: "credential_rejected" });
411
+ let d = m({
412
+ ch: s.codeChallenge,
413
+ r: s.redirectUri,
414
+ ns: s.state
415
+ }, i.redirectUris);
416
+ if (!d || Object.keys(s).some((e) => e !== "codeChallenge" && e !== "redirectUri" && e !== "state")) return Q(400, { error: "invalid_input" });
417
+ let p = await a(u);
418
+ if (p) return p;
419
+ let h = ee({
420
+ guest: u,
421
+ provider: o,
422
+ native: d,
423
+ exp: Math.floor(Date.now() / 1e3) + F,
424
+ audience: G
425
+ }, r), g = new URL(P + "/upgrade/native/begin", J(o).redirectUri);
426
+ return g.search = new URLSearchParams({ t: h }).toString(), Q(200, { authorizationUrl: g.toString() });
427
+ }
428
+ if (t !== "complete") return j(404, { error: "not_found" });
429
+ if (Object.keys(s).some((e) => e !== "code" && e !== "codeVerifier" && e !== "credential" && e !== "confirmed") || s.confirmed !== !0 || typeof s.credential != "string" || !p(s.codeVerifier)) return Q(400, { error: "invalid_input" });
430
+ let c = h(s.code, r, G);
431
+ if (!c || !te(s.codeVerifier, c.ch)) return Q(400, { error: "invalid_code" });
432
+ let d = u(c.handoff, r, G), g = D(s.credential);
433
+ if (!d || d.guest.id !== c.guestId || d.guest.tenantId !== W.guest.tenantId || !g || g.guestId !== d.guest.id || g.deviceId !== d.guest.metadata?.guestDeviceId) return Q(403, { error: "upgrade_handoff_rejected" });
434
+ let _ = await a(d.guest);
435
+ if (_) return _;
436
+ let b = await v.runPromise(l({
437
+ profile: d.profile,
438
+ credential: s.credential,
439
+ store: e.store,
440
+ guest: W.guest,
441
+ service: K(e.store),
442
+ users: k.users,
443
+ config: k.auth,
444
+ ...W.configForTransaction ? { configForTransaction: W.configForTransaction } : {},
445
+ ipAddress: e.remoteAddr ?? null,
446
+ userAgent: e.headers["user-agent"] ?? null
447
+ }));
448
+ return b.kind === "refused" ? Q(b.status, {
449
+ error: b.code,
450
+ message: b.message
451
+ }) : Q(200, {
452
+ ok: !0,
453
+ upgraded: !0,
454
+ subject: b.subject,
455
+ session: b.session.value,
456
+ expiresAt: b.session.expiresAt
457
+ });
458
+ }, $ = /* @__PURE__ */ new Map(), xe = async (e, t) => {
459
+ let n = ($.get(e) ?? Promise.resolve()).then(t, t), r = n.then(() => void 0, () => void 0);
460
+ $.set(e, r);
461
+ try {
462
+ return await n;
463
+ } finally {
464
+ $.get(e) === r && $.delete(e);
465
+ }
466
+ }, Se = async (t) => {
467
+ let [n, r, ...i] = t.path.slice((P + "/platform/").length).split("/"), a = n === void 0 ? void 0 : V.get(n);
468
+ if (!a || i.length !== 0 || r !== "sign-in" && r !== "link" || r === "link" && !W) return j(404, { error: "not_found" });
469
+ if (t.method !== "POST") return j(405, { error: "method_not_allowed" });
470
+ if ((t.headers["content-type"] ?? "").split(";")[0]?.trim() !== "application/json") return j(415, { error: "json_required" });
471
+ if (t.rawBody.byteLength > 16384) return j(413, { error: "body_too_large" });
472
+ let s;
473
+ try {
474
+ s = JSON.parse(new TextDecoder().decode(t.rawBody));
475
+ } catch {
476
+ return Q(400, { error: "invalid_input" });
477
+ }
478
+ if (!s || typeof s != "object" || Array.isArray(s)) return Q(400, { error: "invalid_input" });
479
+ let c = s, u = r === "link" ? ["ticket", "credential"] : ["ticket"];
480
+ if (Object.keys(c).some((e) => !u.includes(e)) || typeof c.ticket != "string" || c.ticket.length === 0 || r === "link" && (typeof c.credential != "string" || !D(c.credential))) return Q(400, { error: "invalid_input" });
481
+ if (!t.store && (r === "link" || !k.identities)) return Q(503, { error: "store_unavailable" });
482
+ let d;
483
+ try {
484
+ d = await _(a, c.ticket);
485
+ } catch (t) {
486
+ if (t instanceof e) return Q(t.status, {
487
+ error: t.code,
488
+ message: t.message
489
+ });
490
+ throw t;
491
+ }
492
+ let f = {
493
+ provider: a.id,
494
+ providerAccountId: d.accountId,
495
+ email: null,
496
+ emailVerified: !1,
497
+ emailIsPrivateRelay: !1,
498
+ name: null,
499
+ firstName: null,
500
+ lastName: null,
501
+ avatarUrl: null,
502
+ nameIsFirstAuthorizationOnly: !1,
503
+ raw: {}
504
+ }, p = {
505
+ ipAddress: t.remoteAddr ?? null,
506
+ userAgent: t.headers["user-agent"] ?? null
507
+ }, m = await xe(a.id + "\0" + d.accountId, () => r === "sign-in" ? v.runPromise(o({
508
+ profile: f,
509
+ identities: _e(t),
510
+ users: k.users,
511
+ config: k.auth,
512
+ policy: L,
513
+ ...p
514
+ })) : v.runPromise(l({
515
+ profile: f,
516
+ credential: c.credential,
517
+ store: t.store,
518
+ guest: W.guest,
519
+ service: K(t.store),
520
+ users: k.users,
521
+ config: k.auth,
522
+ ...W.configForTransaction ? { configForTransaction: W.configForTransaction } : {},
523
+ ...p
524
+ })));
525
+ return m.kind === "refused" ? Q(m.status, {
526
+ error: m.code,
527
+ message: m.message
528
+ }) : Q(200, {
529
+ ok: !0,
530
+ subject: m.subject,
531
+ session: m.session.value,
532
+ expiresAt: m.session.expiresAt,
533
+ created: m.created
534
+ });
535
+ }, Ce = async (e) => {
536
+ if (!W) return j(404, { error: "not_found" });
537
+ let t = e.path.slice((P + "/upgrade/").length), r = E(k.auth);
538
+ if (t.startsWith("native/")) return be(e, t.slice(7), r);
539
+ if (t === "csrf") {
540
+ if (e.method !== "GET") return j(405, { error: "method_not_allowed" });
541
+ let t = se(r.current.secret);
542
+ return {
543
+ ...j(200, { token: t }),
544
+ headers: {
545
+ "cache-control": "no-store",
546
+ "set-cookie": C(T, t, {
547
+ path: "/",
548
+ httpOnly: !1,
549
+ secure: R,
550
+ sameSite: "lax",
551
+ maxAgeSeconds: F,
552
+ ...k.auth.cookieDomain ? { domain: k.auth.cookieDomain } : {}
553
+ })
554
+ }
555
+ };
556
+ }
557
+ let i = () => u(w(e.headers.cookie, U), r, G);
558
+ if (t === "receipt") {
559
+ if (e.method !== "GET") return j(405, { error: "method_not_allowed" });
560
+ let t = O(e.headers, r)?.subject;
561
+ if (!e.store || t?.type !== "user" || t.kind === "guest" || !/^guest_[A-Za-z0-9_-]+$/.test(t.id) || t.tenantId !== W.guest.tenantId || typeof t.metadata?.sessionId != "string") return j(403, { error: "credential_rejected" });
562
+ try {
563
+ let n = await v.runPromise(oe(e.store).findSession(t.metadata.sessionId));
564
+ if (!n || n.userId !== t.id || n.tenantId !== t.tenantId || !(n.expiresAt.getTime() > Date.now())) return j(403, { error: "credential_rejected" });
565
+ let r = {
566
+ predicate: b(x("id", t.id), x("tenantId", t.tenantId)),
567
+ order: [],
568
+ take: void 0,
569
+ skip: void 0,
570
+ projection: void 0
571
+ }, i = (await e.store.query({
572
+ table: "users",
573
+ ...r
574
+ }))[0], a = (await e.store.query({
575
+ table: "authGuests",
576
+ ...r
577
+ }))[0];
578
+ return !i || i.id !== t.id || i.tenantId !== t.tenantId || a || i.deactivatedAt !== null && i.deactivatedAt !== void 0 ? j(403, { error: "credential_rejected" }) : j(200, {
579
+ upgraded: !0,
580
+ subject: {
581
+ type: "user",
582
+ id: t.id,
583
+ tenantId: t.tenantId
584
+ }
585
+ });
586
+ } catch {
587
+ return j(503, { error: "store_unavailable" });
588
+ }
589
+ }
590
+ if (t === "pending") {
591
+ if (e.method !== "GET") return j(405, { error: "method_not_allowed" });
592
+ let t = i();
593
+ if (!t || t.guest.tenantId !== W.guest.tenantId || !e.store) return j(403, { error: "upgrade_handoff_rejected" });
594
+ let n = await v.runPromise(v.result(K(e.store).isLive(t.guest)));
595
+ return y.isFailure(n) ? j(503, { error: "store_unavailable" }) : n.success ? j(200, {
596
+ guestId: t.guest.id,
597
+ provider: t.profile.provider,
598
+ email: t.profile.email,
599
+ emailVerified: t.profile.emailVerified,
600
+ expiresAt: t.exp * 1e3
601
+ }) : j(403, { error: "credential_rejected" });
602
+ }
603
+ let a = n(t) && B.includes(t);
604
+ if (!a && t !== "complete") return j(404, { error: "not_found" });
605
+ if (e.method !== "POST") return j(405, { error: "method_not_allowed" });
606
+ if ((e.headers["content-type"] ?? "").split(";")[0]?.trim() !== "application/json") return j(415, { error: "json_required" });
607
+ if (e.rawBody.byteLength > 1024) return j(413, { error: "body_too_large" });
608
+ if (![r.current, r.previous].filter((e) => e !== void 0).some((t) => le(e.headers[ie], w(e.headers.cookie, T), t.secret))) return j(403, { error: "csrf_rejected" });
609
+ let o;
610
+ try {
611
+ o = JSON.parse(new TextDecoder().decode(e.rawBody));
612
+ } catch {
613
+ return j(400, { error: "invalid_input" });
614
+ }
615
+ if (!o || typeof o != "object" || Array.isArray(o)) return j(400, { error: "invalid_input" });
616
+ let s = o;
617
+ if (!e.store) return j(503, { error: "store_unavailable" });
618
+ let c = K(e.store);
619
+ if (a && n(t)) {
620
+ if (Object.keys(s).length !== 0) return j(400, { error: "invalid_input" });
621
+ let n = typeof W.guest.secret == "function" ? W.guest.secret() : W.guest.secret, r = f(O(e.headers, n)?.subject);
622
+ if (!r || r.tenantId !== W.guest.tenantId) return j(403, { error: "credential_rejected" });
623
+ let i = await v.runPromise(v.result(c.isLive(r)));
624
+ if (y.isFailure(i)) return j(503, { error: "store_unavailable" });
625
+ if (!i.success) return j(403, { error: "credential_rejected" });
626
+ let a = Z(e, t, r);
627
+ return {
628
+ ...j(200, { authorizationUrl: a.headers?.location }),
629
+ headers: {
630
+ "cache-control": "no-store",
631
+ "set-cookie": a.headers?.["set-cookie"] ?? []
632
+ }
633
+ };
634
+ }
635
+ if (Object.keys(s).some((e) => e !== "credential" && e !== "confirmed") || typeof s.credential != "string" || s.confirmed !== !0) return j(400, { error: "invalid_input" });
636
+ let d = i(), p = D(s.credential);
637
+ if (!d || !p || d.guest.tenantId !== W.guest.tenantId || p.guestId !== d.guest.id || p.deviceId !== d.guest.metadata?.guestDeviceId) return j(403, { error: "upgrade_handoff_rejected" });
638
+ let m = await v.runPromise(v.result(c.isLive(d.guest)));
639
+ if (y.isFailure(m)) return j(503, { error: "store_unavailable" });
640
+ if (!m.success) return j(403, { error: "credential_rejected" });
641
+ let h = await v.runPromise(l({
642
+ profile: d.profile,
643
+ credential: s.credential,
644
+ store: e.store,
645
+ guest: W.guest,
646
+ service: c,
647
+ users: k.users,
648
+ config: k.auth,
649
+ ...W.configForTransaction ? { configForTransaction: W.configForTransaction } : {},
650
+ ipAddress: e.remoteAddr ?? null,
651
+ userAgent: e.headers["user-agent"] ?? null
652
+ }));
653
+ return h.kind === "refused" ? j(h.status, {
654
+ error: h.code,
655
+ message: h.message
656
+ }) : {
657
+ ...j(200, {
658
+ upgraded: !0,
659
+ subject: h.subject
660
+ }),
661
+ headers: {
662
+ "cache-control": "no-store",
663
+ "set-cookie": [q(U), h.setCookie]
664
+ }
665
+ };
666
+ }, we = {
199
667
  method: "*",
200
- path: C,
668
+ path: P,
201
669
  handle: async (t) => {
202
- let n = N(t.path);
203
- if (!n) return _(404, { error: "not_found" });
670
+ if (t.path.startsWith(P + "/platform/")) return Se(t);
671
+ if (t.path.startsWith(P + "/upgrade/")) try {
672
+ return await Ce(t);
673
+ } catch (t) {
674
+ if (t instanceof e) return X(t.status, t.code, t.message);
675
+ throw t;
676
+ }
677
+ let n = ve(t.path);
678
+ if (!n) return j(404, { error: "not_found" });
204
679
  try {
205
- return n.isCallback ? t.method !== "GET" && t.method !== "POST" ? _(405, { error: "method_not_allowed" }) : await I(t, n.id) : t.method === "GET" ? F(t, n.id) : _(405, { error: "method_not_allowed" });
680
+ return n.isCallback ? t.method !== "GET" && t.method !== "POST" ? j(405, { error: "method_not_allowed" }) : await ye(t, n.id) : t.method === "GET" ? Z(t, n.id) : j(405, { error: "method_not_allowed" });
206
681
  } catch (t) {
207
- if (t instanceof e) return P(t.status, t.code, t.message);
682
+ if (t instanceof e) return X(t.status, t.code, t.message);
208
683
  throw t;
209
684
  }
210
685
  }
211
- }, R = [
686
+ }, Te = [
212
687
  "store:write",
213
688
  "network:outbound:oauth2.googleapis.com",
214
689
  "network:outbound:www.googleapis.com",
215
690
  "network:outbound:github.com",
216
691
  "network:outbound:api.github.com",
217
- "network:outbound:appleid.apple.com"
218
- ], z = O.flatMap((e) => g[e].map((t) => ({
692
+ "network:outbound:appleid.apple.com",
693
+ "network:outbound:discord.com",
694
+ "network:outbound:id.twitch.tv",
695
+ "network:outbound:login.microsoftonline.com",
696
+ "network:outbound:api.epicgames.dev",
697
+ ...[...V.values()].flatMap((e) => e.permissions)
698
+ ], Ee = (e, t) => {
699
+ let n = z[e];
700
+ return t === 0 ? n?.clientId !== void 0 : e === "apple" ? t === 1 ? n?.appleKey?.teamId !== void 0 : t === 2 ? n?.appleKey?.keyId !== void 0 : n?.appleKey?.privateKey !== void 0 : n?.clientSecret !== void 0;
701
+ }, De = B.flatMap((e) => A[e].map((t, n) => ({
219
702
  name: t.name,
220
- required: S.providers[e]?.clientId === void 0,
703
+ required: !Ee(e, n),
221
704
  secret: t.secret,
222
705
  description: t.description
223
- })));
224
- return u({
225
- name: S.name ? `@voltro/plugin-auth-social#${S.name}` : "@voltro/plugin-auth-social",
226
- 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.",
227
- permissions: R,
228
- declaredEnv: z,
706
+ }))), Oe = k.auth.secret === void 0 && k.auth.secrets === void 0 ? [{
707
+ name: "VOLTRO_SESSION_SECRET",
708
+ required: !0,
709
+ secret: !0,
710
+ generate: "base64url",
711
+ description: "Project key for signed login handoffs and ordinary sessions."
712
+ }] : [];
713
+ return re({
714
+ name: k.name ? `@voltro/plugin-auth-social#${k.name}` : "@voltro/plugin-auth-social",
715
+ description: "Social login — Google / GitHub / Apple / Discord / Twitch / Microsoft / Epic, and game sign-in with Steam / EOS / console tickets. Mandatory S256 + state, JWKS-verified ID tokens, a deliberate account-linking policy, and sessions issued through @voltro/plugin-auth.",
716
+ permissions: Te,
717
+ declaredEnv: [
718
+ ...De,
719
+ ...[...V.values()].flatMap((e) => e.declaredEnv),
720
+ ...Oe
721
+ ],
229
722
  extendSchema: { tables: c },
230
- httpRoutes: [L],
231
- onActivate: (e) => l.sync(() => {
232
- for (let e of O) A(e);
723
+ httpRoutes: [we],
724
+ onActivate: (e) => v.sync(() => {
725
+ for (let e of B) J(e);
726
+ for (let e of V.values()) e.check();
727
+ let n = E(k.auth);
728
+ if (ce({ exp: Math.floor(Date.now() / 1e3) + F }, n.current.secret, "social-login"), n.previous && n.previous.secret.length < 32) throw new t("socialAuthPlugin: previous project signing key is too short");
233
729
  e.logger.info("social auth active", {
234
- prefix: C,
235
- providers: O.join(","),
236
- linkPolicy: E,
237
- stateTtlSeconds: w
730
+ prefix: P,
731
+ providers: B.join(","),
732
+ platforms: [...V.keys()].join(","),
733
+ linkPolicy: L,
734
+ stateTtlSeconds: F
238
735
  });
239
736
  }),
240
- onDeactivate: () => l.void
737
+ onDeactivate: () => v.void
241
738
  });
242
739
  };
243
740
  //#endregion
244
- export { S as socialAuthPlugin };
741
+ export { P as socialAuthPlugin };