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 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. The pair `{ id, secret }` is what
42
- * the operator dashboard issues; the SDK signs requests with `secret` and
43
- * identifies them via `id`. */
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. The pair `{ id, secret }` is what
42
- * the operator dashboard issues; the SDK signs requests with `secret` and
43
- * identifies them via `id`. */
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 (!sk || typeof sk !== "object" || typeof sk.id !== "string" || typeof sk.secret !== "string") {
54
- throw new Error("scorezilla/server: secretKey must be an object with string `id` and `secret`");
55
- }
56
- if (sk.id.length === 0) {
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
- if (!sk.secret.startsWith(SECRET_KEY_PREFIX)) {
60
- const shape = `string of length ${sk.secret.length}`;
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.secret must start with "${SECRET_KEY_PREFIX}" (live keys only) (got: ${shape})`
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: sk.id, secret: sk.secret };
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.1";
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 = { maxRetries: this.#config.maxRetries };
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.1";
735
+ var SDK_VERSION = "0.1.0-next.3";
731
736
 
732
737
  exports.SDK_VERSION = SDK_VERSION;
733
738
  exports.Scorezilla = Scorezilla;