@mulmoclaude/common 1.1.1 → 1.2.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
package/dist/envScan.js CHANGED
@@ -23,10 +23,10 @@ export function snakeToLowerCamel(snake) {
23
23
  .toLowerCase()
24
24
  .split("_")
25
25
  .filter((segment) => segment.length > 0);
26
- if (parts.length === 0)
27
- return "";
28
26
  const [head, ...rest] = parts;
29
- return head + rest.map((part) => part[0].toUpperCase() + part.slice(1)).join("");
27
+ if (head === undefined)
28
+ return "";
29
+ return head + rest.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
30
30
  }
31
31
  function claimByHighestPrefix(name, prefixes) {
32
32
  const claimed = prefixes
package/dist/index.d.ts CHANGED
@@ -62,5 +62,20 @@ 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";
package/dist/index.js CHANGED
@@ -116,4 +116,21 @@ 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";
package/dist/ssrf.js CHANGED
@@ -67,12 +67,13 @@ export function stripIpv6Brackets(hostname) {
67
67
  * `[::ffff:127.0.0.1]` as `::ffff:7f00:1`, so matching only the dotted form
68
68
  * would wave the mapped loopback through any URL-sourced check. */
69
69
  function mappedIpv4Value(lower) {
70
- const dotted = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(lower);
71
- if (dotted)
72
- return ipv4ToInt(dotted[1]);
70
+ const dotted = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(lower)?.[1];
71
+ if (dotted !== undefined)
72
+ return ipv4ToInt(dotted);
73
73
  const hex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(lower);
74
- if (hex)
75
- return Number.parseInt(hex[1], 16) * 0x10000 + Number.parseInt(hex[2], 16);
74
+ const [, high, low] = hex ?? [];
75
+ if (high !== undefined && low !== undefined)
76
+ return Number.parseInt(high, 16) * 0x10000 + Number.parseInt(low, 16);
76
77
  return null;
77
78
  }
78
79
  /** Blocks loopback/unspecified, fc00::/7 unique-local, fe80::/10 link-local,
@@ -86,7 +87,7 @@ export function isBlockedIpv6(address) {
86
87
  const mapped = mappedIpv4Value(lower);
87
88
  if (mapped !== null)
88
89
  return isBlockedIpv4Value(mapped);
89
- const [head] = lower.split(":");
90
+ const [head = ""] = lower.split(":");
90
91
  if (head.length === 0)
91
92
  return false;
92
93
  const leading = Number.parseInt(head, 16);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulmoclaude/common",
3
- "version": "1.1.1",
3
+ "version": "1.2.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",