scorezilla 0.3.0-next.2 → 0.3.0-next.3
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/API.md +48 -0
- package/CHANGELOG.md +30 -0
- package/README.md +54 -0
- package/RECIPES.md +41 -0
- package/dist/{errors-CtXMAHtJ.d.cts → errors-CWTmormh.d.cts} +1 -1
- package/dist/{errors-CtXMAHtJ.d.ts → errors-CWTmormh.d.ts} +1 -1
- package/dist/identity.cjs +116 -4
- package/dist/identity.cjs.map +1 -1
- package/dist/identity.d.cts +53 -20
- package/dist/identity.d.ts +53 -20
- package/dist/identity.js +116 -4
- package/dist/identity.js.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/server.cjs +130 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +55 -3
- package/dist/server.d.ts +55 -3
- package/dist/server.js +130 -2
- package/dist/server.js.map +1 -1
- package/package.json +5 -4
package/dist/server.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { F as FetchImpl,
|
|
2
|
-
export { R as RankedEntry,
|
|
1
|
+
import { F as FetchImpl, g as SecretKeyConfig, A as ApiSuccess, a as SubmitScoreResponse, L as LeaderboardResponse, P as PlayerRankResponse, W as WindowAroundResponse } from './errors-CWTmormh.cjs';
|
|
2
|
+
export { R as RankedEntry, e as ScorezillaError, f as ScorezillaErrorCode } from './errors-CWTmormh.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Built-in request verifiers for {@link createScoreSubmitHandler} (#211).
|
|
@@ -131,6 +131,58 @@ interface VerifyFirebaseIdTokenOptions {
|
|
|
131
131
|
*/
|
|
132
132
|
declare function verifyFirebaseIdToken(options: VerifyFirebaseIdTokenOptions): RequestVerifier;
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* `createGitHubOAuthHandler` — the server-side callback leg of the GitHub
|
|
136
|
+
* identity provider (`useAuthProvider({ provider: 'github' })`, ADR 0009).
|
|
137
|
+
*
|
|
138
|
+
* GitHub redirects the sign-in popup here with `?code=&state=`. This handler:
|
|
139
|
+
* 1. validates the query strictly (formats are pinned — anything else is a
|
|
140
|
+
* 400, never reflected);
|
|
141
|
+
* 2. exchanges the code at GitHub's token endpoint — the client secret
|
|
142
|
+
* stays on this server, and the resulting access token is used for ONE
|
|
143
|
+
* `/user` lookup and then discarded;
|
|
144
|
+
* 3. responds with a tiny HTML page that `postMessage`s
|
|
145
|
+
* `{ source: 'scorezilla:github-oauth', state, id }` to `window.opener`,
|
|
146
|
+
* pinned to `allowedOrigin`, and closes the popup.
|
|
147
|
+
*
|
|
148
|
+
* The browser side (`scorezilla/identity`) accepts the message only when the
|
|
149
|
+
* origin matches its `exchangeUrl` and the `state` echoes the one it
|
|
150
|
+
* generated — this page is the only party that can complete a sign-in.
|
|
151
|
+
*
|
|
152
|
+
* Returns a standard `(Request) => Promise<Response>`: deploy it on any web
|
|
153
|
+
* runtime (a Next.js route handler, Hono, Deno, Bun, a Cloudflare Worker).
|
|
154
|
+
* Configure the deployed URL as the OAuth app's callback URL on GitHub.
|
|
155
|
+
*
|
|
156
|
+
* @since 0.3.0
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
interface CreateGitHubOAuthHandlerConfig {
|
|
160
|
+
/** Your GitHub OAuth app client ID. */
|
|
161
|
+
readonly clientId: string;
|
|
162
|
+
/** Your GitHub OAuth app client secret. Server-only — never ships to a browser. */
|
|
163
|
+
readonly clientSecret: string;
|
|
164
|
+
/**
|
|
165
|
+
* The exact origin of the GAME page that opens the sign-in popup, e.g.
|
|
166
|
+
* `https://mygame.example`. Used as the `postMessage` target origin so the
|
|
167
|
+
* sign-in result can only be delivered to your page — never `*`.
|
|
168
|
+
*/
|
|
169
|
+
readonly allowedOrigin: string;
|
|
170
|
+
/** Inject a `fetch` (testing / custom transport). */
|
|
171
|
+
readonly fetch?: FetchImpl;
|
|
172
|
+
}
|
|
173
|
+
/** The web-standard request handler returned by {@link createGitHubOAuthHandler}. */
|
|
174
|
+
type GitHubOAuthHandler = (req: Request) => Promise<Response>;
|
|
175
|
+
/**
|
|
176
|
+
* Build the GitHub OAuth callback endpoint. See module docs for the flow;
|
|
177
|
+
* see `GitHubAuthProviderOptions` in `scorezilla/identity` for the matching
|
|
178
|
+
* client half.
|
|
179
|
+
*
|
|
180
|
+
* **Construction patterns.** In Node/Next, secrets are in `process.env`, so
|
|
181
|
+
* build the handler once at module scope. In Cloudflare Workers, secrets
|
|
182
|
+
* live on the per-request `env` binding, so build it inside `fetch`.
|
|
183
|
+
*/
|
|
184
|
+
declare function createGitHubOAuthHandler(config: CreateGitHubOAuthHandlerConfig): GitHubOAuthHandler;
|
|
185
|
+
|
|
134
186
|
/**
|
|
135
187
|
* `scorezilla/server` — HMAC-signed adapter for game backends (#17, v0.2.0).
|
|
136
188
|
*
|
|
@@ -391,4 +443,4 @@ type ScoreSubmitHandler = (req: Request) => Promise<Response>;
|
|
|
391
443
|
*/
|
|
392
444
|
declare function createScoreSubmitHandler(config: CreateScoreSubmitHandlerConfig): ScoreSubmitHandler;
|
|
393
445
|
|
|
394
|
-
export { ApiSuccess, type CancellableInput, type CreateScoreSubmitHandlerConfig, type GetLeaderboardInput, type GetPlayerRankInput, type GetWindowAroundInput, LeaderboardResponse, type ParsedScoreSubmission, PlayerRankResponse, type RateLimitDecision, type RequestVerifier, type ScoreSubmitCorsOptions, type ScoreSubmitHandler, Scorezilla, type SubmitScoreInput, SubmitScoreResponse, type VerifiedIdentity, type VerifyAuth0JwtOptions, type VerifyClerkJwtOptions, type VerifyFirebaseIdTokenOptions, type VerifyJwtOptions, type VerifySupabaseJwtOptions, WindowAroundResponse, createScoreSubmitHandler, verifyAuth0Jwt, verifyClerkJwt, verifyFirebaseIdToken, verifyJwt, verifySupabaseJwt };
|
|
446
|
+
export { ApiSuccess, type CancellableInput, type CreateGitHubOAuthHandlerConfig, type CreateScoreSubmitHandlerConfig, type GetLeaderboardInput, type GetPlayerRankInput, type GetWindowAroundInput, type GitHubOAuthHandler, LeaderboardResponse, type ParsedScoreSubmission, PlayerRankResponse, type RateLimitDecision, type RequestVerifier, type ScoreSubmitCorsOptions, type ScoreSubmitHandler, Scorezilla, type SubmitScoreInput, SubmitScoreResponse, type VerifiedIdentity, type VerifyAuth0JwtOptions, type VerifyClerkJwtOptions, type VerifyFirebaseIdTokenOptions, type VerifyJwtOptions, type VerifySupabaseJwtOptions, WindowAroundResponse, createGitHubOAuthHandler, createScoreSubmitHandler, verifyAuth0Jwt, verifyClerkJwt, verifyFirebaseIdToken, verifyJwt, verifySupabaseJwt };
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { F as FetchImpl,
|
|
2
|
-
export { R as RankedEntry,
|
|
1
|
+
import { F as FetchImpl, g as SecretKeyConfig, A as ApiSuccess, a as SubmitScoreResponse, L as LeaderboardResponse, P as PlayerRankResponse, W as WindowAroundResponse } from './errors-CWTmormh.js';
|
|
2
|
+
export { R as RankedEntry, e as ScorezillaError, f as ScorezillaErrorCode } from './errors-CWTmormh.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Built-in request verifiers for {@link createScoreSubmitHandler} (#211).
|
|
@@ -131,6 +131,58 @@ interface VerifyFirebaseIdTokenOptions {
|
|
|
131
131
|
*/
|
|
132
132
|
declare function verifyFirebaseIdToken(options: VerifyFirebaseIdTokenOptions): RequestVerifier;
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* `createGitHubOAuthHandler` — the server-side callback leg of the GitHub
|
|
136
|
+
* identity provider (`useAuthProvider({ provider: 'github' })`, ADR 0009).
|
|
137
|
+
*
|
|
138
|
+
* GitHub redirects the sign-in popup here with `?code=&state=`. This handler:
|
|
139
|
+
* 1. validates the query strictly (formats are pinned — anything else is a
|
|
140
|
+
* 400, never reflected);
|
|
141
|
+
* 2. exchanges the code at GitHub's token endpoint — the client secret
|
|
142
|
+
* stays on this server, and the resulting access token is used for ONE
|
|
143
|
+
* `/user` lookup and then discarded;
|
|
144
|
+
* 3. responds with a tiny HTML page that `postMessage`s
|
|
145
|
+
* `{ source: 'scorezilla:github-oauth', state, id }` to `window.opener`,
|
|
146
|
+
* pinned to `allowedOrigin`, and closes the popup.
|
|
147
|
+
*
|
|
148
|
+
* The browser side (`scorezilla/identity`) accepts the message only when the
|
|
149
|
+
* origin matches its `exchangeUrl` and the `state` echoes the one it
|
|
150
|
+
* generated — this page is the only party that can complete a sign-in.
|
|
151
|
+
*
|
|
152
|
+
* Returns a standard `(Request) => Promise<Response>`: deploy it on any web
|
|
153
|
+
* runtime (a Next.js route handler, Hono, Deno, Bun, a Cloudflare Worker).
|
|
154
|
+
* Configure the deployed URL as the OAuth app's callback URL on GitHub.
|
|
155
|
+
*
|
|
156
|
+
* @since 0.3.0
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
interface CreateGitHubOAuthHandlerConfig {
|
|
160
|
+
/** Your GitHub OAuth app client ID. */
|
|
161
|
+
readonly clientId: string;
|
|
162
|
+
/** Your GitHub OAuth app client secret. Server-only — never ships to a browser. */
|
|
163
|
+
readonly clientSecret: string;
|
|
164
|
+
/**
|
|
165
|
+
* The exact origin of the GAME page that opens the sign-in popup, e.g.
|
|
166
|
+
* `https://mygame.example`. Used as the `postMessage` target origin so the
|
|
167
|
+
* sign-in result can only be delivered to your page — never `*`.
|
|
168
|
+
*/
|
|
169
|
+
readonly allowedOrigin: string;
|
|
170
|
+
/** Inject a `fetch` (testing / custom transport). */
|
|
171
|
+
readonly fetch?: FetchImpl;
|
|
172
|
+
}
|
|
173
|
+
/** The web-standard request handler returned by {@link createGitHubOAuthHandler}. */
|
|
174
|
+
type GitHubOAuthHandler = (req: Request) => Promise<Response>;
|
|
175
|
+
/**
|
|
176
|
+
* Build the GitHub OAuth callback endpoint. See module docs for the flow;
|
|
177
|
+
* see `GitHubAuthProviderOptions` in `scorezilla/identity` for the matching
|
|
178
|
+
* client half.
|
|
179
|
+
*
|
|
180
|
+
* **Construction patterns.** In Node/Next, secrets are in `process.env`, so
|
|
181
|
+
* build the handler once at module scope. In Cloudflare Workers, secrets
|
|
182
|
+
* live on the per-request `env` binding, so build it inside `fetch`.
|
|
183
|
+
*/
|
|
184
|
+
declare function createGitHubOAuthHandler(config: CreateGitHubOAuthHandlerConfig): GitHubOAuthHandler;
|
|
185
|
+
|
|
134
186
|
/**
|
|
135
187
|
* `scorezilla/server` — HMAC-signed adapter for game backends (#17, v0.2.0).
|
|
136
188
|
*
|
|
@@ -391,4 +443,4 @@ type ScoreSubmitHandler = (req: Request) => Promise<Response>;
|
|
|
391
443
|
*/
|
|
392
444
|
declare function createScoreSubmitHandler(config: CreateScoreSubmitHandlerConfig): ScoreSubmitHandler;
|
|
393
445
|
|
|
394
|
-
export { ApiSuccess, type CancellableInput, type CreateScoreSubmitHandlerConfig, type GetLeaderboardInput, type GetPlayerRankInput, type GetWindowAroundInput, LeaderboardResponse, type ParsedScoreSubmission, PlayerRankResponse, type RateLimitDecision, type RequestVerifier, type ScoreSubmitCorsOptions, type ScoreSubmitHandler, Scorezilla, type SubmitScoreInput, SubmitScoreResponse, type VerifiedIdentity, type VerifyAuth0JwtOptions, type VerifyClerkJwtOptions, type VerifyFirebaseIdTokenOptions, type VerifyJwtOptions, type VerifySupabaseJwtOptions, WindowAroundResponse, createScoreSubmitHandler, verifyAuth0Jwt, verifyClerkJwt, verifyFirebaseIdToken, verifyJwt, verifySupabaseJwt };
|
|
446
|
+
export { ApiSuccess, type CancellableInput, type CreateGitHubOAuthHandlerConfig, type CreateScoreSubmitHandlerConfig, type GetLeaderboardInput, type GetPlayerRankInput, type GetWindowAroundInput, type GitHubOAuthHandler, LeaderboardResponse, type ParsedScoreSubmission, PlayerRankResponse, type RateLimitDecision, type RequestVerifier, type ScoreSubmitCorsOptions, type ScoreSubmitHandler, Scorezilla, type SubmitScoreInput, SubmitScoreResponse, type VerifiedIdentity, type VerifyAuth0JwtOptions, type VerifyClerkJwtOptions, type VerifyFirebaseIdTokenOptions, type VerifyJwtOptions, type VerifySupabaseJwtOptions, WindowAroundResponse, createGitHubOAuthHandler, createScoreSubmitHandler, verifyAuth0Jwt, verifyClerkJwt, verifyFirebaseIdToken, verifyJwt, verifySupabaseJwt };
|
package/dist/server.js
CHANGED
|
@@ -706,13 +706,141 @@ function verifyFirebaseIdToken(options) {
|
|
|
706
706
|
});
|
|
707
707
|
}
|
|
708
708
|
|
|
709
|
+
// src/github-oauth.ts
|
|
710
|
+
var GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
711
|
+
var GITHUB_USER_URL = "https://api.github.com/user";
|
|
712
|
+
var MESSAGE_SOURCE = "scorezilla:github-oauth";
|
|
713
|
+
var USER_AGENT = "scorezilla-sdk-github-oauth";
|
|
714
|
+
var STATE_RE = /^[A-Za-z0-9_-]{8,128}$/;
|
|
715
|
+
var CODE_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
716
|
+
function createGitHubOAuthHandler(config) {
|
|
717
|
+
if (typeof config.clientId !== "string" || config.clientId.length === 0) {
|
|
718
|
+
throw new TypeError("createGitHubOAuthHandler: clientId is required.");
|
|
719
|
+
}
|
|
720
|
+
if (typeof config.clientSecret !== "string" || config.clientSecret.length === 0) {
|
|
721
|
+
throw new TypeError("createGitHubOAuthHandler: clientSecret is required.");
|
|
722
|
+
}
|
|
723
|
+
let allowedOrigin;
|
|
724
|
+
try {
|
|
725
|
+
const parsed = new URL(config.allowedOrigin);
|
|
726
|
+
allowedOrigin = parsed.origin;
|
|
727
|
+
if (allowedOrigin === "null") throw new Error("opaque origin");
|
|
728
|
+
} catch {
|
|
729
|
+
throw new TypeError(
|
|
730
|
+
"createGitHubOAuthHandler: allowedOrigin must be an absolute origin, e.g. 'https://mygame.example' (got " + JSON.stringify(config.allowedOrigin) + ")."
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
const fetchImpl = config.fetch ?? fetch;
|
|
734
|
+
return async (req) => {
|
|
735
|
+
if (req.method !== "GET") {
|
|
736
|
+
return new Response("method not allowed", { status: 405, headers: { allow: "GET" } });
|
|
737
|
+
}
|
|
738
|
+
const url = new URL(req.url);
|
|
739
|
+
const state = url.searchParams.get("state") ?? "";
|
|
740
|
+
const code = url.searchParams.get("code");
|
|
741
|
+
const ghError = url.searchParams.get("error");
|
|
742
|
+
if (!STATE_RE.test(state)) {
|
|
743
|
+
return new Response("invalid or missing state parameter", { status: 400 });
|
|
744
|
+
}
|
|
745
|
+
if (ghError !== null) {
|
|
746
|
+
return callbackPage(
|
|
747
|
+
{ state, error: ghError === "access_denied" ? "access_denied" : "exchange_failed" },
|
|
748
|
+
allowedOrigin
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
if (code === null || !CODE_RE.test(code)) {
|
|
752
|
+
return new Response("invalid or missing code parameter", { status: 400 });
|
|
753
|
+
}
|
|
754
|
+
let accessToken;
|
|
755
|
+
try {
|
|
756
|
+
const tokenRes = await fetchImpl(GITHUB_TOKEN_URL, {
|
|
757
|
+
method: "POST",
|
|
758
|
+
headers: {
|
|
759
|
+
accept: "application/json",
|
|
760
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
761
|
+
"user-agent": USER_AGENT
|
|
762
|
+
},
|
|
763
|
+
body: new URLSearchParams({
|
|
764
|
+
client_id: config.clientId,
|
|
765
|
+
client_secret: config.clientSecret,
|
|
766
|
+
code
|
|
767
|
+
}).toString()
|
|
768
|
+
});
|
|
769
|
+
const tokenBody = await tokenRes.json();
|
|
770
|
+
if (!tokenRes.ok || typeof tokenBody.access_token !== "string") {
|
|
771
|
+
return callbackPage({ state, error: "exchange_failed" }, allowedOrigin);
|
|
772
|
+
}
|
|
773
|
+
accessToken = tokenBody.access_token;
|
|
774
|
+
} catch {
|
|
775
|
+
return callbackPage({ state, error: "exchange_failed" }, allowedOrigin);
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
const userRes = await fetchImpl(GITHUB_USER_URL, {
|
|
779
|
+
method: "GET",
|
|
780
|
+
headers: {
|
|
781
|
+
authorization: `Bearer ${accessToken}`,
|
|
782
|
+
accept: "application/vnd.github+json",
|
|
783
|
+
"user-agent": USER_AGENT
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
if (!userRes.ok) {
|
|
787
|
+
return callbackPage({ state, error: "exchange_failed" }, allowedOrigin);
|
|
788
|
+
}
|
|
789
|
+
const user = await userRes.json();
|
|
790
|
+
if (typeof user.id !== "number" || !Number.isSafeInteger(user.id)) {
|
|
791
|
+
return callbackPage({ state, error: "exchange_failed" }, allowedOrigin);
|
|
792
|
+
}
|
|
793
|
+
return callbackPage({ state, id: String(user.id) }, allowedOrigin);
|
|
794
|
+
} catch {
|
|
795
|
+
return callbackPage({ state, error: "exchange_failed" }, allowedOrigin);
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
function callbackPage(payload, allowedOrigin) {
|
|
800
|
+
const message = JSON.stringify({ source: MESSAGE_SOURCE, ...payload });
|
|
801
|
+
const target = JSON.stringify(allowedOrigin);
|
|
802
|
+
const html = `<!doctype html>
|
|
803
|
+
<html>
|
|
804
|
+
<head><meta charset="utf-8"><title>Signing in\u2026</title></head>
|
|
805
|
+
<body>
|
|
806
|
+
<script>
|
|
807
|
+
(function () {
|
|
808
|
+
if (window.opener) {
|
|
809
|
+
window.opener.postMessage(${message}, ${target});
|
|
810
|
+
window.close();
|
|
811
|
+
} else {
|
|
812
|
+
document.body.textContent =
|
|
813
|
+
'Sign-in handled \u2014 you can close this window. (If this keeps appearing, ' +
|
|
814
|
+
'the game page may be setting Cross-Origin-Opener-Policy: same-origin, ' +
|
|
815
|
+
'which severs the popup link; use same-origin-allow-popups.)';
|
|
816
|
+
}
|
|
817
|
+
})();
|
|
818
|
+
</script>
|
|
819
|
+
</body>
|
|
820
|
+
</html>`;
|
|
821
|
+
return new Response(html, {
|
|
822
|
+
status: 200,
|
|
823
|
+
headers: {
|
|
824
|
+
"content-type": "text/html; charset=utf-8",
|
|
825
|
+
// One-shot page embedding a one-shot state — never cache it.
|
|
826
|
+
"cache-control": "no-store",
|
|
827
|
+
// The URL carried the OAuth `code`; nothing on this page may leak it.
|
|
828
|
+
"referrer-policy": "no-referrer",
|
|
829
|
+
// Defense in depth for the inline script: nothing else may load or
|
|
830
|
+
// run on this page even if a future regression reflected input here.
|
|
831
|
+
"content-security-policy": "default-src 'none'; script-src 'unsafe-inline'",
|
|
832
|
+
"x-content-type-options": "nosniff"
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
709
837
|
// src/server.ts
|
|
710
838
|
var Scorezilla = class _Scorezilla {
|
|
711
839
|
/** The package version, injected at build time from `package.json`.
|
|
712
840
|
* Mirrors the static on the public-key client so consumers can log
|
|
713
841
|
* the running SDK build the same way regardless of which surface
|
|
714
842
|
* they imported. */
|
|
715
|
-
static version = "0.3.0-next.
|
|
843
|
+
static version = "0.3.0-next.3";
|
|
716
844
|
#keyId;
|
|
717
845
|
#secret;
|
|
718
846
|
#baseUrl;
|
|
@@ -979,6 +1107,6 @@ function isCorsOriginAllowed(rule, origin) {
|
|
|
979
1107
|
return rule.includes(origin);
|
|
980
1108
|
}
|
|
981
1109
|
|
|
982
|
-
export { Scorezilla, ScorezillaError, createScoreSubmitHandler, verifyAuth0Jwt, verifyClerkJwt, verifyFirebaseIdToken, verifyJwt, verifySupabaseJwt };
|
|
1110
|
+
export { Scorezilla, ScorezillaError, createGitHubOAuthHandler, createScoreSubmitHandler, verifyAuth0Jwt, verifyClerkJwt, verifyFirebaseIdToken, verifyJwt, verifySupabaseJwt };
|
|
983
1111
|
//# sourceMappingURL=server.js.map
|
|
984
1112
|
//# sourceMappingURL=server.js.map
|