@relaymesh/relaybus-http 0.0.4 → 0.0.6
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/package.json +2 -4
- package/src/core.ts +203 -0
- package/src/index.ts +1 -1
- package/test/http.test.ts +1 -1
- package/tsconfig.json +1 -5
package/package.json
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relaymesh/relaybus-http",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsc -p tsconfig.json",
|
|
8
8
|
"test": "vitest run"
|
|
9
9
|
},
|
|
10
|
-
"dependencies": {
|
|
11
|
-
"@relaymesh/relaybus-core": "workspace:*"
|
|
12
|
-
}
|
|
10
|
+
"dependencies": {}
|
|
13
11
|
}
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export type DecodedMessage = {
|
|
4
|
+
v: "v1";
|
|
5
|
+
id: string;
|
|
6
|
+
topic: string;
|
|
7
|
+
ts: Date;
|
|
8
|
+
contentType: string;
|
|
9
|
+
payload: Buffer;
|
|
10
|
+
meta: Record<string, string>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type OutgoingMessage = {
|
|
14
|
+
id?: string;
|
|
15
|
+
topic: string;
|
|
16
|
+
ts?: Date;
|
|
17
|
+
contentType?: string;
|
|
18
|
+
payload: Buffer | Uint8Array;
|
|
19
|
+
meta?: Record<string, string>;
|
|
20
|
+
v?: "v1";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type NormalizeOptions = {
|
|
24
|
+
now?: () => Date;
|
|
25
|
+
idGenerator?: () => string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type EnvelopeJSON = {
|
|
29
|
+
v: string;
|
|
30
|
+
id: string;
|
|
31
|
+
topic: string;
|
|
32
|
+
ts: string;
|
|
33
|
+
content_type: string;
|
|
34
|
+
payload_b64: string;
|
|
35
|
+
meta: Record<string, string>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type NormalizedMessage = {
|
|
39
|
+
id: string;
|
|
40
|
+
topic: string;
|
|
41
|
+
ts: Date;
|
|
42
|
+
contentType: string;
|
|
43
|
+
payload: Buffer;
|
|
44
|
+
meta: Record<string, string>;
|
|
45
|
+
v: "v1";
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
49
|
+
|
|
50
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
51
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertStringField(value: unknown, field: string): string {
|
|
55
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
56
|
+
throw new Error(`invalid ${field}`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertMeta(value: unknown): Record<string, string> {
|
|
62
|
+
if (!isRecord(value)) {
|
|
63
|
+
throw new Error("invalid meta");
|
|
64
|
+
}
|
|
65
|
+
const meta: Record<string, string> = {};
|
|
66
|
+
for (const [k, v] of Object.entries(value)) {
|
|
67
|
+
if (typeof v !== "string") {
|
|
68
|
+
throw new Error("invalid meta");
|
|
69
|
+
}
|
|
70
|
+
meta[k] = v;
|
|
71
|
+
}
|
|
72
|
+
return meta;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeMessage(message: OutgoingMessage, options?: NormalizeOptions): NormalizedMessage {
|
|
76
|
+
if (!message) {
|
|
77
|
+
throw new Error("message is required");
|
|
78
|
+
}
|
|
79
|
+
if (!message.topic) {
|
|
80
|
+
throw new Error("topic is required");
|
|
81
|
+
}
|
|
82
|
+
if (message.payload === undefined || message.payload === null) {
|
|
83
|
+
throw new Error("payload is required");
|
|
84
|
+
}
|
|
85
|
+
const payload = Buffer.isBuffer(message.payload)
|
|
86
|
+
? message.payload
|
|
87
|
+
: Buffer.from(message.payload);
|
|
88
|
+
|
|
89
|
+
const now = options?.now ?? (() => new Date());
|
|
90
|
+
const idGenerator = options?.idGenerator ?? randomUUID;
|
|
91
|
+
const id = message.id && message.id.length > 0 ? message.id : idGenerator();
|
|
92
|
+
if (!id) {
|
|
93
|
+
throw new Error("id is required");
|
|
94
|
+
}
|
|
95
|
+
const ts = message.ts ?? now();
|
|
96
|
+
if (!(ts instanceof Date) || Number.isNaN(ts.getTime())) {
|
|
97
|
+
throw new Error("invalid ts");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const meta: Record<string, string> = {};
|
|
101
|
+
if (message.meta) {
|
|
102
|
+
for (const [k, v] of Object.entries(message.meta)) {
|
|
103
|
+
if (typeof v !== "string") {
|
|
104
|
+
throw new Error("invalid meta");
|
|
105
|
+
}
|
|
106
|
+
meta[k] = v;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const contentType = message.contentType && message.contentType.length > 0
|
|
111
|
+
? message.contentType
|
|
112
|
+
: DEFAULT_CONTENT_TYPE;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
id,
|
|
116
|
+
topic: message.topic,
|
|
117
|
+
ts,
|
|
118
|
+
contentType,
|
|
119
|
+
payload,
|
|
120
|
+
meta,
|
|
121
|
+
v: "v1"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isValidBase64(value: string): boolean {
|
|
126
|
+
if (value === "") {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (value.length % 4 !== 0) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const decoded = Buffer.from(value, "base64");
|
|
136
|
+
return decoded.toString("base64") === value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function decodeEnvelope(jsonBytes: Buffer | string): DecodedMessage {
|
|
140
|
+
const raw = Buffer.isBuffer(jsonBytes) ? jsonBytes.toString("utf8") : jsonBytes;
|
|
141
|
+
let parsed: unknown;
|
|
142
|
+
try {
|
|
143
|
+
parsed = JSON.parse(raw);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
throw new Error("invalid json");
|
|
146
|
+
}
|
|
147
|
+
if (!isRecord(parsed)) {
|
|
148
|
+
throw new Error("invalid envelope");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const env = parsed as EnvelopeJSON;
|
|
152
|
+
if (env.v !== "v1") {
|
|
153
|
+
throw new Error("invalid v");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const id = assertStringField(env.id, "id");
|
|
157
|
+
const topic = assertStringField(env.topic, "topic");
|
|
158
|
+
const tsRaw = assertStringField(env.ts, "ts");
|
|
159
|
+
const contentType = assertStringField(env.content_type, "content_type");
|
|
160
|
+
|
|
161
|
+
if (typeof env.payload_b64 !== "string") {
|
|
162
|
+
throw new Error("invalid payload_b64");
|
|
163
|
+
}
|
|
164
|
+
if (!isValidBase64(env.payload_b64)) {
|
|
165
|
+
throw new Error("invalid payload_b64");
|
|
166
|
+
}
|
|
167
|
+
const payload = Buffer.from(env.payload_b64, "base64");
|
|
168
|
+
|
|
169
|
+
const meta = assertMeta(env.meta);
|
|
170
|
+
const ts = new Date(tsRaw);
|
|
171
|
+
if (Number.isNaN(ts.getTime())) {
|
|
172
|
+
throw new Error("invalid ts");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
v: "v1",
|
|
177
|
+
id,
|
|
178
|
+
topic,
|
|
179
|
+
ts,
|
|
180
|
+
contentType,
|
|
181
|
+
payload,
|
|
182
|
+
meta
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function encodeEnvelope(message: OutgoingMessage, options?: NormalizeOptions): Buffer {
|
|
187
|
+
const normalized = normalizeMessage(message, options);
|
|
188
|
+
const envelope: EnvelopeJSON = {
|
|
189
|
+
v: normalized.v,
|
|
190
|
+
id: normalized.id,
|
|
191
|
+
topic: normalized.topic,
|
|
192
|
+
ts: normalized.ts.toISOString(),
|
|
193
|
+
content_type: normalized.contentType,
|
|
194
|
+
payload_b64: normalized.payload.toString("base64"),
|
|
195
|
+
meta: normalized.meta
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return Buffer.from(JSON.stringify(envelope), "utf8");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const defaults = {
|
|
202
|
+
DEFAULT_CONTENT_TYPE
|
|
203
|
+
};
|
package/src/index.ts
CHANGED
package/test/http.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { HttpPublisher, HttpSubscriber } from "../src/index";
|
|
3
|
-
import { decodeEnvelope } from "
|
|
3
|
+
import { decodeEnvelope } from "../src/core";
|
|
4
4
|
|
|
5
5
|
describe("HttpPublisher", () => {
|
|
6
6
|
it("posts encoded envelope", async () => {
|
package/tsconfig.json
CHANGED