@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.
Files changed (48) hide show
  1. package/.turbo/turbo-lint.log +20 -0
  2. package/.turbo/turbo-transpileBrowser.log +5 -0
  3. package/.turbo/turbo-transpileCjs.log +5 -0
  4. package/.turbo/turbo-transpileEsm.log +5 -0
  5. package/.turbo/turbo-transpileTypes.log +5 -0
  6. package/.turbo/turbo-typecheck.log +4 -0
  7. package/LICENSE.txt +13 -0
  8. package/README.md +159 -0
  9. package/build/browser/index.js +374 -0
  10. package/build/browser/index.js.map +1 -0
  11. package/build/cjs/index.cjs +381 -0
  12. package/build/cjs/index.cjs.map +1 -0
  13. package/build/cjs/index.d.cts +90 -0
  14. package/build/esm/index.js +374 -0
  15. package/build/esm/index.js.map +1 -0
  16. package/build/types/AuthModuleConfig.d.ts +1 -0
  17. package/build/types/AuthModuleConfig.d.ts.map +1 -0
  18. package/build/types/__tests__/AuthServiceConfidential.test.d.ts +1 -0
  19. package/build/types/__tests__/AuthServiceConfidential.test.d.ts.map +1 -0
  20. package/build/types/__tests__/AuthServicePublic.test.d.ts +1 -0
  21. package/build/types/__tests__/AuthServicePublic.test.d.ts.map +1 -0
  22. package/build/types/__tests__/AuthServiceStatic.test.d.ts +1 -0
  23. package/build/types/__tests__/AuthServiceStatic.test.d.ts.map +1 -0
  24. package/build/types/auth/AuthServiceBase.d.ts +32 -0
  25. package/build/types/auth/AuthServiceBase.d.ts.map +1 -0
  26. package/build/types/auth/AuthServiceConfidential.d.ts +20 -0
  27. package/build/types/auth/AuthServiceConfidential.d.ts.map +1 -0
  28. package/build/types/auth/AuthServicePublic.d.ts +23 -0
  29. package/build/types/auth/AuthServicePublic.d.ts.map +1 -0
  30. package/build/types/auth/AuthServiceStatic.d.ts +17 -0
  31. package/build/types/auth/AuthServiceStatic.d.ts.map +1 -0
  32. package/build/types/index.d.ts +4 -0
  33. package/build/types/index.d.ts.map +1 -0
  34. package/build/types/utils/jwtUtils.d.ts +28 -0
  35. package/build/types/utils/jwtUtils.d.ts.map +1 -0
  36. package/package.json +74 -0
  37. package/src/AuthModuleConfig.ts +17 -0
  38. package/src/__tests__/AuthServiceConfidential.test.ts +407 -0
  39. package/src/__tests__/AuthServicePublic.test.ts +413 -0
  40. package/src/__tests__/AuthServiceStatic.test.ts +341 -0
  41. package/src/auth/AuthServiceBase.ts +201 -0
  42. package/src/auth/AuthServiceConfidential.ts +96 -0
  43. package/src/auth/AuthServicePublic.ts +124 -0
  44. package/src/auth/AuthServiceStatic.ts +91 -0
  45. package/src/index.ts +20 -0
  46. package/src/utils/jwtUtils.ts +127 -0
  47. package/tsconfig.json +21 -0
  48. 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
+ });