@voltro/plugin-auth-social 0.84.0 → 0.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ import { SocialAccountLinkError as e } from "./errors.js";
2
+ //#region src/linking.ts
3
+ var t = (t) => {
4
+ let { profile: n, linkedUserId: r, existingUserIdByEmail: i, policy: a } = t;
5
+ return r === null ? n.email === null ? {
6
+ kind: "refuse",
7
+ error: new e("no_email", `${n.provider} did not return an email address for this account. Add and verify an email with the provider, or sign in another way.`, 400)
8
+ } : i === null ? { kind: "create-user" } : a === "never" ? {
9
+ kind: "refuse",
10
+ error: new e("account_exists_unlinked", `An account already exists for ${n.email}. Sign in with your existing method, then connect ${n.provider} from your account settings.`)
11
+ } : n.emailVerified ? n.emailIsPrivateRelay ? {
12
+ kind: "refuse",
13
+ error: new e("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.")
14
+ } : {
15
+ kind: "link",
16
+ userId: i
17
+ } : {
18
+ kind: "refuse",
19
+ error: new e("email_not_verified", `${n.provider} did not verify ownership of ${n.email}, so it cannot be attached to the existing account with that address.`)
20
+ } : {
21
+ kind: "sign-in",
22
+ userId: r
23
+ };
24
+ };
25
+ //#endregion
26
+ export { t as decideSocialLink };
package/dist/login.js ADDED
@@ -0,0 +1,27 @@
1
+ import { randomToken as e, s256 as t } from "./pkce.js";
2
+ import { SocialConfigError as n } from "./errors.js";
3
+ import { socialProvider as r } from "./providers.js";
4
+ //#region src/login.ts
5
+ var i = (i) => {
6
+ if (!i.clientId) throw new n("socialBeginLogin: `clientId` is required");
7
+ if (!i.redirectUri) throw new n("socialBeginLogin: `redirectUri` is required");
8
+ let a = r(i.provider, i.endpoints ?? {}), o = i.randomness?.state ?? e(), s = i.randomness?.codeVerifier ?? e(), c = a.kind === "oidc" ? i.randomness?.nonce ?? e() : "", l = i.scopes ?? a.defaultScopes, u = new URLSearchParams({
9
+ client_id: i.clientId,
10
+ redirect_uri: i.redirectUri,
11
+ response_type: "code",
12
+ scope: l.join(" "),
13
+ state: o,
14
+ code_challenge: t(s),
15
+ code_challenge_method: "S256"
16
+ });
17
+ return c && u.set("nonce", c), a.responseMode === "form_post" && u.set("response_mode", "form_post"), i.prompt && u.set("prompt", i.prompt), i.loginHint && u.set(a.id === "github" ? "login" : "login_hint", i.loginHint), {
18
+ provider: a.id,
19
+ url: `${a.authorizeUrl}?${u.toString()}`,
20
+ state: o,
21
+ codeVerifier: s,
22
+ nonce: c,
23
+ responseMode: a.responseMode
24
+ };
25
+ };
26
+ //#endregion
27
+ export { i as socialBeginLogin };
package/dist/pkce.js ADDED
@@ -0,0 +1,5 @@
1
+ import { createHash as e, randomBytes as t } from "node:crypto";
2
+ //#region src/pkce.ts
3
+ var n = () => t(32).toString("base64url"), r = (t) => e("sha256").update(t, "ascii").digest().toString("base64url");
4
+ //#endregion
5
+ export { n as randomToken, r as s256 };
package/dist/plugin.js ADDED
@@ -0,0 +1,244 @@
1
+ import { SocialAuthError as e, SocialConfigError as t } from "./errors.js";
2
+ import { isSocialProviderId as n, socialProvider as r } from "./providers.js";
3
+ import { socialBeginLogin as i } from "./login.js";
4
+ import { socialCompleteLogin as a } from "./callback.js";
5
+ import { completeSocialSignIn as o } from "./signIn.js";
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";
10
+ //#region src/plugin.ts
11
+ var p = "/auth/social", m = 600, h = "voltro:oauth", g = {
12
+ google: [{
13
+ name: "VOLTRO_GOOGLE_CLIENT_ID",
14
+ secret: !1,
15
+ description: "Google OAuth client id (…apps.googleusercontent.com)."
16
+ }, {
17
+ name: "VOLTRO_GOOGLE_CLIENT_SECRET",
18
+ secret: !0,
19
+ description: "Google OAuth client secret."
20
+ }],
21
+ github: [{
22
+ name: "VOLTRO_GITHUB_CLIENT_ID",
23
+ secret: !1,
24
+ description: "GitHub OAuth app client id."
25
+ }, {
26
+ name: "VOLTRO_GITHUB_CLIENT_SECRET",
27
+ secret: !0,
28
+ description: "GitHub OAuth app client secret."
29
+ }],
30
+ apple: [
31
+ {
32
+ name: "VOLTRO_APPLE_CLIENT_ID",
33
+ secret: !1,
34
+ description: "Apple Services ID, e.g. com.example.web."
35
+ },
36
+ {
37
+ name: "VOLTRO_APPLE_TEAM_ID",
38
+ secret: !1,
39
+ description: "Apple Developer Team ID (10 chars)."
40
+ },
41
+ {
42
+ name: "VOLTRO_APPLE_KEY_ID",
43
+ secret: !1,
44
+ description: "Key ID of the Sign-in-with-Apple .p8 key."
45
+ },
46
+ {
47
+ name: "VOLTRO_APPLE_PRIVATE_KEY",
48
+ secret: !0,
49
+ description: "Contents of the .p8 file (PKCS#8 PEM). Apple signs no static secret — this key mints one per exchange."
50
+ }
51
+ ]
52
+ }, _ = (e, t) => ({
53
+ status: e,
54
+ body: JSON.stringify(t),
55
+ contentType: "application/json; charset=utf-8"
56
+ }), v = (e, t) => ({
57
+ status: 302,
58
+ body: "",
59
+ contentType: "text/plain; charset=utf-8",
60
+ headers: {
61
+ location: e,
62
+ ...t === void 0 ? {} : { "set-cookie": t }
63
+ }
64
+ }), y = (e) => Buffer.from(JSON.stringify(e), "utf8").toString("base64url"), b = (e) => {
65
+ if (!e) return null;
66
+ 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
73
+ };
74
+ } catch {
75
+ return null;
76
+ }
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 = {
86
+ id: e,
87
+ clientId: i,
88
+ redirectUri: a,
89
+ endpoints: n.endpoints ?? {},
90
+ ...n.scopes === void 0 ? {} : { scopes: n.scopes }
91
+ };
92
+ if (e === "apple") {
93
+ let e = n.appleKey ?? (r.VOLTRO_APPLE_TEAM_ID && r.VOLTRO_APPLE_KEY_ID && r.VOLTRO_APPLE_PRIVATE_KEY ? {
94
+ teamId: r.VOLTRO_APPLE_TEAM_ID,
95
+ keyId: r.VOLTRO_APPLE_KEY_ID,
96
+ privateKey: r.VOLTRO_APPLE_PRIVATE_KEY.replace(/\\n/g, "\n")
97
+ } : void 0);
98
+ 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
+ return {
100
+ ...o,
101
+ appleKey: e
102
+ };
103
+ }
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}`);
106
+ return {
107
+ ...o,
108
+ clientSecret: s
109
+ };
110
+ }, j = S.identities ?? null, M = (e) => {
111
+ if (j) return j;
112
+ let n = e.store;
113
+ 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 ? {
119
+ id: t,
120
+ isCallback: !1
121
+ } : r.length === 1 && r[0] === "callback" ? {
122
+ id: t,
123
+ isCallback: !0
124
+ } : 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({
138
+ provider: t,
139
+ clientId: n.clientId,
140
+ redirectUri: n.redirectUri,
141
+ ...n.scopes === void 0 ? {} : { scopes: n.scopes },
142
+ endpoints: n.endpoints,
143
+ ...new URLSearchParams(e.query).get("login_hint") ? { loginHint: new URLSearchParams(e.query).get("login_hint") } : {}
144
+ }), s = d(T, y({
145
+ p: t,
146
+ s: o.state,
147
+ v: o.codeVerifier,
148
+ n: o.nonce
149
+ }), {
150
+ maxAgeSeconds: w,
151
+ 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({
162
+ 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,
167
+ code: n.get("code") ?? "",
168
+ state: n.get("state") ?? "",
169
+ expectedState: s?.s ?? "",
170
+ codeVerifier: s?.v ?? "",
171
+ expectedNonce: s?.n ?? "",
172
+ ...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,
180
+ ipAddress: e.remoteAddr ?? null,
181
+ userAgent: e.headers["user-agent"] ?? null
182
+ }));
183
+ if (p.kind === "refused") return P(p.status, p.code, p.message);
184
+ let m = d(T, "", {
185
+ maxAgeSeconds: 0,
186
+ path: C,
187
+ secure: D
188
+ });
189
+ return {
190
+ status: 302,
191
+ body: "",
192
+ contentType: "text/plain; charset=utf-8",
193
+ headers: {
194
+ location: S.successRedirect ?? S.auth.successRedirect ?? "/",
195
+ "set-cookie": [m, p.setCookie]
196
+ }
197
+ };
198
+ }, L = {
199
+ method: "*",
200
+ path: C,
201
+ handle: async (t) => {
202
+ let n = N(t.path);
203
+ if (!n) return _(404, { error: "not_found" });
204
+ 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" });
206
+ } catch (t) {
207
+ if (t instanceof e) return P(t.status, t.code, t.message);
208
+ throw t;
209
+ }
210
+ }
211
+ }, R = [
212
+ "store:write",
213
+ "network:outbound:oauth2.googleapis.com",
214
+ "network:outbound:www.googleapis.com",
215
+ "network:outbound:github.com",
216
+ "network:outbound:api.github.com",
217
+ "network:outbound:appleid.apple.com"
218
+ ], z = O.flatMap((e) => g[e].map((t) => ({
219
+ name: t.name,
220
+ required: S.providers[e]?.clientId === void 0,
221
+ secret: t.secret,
222
+ 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,
229
+ extendSchema: { tables: c },
230
+ httpRoutes: [L],
231
+ onActivate: (e) => l.sync(() => {
232
+ for (let e of O) A(e);
233
+ e.logger.info("social auth active", {
234
+ prefix: C,
235
+ providers: O.join(","),
236
+ linkPolicy: E,
237
+ stateTtlSeconds: w
238
+ });
239
+ }),
240
+ onDeactivate: () => l.void
241
+ });
242
+ };
243
+ //#endregion
244
+ export { S as socialAuthPlugin };
@@ -0,0 +1,70 @@
1
+ import { SocialConfigError as e } from "./errors.js";
2
+ //#region src/providers.ts
3
+ var t = {
4
+ id: "google",
5
+ label: "Google",
6
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
7
+ tokenUrl: "https://oauth2.googleapis.com/token",
8
+ jwksUrl: "https://www.googleapis.com/oauth2/v3/certs",
9
+ issuer: "https://accounts.google.com",
10
+ apiBase: null,
11
+ defaultScopes: [
12
+ "openid",
13
+ "email",
14
+ "profile"
15
+ ],
16
+ kind: "oidc",
17
+ responseMode: "query",
18
+ assertsEmailVerification: !0,
19
+ pkceEnforced: !0
20
+ }, n = {
21
+ id: "github",
22
+ label: "GitHub",
23
+ authorizeUrl: "https://github.com/login/oauth/authorize",
24
+ tokenUrl: "https://github.com/login/oauth/access_token",
25
+ jwksUrl: null,
26
+ issuer: null,
27
+ apiBase: "https://api.github.com",
28
+ defaultScopes: ["read:user", "user:email"],
29
+ kind: "oauth2",
30
+ responseMode: "query",
31
+ assertsEmailVerification: !0,
32
+ pkceEnforced: !1
33
+ }, r = {
34
+ id: "apple",
35
+ label: "Apple",
36
+ authorizeUrl: "https://appleid.apple.com/auth/authorize",
37
+ tokenUrl: "https://appleid.apple.com/auth/token",
38
+ jwksUrl: "https://appleid.apple.com/auth/keys",
39
+ issuer: "https://appleid.apple.com",
40
+ apiBase: null,
41
+ defaultScopes: ["name", "email"],
42
+ kind: "oidc",
43
+ responseMode: "form_post",
44
+ assertsEmailVerification: !0,
45
+ pkceEnforced: !0
46
+ }, i = {
47
+ google: t,
48
+ github: n,
49
+ apple: r
50
+ }, a = [
51
+ t,
52
+ n,
53
+ r
54
+ ], o = [
55
+ "google",
56
+ "github",
57
+ "apple"
58
+ ], s = (e) => e === "google" || e === "github" || e === "apple", c = (t, n = {}) => {
59
+ if (!s(t)) throw new e(`unknown social provider '${t}' — expected one of ${o.join(", ")}`);
60
+ return {
61
+ ...i[t],
62
+ ...n.authorizeUrl === void 0 ? {} : { authorizeUrl: n.authorizeUrl },
63
+ ...n.tokenUrl === void 0 ? {} : { tokenUrl: n.tokenUrl },
64
+ ...n.jwksUrl === void 0 ? {} : { jwksUrl: n.jwksUrl },
65
+ ...n.issuer === void 0 ? {} : { issuer: n.issuer },
66
+ ...n.apiBase === void 0 ? {} : { apiBase: n.apiBase }
67
+ };
68
+ };
69
+ //#endregion
70
+ export { a as SOCIAL_PROVIDERS, o as SOCIAL_PROVIDER_IDS, s as isSocialProviderId, c as socialProvider };
package/dist/signIn.js ADDED
@@ -0,0 +1,67 @@
1
+ import { SocialAccountLinkError as e } from "./errors.js";
2
+ import { decideSocialLink as t } from "./linking.js";
3
+ import { Effect as n } from "effect";
4
+ import { issueUserSession as r, newAuthId as i, runSubjectGuards as a } from "@voltro/plugin-auth";
5
+ //#region src/signIn.ts
6
+ var o = (e) => ({
7
+ kind: "refused",
8
+ status: e.status,
9
+ code: e.code,
10
+ message: e.message
11
+ }), s = (s) => n.gen(function* () {
12
+ let { profile: c, identities: l, users: u, config: d } = s, f = s.policy ?? "never", p = yield* l.findByProviderAccount(c.provider, c.providerAccountId), m = c.email === null ? null : yield* u.findByEmail(c.email), h = t({
13
+ profile: c,
14
+ linkedUserId: p?.userId ?? null,
15
+ existingUserIdByEmail: m?.id ?? null,
16
+ policy: f
17
+ });
18
+ if (h.kind === "refuse") return o(h.error);
19
+ let g = null, _ = !1, v = !1;
20
+ if (h.kind === "create-user") {
21
+ let t = c.email, r = yield* u.insert({
22
+ id: i("user"),
23
+ email: t,
24
+ passwordHash: null,
25
+ tenantId: d.defaultTenantId
26
+ }).pipe(n.catchTag("UserAlreadyExistsError", (t) => n.succeed(new e("account_exists_unlinked", `An account already exists for ${t.email}. Sign in with your existing method, then connect ${c.provider} from your account settings.`))));
27
+ if (r instanceof e) return o(r);
28
+ g = r, _ = !0;
29
+ } else {
30
+ if (v = h.kind === "link", g = yield* u.findById(h.userId), !g) return o(new e("account_exists_unlinked", "The account this login is linked to no longer exists.", 409));
31
+ let t = d.subjectGuards ?? [];
32
+ if (t.length > 0) {
33
+ let e = yield* a(t, g);
34
+ if (!e.ok) return {
35
+ kind: "refused",
36
+ status: 403,
37
+ code: e.code,
38
+ message: e.message
39
+ };
40
+ }
41
+ }
42
+ p === null ? yield* l.insert({
43
+ userId: g.id,
44
+ provider: c.provider,
45
+ providerAccountId: c.providerAccountId,
46
+ email: c.email,
47
+ emailVerified: c.emailVerified
48
+ }) : yield* l.touch(p.id, /* @__PURE__ */ new Date());
49
+ let y = yield* r(g, u, d, {
50
+ ipAddress: s.ipAddress ?? null,
51
+ userAgent: s.userAgent ?? null
52
+ });
53
+ return {
54
+ kind: "ok",
55
+ userId: g.id,
56
+ subject: y.subject,
57
+ setCookie: y.setCookie,
58
+ created: _,
59
+ linked: v,
60
+ oneTimeName: c.nameIsFirstAuthorizationOnly ? {
61
+ firstName: c.firstName,
62
+ lastName: c.lastName
63
+ } : null
64
+ };
65
+ });
66
+ //#endregion
67
+ export { s as completeSocialSignIn };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-auth-social",
3
- "version": "0.84.0",
3
+ "version": "0.86.0",
4
4
  "description": "First-party social login for the Voltro framework — Sign in with Google, GitHub and Apple. Mandatory PKCE (S256) + state, JWKS-verified ID tokens, a deliberate account-linking policy, and sessions issued through @voltro/plugin-auth. No identity vendor required.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -33,12 +33,12 @@
33
33
  "node": ">=24.0.0"
34
34
  },
35
35
  "dependencies": {
36
- "@voltro/database": "0.84.0",
37
- "@voltro/plugin-auth": "0.84.0",
38
- "@voltro/protocol": "0.84.0"
36
+ "@voltro/database": "0.86.0",
37
+ "@voltro/plugin-auth": "0.86.0",
38
+ "@voltro/protocol": "0.86.0"
39
39
  },
40
40
  "peerDependencies": {
41
- "effect": "^3.22.2"
41
+ "effect": "4.0.1"
42
42
  },
43
43
  "publishConfig": {
44
44
  "access": "public"