@uploadista/client-core 0.0.3
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/.turbo/turbo-build.log +5 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/auth/auth-http-client.d.ts +50 -0
- package/dist/auth/auth-http-client.d.ts.map +1 -0
- package/dist/auth/auth-http-client.js +110 -0
- package/dist/auth/direct-auth.d.ts +38 -0
- package/dist/auth/direct-auth.d.ts.map +1 -0
- package/dist/auth/direct-auth.js +95 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/no-auth.d.ts +26 -0
- package/dist/auth/no-auth.d.ts.map +1 -0
- package/dist/auth/no-auth.js +33 -0
- package/dist/auth/saas-auth.d.ts +80 -0
- package/dist/auth/saas-auth.d.ts.map +1 -0
- package/dist/auth/saas-auth.js +167 -0
- package/dist/auth/types.d.ts +101 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +8 -0
- package/dist/chunk-buffer.d.ts +209 -0
- package/dist/chunk-buffer.d.ts.map +1 -0
- package/dist/chunk-buffer.js +236 -0
- package/dist/client/create-uploadista-client.d.ts +369 -0
- package/dist/client/create-uploadista-client.d.ts.map +1 -0
- package/dist/client/create-uploadista-client.js +518 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +3 -0
- package/dist/client/uploadista-api.d.ts +284 -0
- package/dist/client/uploadista-api.d.ts.map +1 -0
- package/dist/client/uploadista-api.js +444 -0
- package/dist/client/uploadista-websocket-manager.d.ts +110 -0
- package/dist/client/uploadista-websocket-manager.d.ts.map +1 -0
- package/dist/client/uploadista-websocket-manager.js +207 -0
- package/dist/error.d.ts +106 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +69 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/logger.d.ts +70 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +59 -0
- package/dist/mock-data-store.d.ts +30 -0
- package/dist/mock-data-store.d.ts.map +1 -0
- package/dist/mock-data-store.js +88 -0
- package/dist/network-monitor.d.ts +262 -0
- package/dist/network-monitor.d.ts.map +1 -0
- package/dist/network-monitor.js +291 -0
- package/dist/services/abort-controller-service.d.ts +19 -0
- package/dist/services/abort-controller-service.d.ts.map +1 -0
- package/dist/services/abort-controller-service.js +4 -0
- package/dist/services/checksum-service.d.ts +4 -0
- package/dist/services/checksum-service.d.ts.map +1 -0
- package/dist/services/checksum-service.js +1 -0
- package/dist/services/file-reader-service.d.ts +38 -0
- package/dist/services/file-reader-service.d.ts.map +1 -0
- package/dist/services/file-reader-service.js +4 -0
- package/dist/services/fingerprint-service.d.ts +4 -0
- package/dist/services/fingerprint-service.d.ts.map +1 -0
- package/dist/services/fingerprint-service.js +1 -0
- package/dist/services/http-client.d.ts +182 -0
- package/dist/services/http-client.d.ts.map +1 -0
- package/dist/services/http-client.js +1 -0
- package/dist/services/id-generation-service.d.ts +10 -0
- package/dist/services/id-generation-service.d.ts.map +1 -0
- package/dist/services/id-generation-service.js +1 -0
- package/dist/services/index.d.ts +11 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +10 -0
- package/dist/services/platform-service.d.ts +48 -0
- package/dist/services/platform-service.d.ts.map +1 -0
- package/dist/services/platform-service.js +10 -0
- package/dist/services/service-container.d.ts +25 -0
- package/dist/services/service-container.d.ts.map +1 -0
- package/dist/services/service-container.js +1 -0
- package/dist/services/storage-service.d.ts +26 -0
- package/dist/services/storage-service.d.ts.map +1 -0
- package/dist/services/storage-service.js +1 -0
- package/dist/services/websocket-service.d.ts +36 -0
- package/dist/services/websocket-service.d.ts.map +1 -0
- package/dist/services/websocket-service.js +4 -0
- package/dist/smart-chunker.d.ts +72 -0
- package/dist/smart-chunker.d.ts.map +1 -0
- package/dist/smart-chunker.js +317 -0
- package/dist/storage/client-storage.d.ts +148 -0
- package/dist/storage/client-storage.d.ts.map +1 -0
- package/dist/storage/client-storage.js +62 -0
- package/dist/storage/in-memory-storage-service.d.ts +7 -0
- package/dist/storage/in-memory-storage-service.d.ts.map +1 -0
- package/dist/storage/in-memory-storage-service.js +24 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +2 -0
- package/dist/types/buffered-chunk.d.ts +6 -0
- package/dist/types/buffered-chunk.d.ts.map +1 -0
- package/dist/types/buffered-chunk.js +1 -0
- package/dist/types/chunk-metrics.d.ts +12 -0
- package/dist/types/chunk-metrics.d.ts.map +1 -0
- package/dist/types/chunk-metrics.js +1 -0
- package/dist/types/flow-result.d.ts +11 -0
- package/dist/types/flow-result.d.ts.map +1 -0
- package/dist/types/flow-result.js +1 -0
- package/dist/types/flow-upload-config.d.ts +54 -0
- package/dist/types/flow-upload-config.d.ts.map +1 -0
- package/dist/types/flow-upload-config.js +1 -0
- package/dist/types/flow-upload-item.d.ts +16 -0
- package/dist/types/flow-upload-item.d.ts.map +1 -0
- package/dist/types/flow-upload-item.js +1 -0
- package/dist/types/flow-upload-options.d.ts +41 -0
- package/dist/types/flow-upload-options.d.ts.map +1 -0
- package/dist/types/flow-upload-options.js +1 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +13 -0
- package/dist/types/multi-flow-upload-options.d.ts +33 -0
- package/dist/types/multi-flow-upload-options.d.ts.map +1 -0
- package/dist/types/multi-flow-upload-options.js +1 -0
- package/dist/types/multi-flow-upload-state.d.ts +9 -0
- package/dist/types/multi-flow-upload-state.d.ts.map +1 -0
- package/dist/types/multi-flow-upload-state.js +1 -0
- package/dist/types/performance-insights.d.ts +11 -0
- package/dist/types/performance-insights.d.ts.map +1 -0
- package/dist/types/performance-insights.js +1 -0
- package/dist/types/previous-upload.d.ts +20 -0
- package/dist/types/previous-upload.d.ts.map +1 -0
- package/dist/types/previous-upload.js +9 -0
- package/dist/types/upload-options.d.ts +40 -0
- package/dist/types/upload-options.d.ts.map +1 -0
- package/dist/types/upload-options.js +1 -0
- package/dist/types/upload-response.d.ts +6 -0
- package/dist/types/upload-response.d.ts.map +1 -0
- package/dist/types/upload-response.js +1 -0
- package/dist/types/upload-result.d.ts +57 -0
- package/dist/types/upload-result.d.ts.map +1 -0
- package/dist/types/upload-result.js +1 -0
- package/dist/types/upload-session-metrics.d.ts +16 -0
- package/dist/types/upload-session-metrics.d.ts.map +1 -0
- package/dist/types/upload-session-metrics.js +1 -0
- package/dist/upload/chunk-upload.d.ts +40 -0
- package/dist/upload/chunk-upload.d.ts.map +1 -0
- package/dist/upload/chunk-upload.js +82 -0
- package/dist/upload/flow-upload.d.ts +48 -0
- package/dist/upload/flow-upload.d.ts.map +1 -0
- package/dist/upload/flow-upload.js +240 -0
- package/dist/upload/index.d.ts +3 -0
- package/dist/upload/index.d.ts.map +1 -0
- package/dist/upload/index.js +2 -0
- package/dist/upload/parallel-upload.d.ts +65 -0
- package/dist/upload/parallel-upload.d.ts.map +1 -0
- package/dist/upload/parallel-upload.js +231 -0
- package/dist/upload/single-upload.d.ts +118 -0
- package/dist/upload/single-upload.d.ts.map +1 -0
- package/dist/upload/single-upload.js +332 -0
- package/dist/upload/upload-manager.d.ts +30 -0
- package/dist/upload/upload-manager.d.ts.map +1 -0
- package/dist/upload/upload-manager.js +57 -0
- package/dist/upload/upload-metrics.d.ts +37 -0
- package/dist/upload/upload-metrics.d.ts.map +1 -0
- package/dist/upload/upload-metrics.js +236 -0
- package/dist/upload/upload-storage.d.ts +32 -0
- package/dist/upload/upload-storage.d.ts.map +1 -0
- package/dist/upload/upload-storage.js +46 -0
- package/dist/upload/upload-strategy.d.ts +66 -0
- package/dist/upload/upload-strategy.d.ts.map +1 -0
- package/dist/upload/upload-strategy.js +171 -0
- package/dist/upload/upload-utils.d.ts +26 -0
- package/dist/upload/upload-utils.d.ts.map +1 -0
- package/dist/upload/upload-utils.js +80 -0
- package/package.json +29 -0
- package/src/__tests__/smart-chunking.test.ts +399 -0
- package/src/auth/__tests__/auth-http-client.test.ts +327 -0
- package/src/auth/__tests__/direct-auth.test.ts +135 -0
- package/src/auth/__tests__/no-auth.test.ts +40 -0
- package/src/auth/__tests__/saas-auth.test.ts +337 -0
- package/src/auth/auth-http-client.ts +150 -0
- package/src/auth/direct-auth.ts +121 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/no-auth.ts +39 -0
- package/src/auth/saas-auth.ts +218 -0
- package/src/auth/types.ts +105 -0
- package/src/chunk-buffer.ts +287 -0
- package/src/client/create-uploadista-client.ts +901 -0
- package/src/client/index.ts +3 -0
- package/src/client/uploadista-api.ts +857 -0
- package/src/client/uploadista-websocket-manager.ts +275 -0
- package/src/error.ts +149 -0
- package/src/index.ts +13 -0
- package/src/logger.ts +104 -0
- package/src/mock-data-store.ts +97 -0
- package/src/network-monitor.ts +445 -0
- package/src/services/abort-controller-service.ts +21 -0
- package/src/services/checksum-service.ts +3 -0
- package/src/services/file-reader-service.ts +44 -0
- package/src/services/fingerprint-service.ts +6 -0
- package/src/services/http-client.ts +229 -0
- package/src/services/id-generation-service.ts +9 -0
- package/src/services/index.ts +10 -0
- package/src/services/platform-service.ts +65 -0
- package/src/services/service-container.ts +24 -0
- package/src/services/storage-service.ts +29 -0
- package/src/services/websocket-service.ts +33 -0
- package/src/smart-chunker.ts +451 -0
- package/src/storage/client-storage.ts +186 -0
- package/src/storage/in-memory-storage-service.ts +33 -0
- package/src/storage/index.ts +2 -0
- package/src/types/buffered-chunk.ts +5 -0
- package/src/types/chunk-metrics.ts +11 -0
- package/src/types/flow-result.ts +14 -0
- package/src/types/flow-upload-config.ts +56 -0
- package/src/types/flow-upload-item.ts +16 -0
- package/src/types/flow-upload-options.ts +56 -0
- package/src/types/index.ts +13 -0
- package/src/types/multi-flow-upload-options.ts +39 -0
- package/src/types/multi-flow-upload-state.ts +9 -0
- package/src/types/performance-insights.ts +7 -0
- package/src/types/previous-upload.ts +22 -0
- package/src/types/upload-options.ts +56 -0
- package/src/types/upload-response.ts +6 -0
- package/src/types/upload-result.ts +60 -0
- package/src/types/upload-session-metrics.ts +15 -0
- package/src/upload/chunk-upload.ts +151 -0
- package/src/upload/flow-upload.ts +367 -0
- package/src/upload/index.ts +2 -0
- package/src/upload/parallel-upload.ts +387 -0
- package/src/upload/single-upload.ts +554 -0
- package/src/upload/upload-manager.ts +106 -0
- package/src/upload/upload-metrics.ts +340 -0
- package/src/upload/upload-storage.ts +87 -0
- package/src/upload/upload-strategy.ts +296 -0
- package/src/upload/upload-utils.ts +114 -0
- package/tsconfig.json +23 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { SaasAuthManager } from "../saas-auth";
|
|
3
|
+
import type { SaasAuthConfig } from "../types";
|
|
4
|
+
|
|
5
|
+
// Mock fetch globally
|
|
6
|
+
global.fetch = vi.fn();
|
|
7
|
+
|
|
8
|
+
describe("SaasAuthManager", () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("fetchToken", () => {
|
|
14
|
+
it("should fetch token from auth server", async () => {
|
|
15
|
+
const config: SaasAuthConfig = {
|
|
16
|
+
mode: "saas",
|
|
17
|
+
authServerUrl: "https://auth.example.com/token",
|
|
18
|
+
getCredentials: () => ({
|
|
19
|
+
username: "user",
|
|
20
|
+
password: "pass",
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
vi.mocked(global.fetch).mockResolvedValueOnce({
|
|
25
|
+
ok: true,
|
|
26
|
+
json: async () => ({
|
|
27
|
+
token: "jwt-token-123",
|
|
28
|
+
expiresIn: 3600,
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const manager = new SaasAuthManager(config);
|
|
33
|
+
const result = await manager.fetchToken();
|
|
34
|
+
|
|
35
|
+
expect(result).toEqual({
|
|
36
|
+
token: "jwt-token-123",
|
|
37
|
+
expiresIn: 3600,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
41
|
+
"https://auth.example.com/token",
|
|
42
|
+
{
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: {
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify({ username: "user", password: "pass" }),
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should handle auth server errors", async () => {
|
|
53
|
+
const config: SaasAuthConfig = {
|
|
54
|
+
mode: "saas",
|
|
55
|
+
authServerUrl: "https://auth.example.com/token",
|
|
56
|
+
getCredentials: () => ({ username: "user", password: "wrong" }),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
vi.mocked(global.fetch).mockResolvedValueOnce({
|
|
60
|
+
ok: false,
|
|
61
|
+
status: 401,
|
|
62
|
+
text: async () => JSON.stringify({ error: "Invalid credentials" }),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const manager = new SaasAuthManager(config);
|
|
66
|
+
|
|
67
|
+
await expect(manager.fetchToken()).rejects.toThrow(
|
|
68
|
+
"Failed to fetch auth token: Invalid credentials",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should handle network errors", async () => {
|
|
73
|
+
const config: SaasAuthConfig = {
|
|
74
|
+
mode: "saas",
|
|
75
|
+
authServerUrl: "https://auth.example.com/token",
|
|
76
|
+
getCredentials: () => ({ username: "user", password: "pass" }),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
vi.mocked(global.fetch).mockRejectedValueOnce(new Error("Network error"));
|
|
80
|
+
|
|
81
|
+
const manager = new SaasAuthManager(config);
|
|
82
|
+
|
|
83
|
+
await expect(manager.fetchToken()).rejects.toThrow(
|
|
84
|
+
"Failed to fetch auth token: Network error",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should validate token response format", async () => {
|
|
89
|
+
const config: SaasAuthConfig = {
|
|
90
|
+
mode: "saas",
|
|
91
|
+
authServerUrl: "https://auth.example.com/token",
|
|
92
|
+
getCredentials: () => ({ username: "user", password: "pass" }),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
vi.mocked(global.fetch).mockResolvedValueOnce({
|
|
96
|
+
ok: true,
|
|
97
|
+
json: async () => ({ noToken: "here" }), // Missing token field
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const manager = new SaasAuthManager(config);
|
|
101
|
+
|
|
102
|
+
await expect(manager.fetchToken()).rejects.toThrow(
|
|
103
|
+
"Auth server response missing 'token' field",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("attachToken", () => {
|
|
109
|
+
it("should attach token as Bearer header", async () => {
|
|
110
|
+
const config: SaasAuthConfig = {
|
|
111
|
+
mode: "saas",
|
|
112
|
+
authServerUrl: "https://auth.example.com/token",
|
|
113
|
+
getCredentials: () => ({ username: "user", password: "pass" }),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
vi.mocked(global.fetch).mockResolvedValueOnce({
|
|
117
|
+
ok: true,
|
|
118
|
+
json: async () => ({ token: "jwt-token-123" }),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const manager = new SaasAuthManager(config);
|
|
122
|
+
const result = await manager.attachToken({
|
|
123
|
+
"Content-Type": "application/json",
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(result).toEqual({
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
Authorization: "Bearer jwt-token-123",
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should cache token and reuse it", async () => {
|
|
133
|
+
const config: SaasAuthConfig = {
|
|
134
|
+
mode: "saas",
|
|
135
|
+
authServerUrl: "https://auth.example.com/token",
|
|
136
|
+
getCredentials: () => ({ username: "user", password: "pass" }),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
vi.mocked(global.fetch).mockResolvedValueOnce({
|
|
140
|
+
ok: true,
|
|
141
|
+
json: async () => ({ token: "jwt-token-123" }),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const manager = new SaasAuthManager(config);
|
|
145
|
+
|
|
146
|
+
// First call - should fetch token
|
|
147
|
+
await manager.attachToken();
|
|
148
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
149
|
+
|
|
150
|
+
// Second call - should use cached token
|
|
151
|
+
await manager.attachToken();
|
|
152
|
+
expect(global.fetch).toHaveBeenCalledTimes(1); // Still 1, not 2
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should cache tokens per job ID", async () => {
|
|
156
|
+
const config: SaasAuthConfig = {
|
|
157
|
+
mode: "saas",
|
|
158
|
+
authServerUrl: "https://auth.example.com/token",
|
|
159
|
+
getCredentials: () => ({ username: "user", password: "pass" }),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
vi.mocked(global.fetch)
|
|
163
|
+
.mockResolvedValueOnce({
|
|
164
|
+
ok: true,
|
|
165
|
+
json: async () => ({ token: "jwt-token-1" }),
|
|
166
|
+
})
|
|
167
|
+
.mockResolvedValueOnce({
|
|
168
|
+
ok: true,
|
|
169
|
+
json: async () => ({ token: "jwt-token-2" }),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const manager = new SaasAuthManager(config);
|
|
173
|
+
|
|
174
|
+
// Fetch token for job 1
|
|
175
|
+
const result1 = await manager.attachToken({}, "job-1");
|
|
176
|
+
expect(result1.Authorization).toBe("Bearer jwt-token-1");
|
|
177
|
+
|
|
178
|
+
// Fetch token for job 2
|
|
179
|
+
const result2 = await manager.attachToken({}, "job-2");
|
|
180
|
+
expect(result2.Authorization).toBe("Bearer jwt-token-2");
|
|
181
|
+
|
|
182
|
+
// Reuse token for job 1
|
|
183
|
+
const result3 = await manager.attachToken({}, "job-1");
|
|
184
|
+
expect(result3.Authorization).toBe("Bearer jwt-token-1");
|
|
185
|
+
|
|
186
|
+
// Should have fetched twice (once per job)
|
|
187
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should refetch expired tokens", async () => {
|
|
191
|
+
const config: SaasAuthConfig = {
|
|
192
|
+
mode: "saas",
|
|
193
|
+
authServerUrl: "https://auth.example.com/token",
|
|
194
|
+
getCredentials: () => ({ username: "user", password: "pass" }),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// First token expires in 1 second
|
|
198
|
+
vi.mocked(global.fetch)
|
|
199
|
+
.mockResolvedValueOnce({
|
|
200
|
+
ok: true,
|
|
201
|
+
json: async () => ({
|
|
202
|
+
token: "jwt-token-old",
|
|
203
|
+
expiresIn: 0.001, // Expires very soon
|
|
204
|
+
}),
|
|
205
|
+
})
|
|
206
|
+
.mockResolvedValueOnce({
|
|
207
|
+
ok: true,
|
|
208
|
+
json: async () => ({ token: "jwt-token-new" }),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const manager = new SaasAuthManager(config);
|
|
212
|
+
|
|
213
|
+
// First call - fetch initial token
|
|
214
|
+
await manager.attachToken();
|
|
215
|
+
|
|
216
|
+
// Wait for token to expire
|
|
217
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
218
|
+
|
|
219
|
+
// Second call - should fetch new token because old one expired
|
|
220
|
+
const result = await manager.attachToken();
|
|
221
|
+
expect(result.Authorization).toBe("Bearer jwt-token-new");
|
|
222
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("clearToken", () => {
|
|
227
|
+
it("should clear cached token for specific job", async () => {
|
|
228
|
+
const config: SaasAuthConfig = {
|
|
229
|
+
mode: "saas",
|
|
230
|
+
authServerUrl: "https://auth.example.com/token",
|
|
231
|
+
getCredentials: () => ({ username: "user", password: "pass" }),
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
vi.mocked(global.fetch)
|
|
235
|
+
.mockResolvedValueOnce({
|
|
236
|
+
ok: true,
|
|
237
|
+
json: async () => ({ token: "jwt-token-1" }),
|
|
238
|
+
})
|
|
239
|
+
.mockResolvedValueOnce({
|
|
240
|
+
ok: true,
|
|
241
|
+
json: async () => ({ token: "jwt-token-2" }),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const manager = new SaasAuthManager(config);
|
|
245
|
+
|
|
246
|
+
// Cache token for job
|
|
247
|
+
await manager.attachToken({}, "job-1");
|
|
248
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
249
|
+
|
|
250
|
+
// Clear token
|
|
251
|
+
manager.clearToken("job-1");
|
|
252
|
+
|
|
253
|
+
// Next call should fetch new token
|
|
254
|
+
await manager.attachToken({}, "job-1");
|
|
255
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("clearAllTokens", () => {
|
|
260
|
+
it("should clear all cached tokens", async () => {
|
|
261
|
+
const config: SaasAuthConfig = {
|
|
262
|
+
mode: "saas",
|
|
263
|
+
authServerUrl: "https://auth.example.com/token",
|
|
264
|
+
getCredentials: () => ({ username: "user", password: "pass" }),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
vi.mocked(global.fetch)
|
|
268
|
+
.mockResolvedValueOnce({
|
|
269
|
+
ok: true,
|
|
270
|
+
json: async () => ({ token: "jwt-token-1" }),
|
|
271
|
+
})
|
|
272
|
+
.mockResolvedValueOnce({
|
|
273
|
+
ok: true,
|
|
274
|
+
json: async () => ({ token: "jwt-token-2" }),
|
|
275
|
+
})
|
|
276
|
+
.mockResolvedValueOnce({
|
|
277
|
+
ok: true,
|
|
278
|
+
json: async () => ({ token: "jwt-token-3" }),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const manager = new SaasAuthManager(config);
|
|
282
|
+
|
|
283
|
+
// Cache tokens for multiple jobs
|
|
284
|
+
await manager.attachToken({}, "job-1");
|
|
285
|
+
await manager.attachToken(); // Global token
|
|
286
|
+
|
|
287
|
+
// Clear all
|
|
288
|
+
manager.clearAllTokens();
|
|
289
|
+
|
|
290
|
+
// Next calls should fetch new tokens
|
|
291
|
+
await manager.attachToken();
|
|
292
|
+
expect(global.fetch).toHaveBeenCalledTimes(3);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("getCacheStats", () => {
|
|
297
|
+
it("should return cache statistics", async () => {
|
|
298
|
+
const config: SaasAuthConfig = {
|
|
299
|
+
mode: "saas",
|
|
300
|
+
authServerUrl: "https://auth.example.com/token",
|
|
301
|
+
getCredentials: () => ({ username: "user", password: "pass" }),
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
vi.mocked(global.fetch)
|
|
305
|
+
.mockResolvedValueOnce({
|
|
306
|
+
ok: true,
|
|
307
|
+
json: async () => ({ token: "jwt-token-1" }),
|
|
308
|
+
})
|
|
309
|
+
.mockResolvedValueOnce({
|
|
310
|
+
ok: true,
|
|
311
|
+
json: async () => ({ token: "jwt-token-2" }),
|
|
312
|
+
})
|
|
313
|
+
.mockResolvedValueOnce({
|
|
314
|
+
ok: true,
|
|
315
|
+
json: async () => ({ token: "jwt-token-3" }),
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const manager = new SaasAuthManager(config);
|
|
319
|
+
|
|
320
|
+
// Initially empty
|
|
321
|
+
expect(manager.getCacheStats()).toEqual({
|
|
322
|
+
cachedJobCount: 0,
|
|
323
|
+
hasGlobalToken: false,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Cache tokens
|
|
327
|
+
await manager.attachToken({}, "job-1");
|
|
328
|
+
await manager.attachToken({}, "job-2");
|
|
329
|
+
await manager.attachToken(); // Global
|
|
330
|
+
|
|
331
|
+
expect(manager.getCacheStats()).toEqual({
|
|
332
|
+
cachedJobCount: 2,
|
|
333
|
+
hasGlobalToken: true,
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HttpClient,
|
|
3
|
+
HttpRequestOptions,
|
|
4
|
+
HttpResponse,
|
|
5
|
+
} from "../services/http-client";
|
|
6
|
+
import type { DirectAuthManager } from "./direct-auth";
|
|
7
|
+
import type { NoAuthManager } from "./no-auth";
|
|
8
|
+
import type { SaasAuthManager } from "./saas-auth";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Union type of all auth managers
|
|
12
|
+
*/
|
|
13
|
+
export type AuthManager = DirectAuthManager | SaasAuthManager | NoAuthManager;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Auth-aware HTTP client wrapper.
|
|
17
|
+
*
|
|
18
|
+
* Wraps a standard HttpClient and automatically attaches authentication
|
|
19
|
+
* credentials/tokens to all HTTP requests based on the configured auth manager.
|
|
20
|
+
*
|
|
21
|
+
* The wrapper delegates all non-auth concerns (connection pooling, metrics, etc.)
|
|
22
|
+
* to the underlying HttpClient and only adds the auth layer on top.
|
|
23
|
+
*/
|
|
24
|
+
export class AuthHttpClient implements HttpClient {
|
|
25
|
+
constructor(
|
|
26
|
+
private httpClient: HttpClient,
|
|
27
|
+
private authManager: AuthManager,
|
|
28
|
+
) {}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Make an HTTP request with authentication credentials attached.
|
|
32
|
+
* Calls the auth manager to attach credentials before delegating to the underlying client.
|
|
33
|
+
*/
|
|
34
|
+
async request(
|
|
35
|
+
url: string,
|
|
36
|
+
options: HttpRequestOptions = {},
|
|
37
|
+
): Promise<HttpResponse> {
|
|
38
|
+
try {
|
|
39
|
+
// Attach auth credentials to request headers
|
|
40
|
+
const authenticatedHeaders = await this.attachAuthCredentials(
|
|
41
|
+
options.headers || {},
|
|
42
|
+
url,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Delegate to underlying HTTP client with authenticated headers
|
|
46
|
+
return await this.httpClient.request(url, {
|
|
47
|
+
...options,
|
|
48
|
+
headers: authenticatedHeaders,
|
|
49
|
+
// include credentials for cors if needed
|
|
50
|
+
credentials:
|
|
51
|
+
this.authManager.getType() === "no-auth" ||
|
|
52
|
+
this.authManager.getType() === "saas"
|
|
53
|
+
? "omit"
|
|
54
|
+
: (options.credentials ?? "include"),
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// If auth fails, wrap error with context
|
|
58
|
+
if (error instanceof Error && error.message.includes("auth")) {
|
|
59
|
+
throw error; // Re-throw auth errors as-is
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// For other errors, let them propagate
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Attach authentication credentials to request headers.
|
|
69
|
+
* Delegates to the appropriate auth manager method.
|
|
70
|
+
*/
|
|
71
|
+
private async attachAuthCredentials(
|
|
72
|
+
headers: Record<string, string>,
|
|
73
|
+
url: string,
|
|
74
|
+
): Promise<Record<string, string>> {
|
|
75
|
+
// Check if this is a DirectAuthManager or SaasAuthManager
|
|
76
|
+
if ("attachCredentials" in this.authManager) {
|
|
77
|
+
// DirectAuthManager or NoAuthManager
|
|
78
|
+
return await this.authManager.attachCredentials(headers);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if ("attachToken" in this.authManager) {
|
|
82
|
+
// SaasAuthManager - extract job ID from URL if present
|
|
83
|
+
const jobId = this.extractJobIdFromUrl(url);
|
|
84
|
+
return await this.authManager.attachToken(headers, jobId);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Fallback - return headers unchanged
|
|
88
|
+
return headers;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extract job ID from URL for SaaS mode token caching.
|
|
93
|
+
* Looks for patterns like /upload/{id} or /jobs/{id} in the URL.
|
|
94
|
+
*/
|
|
95
|
+
private extractJobIdFromUrl(url: string): string | undefined {
|
|
96
|
+
// Match patterns like:
|
|
97
|
+
// - /api/upload/{uploadId}
|
|
98
|
+
// - /api/flow/{flowId}/{storageId}
|
|
99
|
+
// - /api/jobs/{jobId}/status
|
|
100
|
+
// - /api/jobs/{jobId}/continue/{nodeId}
|
|
101
|
+
|
|
102
|
+
const uploadMatch = url.match(/\/api\/upload\/([^/?]+)/);
|
|
103
|
+
if (uploadMatch) {
|
|
104
|
+
return uploadMatch[1];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const flowMatch = url.match(/\/api\/flow\/([^/?]+)/);
|
|
108
|
+
if (flowMatch) {
|
|
109
|
+
return flowMatch[1];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const jobMatch = url.match(/\/api\/jobs\/([^/?]+)/);
|
|
113
|
+
if (jobMatch) {
|
|
114
|
+
return jobMatch[1];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// No job ID found - SaaS mode will use global token
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Delegate metrics methods to underlying HTTP client
|
|
123
|
+
*/
|
|
124
|
+
getMetrics() {
|
|
125
|
+
return this.httpClient.getMetrics();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
getDetailedMetrics() {
|
|
129
|
+
return this.httpClient.getDetailedMetrics();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
reset() {
|
|
133
|
+
this.httpClient.reset();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async close() {
|
|
137
|
+
await this.httpClient.close();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async warmupConnections(urls: string[]) {
|
|
141
|
+
await this.httpClient.warmupConnections(urls);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get the underlying auth manager for advanced use cases
|
|
146
|
+
*/
|
|
147
|
+
getAuthManager(): AuthManager {
|
|
148
|
+
return this.authManager;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Logger } from "../logger";
|
|
2
|
+
import type { PlatformService } from "../services/platform-service";
|
|
3
|
+
import type { DirectAuthConfig } from "./types";
|
|
4
|
+
import { BaseAuthManager } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Direct auth manager - handles credential attachment for "bring your own auth" mode.
|
|
8
|
+
*
|
|
9
|
+
* This manager calls the user-provided getCredentials() function before each request
|
|
10
|
+
* and attaches the returned credentials (headers, cookies) to the HTTP request.
|
|
11
|
+
*
|
|
12
|
+
* Supports any authentication protocol: OAuth, JWT, API keys, session cookies, etc.
|
|
13
|
+
*/
|
|
14
|
+
export class DirectAuthManager extends BaseAuthManager {
|
|
15
|
+
constructor(
|
|
16
|
+
private config: DirectAuthConfig,
|
|
17
|
+
private platformService: PlatformService,
|
|
18
|
+
private logger: Logger,
|
|
19
|
+
) {
|
|
20
|
+
super("direct");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Attach credentials to an HTTP request by calling getCredentials() and
|
|
25
|
+
* merging the returned headers/cookies with the request.
|
|
26
|
+
*
|
|
27
|
+
* @param headers - Existing request headers
|
|
28
|
+
* @returns Updated headers with credentials attached
|
|
29
|
+
* @throws Error if getCredentials() throws or returns invalid data
|
|
30
|
+
*/
|
|
31
|
+
async attachCredentials(
|
|
32
|
+
headers: Record<string, string> = {},
|
|
33
|
+
): Promise<Record<string, string>> {
|
|
34
|
+
try {
|
|
35
|
+
if (!this.config.getCredentials) {
|
|
36
|
+
return headers;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Call user's credential provider (may be async)
|
|
40
|
+
const credentials = await Promise.resolve(this.config.getCredentials());
|
|
41
|
+
|
|
42
|
+
// Validate credentials
|
|
43
|
+
if (!credentials || typeof credentials !== "object") {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"getCredentials() must return an object with headers and/or cookies",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Merge credential headers with existing headers
|
|
50
|
+
const updatedHeaders = { ...headers };
|
|
51
|
+
|
|
52
|
+
if (credentials.headers) {
|
|
53
|
+
this.validateHeaders(credentials.headers);
|
|
54
|
+
Object.assign(updatedHeaders, credentials.headers);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Note: Cookie handling would be browser-specific
|
|
58
|
+
// For now, we only support headers as cookies are automatically
|
|
59
|
+
// handled by the browser when using fetch()
|
|
60
|
+
if (credentials.cookies) {
|
|
61
|
+
this.attachCookies(updatedHeaders, credentials.cookies);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return updatedHeaders;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Wrap errors with context
|
|
67
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
68
|
+
throw new Error(`Failed to attach auth credentials: ${message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validate that headers is a valid object with string keys and values
|
|
74
|
+
*/
|
|
75
|
+
private validateHeaders(headers: Record<string, string>): void {
|
|
76
|
+
if (typeof headers !== "object" || headers === null) {
|
|
77
|
+
throw new Error("headers must be an object");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
81
|
+
if (typeof key !== "string" || typeof value !== "string") {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Invalid header: key and value must be strings (got ${key}: ${typeof value})`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Attach cookies to request headers.
|
|
91
|
+
* In browser environments, cookies are automatically handled by fetch().
|
|
92
|
+
* In Node.js, we need to manually add them to the Cookie header.
|
|
93
|
+
*/
|
|
94
|
+
private attachCookies(
|
|
95
|
+
headers: Record<string, string>,
|
|
96
|
+
cookies: Record<string, string>,
|
|
97
|
+
): void {
|
|
98
|
+
// Check if we're in a browser environment
|
|
99
|
+
const isBrowser = this.platformService.isBrowser();
|
|
100
|
+
|
|
101
|
+
if (isBrowser) {
|
|
102
|
+
// In browsers, fetch() automatically sends cookies for same-origin requests
|
|
103
|
+
// For cross-origin, the server needs to set CORS headers and credentials: 'include'
|
|
104
|
+
// We can't manually set cookies in headers for security reasons
|
|
105
|
+
// So we just warn if cookies are provided in direct mode
|
|
106
|
+
this.logger.warn(
|
|
107
|
+
"DirectAuth: Cookies are automatically handled by the browser. " +
|
|
108
|
+
"Ensure your server has proper CORS configuration with credentials support.",
|
|
109
|
+
);
|
|
110
|
+
} else {
|
|
111
|
+
// In Node.js, we can manually build the Cookie header
|
|
112
|
+
const cookieString = Object.entries(cookies)
|
|
113
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
114
|
+
.join("; ");
|
|
115
|
+
|
|
116
|
+
if (cookieString) {
|
|
117
|
+
headers.Cookie = cookieString;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { BaseAuthManager } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* No-auth manager - pass-through implementation for backward compatibility.
|
|
5
|
+
*
|
|
6
|
+
* When no auth configuration is provided, this manager is used to maintain
|
|
7
|
+
* a consistent interface without adding any authentication to requests.
|
|
8
|
+
*/
|
|
9
|
+
export class NoAuthManager extends BaseAuthManager {
|
|
10
|
+
constructor() {
|
|
11
|
+
super("no-auth");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Pass through headers without modification.
|
|
16
|
+
*
|
|
17
|
+
* @param headers - Existing request headers
|
|
18
|
+
* @returns Same headers unchanged
|
|
19
|
+
*/
|
|
20
|
+
async attachCredentials(
|
|
21
|
+
headers: Record<string, string> = {},
|
|
22
|
+
): Promise<Record<string, string>> {
|
|
23
|
+
return headers;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* No-op for clearing tokens (NoAuthManager doesn't cache anything)
|
|
28
|
+
*/
|
|
29
|
+
clearToken(_jobId: string): void {
|
|
30
|
+
// No-op
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* No-op for clearing all tokens
|
|
35
|
+
*/
|
|
36
|
+
clearAllTokens(): void {
|
|
37
|
+
// No-op
|
|
38
|
+
}
|
|
39
|
+
}
|