@primitivedotdev/sdk 1.23.0 → 1.25.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 +24 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +3 -3
- package/dist/{api-mauNzbhi.js → api-BXlPoOdE.js} +127 -2
- package/dist/{index-C11B41Hb.d.ts → index-BGyYkqRn.d.ts} +339 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/interactions/index.d.ts +132 -0
- package/dist/interactions/index.js +494 -0
- package/dist/openapi/index.js +1 -1
- package/dist/{operations.generated-COaLBzbH.js → operations.generated-CPcHOHgd.js} +580 -0
- package/dist/parser/address.d.ts +1 -1
- package/dist/parser/index.d.ts +1 -1
- package/dist/payloads/index.js +1 -1
- package/dist/{payloads-CQtSsUT6.js → payloads-BfJAtqW9.js} +8 -6
- package/dist/sign-m3ZStnNw.d.ts +292 -0
- package/dist/x402/index.d.ts +1 -290
- package/package.json +11 -6
- /package/dist/{address-parser-CJqzx713.d.ts → address-parser-B7L4E0zP.d.ts} +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { i as InteractionEnvelope } from "../sign-m3ZStnNw.js";
|
|
2
|
+
|
|
3
|
+
//#region src/interactions/classify.d.ts
|
|
4
|
+
interface SignalContentPart {
|
|
5
|
+
filename: string | null;
|
|
6
|
+
contentType: string | null;
|
|
7
|
+
}
|
|
8
|
+
type SignalContentInventory = {
|
|
9
|
+
status: "complete";
|
|
10
|
+
parts: readonly SignalContentPart[];
|
|
11
|
+
} | {
|
|
12
|
+
status: "unavailable";
|
|
13
|
+
};
|
|
14
|
+
type SignalContentBodies = {
|
|
15
|
+
status: "complete";
|
|
16
|
+
text: string | null;
|
|
17
|
+
html: string | null;
|
|
18
|
+
} | {
|
|
19
|
+
status: "unavailable";
|
|
20
|
+
};
|
|
21
|
+
interface SignalContentInput {
|
|
22
|
+
/** Complete means the full outer inventory, including offloaded parts. */
|
|
23
|
+
inventory: SignalContentInventory;
|
|
24
|
+
bodies: SignalContentBodies;
|
|
25
|
+
/** Decoded bytes for the unique canonical part, never base64 text. */
|
|
26
|
+
canonicalPartBytes: Uint8Array | null;
|
|
27
|
+
}
|
|
28
|
+
type SignalContentReason = "inventory_unavailable" | "no_canonical_part" | "duplicate_canonical_parts" | "additional_parts" | "part_unavailable" | "invalid_interaction" | "unsupported_signal" | "unsupported_content_type" | "bodies_unavailable" | "html_present" | "text_mismatch" | "informational_signal";
|
|
29
|
+
interface SignalContentResult {
|
|
30
|
+
classification: "plain" | "informational_only" | "mixed_or_unsupported" | "unavailable";
|
|
31
|
+
reason: SignalContentReason;
|
|
32
|
+
/** Syntax/source evidence only. Never authentication or an interpreted receipt. */
|
|
33
|
+
interaction?: InteractionParseResult;
|
|
34
|
+
}
|
|
35
|
+
/** Pure content classification. It establishes no sender, receipt or task authority. */
|
|
36
|
+
declare function classifySignalContent(input: SignalContentInput): SignalContentResult;
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/interactions/signals.d.ts
|
|
39
|
+
/** Explicit preparation of optional email signals. This module never sends automatically. */
|
|
40
|
+
interface SignalParent {
|
|
41
|
+
accountScope: string;
|
|
42
|
+
/** Authenticated original sender, as a bare mailbox. */
|
|
43
|
+
from: string;
|
|
44
|
+
/** Explicit authorized recipient identity to send from, as a bare mailbox. */
|
|
45
|
+
to: string;
|
|
46
|
+
messageId: string | null;
|
|
47
|
+
subject: string | null;
|
|
48
|
+
references: readonly string[];
|
|
49
|
+
}
|
|
50
|
+
type SignalInput = {
|
|
51
|
+
parent: SignalParent;
|
|
52
|
+
} & ({
|
|
53
|
+
kind: "ack";
|
|
54
|
+
status: "received" | "will_process" | "will_not_process";
|
|
55
|
+
note?: string;
|
|
56
|
+
} | {
|
|
57
|
+
kind: "read";
|
|
58
|
+
} | {
|
|
59
|
+
kind: "working";
|
|
60
|
+
expiresAtMs: number;
|
|
61
|
+
});
|
|
62
|
+
interface SignalDependencies {
|
|
63
|
+
uuid: () => string;
|
|
64
|
+
/** Unix time in integer milliseconds. */
|
|
65
|
+
now: () => number;
|
|
66
|
+
}
|
|
67
|
+
/** Persist this whole value before dispatch. requestJson fixes the ordinary send body. */
|
|
68
|
+
interface PreparedSignal {
|
|
69
|
+
readonly accountScope: string;
|
|
70
|
+
readonly preparedAtMs: number;
|
|
71
|
+
readonly expiresAtMs: number | null;
|
|
72
|
+
readonly idempotencyKey: string;
|
|
73
|
+
readonly requestJson: string;
|
|
74
|
+
}
|
|
75
|
+
type SignalPreparation = {
|
|
76
|
+
status: "waiting_on_parent";
|
|
77
|
+
} | {
|
|
78
|
+
status: "prepared";
|
|
79
|
+
prepared: PreparedSignal;
|
|
80
|
+
};
|
|
81
|
+
interface SignalSendBody {
|
|
82
|
+
from: string;
|
|
83
|
+
to: string;
|
|
84
|
+
subject: string;
|
|
85
|
+
body_text: string;
|
|
86
|
+
in_reply_to: string;
|
|
87
|
+
references: string[];
|
|
88
|
+
attachments: {
|
|
89
|
+
filename: string;
|
|
90
|
+
content_type: string;
|
|
91
|
+
content_base64: string;
|
|
92
|
+
}[];
|
|
93
|
+
}
|
|
94
|
+
type SignalSendResult<T> = {
|
|
95
|
+
status: "response";
|
|
96
|
+
result: T;
|
|
97
|
+
} | {
|
|
98
|
+
status: "expired";
|
|
99
|
+
idempotencyKey: string;
|
|
100
|
+
};
|
|
101
|
+
declare function prepareSignalEmail(input: SignalInput, dependencies: SignalDependencies): SignalPreparation;
|
|
102
|
+
/** One ordinary send attempt; the caller owns persistence, reconciliation and retries. */
|
|
103
|
+
declare function sendPreparedSignal<T>(sendMail: (body: SignalSendBody, idempotencyKey: string) => Promise<T>, prepared: PreparedSignal, options: {
|
|
104
|
+
accountScope: string;
|
|
105
|
+
now: () => number;
|
|
106
|
+
}): Promise<SignalSendResult<T>>;
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/interactions/index.d.ts
|
|
109
|
+
declare const MAX_INTERACTION_BYTES = 65536;
|
|
110
|
+
declare const MAX_INTERACTION_DEPTH = 64;
|
|
111
|
+
type InteractionValidationResult = {
|
|
112
|
+
status: "valid";
|
|
113
|
+
envelope: InteractionEnvelope & Record<string, unknown>;
|
|
114
|
+
} | {
|
|
115
|
+
status: "unsupported";
|
|
116
|
+
version: number;
|
|
117
|
+
} | {
|
|
118
|
+
status: "invalid";
|
|
119
|
+
reason: "invalid_input" | "too_large" | "invalid_json" | "invalid_envelope";
|
|
120
|
+
};
|
|
121
|
+
type InteractionParseResult = InteractionValidationResult & {
|
|
122
|
+
/** Present on valid/unsupported input. Bytes are a defensive copy, never reserialized. */source?: {
|
|
123
|
+
text: string;
|
|
124
|
+
bytes?: Uint8Array;
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
/** Parse at most 64 KiB of strict UTF-8 JSON. Unknown protocols remain valid. */
|
|
128
|
+
declare function parseInteractionEnvelope(input: string | Uint8Array): InteractionParseResult;
|
|
129
|
+
/** Validate a decoded JSON value. Cannot detect lost duplicate keys or original bytes. */
|
|
130
|
+
declare function validateInteractionEnvelope(input: unknown): InteractionValidationResult;
|
|
131
|
+
//#endregion
|
|
132
|
+
export { type InteractionEnvelope, InteractionParseResult, InteractionValidationResult, MAX_INTERACTION_BYTES, MAX_INTERACTION_DEPTH, type PreparedSignal, type SignalContentBodies, type SignalContentInput, type SignalContentInventory, type SignalContentPart, type SignalContentReason, type SignalContentResult, type SignalDependencies, type SignalInput, type SignalParent, type SignalPreparation, type SignalSendBody, type SignalSendResult, classifySignalContent, parseInteractionEnvelope, prepareSignalEmail, sendPreparedSignal, validateInteractionEnvelope };
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
//#region src/interactions/signals.ts
|
|
2
|
+
const UUID$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3
|
+
const ATOM = "[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+";
|
|
4
|
+
const MAILBOX = new RegExp(`^${ATOM}(?:\\.${ATOM})*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$`);
|
|
5
|
+
const ID = /^[!-~]+@[!-~]+$/;
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
function check(condition, message) {
|
|
8
|
+
if (!condition) throw new TypeError(message);
|
|
9
|
+
}
|
|
10
|
+
function header(value, limit) {
|
|
11
|
+
check(typeof value === "string" && ![...value].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127) && !/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(value) && encoder.encode(value).length <= limit, "invalid header");
|
|
12
|
+
}
|
|
13
|
+
function wireId(value) {
|
|
14
|
+
check(typeof value === "string", "invalid Message-ID");
|
|
15
|
+
let start = 0;
|
|
16
|
+
let end = value.length;
|
|
17
|
+
while (start < end && value.charCodeAt(start) === 32) start++;
|
|
18
|
+
while (end > start && value.charCodeAt(end - 1) === 32) end--;
|
|
19
|
+
let id = value.slice(start, end);
|
|
20
|
+
if (id.startsWith("<") && id.endsWith(">")) id = id.slice(1, -1);
|
|
21
|
+
check(id.length <= 996 && ID.test(id) && !/[<>]/.test(id) && id.split("@").length === 2, "invalid Message-ID");
|
|
22
|
+
return `<${id}>`;
|
|
23
|
+
}
|
|
24
|
+
function milliseconds(value) {
|
|
25
|
+
check(Number.isSafeInteger(value) && value >= 0 && value <= 0xe677d21fdbff, "invalid clock or expiry");
|
|
26
|
+
}
|
|
27
|
+
function signalJson(value) {
|
|
28
|
+
return JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
29
|
+
}
|
|
30
|
+
function base64(bytes) {
|
|
31
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
32
|
+
let out = "";
|
|
33
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
34
|
+
const a = bytes[i] ?? 0, b = bytes[i + 1] ?? 0, c = bytes[i + 2] ?? 0;
|
|
35
|
+
out += alphabet[a >> 2];
|
|
36
|
+
out += alphabet[(a & 3) << 4 | b >> 4];
|
|
37
|
+
out += i + 1 < bytes.length ? alphabet[(b & 15) << 2 | c >> 6] : "=";
|
|
38
|
+
out += i + 2 < bytes.length ? alphabet[c & 63] : "=";
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function prepareSignalEmail(input, dependencies) {
|
|
43
|
+
const p = input.parent;
|
|
44
|
+
if (p.messageId === null || p.messageId === "") return { status: "waiting_on_parent" };
|
|
45
|
+
const target = wireId(p.messageId);
|
|
46
|
+
for (const address of [p.from, p.to]) {
|
|
47
|
+
header(address, 320);
|
|
48
|
+
check(MAILBOX.test(address), "use one bare mailbox per address");
|
|
49
|
+
}
|
|
50
|
+
const subject = p.subject ?? "";
|
|
51
|
+
header(subject, 998);
|
|
52
|
+
header(p.accountScope, 256);
|
|
53
|
+
check(p.accountScope.length > 0, "accountScope is required");
|
|
54
|
+
check(p.references.length <= 1e3, "too many references");
|
|
55
|
+
let references = p.references.map(wireId).filter((id) => id !== target);
|
|
56
|
+
references.push(target);
|
|
57
|
+
let length = references.reduce((total, id) => total + id.length + 1, -1);
|
|
58
|
+
let start = 0;
|
|
59
|
+
while (references.length - start > 100 || length > 8192) {
|
|
60
|
+
length -= (references[start]?.length ?? 0) + 1;
|
|
61
|
+
start++;
|
|
62
|
+
}
|
|
63
|
+
references = references.slice(start);
|
|
64
|
+
const now = dependencies.now();
|
|
65
|
+
milliseconds(now);
|
|
66
|
+
let expires = null;
|
|
67
|
+
const payload = { subject_message_id: target };
|
|
68
|
+
if (input.kind === "ack") {
|
|
69
|
+
check(signalText("ack", input.status) !== "", "invalid ACK status");
|
|
70
|
+
payload.status = input.status;
|
|
71
|
+
if (input.note !== void 0) {
|
|
72
|
+
check(typeof input.note === "string" && input.note.length <= 2e3 && !input.note.includes("\0") && !/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(input.note), "invalid note");
|
|
73
|
+
payload.note = input.note;
|
|
74
|
+
}
|
|
75
|
+
} else if (input.kind !== "read") {
|
|
76
|
+
check(input.kind === "working", "invalid signal kind");
|
|
77
|
+
milliseconds(input.expiresAtMs);
|
|
78
|
+
check(input.expiresAtMs > now && input.expiresAtMs - now <= 6e4, "working expiry must be within 60 seconds");
|
|
79
|
+
expires = input.expiresAtMs;
|
|
80
|
+
}
|
|
81
|
+
const interaction = dependencies.uuid().toLowerCase(), step = dependencies.uuid().toLowerCase();
|
|
82
|
+
check(UUID$1.test(interaction) && UUID$1.test(step) && interaction !== step, "two distinct UUIDs are required");
|
|
83
|
+
const envelope = signalJson({
|
|
84
|
+
interaction_version: 1,
|
|
85
|
+
interaction_id: `${interaction}@${p.to.split("@")[1]}`,
|
|
86
|
+
protocol: input.kind,
|
|
87
|
+
protocol_version: 1,
|
|
88
|
+
step: input.kind,
|
|
89
|
+
step_id: step,
|
|
90
|
+
prev_step_id: null,
|
|
91
|
+
expires_at: expires === null ? null : new Date(expires).toISOString(),
|
|
92
|
+
payload
|
|
93
|
+
});
|
|
94
|
+
const bytes = encoder.encode(envelope);
|
|
95
|
+
check(bytes.length <= 65536, "signal exceeds 64 KiB");
|
|
96
|
+
const body = {
|
|
97
|
+
from: p.to,
|
|
98
|
+
to: p.from,
|
|
99
|
+
subject: subject || "Re: Your message",
|
|
100
|
+
body_text: signalText(input.kind, payload.status, payload.note),
|
|
101
|
+
in_reply_to: target,
|
|
102
|
+
references,
|
|
103
|
+
attachments: [{
|
|
104
|
+
filename: "interaction.json",
|
|
105
|
+
content_type: "application/json",
|
|
106
|
+
content_base64: base64(bytes)
|
|
107
|
+
}]
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
status: "prepared",
|
|
111
|
+
prepared: Object.freeze({
|
|
112
|
+
accountScope: p.accountScope,
|
|
113
|
+
preparedAtMs: now,
|
|
114
|
+
expiresAtMs: expires,
|
|
115
|
+
idempotencyKey: `signal-${step}`,
|
|
116
|
+
requestJson: signalJson(body)
|
|
117
|
+
})
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** One ordinary send attempt; the caller owns persistence, reconciliation and retries. */
|
|
121
|
+
async function sendPreparedSignal(sendMail, prepared, options) {
|
|
122
|
+
check(options.accountScope === prepared.accountScope && options.accountScope.length > 0, "account scope mismatch");
|
|
123
|
+
const now = options.now();
|
|
124
|
+
milliseconds(now);
|
|
125
|
+
if (prepared.expiresAtMs !== null && now >= prepared.expiresAtMs) return {
|
|
126
|
+
status: "expired",
|
|
127
|
+
idempotencyKey: prepared.idempotencyKey
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
status: "response",
|
|
131
|
+
result: await sendMail(JSON.parse(prepared.requestJson), prepared.idempotencyKey)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Internal shared fallback builder; callers validate the supported signal first. */
|
|
135
|
+
function signalText(kind, status, note) {
|
|
136
|
+
if (kind === "read") return "I read your message.";
|
|
137
|
+
if (kind === "working") return "I am working on your message.";
|
|
138
|
+
const bodies = {
|
|
139
|
+
received: "Received your message.",
|
|
140
|
+
will_process: "I intend to process your message.",
|
|
141
|
+
will_not_process: "I will not process your message."
|
|
142
|
+
};
|
|
143
|
+
const text = status !== void 0 && Object.hasOwn(bodies, status) ? bodies[status] ?? "" : "";
|
|
144
|
+
return note === void 0 ? text : `${text}\n\n${note}`;
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/interactions/classify.ts
|
|
148
|
+
const ENVELOPE_KEYS = [
|
|
149
|
+
"interaction_version",
|
|
150
|
+
"interaction_id",
|
|
151
|
+
"protocol",
|
|
152
|
+
"protocol_version",
|
|
153
|
+
"step",
|
|
154
|
+
"step_id",
|
|
155
|
+
"prev_step_id",
|
|
156
|
+
"expires_at",
|
|
157
|
+
"payload"
|
|
158
|
+
];
|
|
159
|
+
function asciiLower(value) {
|
|
160
|
+
return value.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
|
|
161
|
+
}
|
|
162
|
+
function mediaType(value) {
|
|
163
|
+
const separator = value.indexOf(";");
|
|
164
|
+
let end = separator === -1 ? value.length : separator;
|
|
165
|
+
let start = 0;
|
|
166
|
+
const padding = (index) => value.charCodeAt(index) === 32 || value.charCodeAt(index) === 9;
|
|
167
|
+
while (start < end && padding(start)) start++;
|
|
168
|
+
while (end > start && padding(end - 1)) end--;
|
|
169
|
+
return asciiLower(value.slice(start, end));
|
|
170
|
+
}
|
|
171
|
+
function exactKeys(value, required, optional = []) {
|
|
172
|
+
return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
|
|
173
|
+
}
|
|
174
|
+
function utcExpiry(value) {
|
|
175
|
+
if (typeof value !== "string") return false;
|
|
176
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?Z$/.exec(value);
|
|
177
|
+
if (!match) return false;
|
|
178
|
+
const year = Number(match[1]), month = Number(match[2]), day = Number(match[3]);
|
|
179
|
+
const days = [
|
|
180
|
+
31,
|
|
181
|
+
year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28,
|
|
182
|
+
31,
|
|
183
|
+
30,
|
|
184
|
+
31,
|
|
185
|
+
30,
|
|
186
|
+
31,
|
|
187
|
+
31,
|
|
188
|
+
30,
|
|
189
|
+
31,
|
|
190
|
+
30,
|
|
191
|
+
31
|
|
192
|
+
];
|
|
193
|
+
return year >= 1970 && month >= 1 && month <= 12 && day >= 1 && day <= (days[month - 1] ?? 0) && Number(match[4]) < 24 && Number(match[5]) < 60 && Number(match[6]) < 60;
|
|
194
|
+
}
|
|
195
|
+
function fallback(result) {
|
|
196
|
+
if (result.status !== "valid") return null;
|
|
197
|
+
const e = result.envelope;
|
|
198
|
+
if (!exactKeys(e, ENVELOPE_KEYS) || e.protocol_version !== 1 || ![
|
|
199
|
+
"ack",
|
|
200
|
+
"read",
|
|
201
|
+
"working"
|
|
202
|
+
].includes(e.protocol) || e.step !== e.protocol || e.prev_step_id !== null || e.interaction_id.split("@")[0]?.toLowerCase() === e.step_id.toLowerCase()) return null;
|
|
203
|
+
if (e.protocol === "working" ? !utcExpiry(e.expires_at) : e.expires_at !== null) return null;
|
|
204
|
+
if (!e.payload || typeof e.payload !== "object" || Array.isArray(e.payload)) return null;
|
|
205
|
+
const p = e.payload;
|
|
206
|
+
if (!exactKeys(p, e.protocol === "ack" ? ["subject_message_id", "status"] : ["subject_message_id"], e.protocol === "ack" ? ["note"] : [])) return null;
|
|
207
|
+
const target = p.subject_message_id;
|
|
208
|
+
if (typeof target !== "string" || target.length > 998 || !/^<[!-~]+@[!-~]+>$/.test(target) || target.slice(1, -1).includes("<") || target.slice(1, -1).includes(">") || target.split("@").length !== 2) return null;
|
|
209
|
+
if (e.protocol !== "ack") return signalText(e.protocol);
|
|
210
|
+
if (typeof p.status !== "string" || ![
|
|
211
|
+
"received",
|
|
212
|
+
"will_process",
|
|
213
|
+
"will_not_process"
|
|
214
|
+
].includes(p.status)) return null;
|
|
215
|
+
if (Object.hasOwn(p, "note") && (typeof p.note !== "string" || p.note.length > 2e3 || p.note.includes("\0"))) return null;
|
|
216
|
+
return signalText("ack", p.status, p.note);
|
|
217
|
+
}
|
|
218
|
+
/** Pure content classification. It establishes no sender, receipt or task authority. */
|
|
219
|
+
function classifySignalContent(input) {
|
|
220
|
+
if (input.inventory.status !== "complete" || !Array.isArray(input.inventory.parts)) return {
|
|
221
|
+
classification: "unavailable",
|
|
222
|
+
reason: "inventory_unavailable"
|
|
223
|
+
};
|
|
224
|
+
const parts = input.inventory.parts;
|
|
225
|
+
const canonical = parts.filter((part) => part.filename !== null && asciiLower(part.filename) === "interaction.json");
|
|
226
|
+
if (!canonical.length) return {
|
|
227
|
+
classification: "plain",
|
|
228
|
+
reason: "no_canonical_part"
|
|
229
|
+
};
|
|
230
|
+
if (canonical.length !== 1) return {
|
|
231
|
+
classification: "mixed_or_unsupported",
|
|
232
|
+
reason: "duplicate_canonical_parts"
|
|
233
|
+
};
|
|
234
|
+
const interaction = input.canonicalPartBytes === null ? void 0 : parseInteractionEnvelope(input.canonicalPartBytes);
|
|
235
|
+
const evidence = interaction === void 0 ? {} : { interaction };
|
|
236
|
+
if (parts.length !== 1) return {
|
|
237
|
+
classification: "mixed_or_unsupported",
|
|
238
|
+
reason: "additional_parts",
|
|
239
|
+
...evidence
|
|
240
|
+
};
|
|
241
|
+
if (!interaction) return {
|
|
242
|
+
classification: "unavailable",
|
|
243
|
+
reason: "part_unavailable"
|
|
244
|
+
};
|
|
245
|
+
if (input.bodies.status !== "complete") return {
|
|
246
|
+
classification: "unavailable",
|
|
247
|
+
reason: "bodies_unavailable",
|
|
248
|
+
interaction
|
|
249
|
+
};
|
|
250
|
+
if (interaction.status === "invalid") return {
|
|
251
|
+
classification: "mixed_or_unsupported",
|
|
252
|
+
reason: "invalid_interaction",
|
|
253
|
+
interaction
|
|
254
|
+
};
|
|
255
|
+
if (mediaType(canonical[0]?.contentType ?? "") !== "application/json") return {
|
|
256
|
+
classification: "mixed_or_unsupported",
|
|
257
|
+
reason: "unsupported_content_type",
|
|
258
|
+
interaction
|
|
259
|
+
};
|
|
260
|
+
const text = fallback(interaction);
|
|
261
|
+
if (text === null) return {
|
|
262
|
+
classification: "mixed_or_unsupported",
|
|
263
|
+
reason: "unsupported_signal",
|
|
264
|
+
interaction
|
|
265
|
+
};
|
|
266
|
+
if (input.bodies.html !== null && input.bodies.html !== "") return {
|
|
267
|
+
classification: "mixed_or_unsupported",
|
|
268
|
+
reason: "html_present",
|
|
269
|
+
interaction
|
|
270
|
+
};
|
|
271
|
+
const actual = (input.bodies.text ?? "").replace(/\r\n/g, "\n");
|
|
272
|
+
const expected = text.replace(/\r\n/g, "\n");
|
|
273
|
+
const matches = actual === "" || actual === "\n" || actual === expected || actual === `${expected}\n`;
|
|
274
|
+
return {
|
|
275
|
+
classification: matches ? "informational_only" : "mixed_or_unsupported",
|
|
276
|
+
reason: matches ? "informational_signal" : "text_mismatch",
|
|
277
|
+
interaction
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/interactions/index.ts
|
|
282
|
+
const MAX_INTERACTION_BYTES = 65536;
|
|
283
|
+
const MAX_INTERACTION_DEPTH = 64;
|
|
284
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
285
|
+
const WIRE_ID = new RegExp(`^${UUID.source.slice(1, -1)}@[^\\s@]+$`, "i");
|
|
286
|
+
function scalarString(value) {
|
|
287
|
+
for (let i = 0; i < value.length; i++) {
|
|
288
|
+
const code = value.charCodeAt(i);
|
|
289
|
+
if (code >= 55296 && code <= 56319) {
|
|
290
|
+
const next = value.charCodeAt(++i);
|
|
291
|
+
if (!(next >= 56320 && next <= 57343)) return false;
|
|
292
|
+
} else if (code >= 56320 && code <= 57343) return false;
|
|
293
|
+
}
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
function safeNumber(value) {
|
|
297
|
+
return Number.isFinite(value) && (!Number.isInteger(value) || Number.isSafeInteger(value));
|
|
298
|
+
}
|
|
299
|
+
function scan(text) {
|
|
300
|
+
let at = 0;
|
|
301
|
+
const fail = () => {
|
|
302
|
+
throw new Error("invalid JSON");
|
|
303
|
+
};
|
|
304
|
+
const space = () => {
|
|
305
|
+
while (/[\x20\t\r\n]/.test(text[at] ?? "")) at++;
|
|
306
|
+
};
|
|
307
|
+
const string = () => {
|
|
308
|
+
const start = at++;
|
|
309
|
+
while (at < text.length) {
|
|
310
|
+
const char = text[at++];
|
|
311
|
+
if (char === "\\") {
|
|
312
|
+
at++;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (char === "\"") {
|
|
316
|
+
const value = JSON.parse(text.slice(start, at));
|
|
317
|
+
if (typeof value !== "string" || !scalarString(value)) fail();
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return fail();
|
|
322
|
+
};
|
|
323
|
+
const value = (depth) => {
|
|
324
|
+
space();
|
|
325
|
+
const char = text[at];
|
|
326
|
+
if (char === "{" || char === "[") {
|
|
327
|
+
if (depth >= 64) fail();
|
|
328
|
+
at++;
|
|
329
|
+
space();
|
|
330
|
+
const close = char === "{" ? "}" : "]";
|
|
331
|
+
const keys = /* @__PURE__ */ new Set();
|
|
332
|
+
if (text[at] === close) {
|
|
333
|
+
at++;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
while (at < text.length) {
|
|
337
|
+
if (char === "{") {
|
|
338
|
+
if (text[at] !== "\"") fail();
|
|
339
|
+
const key = string();
|
|
340
|
+
if (keys.has(key)) fail();
|
|
341
|
+
keys.add(key);
|
|
342
|
+
space();
|
|
343
|
+
if (text[at++] !== ":") fail();
|
|
344
|
+
}
|
|
345
|
+
value(depth + 1);
|
|
346
|
+
space();
|
|
347
|
+
if (text[at] === close) {
|
|
348
|
+
at++;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (text[at++] !== ",") fail();
|
|
352
|
+
space();
|
|
353
|
+
}
|
|
354
|
+
fail();
|
|
355
|
+
} else if (char === "\"") string();
|
|
356
|
+
else {
|
|
357
|
+
const start = at;
|
|
358
|
+
while (at < text.length && !/[\x20\t\r\n,\]}]/.test(text[at] ?? "")) at++;
|
|
359
|
+
const token = text.slice(start, at);
|
|
360
|
+
if ([
|
|
361
|
+
"true",
|
|
362
|
+
"false",
|
|
363
|
+
"null"
|
|
364
|
+
].includes(token)) return;
|
|
365
|
+
if (token.length > 128 || !/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(token) || !safeNumber(Number(token))) fail();
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
value(0);
|
|
369
|
+
space();
|
|
370
|
+
if (at !== text.length) fail();
|
|
371
|
+
}
|
|
372
|
+
function envelope(value) {
|
|
373
|
+
const invalid = {
|
|
374
|
+
status: "invalid",
|
|
375
|
+
reason: "invalid_envelope"
|
|
376
|
+
};
|
|
377
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return invalid;
|
|
378
|
+
const obj = value;
|
|
379
|
+
if (!Object.hasOwn(obj, "interaction_version")) return invalid;
|
|
380
|
+
const version = obj.interaction_version;
|
|
381
|
+
if (typeof version !== "number" || !Number.isSafeInteger(version) || version < 1) return invalid;
|
|
382
|
+
if (version !== 1) return {
|
|
383
|
+
status: "unsupported",
|
|
384
|
+
version
|
|
385
|
+
};
|
|
386
|
+
if (![
|
|
387
|
+
"interaction_id",
|
|
388
|
+
"protocol",
|
|
389
|
+
"protocol_version",
|
|
390
|
+
"step",
|
|
391
|
+
"step_id",
|
|
392
|
+
"prev_step_id",
|
|
393
|
+
"expires_at",
|
|
394
|
+
"payload"
|
|
395
|
+
].every((key) => Object.hasOwn(obj, key))) return invalid;
|
|
396
|
+
if (typeof obj.interaction_id !== "string" || !WIRE_ID.test(obj.interaction_id) || typeof obj.protocol !== "string" || !obj.protocol.trim() || typeof obj.protocol_version !== "number" || !Number.isSafeInteger(obj.protocol_version) || obj.protocol_version < 1 || typeof obj.step !== "string" || !obj.step.trim() || typeof obj.step_id !== "string" || !UUID.test(obj.step_id) || !(obj.prev_step_id === null || typeof obj.prev_step_id === "string" && UUID.test(obj.prev_step_id)) || !(obj.expires_at === null || typeof obj.expires_at === "string") || !Object.hasOwn(obj, "payload")) return invalid;
|
|
397
|
+
return {
|
|
398
|
+
status: "valid",
|
|
399
|
+
envelope: obj
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/** Parse at most 64 KiB of strict UTF-8 JSON. Unknown protocols remain valid. */
|
|
403
|
+
function parseInteractionEnvelope(input) {
|
|
404
|
+
try {
|
|
405
|
+
if (typeof input !== "string" && !(input instanceof Uint8Array)) return {
|
|
406
|
+
status: "invalid",
|
|
407
|
+
reason: "invalid_input"
|
|
408
|
+
};
|
|
409
|
+
if (input.length > 65536) return {
|
|
410
|
+
status: "invalid",
|
|
411
|
+
reason: "too_large"
|
|
412
|
+
};
|
|
413
|
+
const bytes = typeof input === "string" ? void 0 : new Uint8Array(input);
|
|
414
|
+
const text = typeof input === "string" ? input : new TextDecoder("utf-8", {
|
|
415
|
+
fatal: true,
|
|
416
|
+
ignoreBOM: true
|
|
417
|
+
}).decode(bytes);
|
|
418
|
+
if (!scalarString(text)) return {
|
|
419
|
+
status: "invalid",
|
|
420
|
+
reason: "invalid_json"
|
|
421
|
+
};
|
|
422
|
+
if (new TextEncoder().encode(text).length > 65536) return {
|
|
423
|
+
status: "invalid",
|
|
424
|
+
reason: "too_large"
|
|
425
|
+
};
|
|
426
|
+
scan(text);
|
|
427
|
+
const result = envelope(JSON.parse(text));
|
|
428
|
+
return result.status === "invalid" ? result : {
|
|
429
|
+
...result,
|
|
430
|
+
source: bytes ? {
|
|
431
|
+
text,
|
|
432
|
+
bytes
|
|
433
|
+
} : { text }
|
|
434
|
+
};
|
|
435
|
+
} catch {
|
|
436
|
+
return {
|
|
437
|
+
status: "invalid",
|
|
438
|
+
reason: "invalid_json"
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/** Validate a decoded JSON value. Cannot detect lost duplicate keys or original bytes. */
|
|
443
|
+
function validateInteractionEnvelope(input) {
|
|
444
|
+
try {
|
|
445
|
+
let budget = MAX_INTERACTION_BYTES;
|
|
446
|
+
const seen = /* @__PURE__ */ new Set();
|
|
447
|
+
const clone = (value, depth) => {
|
|
448
|
+
if (--budget < 0) throw new Error("too large");
|
|
449
|
+
if (value === null || typeof value === "boolean") return value;
|
|
450
|
+
if (typeof value === "string") {
|
|
451
|
+
if (value.length > budget) throw new Error("too large");
|
|
452
|
+
budget -= new TextEncoder().encode(value).length;
|
|
453
|
+
if (budget < 0 || !scalarString(value)) throw new Error("invalid string");
|
|
454
|
+
return value;
|
|
455
|
+
}
|
|
456
|
+
if (typeof value === "number" && safeNumber(value)) return value;
|
|
457
|
+
if (typeof value !== "object" || !value || depth >= 64 || seen.has(value)) throw new Error("invalid value");
|
|
458
|
+
const array = Array.isArray(value);
|
|
459
|
+
if (array ? Object.getPrototypeOf(value) !== Array.prototype : ![Object.prototype, null].includes(Object.getPrototypeOf(value))) throw new Error("not JSON");
|
|
460
|
+
seen.add(value);
|
|
461
|
+
const out = array ? [] : Object.create(null);
|
|
462
|
+
const keys = Reflect.ownKeys(value);
|
|
463
|
+
if (keys.length - (array ? 1 : 0) > budget) throw new Error("too large");
|
|
464
|
+
if (array && Object.getOwnPropertyDescriptor(value, "length")?.value !== keys.length - 1) throw new Error("sparse array");
|
|
465
|
+
for (const key of keys) {
|
|
466
|
+
if (array && key === "length") continue;
|
|
467
|
+
if (typeof key !== "string") throw new Error("invalid key");
|
|
468
|
+
if (!array && key.length >= budget || array && key.length > 10) throw new Error("too large");
|
|
469
|
+
if (!scalarString(key)) throw new Error("invalid key");
|
|
470
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
471
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) throw new Error("not JSON data");
|
|
472
|
+
if (array && !/^(0|[1-9]\d*)$/.test(key)) throw new Error("invalid array key");
|
|
473
|
+
if (!array) budget -= 1 + new TextEncoder().encode(key).length;
|
|
474
|
+
Object.defineProperty(out, key, {
|
|
475
|
+
value: clone(descriptor.value, depth + 1),
|
|
476
|
+
enumerable: true,
|
|
477
|
+
writable: true,
|
|
478
|
+
configurable: true
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
if (array && out.length !== keys.length - 1) throw new Error("sparse array");
|
|
482
|
+
seen.delete(value);
|
|
483
|
+
return out;
|
|
484
|
+
};
|
|
485
|
+
return envelope(clone(input, 0));
|
|
486
|
+
} catch {
|
|
487
|
+
return {
|
|
488
|
+
status: "invalid",
|
|
489
|
+
reason: "invalid_input"
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
//#endregion
|
|
494
|
+
export { MAX_INTERACTION_BYTES, MAX_INTERACTION_DEPTH, classifySignalContent, parseInteractionEnvelope, prepareSignalEmail, sendPreparedSignal, validateInteractionEnvelope };
|
package/dist/openapi/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as openapiDocument, t as operationManifest } from "../operations.generated-
|
|
1
|
+
import { n as openapiDocument, t as operationManifest } from "../operations.generated-CPcHOHgd.js";
|
|
2
2
|
export { openapiDocument, operationManifest };
|