@mulmobridge/client 0.1.5 → 0.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.
@@ -0,0 +1 @@
1
+ export declare function frameText(data: Buffer | ArrayBuffer | Buffer[]): string;
package/dist/frame.js ADDED
@@ -0,0 +1,15 @@
1
+ // Decode a `ws` message frame to a utf8 string, shared by the WebSocket-based
2
+ // bridges (Mastodon, Signal).
3
+ //
4
+ // `ws` hands the listener `Buffer | ArrayBuffer | Buffer[]`. The default
5
+ // binaryType is nodebuffer so a Buffer is what actually arrives, but the type
6
+ // admits ArrayBuffer — whose `toString()` is the literal "[object ArrayBuffer]",
7
+ // i.e. a frame silently parsed as garbage. Normalise instead of trusting the
8
+ // runtime default to hold.
9
+ export function frameText(data) {
10
+ if (Buffer.isBuffer(data))
11
+ return data.toString("utf8");
12
+ if (Array.isArray(data))
13
+ return Buffer.concat(data).toString("utf8");
14
+ return Buffer.from(data).toString("utf8");
15
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export type JsonRecord = Record<string, unknown>;
2
+ /** Narrow a parsed JSON value to a record, defaulting non-objects to `{}` —
3
+ * the `isRecord(json) ? json : {}` idiom the REST bridges repeated so they
4
+ * never have to cast `JSON.parse` output. */
5
+ export declare function asJsonRecord(json: unknown): JsonRecord;
6
+ /** `fetch` + non-2xx guard + JSON-record narrow. Callers own the URL, auth
7
+ * headers, and timeout (via `init.signal`); `errorLabel` prefixes the thrown
8
+ * non-2xx error (e.g. "GET /im.list"). Network errors propagate from `fetch`
9
+ * unchanged, matching the callers' existing try/catch expectations. */
10
+ export declare function fetchJsonRecord(url: string, init: RequestInit, errorLabel: string): Promise<JsonRecord>;
package/dist/http.js ADDED
@@ -0,0 +1,22 @@
1
+ // Minimal JSON-over-REST fetch skeleton shared by the polling bridges
2
+ // (Rocket.Chat, Zulip), which used to carry byte-identical GET/POST wrappers.
3
+ import { isRecord } from "@mulmoclaude/common";
4
+ const MAX_ERROR_BODY_CHARS = 200;
5
+ /** Narrow a parsed JSON value to a record, defaulting non-objects to `{}` —
6
+ * the `isRecord(json) ? json : {}` idiom the REST bridges repeated so they
7
+ * never have to cast `JSON.parse` output. */
8
+ export function asJsonRecord(json) {
9
+ return isRecord(json) ? json : {};
10
+ }
11
+ /** `fetch` + non-2xx guard + JSON-record narrow. Callers own the URL, auth
12
+ * headers, and timeout (via `init.signal`); `errorLabel` prefixes the thrown
13
+ * non-2xx error (e.g. "GET /im.list"). Network errors propagate from `fetch`
14
+ * unchanged, matching the callers' existing try/catch expectations. */
15
+ export async function fetchJsonRecord(url, init, errorLabel) {
16
+ const res = await fetch(url, init);
17
+ if (!res.ok) {
18
+ const text = await res.text().catch(() => "");
19
+ throw new Error(`${errorLabel}: ${res.status} ${text.slice(0, MAX_ERROR_BODY_CHARS)}`);
20
+ }
21
+ return asJsonRecord(await res.json());
22
+ }
package/dist/index.d.ts CHANGED
@@ -2,5 +2,7 @@ export { createBridgeClient, requireBearerToken, type MessageAck, type PushEvent
2
2
  export { readBridgeToken, TOKEN_FILE_PATH } from "./token.js";
3
3
  export { readBridgeEnvOptions } from "./options.js";
4
4
  export { chunkText } from "./text.js";
5
+ export { frameText } from "./frame.js";
6
+ export { asJsonRecord, fetchJsonRecord, type JsonRecord } from "./http.js";
5
7
  export { formatAckReply } from "./reply.js";
6
8
  export { mimeFromExtension, isImageMime, isPdfMime, isSupportedAttachmentMime, isNativeAttachmentMime, parseDataUrl, buildDataUrl, type ParsedDataUrl, } from "./mime.js";
package/dist/index.js CHANGED
@@ -3,5 +3,7 @@ export { createBridgeClient, requireBearerToken } from "./client.js";
3
3
  export { readBridgeToken, TOKEN_FILE_PATH } from "./token.js";
4
4
  export { readBridgeEnvOptions } from "./options.js";
5
5
  export { chunkText } from "./text.js";
6
+ export { frameText } from "./frame.js";
7
+ export { asJsonRecord, fetchJsonRecord } from "./http.js";
6
8
  export { formatAckReply } from "./reply.js";
7
9
  export { mimeFromExtension, isImageMime, isPdfMime, isSupportedAttachmentMime, isNativeAttachmentMime, parseDataUrl, buildDataUrl, } from "./mime.js";
package/dist/options.js CHANGED
@@ -9,38 +9,15 @@
9
9
  //
10
10
  // Both forms strip the prefix and convert the `UPPER_SNAKE` tail to
11
11
  // `lowerCamel`. Empty string values are dropped so a stray
12
- // `FOO=""` doesn't shadow `BAR`'s match.
12
+ // `FOO=""` doesn't shadow `BAR`'s match. The scan itself is the
13
+ // shared `scanEnvOptions` (#2487) — the host's relay path resolves
14
+ // its `RELAY_*` scheme through the same algorithm.
13
15
  //
14
16
  // The `_BRIDGE_` segment is deliberate: it lets the bridge keep its
15
17
  // own secrets (`SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`, …) naturally
16
18
  // outside the scrape — they have no `_BRIDGE_` segment so they're
17
19
  // never picked up, no reserved-list needed.
18
- // Convert UPPER_SNAKE_CASE → lowerCamelCase. Leading digits and
19
- // adjacent underscores degrade gracefully (adjacent underscores
20
- // collapse to a single word break; leading digits are allowed but
21
- // kept as-is after the first segment is lowercased).
22
- function snakeToLowerCamel(snake) {
23
- const parts = snake
24
- .toLowerCase()
25
- .split("_")
26
- .filter((segment) => segment.length > 0);
27
- if (parts.length === 0)
28
- return "";
29
- const [head, ...rest] = parts;
30
- return head + rest.map((part) => (part ? part[0].toUpperCase() + part.slice(1) : "")).join("");
31
- }
32
- // Strip the prefix, return null if the name doesn't match.
33
- function matchBridgePrefix(name, transportPrefix) {
34
- if (name.startsWith(transportPrefix)) {
35
- const tail = name.slice(transportPrefix.length);
36
- return tail.length > 0 ? tail : null;
37
- }
38
- if (name.startsWith("BRIDGE_")) {
39
- const tail = name.slice("BRIDGE_".length);
40
- return tail.length > 0 ? tail : null;
41
- }
42
- return null;
43
- }
20
+ import { scanEnvOptions } from "@mulmoclaude/common";
44
21
  /**
45
22
  * Read `<TRANSPORT>_BRIDGE_*` and `BRIDGE_*` env vars into a
46
23
  * lowerCamelCase-keyed bag ready to hand to `createBridgeClient`.
@@ -64,25 +41,5 @@ function matchBridgePrefix(name, transportPrefix) {
64
41
  */
65
42
  export function readBridgeEnvOptions(transportId, env) {
66
43
  const transportPrefix = `${transportId.toUpperCase().replace(/-/g, "_")}_BRIDGE_`;
67
- const shared = {};
68
- const specific = {};
69
- for (const [name, value] of Object.entries(env)) {
70
- if (typeof value !== "string" || value.length === 0)
71
- continue;
72
- const tail = matchBridgePrefix(name, transportPrefix);
73
- if (tail === null)
74
- continue;
75
- const key = snakeToLowerCamel(tail);
76
- if (!key)
77
- continue;
78
- if (name.startsWith(transportPrefix)) {
79
- specific[key] = value;
80
- }
81
- else {
82
- shared[key] = value;
83
- }
84
- }
85
- // Transport-specific overrides shared on conflict — spread order
86
- // (shared first, then specific) gives exactly that behaviour.
87
- return { ...shared, ...specific };
44
+ return scanEnvOptions(env, { prefixes: ["BRIDGE_", transportPrefix] });
88
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulmobridge/client",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "Socket.io client library for MulmoBridge — shared by all bridge implementations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -46,10 +46,11 @@
46
46
  "author": "Receptron Team",
47
47
  "dependencies": {
48
48
  "@mulmobridge/protocol": "^0.1.4",
49
+ "@mulmoclaude/common": "^1.1.0",
49
50
  "socket.io-client": "^4.0.0"
50
51
  },
51
52
  "devDependencies": {
52
- "@types/node": "^26.1.0",
53
+ "@types/node": "^26.1.1",
53
54
  "typescript": "^6.0.3"
54
55
  }
55
56
  }