@camerontaylor/paseo-client 0.9.0-beta.2.fork.1 → 0.10.1-fork.1

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.
@@ -22,6 +22,7 @@ export declare class ConnectionSubscriptions extends OwnedSubscriptions {
22
22
  };
23
23
  }
24
24
  export declare const DEFAULT_CLIENT_CAPABILITIES: {
25
+ hello_rejection: true;
25
26
  owned_subscriptions: true;
26
27
  all_providers: true;
27
28
  selective_agent_timeline: true;
@@ -90,6 +90,7 @@ export class ConnectionSubscriptions extends OwnedSubscriptions {
90
90
  // Protocol support belongs to the installed client. Only browser hosting needs
91
91
  // a resource supplied by the caller. Keep this exhaustive as the protocol evolves.
92
92
  export const DEFAULT_CLIENT_CAPABILITIES = {
93
+ [CLIENT_CAPS.helloRejection]: true,
93
94
  [CLIENT_CAPS.ownedSubscriptions]: true,
94
95
  [CLIENT_CAPS.allProviders]: true,
95
96
  [CLIENT_CAPS.selectiveAgentTimeline]: true,
@@ -4,5 +4,6 @@ export declare function createRelayE2eeTransportFactory(args: {
4
4
  daemonPublicKeyB64: string;
5
5
  logger: TransportLogger;
6
6
  }): DaemonTransportFactory;
7
+ export declare function createRelayTransportFactory(baseFactory: DaemonTransportFactory): DaemonTransportFactory;
7
8
  export declare function createEncryptedTransport(base: DaemonTransport, daemonPublicKeyB64: string, logger: TransportLogger): DaemonTransport;
8
9
  //# sourceMappingURL=daemon-client-relay-e2ee-transport.d.ts.map
@@ -1,11 +1,15 @@
1
1
  import { createClientChannel, } from "@camerontaylor/paseo-relay/e2ee";
2
2
  import { extractRelayMessage, normalizeTransportPayload } from "./daemon-client-transport-utils.js";
3
3
  export function createRelayE2eeTransportFactory(args) {
4
- return ({ url, headers }) => {
5
- const base = args.baseFactory({ url, headers });
4
+ return ({ url }) => {
5
+ const base = args.baseFactory({ url });
6
6
  return createEncryptedTransport(base, args.daemonPublicKeyB64, args.logger);
7
7
  };
8
8
  }
9
+ export function createRelayTransportFactory(baseFactory) {
10
+ // The relay upgrade precedes E2EE. Only the encrypted hello may carry daemon credentials.
11
+ return ({ url }) => baseFactory({ url });
12
+ }
9
13
  export function createEncryptedTransport(base, daemonPublicKeyB64, logger) {
10
14
  let channel = null;
11
15
  let opened = false;
@@ -1,5 +1,5 @@
1
1
  export type { DaemonTransport, DaemonTransportFactory, TransportLogger, WebSocketFactory, WebSocketLike, } from "./daemon-client-transport-types.js";
2
2
  export { decodeMessageData, describeTransportClose, describeTransportError, encodeUtf8String, extractRelayMessage, normalizeTransportPayload, safeRandomId, } from "./daemon-client-transport-utils.js";
3
- export { createEncryptedTransport, createRelayE2eeTransportFactory, } from "./daemon-client-relay-e2ee-transport.js";
3
+ export { createEncryptedTransport, createRelayE2eeTransportFactory, createRelayTransportFactory, } from "./daemon-client-relay-e2ee-transport.js";
4
4
  export { bindWsHandler, createWebSocketTransportFactory, defaultWebSocketFactory, } from "./daemon-client-websocket-transport.js";
5
5
  //# sourceMappingURL=daemon-client-transport.d.ts.map
@@ -1,4 +1,4 @@
1
1
  export { decodeMessageData, describeTransportClose, describeTransportError, encodeUtf8String, extractRelayMessage, normalizeTransportPayload, safeRandomId, } from "./daemon-client-transport-utils.js";
2
- export { createEncryptedTransport, createRelayE2eeTransportFactory, } from "./daemon-client-relay-e2ee-transport.js";
2
+ export { createEncryptedTransport, createRelayE2eeTransportFactory, createRelayTransportFactory, } from "./daemon-client-relay-e2ee-transport.js";
3
3
  export { bindWsHandler, createWebSocketTransportFactory, defaultWebSocketFactory, } from "./daemon-client-websocket-transport.js";
4
4
  //# sourceMappingURL=daemon-client-transport.js.map
@@ -30,6 +30,12 @@ export type ImportAgentInput = (ImportAgentInputBase & {
30
30
  provider: AgentProvider;
31
31
  sessionId: string;
32
32
  });
33
+ export type DaemonAuthFailureReason = "password_required" | "incorrect_password";
34
+ export declare class DaemonAuthenticationError extends Error {
35
+ readonly reason: DaemonAuthFailureReason;
36
+ constructor(reason: DaemonAuthFailureReason);
37
+ }
38
+ export declare function getDaemonAuthFailureReason(error: unknown): DaemonAuthFailureReason | null;
33
39
  export type { DaemonTransport, DaemonTransportFactory, WebSocketFactory, WebSocketLike, } from "./daemon-client-transport.js";
34
40
  export type { TerminalStreamEvent };
35
41
  export type ConnectionState = {
@@ -114,6 +120,7 @@ export interface DaemonClientConfig {
114
120
  appVersion?: string;
115
121
  runtimeGeneration?: number | null;
116
122
  password?: string;
123
+ localCredential?: () => string | undefined | Promise<string | undefined>;
117
124
  authHeader?: string;
118
125
  suppressSendErrors?: boolean;
119
126
  transportFactory?: DaemonTransportFactory;
@@ -578,6 +585,7 @@ export declare class DaemonClient {
578
585
  private readonly providerSnapshotUpdates;
579
586
  private readonly owned;
580
587
  private transport;
588
+ private helloAuth;
581
589
  private transportCleanup;
582
590
  private rawMessageListeners;
583
591
  private messageHandlers;
@@ -594,6 +602,7 @@ export declare class DaemonClient {
594
602
  private connectResolve;
595
603
  private connectReject;
596
604
  private lastErrorValue;
605
+ private authFailureReasonValue;
597
606
  private connectionState;
598
607
  private readonly terminalStreams;
599
608
  private pendingBinaryFileReads;
@@ -616,6 +625,7 @@ export declare class DaemonClient {
616
625
  constructor(config: DaemonClientConfig);
617
626
  connect(): Promise<void>;
618
627
  private attemptConnect;
628
+ private assertEncryptedRelayAuth;
619
629
  private resolveConnect;
620
630
  private rejectConnect;
621
631
  close(): Promise<void>;
@@ -628,6 +638,7 @@ export declare class DaemonClient {
628
638
  get isConnected(): boolean;
629
639
  get isConnecting(): boolean;
630
640
  get lastError(): string | null;
641
+ get authFailureReason(): DaemonAuthFailureReason | null;
631
642
  getLastLivenessRttMs(): number | null;
632
643
  subscribe(handler: DaemonEventHandler): () => void;
633
644
  subscribeRawMessages(handler: (message: SessionOutboundMessage) => void): () => void;
@@ -7,7 +7,7 @@ import { AgentCreateFailedStatusPayloadSchema, AgentCreatedStatusPayloadSchema,
7
7
  import { validateWSOutboundMessage } from "@camerontaylor/paseo-protocol/validation/ws-outbound";
8
8
  import { isRelayClientWebSocketUrl } from "@camerontaylor/paseo-protocol/daemon-endpoints";
9
9
  import { asUint8Array, decodeFileTransferFrame, encodeFileTransferFrame, decodeTerminalStreamFrame, FileTransferOpcode, TerminalStreamOpcode, } from "@camerontaylor/paseo-protocol/binary-frames/index";
10
- import { createRelayE2eeTransportFactory, createWebSocketTransportFactory, decodeMessageData, defaultWebSocketFactory, describeTransportClose, describeTransportError, } from "./daemon-client-transport.js";
10
+ import { createRelayE2eeTransportFactory, createRelayTransportFactory, createWebSocketTransportFactory, decodeMessageData, defaultWebSocketFactory, describeTransportClose, describeTransportError, } from "./daemon-client-transport.js";
11
11
  import { DaemonClientRuntimeMetrics } from "./daemon-client-runtime-metrics.js";
12
12
  import { normalizeListProviderModelsPayload, normalizeProviderSnapshotUpdateMessage, normalizeProvidersSnapshotPayload, } from "./compat/normalize-provider-models.js";
13
13
  import { TerminalStreamRouter } from "./terminal-stream-router.js";
@@ -27,6 +27,55 @@ function normalizePassword(value) {
27
27
  }
28
28
  return value.length > 0 ? value : null;
29
29
  }
30
+ function compatibleBearerPassword(password) {
31
+ return password && /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(password) ? password : null;
32
+ }
33
+ export class DaemonAuthenticationError extends Error {
34
+ constructor(reason) {
35
+ super(reason === "password_required" ? "Password required" : "Incorrect password");
36
+ this.name = "DaemonAuthenticationError";
37
+ this.reason = reason;
38
+ }
39
+ }
40
+ export function getDaemonAuthFailureReason(error) {
41
+ return error instanceof DaemonAuthenticationError ? error.reason : null;
42
+ }
43
+ function authFailureFromLegacyClose(event) {
44
+ if (!event || typeof event !== "object" || !("reason" in event))
45
+ return null;
46
+ if (event.reason === "Password required")
47
+ return "password_required";
48
+ if (event.reason === "Incorrect password")
49
+ return "incorrect_password";
50
+ return null;
51
+ }
52
+ function chooseConnectionAuth(config, localCredential) {
53
+ const password = normalizePassword(config.password);
54
+ let helloAuth;
55
+ if (localCredential)
56
+ helloAuth = { kind: "localCredential", token: localCredential };
57
+ else if (password)
58
+ helloAuth = { kind: "password", password };
59
+ const headers = {};
60
+ const compatibleBearer = localCredential ? null : compatibleBearerPassword(password);
61
+ // COMPAT(headerAuth): added in v0.9.1, remove after 2027-03-24.
62
+ if (compatibleBearer)
63
+ headers.Authorization = `Bearer ${compatibleBearer}`;
64
+ else if (!localCredential && config.authHeader)
65
+ headers.Authorization = config.authHeader;
66
+ return {
67
+ helloAuth,
68
+ headers,
69
+ ...(compatibleBearer ? { protocols: [`paseo.bearer.${compatibleBearer}`] } : {}),
70
+ };
71
+ }
72
+ function resolveConnectionAuth(config) {
73
+ const resolution = config.localCredential?.();
74
+ if (resolution instanceof Promise) {
75
+ return resolution.then((credential) => chooseConnectionAuth(config, credential));
76
+ }
77
+ return chooseConnectionAuth(config, resolution);
78
+ }
30
79
  function extractCorrelatedResponseIdentity(input) {
31
80
  if (!input || typeof input !== "object") {
32
81
  return null;
@@ -279,6 +328,7 @@ export class DaemonClient {
279
328
  this.connectResolve = null;
280
329
  this.connectReject = null;
281
330
  this.lastErrorValue = null;
331
+ this.authFailureReasonValue = null;
282
332
  this.connectionState = { status: "idle" };
283
333
  this.terminalStreams = new TerminalStreamRouter();
284
334
  this.pendingBinaryFileReads = new Map();
@@ -393,7 +443,7 @@ export class DaemonClient {
393
443
  });
394
444
  return this.connectPromise;
395
445
  }
396
- attemptConnect() {
446
+ async attemptConnect() {
397
447
  if (this.connectionState.status === "disposed") {
398
448
  this.rejectConnect(new Error("Daemon client is disposed"));
399
449
  return;
@@ -411,30 +461,30 @@ export class DaemonClient {
411
461
  clearTimeout(this.reconnectTimeout);
412
462
  this.reconnectTimeout = null;
413
463
  }
414
- const headers = {};
415
- const password = normalizePassword(this.config.password);
416
- if (password) {
417
- headers.Authorization = `Bearer ${password}`;
418
- }
419
- else if (this.config.authHeader) {
420
- headers.Authorization = this.config.authHeader;
421
- }
422
- const protocols = password ? [`paseo.bearer.${password}`] : undefined;
423
464
  try {
465
+ const resolution = resolveConnectionAuth(this.config);
466
+ const selected = resolution instanceof Promise ? await resolution : resolution;
467
+ if (!this.shouldReconnect)
468
+ return;
469
+ this.helloAuth = selected.helloAuth;
424
470
  // Reconnect can overlap with browser close/error delivery ordering.
425
471
  // Always dispose previous transport before constructing the next one.
426
472
  this.disposeTransport();
427
473
  const baseTransportFactory = this.config.transportFactory ??
428
474
  createWebSocketTransportFactory(this.config.webSocketFactory ?? defaultWebSocketFactory);
429
- const shouldUseRelayE2ee = this.config.e2ee?.enabled === true && isRelayClientWebSocketUrl(this.config.url);
430
- let transportFactory = baseTransportFactory;
475
+ const isRelayTransport = isRelayClientWebSocketUrl(this.config.url);
476
+ const shouldUseRelayE2ee = this.config.e2ee?.enabled === true && isRelayTransport;
477
+ this.assertEncryptedRelayAuth(selected.helloAuth, isRelayTransport);
478
+ let transportFactory = isRelayTransport
479
+ ? createRelayTransportFactory(baseTransportFactory)
480
+ : baseTransportFactory;
431
481
  if (shouldUseRelayE2ee) {
432
482
  const daemonPublicKeyB64 = this.config.e2ee?.daemonPublicKeyB64;
433
483
  if (!daemonPublicKeyB64) {
434
484
  throw new Error("daemonPublicKeyB64 is required for relay E2EE");
435
485
  }
436
486
  transportFactory = createRelayE2eeTransportFactory({
437
- baseFactory: baseTransportFactory,
487
+ baseFactory: transportFactory,
438
488
  daemonPublicKeyB64,
439
489
  logger: this.logger,
440
490
  });
@@ -442,11 +492,12 @@ export class DaemonClient {
442
492
  const transportUrl = this.resolveTransportUrlForAttempt();
443
493
  const transport = transportFactory({
444
494
  url: transportUrl,
445
- headers,
446
- ...(protocols ? { protocols } : {}),
495
+ headers: selected.headers,
496
+ ...(selected.protocols ? { protocols: selected.protocols } : {}),
447
497
  });
448
498
  this.transport = transport;
449
499
  this.lastServerInfoMessage = null;
500
+ this.authFailureReasonValue = null;
450
501
  this.updateConnectionState({
451
502
  status: "connecting",
452
503
  attempt: this.reconnectAttempt,
@@ -472,7 +523,7 @@ export class DaemonClient {
472
523
  this.pendingGenericTransportErrorTimeout = null;
473
524
  }
474
525
  this.lastErrorValue = null;
475
- this.sendHelloMessage();
526
+ void this.sendHelloMessage();
476
527
  }),
477
528
  transport.onClose((event) => {
478
529
  this.resetConnectTimeout();
@@ -480,7 +531,10 @@ export class DaemonClient {
480
531
  clearTimeout(this.pendingGenericTransportErrorTimeout);
481
532
  this.pendingGenericTransportErrorTimeout = null;
482
533
  }
483
- const reason = describeTransportClose(event);
534
+ this.authFailureReasonValue ?? (this.authFailureReasonValue = authFailureFromLegacyClose(event));
535
+ const reason = this.authFailureReasonValue
536
+ ? new DaemonAuthenticationError(this.authFailureReasonValue).message
537
+ : describeTransportClose(event);
484
538
  if (reason) {
485
539
  this.lastErrorValue = reason;
486
540
  }
@@ -544,6 +598,12 @@ export class DaemonClient {
544
598
  this.rejectConnect(error instanceof Error ? error : new Error(message));
545
599
  }
546
600
  }
601
+ assertEncryptedRelayAuth(helloAuth, isRelayTransport) {
602
+ if (!isRelayTransport || !helloAuth || this.config.e2ee?.enabled === true)
603
+ return;
604
+ this.setReconnectEnabled(false);
605
+ throw new Error("Relay credentials require E2EE");
606
+ }
547
607
  resolveConnect() {
548
608
  if (this.connectResolve) {
549
609
  this.connectResolve();
@@ -653,6 +713,9 @@ export class DaemonClient {
653
713
  get lastError() {
654
714
  return this.lastErrorValue;
655
715
  }
716
+ get authFailureReason() {
717
+ return this.authFailureReasonValue;
718
+ }
656
719
  getLastLivenessRttMs() {
657
720
  return this.lastLivenessRttMs;
658
721
  }
@@ -4174,8 +4237,9 @@ export class DaemonClient {
4174
4237
  resolveTransportUrlForAttempt() {
4175
4238
  return this.config.url;
4176
4239
  }
4177
- sendHelloMessage() {
4178
- if (!this.transport) {
4240
+ async sendHelloMessage() {
4241
+ const transport = this.transport;
4242
+ if (!transport) {
4179
4243
  this.scheduleReconnect({
4180
4244
  reason: "Transport unavailable before hello",
4181
4245
  event: "HELLO_TRANSPORT_MISSING",
@@ -4184,11 +4248,15 @@ export class DaemonClient {
4184
4248
  return;
4185
4249
  }
4186
4250
  try {
4251
+ if (this.transport !== transport)
4252
+ return;
4253
+ const auth = this.helloAuth;
4187
4254
  this.sendJsonMessage("hello", "hello", {
4188
4255
  type: "hello",
4189
4256
  clientId: this.config.clientId,
4190
4257
  clientType: this.config.clientType ?? "cli",
4191
4258
  protocolVersion: 1,
4259
+ ...(auth ? { auth } : {}),
4192
4260
  capabilities: {
4193
4261
  ...DEFAULT_CLIENT_CAPABILITIES,
4194
4262
  // COMPAT(sideConversations): added in 0.7.0-beta.2.fork.1, fork-only.
@@ -4322,6 +4390,17 @@ export class DaemonClient {
4322
4390
  this.runtimeMetrics?.recordMessage("pong", bytes, perfNow() - startMs);
4323
4391
  return;
4324
4392
  }
4393
+ if (parsed.data.type === "hello.rejected") {
4394
+ const reasonMessage = {
4395
+ password_required: "Password required",
4396
+ incorrect_password: "Incorrect password",
4397
+ incompatible_protocol: "Incompatible protocol version",
4398
+ };
4399
+ this.lastErrorValue = reasonMessage[parsed.data.reason];
4400
+ this.authFailureReasonValue =
4401
+ parsed.data.reason === "incompatible_protocol" ? null : parsed.data.reason;
4402
+ return;
4403
+ }
4325
4404
  this.traceInstant("paseo.ws.message.inbound", {
4326
4405
  envelopeType: "session",
4327
4406
  messageType: parsed.data.message.type,
@@ -4494,13 +4573,17 @@ export class DaemonClient {
4494
4573
  this.rejectPingProbe(new DaemonConnectionError(reason ?? "Connection lost"));
4495
4574
  this.terminalStreams.clearSlots();
4496
4575
  this.lastServerInfoMessage = null;
4576
+ if (this.authFailureReasonValue)
4577
+ this.setReconnectEnabled(false);
4497
4578
  if (wasDisposed) {
4498
4579
  this.rejectConnect(new Error(reason ?? "Daemon client is disposed"));
4499
4580
  return;
4500
4581
  }
4501
4582
  this.emitDisconnectedStateForReconnect(reason, input);
4502
4583
  if (!this.shouldReconnect || this.config.reconnect?.enabled === false) {
4503
- this.rejectConnect(new Error(reason ?? "Transport disconnected before connect"));
4584
+ this.rejectConnect(this.authFailureReasonValue
4585
+ ? new DaemonAuthenticationError(this.authFailureReasonValue)
4586
+ : new Error(reason ?? "Transport disconnected before connect"));
4504
4587
  return;
4505
4588
  }
4506
4589
  this.armReconnectTimer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camerontaylor/paseo-client",
3
- "version": "0.9.0-beta.2.fork.1",
3
+ "version": "0.10.1-fork.1",
4
4
  "description": "Paseo client SDK package",
5
5
  "files": [
6
6
  "dist",
@@ -39,8 +39,8 @@
39
39
  "test": "vitest run"
40
40
  },
41
41
  "dependencies": {
42
- "@camerontaylor/paseo-protocol": "0.9.0-beta.2.fork.1",
43
- "@camerontaylor/paseo-relay": "0.9.0-beta.2.fork.1",
42
+ "@camerontaylor/paseo-protocol": "0.10.1-fork.1",
43
+ "@camerontaylor/paseo-relay": "0.10.1-fork.1",
44
44
  "zod": "^4.4.3"
45
45
  },
46
46
  "devDependencies": {