@byokit/realtime 0.2.0 → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.1 (2026-09-30)
6
+
7
+
8
+
9
+ - FIX: Report saved-sign-in failure reasons and plain remedies in advisory auth checks while preserving existing status results.
10
+
5
11
  ## 0.2.0 (2026-09-30)
6
12
 
7
13
 
package/README.md CHANGED
@@ -85,28 +85,40 @@ The app's `peek` callback must also never refresh, prompt or start login.
85
85
 
86
86
  ```ts
87
87
  import { realtimeEngine, toolBridge } from '@byokit/realtime/node';
88
- import type { RealtimeAuth, RealtimeHostFrame, RealtimeTool } from '@byokit/realtime';
88
+ import type { RealtimeAuth, RealtimeHostFrame, RealtimeTool, RealtimeAuthPeekResult, RealtimeAuthResult } from '@byokit/realtime';
89
89
 
90
90
  declare const auth: Extract<RealtimeAuth, { kind: 'plan' }>;
91
91
  declare const tools: RealtimeTool[];
92
92
  declare const bridge: ReturnType<typeof toolBridge>;
93
93
  declare const emit: (frame: RealtimeHostFrame) => void;
94
- declare const peekSavedSignIn: (signal: AbortSignal) => Promise<boolean>;
94
+ declare const peekSavedSignIn: (signal: AbortSignal) => Promise<RealtimeAuthPeekResult>;
95
95
  declare const updateSettingsBadge: (status: 'ready' | 'signed-out' | 'unknown') => void;
96
+ declare const showSignInMessage: (result: RealtimeAuthResult) => void;
96
97
  const engine = realtimeEngine({
97
98
  engine: 'chatgpt', auth,
98
99
  tools, bridge, emit,
99
- authCheck: { peek: peekSavedSignIn, onStatus: status => updateSettingsBadge(status) },
100
+ authCheck: { peek: peekSavedSignIn, onStatus: status => updateSettingsBadge(status), onResult: showSignInMessage },
100
101
  });
101
102
  ```
102
103
 
103
104
  `ready` means the app reports a saved sign-in, not provider entitlement.
104
- `signed-out` means the lookup returned false. Errors or a timeout produce
105
- `unknown`; the default timeout is one second, capped at ten seconds. Close
105
+ `signed-out` means the lookup returned false or found an unusable saved sign-in.
106
+ Unrecognized errors or a timeout produce `unknown`; the default timeout is one second, capped at ten seconds. Close
106
107
  cancels pending checks and suppresses late notifications. Normal session
107
108
  authentication still needs the credential from `auth.access`.
108
109
  For a standalone settings lookup, `realtimeAuthCheck({ peek })` is also exported
109
- from the portable entry and returns `{ result, close }`. Do not await this check
110
+ from the portable entry and returns `{ result, details, close }`. `result` still
111
+ resolves to the status string; `details` resolves to `{ state, reason?, message? }`,
112
+ and engine `onResult` receives the same diagnostic object. A boolean or
113
+ `{ state }` peek keeps the previous behavior without adding a reason.
114
+
115
+ To report an expired saved sign-in, the read-only peek can return
116
+ `{ state: 'signed-out', reason: 'login-expired' }`. Built-in reasons are
117
+ `credential-permissions`, `login-expired`, `missing` and `unknown`; apps can
118
+ supply additional reason strings. Messages use fixed plain wording and never
119
+ copy exception text or credential paths. Missing-file errors map to `missing`,
120
+ permission errors to `credential-permissions`, and expired-login errors to
121
+ `login-expired`; unrecognized errors and timeouts report `unknown`. Do not await this check
110
122
  before opening a voice session or show a login prompt from its result.
111
123
 
112
124
  ## Lifecycle announcements and semantic app tools
package/dist/auth.d.ts CHANGED
@@ -1,7 +1,20 @@
1
1
  export type RealtimeAuthStatus = 'ready' | 'signed-out' | 'unknown';
