@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,545 @@
1
+ import { spawn } from "node:child_process";
2
+ import { TextDecoder } from "node:util";
3
+
4
+ export const PAIRING_CLIENT_ROUTES = Object.freeze({
5
+ claim: "/v0.1/pairing-sessions/claim",
6
+ poll: "/v0.1/pairing-sessions/poll",
7
+ deviceStart: "/v0.1/device-authorizations",
8
+ devicePoll: "/v0.1/device-authorizations/poll",
9
+ });
10
+
11
+ const OPTION_FIELDS = Object.freeze(["baseUrl", "requestTimeoutMs", "openBrowser", "sleep"]);
12
+ const PAIR_INPUT_FIELDS = Object.freeze(["userCode"]);
13
+ const CONNECT_INPUT_FIELDS = Object.freeze(["deviceName"]);
14
+ const DEVICE_AUTHORIZATION_FIELDS = Object.freeze([
15
+ "type",
16
+ "protocol_version",
17
+ "authorization_id",
18
+ "device_code",
19
+ "verification_uri",
20
+ "expires_at",
21
+ "poll_interval_seconds",
22
+ ]);
23
+ const DEVICE_STATUS_FIELDS = Object.freeze([
24
+ "type",
25
+ "protocol_version",
26
+ "authorization_id",
27
+ "status",
28
+ "expires_at",
29
+ "poll_interval_seconds",
30
+ ]);
31
+ const DEVICE_CREDENTIAL_FIELDS = Object.freeze([
32
+ "type",
33
+ "protocol_version",
34
+ "authorization_id",
35
+ "connector_id",
36
+ "connector_token",
37
+ "connector_expires_at",
38
+ "duplicate",
39
+ ]);
40
+ const CLAIM_FIELDS = Object.freeze([
41
+ "type",
42
+ "protocol_version",
43
+ "pairing_id",
44
+ "device_code",
45
+ "verification_uri",
46
+ "expires_at",
47
+ "poll_interval_seconds",
48
+ ]);
49
+ const STATUS_FIELDS = Object.freeze([
50
+ "type",
51
+ "protocol_version",
52
+ "pairing_id",
53
+ "status",
54
+ "expires_at",
55
+ "poll_interval_seconds",
56
+ ]);
57
+ const CREDENTIAL_FIELDS = Object.freeze([
58
+ "type",
59
+ "protocol_version",
60
+ "pairing_id",
61
+ "connector_id",
62
+ "connector_token",
63
+ "connector_expires_at",
64
+ "duplicate",
65
+ ]);
66
+ const ERROR_FIELDS = Object.freeze(["error"]);
67
+ const ERROR_BODY_FIELDS = Object.freeze(["code"]);
68
+ const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/;
69
+ const TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
70
+ const USER_CODE_PATTERN = /^[A-F0-9]{16}$/;
71
+ const CONTENT_TYPE_PATTERN = /^application\/json(?:\s*;\s*charset=utf-8)?$/i;
72
+ const MIN_REQUEST_TIMEOUT_MS = 100;
73
+ const MAX_REQUEST_TIMEOUT_MS = 60_000;
74
+ const MAX_RESPONSE_BYTES = 32 * 1_024;
75
+
76
+ export class PairingClientError extends Error {
77
+ constructor(code, message, options = {}) {
78
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
79
+ this.name = "PairingClientError";
80
+ this.code = code;
81
+ this.statusCode = options.statusCode;
82
+ }
83
+ }
84
+
85
+ export class LocalConnectorPairingClient {
86
+ #baseUrl;
87
+ #requestTimeoutMs;
88
+ #openBrowser;
89
+ #sleep;
90
+
91
+ constructor(options) {
92
+ requireExactRecord(options, OPTION_FIELDS, ["baseUrl"], "Pairing client options");
93
+ this.#baseUrl = requireReceiverOrigin(options.baseUrl);
94
+ this.#requestTimeoutMs = requireTimeout(options.requestTimeoutMs ?? 5_000);
95
+ this.#openBrowser = options.openBrowser ?? defaultOpenBrowser;
96
+ this.#sleep = options.sleep ?? defaultSleep;
97
+ if (typeof this.#openBrowser !== "function") {
98
+ throw new TypeError("Pairing client openBrowser must be a function");
99
+ }
100
+ if (typeof this.#sleep !== "function") {
101
+ throw new TypeError("Pairing client sleep must be a function");
102
+ }
103
+ }
104
+
105
+ async pair(input, onReady = undefined) {
106
+ requireExactRecord(input, PAIR_INPUT_FIELDS, PAIR_INPUT_FIELDS, "Pairing input");
107
+ const userCode = normalizeUserCode(input.userCode);
108
+ const claimResponse = await this.#post(PAIRING_CLIENT_ROUTES.claim, {
109
+ user_code: formatUserCode(userCode),
110
+ });
111
+ if (claimResponse.status !== 200) throw await parseHttpFailure(claimResponse);
112
+ const claim = normalizeClaim(await parseJsonResponse(claimResponse), this.#baseUrl);
113
+ if (typeof onReady === "function") {
114
+ await onReady({
115
+ verificationUri: claim.verification_uri,
116
+ expiresAt: claim.expires_at,
117
+ pollIntervalSeconds: claim.poll_interval_seconds,
118
+ });
119
+ }
120
+ let browserOpened = false;
121
+ try {
122
+ browserOpened = (await this.#openBrowser(claim.verification_uri)) === true;
123
+ } catch {
124
+ browserOpened = false;
125
+ }
126
+
127
+ const deadline = Date.parse(claim.expires_at);
128
+ while (true) {
129
+ const response = await this.#post(PAIRING_CLIENT_ROUTES.poll, {
130
+ device_code: claim.device_code,
131
+ });
132
+ if (response.status === 202) {
133
+ const pending = normalizeStatus(await parseJsonResponse(response));
134
+ if (pending.pairing_id !== claim.pairing_id) {
135
+ throw pairingFailure("pairing_response_invalid", "Pairing status is mismatched");
136
+ }
137
+ if (Date.now() >= deadline) throw pairingFailure("pairing_expired", "Pairing expired");
138
+ await this.#sleep(pending.poll_interval_seconds * 1_000);
139
+ continue;
140
+ }
141
+ if (response.status !== 200) throw await parseHttpFailure(response);
142
+ const credentials = normalizeCredentials(await parseJsonResponse(response));
143
+ if (credentials.pairing_id !== claim.pairing_id) {
144
+ throw pairingFailure("pairing_response_invalid", "Pairing credentials are mismatched");
145
+ }
146
+ return Object.freeze({ ...credentials, browserOpened });
147
+ }
148
+ }
149
+
150
+ async connect(input, onReady = undefined) {
151
+ requireExactRecord(input, CONNECT_INPUT_FIELDS, CONNECT_INPUT_FIELDS, "Device connection input");
152
+ const deviceName = requireDeviceName(input.deviceName);
153
+ const startResponse = await this.#post(PAIRING_CLIENT_ROUTES.deviceStart, {
154
+ device_name: deviceName,
155
+ });
156
+ if (startResponse.status !== 201) throw await parseHttpFailure(startResponse);
157
+ const authorization = normalizeDeviceAuthorization(
158
+ await parseJsonResponse(startResponse),
159
+ this.#baseUrl,
160
+ );
161
+ if (typeof onReady === "function") {
162
+ await onReady({
163
+ verificationUri: authorization.verification_uri,
164
+ expiresAt: authorization.expires_at,
165
+ pollIntervalSeconds: authorization.poll_interval_seconds,
166
+ });
167
+ }
168
+ let browserOpened = false;
169
+ try {
170
+ browserOpened = (await this.#openBrowser(authorization.verification_uri)) === true;
171
+ } catch {
172
+ browserOpened = false;
173
+ }
174
+
175
+ const deadline = Date.parse(authorization.expires_at);
176
+ while (true) {
177
+ const response = await this.#post(PAIRING_CLIENT_ROUTES.devicePoll, {
178
+ device_code: authorization.device_code,
179
+ });
180
+ if (response.status === 202) {
181
+ const pending = normalizeDeviceStatus(await parseJsonResponse(response));
182
+ if (pending.authorization_id !== authorization.authorization_id) {
183
+ throw pairingFailure("pairing_response_invalid", "Device authorization status is mismatched");
184
+ }
185
+ if (Date.now() >= deadline) {
186
+ throw pairingFailure("device_authorization_expired", "Device authorization expired");
187
+ }
188
+ await this.#sleep(pending.poll_interval_seconds * 1_000);
189
+ continue;
190
+ }
191
+ if (response.status !== 200) throw await parseHttpFailure(response);
192
+ const credentials = normalizeDeviceCredentials(await parseJsonResponse(response));
193
+ if (credentials.authorization_id !== authorization.authorization_id) {
194
+ throw pairingFailure("pairing_response_invalid", "Device credentials are mismatched");
195
+ }
196
+ return Object.freeze({ ...credentials, browserOpened });
197
+ }
198
+ }
199
+
200
+ async #post(path, body) {
201
+ let response;
202
+ try {
203
+ response = await globalThis.fetch(`${this.#baseUrl}${path}`, {
204
+ method: "POST",
205
+ headers: {
206
+ Accept: "application/json",
207
+ "Content-Type": "application/json",
208
+ },
209
+ body: JSON.stringify(body),
210
+ cache: "no-store",
211
+ credentials: "omit",
212
+ redirect: "manual",
213
+ signal: AbortSignal.timeout(this.#requestTimeoutMs),
214
+ });
215
+ } catch (error) {
216
+ if (error?.name === "TimeoutError" || error?.name === "AbortError") {
217
+ throw pairingFailure("pairing_request_timeout", "Pairing request timed out", { cause: error });
218
+ }
219
+ throw pairingFailure("pairing_network_error", "Pairing request failed", { cause: error });
220
+ }
221
+ if (response.status >= 300 && response.status <= 399) {
222
+ try {
223
+ await response.body?.cancel();
224
+ } catch {
225
+ // Redirect rejection remains authoritative even if the body cannot be cancelled.
226
+ }
227
+ throw pairingFailure("pairing_redirect_rejected", "Pairing client does not follow redirects", {
228
+ statusCode: response.status,
229
+ });
230
+ }
231
+ return response;
232
+ }
233
+ }
234
+
235
+ function normalizeDeviceAuthorization(value, expectedOrigin) {
236
+ requireExactRecord(
237
+ value,
238
+ DEVICE_AUTHORIZATION_FIELDS,
239
+ DEVICE_AUTHORIZATION_FIELDS,
240
+ "Device authorization response",
241
+ );
242
+ if (value.type !== "webmcp.connector_device_authorization" || value.protocol_version !== "0.1") {
243
+ throw pairingFailure("pairing_response_invalid", "Device authorization response is unsupported");
244
+ }
245
+ return {
246
+ authorization_id: requireIdentifier(value.authorization_id, "authorization_id"),
247
+ device_code: requireToken(value.device_code, "device_code"),
248
+ verification_uri: requireDeviceVerificationUri(value.verification_uri, expectedOrigin),
249
+ expires_at: requireTimestamp(value.expires_at, "expires_at"),
250
+ poll_interval_seconds: requirePollInterval(value.poll_interval_seconds),
251
+ };
252
+ }
253
+
254
+ function normalizeDeviceStatus(value) {
255
+ requireExactRecord(value, DEVICE_STATUS_FIELDS, DEVICE_STATUS_FIELDS, "Device authorization status");
256
+ if (
257
+ value.type !== "webmcp.connector_device_authorization_status" ||
258
+ value.protocol_version !== "0.1" ||
259
+ value.status !== "pending"
260
+ ) {
261
+ throw pairingFailure("pairing_response_invalid", "Device authorization status is unsupported");
262
+ }
263
+ return {
264
+ authorization_id: requireIdentifier(value.authorization_id, "authorization_id"),
265
+ expires_at: requireTimestamp(value.expires_at, "expires_at"),
266
+ poll_interval_seconds: requirePollInterval(value.poll_interval_seconds),
267
+ };
268
+ }
269
+
270
+ function normalizeDeviceCredentials(value) {
271
+ requireExactRecord(value, DEVICE_CREDENTIAL_FIELDS, DEVICE_CREDENTIAL_FIELDS, "Device credentials");
272
+ if (value.type !== "webmcp.connector_credentials" || value.protocol_version !== "0.1") {
273
+ throw pairingFailure("pairing_response_invalid", "Device credential response is unsupported");
274
+ }
275
+ if (typeof value.duplicate !== "boolean") {
276
+ throw pairingFailure("pairing_response_invalid", "Device credential duplicate flag is invalid");
277
+ }
278
+ return {
279
+ authorization_id: requireIdentifier(value.authorization_id, "authorization_id"),
280
+ connector_id: requireIdentifier(value.connector_id, "connector_id"),
281
+ connector_token: requireToken(value.connector_token, "connector_token"),
282
+ connector_expires_at: requireTimestamp(value.connector_expires_at, "connector_expires_at"),
283
+ duplicate: value.duplicate,
284
+ };
285
+ }
286
+
287
+ function normalizeClaim(value, expectedOrigin) {
288
+ requireExactRecord(value, CLAIM_FIELDS, CLAIM_FIELDS, "Pairing claim response");
289
+ if (value.type !== "webmcp.connector_pairing_claim" || value.protocol_version !== "0.1") {
290
+ throw pairingFailure("pairing_response_invalid", "Pairing claim response is unsupported");
291
+ }
292
+ return {
293
+ pairing_id: requireIdentifier(value.pairing_id, "pairing_id"),
294
+ device_code: requireToken(value.device_code, "device_code"),
295
+ verification_uri: requireVerificationUri(value.verification_uri, expectedOrigin),
296
+ expires_at: requireTimestamp(value.expires_at, "expires_at"),
297
+ poll_interval_seconds: requirePollInterval(value.poll_interval_seconds),
298
+ };
299
+ }
300
+
301
+ function normalizeStatus(value) {
302
+ requireExactRecord(value, STATUS_FIELDS, STATUS_FIELDS, "Pairing status response");
303
+ if (
304
+ value.type !== "webmcp.connector_pairing_status" ||
305
+ value.protocol_version !== "0.1" ||
306
+ value.status !== "pending"
307
+ ) {
308
+ throw pairingFailure("pairing_response_invalid", "Pairing status response is unsupported");
309
+ }
310
+ return {
311
+ pairing_id: requireIdentifier(value.pairing_id, "pairing_id"),
312
+ expires_at: requireTimestamp(value.expires_at, "expires_at"),
313
+ poll_interval_seconds: requirePollInterval(value.poll_interval_seconds),
314
+ };
315
+ }
316
+
317
+ function normalizeCredentials(value) {
318
+ requireExactRecord(value, CREDENTIAL_FIELDS, CREDENTIAL_FIELDS, "Connector credential response");
319
+ if (value.type !== "webmcp.connector_credentials" || value.protocol_version !== "0.1") {
320
+ throw pairingFailure("pairing_response_invalid", "Connector credential response is unsupported");
321
+ }
322
+ if (typeof value.duplicate !== "boolean") {
323
+ throw pairingFailure("pairing_response_invalid", "Connector credential duplicate flag is invalid");
324
+ }
325
+ return {
326
+ pairing_id: requireIdentifier(value.pairing_id, "pairing_id"),
327
+ connector_id: requireIdentifier(value.connector_id, "connector_id"),
328
+ connector_token: requireToken(value.connector_token, "connector_token"),
329
+ connector_expires_at: requireTimestamp(value.connector_expires_at, "connector_expires_at"),
330
+ duplicate: value.duplicate,
331
+ };
332
+ }
333
+
334
+ async function parseJsonResponse(response) {
335
+ const declared = response.headers.get("content-length");
336
+ if (declared !== null && (!/^(?:0|[1-9][0-9]*)$/.test(declared) || Number(declared) > MAX_RESPONSE_BYTES)) {
337
+ throw pairingFailure("pairing_response_invalid", "Pairing response is too large");
338
+ }
339
+ const buffer = Buffer.from(await response.arrayBuffer());
340
+ if (buffer.length > MAX_RESPONSE_BYTES) throw pairingFailure("pairing_response_invalid", "Pairing response is too large");
341
+ if (!CONTENT_TYPE_PATTERN.test(response.headers.get("content-type") ?? "")) {
342
+ throw pairingFailure("pairing_response_invalid", "Pairing response content type is invalid");
343
+ }
344
+ let value;
345
+ try {
346
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffer));
347
+ } catch {
348
+ throw pairingFailure("pairing_response_invalid", "Pairing response JSON is invalid");
349
+ }
350
+ return value;
351
+ }
352
+
353
+ async function parseHttpFailure(response) {
354
+ let code = "pairing_http_error";
355
+ try {
356
+ const value = await parseJsonResponse(response);
357
+ requireExactRecord(value, ERROR_FIELDS, ERROR_FIELDS, "Pairing error response");
358
+ requireExactRecord(value.error, ERROR_BODY_FIELDS, ERROR_BODY_FIELDS, "Pairing error body");
359
+ if (typeof value.error.code === "string" && /^[a-z][a-z0-9_]{0,95}$/.test(value.error.code)) {
360
+ code = value.error.code;
361
+ }
362
+ } catch {
363
+ // The public failure remains one bounded code.
364
+ }
365
+ return pairingFailure(code, "Pairing request was rejected", { statusCode: response.status });
366
+ }
367
+
368
+ function requireReceiverOrigin(value) {
369
+ if (typeof value !== "string" || value.length === 0 || value.length > 2_048) {
370
+ throw pairingFailure("pairing_origin_invalid", "Receiver origin is invalid");
371
+ }
372
+ let parsed;
373
+ try {
374
+ parsed = new URL(value);
375
+ } catch {
376
+ throw pairingFailure("pairing_origin_invalid", "Receiver origin is invalid");
377
+ }
378
+ const loopback = ["127.0.0.1", "[::1]", "::1", "localhost"].includes(parsed.hostname);
379
+ if (
380
+ !["http:", "https:"].includes(parsed.protocol) ||
381
+ (parsed.protocol === "http:" && !loopback) ||
382
+ parsed.username ||
383
+ parsed.password ||
384
+ parsed.pathname !== "/" ||
385
+ parsed.search ||
386
+ parsed.hash ||
387
+ parsed.origin !== value
388
+ ) {
389
+ throw pairingFailure("pairing_origin_invalid", "Receiver origin is invalid");
390
+ }
391
+ return value;
392
+ }
393
+
394
+ function requireVerificationUri(value, expectedOrigin) {
395
+ if (typeof value !== "string" || value.length > 2_048) {
396
+ throw pairingFailure("pairing_response_invalid", "Pairing verification URI is invalid");
397
+ }
398
+ let parsed;
399
+ try {
400
+ parsed = new URL(value);
401
+ } catch {
402
+ throw pairingFailure("pairing_response_invalid", "Pairing verification URI is invalid");
403
+ }
404
+ if (
405
+ !["http:", "https:"].includes(parsed.protocol) ||
406
+ parsed.username ||
407
+ parsed.password ||
408
+ parsed.pathname !== "/pairing" ||
409
+ parsed.hash ||
410
+ parsed.origin !== expectedOrigin ||
411
+ parsed.searchParams.getAll("code").length !== 1 ||
412
+ [...parsed.searchParams.keys()].some((key) => key !== "code")
413
+ ) {
414
+ throw pairingFailure("pairing_response_invalid", "Pairing verification URI is invalid");
415
+ }
416
+ normalizeUserCode(parsed.searchParams.get("code"));
417
+ return value;
418
+ }
419
+
420
+ function requireDeviceVerificationUri(value, expectedOrigin) {
421
+ if (typeof value !== "string" || value.length > 2_048) {
422
+ throw pairingFailure("pairing_response_invalid", "Device verification URI is invalid");
423
+ }
424
+ let parsed;
425
+ try {
426
+ parsed = new URL(value);
427
+ } catch {
428
+ throw pairingFailure("pairing_response_invalid", "Device verification URI is invalid");
429
+ }
430
+ if (
431
+ !["http:", "https:"].includes(parsed.protocol) ||
432
+ parsed.username ||
433
+ parsed.password ||
434
+ parsed.pathname !== "/connect" ||
435
+ parsed.hash ||
436
+ parsed.origin !== expectedOrigin ||
437
+ parsed.searchParams.getAll("token").length !== 1 ||
438
+ [...parsed.searchParams.keys()].some((key) => key !== "token")
439
+ ) {
440
+ throw pairingFailure("pairing_response_invalid", "Device verification URI is invalid");
441
+ }
442
+ requireToken(parsed.searchParams.get("token"), "authorization_token");
443
+ return value;
444
+ }
445
+
446
+ function requireDeviceName(value) {
447
+ if (typeof value !== "string") {
448
+ throw pairingFailure("device_name_invalid", "Device name is invalid");
449
+ }
450
+ const name = value.trim();
451
+ if (name.length < 2 || Buffer.byteLength(name, "utf8") > 80 || /[\u0000-\u001f\u007f]/.test(name)) {
452
+ throw pairingFailure("device_name_invalid", "Device name is invalid");
453
+ }
454
+ return name;
455
+ }
456
+
457
+ function normalizeUserCode(value) {
458
+ if (typeof value !== "string") throw pairingFailure("pairing_code_invalid", "Pairing code is invalid");
459
+ const normalized = value.replaceAll("-", "").toUpperCase();
460
+ if (!USER_CODE_PATTERN.test(normalized)) throw pairingFailure("pairing_code_invalid", "Pairing code is invalid");
461
+ return normalized;
462
+ }
463
+
464
+ function formatUserCode(value) {
465
+ const normalized = normalizeUserCode(value);
466
+ return `${normalized.slice(0, 4)}-${normalized.slice(4, 8)}-${normalized.slice(8, 12)}-${normalized.slice(12)}`;
467
+ }
468
+
469
+ function requireToken(value, label) {
470
+ if (typeof value !== "string" || !TOKEN_PATTERN.test(value)) {
471
+ throw pairingFailure("pairing_response_invalid", `${label} is invalid`);
472
+ }
473
+ return value;
474
+ }
475
+
476
+ function requireIdentifier(value, label) {
477
+ if (typeof value !== "string" || !IDENTIFIER_PATTERN.test(value)) {
478
+ throw pairingFailure("pairing_response_invalid", `${label} is invalid`);
479
+ }
480
+ return value;
481
+ }
482
+
483
+ function requireTimestamp(value, label) {
484
+ if (typeof value !== "string" || value.length > 27) {
485
+ throw pairingFailure("pairing_response_invalid", `${label} is invalid`);
486
+ }
487
+ const parsed = Date.parse(value);
488
+ if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) {
489
+ throw pairingFailure("pairing_response_invalid", `${label} is invalid`);
490
+ }
491
+ return value;
492
+ }
493
+
494
+ function requirePollInterval(value) {
495
+ if (!Number.isSafeInteger(value) || value < 1 || value > 60) {
496
+ throw pairingFailure("pairing_response_invalid", "Pairing poll interval is invalid");
497
+ }
498
+ return value;
499
+ }
500
+
501
+ function requireTimeout(value) {
502
+ if (!Number.isSafeInteger(value) || value < MIN_REQUEST_TIMEOUT_MS || value > MAX_REQUEST_TIMEOUT_MS) {
503
+ throw pairingFailure("pairing_timeout_invalid", "Pairing request timeout is invalid");
504
+ }
505
+ return value;
506
+ }
507
+
508
+ function pairingFailure(code, message, options = {}) {
509
+ return new PairingClientError(code, message, options);
510
+ }
511
+
512
+ function requireExactRecord(value, allowedFields, requiredFields, label) {
513
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
514
+ throw pairingFailure("pairing_input_invalid", `${label} must be an object`);
515
+ }
516
+ const prototype = Object.getPrototypeOf(value);
517
+ if (prototype !== Object.prototype && prototype !== null) {
518
+ throw pairingFailure("pairing_input_invalid", `${label} must be a plain object`);
519
+ }
520
+ const fields = Object.keys(value);
521
+ if (fields.some((field) => !allowedFields.includes(field)) || requiredFields.some((field) => !fields.includes(field))) {
522
+ throw pairingFailure("pairing_input_invalid", `${label} fields are invalid`);
523
+ }
524
+ }
525
+
526
+ async function defaultSleep(milliseconds) {
527
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
528
+ }
529
+
530
+ async function defaultOpenBrowser(url) {
531
+ const command = process.platform === "darwin"
532
+ ? "open"
533
+ : process.platform === "win32"
534
+ ? "cmd.exe"
535
+ : "xdg-open";
536
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
537
+ return await new Promise((resolve) => {
538
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
539
+ child.once("error", () => resolve(false));
540
+ child.once("spawn", () => {
541
+ child.unref();
542
+ resolve(true);
543
+ });
544
+ });
545
+ }
@@ -0,0 +1,99 @@
1
+ import process from "node:process";
2
+
3
+ const RESET = "\u001b[0m";
4
+ const BOLD = "\u001b[1m";
5
+ const DIM = "\u001b[2m";
6
+ const GREEN = "\u001b[32m";
7
+ const YELLOW = "\u001b[33m";
8
+ const RED = "\u001b[31m";
9
+ const CYAN = "\u001b[36m";
10
+ const SPINNER_FRAMES = ["·", "✦", "✧", "✦"];
11
+
12
+ /**
13
+ * Small dependency-free terminal presentation for the Local Connector CLI.
14
+ * It never prints credentials and automatically stays silent when stdout is piped.
15
+ */
16
+ export function createTerminalUi(options = {}) {
17
+ const output = options.output ?? process.stdout;
18
+ const errorOutput = options.errorOutput ?? process.stderr;
19
+ const interactive = options.interactive ?? Boolean(output.isTTY);
20
+ const color = interactive && options.color !== false && !process.env.NO_COLOR && process.env.TERM !== "dumb";
21
+ let spinnerTimer = null;
22
+ let spinnerMessage = "";
23
+ let spinnerFrame = 0;
24
+
25
+ const style = (value, code) => color ? `${code}${value}${RESET}` : value;
26
+ const write = (value) => output.write(`${value}\n`);
27
+ const clearSpinnerLine = () => {
28
+ if (spinnerTimer === null) return;
29
+ output.write("\r\u001b[2K");
30
+ clearInterval(spinnerTimer);
31
+ spinnerTimer = null;
32
+ };
33
+ const renderSpinner = () => {
34
+ output.write(`\r\u001b[2K ${style(SPINNER_FRAMES[spinnerFrame], CYAN)} ${spinnerMessage}`);
35
+ spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
36
+ };
37
+
38
+ return Object.freeze({
39
+ interactive,
40
+
41
+ begin(title, subtitle) {
42
+ if (!interactive) return;
43
+ write("");
44
+ write(` ${style("RE-ENTRY", BOLD)} ${style("LOCAL CONNECTOR", DIM)}`);
45
+ write(` ${style(title, BOLD)}`);
46
+ if (subtitle) write(` ${style(subtitle, DIM)}`);
47
+ write("");
48
+ },
49
+
50
+ step(label, detail) {
51
+ if (!interactive) return;
52
+ write(` ${style("→", CYAN)} ${style(label, BOLD)}${detail ? ` ${detail}` : ""}`);
53
+ },
54
+
55
+ success(label, detail) {
56
+ if (!interactive) return;
57
+ write(` ${style("✓", GREEN)} ${style(label, BOLD)}${detail ? ` ${detail}` : ""}`);
58
+ },
59
+
60
+ info(label, detail) {
61
+ if (!interactive) return;
62
+ write(` ${style("·", CYAN)} ${style(label, BOLD)}${detail ? ` ${detail}` : ""}`);
63
+ },
64
+
65
+ warning(label, detail) {
66
+ if (!interactive) return;
67
+ write(` ${style("!", YELLOW)} ${style(label, BOLD)}${detail ? ` ${detail}` : ""}`);
68
+ },
69
+
70
+ wait(message) {
71
+ if (!interactive) return;
72
+ clearSpinnerLine();
73
+ spinnerMessage = message;
74
+ spinnerFrame = 0;
75
+ renderSpinner();
76
+ spinnerTimer = setInterval(renderSpinner, 450);
77
+ spinnerTimer.unref?.();
78
+ },
79
+
80
+ stopWait(label, detail, outcome = "success") {
81
+ if (!interactive) return;
82
+ clearSpinnerLine();
83
+ if (outcome === "warning") this.warning(label, detail);
84
+ else if (outcome === "info") this.info(label, detail);
85
+ else this.success(label, detail);
86
+ },
87
+
88
+ error(label, detail, hint) {
89
+ if (!interactive) return;
90
+ clearSpinnerLine();
91
+ errorOutput.write(` ${style("✕", RED)} ${style(label, BOLD)}${detail ? ` ${detail}` : ""}\n`);
92
+ if (hint) errorOutput.write(` ${style("Next:", DIM)} ${hint}\n`);
93
+ },
94
+
95
+ close() {
96
+ clearSpinnerLine();
97
+ },
98
+ });
99
+ }