@mulmoclaude/common 1.1.2 → 1.3.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/README.md CHANGED
@@ -37,6 +37,7 @@ helpers below.
37
37
  |---|---|---|
38
38
  | `errorMessage(v, fallback?)` | `string` | unknown caught value → human-readable string; **isomorphic**, so Vue/browser surfaces use it too |
39
39
  | `toUtcIsoDate(timestamp)` | `string` | `Date` → `YYYY-MM-DD` in UTC — for dates that must not shift with the host's local timezone |
40
+ | `splitJwtSegments(token)` | `JwtSegments \| null` | JWS compact serialization → its three segments; `null` for anything that isn't **exactly** three. Pure string work, so the Node bridges and the Workers relay share the guard while decoding differently |
40
41
 
41
42
  `errorMessage` surfaces a non-empty string `details` (gRPC convention) or
42
43
  `message` field of a non-Error object (`details` wins) instead of
@@ -0,0 +1,21 @@
1
+ export interface IntRange {
2
+ min?: number;
3
+ max?: number;
4
+ }
5
+ /**
6
+ * `Number()`-based integer coercion with an optional range, falling back when the
7
+ * value is absent, empty, non-integer, or out of range.
8
+ *
9
+ * `Number()` — not `parseInt` — on purpose: it is what the server has always used,
10
+ * so `0x1f`, `1e3`, `+3100` and `3100.0` coerce exactly as they did, and a
11
+ * whitespace-only value coerces to 0. Anything stricter here would silently
12
+ * disagree with the backend.
13
+ */
14
+ export declare function asInt(value: string | undefined, fallback: number, opts?: IntRange): number;
15
+ /** The port range the backend accepts, so both sides bound the value identically.
16
+ * `min: 0` is deliberate — 0 asks the OS for an ephemeral port.
17
+ *
18
+ * `Required<IntRange>`, not `IntRange`: both bounds are always present here, and
19
+ * saying so lets a caller compare against them (`port <= PORT_RANGE.max`) without
20
+ * a non-null assertion. Still assignable wherever an `IntRange` is expected. */
21
+ export declare const PORT_RANGE: Required<IntRange>;
@@ -0,0 +1,36 @@
1
+ // Env-var type coercion for ports and other integer settings.
2
+ //
3
+ // It started inside `server/system/env.ts` (#504-era), moved to
4
+ // `server/utils/envCoerce.ts` when #2650 needed the SAME rule for `yarn dev`'s
5
+ // Vite proxy — the proxy has to resolve `PORT` exactly as the backend does, or
6
+ // the two point at different servers — and moved here when #3084 needed it for
7
+ // the messaging bridges too. A second opinion about "is this a port" is that
8
+ // bug one level down, so there is exactly one copy and every tier imports it.
9
+ /**
10
+ * `Number()`-based integer coercion with an optional range, falling back when the
11
+ * value is absent, empty, non-integer, or out of range.
12
+ *
13
+ * `Number()` — not `parseInt` — on purpose: it is what the server has always used,
14
+ * so `0x1f`, `1e3`, `+3100` and `3100.0` coerce exactly as they did, and a
15
+ * whitespace-only value coerces to 0. Anything stricter here would silently
16
+ * disagree with the backend.
17
+ */
18
+ export function asInt(value, fallback, opts = {}) {
19
+ if (value === undefined || value === "")
20
+ return fallback;
21
+ const parsed = Number(value);
22
+ if (!Number.isInteger(parsed))
23
+ return fallback;
24
+ if (opts.min !== undefined && parsed < opts.min)
25
+ return fallback;
26
+ if (opts.max !== undefined && parsed > opts.max)
27
+ return fallback;
28
+ return parsed;
29
+ }
30
+ /** The port range the backend accepts, so both sides bound the value identically.
31
+ * `min: 0` is deliberate — 0 asks the OS for an ephemeral port.
32
+ *
33
+ * `Required<IntRange>`, not `IntRange`: both bounds are always present here, and
34
+ * saying so lets a caller compare against them (`port <= PORT_RANGE.max`) without
35
+ * a non-null assertion. Still assignable wherever an `IntRange` is expected. */
36
+ export const PORT_RANGE = Object.freeze({ min: 0, max: 65_535 });
package/dist/index.d.ts CHANGED
@@ -62,5 +62,21 @@ export declare function toUtcIsoDate(timestamp: Date): string;
62
62
  * introduces contain none of the other four characters, so nothing is
63
63
  * double-escaped. Not a sanitiser: it neither strips tags nor validates URLs. */
64
64
  export declare function escapeHtml(value: string): string;
65
+ export interface JwtSegments {
66
+ headerSegment: string;
67
+ payloadSegment: string;
68
+ signatureSegment: string;
69
+ }
70
+ /** Split a JWS compact serialization into its three segments, or `null` when the
71
+ * token isn't well-formed. Pure string work, so the Node bridges and the
72
+ * Cloudflare Workers relay — which decode the segments differently (`Buffer` vs
73
+ * `atob`) — can still share this one guard.
74
+ *
75
+ * A JWS compact serialization is EXACTLY three segments. Both a short token and
76
+ * a longer one (a five-segment JWE, or an attacker appending `.junk`) are
77
+ * rejected outright — never parsed from their first three segments, which would
78
+ * let the signed input disagree with the token. */
79
+ export declare function splitJwtSegments(token: string): JwtSegments | null;
65
80
  export { scanEnvOptions, snakeToLowerCamel, type ScanEnvOptionsConfig } from "./envScan.js";
66
81
  export type { MinimalLogger, StructuredLogger } from "./logger.js";
82
+ export { asInt, PORT_RANGE, type IntRange } from "./envCoerce.js";
package/dist/index.js CHANGED
@@ -116,4 +116,22 @@ const HTML_ESCAPES = new Map([
116
116
  export function escapeHtml(value) {
117
117
  return value.replace(/[&<>"']/g, (char) => HTML_ESCAPES.get(char) ?? char);
118
118
  }
119
+ /** Split a JWS compact serialization into its three segments, or `null` when the
120
+ * token isn't well-formed. Pure string work, so the Node bridges and the
121
+ * Cloudflare Workers relay — which decode the segments differently (`Buffer` vs
122
+ * `atob`) — can still share this one guard.
123
+ *
124
+ * A JWS compact serialization is EXACTLY three segments. Both a short token and
125
+ * a longer one (a five-segment JWE, or an attacker appending `.junk`) are
126
+ * rejected outright — never parsed from their first three segments, which would
127
+ * let the signed input disagree with the token. */
128
+ export function splitJwtSegments(token) {
129
+ const [headerSegment, payloadSegment, signatureSegment, ...extraSegments] = token.split(".");
130
+ if (headerSegment === undefined || payloadSegment === undefined || signatureSegment === undefined)
131
+ return null;
132
+ if (extraSegments.length > 0)
133
+ return null;
134
+ return { headerSegment, payloadSegment, signatureSegment };
135
+ }
119
136
  export { scanEnvOptions, snakeToLowerCamel } from "./envScan.js";
137
+ export { asInt, PORT_RANGE } from "./envCoerce.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulmoclaude/common",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "description": "General-purpose pure utilities (type guards, etc.) shared across the MulmoClaude host, bridges, and plugins",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",