@alia.onl/sdk 19.0.0 → 19.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @alia.onl/sdk
2
2
 
3
+ ## 19.1.0
4
+
5
+ An app's own agent may be confined to that app by Oxy (Alia ADR 0021, Oxy ADRs 0044 and 0045).
6
+
7
+ ### Added
8
+
9
+ - **The SDK says which Oxy app it is in.** Every request that can open an agent — the REST API
10
+ (`useAliaApi`: profiles, features, conversations, threads), the chat stream and the voice call —
11
+ carries the host's Oxy-signed app assertion as `X-Oxy-App-Assertion` (on the voice socket, as
12
+ `appAssertion` in the `auth` frame, because a socket cannot carry a header). It is read at send
13
+ time from the host's `OxyProvider` (`useAppAssertion()` of `@oxy.so/services`), and nothing is
14
+ sent when the provider has none. Alia verifies it and opens an agent Oxy confines to an app only
15
+ inside that app; every other agent is unaffected. Dictation and read-aloud reach no agent and
16
+ send none.
17
+
18
+ ### Changed
19
+
20
+ - **Peer `@oxy.so/services` >= 11.12.0**, the first release whose `OxyProvider` obtains the app
21
+ assertion.
22
+
3
23
  ## 19.0.0
4
24
 
5
25
  Every agent is the same agent, and any app embeds any of them (ADR 0020; the wire contract is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alia.onl/sdk",
3
- "version": "19.0.0",
3
+ "version": "19.1.0",
4
4
  "description": "Alia AI chat SDK — the conversation runtime, chat UI, voice calls and dictation for Oxy apps",
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
@@ -54,7 +54,7 @@
54
54
  "@expo/vector-icons": "^15.1.1",
55
55
  "@oxy.so/bloom": ">=10.9.0",
56
56
  "@oxy.so/core": ">=3.0.0",
57
- "@oxy.so/services": ">=8.0.0",
57
+ "@oxy.so/services": ">=11.12.0",
58
58
  "@tanstack/react-query": "^5.101",
59
59
  "@react-native-async-storage/async-storage": "^2.2.0",
60
60
  "expo-blur": "^57.0.0",
@@ -25,6 +25,7 @@ import { VoiceCall, type VoiceCallSnapshot, type VoiceSocketFactory } from '../l
25
25
  import { VoiceLevelChannel, type VoiceLevelSource } from '../lib/voice-levels';
26
26
  import type { ProductVoice, VoiceCloseReason } from '../lib/voice-protocol';
27
27
  import { ALIA_API_URL } from '../lib/origins';
28
+ import { useAppAssertionToken } from '../runtime/app-assertion';
28
29
 
29
30
  export interface UseVoiceRoomOptions {
30
31
  apiUrl?: string;
@@ -99,6 +100,7 @@ export function agentStateOf(state: VoiceCallState): AgentState {
99
100
 
100
101
  export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
101
102
  const { oxyServices } = useOxy();
103
+ const getAppAssertion = useAppAssertionToken();
102
104
  const levels = useMemo(() => new VoiceLevelChannel(), []);
103
105
  const [snapshot, setSnapshot] = useState<VoiceCallSnapshot>(IDLE);
104
106
  const callRef = useRef<VoiceCall | null>(null);
@@ -119,6 +121,7 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
119
121
  const call = new VoiceCall({
120
122
  apiUrl: current.apiUrl || ALIA_API_URL,
121
123
  getToken,
124
+ getAppAssertion,
122
125
  conversationId: current.conversationId ?? newConversationId(),
123
126
  ...(current.agentId === undefined ? {} : { agentId: current.agentId }),
124
127
  voice: current.voicePreference ?? 'female',
@@ -134,7 +137,7 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
134
137
  });
135
138
  callRef.current = call;
136
139
  await call.start();
137
- }, [getToken, levels]);
140
+ }, [getToken, getAppAssertion, levels]);
138
141
 
139
142
  /** Hang up and forget the call: the room is back to `disconnected`. */
