@voltro/plugin-auth-social 0.85.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/CHANGELOG.md +5987 -0
- package/README.md +8 -1
- package/THIRD-PARTY-NOTICES.md +1122 -31
- package/dist/apple.js +64 -0
- package/dist/callback.js +129 -0
- package/dist/discord.js +38 -0
- package/dist/epic.js +39 -0
- package/dist/errors.js +34 -0
- package/dist/github.js +37 -0
- package/dist/guestHandoff.js +61 -0
- package/dist/guestUpgrade.js +97 -0
- package/dist/identityStore.js +124 -0
- package/dist/index.d.ts +292 -16
- package/dist/index.js +16 -721
- package/dist/linking.js +23 -0
- package/dist/login.js +32 -0
- package/dist/nativeGuestUpgrade.js +66 -0
- package/dist/pkce.js +5 -0
- package/dist/platform.js +115 -0
- package/dist/plugin.js +741 -0
- package/dist/providers.js +130 -0
- package/dist/signIn.js +84 -0
- package/dist/steam.js +152 -0
- package/dist/web/guestUpgradeClient.js +109 -0
- package/dist/web.d.ts +54 -0
- package/dist/web.js +1 -0
- package/package.json +11 -5
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
id: "discord",
|
|
48
|
+
label: "Discord",
|
|
49
|
+
authorizeUrl: "https://discord.com/oauth2/authorize",
|
|
50
|
+
tokenUrl: "https://discord.com/api/oauth2/token",
|
|
51
|
+
jwksUrl: null,
|
|
52
|
+
issuer: null,
|
|
53
|
+
apiBase: "https://discord.com/api/v10",
|
|
54
|
+
defaultScopes: ["identify", "email"],
|
|
55
|
+
kind: "oauth2",
|
|
56
|
+
responseMode: "query",
|
|
57
|
+
assertsEmailVerification: !0,
|
|
58
|
+
pkceEnforced: !1
|
|
59
|
+
}, a = {
|
|
60
|
+
id: "twitch",
|
|
61
|
+
label: "Twitch",
|
|
62
|
+
authorizeUrl: "https://id.twitch.tv/oauth2/authorize",
|
|
63
|
+
tokenUrl: "https://id.twitch.tv/oauth2/token",
|
|
64
|
+
jwksUrl: "https://id.twitch.tv/oauth2/keys",
|
|
65
|
+
issuer: "https://id.twitch.tv/oauth2",
|
|
66
|
+
apiBase: null,
|
|
67
|
+
defaultScopes: ["openid", "user:read:email"],
|
|
68
|
+
kind: "oidc",
|
|
69
|
+
responseMode: "query",
|
|
70
|
+
assertsEmailVerification: !0,
|
|
71
|
+
pkceEnforced: !1
|
|
72
|
+
}, o = {
|
|
73
|
+
id: "microsoft",
|
|
74
|
+
label: "Microsoft",
|
|
75
|
+
authorizeUrl: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize",
|
|
76
|
+
tokenUrl: "https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
|
|
77
|
+
jwksUrl: "https://login.microsoftonline.com/consumers/discovery/v2.0/keys",
|
|
78
|
+
issuer: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0",
|
|
79
|
+
apiBase: null,
|
|
80
|
+
defaultScopes: [
|
|
81
|
+
"openid",
|
|
82
|
+
"email",
|
|
83
|
+
"profile"
|
|
84
|
+
],
|
|
85
|
+
kind: "oidc",
|
|
86
|
+
responseMode: "query",
|
|
87
|
+
assertsEmailVerification: !1,
|
|
88
|
+
pkceEnforced: !0
|
|
89
|
+
}, s = {
|
|
90
|
+
id: "epic",
|
|
91
|
+
label: "Epic Games",
|
|
92
|
+
authorizeUrl: "https://www.epicgames.com/id/authorize",
|
|
93
|
+
tokenUrl: "https://api.epicgames.dev/epic/oauth/v2/token",
|
|
94
|
+
jwksUrl: null,
|
|
95
|
+
issuer: null,
|
|
96
|
+
apiBase: "https://api.epicgames.dev/epic/oauth/v2",
|
|
97
|
+
defaultScopes: ["basic_profile"],
|
|
98
|
+
kind: "oauth2",
|
|
99
|
+
responseMode: "query",
|
|
100
|
+
assertsEmailVerification: !1,
|
|
101
|
+
pkceEnforced: !1
|
|
102
|
+
}, c = {
|
|
103
|
+
google: t,
|
|
104
|
+
github: n,
|
|
105
|
+
apple: r,
|
|
106
|
+
discord: i,
|
|
107
|
+
twitch: a,
|
|
108
|
+
microsoft: o,
|
|
109
|
+
epic: s
|
|
110
|
+
}, l = [
|
|
111
|
+
t,
|
|
112
|
+
n,
|
|
113
|
+
r,
|
|
114
|
+
i,
|
|
115
|
+
a,
|
|
116
|
+
o,
|
|
117
|
+
s
|
|
118
|
+
], u = l.map((e) => e.id), d = (e) => Object.hasOwn(c, e), f = (t, n = {}) => {
|
|
119
|
+
if (!d(t)) throw new e(`unknown social provider '${t}' — expected one of ${u.join(", ")}`);
|
|
120
|
+
return {
|
|
121
|
+
...c[t],
|
|
122
|
+
...n.authorizeUrl === void 0 ? {} : { authorizeUrl: n.authorizeUrl },
|
|
123
|
+
...n.tokenUrl === void 0 ? {} : { tokenUrl: n.tokenUrl },
|
|
124
|
+
...n.jwksUrl === void 0 ? {} : { jwksUrl: n.jwksUrl },
|
|
125
|
+
...n.issuer === void 0 ? {} : { issuer: n.issuer },
|
|
126
|
+
...n.apiBase === void 0 ? {} : { apiBase: n.apiBase }
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
//#endregion
|
|
130
|
+
export { l as SOCIAL_PROVIDERS, u as SOCIAL_PROVIDER_IDS, d as isSocialProviderId, f as socialProvider };
|
package/dist/signIn.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
if (p === null) {
|
|
43
|
+
if (!(yield* l.insert({
|
|
44
|
+
userId: g.id,
|
|
45
|
+
provider: c.provider,
|
|
46
|
+
providerAccountId: c.providerAccountId,
|
|
47
|
+
email: c.email,
|
|
48
|
+
emailVerified: c.emailVerified
|
|
49
|
+
}).pipe(n.as(!0), n.catchTag("SocialIdentityConflict", () => n.succeed(!1))))) {
|
|
50
|
+
let t = yield* l.findByProviderAccount(c.provider, c.providerAccountId), n = t === null ? null : yield* u.findById(t.userId);
|
|
51
|
+
if (n === null) return o(new e("account_exists_unlinked", "The account this login is linked to no longer exists.", 409));
|
|
52
|
+
let r = yield* a(d.subjectGuards ?? [], n);
|
|
53
|
+
if (!r.ok) return {
|
|
54
|
+
kind: "refused",
|
|
55
|
+
status: 403,
|
|
56
|
+
code: r.code,
|
|
57
|
+
message: r.message
|
|
58
|
+
};
|
|
59
|
+
g = n, _ = !1, v = !1;
|
|
60
|
+
}
|
|
61
|
+
} else yield* l.touch(p.id, /* @__PURE__ */ new Date());
|
|
62
|
+
let y = yield* r(g, u, d, {
|
|
63
|
+
ipAddress: s.ipAddress ?? null,
|
|
64
|
+
userAgent: s.userAgent ?? null
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
kind: "ok",
|
|
68
|
+
userId: g.id,
|
|
69
|
+
subject: y.subject,
|
|
70
|
+
setCookie: y.setCookie,
|
|
71
|
+
session: {
|
|
72
|
+
value: y.value,
|
|
73
|
+
expiresAt: y.expiresAt
|
|
74
|
+
},
|
|
75
|
+
created: _,
|
|
76
|
+
linked: v,
|
|
77
|
+
oneTimeName: c.nameIsFirstAuthorizationOnly ? {
|
|
78
|
+
firstName: c.firstName,
|
|
79
|
+
lastName: c.lastName
|
|
80
|
+
} : null
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
//#endregion
|
|
84
|
+
export { s as completeSocialSignIn };
|
package/dist/steam.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { randomToken as e } from "./pkce.js";
|
|
2
|
+
import { PlatformTicketError as t, SocialAuthError as n, SocialConfigError as r, SocialExchangeError as i, SocialStateMismatchError as a } from "./errors.js";
|
|
3
|
+
import { Effect as o } from "effect";
|
|
4
|
+
import { timingSafeStringEqual as s } from "@voltro/protocol/session";
|
|
5
|
+
import { createHash as c } from "node:crypto";
|
|
6
|
+
//#region src/steam.ts
|
|
7
|
+
var l = "http://specs.openid.net/auth/2.0", u = "https://steamcommunity.com/openid/", d = `${l}/identifier_select`, f = (e) => typeof e == "string" && /^[0-9]{17,20}$/.test(e) && BigInt(e) > 0n && BigInt(e) <= 18446744073709551615n, p = 65536, m = 5e3, h = async (e) => {
|
|
8
|
+
let t = e.body?.getReader();
|
|
9
|
+
if (!t) return "";
|
|
10
|
+
let n = [], r = 0;
|
|
11
|
+
for (;;) {
|
|
12
|
+
let { done: e, value: i } = await t.read();
|
|
13
|
+
if (e) break;
|
|
14
|
+
if (r += i.byteLength, r > p) return await t.cancel().catch(() => void 0), null;
|
|
15
|
+
n.push(i);
|
|
16
|
+
}
|
|
17
|
+
return new TextDecoder().decode(Buffer.concat(n));
|
|
18
|
+
}, g = (e) => ({
|
|
19
|
+
provider: "steam",
|
|
20
|
+
providerAccountId: e,
|
|
21
|
+
email: null,
|
|
22
|
+
emailVerified: !1,
|
|
23
|
+
emailIsPrivateRelay: !1,
|
|
24
|
+
name: null,
|
|
25
|
+
firstName: null,
|
|
26
|
+
lastName: null,
|
|
27
|
+
avatarUrl: null,
|
|
28
|
+
nameIsFirstAuthorizationOnly: !1,
|
|
29
|
+
raw: { steamId: e }
|
|
30
|
+
}), _ = (e) => {
|
|
31
|
+
let t;
|
|
32
|
+
try {
|
|
33
|
+
t = new URL(e);
|
|
34
|
+
} catch {
|
|
35
|
+
throw new r("steam: invalid absolute callback URL");
|
|
36
|
+
}
|
|
37
|
+
if (!["https:", "http:"].includes(t.protocol) || t.username || t.password || t.hash) throw new r("steam: callback URL requires HTTP(S), without credentials or fragments");
|
|
38
|
+
return t;
|
|
39
|
+
}, v = (t) => {
|
|
40
|
+
let n = _(t.returnTo);
|
|
41
|
+
if (n.searchParams.has("state")) throw new r("steam: state must be server minted");
|
|
42
|
+
let i = _(t.realm ?? `${n.origin}/`);
|
|
43
|
+
if (i.search || i.origin !== n.origin || !(n.pathname === i.pathname || n.pathname.startsWith(i.pathname.endsWith("/") ? i.pathname : `${i.pathname}/`))) throw new r("steam: callback is outside the configured realm");
|
|
44
|
+
let a = e();
|
|
45
|
+
return n.searchParams.set("state", a), {
|
|
46
|
+
url: `${u}?${new URLSearchParams({
|
|
47
|
+
"openid.ns": l,
|
|
48
|
+
"openid.mode": "checkid_setup",
|
|
49
|
+
"openid.return_to": n.href,
|
|
50
|
+
"openid.realm": i.href,
|
|
51
|
+
"openid.identity": d,
|
|
52
|
+
"openid.claimed_id": d
|
|
53
|
+
})}`,
|
|
54
|
+
state: a,
|
|
55
|
+
returnTo: n.href,
|
|
56
|
+
realm: i.href
|
|
57
|
+
};
|
|
58
|
+
}, y = (e) => o.gen(function* () {
|
|
59
|
+
let t = yield* o.tryPromise({
|
|
60
|
+
try: async () => {
|
|
61
|
+
let t = e.maxNonceAgeMs ?? 3e5, n = e.clockSkewMs ?? 3e4;
|
|
62
|
+
if (!e.namespace || !Number.isSafeInteger(t) || t < 1 || t > 36e5 || !Number.isSafeInteger(n) || n < 0 || n > 3e5 || e.multiNode === !0 && !e.hotStore.capabilities.crossNode) throw new r("steam: invalid replay owner, lifetime or multi-node hot store");
|
|
63
|
+
if (e.callbackUrl.length > 16384) throw new a("steam: callback exceeds the admission budget");
|
|
64
|
+
let o = _(e.callbackUrl), c = _(e.expectedReturnTo), d = o.searchParams, p = /* @__PURE__ */ new Set();
|
|
65
|
+
for (let e of d.keys()) {
|
|
66
|
+
if (p.has(e)) throw new a("steam: duplicate callback parameter");
|
|
67
|
+
p.add(e);
|
|
68
|
+
}
|
|
69
|
+
if (!e.expectedState || d.get("state") === null || !s(d.get("state"), e.expectedState) || c.searchParams.get("state") !== e.expectedState || o.origin !== c.origin || o.pathname !== c.pathname) throw new a("steam: state or callback location mismatch");
|
|
70
|
+
for (let [e, t] of c.searchParams) if (d.get(e) !== t) throw new a("steam: callback query binding mismatch");
|
|
71
|
+
if (d.get("openid.ns") !== l || d.get("openid.mode") !== "id_res" || d.get("openid.op_endpoint") !== u || d.get("openid.return_to") !== e.expectedReturnTo) throw new a("steam: invalid OpenID provider or return binding");
|
|
72
|
+
let g = d.get("openid.claimed_id") ?? "", v = /^https?:\/\/steamcommunity\.com\/openid\/id\/([0-9]{17,20})$/.exec(g);
|
|
73
|
+
if (!v || !f(v[1]) || d.get("openid.identity") !== g) throw new a("steam: invalid Steam claimed identity");
|
|
74
|
+
let y = new Set((d.get("openid.signed") ?? "").split(","));
|
|
75
|
+
if ([
|
|
76
|
+
"op_endpoint",
|
|
77
|
+
"return_to",
|
|
78
|
+
"response_nonce",
|
|
79
|
+
"assoc_handle",
|
|
80
|
+
"claimed_id",
|
|
81
|
+
"identity"
|
|
82
|
+
].some((e) => !y.has(e)) || !d.get("openid.assoc_handle") || !d.get("openid.sig")) throw new a("steam: assertion is missing required signed fields");
|
|
83
|
+
let b = d.get("openid.response_nonce") ?? "";
|
|
84
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z.+$/.test(b) || b.length > 255) throw new a("steam: malformed response nonce");
|
|
85
|
+
let x = Date.parse(b.slice(0, 20)), S = (e.now ?? Date.now)();
|
|
86
|
+
if (!Number.isFinite(S) || !Number.isFinite(x) || new Date(x).toISOString().replace(".000Z", "Z") !== b.slice(0, 20) || x > S + n || x < S - t) throw new a("steam: response nonce outside the admission lifetime");
|
|
87
|
+
let C = new URLSearchParams();
|
|
88
|
+
for (let [e, t] of d) e.startsWith("openid.") && C.set(e, t);
|
|
89
|
+
C.set("openid.mode", "check_authentication");
|
|
90
|
+
let w;
|
|
91
|
+
try {
|
|
92
|
+
w = await (e.fetchImpl ?? fetch)(e.verificationUrl ?? u, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
95
|
+
body: C.toString(),
|
|
96
|
+
redirect: "error",
|
|
97
|
+
signal: AbortSignal.timeout(e.timeoutMs ?? m)
|
|
98
|
+
});
|
|
99
|
+
} catch {
|
|
100
|
+
throw new i("exchange_failed", "steam: OpenID verification request failed");
|
|
101
|
+
}
|
|
102
|
+
if (!w.ok) throw new i("exchange_failed", "steam: OpenID verification refused");
|
|
103
|
+
let T = await h(w).catch(() => null);
|
|
104
|
+
if (T === null || T.length > 8192) throw new i("exchange_failed", "steam: malformed OpenID verification response");
|
|
105
|
+
let E = /* @__PURE__ */ new Map();
|
|
106
|
+
for (let e of T.split("\n")) {
|
|
107
|
+
if (!e) continue;
|
|
108
|
+
let t = e.indexOf(":"), n = e.slice(0, t);
|
|
109
|
+
if (t < 1 || E.has(n)) throw new i("exchange_failed", "steam: malformed OpenID verification response");
|
|
110
|
+
E.set(n, e.slice(t + 1).replace(/\r$/, ""));
|
|
111
|
+
}
|
|
112
|
+
if (E.get("ns") !== l || E.get("is_valid") !== "true") throw new i("exchange_failed", "steam: OpenID assertion was not verified");
|
|
113
|
+
return {
|
|
114
|
+
value: b,
|
|
115
|
+
accountId: v[1],
|
|
116
|
+
ttlMs: t + n
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
catch: (e) => e instanceof n ? e : new i("exchange_failed", "steam: OpenID verification failed")
|
|
120
|
+
}), d = `${e.namespace}:steam:nonce:${c("sha256").update(JSON.stringify([u, t.value])).digest("hex")}`;
|
|
121
|
+
return (yield* e.hotStore.setNx(d, "consumed", t.ttlMs).pipe(o.catch(() => o.fail(new i("exchange_failed", "steam: replay admission unavailable"))), o.catchDefect(() => o.fail(new i("exchange_failed", "steam: replay admission unavailable"))))) ? g(t.accountId) : yield* o.fail(new a("steam: assertion already consumed"));
|
|
122
|
+
}), b = async (e) => {
|
|
123
|
+
if (!Number.isSafeInteger(e.appId) || e.appId < 1 || e.appId > 4294967295 || !e.identity || e.identity.length > 255 || !e.publisherKey) throw new r("steam: appId, ticket identity and publisher key are required");
|
|
124
|
+
if (!/^(?:[a-fA-F0-9]{2})+$/.test(e.ticket) || e.ticket.length > 16384) throw new t("steam: malformed Web API ticket");
|
|
125
|
+
let a = new URL(e.verificationUrl ?? "https://partner.steam-api.com/ISteamUserAuth/AuthenticateUserTicket/v1/");
|
|
126
|
+
a.searchParams.set("key", e.publisherKey), a.searchParams.set("appid", String(e.appId)), a.searchParams.set("ticket", e.ticket), a.searchParams.set("identity", e.identity);
|
|
127
|
+
let o;
|
|
128
|
+
try {
|
|
129
|
+
o = await (e.fetchImpl ?? fetch)(a.href, {
|
|
130
|
+
redirect: "error",
|
|
131
|
+
headers: { accept: "application/json" },
|
|
132
|
+
signal: AbortSignal.timeout(e.timeoutMs ?? m)
|
|
133
|
+
});
|
|
134
|
+
} catch {
|
|
135
|
+
throw new i("exchange_failed", "steam: ticket verification request failed");
|
|
136
|
+
}
|
|
137
|
+
if (!o.ok) throw new i("exchange_failed", "steam: ticket verification refused");
|
|
138
|
+
let s;
|
|
139
|
+
try {
|
|
140
|
+
let e = await h(o);
|
|
141
|
+
if (e === null) throw Error("oversized");
|
|
142
|
+
s = JSON.parse(e);
|
|
143
|
+
} catch {
|
|
144
|
+
throw new i("malformed_token_response", "steam: malformed ticket verification response");
|
|
145
|
+
}
|
|
146
|
+
let c = typeof s == "object" && s ? s : {}, l = typeof c.response == "object" && c.response !== null ? c.response : {}, u = typeof l.params == "object" && l.params !== null ? l.params : {};
|
|
147
|
+
if (u.result !== "OK" || !f(u.steamid)) throw new t("steam: invalid Web API ticket");
|
|
148
|
+
if (u.publisherbanned === !0 && e.allowPublisherBanned !== !0) throw new n(403, "guard_rejected", "steam: the publisher banned this account");
|
|
149
|
+
return g(u.steamid);
|
|
150
|
+
};
|
|
151
|
+
//#endregion
|
|
152
|
+
export { v as steamBeginOpenId, y as steamCompleteOpenId, b as verifySteamWebApiTicket };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { isSocialProviderId as e } from "../providers.js";
|
|
2
|
+
import { parseGuestCredential as t } from "@voltro/plugin-auth/guest";
|
|
3
|
+
//#region src/web/guestUpgradeClient.ts
|
|
4
|
+
var n = class extends Error {
|
|
5
|
+
code;
|
|
6
|
+
status;
|
|
7
|
+
constructor(e, t) {
|
|
8
|
+
super("Guest upgrade: " + e), this.code = e, this.status = t, this.name = "GuestUpgradeClientError";
|
|
9
|
+
}
|
|
10
|
+
}, r = (e) => typeof e == "object" && e && !Array.isArray(e) ? e : null, i = (i) => {
|
|
11
|
+
let a = new URL(i.apiBaseUrl), o = i.prefix ?? "/auth/social";
|
|
12
|
+
if (!/^https?:$/.test(a.protocol) || a.username || a.password || a.search || a.hash || !/^\/[A-Za-z0-9/_-]+$/.test(o) || o.endsWith("/")) throw new n("invalid_response");
|
|
13
|
+
let s = i.apiBaseUrl.replace(/\/+$/, "") + o + "/upgrade", c = i.fetch ?? globalThis.fetch, l = async (e, t) => {
|
|
14
|
+
try {
|
|
15
|
+
let i = {};
|
|
16
|
+
if (t !== void 0) {
|
|
17
|
+
let e = await c(s + "/csrf", {
|
|
18
|
+
credentials: "include",
|
|
19
|
+
cache: "no-store",
|
|
20
|
+
redirect: "error"
|
|
21
|
+
});
|
|
22
|
+
if (!e.ok) throw new n("transport_failed", e.status);
|
|
23
|
+
let t = r(await e.json());
|
|
24
|
+
if (typeof t?.token != "string" || !t.token) throw new n("invalid_response");
|
|
25
|
+
i["x-csrf-token"] = t.token, i["content-type"] = "application/json";
|
|
26
|
+
}
|
|
27
|
+
let a = await c(s + e, {
|
|
28
|
+
credentials: "include",
|
|
29
|
+
cache: "no-store",
|
|
30
|
+
redirect: "error",
|
|
31
|
+
...t === void 0 ? {} : {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: i,
|
|
34
|
+
body: JSON.stringify(t)
|
|
35
|
+
}
|
|
36
|
+
}), o = r(await a.json());
|
|
37
|
+
if (!a.ok) {
|
|
38
|
+
let e = o?.error;
|
|
39
|
+
throw new n(e === "account_merge_required" || e === "credential_rejected" || e === "upgrade_handoff_rejected" ? e : "transport_failed", a.status);
|
|
40
|
+
}
|
|
41
|
+
if (!o) throw new n("invalid_response");
|
|
42
|
+
return o;
|
|
43
|
+
} catch (e) {
|
|
44
|
+
throw e instanceof n ? e : new n("transport_failed");
|
|
45
|
+
}
|
|
46
|
+
}, u = async (e) => {
|
|
47
|
+
try {
|
|
48
|
+
return await e();
|
|
49
|
+
} catch (e) {
|
|
50
|
+
throw e instanceof n ? e : new n("storage_unavailable");
|
|
51
|
+
}
|
|
52
|
+
}, d = async () => {
|
|
53
|
+
let e = await u(() => i.storage.read());
|
|
54
|
+
if (e === null || !t(e)) throw new n("credential_rejected");
|
|
55
|
+
return e;
|
|
56
|
+
}, f = (e, t) => {
|
|
57
|
+
let i = r(e.subject);
|
|
58
|
+
if (e.upgraded !== !0 || i?.type !== "user" || i.kind === "guest" || i.id !== t || typeof i.tenantId != "string" || !i.tenantId.trim()) throw new n("invalid_response");
|
|
59
|
+
return {
|
|
60
|
+
upgraded: !0,
|
|
61
|
+
subject: {
|
|
62
|
+
type: "user",
|
|
63
|
+
id: t,
|
|
64
|
+
tenantId: i.tenantId
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
begin: (t) => u(() => i.storage.exclusive(async () => {
|
|
70
|
+
if (!e(t)) throw new n("invalid_response");
|
|
71
|
+
await d();
|
|
72
|
+
let r = await l("/" + t, {});
|
|
73
|
+
if (typeof r.authorizationUrl != "string") throw new n("invalid_response");
|
|
74
|
+
let i;
|
|
75
|
+
try {
|
|
76
|
+
i = new URL(r.authorizationUrl);
|
|
77
|
+
} catch {
|
|
78
|
+
throw new n("invalid_response");
|
|
79
|
+
}
|
|
80
|
+
if (!/^https?:$/.test(i.protocol) || i.username || i.password || i.hash) throw new n("invalid_response");
|
|
81
|
+
return i.toString();
|
|
82
|
+
})),
|
|
83
|
+
pending: async () => {
|
|
84
|
+
let t = await l("/pending");
|
|
85
|
+
if (typeof t.guestId != "string" || !/^guest_[A-Za-z0-9_-]+$/.test(t.guestId) || typeof t.provider != "string" || !e(t.provider) || t.email !== null && typeof t.email != "string" || typeof t.emailVerified != "boolean" || typeof t.expiresAt != "number" || !Number.isSafeInteger(t.expiresAt) || t.expiresAt <= Date.now()) throw new n("invalid_response");
|
|
86
|
+
return {
|
|
87
|
+
guestId: t.guestId,
|
|
88
|
+
provider: t.provider,
|
|
89
|
+
email: t.email,
|
|
90
|
+
emailVerified: t.emailVerified,
|
|
91
|
+
expiresAt: t.expiresAt
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
confirm: (e) => u(() => i.storage.exclusive(async () => {
|
|
95
|
+
if (e.confirmed !== !0) throw new n("credential_rejected");
|
|
96
|
+
let r = await d(), a = t(r), o = await l("/complete", {
|
|
97
|
+
credential: r,
|
|
98
|
+
confirmed: !0
|
|
99
|
+
}), s = f(o, a.guestId);
|
|
100
|
+
return await u(() => i.storage.remove()), s;
|
|
101
|
+
})),
|
|
102
|
+
recover: () => u(() => i.storage.exclusive(async () => {
|
|
103
|
+
let e = await d(), n = f(await l("/receipt"), t(e).guestId);
|
|
104
|
+
return await u(() => i.storage.remove()), n;
|
|
105
|
+
}))
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
//#endregion
|
|
109
|
+
export { n as GuestUpgradeClientError, i as createGuestUpgradeClient };
|
package/dist/web.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { GuestCredentialStore } from '@voltro/plugin-auth/guest';
|
|
2
|
+
|
|
3
|
+
/** Browser cookie presentation only. Device proofs stay in durable storage and
|
|
4
|
+
* in the confirmed JSON POST, never navigation URLs or login-state cookies. */
|
|
5
|
+
export declare const createGuestUpgradeClient: (options: GuestUpgradeClientOptions) => {
|
|
6
|
+
/** Call after an authenticated guest session exists, then navigate to the
|
|
7
|
+
* returned authorization URL in this browser. Does not send device proof. */
|
|
8
|
+
begin: (provider: SocialProviderId) => Promise<string>;
|
|
9
|
+
pending: () => Promise<GuestUpgradePending>;
|
|
10
|
+
/** Call only after explicit confirmation. Storage remains untouched on an
|
|
11
|
+
* uncertain/refused response; no replacement guest identity is created. */
|
|
12
|
+
confirm: (confirmation: {
|
|
13
|
+
readonly confirmed: true;
|
|
14
|
+
}) => Promise<GuestUpgradeCompletion>;
|
|
15
|
+
/** After uncertain delivery, prove a live registered session for the exact
|
|
16
|
+
* stored guest ID. If its cookie was lost too, sign in with the linked
|
|
17
|
+
* provider first. Refusal never erases storage or creates another guest. */
|
|
18
|
+
recover: () => Promise<GuestUpgradeCompletion>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export declare class GuestUpgradeClientError extends Error {
|
|
22
|
+
readonly code: 'storage_unavailable' | 'credential_rejected' | 'account_merge_required' | 'transport_failed' | 'invalid_response' | 'upgrade_handoff_rejected';
|
|
23
|
+
readonly status?: number | undefined;
|
|
24
|
+
constructor(code: 'storage_unavailable' | 'credential_rejected' | 'account_merge_required' | 'transport_failed' | 'invalid_response' | 'upgrade_handoff_rejected', status?: number | undefined);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export declare interface GuestUpgradeClientOptions {
|
|
28
|
+
readonly apiBaseUrl: string;
|
|
29
|
+
readonly storage: GuestCredentialStore;
|
|
30
|
+
readonly prefix?: string;
|
|
31
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export declare interface GuestUpgradeCompletion {
|
|
35
|
+
readonly upgraded: true;
|
|
36
|
+
readonly subject: {
|
|
37
|
+
readonly type: 'user';
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly tenantId: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export declare interface GuestUpgradePending {
|
|
44
|
+
readonly guestId: string;
|
|
45
|
+
readonly provider: SocialProviderId;
|
|
46
|
+
readonly email: string | null;
|
|
47
|
+
readonly emailVerified: boolean;
|
|
48
|
+
readonly expiresAt: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The providers this package ships. */
|
|
52
|
+
declare type SocialProviderId = 'google' | 'github' | 'apple' | 'discord' | 'twitch' | 'microsoft' | 'epic';
|
|
53
|
+
|
|
54
|
+
export { }
|
package/dist/web.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GuestUpgradeClientError, createGuestUpgradeClient } from "./web/guestUpgradeClient.js";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-auth-social",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "First-party social login
|
|
3
|
+
"version": "0.87.0",
|
|
4
|
+
"description": "First-party social login with state, S256, provider identity verification, explicit account linking and shared Auth sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
7
7
|
"typescript",
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
"import": "./dist/index.js",
|
|
24
24
|
"default": "./dist/index.js"
|
|
25
25
|
},
|
|
26
|
+
"./web": {
|
|
27
|
+
"types": "./dist/web.d.ts",
|
|
28
|
+
"import": "./dist/web.js",
|
|
29
|
+
"default": "./dist/web.js"
|
|
30
|
+
},
|
|
26
31
|
"./package.json": "./package.json"
|
|
27
32
|
},
|
|
28
33
|
"main": "./dist/index.js",
|
|
@@ -33,9 +38,10 @@
|
|
|
33
38
|
"node": ">=24.0.0"
|
|
34
39
|
},
|
|
35
40
|
"dependencies": {
|
|
36
|
-
"@voltro/database": "0.
|
|
37
|
-
"@voltro/plugin-auth": "0.
|
|
38
|
-
"@voltro/protocol": "0.
|
|
41
|
+
"@voltro/database": "0.87.0",
|
|
42
|
+
"@voltro/plugin-auth": "0.87.0",
|
|
43
|
+
"@voltro/protocol": "0.87.0",
|
|
44
|
+
"@voltro/kv": "0.87.0"
|
|
39
45
|
},
|
|
40
46
|
"peerDependencies": {
|
|
41
47
|
"effect": "4.0.1"
|