@pikku/core 0.12.2 → 0.12.4

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 (282) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/crypto-utils.d.ts +10 -0
  3. package/dist/crypto-utils.js +62 -0
  4. package/dist/function/function-runner.js +4 -4
  5. package/dist/function/functions.types.d.ts +26 -1
  6. package/dist/function/functions.types.js +16 -0
  7. package/dist/handle-error.d.ts +1 -1
  8. package/dist/handle-error.js +7 -3
  9. package/dist/index.d.ts +14 -2
  10. package/dist/index.js +1 -1
  11. package/dist/pikku-state.js +3 -1
  12. package/dist/schema.js +1 -1
  13. package/dist/services/in-memory-workflow-service.d.ts +22 -3
  14. package/dist/services/in-memory-workflow-service.js +77 -1
  15. package/dist/services/workflow-service.d.ts +8 -2
  16. package/dist/testing/index.d.ts +2 -0
  17. package/dist/testing/index.js +1 -0
  18. package/dist/testing/service-tests.d.ts +31 -0
  19. package/dist/testing/service-tests.js +598 -0
  20. package/dist/types/core.types.d.ts +9 -1
  21. package/dist/types/state.types.d.ts +4 -2
  22. package/dist/utils/hash.d.ts +2 -0
  23. package/dist/utils/hash.js +23 -0
  24. package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +6 -0
  25. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +348 -0
  26. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +1 -1
  27. package/dist/wirings/ai-agent/ai-agent-memory.js +6 -5
  28. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +10 -7
  29. package/dist/wirings/ai-agent/ai-agent-prepare.js +127 -47
  30. package/dist/wirings/ai-agent/ai-agent-runner.d.ts +4 -0
  31. package/dist/wirings/ai-agent/ai-agent-runner.js +381 -27
  32. package/dist/wirings/ai-agent/ai-agent-stream.d.ts +7 -2
  33. package/dist/wirings/ai-agent/ai-agent-stream.js +109 -46
  34. package/dist/wirings/ai-agent/ai-agent.types.d.ts +22 -1
  35. package/dist/wirings/ai-agent/index.d.ts +1 -2
  36. package/dist/wirings/ai-agent/index.js +1 -2
  37. package/dist/wirings/channel/channel-handler.js +5 -1
  38. package/dist/wirings/channel/channel.types.d.ts +1 -7
  39. package/dist/wirings/channel/local/local-channel-handler.d.ts +1 -1
  40. package/dist/wirings/channel/local/local-channel-handler.js +5 -3
  41. package/dist/wirings/channel/local/local-channel-runner.js +14 -4
  42. package/dist/wirings/channel/serverless/serverless-channel-runner.js +4 -1
  43. package/dist/wirings/cli/cli-runner.js +10 -2
  44. package/dist/wirings/cli/cli.types.d.ts +1 -0
  45. package/dist/wirings/http/http-routes.js +0 -1
  46. package/dist/wirings/http/http-runner.d.ts +1 -1
  47. package/dist/wirings/http/http-runner.js +25 -3
  48. package/dist/wirings/http/http.types.d.ts +3 -11
  49. package/dist/wirings/http/index.d.ts +2 -1
  50. package/dist/wirings/http/index.js +1 -0
  51. package/dist/wirings/http/pikku-fetch-http-request.d.ts +1 -0
  52. package/dist/wirings/http/pikku-fetch-http-request.js +3 -0
  53. package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
  54. package/dist/wirings/http/pikku-fetch-http-response.js +4 -4
  55. package/dist/wirings/http/web-request.d.ts +12 -0
  56. package/dist/wirings/http/web-request.js +85 -0
  57. package/dist/wirings/mcp/mcp-endpoint-registry.js +1 -1
  58. package/dist/wirings/mcp/mcp-runner.js +13 -1
  59. package/dist/wirings/rpc/rpc-runner.d.ts +30 -6
  60. package/dist/wirings/rpc/rpc-runner.js +36 -14
  61. package/dist/wirings/rpc/rpc-types.d.ts +6 -1
  62. package/dist/wirings/rpc/wire-addon.d.ts +1 -0
  63. package/dist/wirings/rpc/wire-addon.js +1 -1
  64. package/dist/wirings/scheduler/scheduler-runner.js +1 -0
  65. package/dist/wirings/workflow/graph/graph-validation.d.ts +3 -0
  66. package/dist/wirings/workflow/graph/graph-validation.js +175 -0
  67. package/dist/wirings/workflow/graph/index.d.ts +1 -0
  68. package/dist/wirings/workflow/graph/index.js +1 -0
  69. package/dist/wirings/workflow/index.d.ts +1 -1
  70. package/dist/wirings/workflow/pikku-workflow-service.d.ts +8 -2
  71. package/dist/wirings/workflow/pikku-workflow-service.js +1 -2
  72. package/dist/wirings/workflow/workflow.types.d.ts +10 -1
  73. package/lcov.info +18891 -0
  74. package/package.json +4 -2
  75. package/dist/wirings/ai-agent/ai-agent-assistant-ui.d.ts +0 -5
  76. package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +0 -168
  77. package/src/crypto-utils.test.ts +0 -116
  78. package/src/crypto-utils.ts +0 -99
  79. package/src/errors/error-handler.ts +0 -62
  80. package/src/errors/error.test.ts +0 -195
  81. package/src/errors/errors.ts +0 -374
  82. package/src/errors/index.ts +0 -2
  83. package/src/factory-functions.test.ts +0 -109
  84. package/src/function/function-runner.test.ts +0 -536
  85. package/src/function/function-runner.ts +0 -365
  86. package/src/function/functions.types.ts +0 -267
  87. package/src/function/index.ts +0 -5
  88. package/src/handle-error.test.ts +0 -361
  89. package/src/handle-error.ts +0 -65
  90. package/src/index.ts +0 -104
  91. package/src/internal.ts +0 -6
  92. package/src/middleware/auth-apikey.test.ts +0 -363
  93. package/src/middleware/auth-apikey.ts +0 -47
  94. package/src/middleware/auth-bearer.test.ts +0 -450
  95. package/src/middleware/auth-bearer.ts +0 -72
  96. package/src/middleware/auth-cookie.test.ts +0 -528
  97. package/src/middleware/auth-cookie.ts +0 -80
  98. package/src/middleware/cors.test.ts +0 -424
  99. package/src/middleware/cors.ts +0 -104
  100. package/src/middleware/index.ts +0 -5
  101. package/src/middleware/remote-auth.test.ts +0 -488
  102. package/src/middleware/remote-auth.ts +0 -68
  103. package/src/middleware/timeout.ts +0 -15
  104. package/src/middleware-runner.test.ts +0 -418
  105. package/src/middleware-runner.ts +0 -234
  106. package/src/permissions.test.ts +0 -434
  107. package/src/permissions.ts +0 -319
  108. package/src/pikku-request.ts +0 -23
  109. package/src/pikku-response.ts +0 -5
  110. package/src/pikku-state.test.ts +0 -224
  111. package/src/pikku-state.ts +0 -214
  112. package/src/run-tests-script.test.ts +0 -49
  113. package/src/schema.test.ts +0 -249
  114. package/src/schema.ts +0 -151
  115. package/src/services/ai-agent-runner-service.ts +0 -41
  116. package/src/services/ai-run-state-service.ts +0 -20
  117. package/src/services/ai-storage-service.ts +0 -39
  118. package/src/services/content-service.ts +0 -76
  119. package/src/services/deployment-service.ts +0 -22
  120. package/src/services/gateway-service.ts +0 -20
  121. package/src/services/gopass-secrets.ts +0 -98
  122. package/src/services/in-memory-ai-run-state-service.ts +0 -73
  123. package/src/services/in-memory-trigger-service.ts +0 -64
  124. package/src/services/in-memory-workflow-service.test.ts +0 -351
  125. package/src/services/in-memory-workflow-service.ts +0 -395
  126. package/src/services/index.ts +0 -56
  127. package/src/services/jwt-service.ts +0 -30
  128. package/src/services/local-content.ts +0 -120
  129. package/src/services/local-gateway-service.ts +0 -62
  130. package/src/services/local-secrets.test.ts +0 -80
  131. package/src/services/local-secrets.ts +0 -94
  132. package/src/services/local-variables.test.ts +0 -61
  133. package/src/services/local-variables.ts +0 -39
  134. package/src/services/logger-console.test.ts +0 -118
  135. package/src/services/logger-console.ts +0 -98
  136. package/src/services/logger.ts +0 -57
  137. package/src/services/scheduler-service.ts +0 -86
  138. package/src/services/schema-service.ts +0 -33
  139. package/src/services/scoped-secret-service.test.ts +0 -74
  140. package/src/services/scoped-secret-service.ts +0 -41
  141. package/src/services/secret-service.ts +0 -38
  142. package/src/services/trigger-service.ts +0 -17
  143. package/src/services/typed-secret-service.test.ts +0 -93
  144. package/src/services/typed-secret-service.ts +0 -70
  145. package/src/services/typed-variables-service.test.ts +0 -73
  146. package/src/services/typed-variables-service.ts +0 -81
  147. package/src/services/user-session-service.test.ts +0 -113
  148. package/src/services/user-session-service.ts +0 -86
  149. package/src/services/variables-service.ts +0 -11
  150. package/src/services/workflow-service.ts +0 -84
  151. package/src/time-utils.test.ts +0 -56
  152. package/src/time-utils.ts +0 -107
  153. package/src/types/core.types.ts +0 -470
  154. package/src/types/state.types.ts +0 -186
  155. package/src/utils.test.ts +0 -350
  156. package/src/utils.ts +0 -129
  157. package/src/version.test.ts +0 -80
  158. package/src/version.ts +0 -25
  159. package/src/wirings/ai-agent/ai-agent-assistant-ui.test.ts +0 -363
  160. package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +0 -238
  161. package/src/wirings/ai-agent/ai-agent-helpers.test.ts +0 -152
  162. package/src/wirings/ai-agent/ai-agent-helpers.ts +0 -76
  163. package/src/wirings/ai-agent/ai-agent-memory.ts +0 -309
  164. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +0 -115
  165. package/src/wirings/ai-agent/ai-agent-model-config.ts +0 -43
  166. package/src/wirings/ai-agent/ai-agent-prepare.ts +0 -522
  167. package/src/wirings/ai-agent/ai-agent-registry.test.ts +0 -37
  168. package/src/wirings/ai-agent/ai-agent-registry.ts +0 -82
  169. package/src/wirings/ai-agent/ai-agent-runner.test.ts +0 -317
  170. package/src/wirings/ai-agent/ai-agent-runner.ts +0 -251
  171. package/src/wirings/ai-agent/ai-agent-stream.test.ts +0 -652
  172. package/src/wirings/ai-agent/ai-agent-stream.ts +0 -1060
  173. package/src/wirings/ai-agent/ai-agent.types.ts +0 -360
  174. package/src/wirings/ai-agent/index.ts +0 -41
  175. package/src/wirings/channel/channel-common.ts +0 -90
  176. package/src/wirings/channel/channel-handler.ts +0 -241
  177. package/src/wirings/channel/channel-middleware-runner.ts +0 -120
  178. package/src/wirings/channel/channel-runner.ts +0 -166
  179. package/src/wirings/channel/channel-store.ts +0 -29
  180. package/src/wirings/channel/channel.types.ts +0 -178
  181. package/src/wirings/channel/define-channel-routes.ts +0 -25
  182. package/src/wirings/channel/eventhub-service.ts +0 -34
  183. package/src/wirings/channel/eventhub-store.ts +0 -13
  184. package/src/wirings/channel/index.ts +0 -24
  185. package/src/wirings/channel/local/index.ts +0 -3
  186. package/src/wirings/channel/local/local-channel-handler.ts +0 -79
  187. package/src/wirings/channel/local/local-channel-runner.test.ts +0 -215
  188. package/src/wirings/channel/local/local-channel-runner.ts +0 -200
  189. package/src/wirings/channel/local/local-eventhub-service.test.ts +0 -95
  190. package/src/wirings/channel/local/local-eventhub-service.ts +0 -101
  191. package/src/wirings/channel/log-channels.ts +0 -20
  192. package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +0 -54
  193. package/src/wirings/channel/pikku-abstract-channel-handler.ts +0 -45
  194. package/src/wirings/channel/serverless/index.ts +0 -5
  195. package/src/wirings/channel/serverless/serverless-channel-runner.ts +0 -286
  196. package/src/wirings/cli/channel/cli-channel-runner.ts +0 -136
  197. package/src/wirings/cli/channel/index.ts +0 -1
  198. package/src/wirings/cli/cli-runner.test.ts +0 -403
  199. package/src/wirings/cli/cli-runner.ts +0 -519
  200. package/src/wirings/cli/cli.types.ts +0 -357
  201. package/src/wirings/cli/command-parser.test.ts +0 -445
  202. package/src/wirings/cli/command-parser.ts +0 -504
  203. package/src/wirings/cli/define-cli-commands.ts +0 -24
  204. package/src/wirings/cli/index.ts +0 -19
  205. package/src/wirings/gateway/gateway-runner.test.ts +0 -474
  206. package/src/wirings/gateway/gateway-runner.ts +0 -411
  207. package/src/wirings/gateway/gateway.types.ts +0 -149
  208. package/src/wirings/gateway/index.ts +0 -13
  209. package/src/wirings/http/http-routes.test.ts +0 -322
  210. package/src/wirings/http/http-routes.ts +0 -198
  211. package/src/wirings/http/http-runner.test.ts +0 -144
  212. package/src/wirings/http/http-runner.ts +0 -563
  213. package/src/wirings/http/http.types.ts +0 -378
  214. package/src/wirings/http/index.ts +0 -25
  215. package/src/wirings/http/log-http-routes.ts +0 -22
  216. package/src/wirings/http/pikku-fetch-http-request.test.ts +0 -237
  217. package/src/wirings/http/pikku-fetch-http-request.ts +0 -166
  218. package/src/wirings/http/pikku-fetch-http-response.test.ts +0 -82
  219. package/src/wirings/http/pikku-fetch-http-response.ts +0 -147
  220. package/src/wirings/http/routers/http-router.ts +0 -13
  221. package/src/wirings/http/routers/path-to-regex.test.ts +0 -319
  222. package/src/wirings/http/routers/path-to-regex.ts +0 -126
  223. package/src/wirings/mcp/index.ts +0 -27
  224. package/src/wirings/mcp/mcp-endpoint-registry.test.ts +0 -389
  225. package/src/wirings/mcp/mcp-endpoint-registry.ts +0 -159
  226. package/src/wirings/mcp/mcp-runner.ts +0 -305
  227. package/src/wirings/mcp/mcp.types.ts +0 -216
  228. package/src/wirings/node/index.ts +0 -2
  229. package/src/wirings/node/node.types.ts +0 -23
  230. package/src/wirings/oauth2/index.ts +0 -3
  231. package/src/wirings/oauth2/oauth2-client.test.ts +0 -929
  232. package/src/wirings/oauth2/oauth2-client.ts +0 -335
  233. package/src/wirings/oauth2/oauth2.types.ts +0 -69
  234. package/src/wirings/oauth2/wire-oauth2-credential.ts +0 -23
  235. package/src/wirings/queue/index.ts +0 -31
  236. package/src/wirings/queue/queue-runner.test.ts +0 -710
  237. package/src/wirings/queue/queue-runner.ts +0 -192
  238. package/src/wirings/queue/queue.types.ts +0 -189
  239. package/src/wirings/queue/register-queue-helper.ts +0 -60
  240. package/src/wirings/queue/validate-worker-config.test.ts +0 -108
  241. package/src/wirings/queue/validate-worker-config.ts +0 -116
  242. package/src/wirings/rpc/index.ts +0 -4
  243. package/src/wirings/rpc/rpc-runner.ts +0 -439
  244. package/src/wirings/rpc/rpc-types.ts +0 -51
  245. package/src/wirings/rpc/wire-addon.ts +0 -20
  246. package/src/wirings/scheduler/index.ts +0 -11
  247. package/src/wirings/scheduler/log-schedulers.ts +0 -20
  248. package/src/wirings/scheduler/scheduler-runner.test.ts +0 -660
  249. package/src/wirings/scheduler/scheduler-runner.ts +0 -132
  250. package/src/wirings/scheduler/scheduler.types.ts +0 -53
  251. package/src/wirings/secret/index.ts +0 -9
  252. package/src/wirings/secret/secret.types.ts +0 -32
  253. package/src/wirings/secret/validate-secret-definitions.test.ts +0 -140
  254. package/src/wirings/secret/validate-secret-definitions.ts +0 -82
  255. package/src/wirings/trigger/index.ts +0 -10
  256. package/src/wirings/trigger/pikku-trigger-service.ts +0 -112
  257. package/src/wirings/trigger/trigger-runner.test.ts +0 -79
  258. package/src/wirings/trigger/trigger-runner.ts +0 -135
  259. package/src/wirings/trigger/trigger.types.ts +0 -178
  260. package/src/wirings/variable/index.ts +0 -8
  261. package/src/wirings/variable/validate-variable-definitions.test.ts +0 -91
  262. package/src/wirings/variable/validate-variable-definitions.ts +0 -69
  263. package/src/wirings/variable/variable.types.ts +0 -22
  264. package/src/wirings/workflow/dsl/index.ts +0 -31
  265. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +0 -320
  266. package/src/wirings/workflow/dsl/workflow-runner.ts +0 -28
  267. package/src/wirings/workflow/graph/graph-node.ts +0 -222
  268. package/src/wirings/workflow/graph/graph-runner.test.ts +0 -487
  269. package/src/wirings/workflow/graph/graph-runner.ts +0 -816
  270. package/src/wirings/workflow/graph/index.ts +0 -14
  271. package/src/wirings/workflow/graph/template.test.ts +0 -49
  272. package/src/wirings/workflow/graph/template.ts +0 -42
  273. package/src/wirings/workflow/graph/wire-workflow-graph.ts +0 -29
  274. package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -104
  275. package/src/wirings/workflow/index.ts +0 -81
  276. package/src/wirings/workflow/pikku-workflow-service.test.ts +0 -200
  277. package/src/wirings/workflow/pikku-workflow-service.ts +0 -1165
  278. package/src/wirings/workflow/workflow-helpers.test.ts +0 -129
  279. package/src/wirings/workflow/workflow-helpers.ts +0 -79
  280. package/src/wirings/workflow/workflow.types.ts +0 -266
  281. package/tsconfig.json +0 -14
  282. package/tsconfig.tsbuildinfo +0 -1
