@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 +20 -0
- package/package.json +2 -2
- package/src/hooks/useVoiceRoom.ts +4 -1
- package/src/lib/voice-call.ts +22 -1
- package/src/lib/voice-protocol.ts +2 -1
- package/src/runtime/api.ts +27 -9
- package/src/runtime/app-assertion.ts +53 -0
- package/src/runtime/use-conversation-stream.ts +6 -0
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.
|
|
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": ">=
|
|
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(() => {
|
package/src/lib/voice-call.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|
package/src/runtime/api.ts
CHANGED
|
@@ -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
|
-
/**
|
|
83
|
-
|
|
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
|
-
|
|
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,
|