@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.
@@ -44,19 +44,26 @@ var SinasSDK = (() => {
44
44
  __export(src_exports, {
45
45
  SinasAuthError: () => SinasAuthError,
46
46
  SinasClient: () => SinasClient,
47
+ SinasClientAuth: () => SinasClientAuth,
47
48
  SinasError: () => SinasError,
48
49
  SinasNotFoundError: () => SinasNotFoundError,
49
50
  SinasPermissionError: () => SinasPermissionError,
50
51
  SinasProvider: () => SinasProvider,
51
52
  SinasTimeoutError: () => SinasTimeoutError,
52
53
  approveToolCall: () => approveToolCall,
54
+ checkPermissions: () => checkPermissions,
53
55
  createChat: () => createChat,
54
56
  executeFunction: () => executeFunction,
55
57
  executeQuery: () => executeQuery,
56
58
  generateFileUrl: () => generateFileUrl,
57
59
  getChat: () => getChat,
60
+ getInfo: () => getInfo,
61
+ getMe: () => getMe,
58
62
  listCollections: () => listCollections,
59
63
  listFiles: () => listFiles,
64
+ login: () => login,
65
+ logout: () => logout,
66
+ refresh: () => refresh,
60
67
  sendMessage: () => sendMessage,
61
68
  stateDelete: () => stateDelete,
62
69
  stateGet: () => stateGet,
@@ -65,12 +72,14 @@ var SinasSDK = (() => {
65
72
  streamFromChannel: () => streamFromChannel,
66
73
  streamMessage: () => streamMessage,
67
74
  uploadFile: () => uploadFile,
75
+ useAuth: () => useAuth,
68
76
  useChat: () => useChat,
69
77
  useClient: () => useClient,
70
78
  useExecute: () => useExecute,
71
79
  useQuery: () => useQuery,
72
80
  useSinas: () => useSinas,
73
- useStateStore: () => useStateStore
81
+ useStateStore: () => useStateStore,
82
+ verifyOTP: () => verifyOTP
74
83
  });
75
84
 
76
85
  // src/client/errors.ts
@@ -107,16 +116,131 @@ var SinasSDK = (() => {
107
116
  }
108
117
  };
109
118
 
119
+ // src/client/auth.ts
120
+ function trimBase(baseUrl) {
121
+ return baseUrl.replace(/\/+$/, "");
122
+ }
123
+ async function authFetch(baseUrl, path, init = {}, accessToken) {
124
+ const headers = new Headers(init.headers);
125
+ if (!headers.has("Content-Type") && init.body !== void 0) {
126
+ headers.set("Content-Type", "application/json");
127
+ }
128
+ if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`);
129
+ const res = await fetch(`${trimBase(baseUrl)}${path}`, { ...init, headers });
130
+ if (res.ok || res.status === 204) return res;
131
+ let detail = res.statusText;
132
+ try {
133
+ const body = await res.clone().json();
134
+ detail = body.detail || body.message || detail;
135
+ } catch {
136
+ }
137
+ if (res.status === 401) throw new SinasAuthError(detail);
138
+ if (res.status === 403) throw new SinasPermissionError(detail);
139
+ if (res.status === 404) throw new SinasNotFoundError(detail);
140
+ throw new SinasError(detail, res.status, detail);
141
+ }
142
+ async function getInfo(baseUrl) {
143
+ const res = await authFetch(baseUrl, "/info");
144
+ return await res.json();
145
+ }
146
+ async function login(baseUrl, request) {
147
+ const res = await authFetch(baseUrl, "/auth/login", {
148
+ method: "POST",
149
+ body: JSON.stringify(request)
150
+ });
151
+ return await res.json();
152
+ }
153
+ async function verifyOTP(baseUrl, request) {
154
+ const res = await authFetch(baseUrl, "/auth/verify-otp", {
155
+ method: "POST",
156
+ body: JSON.stringify(request)
157
+ });
158
+ return await res.json();
159
+ }
160
+ async function refresh(baseUrl, refreshToken) {
161
+ const res = await authFetch(baseUrl, "/auth/refresh", {
162
+ method: "POST",
163
+ body: JSON.stringify({ refresh_token: refreshToken })
164
+ });
165
+ return await res.json();
166
+ }
167
+ async function logout(baseUrl, refreshToken) {
168
+ try {
169
+ await authFetch(baseUrl, "/auth/logout", {
170
+ method: "POST",
171
+ body: JSON.stringify({ refresh_token: refreshToken })
172
+ });
173
+ } catch (e) {
174
+ if (e instanceof SinasNotFoundError) return;
175
+ throw e;
176
+ }
177
+ }
178
+ async function getMe(baseUrl, accessToken) {
179
+ const res = await authFetch(baseUrl, "/auth/me", { method: "GET" }, accessToken);
180
+ return await res.json();
181
+ }
182
+ async function checkPermissions(baseUrl, accessToken, request) {
183
+ const res = await authFetch(
184
+ baseUrl,
185
+ "/auth/check-permissions",
186
+ {
187
+ method: "POST",
188
+ body: JSON.stringify({ logic: "AND", ...request })
189
+ },
190
+ accessToken
191
+ );
192
+ return await res.json();
193
+ }
194
+
110
195
  // src/client/SinasClient.ts
196
+ function hasWindow() {
197
+ return typeof window !== "undefined";
198
+ }
199
+ function trim(url) {
200
+ return url.replace(/\/+$/, "");
201
+ }
202
+ var SinasClientAuth = class {
203
+ constructor(client) {
204
+ this.client = client;
205
+ }
206
+ getInfo() {
207
+ return getInfo(this.client.requireBaseUrl());
208
+ }
209
+ login(request) {
210
+ return login(this.client.requireBaseUrl(), request);
211
+ }
212
+ verifyOTP(request) {
213
+ return verifyOTP(this.client.requireBaseUrl(), request);
214
+ }
215
+ refresh(refreshToken) {
216
+ return refresh(this.client.requireBaseUrl(), refreshToken);
217
+ }
218
+ logout(refreshToken) {
219
+ return logout(this.client.requireBaseUrl(), refreshToken);
220
+ }
221
+ getMe() {
222
+ const token = this.client.getAccessToken();
223
+ if (!token) throw new SinasAuthError("No access token available");
224
+ return getMe(this.client.requireBaseUrl(), token);
225
+ }
226
+ checkPermissions(request) {
227
+ const token = this.client.getAccessToken();
228
+ if (!token) throw new SinasAuthError("No access token available");
229
+ return checkPermissions(this.client.requireBaseUrl(), token, request);
230
+ }
231
+ };
111
232
  var SinasClient = class {
112
- constructor(config) {
113
- this.configOverride = config;
233
+ constructor(options = {}) {
234
+ this.refreshInFlight = null;
235
+ this.options = options;
236
+ this.auth = new SinasClientAuth(this);
114
237
  }
238
+ // ─── Config / base URL ───
115
239
  getConfig() {
116
- const config = this.configOverride || window.__SINAS_CONFIG__;
240
+ const config = this.options.config || (hasWindow() ? window.__SINAS_CONFIG__ : void 0);
117
241
  if (!config) {
118
242
  return {
119
- apiBase: "",
243
+ apiBase: this.options.baseUrl ? `${trim(this.options.baseUrl)}/api/v1` : "",
120
244
  component: { namespace: "", name: "", version: "" },
121
245
  resources: {
122
246
  enabledAgents: [],
@@ -133,12 +257,56 @@ var SinasSDK = (() => {
133
257
  getInput() {
134
258
  return this.getConfig().input || {};
135
259
  }
260
+ /** Workspace base URL (root of the Sinas instance). Throws if unset. */
261
+ requireBaseUrl() {
262
+ const url = this.getBaseUrl();
263
+ if (!url) {
264
+ throw new SinasError(
265
+ "No baseUrl configured. Construct with `new SinasClient({ baseUrl })` for standalone use.",
266
+ 0
267
+ );
268
+ }
269
+ return url;
270
+ }
271
+ getBaseUrl() {
272
+ if (this.options.baseUrl) return trim(this.options.baseUrl);
273
+ const apiBase = this.getConfig().apiBase;
274
+ if (!apiBase) return null;
275
+ return trim(apiBase).replace(/\/api\/v1$/, "");
276
+ }
277
+ // ─── Tokens ───
278
+ getAccessToken() {
279
+ if (this.options.getAccessToken) return this.options.getAccessToken();
280
+ if (hasWindow()) return window.__SINAS_AUTH_TOKEN__ ?? null;
281
+ return null;
282
+ }
283
+ getRefreshToken() {
284
+ return this.options.getRefreshToken ? this.options.getRefreshToken() : null;
285
+ }
286
+ async tryRefresh() {
287
+ const refreshToken = this.getRefreshToken();
288
+ if (!refreshToken) return null;
289
+ if (this.refreshInFlight) return this.refreshInFlight;
290
+ const baseUrl = this.getBaseUrl();
291
+ if (!baseUrl) return null;
292
+ this.refreshInFlight = (async () => {
293
+ try {
294
+ const resp = await refresh(baseUrl, refreshToken);
295
+ this.options.onTokenRefresh?.(resp.access_token);
296
+ return resp.access_token;
297
+ } catch {
298
+ return null;
299
+ } finally {
300
+ this.refreshInFlight = null;
301
+ }
302
+ })();
303
+ return this.refreshInFlight;
304
+ }
305
+ // ─── Headers / proxy ───
136
306
  getAuthHeaders() {
137
- const token = window.__SINAS_AUTH_TOKEN__;
307
+ const token = this.getAccessToken();
138
308
  const headers = { "Content-Type": "application/json" };
139
- if (token) {
140
- headers["Authorization"] = `Bearer ${token}`;
141
- }
309
+ if (token) headers["Authorization"] = `Bearer ${token}`;
142
310
  return headers;
143
311
  }
144
312
  getProxyBase() {
@@ -146,7 +314,8 @@ var SinasSDK = (() => {
146
314
  const comp = config.component;
147
315
  return `${config.apiBase}/components/${comp.namespace}/${comp.name}/proxy`;
148
316
  }
149
- async fetch(url, options = {}) {
317
+ // ─── Fetch with refresh-on-401 ───
318
+ async fetch(url, options = {}, _retried = false) {
150
319
  const res = await fetch(url, {
151
320
  ...options,
152
321
  headers: {
@@ -154,15 +323,18 @@ var SinasSDK = (() => {
154
323
  ...options.headers || {}
155
324
  }
156
325
  });
157
- if (!res.ok) {
158
- const body = await res.json().catch(() => ({ detail: res.statusText }));
159
- const message = body.detail || body.message || res.statusText;
160
- if (res.status === 401) throw new SinasAuthError(message);
161
- if (res.status === 403) throw new SinasPermissionError(message);
162
- if (res.status === 404) throw new SinasNotFoundError(message);
163
- throw new SinasError(message, res.status);
326
+ if (res.ok || res.status === 204) return res;
327
+ if (res.status === 401 && !_retried) {
328
+ const newToken = await this.tryRefresh();
329
+ if (newToken) return this.fetch(url, options, true);
330
+ this.options.onUnauthenticated?.();
164
331
  }
165
- return res;
332
+ const body = await res.json().catch(() => ({ detail: res.statusText }));
333
+ const message = body.detail || body.message || res.statusText;
334
+ if (res.status === 401) throw new SinasAuthError(message);
335
+ if (res.status === 403) throw new SinasPermissionError(message);
336
+ if (res.status === 404) throw new SinasNotFoundError(message);
337
+ throw new SinasError(message, res.status);
166
338
  }
167
339
  };
168
340
 
@@ -486,8 +658,14 @@ var SinasSDK = (() => {
486
658
  // src/hooks/context.ts
487
659
  var import_react = __toESM(require_react(), 1);
488
660
  var SinasContext = import_react.default.createContext(null);
489
- function SinasProvider({ client, children }) {
490
- const value = import_react.default.useMemo(() => client || new SinasClient(), [client]);
661
+ function SinasProvider({ client, options, children }) {
662
+ const value = import_react.default.useMemo(
663
+ () => client || new SinasClient(options),
664
+ // We intentionally only re-create when `client` reference changes; `options`
665
+ // is expected to be a stable object (callbacks reading from localStorage).
666
+ // eslint-disable-next-line react-hooks/exhaustive-deps
667
+ [client]
668
+ );
491
669
  return import_react.default.createElement(SinasContext.Provider, { value }, children);
492
670
  }
493
671
  function useClient() {
@@ -667,8 +845,6 @@ var SinasSDK = (() => {
667
845
  const abortRef = import_react5.default.useRef(null);
668
846
  const chatIdRef = import_react5.default.useRef(chatId);
669
847
  chatIdRef.current = chatId;
670
- const activeAssistantIdRef = import_react5.default.useRef("");
671
- const activeToolCallMsgIdRef = import_react5.default.useRef("");
672
848
  import_react5.default.useEffect(() => {
673
849
  if (!options?.chatId) return;
674
850
  let cancelled = false;
@@ -698,24 +874,34 @@ var SinasSDK = (() => {
698
874
  const handleStreamChunks = import_react5.default.useCallback(
699
875
  (chunk) => {
700
876
  if (chunk.event === "message") {
701
- try {
702
- const parsed = JSON.parse(chunk.data);
703
- if (parsed.content) {
704
- const targetId = activeAssistantIdRef.current;
705
- setMessages(
706
- (prev) => prev.map(
707
- (m) => m.id === targetId ? { ...m, content: (m.content || "") + parsed.content } : m
708
- )
709
- );
877
+ const newContent = (() => {
878
+ try {
879
+ const parsed = JSON.parse(chunk.data);
880
+ return parsed.content || "";
881
+ } catch {
882
+ return chunk.data;
710
883
  }
711
- } catch {
712
- const targetId = activeAssistantIdRef.current;
713
- setMessages(
714
- (prev) => prev.map(
715
- (m) => m.id === targetId ? { ...m, content: (m.content || "") + chunk.data } : m
716
- )
717
- );
718
- }
884
+ })();
885
+ if (!newContent) return;
886
+ setMessages((prev) => {
887
+ const last = prev[prev.length - 1];
888
+ if (last && last.role === "assistant" && !last.toolCalls && last.streaming) {
889
+ const next = [...prev];
890
+ next[next.length - 1] = { ...last, content: (last.content || "") + newContent };
891
+ return next;
892
+ }
893
+ return [...prev, {
894
+ id: `tmp-text-${Date.now()}`,
895
+ chatId: chatIdRef.current || "",
896
+ role: "assistant",
897
+ content: newContent,
898
+ toolCalls: null,
899
+ toolCallId: null,
900
+ name: null,
901
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
902
+ streaming: true
903
+ }];
904
+ });
719
905
  } else if (chunk.event === "tool_start") {
720
906
  try {
721
907
  const parsed = JSON.parse(chunk.data);
@@ -723,35 +909,29 @@ var SinasSDK = (() => {
723
909
  ...prev,
724
910
  { toolCallId: parsed.tool_call_id, name: parsed.name, description: parsed.description, status: "running" }
725
911
  ]);
726
- const placeholderId = activeAssistantIdRef.current;
727
- const toolCallMsg = {
728
- id: `tmp-tc-${parsed.tool_call_id}`,
729
- chatId: chatIdRef.current || "",
730
- role: "assistant",
731
- content: null,
732
- toolCalls: [{
733
- id: parsed.tool_call_id,
734
- type: "function",
735
- function: { name: parsed.name, arguments: parsed.arguments || "{}" },
736
- description: parsed.description
737
- }],
738
- toolCallId: null,
739
- name: null,
740
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
741
- streaming: true
742
- };
743
912
  setMessages((prev) => {
744
- const alreadyExists = prev.some(
745
- (m) => m.toolCalls?.some((tc) => tc.id === parsed.tool_call_id)
746
- );
747
- if (alreadyExists) return prev;
748
- const idx = prev.findIndex((m) => m.id === placeholderId);
749
- if (idx >= 0) {
750
- const next = [...prev];
751
- next.splice(idx, 0, toolCallMsg);
752
- return next;
913
+ if (prev.some((m) => m.toolCalls?.some((tc) => tc.id === parsed.tool_call_id))) {
914
+ return prev;
753
915
  }
754
- return [...prev, toolCallMsg];
916
+ const next = prev.map(
917
+ (m, i) => i === prev.length - 1 && m.streaming && m.role === "assistant" && !m.toolCalls ? { ...m, streaming: false } : m
918
+ );
919
+ return [...next, {
920
+ id: `tmp-tc-${parsed.tool_call_id}`,
921
+ chatId: chatIdRef.current || "",
922
+ role: "assistant",
923
+ content: null,
924
+ toolCalls: [{
925
+ id: parsed.tool_call_id,
926
+ type: "function",
927
+ function: { name: parsed.name, arguments: parsed.arguments || "{}" },
928
+ description: parsed.description
929
+ }],
930
+ toolCallId: null,
931
+ name: null,
932
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
933
+ streaming: true
934
+ }];
755
935
  });
756
936
  } catch {
757
937
  }
@@ -853,20 +1033,6 @@ var SinasSDK = (() => {
853
1033
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
854
1034
  };
855
1035
  setMessages((prev) => [...prev, userMsg]);
856
- const assistantId = `tmp-assistant-${Date.now()}`;
857
- activeAssistantIdRef.current = assistantId;
858
- const assistantMsg = {
859
- id: assistantId,
860
- chatId: activeChatId,
861
- role: "assistant",
862
- content: "",
863
- toolCalls: null,
864
- toolCallId: null,
865
- name: null,
866
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
867
- streaming: true
868
- };
869
- setMessages((prev) => [...prev, assistantMsg]);
870
1036
  const controller = streamMessage(
871
1037
  client,
872
1038
  activeChatId,
@@ -875,11 +1041,6 @@ var SinasSDK = (() => {
875
1041
  () => {
876
1042
  setStreaming(false);
877
1043
  setToolStatus([]);
878
- setMessages(
879
- (prev) => prev.filter(
880
- (m) => !(m.streaming && m.role === "assistant" && !m.content && !m.toolCalls)
881
- )
882
- );
883
1044
  refreshMessages(activeChatId);
884
1045
  },
885
1046
  (err) => {
@@ -901,22 +1062,6 @@ var SinasSDK = (() => {
901
1062
  setPendingApprovals((prev) => prev.filter((a) => a.toolCallId !== toolCallId));
902
1063
  setStreaming(true);
903
1064
  setError(null);
904
- const assistantId = `tmp-resume-${Date.now()}`;
905
- activeAssistantIdRef.current = assistantId;
906
- if (approved) {
907
- const assistantMsg = {
908
- id: assistantId,
909
- chatId: currentChatId,
910
- role: "assistant",
911
- content: "",
912
- toolCalls: null,
913
- toolCallId: null,
914
- name: null,
915
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
916
- streaming: true
917
- };
918
- setMessages((prev) => [...prev, assistantMsg]);
919
- }
920
1065
  (async () => {
921
1066
  try {
922
1067
  const result = await approveToolCall(client, currentChatId, toolCallId, approved);
@@ -995,6 +1140,81 @@ var SinasSDK = (() => {
995
1140
  useChat
996
1141
  };
997
1142
  }
1143
+
1144
+ // src/hooks/useAuth.ts
1145
+ var import_react6 = __toESM(require_react(), 1);
1146
+ function useAuth(options = {}) {
1147
+ const { autoRefresh = true } = options;
1148
+ const client = useClient();
1149
+ const [status, setStatus] = import_react6.default.useState("loading");
1150
+ const [user, setUser] = import_react6.default.useState(null);
1151
+ const [info, setInfo] = import_react6.default.useState(null);
1152
+ const [error, setError] = import_react6.default.useState(null);
1153
+ const refresh2 = import_react6.default.useCallback(async () => {
1154
+ setError(null);
1155
+ try {
1156
+ const [infoResp, meResp] = await Promise.all([
1157
+ client.auth.getInfo().catch(() => null),
1158
+ client.getAccessToken() ? client.auth.getMe().catch(() => null) : Promise.resolve(null)
1159
+ ]);
1160
+ if (infoResp) setInfo(infoResp);
1161
+ if (meResp) {
1162
+ setUser(meResp);
1163
+ setStatus("authenticated");
1164
+ } else {
1165
+ setUser(null);
1166
+ setStatus("unauthenticated");
1167
+ }
1168
+ } catch (e) {
1169
+ setError(e instanceof Error ? e.message : String(e));
1170
+ setStatus("unauthenticated");
1171
+ }
1172
+ }, [client]);
1173
+ import_react6.default.useEffect(() => {
1174
+ if (autoRefresh) void refresh2();
1175
+ }, [autoRefresh, refresh2]);
1176
+ const login2 = import_react6.default.useCallback(
1177
+ async (email, password) => {
1178
+ setError(null);
1179
+ try {
1180
+ return await client.auth.login({ email, password });
1181
+ } catch (e) {
1182
+ const msg = e instanceof Error ? e.message : String(e);
1183
+ setError(msg);
1184
+ throw e;
1185
+ }
1186
+ },
1187
+ [client]
1188
+ );
1189
+ const verifyOTP2 = import_react6.default.useCallback(
1190
+ async (sessionId, otpCode) => {
1191
+ setError(null);
1192
+ try {
1193
+ return await client.auth.verifyOTP({ session_id: sessionId, otp_code: otpCode });
1194
+ } catch (e) {
1195
+ const msg = e instanceof Error ? e.message : String(e);
1196
+ setError(msg);
1197
+ throw e;
1198
+ }
1199
+ },
1200
+ [client]
1201
+ );
1202
+ const logout2 = import_react6.default.useCallback(
1203
+ async (refreshToken) => {
1204
+ if (refreshToken) {
1205
+ try {
1206
+ await client.auth.logout(refreshToken);
1207
+ } catch (e) {
1208
+ if (!(e instanceof SinasAuthError)) throw e;
1209
+ }
1210
+ }
1211
+ setUser(null);
1212
+ setStatus("unauthenticated");
1213
+ },
1214
+ [client]
1215
+ );
1216
+ return { status, user, info, error, refresh: refresh2, login: login2, verifyOTP: verifyOTP2, logout: logout2 };
1217
+ }
998
1218
  return __toCommonJS(src_exports);
999
1219
  })();
1000
1220
  //# sourceMappingURL=sinas-sdk.umd.js.map