@intx/mime 0.1.2
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 +32 -0
- package/package.json +17 -0
- package/src/index.test.ts +978 -0
- package/src/index.ts +37 -0
- package/src/mail-builder.test.ts +740 -0
- package/src/mail-builder.ts +492 -0
- package/src/mime.ts +990 -0
- package/src/pgp-sign.ts +178 -0
- package/tsconfig.json +4 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builders for InboundMessage and OutboundMessage shapes.
|
|
3
|
+
*
|
|
4
|
+
* Constructing these by hand requires assembling MessageRef, MessageHeaders,
|
|
5
|
+
* payload envelopes, signature status, and other mail-shaped fields that the
|
|
6
|
+
* transport normally produces after parsing wire bytes. These builders
|
|
7
|
+
* collapse that boilerplate behind two factories with sensible defaults.
|
|
8
|
+
*
|
|
9
|
+
* The builders use the parsed-shape MessageHeaders from
|
|
10
|
+
* @intx/types/runtime (where date is an ISO string), NOT the
|
|
11
|
+
* wire-shape MessageHeaders local to this package (where date is a Date
|
|
12
|
+
* object and headers are serialised to RFC 2822 bytes via assembleMessage).
|
|
13
|
+
*
|
|
14
|
+
* Consumers import the message types from @intx/types directly; the
|
|
15
|
+
* @intx/mime barrel does not re-export them.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { type } from "arktype";
|
|
19
|
+
import type {
|
|
20
|
+
InboundMessage,
|
|
21
|
+
MessageAttachment,
|
|
22
|
+
MessageHeaders,
|
|
23
|
+
MessageRef,
|
|
24
|
+
OutboundMessage,
|
|
25
|
+
} from "@intx/types/runtime";
|
|
26
|
+
import { InterchangeType, SignatureStatus } from "@intx/types/runtime";
|
|
27
|
+
import { generateMessageId } from "./mime";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default schema version for structured payloads. Matches
|
|
31
|
+
* docs/MESSAGE.md § Payload Structure, which specifies "version": "1" as
|
|
32
|
+
* the current schema version for every Interchange payload type. Audit
|
|
33
|
+
* this default whenever the documented schema version increments.
|
|
34
|
+
*/
|
|
35
|
+
const DEFAULT_PAYLOAD_VERSION = "1";
|
|
36
|
+
|
|
37
|
+
const MESSAGE_ID_RE = /^<[^<>\s@]+@[^<>\s@]+>$/;
|
|
38
|
+
const ADDRESS_RE = /^[^@\s]+@[^@\s]+$/;
|
|
39
|
+
|
|
40
|
+
const CONVERSATION_TYPE_PREFIX = "conversation.";
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// InboundMessage builder
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Structured payload envelope for an inbound message. `version` defaults to
|
|
48
|
+
* the current schema version per docs/MESSAGE.md.
|
|
49
|
+
*/
|
|
50
|
+
export type InboundPayloadInput = {
|
|
51
|
+
type: InterchangeType;
|
|
52
|
+
body: Record<string, unknown>;
|
|
53
|
+
version?: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type CreateInboundMessageOpts = {
|
|
57
|
+
from: string;
|
|
58
|
+
to: string | string[];
|
|
59
|
+
|
|
60
|
+
/** Plain-text body. Mutually exclusive with `payload`. */
|
|
61
|
+
content?: string;
|
|
62
|
+
|
|
63
|
+
/** Structured JSON envelope. Mutually exclusive with `content`. */
|
|
64
|
+
payload?: InboundPayloadInput;
|
|
65
|
+
|
|
66
|
+
cc?: string | string[];
|
|
67
|
+
subject?: string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Defaults to `new Date().toISOString()`. Accepts Date or any string
|
|
71
|
+
* parseable by `new Date(...)`; stored as an ISO 8601 string.
|
|
72
|
+
*/
|
|
73
|
+
date?: Date | string;
|
|
74
|
+
|
|
75
|
+
/** Defaults to `generateMessageId(from)`. Must be of the form `<id@host>`. */
|
|
76
|
+
messageId?: string;
|
|
77
|
+
|
|
78
|
+
inReplyTo?: string;
|
|
79
|
+
references?: string[];
|
|
80
|
+
listId?: string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Interchange-Type header value. Auto-derived from `payload.type` when a
|
|
84
|
+
* payload is supplied; throws if explicitly set to a value that conflicts
|
|
85
|
+
* with `payload.type`.
|
|
86
|
+
*/
|
|
87
|
+
interchangeType?: InterchangeType;
|
|
88
|
+
|
|
89
|
+
correlationId?: string;
|
|
90
|
+
tenantId?: string;
|
|
91
|
+
agentId?: string;
|
|
92
|
+
sessionId?: string;
|
|
93
|
+
offeringId?: string;
|
|
94
|
+
schemaVersion?: string;
|
|
95
|
+
traceparent?: string;
|
|
96
|
+
tracestate?: string;
|
|
97
|
+
|
|
98
|
+
attachments?: MessageAttachment[];
|
|
99
|
+
|
|
100
|
+
/** Merged with `{ uid: 1, mailbox: "INBOX" }`. */
|
|
101
|
+
ref?: Partial<MessageRef>;
|
|
102
|
+
|
|
103
|
+
flags?: string[];
|
|
104
|
+
|
|
105
|
+
/** Defaults to `"missing"`. */
|
|
106
|
+
signatureStatus?: SignatureStatus;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export function createInboundMessage(
|
|
110
|
+
opts: CreateInboundMessageOpts,
|
|
111
|
+
): InboundMessage {
|
|
112
|
+
const fn = "createInboundMessage";
|
|
113
|
+
|
|
114
|
+
requireAddress(opts.from, "from", fn);
|
|
115
|
+
const to = normalizeAndValidateAddressArray(opts.to, "to", fn);
|
|
116
|
+
|
|
117
|
+
validateBodyExclusivity(opts.content, opts.payload, fn);
|
|
118
|
+
|
|
119
|
+
if (opts.payload !== undefined) {
|
|
120
|
+
validateInterchangeType(opts.payload.type, "payload.type", fn);
|
|
121
|
+
if (isConversationType(opts.payload.type)) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`${fn}: conversation types must use \`content\` instead of \`payload\`; got \`payload.type\`: ${opts.payload.type}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
validatePayloadBody(opts.payload.body, "payload.body", fn);
|
|
127
|
+
if (opts.payload.version !== undefined) {
|
|
128
|
+
if (
|
|
129
|
+
typeof opts.payload.version !== "string" ||
|
|
130
|
+
opts.payload.version.length === 0
|
|
131
|
+
) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`${fn}: \`payload.version\`, when provided, must be a non-empty string`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (opts.interchangeType !== undefined) {
|
|
140
|
+
validateInterchangeType(opts.interchangeType, "interchangeType", fn);
|
|
141
|
+
if (
|
|
142
|
+
opts.payload !== undefined &&
|
|
143
|
+
opts.interchangeType !== opts.payload.type
|
|
144
|
+
) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`${fn}: \`interchangeType\` (${opts.interchangeType}) conflicts with \`payload.type\` (${opts.payload.type})`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (opts.messageId !== undefined) {
|
|
152
|
+
validateMessageId(opts.messageId, "messageId", fn);
|
|
153
|
+
}
|
|
154
|
+
if (opts.inReplyTo !== undefined) {
|
|
155
|
+
validateMessageId(opts.inReplyTo, "inReplyTo", fn);
|
|
156
|
+
}
|
|
157
|
+
if (opts.references !== undefined) {
|
|
158
|
+
if (opts.references.length === 0) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`${fn}: \`references\`, when provided, must contain at least one entry`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
opts.references.forEach((ref, i) => {
|
|
164
|
+
validateMessageId(ref, `references[${i}]`, fn);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const cc =
|
|
169
|
+
opts.cc === undefined
|
|
170
|
+
? undefined
|
|
171
|
+
: normalizeAndValidateAddressArray(opts.cc, "cc", fn);
|
|
172
|
+
|
|
173
|
+
rejectEmptyStringIfPresent(opts.content, "content", fn);
|
|
174
|
+
rejectEmptyStringIfPresent(opts.subject, "subject", fn);
|
|
175
|
+
rejectEmptyStringIfPresent(opts.listId, "listId", fn);
|
|
176
|
+
rejectEmptyStringIfPresent(opts.correlationId, "correlationId", fn);
|
|
177
|
+
rejectEmptyStringIfPresent(opts.tenantId, "tenantId", fn);
|
|
178
|
+
rejectEmptyStringIfPresent(opts.agentId, "agentId", fn);
|
|
179
|
+
rejectEmptyStringIfPresent(opts.sessionId, "sessionId", fn);
|
|
180
|
+
rejectEmptyStringIfPresent(opts.offeringId, "offeringId", fn);
|
|
181
|
+
rejectEmptyStringIfPresent(opts.schemaVersion, "schemaVersion", fn);
|
|
182
|
+
rejectEmptyStringIfPresent(opts.traceparent, "traceparent", fn);
|
|
183
|
+
rejectEmptyStringIfPresent(opts.tracestate, "tracestate", fn);
|
|
184
|
+
|
|
185
|
+
if (opts.flags !== undefined) {
|
|
186
|
+
opts.flags.forEach((flag, i) => {
|
|
187
|
+
if (typeof flag !== "string" || flag.length === 0) {
|
|
188
|
+
throw new Error(`${fn}: \`flags[${i}]\` must be a non-empty string`);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const signatureStatus = opts.signatureStatus ?? "missing";
|
|
194
|
+
const validatedStatus = SignatureStatus(signatureStatus);
|
|
195
|
+
if (validatedStatus instanceof type.errors) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`${fn}: \`signatureStatus\` is not a recognised SignatureStatus: ${validatedStatus.summary}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const date = normalizeDate(opts.date, "date", fn);
|
|
202
|
+
const messageId = opts.messageId ?? generateMessageId(opts.from);
|
|
203
|
+
const derivedInterchangeType = opts.interchangeType ?? opts.payload?.type;
|
|
204
|
+
|
|
205
|
+
const headers: MessageHeaders = { from: opts.from, to, date, messageId };
|
|
206
|
+
if (cc !== undefined) headers.cc = cc;
|
|
207
|
+
if (opts.subject !== undefined) headers.subject = opts.subject;
|
|
208
|
+
if (opts.inReplyTo !== undefined) headers.inReplyTo = opts.inReplyTo;
|
|
209
|
+
if (opts.references !== undefined) headers.references = opts.references;
|
|
210
|
+
if (opts.listId !== undefined) headers.listId = opts.listId;
|
|
211
|
+
if (derivedInterchangeType !== undefined) {
|
|
212
|
+
headers.interchangeType = derivedInterchangeType;
|
|
213
|
+
}
|
|
214
|
+
if (opts.correlationId !== undefined) {
|
|
215
|
+
headers.interchangeCorrelationId = opts.correlationId;
|
|
216
|
+
}
|
|
217
|
+
if (opts.tenantId !== undefined) headers.interchangeTenantId = opts.tenantId;
|
|
218
|
+
if (opts.agentId !== undefined) headers.interchangeAgentId = opts.agentId;
|
|
219
|
+
if (opts.sessionId !== undefined) {
|
|
220
|
+
headers.interchangeSessionId = opts.sessionId;
|
|
221
|
+
}
|
|
222
|
+
if (opts.offeringId !== undefined) {
|
|
223
|
+
headers.interchangeOfferingId = opts.offeringId;
|
|
224
|
+
}
|
|
225
|
+
if (opts.schemaVersion !== undefined) {
|
|
226
|
+
headers.interchangeSchemaVersion = opts.schemaVersion;
|
|
227
|
+
}
|
|
228
|
+
if (opts.traceparent !== undefined) headers.traceparent = opts.traceparent;
|
|
229
|
+
if (opts.tracestate !== undefined) headers.tracestate = opts.tracestate;
|
|
230
|
+
|
|
231
|
+
if (opts.ref?.uid !== undefined) {
|
|
232
|
+
if (
|
|
233
|
+
typeof opts.ref.uid !== "number" ||
|
|
234
|
+
!Number.isInteger(opts.ref.uid) ||
|
|
235
|
+
!Number.isFinite(opts.ref.uid) ||
|
|
236
|
+
opts.ref.uid < 1
|
|
237
|
+
) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`${fn}: \`ref.uid\`, when provided, must be a positive integer (IMAP UID)`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const ref: MessageRef = {
|
|
244
|
+
uid: opts.ref?.uid ?? 1,
|
|
245
|
+
mailbox: opts.ref?.mailbox ?? "INBOX",
|
|
246
|
+
};
|
|
247
|
+
if (typeof ref.mailbox !== "string" || ref.mailbox.length === 0) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`${fn}: \`ref.mailbox\`, when provided, must be a non-empty string`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const result: InboundMessage = {
|
|
254
|
+
ref,
|
|
255
|
+
headers,
|
|
256
|
+
flags: opts.flags ?? [],
|
|
257
|
+
signatureStatus,
|
|
258
|
+
};
|
|
259
|
+
if (opts.content !== undefined) result.content = opts.content;
|
|
260
|
+
if (opts.payload !== undefined) {
|
|
261
|
+
result.payload = {
|
|
262
|
+
type: opts.payload.type,
|
|
263
|
+
version: opts.payload.version ?? DEFAULT_PAYLOAD_VERSION,
|
|
264
|
+
body: opts.payload.body,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (opts.attachments !== undefined && opts.attachments.length > 0) {
|
|
268
|
+
result.attachments = opts.attachments;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// OutboundMessage builder
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
export type CreateOutboundMessageOpts = {
|
|
279
|
+
to: string | string[];
|
|
280
|
+
|
|
281
|
+
/** Interchange payload type. Determines content vs payload semantics. */
|
|
282
|
+
type: InterchangeType;
|
|
283
|
+
|
|
284
|
+
/** Plain-text body. Mutually exclusive with `payload`. */
|
|
285
|
+
content?: string;
|
|
286
|
+
|
|
287
|
+
/** Structured JSON envelope body. Mutually exclusive with `content`. */
|
|
288
|
+
payload?: Record<string, unknown>;
|
|
289
|
+
|
|
290
|
+
cc?: string | string[];
|
|
291
|
+
subject?: string;
|
|
292
|
+
|
|
293
|
+
/** Human-readable summary used as the text/plain part for structured types. */
|
|
294
|
+
summary?: string;
|
|
295
|
+
|
|
296
|
+
inReplyTo?: string;
|
|
297
|
+
correlationId?: string;
|
|
298
|
+
sessionId?: string;
|
|
299
|
+
tenantId?: string;
|
|
300
|
+
|
|
301
|
+
attachments?: MessageAttachment[];
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export function createOutboundMessage(
|
|
305
|
+
opts: CreateOutboundMessageOpts,
|
|
306
|
+
): OutboundMessage {
|
|
307
|
+
const fn = "createOutboundMessage";
|
|
308
|
+
|
|
309
|
+
validateInterchangeType(opts.type, "type", fn);
|
|
310
|
+
// Validate addresses without mutating the source shape; the OutboundMessage
|
|
311
|
+
// type preserves `string | string[]` and downstream consumers handle both.
|
|
312
|
+
normalizeAndValidateAddressArray(opts.to, "to", fn);
|
|
313
|
+
if (opts.cc !== undefined) {
|
|
314
|
+
normalizeAndValidateAddressArray(opts.cc, "cc", fn);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
validateBodyExclusivity(opts.content, opts.payload, fn);
|
|
318
|
+
|
|
319
|
+
if (isConversationType(opts.type)) {
|
|
320
|
+
if (opts.payload !== undefined) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
`${fn}: conversation \`type\` ${opts.type} must use \`content\` instead of \`payload\``,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (opts.content === undefined) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`${fn}: conversation \`type\` ${opts.type} requires \`content\``,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
if (opts.content !== undefined) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`${fn}: non-conversation \`type\` ${opts.type} must use \`payload\` instead of \`content\``,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
if (opts.payload === undefined) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`${fn}: non-conversation \`type\` ${opts.type} requires \`payload\``,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (opts.payload !== undefined) {
|
|
343
|
+
validatePayloadBody(opts.payload, "payload", fn);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (opts.inReplyTo !== undefined) {
|
|
347
|
+
validateMessageId(opts.inReplyTo, "inReplyTo", fn);
|
|
348
|
+
}
|
|
349
|
+
rejectEmptyStringIfPresent(opts.content, "content", fn);
|
|
350
|
+
rejectEmptyStringIfPresent(opts.subject, "subject", fn);
|
|
351
|
+
rejectEmptyStringIfPresent(opts.summary, "summary", fn);
|
|
352
|
+
rejectEmptyStringIfPresent(opts.correlationId, "correlationId", fn);
|
|
353
|
+
rejectEmptyStringIfPresent(opts.sessionId, "sessionId", fn);
|
|
354
|
+
rejectEmptyStringIfPresent(opts.tenantId, "tenantId", fn);
|
|
355
|
+
|
|
356
|
+
const result: OutboundMessage = { to: opts.to, type: opts.type };
|
|
357
|
+
if (opts.cc !== undefined) result.cc = opts.cc;
|
|
358
|
+
if (opts.subject !== undefined) result.subject = opts.subject;
|
|
359
|
+
if (opts.content !== undefined) result.content = opts.content;
|
|
360
|
+
if (opts.payload !== undefined) result.payload = opts.payload;
|
|
361
|
+
if (opts.summary !== undefined) result.summary = opts.summary;
|
|
362
|
+
if (opts.attachments !== undefined && opts.attachments.length > 0) {
|
|
363
|
+
result.attachments = opts.attachments;
|
|
364
|
+
}
|
|
365
|
+
if (opts.inReplyTo !== undefined) result.inReplyTo = opts.inReplyTo;
|
|
366
|
+
if (opts.correlationId !== undefined) {
|
|
367
|
+
result.correlationId = opts.correlationId;
|
|
368
|
+
}
|
|
369
|
+
if (opts.sessionId !== undefined) result.sessionId = opts.sessionId;
|
|
370
|
+
if (opts.tenantId !== undefined) result.tenantId = opts.tenantId;
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// Validation helpers
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
function rejectEmptyStringIfPresent(
|
|
379
|
+
value: string | undefined,
|
|
380
|
+
field: string,
|
|
381
|
+
fn: string,
|
|
382
|
+
): void {
|
|
383
|
+
if (value !== undefined && value.length === 0) {
|
|
384
|
+
throw new Error(
|
|
385
|
+
`${fn}: \`${field}\`, when provided, must be a non-empty string`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function requireAddress(value: unknown, field: string, fn: string): void {
|
|
391
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
392
|
+
throw new Error(`${fn}: \`${field}\` must be a non-empty string`);
|
|
393
|
+
}
|
|
394
|
+
if (!ADDRESS_RE.test(value)) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`${fn}: \`${field}\` must be an RFC 5322 address of the form \`local@domain\`; got: ${value}`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function normalizeAndValidateAddressArray(
|
|
402
|
+
input: string | string[],
|
|
403
|
+
field: string,
|
|
404
|
+
fn: string,
|
|
405
|
+
): string[] {
|
|
406
|
+
if (typeof input === "string") {
|
|
407
|
+
requireAddress(input, field, fn);
|
|
408
|
+
return [input];
|
|
409
|
+
}
|
|
410
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`${fn}: \`${field}\` must contain at least one recipient address`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
input.forEach((entry, i) => {
|
|
416
|
+
requireAddress(entry, `${field}[${i}]`, fn);
|
|
417
|
+
});
|
|
418
|
+
return input;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function validatePayloadBody(value: unknown, field: string, fn: string): void {
|
|
422
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
`${fn}: \`${field}\` must be a plain object (got ${
|
|
425
|
+
value === null ? "null" : Array.isArray(value) ? "array" : typeof value
|
|
426
|
+
})`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function isConversationType(t: InterchangeType): boolean {
|
|
432
|
+
return t.startsWith(CONVERSATION_TYPE_PREFIX);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function validateInterchangeType(
|
|
436
|
+
value: unknown,
|
|
437
|
+
field: string,
|
|
438
|
+
fn: string,
|
|
439
|
+
): void {
|
|
440
|
+
const validated = InterchangeType(value);
|
|
441
|
+
if (validated instanceof type.errors) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
`${fn}: \`${field}\` is not a valid InterchangeType: ${validated.summary}`,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function validateMessageId(value: string, field: string, fn: string): void {
|
|
449
|
+
if (!MESSAGE_ID_RE.test(value)) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
`${fn}: \`${field}\` must be an RFC 2822 message identifier of the form \`<id@host>\`; got: ${value}`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function normalizeDate(
|
|
457
|
+
input: Date | string | undefined,
|
|
458
|
+
field: string,
|
|
459
|
+
fn: string,
|
|
460
|
+
): string {
|
|
461
|
+
if (input === undefined) return new Date().toISOString();
|
|
462
|
+
if (input instanceof Date) {
|
|
463
|
+
if (Number.isNaN(input.getTime())) {
|
|
464
|
+
throw new Error(`${fn}: \`${field}\` is an Invalid Date`);
|
|
465
|
+
}
|
|
466
|
+
return input.toISOString();
|
|
467
|
+
}
|
|
468
|
+
if (typeof input !== "string" || input.length === 0) {
|
|
469
|
+
throw new Error(
|
|
470
|
+
`${fn}: \`${field}\`, when provided, must be a Date or a non-empty string`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
const parsed = new Date(input);
|
|
474
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
475
|
+
throw new Error(
|
|
476
|
+
`${fn}: \`${field}\` is not a parseable date string: ${input}`,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
return parsed.toISOString();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function validateBodyExclusivity(
|
|
483
|
+
content: unknown,
|
|
484
|
+
payload: unknown,
|
|
485
|
+
fn: string,
|
|
486
|
+
): void {
|
|
487
|
+
if (content !== undefined && payload !== undefined) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`${fn}: \`content\` and \`payload\` are mutually exclusive; provide at most one`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|