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.
- package/dist/AssistantCloudAuthStrategy.d.ts.map +1 -1
- package/dist/AssistantCloudAuthStrategy.js +32 -7
- package/dist/AssistantCloudAuthStrategy.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/AssistantCloudAuthStrategy.ts +82 -19
- package/src/tests/AssistantCloudAPI.test.ts +25 -0
- package/src/tests/AssistantCloudAuthStrategy.test.ts +152 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssistantCloudAuthStrategy.d.ts","names":[],"sources":["../src/AssistantCloudAuthStrategy.ts"],"mappings":";
|
|
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 {
|
|
121
|
-
if (refresh_token) writeRefreshToken(refresh_token);
|
|
122
|
-
return
|
|
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 {
|
|
128
|
-
|
|
129
|
-
|
|
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.
|
|
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.
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
writeRefreshToken(
|
|
220
|
-
|
|
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 = {
|
|
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:
|
|
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(
|
|
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(
|
|
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", () => {
|