@matuto-ai/opencode-psst-connector 0.0.0-stage → 0.7.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +182 -2
  4. package/docs/active-conversation.md +68 -0
  5. package/docs/message-lifecycle.md +31 -0
  6. package/docs/migration-0.6.0.md +21 -0
  7. package/docs/native-automation.md +37 -0
  8. package/docs/presence-notices.md +21 -0
  9. package/docs/publishing.md +19 -0
  10. package/docs/reference-snapshot.sha256 +7 -0
  11. package/docs/setup-acceptance.md +96 -0
  12. package/docs/setup-context-recovery.md +55 -0
  13. package/docs/verification.md +39 -0
  14. package/package.json +42 -4
  15. package/plugin/index.mjs +18 -0
  16. package/plugin/tui.mjs +188 -0
  17. package/src/active-presence.mjs +97 -0
  18. package/src/approval-helper.mjs +11 -0
  19. package/src/config.mjs +50 -0
  20. package/src/enrollment.mjs +59 -0
  21. package/src/identity-recovery.mjs +94 -0
  22. package/src/installation.mjs +127 -0
  23. package/src/message-format.mjs +26 -0
  24. package/src/message-preview.mjs +29 -0
  25. package/src/native-delivery.mjs +169 -0
  26. package/src/native-inbox.mjs +37 -0
  27. package/src/native-incarnation.mjs +67 -0
  28. package/src/native-lock.mjs +17 -0
  29. package/src/native-metadata.mjs +15 -0
  30. package/src/native-plugin.mjs +336 -0
  31. package/src/native-setup.mjs +249 -0
  32. package/src/opencode-adapter.mjs +223 -0
  33. package/src/presence-notices.mjs +90 -0
  34. package/src/project-metadata.mjs +21 -0
  35. package/src/reference-pin.mjs +21 -0
  36. package/src/reference.mjs +20 -0
  37. package/src/setup-rpc.mjs +16 -0
  38. package/src/tool-schema.mjs +5 -0
  39. package/src/tool-scope.mjs +26 -0
  40. package/vendor/broker/LICENSE +21 -0
  41. package/vendor/broker/ORIGIN.md +12 -0
  42. package/vendor/broker/reference/client.mjs +439 -0
  43. package/vendor/broker/reference/errors.mjs +43 -0
  44. package/vendor/broker/reference/inbox.mjs +334 -0
  45. package/vendor/broker/reference/mcp-stdio.mjs +160 -0
  46. package/vendor/broker/reference/runtime.mjs +329 -0
  47. package/vendor/broker/reference/tools.mjs +88 -0
  48. package/vendor/broker/src/protocol.mjs +325 -0
  49. package/vendor/enrollment/LICENSE +21 -0
  50. package/vendor/enrollment/ORIGIN.md +20 -0
  51. package/vendor/enrollment/approval.mjs +181 -0
  52. package/vendor/enrollment/enrollment.mjs +137 -0