2
+ /** Additional app-defined reasons are allowed without changing the state contract. */
3
+ export type RealtimeAuthReason = 'credential-permissions' | 'login-expired' | 'missing' | 'unknown' | (string & {});
4
+ export type RealtimeAuthResult = {
5
+ state: RealtimeAuthStatus;
6
+ reason?: RealtimeAuthReason;
7
+ message?: string;
8
+ };
9
+ export type RealtimeAuthPeekResult = boolean | {
10
+ state: RealtimeAuthStatus;
11
+ reason?: RealtimeAuthReason;
12
+ };
2
13
  export type RealtimeAuthCheckOptions = {
3
14
  /** Read-only saved-sign-in check. Must never start login, refresh access or prompt. */
4
- peek(signal: AbortSignal): Promise<boolean>;
15
+ peek(signal: AbortSignal): Promise<RealtimeAuthPeekResult>;
16
+ /** Diagnostic observer, also available through the standalone details promise. */
17
+ onResult?(result: RealtimeAuthResult): void;
5
18
  onStatus?(status: RealtimeAuthStatus): void;
6
19
  /** Defaults to 1 second; capped at 10 seconds. */
7
20
  timeoutMs?: number;
@@ -9,5 +22,6 @@ export type RealtimeAuthCheckOptions = {
9
22
  /** Advisory only: session startup must proceed independently of this check. */
10
23
  export declare function realtimeAuthCheck(options: RealtimeAuthCheckOptions): {
11
24
  result: Promise<RealtimeAuthStatus>;
25
+ details: Promise<RealtimeAuthResult>;
12
26
  close(): void;
13
27
  };
package/dist/auth.js CHANGED
@@ -1,29 +1,58 @@
1
+ function describe(state, reason) {
2
+ if (!reason)
3
+ return { state };
4
+ const message = reason === 'credential-permissions' ? 'Your saved sign-in must be readable only by you.'
5
+ : reason === 'login-expired' ? 'Your saved sign-in has expired. Sign in again in settings.'
6
+ : reason === 'missing' ? 'No saved sign-in was found. Sign in in settings.'
7
+ : 'Your saved sign-in could not be checked.';
8
+ return { state, reason, message };
9
+ }
10
+ function failure(error) {
11
+ const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
12
+ const reason = error && typeof error === 'object' && 'reason' in error ? error.reason : undefined;
13
+ if (reason === 'credential-permissions' || reason === 'login-expired' || reason === 'missing' || reason === 'unknown')
14
+ return describe(reason === 'unknown' ? 'unknown' : 'signed-out', reason);
15
+ if (code === 'ENOENT')
16
+ return describe('signed-out', 'missing');
17
+ if (code === 'EACCES' || code === 'EPERM' || code === 'credential-permissions' || (error instanceof Error && /owner.only|readable only by|permissions.*(?:credential|sign-in)|(?:credential|sign-in).*permissions/i.test(error.message)))
18
+ return describe('signed-out', 'credential-permissions');
19
+ if (code === 'login-expired' || code === 'TOKEN_EXPIRED' || (error instanceof Error && /\b(?:login|sign-in|token|credential)\b.*\bexpired\b/i.test(error.message)))
20
+ return describe('signed-out', 'login-expired');
21
+ return describe('unknown', 'unknown');
22
+ }
1
23
  /** Advisory only: session startup must proceed independently of this check. */
2
24
  export function realtimeAuthCheck(options) {
3
25
  const controller = new AbortController();
4
26
  let settled = false;
5
27
  let resolve;
6
28
  const result = new Promise(done => { resolve = done; });
7
- const finish = (status, notify = true) => {
29
+ let resolveDetails;
30
+ const details = new Promise(done => { resolveDetails = done; });
31
+ const finish = (value, notify = true) => {
8
32
  if (settled)
9
33
  return;
10
34
  settled = true;
11
35
  clearTimeout(timer);
12
36
  controller.abort();
13
- resolve(status);
37
+ resolve(value.state);
38
+ resolveDetails(value);
14
39
  // A settings observer cannot fail a call or create an unhandled rejection.
15
40
  if (notify) {
16
41
  try {
17
- options.onStatus?.(status);
42
+ options.onStatus?.(value.state);
43
+ }
44
+ catch { }
45
+ try {
46
+ options.onResult?.(value);
18
47
  }
19
48
  catch { }
20
49
  }
21
50
  };
22
51
  const timeout = options.timeoutMs ?? 1000;
23
- const timer = setTimeout(() => finish('unknown'), Number.isFinite(timeout) ? Math.min(10000, Math.max(1, timeout)) : 1000);
52
+ const timer = setTimeout(() => finish(describe('unknown', 'unknown')), Number.isFinite(timeout) ? Math.min(10000, Math.max(1, timeout)) : 1000);
24
53
  void Promise.resolve().then(() => {
25
54
  if (!settled)
26
55
  return options.peek(controller.signal);
27
- }).then(value => finish(value === true ? 'ready' : value === false ? 'signed-out' : 'unknown'), () => finish('unknown'));
28
- return { result, close() { finish('unknown', false); } };
56
+ }).then(value => finish(typeof value === 'object' && value !== null ? describe(value.state, value.reason) : describe(value === true ? 'ready' : value === false ? 'signed-out' : 'unknown')), error => finish(failure(error)));
57
+ return { result, details, close() { finish(describe('unknown', 'unknown'), false); } };
29
58
  }
package/dist/node.d.ts CHANGED
@@ -5,7 +5,7 @@ import { toolBridge } from './tools.ts';
5
5
  import type { RealtimeAuthCheckOptions } from './auth.ts';
6
6
  export { toolBridge, appBridge, delegationHandler } from './tools.ts';
7
7
  export { realtimeAuthCheck } from './auth.ts';
8
- export type { RealtimeAuthCheckOptions, RealtimeAuthStatus } from './auth.ts';
8
+ export type { RealtimeAuthCheckOptions, RealtimeAuthStatus, RealtimeAuthReason, RealtimeAuthResult, RealtimeAuthPeekResult } from './auth.ts';
9
9
  export type { ToolHandler, ToolBridgeOptions } from './tools.ts';
10
10
  export type { RealtimeSignalingIdentity } from './adapter.ts';
11
11
  export type RealtimeEngineOptions = RealtimeConfig & {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/realtime",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Realtime voice sessions with host-held credentials and injected audio.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",