@4xeoz/re-entry 0.2.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 (29) hide show
  1. package/README.md +337 -0
  2. package/node_modules/@webmcp-challenge/reentry-core/README.md +112 -0
  3. package/node_modules/@webmcp-challenge/reentry-core/package.json +38 -0
  4. package/node_modules/@webmcp-challenge/reentry-core/protocol/test-vectors/v0.1.json +47 -0
  5. package/node_modules/@webmcp-challenge/reentry-core/src/agent-adapter.mjs +438 -0
  6. package/node_modules/@webmcp-challenge/reentry-core/src/cloud-receiver-http.mjs +268 -0
  7. package/node_modules/@webmcp-challenge/reentry-core/src/host-sdk.mjs +278 -0
  8. package/node_modules/@webmcp-challenge/reentry-core/src/index.mjs +3 -0
  9. package/node_modules/@webmcp-challenge/reentry-core/src/local-connector-client.mjs +530 -0
  10. package/node_modules/@webmcp-challenge/reentry-core/src/managed-context-adapter.mjs +275 -0
  11. package/node_modules/@webmcp-challenge/reentry-core/src/protocol.mjs +845 -0
  12. package/node_modules/@webmcp-challenge/reentry-core/src/receiver-core.mjs +867 -0
  13. package/node_modules/@webmcp-challenge/reentry-core/src/receiver-delivery.mjs +613 -0
  14. package/node_modules/@webmcp-challenge/reentry-core/src/receiver-http-contract.mjs +24 -0
  15. package/node_modules/@webmcp-challenge/reentry-core/src/receiver-support.mjs +151 -0
  16. package/node_modules/@webmcp-challenge/reentry-core/src/sqlite-receiver-schema.mjs +184 -0
  17. package/node_modules/@webmcp-challenge/reentry-core/src/sqlite-receiver-store.mjs +573 -0
  18. package/package.json +44 -0
  19. package/src/browser-prompt.mjs +18 -0
  20. package/src/codex-discovery.mjs +246 -0
  21. package/src/codex-exec-adapter.mjs +197 -0
  22. package/src/codex-queue-adapter.mjs +214 -0
  23. package/src/credentials.mjs +87 -0
  24. package/src/index.mjs +6 -0
  25. package/src/local-connector.mjs +82 -0
  26. package/src/macos-service.mjs +184 -0
  27. package/src/main.mjs +733 -0
  28. package/src/pairing-client.mjs +545 -0
  29. package/src/terminal-ui.mjs +99 -0
