@matuto-ai/opencode-broker-connector 0.0.0-stage → 0.1.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,299 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ export const TYPES = Object.freeze(['pi', 'antigravity-cli', 'claude-code', 'opencode']);
4
+ export const DEFAULTS = Object.freeze({
5
+ heartbeat_seconds: 60, lease_seconds: 180, claim_seconds: 120, max_wait_seconds: 30,
6
+ default_ttl_seconds: 86400, min_ttl_seconds: 60, max_ttl_seconds: 604800,
7
+ max_message_bytes: 65536, max_request_bytes: 98304,
8
+ max_pending_messages: 1000, max_pending_bytes: 67108864,
9
+ send_per_minute: 60, send_burst: 20, max_attempts: 10,
10
+ retention_seconds: 604800, registration_retention_seconds: 86400,
11
+ retry_window_seconds: 86400,
12
+ });
13
+
14
+ const ERROR_TEXT = Object.freeze({
15
+ invalid_request: ['The request does not match the v1 contract.', 'Correct the indicated field before retrying.'],
16
+ unauthenticated: ['Authentication is required.', 'Use an enrolled credential through the Authorization header.'],
17
+ forbidden: ['This operation is not authorized.', 'Check the enrolled adapter permissions.'],
18
+ recipient_not_found: ['The recipient is not available.', 'Select an exact authorized client ID from discovery.'],
19
+ message_not_found: ['The message is not available.', 'Use a message ID owned by this principal.'],
20
+ identity_conflict: ['The session identity conflicts with an existing registration.', 'Do not adopt another session; verify the local session identity.'],
21
+ stale_lease_epoch: ['Reconnect this session before retrying.', 'Read owned registration state and reconnect; do not blindly replay a mutation.'],
22
+ idempotency_conflict: ['This key was already used for a different request.', 'Reconcile the original request; do not change an uncertain request.'],
23
+ metadata_conflict: ['The metadata revision changed.', 'Read the current revision and reconcile the metadata patch.'],
24
+ claim_conflict: ['The claim is not current.', 'Reconcile the claim before acknowledging or requesting another.'],
25
+ session_closed: ['This session is closed.', 'Create a new identity only for a new harness session.'],
26
+ message_expired: ['The message transport deadline passed.', 'Read the receipt; expiry does not retract an accepted inbox item.'],
27
+ payload_too_large: ['The request exceeds a byte or item limit.', 'Reduce the indicated field before retrying.'],
28
+ incompatible_receiver_transport: ['The recipient does not support this durable-inbox transport.', 'Choose a compatible recipient or update its adapter.'],
29
+ receiving_disabled: ['The recipient has disabled transport receiving.', 'Wait for the recipient operator to enable receiving.'],
30
+ rate_limited: ['The request rate limit was reached.', 'Honor Retry-After; reuse the same key for an uncertain request.'],
31
+ mailbox_full: ['The destination mailbox is full.', 'Honor Retry-After; do not discard or duplicate an uncertain send.'],
32
+ storage_unavailable: ['Durable storage is unavailable.', 'Restore storage health; retry only with the original idempotency key.'],
33
+ not_ready: ['The broker is not ready.', 'Wait for readiness; retry only idempotent operations.'],
34
+ });
35
+
36
+ function contractReference(field) {
37
+ if (field === 'headers.X-Lease-Epoch') return 'openapi.json#/components/parameters/LeaseEpoch';
38
+ if (field === 'headers.X-Client-ID') return 'openapi.json#/components/parameters/ClientId';
39
+ if (field === 'headers.Idempotency-Key') return 'openapi.json#/components/parameters/IdempotencyKey';
40
+ return 'openapi.json#/components/schemas/Error';
41
+ }
42
+
43
+ export class ApiError extends Error {
44
+ constructor(status, code, field = null, options = {}) {
45
+ const text = ERROR_TEXT[code] ?? ERROR_TEXT.invalid_request;
46
+ super(options.message ?? text[0]);
47
+ this.name = 'ApiError';
48
+ this.status = status;
49
+ this.code = code;
50
+ this.field = field;
51
+ this.retryable = options.retryable ?? [429, 503].includes(status);
52
+ this.effect = options.effect ?? 'not_applied';
53
+ this.action = options.action ?? text[1];
54
+ this.contract_ref = options.contract_ref ?? contractReference(field);
55
+ this.retry_after = options.retry_after;
56
+ this.details = options.details;
57
+ }
58
+
59
+ toJSON(requestId) {
60
+ const error = {
61
+ code: this.code, message: this.message, retryable: this.retryable,
62
+ field: this.field, contract_ref: this.contract_ref, action: this.action,
63
+ effect: this.effect, request_id: requestId ?? null,
64
+ };
65
+ if (this.details !== undefined) error.details = this.details;
66
+ return { error };
67
+ }
68
+ }
69
+
70
+ function invalid(field, code = 'invalid_request', status = 400) {
71
+ throw new ApiError(status, code, field);
72
+ }
73
+
74
+ function object(value, allowed, required, path) {
75
+ if (value === null || typeof value !== 'object' || Array.isArray(value)
76
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) invalid(path);
77
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) invalid(path);
78
+ for (const key of required) if (!Object.hasOwn(value, key)) invalid(`${path}.${key}`);
79
+ return value;
80
+ }
81
+
82
+ function string(value, path, { maxChars = 256, maxBytes = maxChars * 4, minChars = 0, nullable = false, label = false } = {}) {
83
+ if (nullable && value === null) return null;
84
+ if (typeof value !== 'string' || !value.isWellFormed()) invalid(path);
85
+ const size = [...value].length;
86
+ if (size < minChars) invalid(path);
87
+ if (size > maxChars || Buffer.byteLength(value, 'utf8') > maxBytes) invalid(path, 'payload_too_large', 413);
88
+ if (label && /[\u0000-\u001f\u007f-\u009f]/u.test(value)) invalid(path);
89
+ return value;
90
+ }
91
+
92
+ function integer(value, min, max, path) {
93
+ if (!Number.isSafeInteger(value) || value < min || value > max) invalid(path);
94
+ return value;
95
+ }
96
+
97
+ function boolean(value, path) {
98
+ if (typeof value !== 'boolean') invalid(path);
99
+ return value;
100
+ }
101
+
102
+ function oneOf(value, values, path) {
103
+ if (!values.includes(value)) invalid(path);
104
+ return value;
105
+ }
106
+
107
+ export function validateUuid(value, path = 'id') {
108
+ if (typeof value !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) invalid(path);
109
+ return value.toLowerCase();
110
+ }
111
+
112
+ export function validateIdempotencyKey(value, path = 'headers.Idempotency-Key') {
113
+ if (typeof value !== 'string' || !/^[A-Za-z0-9._~:+\/-]{1,128}$/.test(value)) invalid(path);
114
+ return value;
115
+ }
116
+
117
+ function model(value, path) {
118
+ object(value, ['provider', 'id', 'source'], ['provider', 'id', 'source'], path);
119
+ const result = {
120
+ provider: string(value.provider, `${path}.provider`, { maxChars: 128, nullable: true, label: true }),
121
+ id: string(value.id, `${path}.id`, { maxChars: 256, nullable: true, label: true }),
122
+ source: oneOf(value.source, ['observed', 'configured', 'unknown'], `${path}.source`),
123
+ };
124
+ if (result.source === 'unknown' && (result.id !== null || result.provider !== null)) invalid(path);
125
+ return result;
126
+ }
127
+
128
+ export function validateMetadata(value, { patch = false, path = 'body.metadata' } = {}) {
129
+ object(value, ['name', 'model', 'project'], patch ? [] : ['name', 'model', 'project'], path);
130
+ if (patch && Object.keys(value).length === 0) invalid(path);
131
+ const result = {};
132
+ if (Object.hasOwn(value, 'name')) result.name = string(value.name, `${path}.name`, { maxChars: 128, minChars: 1, label: true });
133
+ if (Object.hasOwn(value, 'model')) result.model = model(value.model, `${path}.model`);
134
+ if (Object.hasOwn(value, 'project')) result.project = string(value.project, `${path}.project`, { maxChars: 256, nullable: true, label: true });
135
+ return result;
136
+ }
137
+
138
+ export function validateRegistration(value) {
139
+ const fields = ['protocol_version', 'client_id', 'client_type', 'session_instance_id', 'connection_id', 'previous_epoch', 'metadata', 'capabilities'];
140
+ object(value, fields, fields, 'body');
141
+ const type = oneOf(value.client_type, TYPES, 'body.client_type');
142
+ const c = object(value.capabilities, ['protocol_version', 'adapter_version', 'client_type', 'receive_enabled', 'durable_inbox'], ['protocol_version', 'adapter_version', 'client_type', 'receive_enabled', 'durable_inbox'], 'body.capabilities');
143
+ if (c.client_type !== type) invalid('body.capabilities.client_type');
144
+ return {
145
+ protocol_version: oneOf(value.protocol_version, ['1'], 'body.protocol_version'),
146
+ client_id: validateUuid(value.client_id, 'body.client_id'),
147
+ client_type: type,
148
+ session_instance_id: validateUuid(value.session_instance_id, 'body.session_instance_id'),
149
+ connection_id: validateUuid(value.connection_id, 'body.connection_id'),
150
+ previous_epoch: integer(value.previous_epoch, 0, Number.MAX_SAFE_INTEGER - 1, 'body.previous_epoch'),
151
+ metadata: validateMetadata(value.metadata),
152
+ capabilities: {
153
+ protocol_version: oneOf(c.protocol_version, ['1'], 'body.capabilities.protocol_version'),
154
+ adapter_version: string(c.adapter_version, 'body.capabilities.adapter_version', { maxChars: 128, minChars: 1, label: true }),
155
+ client_type: type,
156
+ receive_enabled: boolean(c.receive_enabled, 'body.capabilities.receive_enabled'),
157
+ durable_inbox: boolean(c.durable_inbox, 'body.capabilities.durable_inbox'),
158
+ },
159
+ };
160
+ }
161
+
162
+ export function validateMetadataPatch(value) {
163
+ object(value, ['metadata_patch', 'expected_metadata_revision'], ['metadata_patch', 'expected_metadata_revision'], 'body');
164
+ return {
165
+ metadata_patch: validateMetadata(value.metadata_patch, { patch: true, path: 'body.metadata_patch' }),
166
+ expected_metadata_revision: integer(value.expected_metadata_revision, 1, Number.MAX_SAFE_INTEGER, 'body.expected_metadata_revision'),
167
+ };
168
+ }
169
+
170
+ function content(value, path, limits = DEFAULTS) {
171
+ object(value, ['type', 'text'], ['type', 'text'], path);
172
+ return {
173
+ type: oneOf(value.type, ['text/plain'], `${path}.type`),
174
+ text: string(value.text, `${path}.text`, { maxChars: Math.min(DEFAULTS.max_message_bytes, limits.max_message_bytes), maxBytes: Math.min(DEFAULTS.max_message_bytes, limits.max_message_bytes) }),
175
+ };
176
+ }
177
+
178
+ export function validateSend(value, { limits: configured = DEFAULTS } = {}) {
179
+ const limits = { ...DEFAULTS, ...configured };
180
+ object(value, ['recipient_id', 'content', 'ttl_seconds', 'correlation_id', 'reply_to_message_id', 'metadata_patch', 'expected_metadata_revision'], ['recipient_id', 'content'], 'body');
181
+ const result = {
182
+ recipient_id: validateUuid(value.recipient_id, 'body.recipient_id'),
183
+ content: content(value.content, 'body.content', limits),
184
+ ttl_seconds: integer(value.ttl_seconds ?? limits.default_ttl_seconds, Math.max(DEFAULTS.min_ttl_seconds, limits.min_ttl_seconds), Math.min(DEFAULTS.max_ttl_seconds, limits.max_ttl_seconds), 'body.ttl_seconds'),
185
+ correlation_id: string(value.correlation_id ?? null, 'body.correlation_id', { maxChars: 128, maxBytes: 512, nullable: true }),
186
+ reply_to_message_id: value.reply_to_message_id == null ? null : validateUuid(value.reply_to_message_id, 'body.reply_to_message_id'),
187
+ };
188
+ if (Object.hasOwn(value, 'ttl_seconds') && value.ttl_seconds === null) invalid('body.ttl_seconds');
189
+ if (Object.hasOwn(value, 'metadata_patch')) {
190
+ if (!Object.hasOwn(value, 'expected_metadata_revision')) invalid('body.expected_metadata_revision');
191
+ result.metadata_patch = validateMetadata(value.metadata_patch, { patch: true, path: 'body.metadata_patch' });
192
+ result.expected_metadata_revision = integer(value.expected_metadata_revision, 1, Number.MAX_SAFE_INTEGER, 'body.expected_metadata_revision');
193
+ } else if (Object.hasOwn(value, 'expected_metadata_revision')) invalid('body.expected_metadata_revision');
194
+ return result;
195
+ }
196
+
197
+ export function validateClaim(value, { limits: configured = DEFAULTS } = {}) {
198
+ const limits = { ...DEFAULTS, ...configured };
199
+ object(value, ['wait_seconds'], [], 'body');
200
+ return { wait_seconds: integer(Object.hasOwn(value, 'wait_seconds') ? value.wait_seconds : 0, 0, Math.min(DEFAULTS.max_wait_seconds, limits.max_wait_seconds), 'body.wait_seconds') };
201
+ }
202
+
203
+ export function validateRenew(value) {
204
+ object(value, ['claim_id'], ['claim_id'], 'body');
205
+ return { claim_id: validateUuid(value.claim_id, 'body.claim_id') };
206
+ }
207
+
208
+ export function validateCompletion(value) {
209
+ object(value, ['claim_id', 'outcome', 'reason'], ['claim_id', 'outcome'], 'body');
210
+ const outcome = oneOf(value.outcome, ['delivered', 'retryable_not_stored', 'permanent_not_stored'], 'body.outcome');
211
+ const reason = oneOf(value.reason ?? 'none', ['none', 'inbox_full', 'storage_unavailable', 'unsupported_envelope'], 'body.reason');
212
+ if (Object.hasOwn(value, 'reason') && value.reason === null) invalid('body.reason');
213
+ if (outcome === 'delivered' && reason !== 'none') invalid('body.reason');
214
+ return { claim_id: validateUuid(value.claim_id, 'body.claim_id'), outcome, reason };
215
+ }
216
+
217
+ export function validateEmpty(value) {
218
+ object(value, [], [], 'body');
219
+ return {};
220
+ }
221
+
222
+ function timestamp(value, path) {
223
+ if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value) || !Number.isFinite(Date.parse(value))) invalid(path);
224
+ const normalized = new Date(Date.parse(value)).toISOString();
225
+ if (normalized !== (value.includes('.') ? value : value.replace('Z', '.000Z'))) invalid(path);
226
+ return value;
227
+ }
228
+
229
+ export function validateEnvelope(value) {
230
+ const fields = ['protocol_version', 'message_id', 'recipient_id', 'recipient_sequence', 'sender', 'origin', 'authority', 'content', 'accepted_at', 'expires_at', 'correlation_id', 'reply_to_message_id'];
231
+ object(value, fields, fields, 'envelope');
232
+ object(value.sender, ['client_id', 'client_type', 'name', 'model', 'project', 'metadata_revision'], ['client_id', 'client_type', 'name', 'model', 'project', 'metadata_revision'], 'envelope.sender');
233
+ const meta = validateMetadata({ name: value.sender.name, model: value.sender.model, project: value.sender.project }, { path: 'envelope.sender' });
234
+ const acceptedAt = timestamp(value.accepted_at, 'envelope.accepted_at');
235
+ const expiresAt = timestamp(value.expires_at, 'envelope.expires_at');
236
+ if (Date.parse(expiresAt) <= Date.parse(acceptedAt)) invalid('envelope.expires_at');
237
+ return {
238
+ protocol_version: oneOf(value.protocol_version, ['1'], 'envelope.protocol_version'),
239
+ message_id: validateUuid(value.message_id, 'envelope.message_id'),
240
+ recipient_id: validateUuid(value.recipient_id, 'envelope.recipient_id'),
241
+ recipient_sequence: integer(value.recipient_sequence, 1, Number.MAX_SAFE_INTEGER, 'envelope.recipient_sequence'),
242
+ sender: { client_id: validateUuid(value.sender.client_id, 'envelope.sender.client_id'), client_type: oneOf(value.sender.client_type, TYPES, 'envelope.sender.client_type'), ...meta, metadata_revision: integer(value.sender.metadata_revision, 1, Number.MAX_SAFE_INTEGER, 'envelope.sender.metadata_revision') },
243
+ origin: oneOf(value.origin, ['broker_agent'], 'envelope.origin'),
244
+ authority: oneOf(value.authority, ['untrusted_agent_input'], 'envelope.authority'),
245
+ content: content(value.content, 'envelope.content'),
246
+ accepted_at: acceptedAt, expires_at: expiresAt,
247
+ correlation_id: string(value.correlation_id, 'envelope.correlation_id', { maxChars: 128, maxBytes: 512, nullable: true }),
248
+ reply_to_message_id: value.reply_to_message_id === null ? null : validateUuid(value.reply_to_message_id, 'envelope.reply_to_message_id'),
249
+ };
250
+ }
251
+
252
+ /**
253
+ * v1 canonical JSON: recursively sort object keys by UTF-16 code units, serialize
254
+ * primitives with JSON.stringify, preserve array order, and encode as UTF-8.
255
+ * No Unicode normalization. Reject non-JSON values, malformed Unicode, cycles,
256
+ * excessive nesting, unsafe/nonfinite numbers, and sparse arrays.
257
+ */
258
+ export function canonical(value) {
259
+ const ancestors = new Set();
260
+ function visit(item, depth) {
261
+ if (depth > 32) invalid('canonical.depth');
262
+ if (item === null || typeof item === 'boolean') return JSON.stringify(item);
263
+ if (typeof item === 'string') {
264
+ if (!item.isWellFormed()) invalid('canonical.string');
265
+ return JSON.stringify(item);
266
+ }
267
+ if (typeof item === 'number') {
268
+ if (!Number.isFinite(item) || (Number.isInteger(item) && !Number.isSafeInteger(item))) invalid('canonical.number');
269
+ return JSON.stringify(item);
270
+ }
271
+ if (typeof item !== 'object' || ancestors.has(item)) invalid('canonical.value');
272
+ ancestors.add(item);
273
+ let result;
274
+ if (Array.isArray(item)) {
275
+ if (item.length > 10000) invalid('canonical.array');
276
+ const parts = [];
277
+ for (let i = 0; i < item.length; i++) {
278
+ if (!Object.hasOwn(item, i)) invalid('canonical.array');
279
+ parts.push(visit(item[i], depth + 1));
280
+ }
281
+ result = `[${parts.join(',')}]`;
282
+ } else {
283
+ if (![Object.prototype, null].includes(Object.getPrototypeOf(item))) invalid('canonical.object');
284
+ const keys = Object.keys(item).sort();
285
+ if (keys.length > 10000) invalid('canonical.object');
286
+ result = `{${keys.map((key) => {
287
+ if (!key.isWellFormed()) invalid('canonical.key');
288
+ return `${JSON.stringify(key)}:${visit(item[key], depth + 1)}`;
289
+ }).join(',')}}`;
290
+ }
291
+ ancestors.delete(item);
292
+ return result;
293
+ }
294
+ return visit(value, 0);
295
+ }
296
+
297
+ export function digest(value) {
298
+ return createHash('sha256').update(canonical(value), 'utf8').digest('hex');
299
+ }