scorezilla 0.1.0-next.1 → 0.1.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/CHANGELOG.md +69 -0
- package/README.md +4 -4
- package/dist/{errors-Bp9ZDYqm.d.cts → errors-CUAQsaVS.d.cts} +15 -7
- package/dist/{errors-Bp9ZDYqm.d.ts → errors-CUAQsaVS.d.ts} +15 -7
- package/dist/index.cjs +18 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -13
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +77 -16
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +11 -2
- package/dist/server.d.ts +11 -2
- package/dist/server.js +77 -16
- package/dist/server.js.map +1 -1
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-next.3
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#25](https://github.com/isco-tec/scorezilla-js/pull/25) [`5f7025b`](https://github.com/isco-tec/scorezilla-js/commit/5f7025b7b06dded92b3e710316454eb8c891d053) Thanks [@isco-tec](https://github.com/isco-tec)! - **`scorezilla/server`: HMAC requests now bind to the target host (v=2).**
|
|
8
|
+
|
|
9
|
+
The canonical signing string includes the host of the request URL, so a signature minted against staging cannot be replayed against prod (or any other origin that happens to share key material). SigV4 has had this since day one; closing the gap before MCP locks the format.
|
|
10
|
+
|
|
11
|
+
Wire format change:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Authorization: Scorezilla-HMAC-SHA256 keyId=…, ts=…, nonce=…, signature=…, v=2
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The `v=2` parameter is new. The canonical signing string now has six lines instead of five — `host` is inserted between `METHOD` and `pathAndQuery`, lowercased per RFC 9110 §4.2.4.
|
|
18
|
+
|
|
19
|
+
**Backward compatibility.** The API verifier still accepts v=1 (no `v=` field, no host binding) during the rollout window — your existing pre-next.3 SDK builds will keep working until v=1 is deprecated. The SDK itself, however, emits v=2 unconditionally from next.3 onwards. If you've forked or wrapped `buildHmacAuthHeader` / `buildSigningString`, you'll need to thread a `host` parameter through.
|
|
20
|
+
|
|
21
|
+
**For consumers of `Scorezilla` from `scorezilla/server`:** no API changes. The constructor derives `host` from `baseUrl` automatically. As an added safety net, an invalid `baseUrl` now throws at construction time rather than producing 401 mismatches at every request.
|
|
22
|
+
|
|
23
|
+
**For low-level consumers of `buildSigningString` / `buildHmacAuthHeader`:** a new `host` parameter is now required. The latter also accepts an optional `version: 1 | 2` for explicit backward-compat scenarios.
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- [#28](https://github.com/isco-tec/scorezilla-js/pull/28) [`94b39d2`](https://github.com/isco-tec/scorezilla-js/commit/94b39d2e67ec7bba2681145560529f058acfc633) Thanks [@isco-tec](https://github.com/isco-tec)! - **Pre-release security hardening pass.** Three issues surfaced by a focused security review before the first public release of `scorezilla/server`:
|
|
28
|
+
- **HIGH — nonce injection.** `buildHmacAuthHeader` now requires injected nonces to be at least 16 characters. The default path (`crypto.randomUUID()`) is unaffected. Previously a misconfigured caller could pass `nonce: ''` and silently sign a header with no replay protection — the server has no minimum-length check, so the API would accept it. Now caught at SDK build-time with a clear error.
|
|
29
|
+
- **HIGH — server policy disclosure.** `HMAC_TIMESTAMP_WINDOW_SECONDS = 300` was previously exported as documentation of the API's clock-skew tolerance. Removed from the public API — publishing the server's replay-protection window in the SDK's types was unnecessary information disclosure and narrowed the pre-computation window for any future timestamp-forgery work. The SDK has no behavioral dependency on the value (it always emits a fresh `ts = floor(Date.now() / 1000)`).
|
|
30
|
+
- **MEDIUM — `EdgeRuntime` polyfill bypass.** The server adapter's runtime browser-guard previously trusted `globalThis.EdgeRuntime` as a "this is a server" signal. A browser extension or bundler misconfig setting that global would have bypassed the guard. Removed from the trusted set — the package's `exports.browser` condition remains the primary gate, and the runtime check now refuses to trust globals that can be polyfilled from a browser context. Real Vercel Edge runtimes still pass the guard because they don't have `window`/`document`.
|
|
31
|
+
|
|
32
|
+
No external-attack surface was exploitable; all three are self-inflicted/defense-in-depth issues caught before the first stable release. Tests added: 4 for nonce validation (rejects empty / too-short, accepts at-floor / default UUID), 1 for the EdgeRuntime polyfill scenario.
|
|
33
|
+
|
|
34
|
+
## 0.1.0-next.2
|
|
35
|
+
|
|
36
|
+
### Minor Changes
|
|
37
|
+
|
|
38
|
+
- [#23](https://github.com/isco-tec/scorezilla-js/pull/23) [`5163e31`](https://github.com/isco-tec/scorezilla-js/commit/5163e3130ac208d591b026d66870ce5a194f90b6) Thanks [@isco-tec](https://github.com/isco-tec)! - **`scorezilla/server` adopts a single-token secret-key format.** Breaking
|
|
39
|
+
change vs. v0.1.0-next.1; we're still in pre-release so this is permitted.
|
|
40
|
+
|
|
41
|
+
Before (v0.1.0-next.1):
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
new Scorezilla({
|
|
45
|
+
secretKey: { id: 'sk-id-uuid', secret: 'sk_live_xxxxx' },
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
After (v0.1.0-next.2):
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
new Scorezilla({
|
|
53
|
+
secretKey: 'sk_live_<keyId>_xxxxx', // one self-contained token
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The keyId is embedded in the secret string itself; the SDK parses it
|
|
58
|
+
out via `/^sk_live_([0-9a-f]{8}-...)_/` before signing. The HMAC key
|
|
59
|
+
remains the WHOLE plaintext. Wire format on the Authorization header
|
|
60
|
+
is unchanged. API verification is unchanged.
|
|
61
|
+
|
|
62
|
+
Rationale: matches Stripe's design and the public-key client's
|
|
63
|
+
single-string shape. One value to copy from the dashboard, one to
|
|
64
|
+
manage in env config, one to pass into the constructor. Previously
|
|
65
|
+
users had to manage two distinct values (`id` AND `secret`) which
|
|
66
|
+
created confusion — they're functionally one credential.
|
|
67
|
+
|
|
68
|
+
**To upgrade**: issue (or rotate) a fresh secret key in the dashboard.
|
|
69
|
+
Old-format keys (no embedded keyId) are rejected by the SDK with a
|
|
70
|
+
clear error message pointing at the migration path.
|
|
71
|
+
|
|
3
72
|
## 0.1.0-next.1
|
|
4
73
|
|
|
5
74
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -88,11 +88,11 @@ submission server-side with a `sk_live_*` secret:
|
|
|
88
88
|
```ts
|
|
89
89
|
import { Scorezilla, ScorezillaError } from 'scorezilla/server';
|
|
90
90
|
|
|
91
|
+
// Single self-contained token from the dashboard. Format:
|
|
92
|
+
// sk_live_<keyId>_<random>
|
|
93
|
+
// The SDK parses the keyId out internally; you only manage one value.
|
|
91
94
|
const sz = new Scorezilla({
|
|
92
|
-
secretKey:
|
|
93
|
-
id: process.env.SCOREZILLA_KEY_ID!,
|
|
94
|
-
secret: process.env.SCOREZILLA_KEY_SECRET!, // never ship to a browser
|
|
95
|
-
},
|
|
95
|
+
secretKey: process.env.SCOREZILLA_SECRET_KEY!, // never ship to a browser
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
await sz.submitScore({ boardId, playerId, score, metadata });
|
|
@@ -31,6 +31,12 @@ interface BaseConfig {
|
|
|
31
31
|
/** Override the default `User-Agent` header (Node/Workers/Bun/Deno —
|
|
32
32
|
* browsers silently ignore the value). */
|
|
33
33
|
userAgent?: string;
|
|
34
|
+
/** Injectable sleep implementation for the retry loop's inter-attempt
|
|
35
|
+
* pause. Exists for tests that need deterministic, zero-delay retries
|
|
36
|
+
* rather than real wall-clock backoff. Production code should leave
|
|
37
|
+
* this unset to use the default exponential backoff with jitter.
|
|
38
|
+
* @internal */
|
|
39
|
+
sleepImpl?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
34
40
|
}
|
|
35
41
|
/** Public-key auth: browser-safe path. The key is fingerprinted to a game
|
|
36
42
|
* on the server side via `pk_<gameSlug>_<base62>`. */
|
|
@@ -38,14 +44,16 @@ type PublicKeyConfig = BaseConfig & {
|
|
|
38
44
|
publicKey: string;
|
|
39
45
|
secretKey?: never;
|
|
40
46
|
};
|
|
41
|
-
/** Secret-key auth: server-side HMAC.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
47
|
+
/** Secret-key auth: server-side HMAC. A single self-contained token of the
|
|
48
|
+
* shape `sk_live_<keyId>_<random>`. The SDK parses the keyId out and uses
|
|
49
|
+
* the whole string as the HMAC key. One value to copy, one to manage —
|
|
50
|
+
* matches Stripe's design and the public-key client's single-string shape.
|
|
51
|
+
*
|
|
52
|
+
* Past versions of the SDK took `{ id, secret }` separately. That was an
|
|
53
|
+
* unnecessary cognitive tax — the id was always derivable from a properly-
|
|
54
|
+
* formatted secret. v0.1.0-next.2+ takes the single-string form. */
|
|
44
55
|
type SecretKeyConfig = BaseConfig & {
|
|
45
|
-
secretKey:
|
|
46
|
-
id: string;
|
|
47
|
-
secret: string;
|
|
48
|
-
};
|
|
56
|
+
secretKey: string;
|
|
49
57
|
publicKey?: never;
|
|
50
58
|
};
|
|
51
59
|
/** The top-level config type. The union is open for additional auth modes
|
|
@@ -31,6 +31,12 @@ interface BaseConfig {
|
|
|
31
31
|
/** Override the default `User-Agent` header (Node/Workers/Bun/Deno —
|
|
32
32
|
* browsers silently ignore the value). */
|
|
33
33
|
userAgent?: string;
|
|
34
|
+
/** Injectable sleep implementation for the retry loop's inter-attempt
|
|
35
|
+
* pause. Exists for tests that need deterministic, zero-delay retries
|
|
36
|
+
* rather than real wall-clock backoff. Production code should leave
|
|
37
|
+
* this unset to use the default exponential backoff with jitter.
|
|
38
|
+
* @internal */
|
|
39
|
+
sleepImpl?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
34
40
|
}
|
|
35
41
|
/** Public-key auth: browser-safe path. The key is fingerprinted to a game
|
|
36
42
|
* on the server side via `pk_<gameSlug>_<base62>`. */
|
|
@@ -38,14 +44,16 @@ type PublicKeyConfig = BaseConfig & {
|
|
|
38
44
|
publicKey: string;
|
|
39
45
|
secretKey?: never;
|
|
40
46
|
};
|
|
41
|
-
/** Secret-key auth: server-side HMAC.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
47
|
+
/** Secret-key auth: server-side HMAC. A single self-contained token of the
|
|
48
|
+
* shape `sk_live_<keyId>_<random>`. The SDK parses the keyId out and uses
|
|
49
|
+
* the whole string as the HMAC key. One value to copy, one to manage —
|
|
50
|
+
* matches Stripe's design and the public-key client's single-string shape.
|
|
51
|
+
*
|
|
52
|
+
* Past versions of the SDK took `{ id, secret }` separately. That was an
|
|
53
|
+
* unnecessary cognitive tax — the id was always derivable from a properly-
|
|
54
|
+
* formatted secret. v0.1.0-next.2+ takes the single-string form. */
|
|
44
55
|
type SecretKeyConfig = BaseConfig & {
|
|
45
|
-
secretKey:
|
|
46
|
-
id: string;
|
|
47
|
-
secret: string;
|
|
48
|
-
};
|
|
56
|
+
secretKey: string;
|
|
49
57
|
publicKey?: never;
|
|
50
58
|
};
|
|
51
59
|
/** The top-level config type. The union is open for additional auth modes
|
package/dist/index.cjs
CHANGED
|
@@ -32,6 +32,7 @@ function validateConfig(cfg) {
|
|
|
32
32
|
fetch: cfg.fetch,
|
|
33
33
|
timeoutMs: cfg.timeoutMs,
|
|
34
34
|
maxRetries: cfg.maxRetries,
|
|
35
|
+
sleepImpl: cfg.sleepImpl,
|
|
35
36
|
userAgent: cfg.userAgent,
|
|
36
37
|
auth
|
|
37
38
|
};
|
|
@@ -45,24 +46,25 @@ function validatePublicKeyValue(pk) {
|
|
|
45
46
|
}
|
|
46
47
|
return pk;
|
|
47
48
|
}
|
|
49
|
+
var SECRET_KEY_PATTERN = /^sk_live_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})_[A-Za-z0-9]+$/;
|
|
48
50
|
function validateSecretKey(cfg) {
|
|
49
51
|
if (!cfg || typeof cfg !== "object") {
|
|
50
52
|
throw new Error("scorezilla/server: config must be an object with a secretKey field");
|
|
51
53
|
}
|
|
52
54
|
const sk = cfg.secretKey;
|
|
53
|
-
if (
|
|
54
|
-
throw new Error(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
throw new Error("scorezilla/server: secretKey.id must be a non-empty string");
|
|
55
|
+
if (typeof sk !== "string") {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`scorezilla/server: secretKey must be a single string of the shape ${SECRET_KEY_PREFIX}<keyId>_<random> (got: ${typeof sk})`
|
|
58
|
+
);
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
const match = SECRET_KEY_PATTERN.exec(sk);
|
|
61
|
+
if (!match) {
|
|
62
|
+
const shape = `string of length ${sk.length}`;
|
|
61
63
|
throw new Error(
|
|
62
|
-
`scorezilla/server: secretKey
|
|
64
|
+
`scorezilla/server: secretKey must match ${SECRET_KEY_PATTERN.toString()} (got: ${shape}). v0.1.0-next.2 switched to a single-token format \u2014 if you have a pre-next.2 pair, issue a fresh key in the dashboard to upgrade.`
|
|
63
65
|
);
|
|
64
66
|
}
|
|
65
|
-
return { keyId:
|
|
67
|
+
return { keyId: match[1], secret: sk };
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
// src/paths.ts
|
|
@@ -551,7 +553,7 @@ function validateMetadata(metadata) {
|
|
|
551
553
|
}
|
|
552
554
|
var Scorezilla = class _Scorezilla {
|
|
553
555
|
/** The package version, injected at build time from `package.json`. */
|
|
554
|
-
static version = "0.1.0-next.
|
|
556
|
+
static version = "0.1.0-next.3";
|
|
555
557
|
#config;
|
|
556
558
|
#userAgent;
|
|
557
559
|
#authHeader;
|
|
@@ -716,8 +718,11 @@ var Scorezilla = class _Scorezilla {
|
|
|
716
718
|
if (opts.body !== void 0) requestOpts.body = opts.body;
|
|
717
719
|
if (this.#config.fetch !== void 0) requestOpts.fetchImpl = this.#config.fetch;
|
|
718
720
|
if (this.#config.timeoutMs !== void 0) requestOpts.timeoutMs = this.#config.timeoutMs;
|
|
719
|
-
if (this.#config.maxRetries !== void 0) {
|
|
720
|
-
requestOpts.retry = {
|
|
721
|
+
if (this.#config.maxRetries !== void 0 || this.#config.sleepImpl !== void 0) {
|
|
722
|
+
requestOpts.retry = {
|
|
723
|
+
...this.#config.maxRetries !== void 0 ? { maxRetries: this.#config.maxRetries } : {},
|
|
724
|
+
...this.#config.sleepImpl !== void 0 ? { sleepImpl: this.#config.sleepImpl } : {}
|
|
725
|
+
};
|
|
721
726
|
}
|
|
722
727
|
return request(requestOpts);
|
|
723
728
|
}
|
|
@@ -727,7 +732,7 @@ function createClient(config) {
|
|
|
727
732
|
}
|
|
728
733
|
|
|
729
734
|
// src/index.ts
|
|
730
|
-
var SDK_VERSION = "0.1.0-next.
|
|
735
|
+
var SDK_VERSION = "0.1.0-next.3";
|
|
731
736
|
|
|
732
737
|
exports.SDK_VERSION = SDK_VERSION;
|
|
733
738
|
exports.Scorezilla = Scorezilla;
|