@@ -0,0 +1,530 @@
1
+ import { TextDecoder } from "node:util";
2
+
3
+ import {
4
+ PROTOCOL_VERSION,
5
+ canonicalJson,
6
+ validateContinuationReceipt,
7
+ } from "./protocol.mjs";
8
+ import {
9
+ DELIVERY_ACKNOWLEDGEMENT_TYPE,
10
+ DELIVERY_LEASE_TYPE,
11
+ } from "./receiver-delivery.mjs";
12
+ import {
13
+ RECEIVER_HTTP_CONTENT_TYPE,
14
+ RECEIVER_HTTP_LIMITS,
15
+ RECEIVER_HTTP_ROUTES,
16
+ } from "./receiver-http-contract.mjs";
17
+
18
+ const CLIENT_OPTION_FIELDS = Object.freeze([
19
+ "baseUrl",
20
+ "connectorToken",
21
+ "requestTimeoutMs",
22
+ ]);
23
+ const CLAIM_INPUT_FIELDS = Object.freeze(["claimToken"]);
24
+ const ACKNOWLEDGEMENT_INPUT_FIELDS = Object.freeze([
25
+ "deliveryId",
26
+ "leaseToken",
27
+ "effectToken",
28
+ ]);
29
+ const CLAIM_RESULT_FIELDS = Object.freeze(["duplicate", "lease"]);
30
+ const LEASE_FIELDS = Object.freeze([
31
+ "type",
32
+ "protocol_version",
33
+ "delivery_id",
34
+ "event_id",
35
+ "attempt",
36
+ "lease_token",
37
+ "lease_expires_at",
38
+ "continuation",
39
+ "receipt",
40
+ ]);
41
+ const CONTINUATION_FIELDS = Object.freeze([
42
+ "correlation_id",
43
+ "workflow_id",
44
+ "event_type",
45
+ "event_sequence",
46
+ "state_version",
47
+ "occurred_at",
48
+ "canonical_url",
49
+ ]);
50
+ const ACKNOWLEDGEMENT_FIELDS = Object.freeze([
51
+ "type",
52
+ "protocol_version",
53
+ "delivery_id",
54
+ "event_id",
55
+ "effect_id",
56
+ "acknowledged",
57
+ "duplicate",
58
+ "status",
59
+ ]);
60
+ const ERROR_RESPONSE_FIELDS = Object.freeze(["error"]);
61
+ const ERROR_FIELDS = Object.freeze(["code"]);
62
+ const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/;
63
+ const ERROR_CODE_PATTERN = /^[a-z][a-z0-9_]{0,95}$/;
64
+ const CLAIM_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
65
+ const CONTENT_TYPE_PATTERN = /^application\/json(?:\s*;\s*charset=utf-8)?$/i;
66
+ const MAX_OPAQUE_TOKEN_BYTES = 4 * 1_024;
67
+ const MIN_REQUEST_TIMEOUT_MS = 100;
68
+ const MAX_REQUEST_TIMEOUT_MS = 60 * 1_000;
69
+
70
+ export class LocalConnectorClient {
71
+ #baseUrl;
72
+ #connectorToken;
73
+ #requestTimeoutMs;
74
+
75
+ constructor(options) {
76
+ requireClientInput(options, CLIENT_OPTION_FIELDS, "Local Connector client options");
77
+ this.#baseUrl = requireReceiverOrigin(options.baseUrl);
78
+ this.#connectorToken = requireOpaqueToken(
79
+ options.connectorToken,
80
+ "Connector token",
81
+ "connector_token_invalid",
82
+ );
83
+ if (
84
+ !Number.isSafeInteger(options.requestTimeoutMs) ||
85
+ options.requestTimeoutMs < MIN_REQUEST_TIMEOUT_MS ||
86
+ options.requestTimeoutMs > MAX_REQUEST_TIMEOUT_MS
87
+ ) {
88
+ throw clientFailure(
89
+ "connector_timeout_invalid",
90
+ "Local Connector requestTimeoutMs must be between 100 and 60000",
91
+ );
92
+ }
93
+ if (typeof globalThis.fetch !== "function") {
94
+ throw new TypeError("Local Connector client requires platform fetch");
95
+ }
96
+ this.#requestTimeoutMs = options.requestTimeoutMs;
97
+ }
98
+
99
+ async claimDelivery(input) {
100
+ requireClientInput(input, CLAIM_INPUT_FIELDS, "Delivery claim input");
101
+ const claimToken = requireClaimToken(input.claimToken, "Delivery claim token");
102
+ const response = await this.#post(RECEIVER_HTTP_ROUTES.claim, {
103
+ connector_token: this.#connectorToken,
104
+ claim_token: claimToken,
105
+ });
106
+ if (response.status === 204) {
107
+ await requireEmptyResponse(response);
108
+ return null;
109
+ }
110
+ if (response.status !== 200) {
111
+ throw await parseHttpFailure(response);
112
+ }
113
+ const value = await parseCanonicalJsonResponse(response);
114
+ return normalizeClaimResult(value, claimToken, Date.now());
115
+ }
116
+
117
+ async acknowledgeDelivery(input) {
118
+ requireClientInput(input, ACKNOWLEDGEMENT_INPUT_FIELDS, "Delivery acknowledgement input");
119
+ const deliveryId = requireIdentifier(
120
+ input.deliveryId,
121
+ "deliveryId",
122
+ "connector_input_invalid",
123
+ );
124
+ const leaseToken = requireClaimToken(input.leaseToken, "Delivery lease token");
125
+ const effectToken = requireOpaqueToken(
126
+ input.effectToken,
127
+ "Host-effect token",
128
+ "host_effect_token_invalid",
129
+ );
130
+ const response = await this.#post(RECEIVER_HTTP_ROUTES.acknowledgement, {
131
+ connector_token: this.#connectorToken,
132
+ delivery_id: deliveryId,
133
+ lease_token: leaseToken,
134
+ effect_token: effectToken,
135
+ });
136
+ if (response.status !== 200) {
137
+ throw await parseHttpFailure(response);
138
+ }
139
+ const value = await parseCanonicalJsonResponse(response);
140
+ return normalizeAcknowledgement(value, deliveryId);
141
+ }
142
+
143
+ async #post(path, body) {
144
+ const target = `${this.#baseUrl}${path}`;
145
+ let response;
146
+ try {
147
+ response = await globalThis.fetch(target, {
148
+ method: "POST",
149
+ headers: {
150
+ Accept: RECEIVER_HTTP_CONTENT_TYPE,
151
+ "Content-Type": RECEIVER_HTTP_CONTENT_TYPE,
152
+ },
153
+ body: canonicalJson(body),
154
+ cache: "no-store",
155
+ credentials: "omit",
156
+ redirect: "manual",
157
+ signal: AbortSignal.timeout(this.#requestTimeoutMs),
158
+ });
159
+ } catch (error) {
160
+ if (error?.name === "TimeoutError" || error?.name === "AbortError") {
161
+ throw clientFailure(
162
+ "connector_request_timeout",
163
+ "Local Connector request timed out",
164
+ undefined,
165
+ error,
166
+ );
167
+ }
168
+ throw clientFailure(
169
+ "connector_network_error",
170
+ "Local Connector request failed",
171
+ undefined,
172
+ error,
173
+ );
174
+ }
175
+ if (response.status >= 300 && response.status <= 399) {
176
+ try {
177
+ await response.body?.cancel();
178
+ } catch {
179
+ // Redirect rejection is authoritative even if the response body cannot be cancelled.
180
+ }
181
+ throw clientFailure(
182
+ "connector_redirect_rejected",
183
+ "Local Connector does not follow redirects",
184
+ response.status,
185
+ );
186
+ }
187
+ return response;
188
+ }
189
+ }
190
+
191
+ export class ConnectorTransportError extends Error {
192
+ constructor(code, message, { statusCode, cause } = {}) {
193
+ super(message, cause === undefined ? undefined : { cause });
194
+ this.name = "ConnectorTransportError";
195
+ this.code = code;
196
+ this.statusCode = statusCode;
197
+ }
198
+ }
199
+
200
+ function requireClientInput(value, expectedFields, label) {
201
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
202
+ throw clientFailure("connector_input_invalid", `${label} must be an object`);
203
+ }
204
+ const prototype = Object.getPrototypeOf(value);
205
+ if (prototype !== Object.prototype && prototype !== null) {
206
+ throw clientFailure("connector_input_invalid", `${label} must be a plain object`);
207
+ }
208
+ for (const key of Reflect.ownKeys(value)) {
209
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
210
+ if (typeof key === "symbol" || !descriptor?.enumerable || !("value" in descriptor)) {
211
+ throw clientFailure("connector_input_invalid", `${label} contains an invalid property`);
212
+ }
213
+ }
214
+ const actual = Object.keys(value).sort();
215
+ const expected = [...expectedFields].sort();
216
+ if (
217
+ actual.length !== expected.length ||
218
+ actual.some((field, index) => field !== expected[index])
219
+ ) {
220
+ throw clientFailure("connector_input_invalid", `${label} fields are invalid`);
221
+ }
222
+ }
223
+
224
+ function requireReceiverOrigin(value) {
225
+ if (typeof value !== "string" || value.length === 0 || value.length > 2_048) {
226
+ throw clientFailure("connector_origin_invalid", "Receiver origin is invalid");
227
+ }
228
+ let parsed;
229
+ try {
230
+ parsed = new URL(value);
231
+ } catch {
232
+ throw clientFailure("connector_origin_invalid", "Receiver origin is invalid");
233
+ }
234
+ const loopback = ["127.0.0.1", "[::1]", "::1"].includes(parsed.hostname);
235
+ if (
236
+ !["http:", "https:"].includes(parsed.protocol) ||
237
+ (parsed.protocol === "http:" && !loopback) ||
238
+ parsed.username ||
239
+ parsed.password ||
240
+ parsed.pathname !== "/" ||
241
+ parsed.search ||
242
+ parsed.hash ||
243
+ parsed.origin !== value
244
+ ) {
245
+ throw clientFailure("connector_origin_invalid", "Receiver origin is invalid");
246
+ }
247
+ return value;
248
+ }
249
+
250
+ function requireOpaqueToken(value, label, code) {
251
+ if (
252
+ typeof value !== "string" ||
253
+ value.length === 0 ||
254
+ Buffer.byteLength(value, "utf8") > MAX_OPAQUE_TOKEN_BYTES ||
255
+ /[^\x21-\x7e]/.test(value)
256
+ ) {
257
+ throw clientFailure(code, `${label} is invalid`);
258
+ }
259
+ return value;
260
+ }
261
+
262
+ function requireClaimToken(value, label) {
263
+ if (typeof value !== "string" || !CLAIM_TOKEN_PATTERN.test(value)) {
264
+ throw clientFailure("delivery_claim_token_invalid", `${label} is invalid`);
265
+ }
266
+ const decoded = Buffer.from(value, "base64url");
267
+ if (decoded.length !== 32 || decoded.toString("base64url") !== value) {
268
+ throw clientFailure("delivery_claim_token_invalid", `${label} is invalid`);
269
+ }
270
+ return value;
271
+ }
272
+
273
+ function requireIdentifier(value, label, code = "connector_response_invalid") {
274
+ if (
275
+ typeof value !== "string" ||
276
+ Buffer.byteLength(value, "utf8") > 160 ||
277
+ !IDENTIFIER_PATTERN.test(value)
278
+ ) {
279
+ throw clientFailure(code, `${label} is invalid`);
280
+ }
281
+ return value;
282
+ }
283
+
284
+ function normalizeClaimResult(value, expectedToken, nowMs) {
285
+ try {
286
+ requireResponseRecord(value, CLAIM_RESULT_FIELDS);
287
+ if (typeof value.duplicate !== "boolean") throw invalidResponse();
288
+ requireResponseRecord(value.lease, LEASE_FIELDS);
289
+ const lease = value.lease;
290
+ if (
291
+ lease.type !== DELIVERY_LEASE_TYPE ||
292
+ lease.protocol_version !== PROTOCOL_VERSION ||
293
+ lease.lease_token !== expectedToken ||
294
+ !Number.isSafeInteger(lease.attempt) ||
295
+ lease.attempt < 1 ||
296
+ lease.attempt > 100
297
+ ) {
298
+ throw invalidResponse();
299
+ }
300
+ const leaseExpiresAt = requireTimestamp(lease.lease_expires_at);
301
+ if (leaseExpiresAt <= nowMs) throw invalidResponse();
302
+ const receipt = validateContinuationReceipt(lease.receipt);
303
+ if (leaseExpiresAt > Date.parse(receipt.expires_at)) throw invalidResponse();
304
+ const continuation = normalizeContinuation(lease.continuation);
305
+ if (
306
+ continuation.correlation_id !== receipt.correlation_id ||
307
+ continuation.workflow_id !== receipt.workflow_id ||
308
+ continuation.event_type !== receipt.event_type ||
309
+ continuation.canonical_url !== receipt.canonical_url
310
+ ) {
311
+ throw invalidResponse();
312
+ }
313
+ return deepFreeze({
314
+ duplicate: value.duplicate,
315
+ lease: {
316
+ type: DELIVERY_LEASE_TYPE,
317
+ protocol_version: PROTOCOL_VERSION,
318
+ delivery_id: requireIdentifier(lease.delivery_id, "lease delivery_id"),
319
+ event_id: requireIdentifier(lease.event_id, "lease event_id"),
320
+ attempt: lease.attempt,
321
+ lease_token: expectedToken,
322
+ lease_expires_at: lease.lease_expires_at,
323
+ continuation,
324
+ receipt,
325
+ },
326
+ });
327
+ } catch (error) {
328
+ if (error instanceof ConnectorTransportError) throw error;
329
+ throw invalidResponse(error);
330
+ }
331
+ }
332
+
333
+ function normalizeContinuation(value) {
334
+ requireResponseRecord(value, CONTINUATION_FIELDS);
335
+ if (
336
+ value.event_sequence !== 1 ||
337
+ !Number.isSafeInteger(value.state_version) ||
338
+ value.state_version < 0
339
+ ) {
340
+ throw invalidResponse();
341
+ }
342
+ requireTimestamp(value.occurred_at);
343
+ return {
344
+ correlation_id: requireIdentifier(value.correlation_id, "continuation correlation_id"),
345
+ workflow_id: requireIdentifier(value.workflow_id, "continuation workflow_id"),
346
+ event_type: requireIdentifier(value.event_type, "continuation event_type"),
347
+ event_sequence: 1,
348
+ state_version: value.state_version,
349
+ occurred_at: value.occurred_at,
350
+ canonical_url: value.canonical_url,
351
+ };
352
+ }
353
+
354
+ function normalizeAcknowledgement(value, expectedDeliveryId) {
355
+ try {
356
+ requireResponseRecord(value, ACKNOWLEDGEMENT_FIELDS);
357
+ if (
358
+ value.type !== DELIVERY_ACKNOWLEDGEMENT_TYPE ||
359
+ value.protocol_version !== PROTOCOL_VERSION ||
360
+ value.delivery_id !== expectedDeliveryId ||
361
+ value.acknowledged !== true ||
362
+ typeof value.duplicate !== "boolean" ||
363
+ value.status !== "acknowledged"
364
+ ) {
365
+ throw invalidResponse();
366
+ }
367
+ return deepFreeze({
368
+ type: DELIVERY_ACKNOWLEDGEMENT_TYPE,
369
+ protocol_version: PROTOCOL_VERSION,
370
+ delivery_id: requireIdentifier(value.delivery_id, "acknowledgement delivery_id"),
371
+ event_id: requireIdentifier(value.event_id, "acknowledgement event_id"),
372
+ effect_id: requireIdentifier(value.effect_id, "acknowledgement effect_id"),
373
+ acknowledged: true,
374
+ duplicate: value.duplicate,
375
+ status: "acknowledged",
376
+ });
377
+ } catch (error) {
378
+ if (error instanceof ConnectorTransportError) throw error;
379
+ throw invalidResponse(error);
380
+ }
381
+ }
382
+
383
+ function requireResponseRecord(value, expectedFields) {
384
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw invalidResponse();
385
+ const prototype = Object.getPrototypeOf(value);
386
+ if (prototype !== Object.prototype && prototype !== null) throw invalidResponse();
387
+ const actual = Object.keys(value).sort();
388
+ const expected = [...expectedFields].sort();
389
+ if (
390
+ actual.length !== expected.length ||
391
+ actual.some((field, index) => field !== expected[index])
392
+ ) {
393
+ throw invalidResponse();
394
+ }
395
+ }
396
+
397
+ function requireTimestamp(value) {
398
+ if (typeof value !== "string" || value.length > 27) throw invalidResponse();
399
+ const parsed = Date.parse(value);
400
+ if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) {
401
+ throw invalidResponse();
402
+ }
403
+ return parsed;
404
+ }
405
+
406
+ async function requireEmptyResponse(response) {
407
+ const body = await readBoundedBody(response);
408
+ if (body.length !== 0 || response.headers.has("content-type")) {
409
+ throw invalidResponse();
410
+ }
411
+ }
412
+
413
+ async function parseCanonicalJsonResponse(response) {
414
+ const contentType = response.headers.get("content-type");
415
+ if (contentType === null || !CONTENT_TYPE_PATTERN.test(contentType)) {
416
+ throw invalidResponse();
417
+ }
418
+ const body = await readBoundedBody(response);
419
+ if (body.length === 0) throw invalidResponse();
420
+ let value;
421
+ try {
422
+ value = JSON.parse(body);
423
+ if (canonicalJson(value) !== body) throw invalidResponse();
424
+ } catch (error) {
425
+ if (error instanceof ConnectorTransportError) throw error;
426
+ throw invalidResponse(error);
427
+ }
428
+ return value;
429
+ }
430
+
431
+ async function parseHttpFailure(response) {
432
+ try {
433
+ const value = await parseCanonicalJsonResponse(response);
434
+ requireResponseRecord(value, ERROR_RESPONSE_FIELDS);
435
+ requireResponseRecord(value.error, ERROR_FIELDS);
436
+ if (typeof value.error.code !== "string" || !ERROR_CODE_PATTERN.test(value.error.code)) {
437
+ throw invalidResponse();
438
+ }
439
+ return clientFailure(
440
+ value.error.code,
441
+ "Cloud Receiver rejected the Local Connector request",
442
+ response.status,
443
+ );
444
+ } catch (error) {
445
+ if (error instanceof ConnectorTransportError && error.code !== "connector_response_invalid") {
446
+ return error;
447
+ }
448
+ return clientFailure(
449
+ "connector_http_error",
450
+ "Cloud Receiver returned an invalid error response",
451
+ response.status,
452
+ error,
453
+ );
454
+ }
455
+ }
456
+
457
+ async function readBoundedBody(response) {
458
+ const declared = response.headers.get("content-length");
459
+ if (declared !== null) {
460
+ if (!/^(?:0|[1-9][0-9]*)$/.test(declared)) throw invalidResponse();
461
+ if (Number(declared) > RECEIVER_HTTP_LIMITS.responseBytes) {
462
+ await response.body?.cancel();
463
+ throw clientFailure(
464
+ "connector_response_too_large",
465
+ "Cloud Receiver response is too large",
466
+ response.status,
467
+ );
468
+ }
469
+ }
470
+ if (response.body === null) return "";
471
+
472
+ const reader = response.body.getReader();
473
+ const chunks = [];
474
+ let size = 0;
475
+ try {
476
+ while (true) {
477
+ const { done, value } = await reader.read();
478
+ if (done) break;
479
+ const chunk = Buffer.from(value);
480
+ size += chunk.length;
481
+ if (size > RECEIVER_HTTP_LIMITS.responseBytes) {
482
+ await reader.cancel();
483
+ throw clientFailure(
484
+ "connector_response_too_large",
485
+ "Cloud Receiver response is too large",
486
+ response.status,
487
+ );
488
+ }
489
+ chunks.push(chunk);
490
+ }
491
+ } catch (error) {
492
+ if (error instanceof ConnectorTransportError) throw error;
493
+ throw clientFailure(
494
+ "connector_network_error",
495
+ "Cloud Receiver response stream failed",
496
+ response.status,
497
+ error,
498
+ );
499
+ } finally {
500
+ reader.releaseLock();
501
+ }
502
+ try {
503
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
504
+ Buffer.concat(chunks),
505
+ );
506
+ } catch (error) {
507
+ throw invalidResponse(error);
508
+ }
509
+ }
510
+
511
+ function invalidResponse(cause) {
512
+ return clientFailure(
513
+ "connector_response_invalid",
514
+ "Cloud Receiver response is invalid",
515
+ undefined,
516
+ cause,
517
+ );
518
+ }
519
+
520
+ function clientFailure(code, message, statusCode, cause) {
521
+ return new ConnectorTransportError(code, message, { statusCode, cause });
522
+ }
523
+
524
+ function deepFreeze(value) {
525
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
526
+ for (const child of Object.values(value)) deepFreeze(child);
527
+ Object.freeze(value);
528
+ }
529
+ return value;
530
+ }