140
143
  const disconnect = useCallback(() => {
@@ -77,6 +77,11 @@ export interface VoiceCallOptions {
77
77
  readonly apiUrl: string;
78
78
  /** The person's Oxy session token, read at connect and every few minutes. */
79
79
  readonly getToken: () => string | null;
80
+ /**
81
+ * The host's Oxy app assertion, sent with the first `auth` of each socket so
82
+ * an agent Oxy confines to an app opens only there (ADR 0021). Absent: none.
83
+ */
84
+ readonly getAppAssertion?: () => Promise<string | null>;
80
85
  readonly conversationId: string;
81
86
  readonly agentId?: string;
82
87
  readonly voice: ProductVoice;
@@ -251,7 +256,23 @@ export class VoiceCall {
251
256
  return;
252
257
  }
253
258
  this.lastToken = token;
254
- socket.send(JSON.stringify({ type: 'auth', token } satisfies VoiceClientEvent));
259
+ const getAppAssertion = this.options.getAppAssertion;
260
+ if (getAppAssertion === undefined) {
261
+ socket.send(JSON.stringify({ type: 'auth', token } satisfies VoiceClientEvent));
262
+ return;
263
+ }
264
+ void getAppAssertion()
265
+ .catch(() => null)
266
+ .then((appAssertion) => {
267
+ if (this.socket !== socket || this.finished || socket.readyState !== OPEN) return;
268
+ socket.send(
269
+ JSON.stringify({
270
+ type: 'auth',
271
+ token,
272
+ ...(appAssertion ? { appAssertion } : {}),
273
+ } satisfies VoiceClientEvent),
274
+ );
275
+ });
255
276
  };
256
277
  socket.onmessage = (event) => {
257
278
  if (this.socket !== socket || this.finished) return;
@@ -85,7 +85,8 @@ const CLOSE_REASONS: ReadonlySet<string> = new Set<VoiceCloseReason>([
85
85
  export type VoiceRole = 'user' | 'assistant';
86
86
 
87
87
  export type VoiceClientEvent =
88
- | { type: 'auth'; token: string }
88
+ /** `appAssertion`: the host's Oxy app assertion (ADR 0021); a socket cannot carry the header. */
89
+ | { type: 'auth'; token: string; appAssertion?: string }
89
90
  | {
90
91
  type: 'session.start';
91
92
  protocol: number;
@@ -1,5 +1,10 @@
1
1
  import { useOxy } from '@oxy.so/services/ui/client';
2
2
  import { useMemo } from 'react';
3
+ import {
4
+ type AppAssertionHeaders,
5
+ NO_APP_ASSERTION,
6
+ useAppAssertionHeaders,
7
+ } from './app-assertion';
3
8
  import { useAliaRuntime } from './config';
4
9
 
5
10
  /**
@@ -79,24 +84,33 @@ export interface LinkableOxy {
79
84
  };
80
85
  }
81
86
 
82
- /** Build the API over an Oxy client for `apiUrl`. Exposed for tests and for non-React callers. */
83
- export function createAliaApi(oxyServices: LinkableOxy, apiUrl: string): AliaApi {
87
+ /**
88
+ * Build the API over an Oxy client for `apiUrl`. Exposed for tests and for non-React callers.
89
+ *
90
+ * @param appAssertionHeaders the host's `X-Oxy-App-Assertion` (`app-assertion.ts`), on every request
91
+ */
92
+ export function createAliaApi(
93
+ oxyServices: LinkableOxy,
94
+ apiUrl: string,
95
+ appAssertionHeaders: AppAssertionHeaders = NO_APP_ASSERTION,
96
+ ): AliaApi {
84
97
  async function send<T>(
85
98
  method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
86
99
  path: string,
87
100
  request: AliaApiRequest = {},
88
101
  ): Promise<AliaApiResult<T>> {
102
+ const assertion = await appAssertionHeaders();
89
103
  const linked = oxyServices.createLinkedClient({ baseURL: apiUrl });
90
104
  try {
105
+ const headers = {
106
+ ...assertion,
107
+ ...(request.body === undefined ? {} : { 'Content-Type': 'application/json' }),
108
+ };
91
109
  const response = await linked.client.requestAuthenticatedResponse({
92
110
  method,
93
111
  url: withParams(path, request.params),
94
- ...(request.body === undefined
95
- ? {}
96
- : {
97
- body: JSON.stringify(request.body),
98
- headers: { 'Content-Type': 'application/json' },
99
- }),
112
+ ...(request.body === undefined ? {} : { body: JSON.stringify(request.body) }),
113
+ ...(Object.keys(headers).length === 0 ? {} : { headers }),
100
114
  ...(request.signal ? { signal: request.signal } : {}),
101
115
  });
102
116
  const data = await readBody(response);
@@ -119,5 +133,9 @@ export function createAliaApi(oxyServices: LinkableOxy, apiUrl: string): AliaApi
119
133
  export function useAliaApi(): AliaApi {
120
134
  const { oxyServices } = useOxy();
121
135
  const { apiUrl } = useAliaRuntime();
122
- return useMemo(() => createAliaApi(oxyServices as LinkableOxy, apiUrl), [oxyServices, apiUrl]);
136
+ const appAssertionHeaders = useAppAssertionHeaders();
137
+ return useMemo(
138
+ () => createAliaApi(oxyServices as LinkableOxy, apiUrl, appAssertionHeaders),
139
+ [oxyServices, apiUrl, appAssertionHeaders],
140
+ );
123
141
  }
@@ -0,0 +1,53 @@
1
+ // `@oxy.so/services/ui`, not the root barrel (which ships the sign-in panels, `check:entries`):
2
+ // `ui/client` does not export `useAppAssertion`.
3
+ import { useAppAssertion } from '@oxy.so/services/ui';
4
+ import { useMemo } from 'react';
5
+
6
+ /**
7
+ * Which Oxy app this conversation is in, for Alia (Alia ADR 0021, Oxy ADR 0045).
8
+ *
9
+ * An Oxy app may confine its own agent to itself — Clarity's assistant opens
10
+ * only inside Clarity — and the official apps share one session that names no
11
+ * app, so Alia reads the app from the Oxy-signed assertion the host's
12
+ * `OxyProvider` obtains and keeps current. Every request the SDK makes to Alia
13
+ * carries it as `X-Oxy-App-Assertion` when there is one; without one (an app
14
+ * Oxy cannot recognise yet, a signed-out person) nothing is sent, and only a
15
+ * confined agent is affected.
16
+ *
17
+ * The SDK never builds, stores or inspects the assertion: it is the host's
18
+ * provider's, read at send time so it is never stale.
19
+ */
20
+ export type AppAssertionHeaders = () => Promise<Record<string, string>>;
21
+
22
+ /** No app assertion: what a caller outside a React tree, or a test, passes. */
23
+ export const NO_APP_ASSERTION: AppAssertionHeaders = async () => ({});
24
+
25
+ /** The host's app assertion as request headers, `{}` when it has none or cannot get one. */
26
+ export function useAppAssertionHeaders(): AppAssertionHeaders {
27
+ const { getAppAssertionHeaders } = useAppAssertion();
28
+ return useMemo(
29
+ () => async () => {
30
+ try {
31
+ return await getAppAssertionHeaders();
32
+ } catch {
33
+ return {};
34
+ }
35
+ },
36
+ [getAppAssertionHeaders],
37
+ );
38
+ }
39
+
40
+ /** The host's app assertion itself, for the voice socket, which cannot carry a header. */
41
+ export function useAppAssertionToken(): () => Promise<string | null> {
42
+ const { getAppAssertion } = useAppAssertion();
43
+ return useMemo(
44
+ () => async () => {
45
+ try {
46
+ return await getAppAssertion();
47
+ } catch {
48
+ return null;
49
+ }
50
+ },
51
+ [getAppAssertion],
52
+ );
53
+ }
@@ -3,6 +3,7 @@ import { useOxy } from '@oxy.so/services/ui/client';
3
3
  import { useQueryClient } from '@tanstack/react-query';
4
4
  import { useCallback, useEffect, useRef, useState } from 'react';
5
5
  import { useLabels } from '../lib/labels';
6
+ import { useAppAssertionHeaders } from './app-assertion';
6
7
  import { type AliaArtifact, useAliaRuntime } from './config';
7
8
  import { errorCode, errorMessage as getErrorMessage, errorName, errorStatus } from './error-utils';
8
9
  import { agentMessageId, buildOutboundMessages, isAgentMessageOf } from './message-history';
@@ -201,6 +202,7 @@ export function useConversationStream({
201
202
  */
202
203
  const turnOptionsRef = useRef(new Map<string, Pick<SendOptions, 'mcpServerId' | 'skillNames'>>());
203
204
  const { oxyServices, user } = useOxy();
205
+ const appAssertionHeaders = useAppAssertionHeaders();
204
206
  const queryClient = useQueryClient();
205
207
  const abortControllerRef = useRef<AbortController | null>(null);
206
208
 
@@ -615,11 +617,14 @@ export function useConversationStream({
615
617
 
616
618
  // Authentication, the preflight refresh and the single 401 replay belong
617
619
  // to the linked Oxy client; this layer never reads or writes a bearer.
620
+ // Which Oxy app this turn is in (ADR 0021): an agent Oxy confines to an app opens only there.
621
+ const appAssertion = await appAssertionHeaders();
618
622
  linked = oxyServices.createLinkedClient({ baseURL: apiUrl });
619
623
  const response = await linked.client.requestAuthenticatedResponse({
620
624
  method: 'POST',
621
625
  url: API_ROUTES.chat,
622
626
  headers: {
627
+ ...appAssertion,
623
628
  Accept: 'text/event-stream',
624
629
  'Content-Type': 'application/json',
625
630
  ...(deviceInfo === undefined ? {} : { 'X-Device-Info': JSON.stringify(deviceInfo) }),
@@ -1347,6 +1352,7 @@ export function useConversationStream({
1347
1352
  host,
1348
1353
  labels,
1349
1354
  oxyServices,
1355
+ appAssertionHeaders,
1350
1356
  queryClient,
1351
1357
  conversationId,
1352
1358
  model,