@palantir/pack.auth.foundry 0.0.1-beta.1
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-lint.log +20 -0
- package/.turbo/turbo-transpileBrowser.log +5 -0
- package/.turbo/turbo-transpileCjs.log +5 -0
- package/.turbo/turbo-transpileEsm.log +5 -0
- package/.turbo/turbo-transpileTypes.log +5 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE.txt +13 -0
- package/README.md +159 -0
- package/build/browser/index.js +374 -0
- package/build/browser/index.js.map +1 -0
- package/build/cjs/index.cjs +381 -0
- package/build/cjs/index.cjs.map +1 -0
- package/build/cjs/index.d.cts +90 -0
- package/build/esm/index.js +374 -0
- package/build/esm/index.js.map +1 -0
- package/build/types/AuthModuleConfig.d.ts +1 -0
- package/build/types/AuthModuleConfig.d.ts.map +1 -0
- package/build/types/__tests__/AuthServiceConfidential.test.d.ts +1 -0
- package/build/types/__tests__/AuthServiceConfidential.test.d.ts.map +1 -0
- package/build/types/__tests__/AuthServicePublic.test.d.ts +1 -0
- package/build/types/__tests__/AuthServicePublic.test.d.ts.map +1 -0
- package/build/types/__tests__/AuthServiceStatic.test.d.ts +1 -0
- package/build/types/__tests__/AuthServiceStatic.test.d.ts.map +1 -0
- package/build/types/auth/AuthServiceBase.d.ts +32 -0
- package/build/types/auth/AuthServiceBase.d.ts.map +1 -0
- package/build/types/auth/AuthServiceConfidential.d.ts +20 -0
- package/build/types/auth/AuthServiceConfidential.d.ts.map +1 -0
- package/build/types/auth/AuthServicePublic.d.ts +23 -0
- package/build/types/auth/AuthServicePublic.d.ts.map +1 -0
- package/build/types/auth/AuthServiceStatic.d.ts +17 -0
- package/build/types/auth/AuthServiceStatic.d.ts.map +1 -0
- package/build/types/index.d.ts +4 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/utils/jwtUtils.d.ts +28 -0
- package/build/types/utils/jwtUtils.d.ts.map +1 -0
- package/package.json +74 -0
- package/src/AuthModuleConfig.ts +17 -0
- package/src/__tests__/AuthServiceConfidential.test.ts +407 -0
- package/src/__tests__/AuthServicePublic.test.ts +413 -0
- package/src/__tests__/AuthServiceStatic.test.ts +341 -0
- package/src/auth/AuthServiceBase.ts +201 -0
- package/src/auth/AuthServiceConfidential.ts +96 -0
- package/src/auth/AuthServicePublic.ts +124 -0
- package/src/auth/AuthServiceStatic.ts +91 -0
- package/src/index.ts +20 -0
- package/src/utils/jwtUtils.ts +127 -0
- package/tsconfig.json +21 -0
- package/vitest.config.mjs +26 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Palantir Technologies, Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Logger } from "@osdk/api";
|
|
18
|
+
import type { Client } from "@osdk/client";
|
|
19
|
+
import type { PublicOauthClient } from "@osdk/oauth";
|
|
20
|
+
import { AuthState, type AuthStateChangeEvent, type BaseAuthService } from "@palantir/pack.auth";
|
|
21
|
+
import type { PackAppInternal } from "@palantir/pack.core";
|
|
22
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
23
|
+
import type { MockProxy } from "vitest-mock-extended";
|
|
24
|
+
import { mock } from "vitest-mock-extended";
|
|
25
|
+
import { createPublicOauthService } from "../auth/AuthServicePublic.js";
|
|
26
|
+
|
|
27
|
+
describe("PublicOauthService", () => {
|
|
28
|
+
let mockOauthClient: PublicOauthClient;
|
|
29
|
+
let mockApp: PackAppInternal;
|
|
30
|
+
let mockLogger: MockProxy<Logger>;
|
|
31
|
+
let service: BaseAuthService;
|
|
32
|
+
let eventListeners: Record<string, (() => void)[]>;
|
|
33
|
+
|
|
34
|
+
// Store references to mocks for easy access in tests
|
|
35
|
+
let mockGetTokenOrUndefined: ReturnType<typeof vi.fn>;
|
|
36
|
+
let mockSignIn: ReturnType<typeof vi.fn>;
|
|
37
|
+
let mockSignOut: ReturnType<typeof vi.fn>;
|
|
38
|
+
let mockRefresh: ReturnType<typeof vi.fn>;
|
|
39
|
+
let mockAddEventListener: ReturnType<typeof vi.fn>;
|
|
40
|
+
let mockRemoveEventListener: ReturnType<typeof vi.fn>;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
eventListeners = {};
|
|
44
|
+
|
|
45
|
+
mockLogger = mock<Logger>();
|
|
46
|
+
mockLogger.child.mockReturnValue(mockLogger);
|
|
47
|
+
|
|
48
|
+
const mockOsdkClient = mock<Client>();
|
|
49
|
+
|
|
50
|
+
mockApp = {
|
|
51
|
+
config: {
|
|
52
|
+
app: { appId: "test-app" },
|
|
53
|
+
isTestMode: false,
|
|
54
|
+
logger: mockLogger,
|
|
55
|
+
osdkClient: mockOsdkClient,
|
|
56
|
+
remote: {
|
|
57
|
+
packWsPath: "/pack/ws",
|
|
58
|
+
baseUrl: "https://test.example.com",
|
|
59
|
+
fetchFn: globalThis.fetch,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
getModule: vi.fn(),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const tokenProvider = vi.fn().mockResolvedValue("oauth-token");
|
|
66
|
+
mockGetTokenOrUndefined = vi.fn().mockReturnValue(undefined);
|
|
67
|
+
mockSignIn = vi.fn().mockResolvedValue({
|
|
68
|
+
access_token: "test-token",
|
|
69
|
+
expires_in: 3600,
|
|
70
|
+
expires_at: Date.now() + 3600000,
|
|
71
|
+
});
|
|
72
|
+
mockSignOut = vi.fn().mockResolvedValue(undefined);
|
|
73
|
+
mockRefresh = vi.fn().mockResolvedValue({
|
|
74
|
+
access_token: "refreshed-token",
|
|
75
|
+
expires_in: 3600,
|
|
76
|
+
expires_at: Date.now() + 3600000,
|
|
77
|
+
});
|
|
78
|
+
mockAddEventListener = vi.fn().mockImplementation(
|
|
79
|
+
(event: string, listener: (evt: unknown) => void) => {
|
|
80
|
+
if (!eventListeners[event]) {
|
|
81
|
+
eventListeners[event] = [];
|
|
82
|
+
}
|
|
83
|
+
eventListeners[event].push(() => {
|
|
84
|
+
listener({});
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
mockRemoveEventListener = vi.fn().mockImplementation(
|
|
89
|
+
(event: string, listener: (evt: unknown) => void) => {
|
|
90
|
+
if (eventListeners[event]) {
|
|
91
|
+
const index = eventListeners[event].findIndex(storedListener =>
|
|
92
|
+
storedListener.toString() === listener.toString()
|
|
93
|
+
);
|
|
94
|
+
if (index > -1) {
|
|
95
|
+
eventListeners[event].splice(index, 1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
mockOauthClient = Object.assign(tokenProvider, {
|
|
102
|
+
getTokenOrUndefined: mockGetTokenOrUndefined,
|
|
103
|
+
signIn: mockSignIn,
|
|
104
|
+
signOut: mockSignOut,
|
|
105
|
+
refresh: mockRefresh,
|
|
106
|
+
addEventListener: mockAddEventListener,
|
|
107
|
+
removeEventListener: mockRemoveEventListener,
|
|
108
|
+
}) as PublicOauthClient;
|
|
109
|
+
|
|
110
|
+
service = createPublicOauthService(mockApp, mockOauthClient);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Helper to trigger OAuth events
|
|
114
|
+
const triggerEvent = (eventType: string) => {
|
|
115
|
+
if (eventListeners[eventType]) {
|
|
116
|
+
eventListeners[eventType].forEach(listener => {
|
|
117
|
+
listener();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
describe("initialization", () => {
|
|
123
|
+
it("should start in initializing state", () => {
|
|
124
|
+
expect(service.getCurrentState()).toBe(AuthState.Initializing);
|
|
125
|
+
expect(service.isAuthenticated()).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should start in initializing state even if token exists", () => {
|
|
129
|
+
mockGetTokenOrUndefined.mockReturnValue("existing-token");
|
|
130
|
+
|
|
131
|
+
const authenticatedService = createPublicOauthService(mockApp, mockOauthClient);
|
|
132
|
+
expect(authenticatedService.getCurrentState()).toBe(AuthState.Initializing);
|
|
133
|
+
expect(authenticatedService.isAuthenticated()).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should setup event listeners", () => {
|
|
137
|
+
expect(mockAddEventListener).toHaveBeenCalledWith("signIn", expect.any(Function));
|
|
138
|
+
expect(mockAddEventListener).toHaveBeenCalledWith(
|
|
139
|
+
"signOut",
|
|
140
|
+
expect.any(Function),
|
|
141
|
+
);
|
|
142
|
+
expect(mockAddEventListener).toHaveBeenCalledWith(
|
|
143
|
+
"refresh",
|
|
144
|
+
expect.any(Function),
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("getToken method", () => {
|
|
150
|
+
it("should return token from OAuth client", async () => {
|
|
151
|
+
const token = await service.getToken();
|
|
152
|
+
expect(token).toBe("oauth-token");
|
|
153
|
+
expect(mockOauthClient).toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("authentication methods", () => {
|
|
158
|
+
it("should delegate to OAuth client for signIn", async () => {
|
|
159
|
+
await service.signIn();
|
|
160
|
+
expect(mockSignIn).toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should delegate to OAuth client for signOut", async () => {
|
|
164
|
+
await service.signOut();
|
|
165
|
+
expect(mockSignOut).toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should delegate to OAuth client for refresh", async () => {
|
|
169
|
+
await service.refresh!();
|
|
170
|
+
expect(mockRefresh).toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should delegate to OAuth client for getTokenOrUndefined", () => {
|
|
174
|
+
mockGetTokenOrUndefined.mockReturnValue("test-token");
|
|
175
|
+
|
|
176
|
+
expect(service.getTokenOrUndefined()).toBe("test-token");
|
|
177
|
+
expect(mockGetTokenOrUndefined).toHaveBeenCalled();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should delegate to OAuth client for isAuthenticated", () => {
|
|
181
|
+
mockGetTokenOrUndefined.mockReturnValue("test-token");
|
|
182
|
+
|
|
183
|
+
expect(service.isAuthenticated()).toBe(true);
|
|
184
|
+
|
|
185
|
+
mockGetTokenOrUndefined.mockReturnValue(undefined);
|
|
186
|
+
expect(service.isAuthenticated()).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("state management", () => {
|
|
191
|
+
it("should update state on signIn event", async () => {
|
|
192
|
+
const callback = vi.fn();
|
|
193
|
+
service.onAuthStateChange(callback);
|
|
194
|
+
|
|
195
|
+
// First call is immediate callback with Initializing state
|
|
196
|
+
expect(callback).toHaveBeenNthCalledWith(1, {
|
|
197
|
+
state: AuthState.Initializing,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Wait for signIn to be called (lazy initialization)
|
|
201
|
+
await vi.waitFor(() => {
|
|
202
|
+
expect(mockSignIn).toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Then authenticating state
|
|
206
|
+
expect(callback).toHaveBeenNthCalledWith(2, {
|
|
207
|
+
previousState: AuthState.Initializing,
|
|
208
|
+
state: AuthState.Authenticating,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
triggerEvent("signIn");
|
|
212
|
+
|
|
213
|
+
expect(service.getCurrentState()).toBe(AuthState.Authenticated);
|
|
214
|
+
expect(callback).toHaveBeenCalledWith({
|
|
215
|
+
previousState: AuthState.Authenticating,
|
|
216
|
+
state: AuthState.Authenticated,
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("should update state on signOut event", () => {
|
|
221
|
+
// Start authenticated
|
|
222
|
+
triggerEvent("signIn");
|
|
223
|
+
const callback = vi.fn();
|
|
224
|
+
service.onAuthStateChange(callback);
|
|
225
|
+
|
|
226
|
+
// First call is immediate callback with current state
|
|
227
|
+
expect(callback).toHaveBeenNthCalledWith(1, {
|
|
228
|
+
state: AuthState.Authenticated,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
triggerEvent("signOut");
|
|
232
|
+
|
|
233
|
+
expect(service.getCurrentState()).toBe(AuthState.Unauthenticated);
|
|
234
|
+
expect(callback).toHaveBeenNthCalledWith(2, {
|
|
235
|
+
previousState: AuthState.Authenticated,
|
|
236
|
+
state: AuthState.Unauthenticated,
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("should update state on refresh event", async () => {
|
|
241
|
+
const callback = vi.fn();
|
|
242
|
+
service.onAuthStateChange(callback);
|
|
243
|
+
|
|
244
|
+
// First call is immediate callback with Initializing state
|
|
245
|
+
expect(callback).toHaveBeenNthCalledWith(1, {
|
|
246
|
+
state: AuthState.Initializing,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Wait for signIn to be called (lazy initialization)
|
|
250
|
+
await vi.waitFor(() => {
|
|
251
|
+
expect(mockSignIn).toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
triggerEvent("refresh");
|
|
255
|
+
|
|
256
|
+
expect(service.getCurrentState()).toBe(AuthState.Authenticated);
|
|
257
|
+
expect(callback).toHaveBeenCalledWith({
|
|
258
|
+
previousState: AuthState.Authenticating,
|
|
259
|
+
state: AuthState.Authenticated,
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("should update state to authenticating during signIn", async () => {
|
|
264
|
+
const callback = vi.fn();
|
|
265
|
+
service.onAuthStateChange(callback);
|
|
266
|
+
|
|
267
|
+
// First call is immediate callback
|
|
268
|
+
expect(callback).toHaveBeenNthCalledWith(1, {
|
|
269
|
+
state: AuthState.Initializing,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Wait for lazy initialization signIn
|
|
273
|
+
await vi.waitFor(() => {
|
|
274
|
+
expect(mockSignIn).toHaveBeenCalled();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Should be in Authenticating state
|
|
278
|
+
expect(callback).toHaveBeenCalledWith({
|
|
279
|
+
previousState: AuthState.Initializing,
|
|
280
|
+
state: AuthState.Authenticating,
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("should update state to error on signIn failure", async () => {
|
|
285
|
+
const error = new Error("Sign in failed");
|
|
286
|
+
mockSignIn.mockRejectedValue(error);
|
|
287
|
+
|
|
288
|
+
const callback = vi.fn();
|
|
289
|
+
service.onAuthStateChange(callback);
|
|
290
|
+
|
|
291
|
+
// First call is immediate callback
|
|
292
|
+
expect(callback).toHaveBeenNthCalledWith(1, {
|
|
293
|
+
state: AuthState.Initializing,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Wait for lazy initialization to fail
|
|
297
|
+
await vi.waitFor(() => {
|
|
298
|
+
expect(callback).toHaveBeenCalledWith(
|
|
299
|
+
expect.objectContaining({
|
|
300
|
+
state: AuthState.Error,
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(callback).toHaveBeenCalledWith({
|
|
306
|
+
previousState: AuthState.Authenticating,
|
|
307
|
+
state: AuthState.Error,
|
|
308
|
+
error,
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("should handle multiple state change listeners", () => {
|
|
313
|
+
const callback1 = vi.fn();
|
|
314
|
+
const callback2 = vi.fn();
|
|
315
|
+
|
|
316
|
+
service.onAuthStateChange(callback1);
|
|
317
|
+
service.onAuthStateChange(callback2);
|
|
318
|
+
|
|
319
|
+
triggerEvent("signIn");
|
|
320
|
+
|
|
321
|
+
expect(callback1).toHaveBeenCalled();
|
|
322
|
+
expect(callback2).toHaveBeenCalled();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("should allow unsubscribing from state changes", () => {
|
|
326
|
+
const callback = vi.fn();
|
|
327
|
+
const unsubscribe = service.onAuthStateChange(callback);
|
|
328
|
+
|
|
329
|
+
// Clear the initial callback that fires immediately
|
|
330
|
+
callback.mockClear();
|
|
331
|
+
|
|
332
|
+
unsubscribe();
|
|
333
|
+
triggerEvent("signIn");
|
|
334
|
+
|
|
335
|
+
expect(callback).not.toHaveBeenCalled();
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("error handling", () => {
|
|
340
|
+
it("should handle errors in state change callbacks", () => {
|
|
341
|
+
const errorCallback = vi.fn().mockImplementation(() => {
|
|
342
|
+
throw new Error("Callback error");
|
|
343
|
+
});
|
|
344
|
+
const goodCallback = vi.fn();
|
|
345
|
+
|
|
346
|
+
// Add callbacks - errors should be handled gracefully
|
|
347
|
+
service.onAuthStateChange(errorCallback);
|
|
348
|
+
service.onAuthStateChange(goodCallback);
|
|
349
|
+
|
|
350
|
+
// Clear initial calls to test the actual event-triggered behavior
|
|
351
|
+
errorCallback.mockClear();
|
|
352
|
+
goodCallback.mockClear();
|
|
353
|
+
mockLogger.error.mockClear();
|
|
354
|
+
|
|
355
|
+
triggerEvent("signIn");
|
|
356
|
+
|
|
357
|
+
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
358
|
+
"Error in auth state callback:",
|
|
359
|
+
expect.any(Error),
|
|
360
|
+
);
|
|
361
|
+
expect(goodCallback).toHaveBeenCalled(); // Other callbacks should still work
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("should convert non-Error objects to Error in error state", async () => {
|
|
365
|
+
mockSignIn.mockRejectedValue("string error");
|
|
366
|
+
|
|
367
|
+
const callback = vi.fn();
|
|
368
|
+
service.onAuthStateChange(callback);
|
|
369
|
+
|
|
370
|
+
await expect(service.signIn()).rejects.toThrow("string error");
|
|
371
|
+
|
|
372
|
+
expect(callback).toHaveBeenCalledWith({
|
|
373
|
+
previousState: AuthState.Authenticating,
|
|
374
|
+
state: AuthState.Error,
|
|
375
|
+
error: expect.objectContaining({
|
|
376
|
+
message: "string error",
|
|
377
|
+
}) as Error,
|
|
378
|
+
} as AuthStateChangeEvent);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe("cleanup", () => {
|
|
383
|
+
it("should remove event listeners on dispose", () => {
|
|
384
|
+
service.dispose();
|
|
385
|
+
|
|
386
|
+
expect(mockRemoveEventListener).toHaveBeenCalledWith(
|
|
387
|
+
"signIn",
|
|
388
|
+
expect.any(Function),
|
|
389
|
+
);
|
|
390
|
+
expect(mockRemoveEventListener).toHaveBeenCalledWith(
|
|
391
|
+
"signOut",
|
|
392
|
+
expect.any(Function),
|
|
393
|
+
);
|
|
394
|
+
expect(mockRemoveEventListener).toHaveBeenCalledWith(
|
|
395
|
+
"refresh",
|
|
396
|
+
expect.any(Function),
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("should clear state callbacks on dispose", () => {
|
|
401
|
+
const callback = vi.fn();
|
|
402
|
+
service.onAuthStateChange(callback);
|
|
403
|
+
|
|
404
|
+
// Clear the initial callback that fires immediately
|
|
405
|
+
callback.mockClear();
|
|
406
|
+
|
|
407
|
+
service.dispose();
|
|
408
|
+
triggerEvent("signIn");
|
|
409
|
+
|
|
410
|
+
expect(callback).not.toHaveBeenCalled();
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
});
|