assistant-cloud 0.1.37 → 0.1.38

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.
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantCloudAuthStrategy.d.ts","names":[],"sources":["../src/AssistantCloudAuthStrategy.ts"],"mappings":";KAAY;WACD;EACT,kBAAkB,QAAQ;EAC1B,gBAAgB,SAAS;;cAiCd,yCAAyC;;WACpC;UAER;UACA;UACA;EAGI,YAAA,yBAAyB;EAIxB,kBAAkB,QAAQ;UA0BzB;EAUP,gBAAgB,SAAS;;cAcrB,4CAA4C;;WACvC;EAMJ,YAAA,gBAAgB,gBAAgB;EAM/B,kBAAkB,QAAQ;EAQhC;;cAkDI,+CAA+C;WAC1C;UAER;UACA;EAEI,YAAA;EAgDC,kBAAkB,QAAQ;EAIhC,gBAAgB,SAAS"}
1
+ {"version":3,"file":"AssistantCloudAuthStrategy.d.ts","names":[],"sources":["../src/AssistantCloudAuthStrategy.ts"],"mappings":";KAMY;WACD;EACT,kBAAkB,QAAQ;EAC1B,gBAAgB,SAAS;;cAmFd,yCAAyC;;WACpC;UAER;UACA;UACA;EAGI,YAAA,yBAAyB;EAIxB,kBAAkB,QAAQ;UA0BzB;EAUP,gBAAgB,SAAS;;cAcrB,4CAA4C;;WACvC;EAMJ,YAAA,gBAAgB,gBAAgB;EAM/B,kBAAkB,QAAQ;EAQhC;;cA6CI,+CAA+C;WAC1C;UAER;UACA;EAEI,YAAA;EA4DC,kBAAkB,QAAQ;EAIhC,gBAAgB,SAAS"}
@@ -1,3 +1,4 @@
1
+ import { CloudResponseError, readCloudRecord, readCloudString } from "./cloudResponse.js";
1
2
  //#region src/AssistantCloudAuthStrategy.ts
2
3
  const getJwtExpiry = (jwt) => {
3
4
  try {
@@ -13,6 +14,31 @@ const getJwtExpiry = (jwt) => {
13
14
  throw new Error(`Unable to determine the token expiry: ${error}`);
14
15
  }
15
16
  };
17
+ const readNonEmptyCloudString = (value, field) => {
18
+ const result = readCloudString(value, field);
19
+ if (result.length === 0) throw new CloudResponseError(`Invalid Assistant Cloud response for "${field}": expected a non-empty string`);
20
+ return result;
21
+ };
22
+ const readRefreshTokenResponse = (value, field) => {
23
+ const refreshToken = readCloudRecord(value, field);
24
+ return {
25
+ token: readNonEmptyCloudString(refreshToken.token, `${field}.token`),
26
+ expires_at: readNonEmptyCloudString(refreshToken.expires_at, `${field}.expires_at`)
27
+ };
28
+ };
29
+ const readAuthTokenResponse = async (response, field) => {
30
+ let value;
31
+ try {
32
+ value = await response.json();
33
+ } catch {
34
+ throw new CloudResponseError(`Invalid Assistant Cloud response for "${field}": expected valid JSON`);
35
+ }
36
+ const data = readCloudRecord(value, field);
37
+ return {
38
+ data,
39
+ accessToken: readNonEmptyCloudString(data.access_token, `${field}.access_token`)
40
+ };
41
+ };
16
42
  var AssistantCloudJWTAuthStrategy = class {
17
43
  strategy = "jwt";
18
44
  cachedToken = null;
@@ -117,17 +143,16 @@ var AssistantCloudAnonymousAuthStrategy = class {
117
143
  body: JSON.stringify({ refresh_token: storedRefreshToken.token })
118
144
  });
119
145
  if (response.ok) {
120
- const { access_token, refresh_token } = await response.json();
121
- if (refresh_token) writeRefreshToken(refresh_token);
122
- return access_token;
146
+ const { data, accessToken } = await readAuthTokenResponse(response, "refresh auth token response");
147
+ if (data.refresh_token != null) writeRefreshToken(readRefreshTokenResponse(data.refresh_token, "refresh auth token response.refresh_token"));
148
+ return accessToken;
123
149
  }
124
150
  } else removeRefreshToken();
125
151
  const response = await fetch(`${this.baseUrl}/v1/auth/tokens/anonymous`, { method: "POST" });
126
152
  if (!response.ok) return null;