@@ -1,929 +0,0 @@
1
- import { describe, test, afterEach, mock } from 'node:test'
2
- import * as assert from 'node:assert/strict'
3
- import { OAuth2Client } from './oauth2-client.js'
4
- import type { OAuth2Token, OAuth2AppCredential } from './oauth2.types.js'
5
- import type { OAuth2CredentialConfig } from '../secret/secret.types.js'
6
- import type { SecretService } from '../../services/secret-service.js'
7
-
8
- // Store the original fetch for restoration
9
- const originalFetch = globalThis.fetch
10
-
11
- // Mock SecretService with in-memory store
12
- function createMockSecretService(
13
- initialSecrets: Record<string, unknown> = {}
14
- ): SecretService & { getStoredSecrets: () => Map<string, unknown> } {
15
- const secrets = new Map<string, unknown>(Object.entries(initialSecrets))
16
- return {
17
- getSecret: async (key: string) => secrets.get(key) as string,
18
- getSecretJSON: async <T>(key: string) => secrets.get(key) as T,
19
- setSecretJSON: async (key: string, value: unknown) => {
20
- secrets.set(key, value)
21
- },
22
- deleteSecret: async (_key: string) => {
23
- // Not used in tests
24
- },
25
- hasSecret: async (key: string) => secrets.has(key),
26
- // Helper for tests to inspect stored values
27
- getStoredSecrets: () => secrets,
28
- }
29
- }
30
-
31
- // Helper to create a mock fetch response
32
- function mockFetchResponse(data: unknown, status = 200, ok = true): Response {
33
- return {
34
- ok,
35
- status,
36
- json: async () => data,
37
- text: async () => JSON.stringify(data),
38
- } as Response
39
- }
40
-
41
- // Default test configuration
42
- const defaultConfig: OAuth2CredentialConfig = {
43
- tokenSecretId: 'OAUTH_TOKENS',
44
- authorizationUrl: 'https://example.com/oauth/authorize',
45
- tokenUrl: 'https://example.com/oauth/token',
46
- scopes: ['read', 'write'],
47
- }
48
-
49
- const defaultAppCredential: OAuth2AppCredential = {
50
- clientId: 'test-client-id',
51
- clientSecret: 'test-client-secret',
52
- }
53
-
54
- describe('OAuth2Client', () => {
55
- afterEach(() => {
56
- // Restore original fetch after each test
57
- globalThis.fetch = originalFetch
58
- })
59
-
60
- describe('Token Management', () => {
61
- test('returns cached valid token', async () => {
62
- const validToken: OAuth2Token = {
63
- accessToken: 'valid-access-token',
64
- refreshToken: 'refresh-token',
65
- expiresAt: Date.now() + 3600000, // 1 hour from now
66
- tokenType: 'Bearer',
67
- }
68
-
69
- const secrets = createMockSecretService({
70
- OAUTH_TOKENS: validToken,
71
- APP_CREDS: defaultAppCredential,
72
- })
73
-
74
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
75
-
76
- // First call loads from secrets
77
- const token1 = await client.getAccessToken()
78
- assert.equal(token1, 'valid-access-token')
79
-
80
- // Second call should return cached token without hitting secrets
81
- const token2 = await client.getAccessToken()
82
- assert.equal(token2, 'valid-access-token')
83
- })
84
-
85
- test('refreshes expired token', async () => {
86
- const expiredToken: OAuth2Token = {
87
- accessToken: 'expired-access-token',
88
- refreshToken: 'refresh-token',
89
- expiresAt: Date.now() - 1000, // Expired
90
- tokenType: 'Bearer',
91
- }
92
-
93
- const secrets = createMockSecretService({
94
- OAUTH_TOKENS: expiredToken,
95
- APP_CREDS: defaultAppCredential,
96
- })
97
-
98
- globalThis.fetch = mock.fn(async () =>
99
- mockFetchResponse({
100
- access_token: 'new-access-token',
101
- refresh_token: 'new-refresh-token',
102
- expires_in: 3600,
103
- token_type: 'Bearer',
104
- })
105
- )
106
-
107
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
108
- const token = await client.getAccessToken()
109
-
110
- assert.equal(token, 'new-access-token')
111
- })
112
-
113
- test('loads from secrets on first call', async () => {
114
- const storedToken: OAuth2Token = {
115
- accessToken: 'stored-access-token',
116
- refreshToken: 'stored-refresh-token',
117
- expiresAt: Date.now() + 3600000,
118
- tokenType: 'Bearer',
119
- }
120
-
121
- const secrets = createMockSecretService({
122
- OAUTH_TOKENS: storedToken,
123
- APP_CREDS: defaultAppCredential,
124
- })
125
-
126
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
127
- const token = await client.getAccessToken()
128
-
129
- assert.equal(token, 'stored-access-token')
130
- })
131
-
132
- test('refreshes expired token from secrets', async () => {
133
- const expiredToken: OAuth2Token = {
134
- accessToken: 'expired-token',
135
- refreshToken: 'valid-refresh-token',
136
- expiresAt: Date.now() - 1000, // Expired
137
- tokenType: 'Bearer',
138
- }
139
-
140
- const secrets = createMockSecretService({
141
- OAUTH_TOKENS: expiredToken,
142
- APP_CREDS: defaultAppCredential,
143
- })
144
-
145
- globalThis.fetch = mock.fn(async () =>
146
- mockFetchResponse({
147
- access_token: 'refreshed-token',
148
- expires_in: 3600,
149
- token_type: 'Bearer',
150
- })
151
- )
152
-
153
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
154
- const token = await client.getAccessToken()
155
-
156
- assert.equal(token, 'refreshed-token')
157
- })
158
-
159
- test('throws when no token available', async () => {
160
- const secrets = createMockSecretService({
161
- APP_CREDS: defaultAppCredential,
162
- // No OAUTH_TOKENS
163
- })
164
-
165
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
166
-
167
- await assert.rejects(
168
- async () => await client.getAccessToken(),
169
- /undefined/
170
- )
171
- })
172
-
173
- test('uses 60-second expiry buffer', async () => {
174
- // Token expires in 30 seconds (within the 60-second buffer)
175
- const almostExpiredToken: OAuth2Token = {
176
- accessToken: 'almost-expired-token',
177
- refreshToken: 'refresh-token',
178
- expiresAt: Date.now() + 30000, // 30 seconds from now
179
- tokenType: 'Bearer',
180
- }
181
-
182
- const secrets = createMockSecretService({
183
- OAUTH_TOKENS: almostExpiredToken,
184
- APP_CREDS: defaultAppCredential,
185
- })
186
-
187
- globalThis.fetch = mock.fn(async () =>
188
- mockFetchResponse({
189
- access_token: 'refreshed-due-to-buffer',
190
- expires_in: 3600,
191
- token_type: 'Bearer',
192
- })
193
- )
194
-
195
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
196
- const token = await client.getAccessToken()
197
-
198
- // Should refresh because within 60-second buffer
199
- assert.equal(token, 'refreshed-due-to-buffer')
200
- })
201
- })
202
-
203
- describe('Token Refresh', () => {
204
- test('makes correct HTTP request', async () => {
205
- const tokenWithRefresh: OAuth2Token = {
206
- accessToken: 'old-token',
207
- refreshToken: 'my-refresh-token',
208
- expiresAt: Date.now() - 1000,
209
- tokenType: 'Bearer',
210
- }
211
-
212
- const secrets = createMockSecretService({
213
- OAUTH_TOKENS: tokenWithRefresh,
214
- APP_CREDS: defaultAppCredential,
215
- })
216
-
217
- let capturedRequest: { url: string; options: RequestInit } | null = null
218
-
219
- globalThis.fetch = mock.fn(
220
- async (url: RequestInfo | URL, options?: RequestInit) => {
221
- capturedRequest = { url: url.toString(), options: options! }
222
- return mockFetchResponse({
223
- access_token: 'new-token',
224
- expires_in: 3600,
225
- token_type: 'Bearer',
226
- })
227
- }
228
- ) as typeof fetch
229
-
230
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
231
- await client.getAccessToken()
232
-
233
- assert.ok(capturedRequest, 'Expected capturedRequest to be set')
234
- const req = capturedRequest as { url: string; options: RequestInit }
235
- assert.equal(req.url, 'https://example.com/oauth/token')
236
- assert.equal(req.options.method, 'POST')
237
- assert.equal(
238
- (req.options.headers as Record<string, string>)?.['Content-Type'],
239
- 'application/x-www-form-urlencoded'
240
- )
241
-
242
- const body = req.options.body as string
243
- assert.ok(body.includes('grant_type=refresh_token'))
244
- assert.ok(body.includes('refresh_token=my-refresh-token'))
245
- assert.ok(body.includes('client_id=test-client-id'))
246
- assert.ok(body.includes('client_secret=test-client-secret'))
247
- })
248
-
249
- test('updates cached token', async () => {
250
- const expiredToken: OAuth2Token = {
251
- accessToken: 'expired-token',
252
- refreshToken: 'refresh-token',
253
- expiresAt: Date.now() - 1000,
254
- tokenType: 'Bearer',
255
- }
256
-
257
- const secrets = createMockSecretService({
258
- OAUTH_TOKENS: expiredToken,
259
- APP_CREDS: defaultAppCredential,
260
- })
261
-
262
- let fetchCallCount = 0
263
- globalThis.fetch = mock.fn(async () => {
264
- fetchCallCount++
265
- return mockFetchResponse({
266
- access_token: 'new-cached-token',
267
- expires_in: 3600,
268
- token_type: 'Bearer',
269
- })
270
- })
271
-
272
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
273
-
274
- // First call - triggers refresh
275
- const token1 = await client.getAccessToken()
276
- assert.equal(token1, 'new-cached-token')
277
-
278
- // Second call - should use cached token, no refresh
279
- const token2 = await client.getAccessToken()
280
- assert.equal(token2, 'new-cached-token')
281
- assert.equal(fetchCallCount, 1) // Only one refresh call
282
- })
283
-
284
- test('throws on HTTP error', async () => {
285
- const expiredToken: OAuth2Token = {
286
- accessToken: 'expired-token',
287
- refreshToken: 'refresh-token',
288
- expiresAt: Date.now() - 1000,
289
- tokenType: 'Bearer',
290
- }
291
-
292
- const secrets = createMockSecretService({
293
- OAUTH_TOKENS: expiredToken,
294
- APP_CREDS: defaultAppCredential,
295
- })
296
-
297
- globalThis.fetch = mock.fn(async () =>
298
- mockFetchResponse({ error: 'invalid_grant' }, 400, false)
299
- )
300
-
301
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
302
-
303
- await assert.rejects(
304
- async () => await client.getAccessToken(),
305
- /Token refresh failed: 400/
306
- )
307
- })
308
-
309
- test('throws when no refresh token', async () => {
310
- const tokenWithoutRefresh: OAuth2Token = {
311
- accessToken: 'expired-token',
312
- // No refreshToken
313
- expiresAt: Date.now() - 1000,
314
- tokenType: 'Bearer',
315
- }
316
-
317
- const secrets = createMockSecretService({
318
- OAUTH_TOKENS: tokenWithoutRefresh,
319
- APP_CREDS: defaultAppCredential,
320
- })
321
-
322
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
323
-
324
- await assert.rejects(
325
- async () => await client.getAccessToken(),
326
- /OAuth2 token expired and no refresh token available/
327
- )
328
- })
329
-
330
- test('persists refreshed token to secrets', async () => {
331
- const expiredToken: OAuth2Token = {
332
- accessToken: 'expired-token',
333
- refreshToken: 'refresh-token',
334
- expiresAt: Date.now() - 1000,
335
- tokenType: 'Bearer',
336
- }
337
-
338
- const secrets = createMockSecretService({
339
- OAUTH_TOKENS: expiredToken,
340
- APP_CREDS: defaultAppCredential,
341
- })
342
-
343
- globalThis.fetch = mock.fn(async () =>
344
- mockFetchResponse({
345
- access_token: 'new-persisted-token',
346
- refresh_token: 'new-refresh-token',
347
- expires_in: 3600,
348
- token_type: 'Bearer',
349
- })
350
- )
351
-
352
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
353
- await client.getAccessToken()
354
-
355
- // Verify the token was persisted to secrets
356
- const storedToken = secrets
357
- .getStoredSecrets()
358
- .get('OAUTH_TOKENS') as OAuth2Token
359
- assert.equal(storedToken.accessToken, 'new-persisted-token')
360
- assert.equal(storedToken.refreshToken, 'new-refresh-token')
361
- assert.ok(storedToken.expiresAt && storedToken.expiresAt > Date.now())
362
- })
363
-
364
- test('promise lock prevents concurrent refreshes', async () => {
365
- const expiredToken: OAuth2Token = {
366
- accessToken: 'expired-token',
367
- refreshToken: 'refresh-token',
368
- expiresAt: Date.now() - 1000,
369
- tokenType: 'Bearer',
370
- }
371
-
372
- const secrets = createMockSecretService({
373
- OAUTH_TOKENS: expiredToken,
374
- APP_CREDS: defaultAppCredential,
375
- })
376
-
377
- let fetchCallCount = 0
378
- globalThis.fetch = mock.fn(async () => {
379
- fetchCallCount++
380
- // Simulate network delay
381
- await new Promise((resolve) => setTimeout(resolve, 50))
382
- return mockFetchResponse({
383
- access_token: 'concurrent-token',
384
- expires_in: 3600,
385
- token_type: 'Bearer',
386
- })
387
- })
388
-
389
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
390
-
391
- // Trigger concurrent refresh calls
392
- const [token1, token2, token3] = await Promise.all([
393
- client.getAccessToken(),
394
- client.getAccessToken(),
395
- client.getAccessToken(),
396
- ])
397
-
398
- // All should get the same token
399
- assert.equal(token1, 'concurrent-token')
400
- assert.equal(token2, 'concurrent-token')
401
- assert.equal(token3, 'concurrent-token')
402
-
403
- // Only one actual refresh should have happened
404
- assert.equal(fetchCallCount, 1)
405
- })
406
-
407
- test('preserves refresh token if not returned', async () => {
408
- const expiredToken: OAuth2Token = {
409
- accessToken: 'expired-token',
410
- refreshToken: 'original-refresh-token',
411
- expiresAt: Date.now() - 1000,
412
- tokenType: 'Bearer',
413
- }
414
-
415
- const secrets = createMockSecretService({
416
- OAUTH_TOKENS: expiredToken,
417
- APP_CREDS: defaultAppCredential,
418
- })
419
-
420
- // Response doesn't include refresh_token
421
- globalThis.fetch = mock.fn(async () =>
422
- mockFetchResponse({
423
- access_token: 'new-token',
424
- expires_in: 3600,
425
- token_type: 'Bearer',
426
- // No refresh_token in response
427
- })
428
- )
429
-
430
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
431
- await client.getAccessToken()
432
-
433
- // Expire the new token to trigger another refresh
434
- // The client should still have the original refresh token
435
- // To verify this, we'd need to access internal state or trigger another refresh
436
- // For now, we verify the first refresh worked
437
- const token = await client.getAccessToken()
438
- assert.equal(token, 'new-token')
439
- })
440
- })
441
-
442
- describe('Request Method', () => {
443
- test('adds Authorization header', async () => {
444
- const validToken: OAuth2Token = {
445
- accessToken: 'bearer-token',
446
- expiresAt: Date.now() + 3600000,
447
- tokenType: 'Bearer',
448
- }
449
-
450
- const secrets = createMockSecretService({
451
- OAUTH_TOKENS: validToken,
452
- APP_CREDS: defaultAppCredential,
453
- })
454
-
455
- let capturedHeaders: Record<string, string> | null = null
456
-
457
- globalThis.fetch = mock.fn(
458
- async (_url: RequestInfo | URL, options?: RequestInit) => {
459
- capturedHeaders = options?.headers as Record<string, string>
460
- return mockFetchResponse({ data: 'test' })
461
- }
462
- ) as typeof fetch
463
-
464
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
465
- await client.request('https://api.example.com/resource')
466
-
467
- assert.ok(capturedHeaders, 'Expected capturedHeaders to be set')
468
- const headers = capturedHeaders as Record<string, string>
469
- assert.equal(headers.Authorization, 'Bearer bearer-token')
470
- })
471
-
472
- test('retries on 401 with fresh token', async () => {
473
- const validToken: OAuth2Token = {
474
- accessToken: 'initial-token',
475
- refreshToken: 'refresh-token',
476
- expiresAt: Date.now() + 3600000,
477
- tokenType: 'Bearer',
478
- }
479
-
480
- const secrets = createMockSecretService({
481
- OAUTH_TOKENS: validToken,
482
- APP_CREDS: defaultAppCredential,
483
- })
484
-
485
- let requestCount = 0
486
- globalThis.fetch = mock.fn(
487
- async (url: RequestInfo | URL, options?: RequestInit) => {
488
- requestCount++
489
- const urlStr = url.toString()
490
-
491
- // Token refresh endpoint
492
- if (urlStr === 'https://example.com/oauth/token') {
493
- return mockFetchResponse({
494
- access_token: 'refreshed-token',
495
- expires_in: 3600,
496
- token_type: 'Bearer',
497
- })
498
- }
499
-
500
- // API endpoint
501
- const headers = options?.headers as Record<string, string>
502
- if (headers?.Authorization === 'Bearer initial-token') {
503
- // First request returns 401
504
- return mockFetchResponse({}, 401, false)
505
- }
506
-
507
- // Retry with refreshed token succeeds
508
- return mockFetchResponse({ success: true })
509
- }
510
- ) as typeof fetch
511
-
512
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
513
- const response = await client.request('https://api.example.com/resource')
514
-
515
- assert.equal(response.status, 200)
516
- assert.equal(requestCount, 3) // Initial request + refresh + retry
517
- })
518
-
519
- test('no double retry on 401', async () => {
520
- const validToken: OAuth2Token = {
521
- accessToken: 'token',
522
- refreshToken: 'refresh-token',
523
- expiresAt: Date.now() + 3600000,
524
- tokenType: 'Bearer',
525
- }
526
-
527
- const secrets = createMockSecretService({
528
- OAUTH_TOKENS: validToken,
529
- APP_CREDS: defaultAppCredential,
530
- })
531
-
532
- let apiRequestCount = 0
533
- globalThis.fetch = mock.fn(async (url: RequestInfo | URL) => {
534
- const urlStr = url.toString()
535
-
536
- if (urlStr === 'https://example.com/oauth/token') {
537
- return mockFetchResponse({
538
- access_token: 'new-token',
539
- expires_in: 3600,
540
- token_type: 'Bearer',
541
- })
542
- }
543
-
544
- apiRequestCount++
545
- // Always return 401
546
- return mockFetchResponse({}, 401, false)
547
- }) as typeof fetch
548
-
549
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
550
- const response = await client.request('https://api.example.com/resource')
551
-
552
- assert.equal(response.status, 401)
553
- // Should only be 2 API requests (initial + one retry)
554
- assert.equal(apiRequestCount, 2)
555
- })
556
-
557
- test('passes through other status codes', async () => {
558
- const validToken: OAuth2Token = {
559
- accessToken: 'token',
560
- expiresAt: Date.now() + 3600000,
561
- tokenType: 'Bearer',
562
- }
563
-
564
- const secrets = createMockSecretService({
565
- OAUTH_TOKENS: validToken,
566
- APP_CREDS: defaultAppCredential,
567
- })
568
-
569
- globalThis.fetch = mock.fn(async () =>
570
- mockFetchResponse({ error: 'forbidden' }, 403, false)
571
- )
572
-
573
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
574
- const response = await client.request('https://api.example.com/resource')
575
-
576
- assert.equal(response.status, 403)
577
- })
578
-
579
- test('preserves user headers', async () => {
580
- const validToken: OAuth2Token = {
581
- accessToken: 'token',
582
- expiresAt: Date.now() + 3600000,
583
- tokenType: 'Bearer',
584
- }
585
-
586
- const secrets = createMockSecretService({
587
- OAUTH_TOKENS: validToken,
588
- APP_CREDS: defaultAppCredential,
589
- })
590
-
591
- let capturedHeaders: Record<string, string> | null = null
592
-
593
- globalThis.fetch = mock.fn(
594
- async (_url: RequestInfo | URL, options?: RequestInit) => {
595
- capturedHeaders = options?.headers as Record<string, string>
596
- return mockFetchResponse({ data: 'test' })
597
- }
598
- ) as typeof fetch
599
-
600
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
601
- await client.request('https://api.example.com/resource', {
602
- headers: {
603
- 'Content-Type': 'application/json',
604
- 'X-Custom-Header': 'custom-value',
605
- },
606
- })
607
-
608
- assert.ok(capturedHeaders, 'Expected capturedHeaders to be set')
609
- const headers = capturedHeaders as Record<string, string>
610
- assert.equal(headers['Content-Type'], 'application/json')
611
- assert.equal(headers['X-Custom-Header'], 'custom-value')
612
- assert.equal(headers.Authorization, 'Bearer token')
613
- })
614
- })
615
-
616
- describe('Authorization URL', () => {
617
- test('includes required params', async () => {
618
- const secrets = createMockSecretService({
619
- APP_CREDS: defaultAppCredential,
620
- })
621
-
622
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
623
- const url = await client.getAuthorizationUrl(
624
- 'test-state',
625
- 'http://localhost:3000/callback'
626
- )
627
-
628
- const parsed = new URL(url)
629
- assert.equal(
630
- parsed.origin + parsed.pathname,
631
- 'https://example.com/oauth/authorize'
632
- )
633
- assert.equal(parsed.searchParams.get('response_type'), 'code')
634
- assert.equal(parsed.searchParams.get('client_id'), 'test-client-id')
635
- assert.equal(
636
- parsed.searchParams.get('redirect_uri'),
637
- 'http://localhost:3000/callback'
638
- )
639
- assert.equal(parsed.searchParams.get('scope'), 'read write')
640
- assert.equal(parsed.searchParams.get('state'), 'test-state')
641
- })
642
-
643
- test('includes additional params', async () => {
644
- const configWithParams: OAuth2CredentialConfig = {
645
- ...defaultConfig,
646
- additionalParams: {
647
- access_type: 'offline',
648
- prompt: 'consent',
649
- },
650
- }
651
-
652
- const secrets = createMockSecretService({
653
- APP_CREDS: defaultAppCredential,
654
- })
655
-
656
- const client = new OAuth2Client(configWithParams, 'APP_CREDS', secrets)
657
- const url = await client.getAuthorizationUrl(
658
- 'state',
659
- 'http://localhost/callback'
660
- )
661
-
662
- const parsed = new URL(url)
663
- assert.equal(parsed.searchParams.get('access_type'), 'offline')
664
- assert.equal(parsed.searchParams.get('prompt'), 'consent')
665
- })
666
-
667
- test('URL-encodes values', async () => {
668
- const configWithSpecialScopes: OAuth2CredentialConfig = {
669
- ...defaultConfig,
670
- scopes: ['read:user', 'write:data'],
671
- }
672
-
673
- const secrets = createMockSecretService({
674
- APP_CREDS: defaultAppCredential,
675
- })
676
-
677
- const client = new OAuth2Client(
678
- configWithSpecialScopes,
679
- 'APP_CREDS',
680
- secrets
681
- )
682
- const url = await client.getAuthorizationUrl(
683
- 'state with spaces',
684
- 'http://localhost/callback?param=value'
685
- )
686
-
687
- // URL should be properly encoded
688
- assert.ok(
689
- url.includes('state+with+spaces') ||
690
- url.includes('state%20with%20spaces')
691
- )
692
- assert.ok(
693
- url.includes(
694
- 'redirect_uri=http%3A%2F%2Flocalhost%2Fcallback%3Fparam%3Dvalue'
695
- )
696
- )
697
- })
698
- })
699
-
700
- describe('Code Exchange', () => {
701
- test('makes correct HTTP request', async () => {
702
- const secrets = createMockSecretService({
703
- APP_CREDS: defaultAppCredential,
704
- })
705
-
706
- let capturedRequest: { url: string; options: RequestInit } | null = null
707
-
708
- globalThis.fetch = mock.fn(
709
- async (url: RequestInfo | URL, options?: RequestInit) => {
710
- capturedRequest = { url: url.toString(), options: options! }
711
- return mockFetchResponse({
712
- access_token: 'exchanged-token',
713
- refresh_token: 'refresh-token',
714
- expires_in: 3600,
715
- token_type: 'Bearer',
716
- })
717
- }
718
- ) as typeof fetch
719
-
720
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
721
- await client.exchangeCode('auth-code', 'http://localhost/callback')
722
-
723
- assert.ok(capturedRequest, 'Expected capturedRequest to be set')
724
- const req = capturedRequest as { url: string; options: RequestInit }
725
- assert.equal(req.url, 'https://example.com/oauth/token')
726
- assert.equal(req.options.method, 'POST')
727
-
728
- const body = req.options.body as string
729
- assert.ok(body.includes('grant_type=authorization_code'))
730
- assert.ok(body.includes('code=auth-code'))
731
- assert.ok(body.includes('redirect_uri=http%3A%2F%2Flocalhost%2Fcallback'))
732
- assert.ok(body.includes('client_id=test-client-id'))
733
- assert.ok(body.includes('client_secret=test-client-secret'))
734
- })
735
-
736
- test('caches resulting token', async () => {
737
- const secrets = createMockSecretService({
738
- APP_CREDS: defaultAppCredential,
739
- })
740
-
741
- let fetchCallCount = 0
742
- globalThis.fetch = mock.fn(async () => {
743
- fetchCallCount++
744
- return mockFetchResponse({
745
- access_token: 'new-token',
746
- refresh_token: 'new-refresh',
747
- expires_in: 3600,
748
- token_type: 'Bearer',
749
- })
750
- })
751
-
752
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
753
- const token = await client.exchangeCode(
754
- 'code',
755
- 'http://localhost/callback'
756
- )
757
-
758
- assert.equal(token.accessToken, 'new-token')
759
-
760
- // Subsequent getAccessToken should use cached token
761
- const accessToken = await client.getAccessToken()
762
- assert.equal(accessToken, 'new-token')
763
- assert.equal(fetchCallCount, 1) // No additional fetch
764
- })
765
-
766
- test('throws on HTTP error', async () => {
767
- const secrets = createMockSecretService({
768
- APP_CREDS: defaultAppCredential,
769
- })
770
-
771
- globalThis.fetch = mock.fn(async () =>
772
- mockFetchResponse({ error: 'invalid_code' }, 400, false)
773
- )
774
-
775
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
776
-
777
- await assert.rejects(
778
- async () =>
779
- await client.exchangeCode('bad-code', 'http://localhost/callback'),
780
- /Token exchange failed: 400/
781
- )
782
- })
783
-
784
- test('handles missing optional fields', async () => {
785
- const secrets = createMockSecretService({
786
- APP_CREDS: defaultAppCredential,
787
- })
788
-
789
- globalThis.fetch = mock.fn(async () =>
790
- mockFetchResponse({
791
- access_token: 'token-only',
792
- // No refresh_token, expires_in, or scope
793
- })
794
- )
795
-
796
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
797
- const token = await client.exchangeCode(
798
- 'code',
799
- 'http://localhost/callback'
800
- )
801
-
802
- assert.equal(token.accessToken, 'token-only')
803
- assert.equal(token.refreshToken, undefined)
804
- assert.equal(token.expiresAt, undefined)
805
- assert.equal(token.tokenType, 'Bearer') // Default
806
- assert.equal(token.scope, undefined)
807
- })
808
- })
809
-
810
- describe('Security', () => {
811
- test('validates token response', async () => {
812
- const secrets = createMockSecretService({
813
- APP_CREDS: defaultAppCredential,
814
- })
815
-
816
- // Response without access_token
817
- globalThis.fetch = mock.fn(async () =>
818
- mockFetchResponse({
819
- refresh_token: 'refresh-only',
820
- expires_in: 3600,
821
- })
822
- )
823
-
824
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
825
-
826
- await assert.rejects(
827
- async () =>
828
- await client.exchangeCode('code', 'http://localhost/callback'),
829
- /Invalid token response: missing access_token/
830
- )
831
- })
832
-
833
- test('validates access_token is a string', async () => {
834
- const secrets = createMockSecretService({
835
- APP_CREDS: defaultAppCredential,
836
- })
837
-
838
- // Response with non-string access_token
839
- globalThis.fetch = mock.fn(async () =>
840
- mockFetchResponse({
841
- access_token: 12345, // Number instead of string
842
- expires_in: 3600,
843
- })
844
- )
845
-
846
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
847
-
848
- await assert.rejects(
849
- async () =>
850
- await client.exchangeCode('code', 'http://localhost/callback'),
851
- /Invalid token response: missing access_token/
852
- )
853
- })
854
-
855
- test('sanitizes error messages', async () => {
856
- const expiredToken: OAuth2Token = {
857
- accessToken: 'expired',
858
- refreshToken: 'refresh',
859
- expiresAt: Date.now() - 1000,
860
- tokenType: 'Bearer',
861
- }
862
-
863
- const secrets = createMockSecretService({
864
- OAUTH_TOKENS: expiredToken,
865
- APP_CREDS: defaultAppCredential,
866
- })
867
-
868
- // Error response with sensitive info
869
- globalThis.fetch = mock.fn(async () =>
870
- mockFetchResponse(
871
- {
872
- error: 'invalid_grant',
873
- error_description: 'Refresh token expired for user@email.com',
874
- },
875
- 400,
876
- false
877
- )
878
- )
879
-
880
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
881
-
882
- await assert.rejects(
883
- async () => await client.getAccessToken(),
884
- (error: Error) => {
885
- // Should NOT contain the email or detailed error
886
- assert.ok(!error.message.includes('user@email.com'))
887
- assert.ok(!error.message.includes('Refresh token expired'))
888
- // Should contain generic error info
889
- assert.ok(error.message.includes('400'))
890
- return true
891
- }
892
- )
893
- })
894
-
895
- test('timeout works', async () => {
896
- const expiredToken: OAuth2Token = {
897
- accessToken: 'expired',
898
- refreshToken: 'refresh',
899
- expiresAt: Date.now() - 1000,
900
- tokenType: 'Bearer',
901
- }
902
-
903
- const secrets = createMockSecretService({
904
- OAUTH_TOKENS: expiredToken,
905
- APP_CREDS: defaultAppCredential,
906
- })
907
-
908
- const client = new OAuth2Client(defaultConfig, 'APP_CREDS', secrets)
909
-
910
- // The timeout is 30 seconds, which is too long for a test
911
- // Instead, we verify that the signal is being passed
912
- // by checking that an abort signal is present in the fetch call
913
- let signalPassed = false
914
- globalThis.fetch = mock.fn(
915
- async (_url: RequestInfo | URL, options?: RequestInit) => {
916
- signalPassed = options?.signal instanceof AbortSignal
917
- return mockFetchResponse({
918
- access_token: 'token',
919
- expires_in: 3600,
920
- token_type: 'Bearer',
921
- })
922
- }
923
- ) as typeof fetch
924
-
925
- await client.getAccessToken()
926
- assert.equal(signalPassed, true)
927
- })
928
- })
929
- })