@@ -0,0 +1,325 @@
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
+ offline_notice_seconds: 600,
7
+ default_ttl_seconds: 86400, min_ttl_seconds: 60, max_ttl_seconds: 604800,
8
+ max_message_bytes: 65536, max_request_bytes: 98304,
9
+ max_pending_messages: 1000, max_pending_bytes: 67108864,
10
+ send_per_minute: 60, send_burst: 20, max_attempts: 10,
11
+ retention_seconds: 604800, registration_retention_seconds: 86400,
12
+ retry_window_seconds: 86400,
13
+ });
14
+
15
+ const ERROR_TEXT = Object.freeze({
16
+ invalid_request: ['The request does not match the v1 contract.', 'Correct the indicated field before retrying.'],
17
+ unauthenticated: ['Authentication is required.', 'Use an enrolled credential through the Authorization header.'],
18
+ forbidden: ['This operation is not authorized.', 'Check the enrolled adapter permissions.'],
19
+ recipient_not_found: ['The recipient is not available.', 'Select an exact authorized client ID from discovery.'],
20
+ message_not_found: ['The message is not available.', 'Use a message ID owned by this principal.'],
21
+ retry_window_elapsed: ['The operation recovery record has expired.', 'Reconcile the existing receipt; only an explicit new send or resend may create another delivery.'],
22
+ history_changed: ['History changed while paging.', 'Restart the listing without a cursor; no continuation was silently skipped.'],
23
+ content_unavailable: ['The original content is no longer retained.', 'Use the receipt; unavailable content cannot be reconstructed.'],
24
+ identity_conflict: ['The session identity conflicts with an existing registration.', 'Do not adopt another session; verify the local session identity.'],
25
+ stale_lease_epoch: ['Reconnect this session before retrying.', 'Read owned registration state and reconnect; do not blindly replay a mutation.'],
26
+ idempotency_conflict: ['This key was already used for a different request.', 'Reconcile the original request; do not change an uncertain request.'],
27
+ metadata_conflict: ['The metadata revision changed.', 'Read the current revision and reconcile the metadata patch.'],
28
+ claim_conflict: ['The claim is not current.', 'Reconcile the claim before acknowledging or requesting another.'],
29
+ registration_upgrade_required: ['Registration v2 is required.', 'Update the connector; do not retry this registration with a new identity.'],
30
+ session_closed: ['This session is closed.', 'Create a new identity only for a new harness session.'],
31
+ message_expired: ['The message transport deadline passed.', 'Read the receipt; expiry does not retract an accepted inbox item.'],
32
+ payload_too_large: ['The request exceeds a byte or item limit.', 'Reduce the indicated field before retrying.'],
33
+ incompatible_receiver_transport: ['The recipient does not support this durable-inbox transport.', 'Choose a compatible recipient or update its adapter.'],
34
+ receiving_disabled: ['The recipient has disabled transport receiving.', 'Wait for the recipient operator to enable receiving.'],
35
+ rate_limited: ['The request rate limit was reached.', 'Honor Retry-After; reuse the same key for an uncertain request.'],
36
+ mailbox_full: ['The destination mailbox is full.', 'Honor Retry-After; do not discard or duplicate an uncertain send.'],
37
+ storage_unavailable: ['Durable storage is unavailable.', 'Restore storage health; retry only with the original idempotency key.'],
38
+ not_ready: ['The broker is not ready.', 'Wait for readiness; retry only idempotent operations.'],
39
+ });
40
+
41
+ function contractReference(field) {
42
+ if (field === 'headers.X-Lease-Epoch') return 'openapi.json#/components/parameters/LeaseEpoch';
43
+ if (field === 'headers.X-Client-ID') return 'openapi.json#/components/parameters/ClientId';
44
+ if (field === 'headers.Idempotency-Key') return 'openapi.json#/components/parameters/IdempotencyKey';
45
+ return 'openapi.json#/components/schemas/Error';
46
+ }
47
+
48
+ export class ApiError extends Error {
49
+ constructor(status, code, field = null, options = {}) {
50
+ const text = ERROR_TEXT[code] ?? ERROR_TEXT.invalid_request;
51
+ super(options.message ?? text[0]);
52
+ this.name = 'ApiError';
53
+ this.status = status;
54
+ this.code = code;
55
+ this.field = field;
56
+ this.retryable = options.retryable ?? [429, 503].includes(status);
57
+ this.effect = options.effect ?? 'not_applied';
58
+ this.action = options.action ?? text[1];
59
+ this.contract_ref = options.contract_ref ?? contractReference(field);
60
+ this.retry_after = options.retry_after;
61
+ this.details = options.details;
62
+ }
63
+
64
+ toJSON(requestId) {
65
+ const error = {
66
+ code: this.code, message: this.message, retryable: this.retryable,
67
+ field: this.field, contract_ref: this.contract_ref, action: this.action,
68
+ effect: this.effect, request_id: requestId ?? null,
69
+ };
70
+ if (this.details !== undefined) error.details = this.details;
71
+ return { error };
72
+ }
73
+ }
74
+
75
+ function invalid(field, code = 'invalid_request', status = 400) {
76
+ throw new ApiError(status, code, field);
77
+ }
78
+
79
+ function object(value, allowed, required, path) {
80
+ if (value === null || typeof value !== 'object' || Array.isArray(value)
81
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) invalid(path);
82
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) invalid(path);
83
+ for (const key of required) if (!Object.hasOwn(value, key)) invalid(`${path}.${key}`);
84
+ return value;
85
+ }
86
+
87
+ function string(value, path, { maxChars = 256, maxBytes = maxChars * 4, minChars = 0, nullable = false, label = false } = {}) {
88
+ if (nullable && value === null) return null;
89
+ if (typeof value !== 'string' || !value.isWellFormed()) invalid(path);
90
+ const size = [...value].length;
91
+ if (size < minChars) invalid(path);
92
+ if (size > maxChars || Buffer.byteLength(value, 'utf8') > maxBytes) invalid(path, 'payload_too_large', 413);
93
+ if (label && /[\u0000-\u001f\u007f-\u009f]/u.test(value)) invalid(path);
94
+ return value;
95
+ }
96
+
97
+ function integer(value, min, max, path) {
98
+ if (!Number.isSafeInteger(value) || value < min || value > max) invalid(path);
99
+ return value;
100
+ }
101
+
102
+ function boolean(value, path) {
103
+ if (typeof value !== 'boolean') invalid(path);
104
+ return value;
105
+ }
106
+
107
+ function oneOf(value, values, path) {
108
+ if (!values.includes(value)) invalid(path);
109
+ return value;
110
+ }
111
+
112
+ export function validateUuid(value, path = 'id') {
113
+ 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);
114
+ return value.toLowerCase();
115
+ }
116
+
117
+ export function validateIdempotencyKey(value, path = 'headers.Idempotency-Key') {
118
+ if (typeof value !== 'string' || !/^[A-Za-z0-9._~:+\/-]{1,128}$/.test(value)) invalid(path);
119
+ return value;
120
+ }
121
+
122
+ function model(value, path) {
123
+ object(value, ['provider', 'id', 'source'], ['provider', 'id', 'source'], path);
124
+ const result = {
125
+ provider: string(value.provider, `${path}.provider`, { maxChars: 128, nullable: true, label: true }),
126
+ id: string(value.id, `${path}.id`, { maxChars: 256, nullable: true, label: true }),
127
+ source: oneOf(value.source, ['observed', 'configured', 'unknown'], `${path}.source`),
128
+ };
129
+ if (result.source === 'unknown' && (result.id !== null || result.provider !== null)) invalid(path);
130
+ return result;
131
+ }
132
+
133
+ export function validateMetadata(value, { patch = false, path = 'body.metadata' } = {}) {
134
+ object(value, ['name', 'model', 'project'], patch ? [] : ['name', 'model', 'project'], path);
135
+ if (patch && Object.keys(value).length === 0) invalid(path);
136
+ const result = {};
137
+ if (Object.hasOwn(value, 'name')) result.name = string(value.name, `${path}.name`, { maxChars: 128, minChars: 1, label: true });
138
+ if (Object.hasOwn(value, 'model')) result.model = model(value.model, `${path}.model`);
139
+ if (Object.hasOwn(value, 'project')) result.project = string(value.project, `${path}.project`, { maxChars: 256, nullable: true, label: true });
140
+ return result;
141
+ }
142
+
143
+ export function validateRegistration(value) {
144
+ const fields = ['protocol_version', 'client_id', 'client_type', 'session_instance_id', 'connection_id', 'previous_epoch', 'metadata', 'capabilities'];
145
+ object(value, [...fields, 'registration_version', 'requested_at'], fields, 'body');
146
+ const version = value.registration_version ?? 1;
147
+ integer(version, 1, 2, 'body.registration_version');
148
+ if (version === 1 && value.requested_at !== undefined) invalid('body.requested_at');
149
+ const freshness = version === 2 ? { registration_version: 2, requested_at: integer(value.requested_at, 0, Number.MAX_SAFE_INTEGER, 'body.requested_at') } : {};
150
+ const type = oneOf(value.client_type, TYPES, 'body.client_type');
151
+ 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');
152
+ if (c.client_type !== type) invalid('body.capabilities.client_type');
153
+ return {
154
+ ...freshness,
155
+ protocol_version: oneOf(value.protocol_version, ['1'], 'body.protocol_version'),
156
+ client_id: validateUuid(value.client_id, 'body.client_id'),
157
+ client_type: type,
158
+ session_instance_id: validateUuid(value.session_instance_id, 'body.session_instance_id'),
159
+ connection_id: validateUuid(value.connection_id, 'body.connection_id'),
160
+ previous_epoch: integer(value.previous_epoch, 0, Number.MAX_SAFE_INTEGER - 1, 'body.previous_epoch'),
161
+ metadata: validateMetadata(value.metadata),
162
+ capabilities: {
163
+ protocol_version: oneOf(c.protocol_version, ['1'], 'body.capabilities.protocol_version'),
164
+ adapter_version: string(c.adapter_version, 'body.capabilities.adapter_version', { maxChars: 128, minChars: 1, label: true }),
165
+ client_type: type,
166
+ receive_enabled: boolean(c.receive_enabled, 'body.capabilities.receive_enabled'),
167
+ durable_inbox: boolean(c.durable_inbox, 'body.capabilities.durable_inbox'),
168
+ },
169
+ };
170
+ }
171
+
172
+ export function validateMetadataPatch(value) {
173
+ object(value, ['metadata_patch', 'expected_metadata_revision'], ['metadata_patch', 'expected_metadata_revision'], 'body');
174
+ return {
175
+ metadata_patch: validateMetadata(value.metadata_patch, { patch: true, path: 'body.metadata_patch' }),
176
+ expected_metadata_revision: integer(value.expected_metadata_revision, 1, Number.MAX_SAFE_INTEGER, 'body.expected_metadata_revision'),
177
+ };
178
+ }
179
+
180
+ function content(value, path, limits = DEFAULTS) {
181
+ object(value, ['type', 'text'], ['type', 'text'], path);
182
+ return {
183
+ type: oneOf(value.type, ['text/plain'], `${path}.type`),
184
+ 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) }),
185
+ };
186
+ }
187
+
188
+ export function validateSend(value, { limits: configured = DEFAULTS } = {}) {
189
+ const limits = { ...DEFAULTS, ...configured };
190
+ object(value, ['recipient_id', 'content', 'ttl_seconds', 'correlation_id', 'reply_to_message_id', 'metadata_patch', 'expected_metadata_revision'], ['recipient_id', 'content'], 'body');
191
+ const result = {
192
+ recipient_id: validateUuid(value.recipient_id, 'body.recipient_id'),
193
+ content: content(value.content, 'body.content', limits),
194
+ 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'),
195
+ correlation_id: string(value.correlation_id ?? null, 'body.correlation_id', { maxChars: 128, maxBytes: 512, nullable: true }),
196
+ reply_to_message_id: value.reply_to_message_id == null ? null : validateUuid(value.reply_to_message_id, 'body.reply_to_message_id'),
197
+ };
198
+ if (Object.hasOwn(value, 'ttl_seconds') && value.ttl_seconds === null) invalid('body.ttl_seconds');
199
+ if (Object.hasOwn(value, 'metadata_patch')) {
200
+ if (!Object.hasOwn(value, 'expected_metadata_revision')) invalid('body.expected_metadata_revision');
201
+ result.metadata_patch = validateMetadata(value.metadata_patch, { patch: true, path: 'body.metadata_patch' });
202
+ result.expected_metadata_revision = integer(value.expected_metadata_revision, 1, Number.MAX_SAFE_INTEGER, 'body.expected_metadata_revision');
203
+ } else if (Object.hasOwn(value, 'expected_metadata_revision')) invalid('body.expected_metadata_revision');
204
+ return result;
205
+ }
206
+
207
+ export function validateClaim(value, { limits: configured = DEFAULTS } = {}) {
208
+ const limits = { ...DEFAULTS, ...configured };
209
+ object(value, ['wait_seconds'], [], 'body');
210
+ 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') };
211
+ }
212
+
213
+ export function validateRenew(value) {
214
+ object(value, ['claim_id'], ['claim_id'], 'body');
215
+ return { claim_id: validateUuid(value.claim_id, 'body.claim_id') };
216
+ }
217
+
218
+ export function validateCompletion(value) {
219
+ object(value, ['claim_id', 'outcome', 'reason'], ['claim_id', 'outcome'], 'body');
220
+ const outcome = oneOf(value.outcome, ['delivered', 'retryable_not_stored', 'permanent_not_stored'], 'body.outcome');
221
+ const reason = oneOf(value.reason ?? 'none', ['none', 'inbox_full', 'storage_unavailable', 'unsupported_envelope'], 'body.reason');
222
+ if (Object.hasOwn(value, 'reason') && value.reason === null) invalid('body.reason');
223
+ if (outcome === 'delivered' && reason !== 'none') invalid('body.reason');
224
+ return { claim_id: validateUuid(value.claim_id, 'body.claim_id'), outcome, reason };
225
+ }
226
+
227
+ export function validateEmpty(value) {
228
+ object(value, [], [], 'body');
229
+ return {};
230
+ }
231
+
232
+ function timestamp(value, path) {
233
+ 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);
234
+ const normalized = new Date(Date.parse(value)).toISOString();
235
+ if (normalized !== (value.includes('.') ? value : value.replace('Z', '.000Z'))) invalid(path);
236
+ return value;
237
+ }
238
+
239
+ export function validateEnvelope(value) {
240
+ const fields = ['protocol_version', 'message_id', 'recipient_id', 'recipient_sequence', 'sender', 'origin', 'authority', 'content', 'accepted_at', 'expires_at', 'correlation_id', 'reply_to_message_id'];
241
+ object(value, fields, fields, 'envelope');
242
+ object(value.sender, ['client_id', 'client_type', 'name', 'model', 'project', 'metadata_revision'], ['client_id', 'client_type', 'name', 'model', 'project', 'metadata_revision'], 'envelope.sender');
243
+ const meta = validateMetadata({ name: value.sender.name, model: value.sender.model, project: value.sender.project }, { path: 'envelope.sender' });
244
+ const acceptedAt = timestamp(value.accepted_at, 'envelope.accepted_at');
245
+ const expiresAt = timestamp(value.expires_at, 'envelope.expires_at');
246
+ if (Date.parse(expiresAt) <= Date.parse(acceptedAt)) invalid('envelope.expires_at');
247
+ return {
248
+ protocol_version: oneOf(value.protocol_version, ['1'], 'envelope.protocol_version'),
249
+ message_id: validateUuid(value.message_id, 'envelope.message_id'),
250
+ recipient_id: validateUuid(value.recipient_id, 'envelope.recipient_id'),
251
+ recipient_sequence: integer(value.recipient_sequence, 1, Number.MAX_SAFE_INTEGER, 'envelope.recipient_sequence'),
252
+ 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') },
253
+ origin: oneOf(value.origin, ['broker_agent'], 'envelope.origin'),
254
+ authority: oneOf(value.authority, ['untrusted_agent_input'], 'envelope.authority'),
255
+ content: content(value.content, 'envelope.content'),
256
+ accepted_at: acceptedAt, expires_at: expiresAt,
257
+ correlation_id: string(value.correlation_id, 'envelope.correlation_id', { maxChars: 128, maxBytes: 512, nullable: true }),
258
+ reply_to_message_id: value.reply_to_message_id === null ? null : validateUuid(value.reply_to_message_id, 'envelope.reply_to_message_id'),
259
+ };
260
+ }
261
+
262
+ /**
263
+ * v1 canonical JSON: recursively sort object keys by UTF-16 code units, serialize
264
+ * primitives with JSON.stringify, preserve array order, and encode as UTF-8.
265
+ * No Unicode normalization. Reject non-JSON values, malformed Unicode, cycles,
266
+ * excessive nesting, unsafe/nonfinite numbers, and sparse arrays.
267
+ */
268
+ export function canonical(value) {
269
+ const ancestors = new Set();
270
+ function visit(item, depth) {
271
+ if (depth > 32) invalid('canonical.depth');
272
+ if (item === null || typeof item === 'boolean') return JSON.stringify(item);
273
+ if (typeof item === 'string') {
274
+ if (!item.isWellFormed()) invalid('canonical.string');
275
+ return JSON.stringify(item);
276
+ }
277
+ if (typeof item === 'number') {
278
+ if (!Number.isFinite(item) || (Number.isInteger(item) && !Number.isSafeInteger(item))) invalid('canonical.number');
279
+ return JSON.stringify(item);
280
+ }
281
+ if (typeof item !== 'object' || ancestors.has(item)) invalid('canonical.value');
282
+ ancestors.add(item);
283
+ let result;
284
+ if (Array.isArray(item)) {
285
+ if (item.length > 10000) invalid('canonical.array');
286
+ const parts = [];
287
+ for (let i = 0; i < item.length; i++) {
288
+ if (!Object.hasOwn(item, i)) invalid('canonical.array');
289
+ parts.push(visit(item[i], depth + 1));
290
+ }
291
+ result = `[${parts.join(',')}]`;
292
+ } else {
293
+ if (![Object.prototype, null].includes(Object.getPrototypeOf(item))) invalid('canonical.object');
294
+ const keys = Object.keys(item).sort();
295
+ if (keys.length > 10000) invalid('canonical.object');
296
+ result = `{${keys.map((key) => {
297
+ if (!key.isWellFormed()) invalid('canonical.key');
298
+ return `${JSON.stringify(key)}:${visit(item[key], depth + 1)}`;
299
+ }).join(',')}}`;
300
+ }
301
+ ancestors.delete(item);
302
+ return result;
303
+ }
304
+ return visit(value, 0);
305
+ }
306
+
307
+ export function digest(value) {
308
+ return createHash('sha256').update(canonical(value), 'utf8').digest('hex');
309
+ }
310
+
311
+ export function validateResend(value, { limits = {} } = {}) {
312
+ object(value, ['ttl_seconds'], [], 'body');
313
+ const merged = { ...DEFAULTS, ...limits };
314
+ return { ttl_seconds: value.ttl_seconds === undefined ? merged.default_ttl_seconds : integer(value.ttl_seconds, Math.max(60, merged.min_ttl_seconds), Math.min(604800, merged.max_ttl_seconds), 'body.ttl_seconds') };
315
+ }
316
+
317
+ /** New internal keys carry a timestamp; legacy keys retain their bounded v1 guarantee. */
318
+ export function validateOperationTime(key, now, retryWindowSeconds) {
319
+ if(!key.startsWith('op_'))return;
320
+ const match=/^op_([0-9]{13})_([A-Za-z0-9_-]{16,96})$/.exec(key);
321
+ if(!match)throw new ApiError(400,'invalid_request','headers.Idempotency-Key');
322
+ const started=Number(match[1]);
323
+ if(started>now+300000)throw new ApiError(400,'invalid_request','headers.Idempotency-Key');
324
+ if(now-started>=retryWindowSeconds*1000)throw new ApiError(410,'retry_window_elapsed','headers.Idempotency-Key');
325
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bruno Orsolon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,20 @@
1
+ # Shared enrollment helper
2
+
3
+ Unmodified `reference/enrollment.mjs` from `matuto-ai/psst` commit `3e908e79966d886a02d372c103e31dec6e900c1a`, independently compared byte-for-byte with the remote committed source. Upstream PR #18 is pending review/merge; this commit pin is not a deployment or release claim.
4
+
5
+ SHA-256: `afeab00d7c2d6768d3537bd7c350e249768897bf81eff9b4ae9796f7c3f9dbe9`.
6
+
7
+ Runtime hash checked by `src/enrollment.mjs` before import. MIT license copied alongside. The existing pinned broker transport/runtime remains unchanged at its separate origin.
8
+
9
+ Source: https://github.com/matuto-ai/psst/blob/3e908e79966d886a02d372c103e31dec6e900c1a/reference/enrollment.mjs
10
+ Operator guide at that commit: https://github.com/matuto-ai/psst/blob/3e908e79966d886a02d372c103e31dec6e900c1a/docs/pairing-operator.md
11
+
12
+ ## URL-only approval helper
13
+
14
+ `approval.mjs` is vendored byte-for-byte from `matuto-ai/psst` commit
15
+ `72f5d2d1b834e02cfb6b2f7dc11c6f1a55729344`, path `reference/approval.mjs`,
16
+ Git blob `8e6128c22835e66c0ab50aa15c27ff16d33e1169`, SHA-256
17
+ `a60c3a0ccd7deb50556586cd67841bb89a300ea3c7f1af3a91d437f856b7a2d5`.
18
+ This is the explicit broker PR #22 dependency, not a claim of a deployed release.
19
+ The connector checks the digest before importing it. The old enrollment helper
20
+ remains unchanged for legacy bundles/receipts.
@@ -0,0 +1,181 @@
1
+ // Single-file helper deliberately vendorable by native connectors. Never emits secrets.
2
+ import { randomBytes, randomUUID, createHash } from 'node:crypto';
3
+ import { openSync, closeSync, fstatSync, readFileSync, writeFileSync, fsyncSync, renameSync, unlinkSync, mkdirSync, lstatSync, realpathSync, readdirSync, constants } from 'node:fs';
4
+ import { resolve, dirname, join, isAbsolute } from 'node:path';
5
+ import https from 'node:https';
6
+ import { DatabaseSync } from 'node:sqlite';
7
+
8
+ const TYPES = ['pi','claude-code','opencode','antigravity-cli'];
9
+ const fail = (code, effect = 'not_applied', retryable=false, retryAfterMs=2000) => { const e = new Error(code); Object.assign(e,{code,effect,retryable,...(retryable?{retryAfterMs:Math.max(1000,Math.min(300000,Math.ceil(retryAfterMs)))}:{})}); throw e; };
10
+ const hash = value => createHash('sha256').update(value).digest('hex');
11
+ function privateDir(path) {
12
+ if (!isAbsolute(path)) fail('private_directory_required');
13
+ mkdirSync(path, {recursive:true, mode:0o700});
14
+ const s = lstatSync(path);
15
+ if (!s.isDirectory() || s.isSymbolicLink() || realpathSync(path) !== resolve(path) || (s.mode & 0o077) || (process.getuid && s.uid !== process.getuid())) fail('private_directory_required');
16
+ }
17
+ function privateRead(path) {
18
+ if (!isAbsolute(path) || realpathSync(dirname(path)) !== resolve(dirname(path))) fail('private_file_required');
19
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
20
+ try {
21
+ const s = fstatSync(fd);
22
+ if (!s.isFile() || s.size > 65536 || (s.mode & 0o077) || (process.getuid && s.uid !== process.getuid())) fail('private_file_required');
23
+ return readFileSync(fd,'utf8');
24
+ } finally { closeSync(fd); }
25
+ }
26
+ function save(path, text) {
27
+ // Reject unsafe existing destinations rather than silently repairing their mode.
28
+ try { privateRead(path); } catch(e) { if(e.code !== 'ENOENT') throw e; }
29
+ const temp = `${path}.${randomUUID()}.tmp`;
30
+ const fd = openSync(temp, constants.O_WRONLY|constants.O_CREAT|constants.O_EXCL|constants.O_NOFOLLOW,0o600);
31
+ try { writeFileSync(fd,text); fsyncSync(fd); } finally { closeSync(fd); }
32
+ renameSync(temp,path);
33
+ const d = openSync(dirname(path),constants.O_RDONLY);try { fsyncSync(d); } finally { closeSync(d); }
34
+ }
35
+ function origin(value) {
36
+ let u; try { u = new URL(value); } catch { fail('https_origin_required'); }
37
+ if (typeof value !== 'string' || /[\u0000-\u0020\u007f]/u.test(value) || u.protocol !== 'https:' || u.username || u.password || u.pathname !== '/' || u.search || u.hash) fail('https_origin_required');
38
+ return u.origin;
39
+ }
40
+ const stateNames=['pending','approved','denied','expired','revoked'];
41
+ const uuid=/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
42
+ function locations({brokerUrl,directory,clientType}) {
43
+ if(!TYPES.includes(clientType))fail('invalid_client_type');
44
+ if(!isAbsolute(directory))fail('private_directory_required');
45
+ const broker=origin(brokerUrl),root=join(directory,'approvals',hash(broker+'\0'+clientType));
46
+ return {broker,root,current:join(root,'current.json'),clientType};
47
+ }
48
+ function load(loc) {
49
+ let pointer;try{pointer=JSON.parse(privateRead(loc.current));}catch(e){if(e.code==='ENOENT')return null;throw e;}
50
+ if(!pointer||!uuid.test(pointer.requestId))fail('invalid_approval_state');
51
+ const dir=join(loc.root,pointer.requestId),state=JSON.parse(privateRead(join(dir,'attempt.json')));
52
+ if(state.requestId!==pointer.requestId||state.brokerUrl!==loc.broker||state.clientType!==loc.clientType
53
+ ||!/^[A-Za-z0-9_-]{43}$/.test(state.token)||!stateNames.includes(state.state)
54
+ ||typeof state.label!=='string'||state.label.length>80)fail('invalid_approval_state');
55
+ return {state,dir};
56
+ }
57
+ function visible({state,dir}) {
58
+ const result={requestId:state.requestId,approvalUrl:state.brokerUrl+'/admin',state:state.state,requestExpiresAt:state.requestExpiresAt??null};
59
+ if(state.state==='approved'){
60
+ if(!state.credential||Date.parse(state.credential.expires_at)<=Date.now())return {...result,state:'expired'};
61
+ result.connectionFile=join(dir,'connection.json');
62
+ Object.assign(result,readEnrollmentReceipt({connectionFile:result.connectionFile,clientType:state.clientType}));
63
+ }
64
+ return result;
65
+ }
66
+ function sanitized(e) {
67
+ if(e.effect&&/^[a-z_]+$/.test(e.code))throw e;
68
+ fail('approval_local_error','unknown');
69
+ }
70
+ export function readApprovalState(options) {
71
+ try{const item=load(locations(options));return item?visible(item):null;}catch(e){sanitized(e);}
72
+ }
73
+ function request(broker,method,path,token,input) {
74
+ return new Promise((resolvePromise,reject)=>{
75
+ const body=input===undefined?undefined:JSON.stringify(input);
76
+ const transportError=()=>Object.assign(new Error('approval_transport_error'),{code:'approval_transport_error',effect:'unknown',retryable:true,retryAfterMs:2000});
77
+ const req=https.request(new URL(path,broker),{method,headers:{Authorization:`Enrollment ${token}`,...(body?{'Content-Type':'application/json','Content-Length':Buffer.byteLength(body)}:{})}},res=>{
78
+ let size=0;const chunks=[];
79
+ res.on('data',chunk=>{size+=chunk.length;if(size>16384)res.destroy();else chunks.push(chunk);});
80
+ res.on('error',()=>reject(transportError()));
81
+ res.on('end',()=>{try{resolvePromise({status:res.statusCode,retryAfter:res.headers['retry-after'],body:JSON.parse(Buffer.concat(chunks).toString('utf8'))});}catch{reject(Object.assign(new Error('invalid_response'),{code:'invalid_response',effect:'unknown',retryable:false}));}});
82
+ });
83
+ const timer=setTimeout(()=>req.destroy(),10000);req.on('close',()=>clearTimeout(timer));
84
+ req.on('error',()=>reject(transportError()));req.end(body);
85
+ });
86
+ }
87
+ function publish(item,result,broker,clientType){
88
+ const {state,dir}=item;
89
+ if(!result||result.request_id!==state.requestId||!stateNames.includes(result.state)
90
+ ||!Number.isFinite(Date.parse(result.request_expires_at)))fail('invalid_response','unknown');
91
+ if(result.state==='approved'){
92
+ const c=result.credential;
93
+ if(!c||c.protocol_version!=='1'||c.client_type!==clientType||!/^enrolled-[0-9a-f-]{36}$/.test(c.credential_id)
94
+ ||!/^installation-[0-9a-f-]{36}$/.test(c.adapter_id)||!Array.isArray(c.scopes)||!c.scopes.length||c.scopes.length>32
95
+ ||c.scopes.some(s=>typeof s!=='string'||!/^[-a-zA-Z0-9_.]{1,128}$/.test(s))||!Number.isFinite(Date.parse(c.expires_at)))fail('invalid_response','unknown');
96
+ if(Date.parse(c.expires_at)<=Date.now())fail('credential_expired');
97
+ save(join(dir,'token'),state.token+'\n');
98
+ save(join(dir,'enrollment.json'),JSON.stringify({format:'psst-enrollment-receipt-v1',broker_url:broker,...c,token_sha256:hash(state.token)})+'\n');
99
+ save(join(dir,'connection.json'),JSON.stringify({format:'broker-client-bundle-v1',protocol_version:'1',broker_url:broker,token_file:'token',enrollment_receipt_file:'enrollment.json',file_paths_relative_to:'connection_file_directory',authorization_header:'Authorization',authorization_scheme:'Bearer',insecure_development:false,allowed_client_types:[clientType],scope:c.scopes[0]})+'\n');
100
+ state.credential=c;
101
+ }
102
+ state.state=result.state;state.requestExpiresAt=result.request_expires_at;
103
+ save(join(dir,'attempt.json'),JSON.stringify(state)+'\n');return visible(item);
104
+ }
105
+ async function operate(options,begin,{transport=request,pollIntervalMs=2000}={}) {
106
+ let lock;
107
+ try {
108
+ const loc=locations(options);
109
+ if(!begin&&!load(loc))fail('approval_setup_required');
110
+ privateDir(options.directory);privateDir(join(options.directory,'approvals'));privateDir(loc.root);
111
+ const lockPath=join(loc.root,'.owner.sqlite');
112
+ try{privateRead(lockPath);}catch(e){if(e.code!=='ENOENT')throw e;}
113
+ const fd=openSync(lockPath,constants.O_WRONLY|constants.O_CREAT|constants.O_NOFOLLOW,0o600);closeSync(fd);
114
+ try{lock=new DatabaseSync(lockPath);lock.exec('PRAGMA journal_mode=DELETE; PRAGMA locking_mode=EXCLUSIVE; BEGIN EXCLUSIVE; CREATE TABLE IF NOT EXISTS owner(id INTEGER);');}
115
+ catch{fail('approval_busy','not_applied',true);}
116
+ let item=load(loc);
117
+ if(begin){
118
+ const replacement=options.replaceRequestId;
119
+ if(replacement!==undefined&&(!uuid.test(replacement)||!item))fail('selection_conflict');
120
+ if(replacement!==undefined&&item.state.requestId!==replacement&&item.state.parentRequestId!==replacement)fail('selection_conflict');
121
+ if(replacement!==undefined&&item.state.requestId===replacement&&item.state.state==='pending')fail('selection_conflict');
122
+ if(!item||(replacement!==undefined&&item.state.requestId===replacement)){
123
+ if(readdirSync(loc.root).length>=130)fail('approval_capacity');
124
+ const label=options.label??options.clientType;
125
+ if(typeof label!=='string'||label.length>80||/[\u0000-\u001f\u007f]/u.test(label))fail('invalid_request');
126
+ const state={requestId:randomUUID(),parentRequestId:replacement??null,brokerUrl:loc.broker,clientType:options.clientType,label,token:randomBytes(32).toString('base64url'),state:'pending'};
127
+ const dir=join(loc.root,state.requestId);privateDir(dir);
128
+ save(join(dir,'attempt.json'),JSON.stringify(state)+'\n');
129
+ save(loc.current,JSON.stringify({requestId:state.requestId})+'\n');item={state,dir};
130
+ }
131
+ }
132
+ if(!item)fail('approval_setup_required');
133
+ if(item.state.state!=='pending')return visible(item);
134
+ const s=item.state;
135
+ if(Number.isFinite(s.retryAt)&&s.retryAt>Date.now()&&s.retryAt-Date.now()<=300000)fail(s.retryCode??'rate_limited',s.retryEffect??'unknown',true,s.retryAt-Date.now());
136
+ if(!begin){
137
+ const now=Date.now();
138
+ if(Number.isFinite(s.lastPollAt)&&now>=s.lastPollAt&&now-s.lastPollAt<pollIntervalMs)return visible(item);
139
+ s.lastPollAt=now;save(join(item.dir,'attempt.json'),JSON.stringify(s)+'\n');
140
+ }
141
+ const path=begin?'/v1/enrollment-requests':`/v1/enrollment-requests/${s.requestId}`;
142
+ const response=await transport(loc.broker,begin?'POST':'GET',path,s.token,begin?{request_id:s.requestId,client_type:s.clientType,label:s.label}:undefined);
143
+ if(![200,201].includes(response.status)){
144
+ const code=response.body?.error?.code;
145
+ let retryAfterMs=response.status===429?60000:5000;
146
+ if(typeof response.retryAfter==='string'){
147
+ const seconds=/^[0-9]{1,6}$/.test(response.retryAfter)?Number(response.retryAfter):NaN;
148
+ const parsed=Number.isFinite(seconds)?seconds*1000:Date.parse(response.retryAfter)-Date.now();
149
+ if(Number.isFinite(parsed))retryAfterMs=Math.max(1000,Math.min(300000,Math.ceil(parsed)));
150
+ }
151
+ if([429,503].includes(response.status)){s.retryAt=Date.now()+retryAfterMs;s.retryCode=code==='not_ready'||code==='storage_unavailable'?code:'rate_limited';s.retryEffect=response.body?.error?.effect==='not_applied'?'not_applied':'unknown';save(join(item.dir,'attempt.json'),JSON.stringify(s)+'\n');}
152
+ fail(['unauthenticated','forbidden','invalid_request','idempotency_conflict','rate_limited','not_ready','storage_unavailable'].includes(code)?code:'invalid_response',response.body?.error?.effect==='not_applied'?'not_applied':'unknown',[429,503].includes(response.status),retryAfterMs);
153
+ }
154
+ delete s.retryAt;delete s.retryCode;delete s.retryEffect;
155
+ return publish(item,response.body,loc.broker,options.clientType);
156
+ }catch(e){sanitized(e);}finally{try{lock?.close();}catch{}}
157
+ }
158
+ export const beginApproval=(options,testOptions)=>operate(options,true,testOptions);
159
+ export const pollApproval=(options,testOptions)=>operate(options,false,testOptions);
160
+
161
+ /** Read only for a helper-enrolled bundle. Missing/corrupt receipt is not a legacy fallback. */
162
+ export function readEnrollmentReceipt({connectionFile,clientType}) {
163
+ try {
164
+ if(!TYPES.includes(clientType))fail('invalid_client_type');
165
+ const bundle=JSON.parse(privateRead(connectionFile));
166
+ const receipt=JSON.parse(privateRead(join(dirname(connectionFile),'enrollment.json')));
167
+ const token=privateRead(join(dirname(connectionFile),'token')).trim();
168
+ if(bundle?.format!=='broker-client-bundle-v1'||bundle.protocol_version!=='1'||bundle.token_file!=='token'||bundle.enrollment_receipt_file!=='enrollment.json'
169
+ ||bundle.authorization_scheme!=='Bearer'||bundle.authorization_header!=='Authorization'||bundle.insecure_development!==false
170
+ ||!Array.isArray(bundle.allowed_client_types)||bundle.allowed_client_types.length!==1||bundle.allowed_client_types[0]!==clientType
171
+ ||receipt?.format!=='psst-enrollment-receipt-v1'||receipt.protocol_version!=='1'||receipt.client_type!==clientType
172
+ ||receipt.broker_url!==origin(bundle.broker_url)||!/^[A-Za-z0-9_-]{43}$/.test(token)||receipt.token_sha256!==hash(token)
173
+ ||!/^enrolled-[0-9a-f-]{36}$/.test(receipt.credential_id)||!/^installation-[0-9a-f-]{36}$/.test(receipt.adapter_id)
174
+ ||!Array.isArray(receipt.scopes)||!receipt.scopes.length||receipt.scopes[0]!==bundle.scope||!Number.isFinite(Date.parse(receipt.expires_at)))fail('invalid_enrollment_receipt');
175
+ if(Date.parse(receipt.expires_at)<=Date.now())fail('credential_expired');
176
+ return {credentialId:receipt.credential_id,expiresAt:receipt.expires_at};
177
+ } catch(e) {
178
+ if(e.effect&&/^[a-z_]+$/.test(e.code))throw e;
179
+ fail('invalid_enrollment_receipt');
180
+ }
181
+ }