127
- const { access_token, refresh_token } = await response.json();
128
- if (!access_token || !refresh_token) return null;
129
- writeRefreshToken(refresh_token);
130
- return access_token;
153
+ const { data, accessToken } = await readAuthTokenResponse(response, "anonymous auth token response");
154
+ writeRefreshToken(readRefreshTokenResponse(data.refresh_token, "anonymous auth token response.refresh_token"));
155
+ return accessToken;
131
156
  });
132
157
  }
133
158
  async getAuthHeaders() {
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantCloudAuthStrategy.js","names":["#authTokenCallback","#apiKey","#userId","#workspaceId"],"sources":["../src/AssistantCloudAuthStrategy.ts"],"sourcesContent":["export type AssistantCloudAuthStrategy = {\n readonly strategy: \"anon\" | \"jwt\" | \"api-key\";\n getAuthHeaders(): Promise<Record<string, string> | false>;\n readAuthHeaders(headers: Headers): void;\n};\n\nconst getJwtExpiry = (jwt: string): number => {\n try {\n const parts = jwt.split(\".\");\n const bodyPart = parts[1];\n if (!bodyPart) {\n throw new Error(\"Invalid JWT format\");\n }\n\n // Convert from Base64Url to Base64 and add padding if necessary\n let base64 = bodyPart.replace(/-/g, \"+\").replace(/_/g, \"/\");\n while (base64.length % 4 !== 0) {\n base64 += \"=\";\n }\n\n // Decode the Base64 string and parse the payload\n const payload = atob(base64);\n const payloadObj = JSON.parse(payload);\n const exp = payloadObj.exp;\n\n if (!exp || typeof exp !== \"number\") {\n throw new Error('JWT does not contain a valid \"exp\" field');\n }\n\n // Convert expiration time to milliseconds\n return exp * 1000;\n } catch (error) {\n throw new Error(`Unable to determine the token expiry: ${error}`);\n }\n};\n\nexport class AssistantCloudJWTAuthStrategy implements AssistantCloudAuthStrategy {\n public readonly strategy = \"jwt\";\n\n private cachedToken: string | null = null;\n private tokenExpiry: number | null = null;\n private tokenRequest: Promise<Record<string, string> | false> | null = null;\n #authTokenCallback: () => Promise<string | null>;\n\n constructor(authTokenCallback: () => Promise<string | null>) {\n this.#authTokenCallback = authTokenCallback;\n }\n\n public async getAuthHeaders(): Promise<Record<string, string> | false> {\n const currentTime = Date.now();\n\n // Use cached token if it's valid for at least 30 more seconds\n if (\n this.cachedToken &&\n this.tokenExpiry &&\n this.tokenExpiry - currentTime > 30 * 1000\n ) {\n return { Authorization: `Bearer ${this.cachedToken}` };\n }\n\n if (!this.tokenRequest) {\n this.tokenRequest = this.fetchAuthHeaders();\n }\n\n const tokenRequest = this.tokenRequest;\n try {\n return await tokenRequest;\n } finally {\n if (this.tokenRequest === tokenRequest) {\n this.tokenRequest = null;\n }\n }\n }\n\n private async fetchAuthHeaders(): Promise<Record<string, string> | false> {\n const token = await this.#authTokenCallback();\n if (!token) return false;\n\n this.cachedToken = token;\n this.tokenExpiry = getJwtExpiry(token);\n\n return { Authorization: `Bearer ${token}` };\n }\n\n public readAuthHeaders(headers: Headers) {\n const authHeader = headers.get(\"Authorization\");\n if (!authHeader) return;\n\n const [scheme, token] = authHeader.split(\" \");\n if (scheme !== \"Bearer\" || !token) {\n throw new Error(\"Invalid auth header received\");\n }\n\n this.cachedToken = token;\n this.tokenExpiry = getJwtExpiry(token);\n }\n}\n\nexport class AssistantCloudAPIKeyAuthStrategy implements AssistantCloudAuthStrategy {\n public readonly strategy = \"api-key\";\n\n #apiKey: string;\n #userId: string;\n #workspaceId: string;\n\n constructor(apiKey: string, userId: string, workspaceId: string) {\n this.#apiKey = apiKey;\n this.#userId = userId;\n this.#workspaceId = workspaceId;\n }\n\n public async getAuthHeaders(): Promise<Record<string, string>> {\n return {\n Authorization: `Bearer ${this.#apiKey}`,\n \"Aui-User-Id\": this.#userId,\n \"Aui-Workspace-Id\": this.#workspaceId,\n };\n }\n\n public readAuthHeaders() {\n // No operation needed for API key auth\n }\n}\n\nconst AUI_REFRESH_TOKEN_NAME = \"aui:refresh_token\";\n\nconst getLocalStorage = (): Storage | null => {\n if (!(\"localStorage\" in globalThis)) return null;\n try {\n return (globalThis as { localStorage: Storage }).localStorage;\n } catch {\n return null;\n }\n};\n\nconst readRefreshToken = ():\n | { token: string; expires_at: string }\n | undefined => {\n const storage = getLocalStorage();\n if (!storage) return undefined;\n try {\n const value = storage.getItem(AUI_REFRESH_TOKEN_NAME);\n return value\n ? (JSON.parse(value) as { token: string; expires_at: string })\n : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst writeRefreshToken = (refreshToken: {\n token: string;\n expires_at: string;\n}): void => {\n const storage = getLocalStorage();\n if (!storage) return;\n try {\n storage.setItem(AUI_REFRESH_TOKEN_NAME, JSON.stringify(refreshToken));\n } catch {}\n};\n\nconst removeRefreshToken = (): void => {\n const storage = getLocalStorage();\n if (!storage) return;\n try {\n storage.removeItem(AUI_REFRESH_TOKEN_NAME);\n } catch {}\n};\n\nexport class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthStrategy {\n public readonly strategy = \"anon\";\n\n private baseUrl: string;\n private jwtStrategy: AssistantCloudJWTAuthStrategy;\n\n constructor(baseUrl: string) {\n this.baseUrl = baseUrl;\n this.jwtStrategy = new AssistantCloudJWTAuthStrategy(async () => {\n const currentTime = Date.now();\n const storedRefreshToken = readRefreshToken();\n\n if (storedRefreshToken) {\n const refreshExpiry = new Date(storedRefreshToken.expires_at).getTime();\n if (refreshExpiry - currentTime > 30 * 1000) {\n const response = await fetch(\n `${this.baseUrl}/v1/auth/tokens/refresh`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ refresh_token: storedRefreshToken.token }),\n },\n );\n\n if (response.ok) {\n const data = await response.json();\n const { access_token, refresh_token } = data;\n if (refresh_token) {\n writeRefreshToken(refresh_token);\n }\n return access_token;\n }\n } else {\n removeRefreshToken();\n }\n }\n\n // No valid refresh token; request a new anonymous token\n const response = await fetch(`${this.baseUrl}/v1/auth/tokens/anonymous`, {\n method: \"POST\",\n });\n\n if (!response.ok) return null;\n\n const data = await response.json();\n const { access_token, refresh_token } = data;\n\n if (!access_token || !refresh_token) return null;\n\n writeRefreshToken(refresh_token);\n return access_token;\n });\n }\n\n public async getAuthHeaders(): Promise<Record<string, string> | false> {\n return this.jwtStrategy.getAuthHeaders();\n }\n\n public readAuthHeaders(headers: Headers): void {\n this.jwtStrategy.readAuthHeaders(headers);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AssistantCloudAuthStrategy.js","names":["#authTokenCallback","#apiKey","#userId","#workspaceId"],"sources":["../src/AssistantCloudAuthStrategy.ts"],"sourcesContent":["import {\n CloudResponseError,\n readCloudRecord,\n readCloudString,\n} from \"./cloudResponse\";\n\nexport type AssistantCloudAuthStrategy = {\n readonly strategy: \"anon\" | \"jwt\" | \"api-key\";\n getAuthHeaders(): Promise<Record<string, string> | false>;\n readAuthHeaders(headers: Headers): void;\n};\n\nconst getJwtExpiry = (jwt: string): number => {\n try {\n const parts = jwt.split(\".\");\n const bodyPart = parts[1];\n if (!bodyPart) {\n throw new Error(\"Invalid JWT format\");\n }\n\n // Convert from Base64Url to Base64 and add padding if necessary\n let base64 = bodyPart.replace(/-/g, \"+\").replace(/_/g, \"/\");\n while (base64.length % 4 !== 0) {\n base64 += \"=\";\n }\n\n // Decode the Base64 string and parse the payload\n const payload = atob(base64);\n const payloadObj = JSON.parse(payload);\n const exp = payloadObj.exp;\n\n if (!exp || typeof exp !== \"number\") {\n throw new Error('JWT does not contain a valid \"exp\" field');\n }\n\n // Convert expiration time to milliseconds\n return exp * 1000;\n } catch (error) {\n throw new Error(`Unable to determine the token expiry: ${error}`);\n }\n};\n\ntype RefreshToken = {\n token: string;\n expires_at: string;\n};\n\nconst readNonEmptyCloudString = (value: unknown, field: string): string => {\n const result = readCloudString(value, field);\n if (result.length === 0) {\n throw new CloudResponseError(\n `Invalid Assistant Cloud response for \"${field}\": expected a non-empty string`,\n );\n }\n return result;\n};\n\nconst readRefreshTokenResponse = (\n value: unknown,\n field: string,\n): RefreshToken => {\n const refreshToken = readCloudRecord(value, field);\n return {\n token: readNonEmptyCloudString(refreshToken.token, `${field}.token`),\n expires_at: readNonEmptyCloudString(\n refreshToken.expires_at,\n `${field}.expires_at`,\n ),\n };\n};\n\nconst readAuthTokenResponse = async (\n response: Response,\n field: string,\n): Promise<{ data: Record<string, unknown>; accessToken: string }> => {\n let value: unknown;\n try {\n value = await response.json();\n } catch {\n throw new CloudResponseError(\n `Invalid Assistant Cloud response for \"${field}\": expected valid JSON`,\n );\n }\n\n const data = readCloudRecord(value, field);\n const accessToken = readNonEmptyCloudString(\n data.access_token,\n `${field}.access_token`,\n );\n return { data, accessToken };\n};\n\nexport class AssistantCloudJWTAuthStrategy implements AssistantCloudAuthStrategy {\n public readonly strategy = \"jwt\";\n\n private cachedToken: string | null = null;\n private tokenExpiry: number | null = null;\n private tokenRequest: Promise<Record<string, string> | false> | null = null;\n #authTokenCallback: () => Promise<string | null>;\n\n constructor(authTokenCallback: () => Promise<string | null>) {\n this.#authTokenCallback = authTokenCallback;\n }\n\n public async getAuthHeaders(): Promise<Record<string, string> | false> {\n const currentTime = Date.now();\n\n // Use cached token if it's valid for at least 30 more seconds\n if (\n this.cachedToken &&\n this.tokenExpiry &&\n this.tokenExpiry - currentTime > 30 * 1000\n ) {\n return { Authorization: `Bearer ${this.cachedToken}` };\n }\n\n if (!this.tokenRequest) {\n this.tokenRequest = this.fetchAuthHeaders();\n }\n\n const tokenRequest = this.tokenRequest;\n try {\n return await tokenRequest;\n } finally {\n if (this.tokenRequest === tokenRequest) {\n this.tokenRequest = null;\n }\n }\n }\n\n private async fetchAuthHeaders(): Promise<Record<string, string> | false> {\n const token = await this.#authTokenCallback();\n if (!token) return false;\n\n this.cachedToken = token;\n this.tokenExpiry = getJwtExpiry(token);\n\n return { Authorization: `Bearer ${token}` };\n }\n\n public readAuthHeaders(headers: Headers) {\n const authHeader = headers.get(\"Authorization\");\n if (!authHeader) return;\n\n const [scheme, token] = authHeader.split(\" \");\n if (scheme !== \"Bearer\" || !token) {\n throw new Error(\"Invalid auth header received\");\n }\n\n this.cachedToken = token;\n this.tokenExpiry = getJwtExpiry(token);\n }\n}\n\nexport class AssistantCloudAPIKeyAuthStrategy implements AssistantCloudAuthStrategy {\n public readonly strategy = \"api-key\";\n\n #apiKey: string;\n #userId: string;\n #workspaceId: string;\n\n constructor(apiKey: string, userId: string, workspaceId: string) {\n this.#apiKey = apiKey;\n this.#userId = userId;\n this.#workspaceId = workspaceId;\n }\n\n public async getAuthHeaders(): Promise<Record<string, string>> {\n return {\n Authorization: `Bearer ${this.#apiKey}`,\n \"Aui-User-Id\": this.#userId,\n \"Aui-Workspace-Id\": this.#workspaceId,\n };\n }\n\n public readAuthHeaders() {\n // No operation needed for API key auth\n }\n}\n\nconst AUI_REFRESH_TOKEN_NAME = \"aui:refresh_token\";\n\nconst getLocalStorage = (): Storage | null => {\n if (!(\"localStorage\" in globalThis)) return null;\n try {\n return (globalThis as { localStorage: Storage }).localStorage;\n } catch {\n return null;\n }\n};\n\nconst readRefreshToken = (): RefreshToken | undefined => {\n const storage = getLocalStorage();\n if (!storage) return undefined;\n try {\n const value = storage.getItem(AUI_REFRESH_TOKEN_NAME);\n return value\n ? (JSON.parse(value) as { token: string; expires_at: string })\n : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst writeRefreshToken = (refreshToken: RefreshToken): void => {\n const storage = getLocalStorage();\n if (!storage) return;\n try {\n storage.setItem(AUI_REFRESH_TOKEN_NAME, JSON.stringify(refreshToken));\n } catch {}\n};\n\nconst removeRefreshToken = (): void => {\n const storage = getLocalStorage();\n if (!storage) return;\n try {\n storage.removeItem(AUI_REFRESH_TOKEN_NAME);\n } catch {}\n};\n\nexport class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthStrategy {\n public readonly strategy = \"anon\";\n\n private baseUrl: string;\n private jwtStrategy: AssistantCloudJWTAuthStrategy;\n\n constructor(baseUrl: string) {\n this.baseUrl = baseUrl;\n this.jwtStrategy = new AssistantCloudJWTAuthStrategy(async () => {\n const currentTime = Date.now();\n const storedRefreshToken = readRefreshToken();\n\n if (storedRefreshToken) {\n const refreshExpiry = new Date(storedRefreshToken.expires_at).getTime();\n if (refreshExpiry - currentTime > 30 * 1000) {\n const response = await fetch(\n `${this.baseUrl}/v1/auth/tokens/refresh`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ refresh_token: storedRefreshToken.token }),\n },\n );\n\n if (response.ok) {\n const { data, accessToken } = await readAuthTokenResponse(\n response,\n \"refresh auth token response\",\n );\n if (data.refresh_token != null) {\n writeRefreshToken(\n readRefreshTokenResponse(\n data.refresh_token,\n \"refresh auth token response.refresh_token\",\n ),\n );\n }\n return accessToken;\n }\n } else {\n removeRefreshToken();\n }\n }\n\n // No valid refresh token; request a new anonymous token\n const response = await fetch(`${this.baseUrl}/v1/auth/tokens/anonymous`, {\n method: \"POST\",\n });\n\n if (!response.ok) return null;\n\n const { data, accessToken } = await readAuthTokenResponse(\n response,\n \"anonymous auth token response\",\n );\n\n writeRefreshToken(\n readRefreshTokenResponse(\n data.refresh_token,\n \"anonymous auth token response.refresh_token\",\n ),\n );\n return accessToken;\n });\n }\n\n public async getAuthHeaders(): Promise<Record<string, string> | false> {\n return this.jwtStrategy.getAuthHeaders();\n }\n\n public readAuthHeaders(headers: Headers): void {\n this.jwtStrategy.readAuthHeaders(headers);\n }\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { CloudAPIError } from "./AssistantCloudAPI.js";
2
1
  import { CloudResponseError } from "./cloudResponse.js";
2
+ import { CloudAPIError } from "./AssistantCloudAPI.js";
3
3
  import { AssistantCloud } from "./AssistantCloud.js";
4
4
  import { CloudMessagePersistence } from "./CloudMessagePersistence.js";
5
5
  import { createFormattedPersistence } from "./FormattedCloudPersistence.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistant-cloud",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "description": "Cloud integration for assistant-ui",
5
5
  "keywords": [
6
6
  "assistant",
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "sideEffects": false,
29
29
  "dependencies": {
30
- "assistant-stream": "^0.3.28"
30
+ "assistant-stream": "^0.3.33"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^26.1.1",
@@ -1,3 +1,9 @@
1
+ import {
2
+ CloudResponseError,
3
+ readCloudRecord,
4
+ readCloudString,
5
+ } from "./cloudResponse";
6
+
1
7
  export type AssistantCloudAuthStrategy = {
2
8
  readonly strategy: "anon" | "jwt" | "api-key";
3
9
  getAuthHeaders(): Promise<Record<string, string> | false>;
@@ -34,6 +40,56 @@ const getJwtExpiry = (jwt: string): number => {
34
40
  }
35
41
  };
36
42
 
43
+ type RefreshToken = {
44
+ token: string;
45
+ expires_at: string;
46
+ };
47
+
48
+ const readNonEmptyCloudString = (value: unknown, field: string): string => {
49
+ const result = readCloudString(value, field);
50
+ if (result.length === 0) {
51
+ throw new CloudResponseError(
52
+ `Invalid Assistant Cloud response for "${field}": expected a non-empty string`,
53
+ );
54
+ }
55
+ return result;
56
+ };
57
+
58
+ const readRefreshTokenResponse = (
59
+ value: unknown,
60
+ field: string,
61
+ ): RefreshToken => {
62
+ const refreshToken = readCloudRecord(value, field);
63
+ return {
64
+ token: readNonEmptyCloudString(refreshToken.token, `${field}.token`),
65
+ expires_at: readNonEmptyCloudString(
66
+ refreshToken.expires_at,
67
+ `${field}.expires_at`,
68
+ ),
69
+ };
70
+ };
71
+
72
+ const readAuthTokenResponse = async (
73
+ response: Response,
74
+ field: string,
75
+ ): Promise<{ data: Record<string, unknown>; accessToken: string }> => {
76
+ let value: unknown;
77
+ try {
78
+ value = await response.json();
79
+ } catch {
80
+ throw new CloudResponseError(
81
+ `Invalid Assistant Cloud response for "${field}": expected valid JSON`,
82
+ );
83
+ }
84
+
85
+ const data = readCloudRecord(value, field);
86
+ const accessToken = readNonEmptyCloudString(
87
+ data.access_token,
88
+ `${field}.access_token`,
89
+ );
90
+ return { data, accessToken };
91
+ };
92
+
37
93
  export class AssistantCloudJWTAuthStrategy implements AssistantCloudAuthStrategy {
38
94
  public readonly strategy = "jwt";
39
95
 
@@ -133,9 +189,7 @@ const getLocalStorage = (): Storage | null => {
133
189
  }
134
190
  };
135
191
 
136
- const readRefreshToken = ():
137
- | { token: string; expires_at: string }
138
- | undefined => {
192
+ const readRefreshToken = (): RefreshToken | undefined => {
139
193
  const storage = getLocalStorage();
140
194
  if (!storage) return undefined;
141
195
  try {
@@ -148,10 +202,7 @@ const readRefreshToken = ():
148
202
  }
149
203
  };
150
204
 
151
- const writeRefreshToken = (refreshToken: {
152
- token: string;
153
- expires_at: string;
154
- }): void => {
205
+ const writeRefreshToken = (refreshToken: RefreshToken): void => {
155
206
  const storage = getLocalStorage();
156
207
  if (!storage) return;
157
208
  try {
@@ -192,12 +243,19 @@ export class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthSt
192
243
  );
193
244
 
194
245
  if (response.ok) {
195
- const data = await response.json();
196
- const { access_token, refresh_token } = data;
197
- if (refresh_token) {
198
- writeRefreshToken(refresh_token);
246
+ const { data, accessToken } = await readAuthTokenResponse(
247
+ response,
248
+ "refresh auth token response",
249
+ );
250
+ if (data.refresh_token != null) {
251
+ writeRefreshToken(
252
+ readRefreshTokenResponse(
253
+ data.refresh_token,
254
+ "refresh auth token response.refresh_token",
255
+ ),
256
+ );
199
257
  }
200
- return access_token;
258
+ return accessToken;
201
259
  }
202
260
  } else {
203
261
  removeRefreshToken();
@@ -211,13 +269,18 @@ export class AssistantCloudAnonymousAuthStrategy implements AssistantCloudAuthSt
211
269
 
212
270
  if (!response.ok) return null;
213
271
 
214
- const data = await response.json();
215
- const { access_token, refresh_token } = data;
216
-
217
- if (!access_token || !refresh_token) return null;
218
-
219
- writeRefreshToken(refresh_token);
220
- return access_token;
272
+ const { data, accessToken } = await readAuthTokenResponse(
273
+ response,
274
+ "anonymous auth token response",
275
+ );
276
+
277
+ writeRefreshToken(
278
+ readRefreshTokenResponse(
279
+ data.refresh_token,
280
+ "anonymous auth token response.refresh_token",
281
+ ),
282
+ );
283
+ return accessToken;
221
284
  });
222
285
  }
223
286
 
@@ -1,5 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { AssistantCloudAPI, CloudAPIError } from "../AssistantCloudAPI";
3
+ import { CloudResponseError } from "../cloudResponse";
3
4
 
4
5
  describe("AssistantCloudAPI", () => {
5
6
  beforeEach(() => {
@@ -118,6 +119,30 @@ describe("AssistantCloudAPI", () => {
118
119
  await expect(api.initializeAuth()).resolves.toBe(false);
119
120
  });
120
121
 
122
+ it("rejects initializeAuth with context for malformed anonymous responses", async () => {
123
+ vi.stubGlobal(
124
+ "fetch",
125
+ vi.fn().mockResolvedValue({
126
+ ok: true,
127
+ json: vi.fn().mockResolvedValue({
128
+ access_token: 123,
129
+ refresh_token: null,
130
+ }),
131
+ }),
132
+ );
133
+
134
+ const api = new AssistantCloudAPI({
135
+ baseUrl: "https://test.example.com",
136
+ anonymous: true,
137
+ });
138
+
139
+ await expect(api.initializeAuth()).rejects.toThrow(
140
+ new CloudResponseError(
141
+ 'Invalid Assistant Cloud response for "anonymous auth token response.access_token": expected a string',
142
+ ),
143
+ );
144
+ });
145
+
121
146
  it("throws APIError with parsed message for JSON error responses", async () => {
122
147
  const fetchMock = vi.fn().mockResolvedValue({
123
148
  ok: false,
@@ -3,10 +3,14 @@ import {
3
3
  AssistantCloudAnonymousAuthStrategy,
4
4
  AssistantCloudJWTAuthStrategy,
5
5
  } from "../AssistantCloudAuthStrategy";
6
+ import { CloudResponseError } from "../cloudResponse";
6
7
 
7
8
  const baseUrl = "https://test.example.com";
8
9
  const accessToken = `${Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url")}.${Buffer.from(JSON.stringify({ exp: 4102444800 })).toString("base64url")}.sig`;
9
- const refreshToken = { token: "r1", expires_at: "2099-01-01" };
10
+ const refreshToken = {
11
+ token: "r1",
12
+ expires_at: "2099-01-01",
13
+ };
10
14
 
11
15
  let originalLocalStorageDescriptor: PropertyDescriptor | undefined;
12
16
 
@@ -17,12 +21,12 @@ const installLocalStorage = (storage: Storage): void => {
17
21
  });
18
22
  };
19
23
 
20
- const mockAnonymousTokenFetch = () => {
24
+ const mockAnonymousTokenFetch = (nextRefreshToken = refreshToken) => {
21
25
  const fetchMock = vi.fn().mockResolvedValue({
22
26
  ok: true,
23
27
  json: vi.fn().mockResolvedValue({
24
28
  access_token: accessToken,
25
- refresh_token: refreshToken,
29
+ refresh_token: nextRefreshToken,
26
30
  }),
27
31
  });
28
32
  vi.stubGlobal("fetch", fetchMock);
@@ -50,7 +54,14 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
50
54
  }
51
55
  });
52
56
 
53
- it("persists the refresh token and returns the anonymous access token", async () => {
57
+ it.each([
58
+ "2099-01-01",
59
+ "2099-01-01T00:00:00Z",
60
+ "2099-01-01T00:00:00+0000",
61
+ "2099-01-01T00:00:00",
62
+ "2099-01-01 00:00:00+00",
63
+ ])("persists refresh tokens with expiry %s", async (expiresAt) => {
64
+ const nextRefreshToken = { ...refreshToken, expires_at: expiresAt };
54
65
  const values = new Map<string, string>();
55
66
  installLocalStorage({
56
67
  getItem: (key) => values.get(key) ?? null,
@@ -61,14 +72,16 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
61
72
  values.delete(key);
62
73
  },
63
74
  } as Storage);
64
- const fetchMock = mockAnonymousTokenFetch();
75
+ const fetchMock = mockAnonymousTokenFetch(nextRefreshToken);
65
76
 
66
77
  const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl);
67
78
 
68
79
  await expect(strategy.getAuthHeaders()).resolves.toEqual({
69
80
  Authorization: `Bearer ${accessToken}`,
70
81
  });
71
- expect(values.get("aui:refresh_token")).toBe(JSON.stringify(refreshToken));
82
+ expect(values.get("aui:refresh_token")).toBe(
83
+ JSON.stringify(nextRefreshToken),
84
+ );
72
85
  expect(fetchMock).toHaveBeenCalledWith(
73
86
  `${baseUrl}/v1/auth/tokens/anonymous`,
74
87
  { method: "POST" },
@@ -170,6 +183,139 @@ describe("AssistantCloudAnonymousAuthStrategy", () => {
170
183
  });
171
184
  expect(values.get("aui:refresh_token")).toBe(JSON.stringify(refreshToken));
172
185
  });
186
+
187
+ it("rejects malformed anonymous token responses without persisting them", async () => {
188
+ const setItem = vi.fn();
189
+ installLocalStorage({
190
+ getItem: () => null,
191
+ setItem,
192
+ removeItem: vi.fn(),
193
+ } as unknown as Storage);
194
+ vi.stubGlobal(
195
+ "fetch",
196
+ vi.fn().mockResolvedValue({
197
+ ok: true,
198
+ json: vi.fn().mockResolvedValue({
199
+ access_token: accessToken,
200
+ refresh_token: "not-a-refresh-token",
201
+ }),
202
+ }),
203
+ );
204
+
205
+ const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl);
206
+
207
+ await expect(strategy.getAuthHeaders()).rejects.toThrow(
208
+ new CloudResponseError(
209
+ 'Invalid Assistant Cloud response for "anonymous auth token response.refresh_token": expected an object',
210
+ ),
211
+ );
212
+ expect(setItem).not.toHaveBeenCalled();
213
+ });
214
+
215
+ it("rejects empty refresh token expiry without persisting it", async () => {
216
+ const setItem = vi.fn();
217
+ installLocalStorage({
218
+ getItem: () => null,
219
+ setItem,
220
+ removeItem: vi.fn(),
221
+ } as unknown as Storage);
222
+ vi.stubGlobal(
223
+ "fetch",
224
+ vi.fn().mockResolvedValue({
225
+ ok: true,
226
+ json: vi.fn().mockResolvedValue({
227
+ access_token: accessToken,
228
+ refresh_token: {
229
+ token: "r2",
230
+ expires_at: "",
231
+ },
232
+ }),
233
+ }),
234
+ );
235
+
236
+ const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl);
237
+
238
+ await expect(strategy.getAuthHeaders()).rejects.toThrow(
239
+ new CloudResponseError(
240
+ 'Invalid Assistant Cloud response for "anonymous auth token response.refresh_token.expires_at": expected a non-empty string',
241
+ ),
242
+ );
243
+ expect(setItem).not.toHaveBeenCalled();
244
+ });
245
+
246
+ it("contextualizes malformed refresh token responses", async () => {
247
+ installLocalStorage({
248
+ getItem: () => JSON.stringify(refreshToken),
249
+ setItem: vi.fn(),
250
+ removeItem: vi.fn(),
251
+ } as unknown as Storage);
252
+ vi.stubGlobal(
253
+ "fetch",
254
+ vi.fn().mockResolvedValue({
255
+ ok: true,
256
+ json: vi.fn().mockResolvedValue({ access_token: 123 }),
257
+ }),
258
+ );
259
+
260
+ const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl);
261
+
262
+ await expect(strategy.getAuthHeaders()).rejects.toThrow(
263
+ new CloudResponseError(
264
+ 'Invalid Assistant Cloud response for "refresh auth token response.access_token": expected a string',
265
+ ),
266
+ );
267
+ });
268
+
269
+ it("accepts valid refresh responses without a rotated refresh token", async () => {
270
+ const setItem = vi.fn();
271
+ installLocalStorage({
272
+ getItem: () => JSON.stringify(refreshToken),
273
+ setItem,
274
+ removeItem: vi.fn(),
275
+ } as unknown as Storage);
276
+ const fetchMock = vi.fn().mockResolvedValue({
277
+ ok: true,
278
+ json: vi.fn().mockResolvedValue({
279
+ access_token: accessToken,
280
+ refresh_token: null,
281
+ }),
282
+ });
283
+ vi.stubGlobal("fetch", fetchMock);
284
+
285
+ const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl);
286
+
287
+ await expect(strategy.getAuthHeaders()).resolves.toEqual({
288
+ Authorization: `Bearer ${accessToken}`,
289
+ });
290
+ expect(fetchMock).toHaveBeenCalledWith(
291
+ `${baseUrl}/v1/auth/tokens/refresh`,
292
+ {
293
+ method: "POST",
294
+ headers: { "Content-Type": "application/json" },
295
+ body: JSON.stringify({ refresh_token: refreshToken.token }),
296
+ },
297
+ );
298
+ expect(setItem).not.toHaveBeenCalled();
299
+ });
300
+
301
+ it("contextualizes invalid JSON token responses", async () => {
302
+ delete (globalThis as { localStorage?: Storage }).localStorage;
303
+ vi.stubGlobal(
304
+ "fetch",
305
+ vi.fn().mockResolvedValue({
306
+ ok: true,
307
+ json: vi.fn().mockRejectedValue(new SyntaxError("Unexpected token")),
308
+ }),
309
+ );
310
+
311
+ const strategy = new AssistantCloudAnonymousAuthStrategy(baseUrl);
312
+
313
+ await expect(strategy.getAuthHeaders()).rejects.toThrow(
314
+ new CloudResponseError(
315
+ 'Invalid Assistant Cloud response for "anonymous auth token response": expected valid JSON',
316
+ ),
317
+ );
318
+ });
173
319
  });
174
320
 
175
321
  describe("AssistantCloudJWTAuthStrategy", () => {