@sinas/sdk 0.2.3 → 0.5.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/dist/index.cjs CHANGED
@@ -32,19 +32,26 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  SinasAuthError: () => SinasAuthError,
34
34
  SinasClient: () => SinasClient,
35
+ SinasClientAuth: () => SinasClientAuth,
35
36
  SinasError: () => SinasError,
36
37
  SinasNotFoundError: () => SinasNotFoundError,
37
38
  SinasPermissionError: () => SinasPermissionError,
38
39
  SinasProvider: () => SinasProvider,
39
40
  SinasTimeoutError: () => SinasTimeoutError,
40
41
  approveToolCall: () => approveToolCall,
42
+ checkPermissions: () => checkPermissions,
41
43
  createChat: () => createChat,
42
44
  executeFunction: () => executeFunction,
43
45
  executeQuery: () => executeQuery,
44
46
  generateFileUrl: () => generateFileUrl,
45
47
  getChat: () => getChat,
48
+ getInfo: () => getInfo,
49
+ getMe: () => getMe,
46
50
  listCollections: () => listCollections,
47
51
  listFiles: () => listFiles,
52
+ login: () => login,
53
+ logout: () => logout,
54
+ refresh: () => refresh,
48
55
  sendMessage: () => sendMessage,
49
56
  stateDelete: () => stateDelete,
50
57
  stateGet: () => stateGet,
@@ -53,12 +60,14 @@ __export(src_exports, {
53
60
  streamFromChannel: () => streamFromChannel,
54
61
  streamMessage: () => streamMessage,
55
62
  uploadFile: () => uploadFile,
63
+ useAuth: () => useAuth,
56
64
  useChat: () => useChat,
57
65
  useClient: () => useClient,
58
66
  useExecute: () => useExecute,
59
67
  useQuery: () => useQuery,
60
68
  useSinas: () => useSinas,
61
- useStateStore: () => useStateStore
69
+ useStateStore: () => useStateStore,
70
+ verifyOTP: () => verifyOTP
62
71
  });
63
72
  module.exports = __toCommonJS(src_exports);
64
73
 
@@ -96,16 +105,131 @@ var SinasTimeoutError = class extends SinasError {
96
105
  }
97
106
  };
98
107
 
