scorezilla 0.3.0-next.1 → 0.3.0-next.2
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 +77 -7
- package/CHANGELOG.md +58 -0
- package/README.md +77 -0
- package/RECIPES.md +186 -0
- package/dist/{errors-B7hyC-C5.d.cts → errors-CtXMAHtJ.d.cts} +1 -1
- package/dist/{errors-B7hyC-C5.d.ts → errors-CtXMAHtJ.d.ts} +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 +215 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +249 -3
- package/dist/server.d.ts +249 -3
- package/dist/server.js +210 -2
- package/dist/server.js.map +1 -1
- package/package.json +11 -1
package/dist/server.js
CHANGED
|
@@ -622,13 +622,97 @@ function defaultUserAgent(version, runtime = detectRuntime()) {
|
|
|
622
622
|
return `scorezilla-js/${version} (${runtime})`;
|
|
623
623
|
}
|
|
624
624
|
|
|
625
|
+
// src/verifiers.ts
|
|
626
|
+
async function loadJose() {
|
|
627
|
+
try {
|
|
628
|
+
return await import('jose');
|
|
629
|
+
} catch (cause) {
|
|
630
|
+
throw new Error(
|
|
631
|
+
"scorezilla/server: the optional peer dependency 'jose' is required for verifyJwt() / verifySupabaseJwt(). Install it with `npm i jose` (or your package manager).",
|
|
632
|
+
{ cause }
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function extractBearerToken(req) {
|
|
637
|
+
const header = req.headers.get("authorization");
|
|
638
|
+
if (!header) return null;
|
|
639
|
+
const trimmed = header.trim();
|
|
640
|
+
if (!/^bearer\s+/i.test(trimmed)) return null;
|
|
641
|
+
return trimmed.replace(/^bearer\s+/i, "").trim() || null;
|
|
642
|
+
}
|
|
643
|
+
function verifyJwt(options) {
|
|
644
|
+
const claim = options.claim ?? "sub";
|
|
645
|
+
let keySet = null;
|
|
646
|
+
return async (req) => {
|
|
647
|
+
const token = extractBearerToken(req);
|
|
648
|
+
if (token === null) return null;
|
|
649
|
+
const jose = await loadJose();
|
|
650
|
+
if (keySet === null) {
|
|
651
|
+
keySet = jose.createRemoteJWKSet(
|
|
652
|
+
new URL(options.jwksUrl),
|
|
653
|
+
options.fetch ? { [jose.customFetch]: options.fetch } : void 0
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
try {
|
|
657
|
+
const { payload } = await jose.jwtVerify(token, keySet, {
|
|
658
|
+
...options.issuer !== void 0 ? { issuer: options.issuer } : {},
|
|
659
|
+
...options.audience !== void 0 ? { audience: options.audience } : {}
|
|
660
|
+
});
|
|
661
|
+
const id = payload[claim];
|
|
662
|
+
return typeof id === "string" && id.length > 0 ? { playerId: id } : null;
|
|
663
|
+
} catch {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function verifySupabaseJwt(options) {
|
|
669
|
+
const base = options.supabaseUrl.replace(/\/+$/, "");
|
|
670
|
+
return verifyJwt({
|
|
671
|
+
jwksUrl: `${base}/auth/v1/.well-known/jwks.json`,
|
|
672
|
+
issuer: `${base}/auth/v1`,
|
|
673
|
+
audience: "authenticated",
|
|
674
|
+
...options.claim !== void 0 ? { claim: options.claim } : {},
|
|
675
|
+
...options.fetch !== void 0 ? { fetch: options.fetch } : {}
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function verifyClerkJwt(options) {
|
|
679
|
+
const issuer = options.issuer.replace(/\/+$/, "");
|
|
680
|
+
return verifyJwt({
|
|
681
|
+
jwksUrl: `${issuer}/.well-known/jwks.json`,
|
|
682
|
+
issuer,
|
|
683
|
+
...options.audience !== void 0 ? { audience: options.audience } : {},
|
|
684
|
+
...options.claim !== void 0 ? { claim: options.claim } : {},
|
|
685
|
+
...options.fetch !== void 0 ? { fetch: options.fetch } : {}
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
function verifyAuth0Jwt(options) {
|
|
689
|
+
const host = options.domain.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
690
|
+
return verifyJwt({
|
|
691
|
+
jwksUrl: `https://${host}/.well-known/jwks.json`,
|
|
692
|
+
issuer: `https://${host}/`,
|
|
693
|
+
audience: options.audience,
|
|
694
|
+
...options.claim !== void 0 ? { claim: options.claim } : {},
|
|
695
|
+
...options.fetch !== void 0 ? { fetch: options.fetch } : {}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
var FIREBASE_JWKS_URL = "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com";
|
|
699
|
+
function verifyFirebaseIdToken(options) {
|
|
700
|
+
return verifyJwt({
|
|
701
|
+
jwksUrl: FIREBASE_JWKS_URL,
|
|
702
|
+
issuer: `https://securetoken.google.com/${options.projectId}`,
|
|
703
|
+
audience: options.projectId,
|
|
704
|
+
...options.claim !== void 0 ? { claim: options.claim } : {},
|
|
705
|
+
...options.fetch !== void 0 ? { fetch: options.fetch } : {}
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
625
709
|
// src/server.ts
|
|
626
710
|
var Scorezilla = class _Scorezilla {
|
|
627
711
|
/** The package version, injected at build time from `package.json`.
|
|
628
712
|
* Mirrors the static on the public-key client so consumers can log
|
|
629
713
|
* the running SDK build the same way regardless of which surface
|
|
630
714
|
* they imported. */
|
|
631
|
-
static version = "0.3.0-next.
|
|
715
|
+
static version = "0.3.0-next.2";
|
|
632
716
|
#keyId;
|
|
633
717
|
#secret;
|
|
634
718
|
#baseUrl;
|
|
@@ -770,7 +854,131 @@ function isRealBrowserEnvironment() {
|
|
|
770
854
|
const hasNodeLikeHost = Boolean(g.process?.versions?.node) || typeof g.Deno !== "undefined" || typeof g.Bun !== "undefined";
|
|
771
855
|
return !hasNodeLikeHost;
|
|
772
856
|
}
|
|
857
|
+
function createScoreSubmitHandler(config) {
|
|
858
|
+
const sz = new Scorezilla({
|
|
859
|
+
secretKey: config.secretKey,
|
|
860
|
+
...config.baseUrl !== void 0 ? { baseUrl: config.baseUrl } : {},
|
|
861
|
+
...config.fetch !== void 0 ? { fetch: config.fetch } : {},
|
|
862
|
+
...config.maxRetries !== void 0 ? { maxRetries: config.maxRetries } : {},
|
|
863
|
+
...config.timeoutMs !== void 0 ? { timeoutMs: config.timeoutMs } : {}
|
|
864
|
+
});
|
|
865
|
+
const parse = config.parseSubmission ?? defaultParseSubmission;
|
|
866
|
+
return async (req) => {
|
|
867
|
+
const cors = config.cors ? buildCorsHeaders(config.cors, req.headers.get("Origin")) : {};
|
|
868
|
+
if (req.method === "OPTIONS" && config.cors) {
|
|
869
|
+
return new Response(null, { status: 204, headers: cors });
|
|
870
|
+
}
|
|
871
|
+
if (req.method !== "POST") {
|
|
872
|
+
return jsonResponse({ ok: false, error: "method_not_allowed" }, 405, cors);
|
|
873
|
+
}
|
|
874
|
+
if (config.rateLimit) {
|
|
875
|
+
const decision = await config.rateLimit(req);
|
|
876
|
+
if (!decision.ok) {
|
|
877
|
+
const headers = { ...cors };
|
|
878
|
+
if (decision.retryAfterSeconds !== void 0) {
|
|
879
|
+
headers["Retry-After"] = String(decision.retryAfterSeconds);
|
|
880
|
+
}
|
|
881
|
+
return jsonResponse({ ok: false, error: "rate_limited" }, 429, headers);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
let identity;
|
|
885
|
+
try {
|
|
886
|
+
identity = await config.verify(req);
|
|
887
|
+
} catch {
|
|
888
|
+
identity = null;
|
|
889
|
+
}
|
|
890
|
+
if (!identity || typeof identity.playerId !== "string" || identity.playerId.length === 0) {
|
|
891
|
+
return jsonResponse({ ok: false, error: "unauthorized" }, 401, cors);
|
|
892
|
+
}
|
|
893
|
+
let submission;
|
|
894
|
+
try {
|
|
895
|
+
submission = await parse(req);
|
|
896
|
+
} catch {
|
|
897
|
+
submission = null;
|
|
898
|
+
}
|
|
899
|
+
if (!submission || typeof submission.score !== "number" || !Number.isFinite(submission.score)) {
|
|
900
|
+
return jsonResponse(
|
|
901
|
+
{ ok: false, error: "bad_request", message: "invalid score submission" },
|
|
902
|
+
400,
|
|
903
|
+
cors
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
const metadata = mergeMetadata(submission.metadata, identity.metadata);
|
|
907
|
+
try {
|
|
908
|
+
const result = await sz.submitScore({
|
|
909
|
+
boardId: config.boardId,
|
|
910
|
+
playerId: identity.playerId,
|
|
911
|
+
score: submission.score,
|
|
912
|
+
...metadata !== void 0 ? { metadata } : {}
|
|
913
|
+
});
|
|
914
|
+
return jsonResponse(
|
|
915
|
+
{
|
|
916
|
+
ok: true,
|
|
917
|
+
rank: result.rank,
|
|
918
|
+
totalEntries: result.totalEntries,
|
|
919
|
+
isPersonalBest: result.isPersonalBest
|
|
920
|
+
},
|
|
921
|
+
200,
|
|
922
|
+
cors
|
|
923
|
+
);
|
|
924
|
+
} catch (err) {
|
|
925
|
+
return mapSubmitError(err, cors);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
async function defaultParseSubmission(req) {
|
|
930
|
+
const raw = await req.json().catch(() => null);
|
|
931
|
+
if (!isPlainObject(raw)) return null;
|
|
932
|
+
const score = raw.score;
|
|
933
|
+
if (typeof score !== "number" || !Number.isFinite(score)) return null;
|
|
934
|
+
return { score, metadata: isPlainObject(raw.metadata) ? raw.metadata : void 0 };
|
|
935
|
+
}
|
|
936
|
+
function isPlainObject(value) {
|
|
937
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
938
|
+
}
|
|
939
|
+
function mergeMetadata(client, verified) {
|
|
940
|
+
if (!client && !verified) return void 0;
|
|
941
|
+
return { ...client ?? {}, ...verified ?? {} };
|
|
942
|
+
}
|
|
943
|
+
function jsonResponse(body, status, extra) {
|
|
944
|
+
return new Response(JSON.stringify(body), {
|
|
945
|
+
status,
|
|
946
|
+
headers: { "Content-Type": "application/json; charset=utf-8", ...extra }
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
function mapSubmitError(err, cors) {
|
|
950
|
+
if (err instanceof ScorezillaError) {
|
|
951
|
+
if (err.isRateLimited()) {
|
|
952
|
+
const headers = { ...cors };
|
|
953
|
+
if (err.retryAfter !== void 0) headers["Retry-After"] = String(err.retryAfter);
|
|
954
|
+
return jsonResponse({ ok: false, error: "rate_limited" }, 429, headers);
|
|
955
|
+
}
|
|
956
|
+
if (err.status >= 400 && err.status < 500) {
|
|
957
|
+
return jsonResponse({ ok: false, error: err.code, message: err.message }, err.status, cors);
|
|
958
|
+
}
|
|
959
|
+
return jsonResponse({ ok: false, error: "upstream_error" }, 502, cors);
|
|
960
|
+
}
|
|
961
|
+
return jsonResponse({ ok: false, error: "server_error" }, 500, cors);
|
|
962
|
+
}
|
|
963
|
+
function buildCorsHeaders(cors, origin) {
|
|
964
|
+
const headers = {
|
|
965
|
+
"Access-Control-Allow-Methods": (cors.methods ?? ["POST", "OPTIONS"]).join(", "),
|
|
966
|
+
"Access-Control-Allow-Headers": (cors.headers ?? ["content-type", "authorization"]).join(", "),
|
|
967
|
+
"Access-Control-Max-Age": String(cors.maxAgeSeconds ?? 600),
|
|
968
|
+
Vary: "Origin"
|
|
969
|
+
};
|
|
970
|
+
if (origin !== null && isCorsOriginAllowed(cors.origin, origin)) {
|
|
971
|
+
headers["Access-Control-Allow-Origin"] = origin;
|
|
972
|
+
}
|
|
973
|
+
return headers;
|
|
974
|
+
}
|
|
975
|
+
function isCorsOriginAllowed(rule, origin) {
|
|
976
|
+
if (typeof rule === "boolean") return rule;
|
|
977
|
+
if (typeof rule === "string") return rule === origin;
|
|
978
|
+
if (typeof rule === "function") return rule(origin);
|
|
979
|
+
return rule.includes(origin);
|
|
980
|
+
}
|
|
773
981
|
|
|
774
|
-
export { Scorezilla, ScorezillaError };
|
|
982
|
+
export { Scorezilla, ScorezillaError, createScoreSubmitHandler, verifyAuth0Jwt, verifyClerkJwt, verifyFirebaseIdToken, verifyJwt, verifySupabaseJwt };
|
|
775
983
|
//# sourceMappingURL=server.js.map
|
|
776
984
|
//# sourceMappingURL=server.js.map
|