108
+ // src/client/auth.ts
109
+ function trimBase(baseUrl) {
110
+ return baseUrl.replace(/\/+$/, "");
111
+ }
112
+ async function authFetch(baseUrl, path, init = {}, accessToken) {
113
+ const headers = new Headers(init.headers);
114
+ if (!headers.has("Content-Type") && init.body !== void 0) {
115
+ headers.set("Content-Type", "application/json");
116
+ }
117
+ if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`);
118
+ const res = await fetch(`${trimBase(baseUrl)}${path}`, { ...init, headers });
119
+ if (res.ok || res.status === 204) return res;
120
+ let detail = res.statusText;
121
+ try {
122
+ const body = await res.clone().json();
123
+ detail = body.detail || body.message || detail;
124
+ } catch {
125
+ }
126
+ if (res.status === 401) throw new SinasAuthError(detail);
127
+ if (res.status === 403) throw new SinasPermissionError(detail);
128
+ if (res.status === 404) throw new SinasNotFoundError(detail);
129
+ throw new SinasError(detail, res.status, detail);
130
+ }
131
+ async function getInfo(baseUrl) {
132
+ const res = await authFetch(baseUrl, "/info");
133
+ return await res.json();
134
+ }
135
+ async function login(baseUrl, request) {
136
+ const res = await authFetch(baseUrl, "/auth/login", {
137
+ method: "POST",
138
+ body: JSON.stringify(request)
139
+ });
140
+ return await res.json();
141
+ }
142
+ async function verifyOTP(baseUrl, request) {
143
+ const res = await authFetch(baseUrl, "/auth/verify-otp", {
144
+ method: "POST",
145
+ body: JSON.stringify(request)
146
+ });
147
+ return await res.json();
148
+ }
149
+ async function refresh(baseUrl, refreshToken) {
150
+ const res = await authFetch(baseUrl, "/auth/refresh", {
151
+ method: "POST",
152
+ body: JSON.stringify({ refresh_token: refreshToken })
153
+ });
154
+ return await res.json();
155
+ }
156
+ async function logout(baseUrl, refreshToken) {
157
+ try {
158
+ await authFetch(baseUrl, "/auth/logout", {
159
+ method: "POST",
160
+ body: JSON.stringify({ refresh_token: refreshToken })
161
+ });
162
+ } catch (e) {
163
+ if (e instanceof SinasNotFoundError) return;
164
+ throw e;
165
+ }
166
+ }
167
+ async function getMe(baseUrl, accessToken) {
168
+ const res = await authFetch(baseUrl, "/auth/me", { method: "GET" }, accessToken);
169
+ return await res.json();
170
+ }
171
+ async function checkPermissions(baseUrl, accessToken, request) {
172
+ const res = await authFetch(
173
+ baseUrl,
174
+ "/auth/check-permissions",
175
+ {
176
+ method: "POST",
177
+ body: JSON.stringify({ logic: "AND", ...request })
178
+ },
179
+ accessToken
180
+ );
181
+ return await res.json();
182
+ }
183
+
99
184
  // src/client/SinasClient.ts
185
+ function hasWindow() {
186
+ return typeof window !== "undefined";
187
+ }
188
+ function trim(url) {
189
+ return url.replace(/\/+$/, "");
190
+ }
191
+ var SinasClientAuth = class {
192
+ constructor(client) {
193
+ this.client = client;
194
+ }
195
+ getInfo() {
196
+ return getInfo(this.client.requireBaseUrl());
197
+ }
198
+ login(request) {
199
+ return login(this.client.requireBaseUrl(), request);
200
+ }
201
+ verifyOTP(request) {
202
+ return verifyOTP(this.client.requireBaseUrl(), request);
203
+ }
204
+ refresh(refreshToken) {
205
+ return refresh(this.client.requireBaseUrl(), refreshToken);
206
+ }
207
+ logout(refreshToken) {
208
+ return logout(this.client.requireBaseUrl(), refreshToken);
209
+ }
210
+ getMe() {
211
+ const token = this.client.getAccessToken();
212
+ if (!token) throw new SinasAuthError("No access token available");
213
+ return getMe(this.client.requireBaseUrl(), token);
214
+ }
215
+ checkPermissions(request) {
216
+ const token = this.client.getAccessToken();
217
+ if (!token) throw new SinasAuthError("No access token available");
218
+ return checkPermissions(this.client.requireBaseUrl(), token, request);
219
+ }
220
+ };
100
221
  var SinasClient = class {
101
- constructor(config) {
102
- this.configOverride = config;
222
+ constructor(options = {}) {
223
+ this.refreshInFlight = null;
224
+ this.options = options;
225
+ this.auth = new SinasClientAuth(this);
103
226
  }
227
+ // ─── Config / base URL ───
104
228
  getConfig() {
105
- const config = this.configOverride || window.__SINAS_CONFIG__;
229
+ const config = this.options.config || (hasWindow() ? window.__SINAS_CONFIG__ : void 0);
106
230
  if (!config) {
107
231
  return {
108
- apiBase: "",
232
+ apiBase: this.options.baseUrl ? `${trim(this.options.baseUrl)}/api/v1` : "",
109
233
  component: { namespace: "", name: "", version: "" },
110
234
  resources: {
111
235
  enabledAgents: [],
@@ -122,12 +246,56 @@ var SinasClient = class {
122
246
  getInput() {
123
247
  return this.getConfig().input || {};
124
248
  }
249
+ /** Workspace base URL (root of the Sinas instance). Throws if unset. */
250
+ requireBaseUrl() {
251
+ const url = this.getBaseUrl();
252
+ if (!url) {
253
+ throw new SinasError(
254
+ "No baseUrl configured. Construct with `new SinasClient({ baseUrl })` for standalone use.",
255
+ 0
256
+ );
257
+ }
258
+ return url;
259
+ }
260
+ getBaseUrl() {
261
+ if (this.options.baseUrl) return trim(this.options.baseUrl);
262
+ const apiBase = this.getConfig().apiBase;
263
+ if (!apiBase) return null;
264
+ return trim(apiBase).replace(/\/api\/v1$/, "");
265
+ }
266
+ // ─── Tokens ───
267
+ getAccessToken() {
268
+ if (this.options.getAccessToken) return this.options.getAccessToken();
269
+ if (hasWindow()) return window.__SINAS_AUTH_TOKEN__ ?? null;
270
+ return null;
271
+ }
272
+ getRefreshToken() {
273
+ return this.options.getRefreshToken ? this.options.getRefreshToken() : null;
274
+ }
275
+ async tryRefresh() {
276
+ const refreshToken = this.getRefreshToken();
277
+ if (!refreshToken) return null;
278
+ if (this.refreshInFlight) return this.refreshInFlight;
279
+ const baseUrl = this.getBaseUrl();
280
+ if (!baseUrl) return null;
281
+ this.refreshInFlight = (async () => {
282
+ try {
283
+ const resp = await refresh(baseUrl, refreshToken);
284
+ this.options.onTokenRefresh?.(resp.access_token);
285
+ return resp.access_token;
286
+ } catch {
287
+ return null;
288
+ } finally {
289
+ this.refreshInFlight = null;
290
+ }
291
+ })();
292
+ return this.refreshInFlight;
293
+ }
294
+ // ─── Headers / proxy ───
125
295
  getAuthHeaders() {
126
- const token = window.__SINAS_AUTH_TOKEN__;
296
+ const token = this.getAccessToken();
127
297
  const headers = { "Content-Type": "application/json" };
128
- if (token) {
129
- headers["Authorization"] = `Bearer ${token}`;
130
- }
298
+ if (token) headers["Authorization"] = `Bearer ${token}`;
131
299
  return headers;
132
300
  }
133
301
  getProxyBase() {
@@ -135,7 +303,8 @@ var SinasClient = class {
135
303
  const comp = config.component;
136
304
  return `${config.apiBase}/components/${comp.namespace}/${comp.name}/proxy`;
137
305
  }
138
- async fetch(url, options = {}) {
306
+ // ─── Fetch with refresh-on-401 ───
307
+ async fetch(url, options = {}, _retried = false) {
139
308
  const res = await fetch(url, {
140
309
  ...options,
141
310
  headers: {
@@ -143,15 +312,18 @@ var SinasClient = class {
143
312
  ...options.headers || {}
144
313
  }
145
314
  });
146
- if (!res.ok) {
147
- const body = await res.json().catch(() => ({ detail: res.statusText }));
148
- const message = body.detail || body.message || res.statusText;
149
- if (res.status === 401) throw new SinasAuthError(message);
150
- if (res.status === 403) throw new SinasPermissionError(message);
151
- if (res.status === 404) throw new SinasNotFoundError(message);
152
- throw new SinasError(message, res.status);
315
+ if (res.ok || res.status === 204) return res;
316
+ if (res.status === 401 && !_retried) {
317
+ const newToken = await this.tryRefresh();
318
+ if (newToken) return this.fetch(url, options, true);
319
+ this.options.onUnauthenticated?.();
153
320
  }
154
- return res;
321
+ const body = await res.json().catch(() => ({ detail: res.statusText }));
322
+ const message = body.detail || body.message || res.statusText;
323
+ if (res.status === 401) throw new SinasAuthError(message);
324
+ if (res.status === 403) throw new SinasPermissionError(message);
325
+ if (res.status === 404) throw new SinasNotFoundError(message);
326
+ throw new SinasError(message, res.status);
155
327
  }
156
328
  };
157
329
 
@@ -475,8 +647,14 @@ var import_react2 = __toESM(require("react"), 1);
475
647
  // src/hooks/context.ts
476
648
  var import_react = __toESM(require("react"), 1);
477
649
  var SinasContext = import_react.default.createContext(null);
478
- function SinasProvider({ client, children }) {
479
- const value = import_react.default.useMemo(() => client || new SinasClient(), [client]);
650
+ function SinasProvider({ client, options, children }) {
651
+ const value = import_react.default.useMemo(
652
+ () => client || new SinasClient(options),
653
+ // We intentionally only re-create when `client` reference changes; `options`
654
+ // is expected to be a stable object (callbacks reading from localStorage).
655
+ // eslint-disable-next-line react-hooks/exhaustive-deps
656
+ [client]
657
+ );
480
658
  return import_react.default.createElement(SinasContext.Provider, { value }, children);
481
659
  }
482
660
  function useClient() {
@@ -656,8 +834,6 @@ function useChat(agentRef, options) {
656
834
  const abortRef = import_react5.default.useRef(null);
657
835
  const chatIdRef = import_react5.default.useRef(chatId);
658
836
  chatIdRef.current = chatId;
659
- const activeAssistantIdRef = import_react5.default.useRef("");
660
- const activeToolCallMsgIdRef = import_react5.default.useRef("");
661
837
  import_react5.default.useEffect(() => {
662
838
  if (!options?.chatId) return;
663
839
  let cancelled = false;
@@ -687,24 +863,34 @@ function useChat(agentRef, options) {
687
863
  const handleStreamChunks = import_react5.default.useCallback(
688
864
  (chunk) => {
689
865
  if (chunk.event === "message") {
690
- try {
691
- const parsed = JSON.parse(chunk.data);
692
- if (parsed.content) {
693
- const targetId = activeAssistantIdRef.current;
694
- setMessages(
695
- (prev) => prev.map(
696
- (m) => m.id === targetId ? { ...m, content: (m.content || "") + parsed.content } : m
697
- )
698
- );
866
+ const newContent = (() => {
867
+ try {
868
+ const parsed = JSON.parse(chunk.data);
869
+ return parsed.content || "";
870
+ } catch {
871
+ return chunk.data;
699
872
  }
700
- } catch {
701
- const targetId = activeAssistantIdRef.current;
702
- setMessages(
703
- (prev) => prev.map(
704
- (m) => m.id === targetId ? { ...m, content: (m.content || "") + chunk.data } : m
705
- )
706
- );
707
- }
873
+ })();
874
+ if (!newContent) return;
875
+ setMessages((prev) => {
876
+ const last = prev[prev.length - 1];
877
+ if (last && last.role === "assistant" && !last.toolCalls && last.streaming) {
878
+ const next = [...prev];
879
+ next[next.length - 1] = { ...last, content: (last.content || "") + newContent };
880
+ return next;
881
+ }
882
+ return [...prev, {
883
+ id: `tmp-text-${Date.now()}`,
884
+ chatId: chatIdRef.current || "",
885
+ role: "assistant",
886
+ content: newContent,
887
+ toolCalls: null,
888
+ toolCallId: null,
889
+ name: null,
890
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
891
+ streaming: true
892
+ }];
893
+ });
708
894
  } else if (chunk.event === "tool_start") {
709
895
  try {
710
896
  const parsed = JSON.parse(chunk.data);
@@ -712,35 +898,29 @@ function useChat(agentRef, options) {
712
898
  ...prev,
713
899
  { toolCallId: parsed.tool_call_id, name: parsed.name, description: parsed.description, status: "running" }
714
900
  ]);
715
- const placeholderId = activeAssistantIdRef.current;
716
- const toolCallMsg = {
717
- id: `tmp-tc-${parsed.tool_call_id}`,
718
- chatId: chatIdRef.current || "",
719
- role: "assistant",
720
- content: null,
721
- toolCalls: [{
722
- id: parsed.tool_call_id,
723
- type: "function",
724
- function: { name: parsed.name, arguments: parsed.arguments || "{}" },
725
- description: parsed.description
726
- }],
727
- toolCallId: null,
728
- name: null,
729
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
730
- streaming: true
731
- };
732
901
  setMessages((prev) => {
733
- const alreadyExists = prev.some(
734
- (m) => m.toolCalls?.some((tc) => tc.id === parsed.tool_call_id)
735
- );
736
- if (alreadyExists) return prev;
737
- const idx = prev.findIndex((m) => m.id === placeholderId);
738
- if (idx >= 0) {
739
- const next = [...prev];
740
- next.splice(idx, 0, toolCallMsg);
741
- return next;
902
+ if (prev.some((m) => m.toolCalls?.some((tc) => tc.id === parsed.tool_call_id))) {
903
+ return prev;
742
904
  }
743
- return [...prev, toolCallMsg];
905
+ const next = prev.map(
906
+ (m, i) => i === prev.length - 1 && m.streaming && m.role === "assistant" && !m.toolCalls ? { ...m, streaming: false } : m
907
+ );
908
+ return [...next, {
909
+ id: `tmp-tc-${parsed.tool_call_id}`,
910
+ chatId: chatIdRef.current || "",
911
+ role: "assistant",
912
+ content: null,
913
+ toolCalls: [{
914
+ id: parsed.tool_call_id,
915
+ type: "function",
916
+ function: { name: parsed.name, arguments: parsed.arguments || "{}" },
917
+ description: parsed.description
918
+ }],
919
+ toolCallId: null,
920
+ name: null,
921
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
922
+ streaming: true
923
+ }];
744
924
  });
745
925
  } catch {
746
926
  }
@@ -842,20 +1022,6 @@ function useChat(agentRef, options) {
842
1022
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
843
1023
  };
844
1024
  setMessages((prev) => [...prev, userMsg]);
845
- const assistantId = `tmp-assistant-${Date.now()}`;
846
- activeAssistantIdRef.current = assistantId;
847
- const assistantMsg = {
848
- id: assistantId,
849
- chatId: activeChatId,
850
- role: "assistant",
851
- content: "",
852
- toolCalls: null,
853
- toolCallId: null,
854
- name: null,
855
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
856
- streaming: true
857
- };
858
- setMessages((prev) => [...prev, assistantMsg]);
859
1025
  const controller = streamMessage(
860
1026
  client,
861
1027
  activeChatId,
@@ -864,11 +1030,6 @@ function useChat(agentRef, options) {
864
1030
  () => {
865
1031
  setStreaming(false);
866
1032
  setToolStatus([]);
867
- setMessages(
868
- (prev) => prev.filter(
869
- (m) => !(m.streaming && m.role === "assistant" && !m.content && !m.toolCalls)
870
- )
871
- );
872
1033
  refreshMessages(activeChatId);
873
1034
  },
874
1035
  (err) => {
@@ -890,22 +1051,6 @@ function useChat(agentRef, options) {
890
1051
  setPendingApprovals((prev) => prev.filter((a) => a.toolCallId !== toolCallId));
891
1052
  setStreaming(true);
892
1053
  setError(null);
893
- const assistantId = `tmp-resume-${Date.now()}`;
894
- activeAssistantIdRef.current = assistantId;
895
- if (approved) {
896
- const assistantMsg = {
897
- id: assistantId,
898
- chatId: currentChatId,
899
- role: "assistant",
900
- content: "",
901
- toolCalls: null,
902
- toolCallId: null,
903
- name: null,
904
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
905
- streaming: true
906
- };
907
- setMessages((prev) => [...prev, assistantMsg]);
908
- }
909
1054
  (async () => {
910
1055
  try {
911
1056
  const result = await approveToolCall(client, currentChatId, toolCallId, approved);
@@ -984,23 +1129,105 @@ function useSinas() {
984
1129
  useChat
985
1130
  };
986
1131
  }
1132
+
1133
+ // src/hooks/useAuth.ts
1134
+ var import_react6 = __toESM(require("react"), 1);
1135
+ function useAuth(options = {}) {
1136
+ const { autoRefresh = true } = options;
1137
+ const client = useClient();
1138
+ const [status, setStatus] = import_react6.default.useState("loading");
1139
+ const [user, setUser] = import_react6.default.useState(null);
1140
+ const [info, setInfo] = import_react6.default.useState(null);
1141
+ const [error, setError] = import_react6.default.useState(null);
1142
+ const refresh2 = import_react6.default.useCallback(async () => {
1143
+ setError(null);
1144
+ try {
1145
+ const [infoResp, meResp] = await Promise.all([
1146
+ client.auth.getInfo().catch(() => null),
1147
+ client.getAccessToken() ? client.auth.getMe().catch(() => null) : Promise.resolve(null)
1148
+ ]);
1149
+ if (infoResp) setInfo(infoResp);
1150
+ if (meResp) {
1151
+ setUser(meResp);
1152
+ setStatus("authenticated");
1153
+ } else {
1154
+ setUser(null);
1155
+ setStatus("unauthenticated");
1156
+ }
1157
+ } catch (e) {
1158
+ setError(e instanceof Error ? e.message : String(e));
1159
+ setStatus("unauthenticated");
1160
+ }
1161
+ }, [client]);
1162
+ import_react6.default.useEffect(() => {
1163
+ if (autoRefresh) void refresh2();
1164
+ }, [autoRefresh, refresh2]);
1165
+ const login2 = import_react6.default.useCallback(
1166
+ async (email, password) => {
1167
+ setError(null);
1168
+ try {
1169
+ return await client.auth.login({ email, password });
1170
+ } catch (e) {
1171
+ const msg = e instanceof Error ? e.message : String(e);
1172
+ setError(msg);
1173
+ throw e;
1174
+ }
1175
+ },
1176
+ [client]
1177
+ );
1178
+ const verifyOTP2 = import_react6.default.useCallback(
1179
+ async (sessionId, otpCode) => {
1180
+ setError(null);
1181
+ try {
1182
+ return await client.auth.verifyOTP({ session_id: sessionId, otp_code: otpCode });
1183
+ } catch (e) {
1184
+ const msg = e instanceof Error ? e.message : String(e);
1185
+ setError(msg);
1186
+ throw e;
1187
+ }
1188
+ },
1189
+ [client]
1190
+ );
1191
+ const logout2 = import_react6.default.useCallback(
1192
+ async (refreshToken) => {
1193
+ if (refreshToken) {
1194
+ try {
1195
+ await client.auth.logout(refreshToken);
1196
+ } catch (e) {
1197
+ if (!(e instanceof SinasAuthError)) throw e;
1198
+ }
1199
+ }
1200
+ setUser(null);
1201
+ setStatus("unauthenticated");
1202
+ },
1203
+ [client]
1204
+ );
1205
+ return { status, user, info, error, refresh: refresh2, login: login2, verifyOTP: verifyOTP2, logout: logout2 };
1206
+ }
987
1207
  // Annotate the CommonJS export names for ESM import in node:
988
1208
  0 && (module.exports = {
989
1209
  SinasAuthError,
990
1210
  SinasClient,
1211
+ SinasClientAuth,
991
1212
  SinasError,
992
1213
  SinasNotFoundError,
993
1214
  SinasPermissionError,
994
1215
  SinasProvider,
995
1216
  SinasTimeoutError,
996
1217
  approveToolCall,
1218
+ checkPermissions,
997
1219
  createChat,
998
1220
  executeFunction,
999
1221
  executeQuery,
1000
1222
  generateFileUrl,
1001
1223
  getChat,
1224
+ getInfo,
1225
+ getMe,
1002
1226
  listCollections,
1003
1227
  listFiles,
1228
+ login,
1229
+ logout,
1230
+ refresh,
1004
1231
  sendMessage,
1005
1232
  stateDelete,
1006
1233
  stateGet,
@@ -1009,11 +1236,13 @@ function useSinas() {
1009
1236
  streamFromChannel,
1010
1237
  streamMessage,
1011
1238
  uploadFile,
1239
+ useAuth,
1012
1240
  useChat,
1013
1241
  useClient,
1014
1242
  useExecute,
1015
1243
  useQuery,
1016
1244
  useSinas,
1017
- useStateStore
1245
+ useStateStore,
1246
+ verifyOTP
1018
1247
  });
1019
1248
  //# sourceMappingURL=index.cjs.map