raffel 0.1.2 → 0.2.2

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 (268) hide show
  1. package/README.md +314 -346
  2. package/dist/adapters/index.d.ts +3 -1
  3. package/dist/adapters/index.d.ts.map +1 -1
  4. package/dist/adapters/index.js +3 -1
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/adapters/s3db/adapter.d.ts.map +1 -1
  7. package/dist/adapters/s3db/adapter.js +0 -3
  8. package/dist/adapters/s3db/adapter.js.map +1 -1
  9. package/dist/adapters/udp.d.ts +83 -0
  10. package/dist/adapters/udp.d.ts.map +1 -0
  11. package/dist/adapters/udp.int.test.d.ts +5 -0
  12. package/dist/adapters/udp.int.test.d.ts.map +1 -0
  13. package/dist/adapters/udp.int.test.js +397 -0
  14. package/dist/adapters/udp.int.test.js.map +1 -0
  15. package/dist/adapters/udp.js +391 -0
  16. package/dist/adapters/udp.js.map +1 -0
  17. package/dist/cache/drivers/file.d.ts.map +1 -1
  18. package/dist/cache/drivers/file.js +13 -1
  19. package/dist/cache/drivers/file.js.map +1 -1
  20. package/dist/cache/drivers/memory.d.ts.map +1 -1
  21. package/dist/cache/drivers/memory.js +1 -0
  22. package/dist/cache/drivers/memory.js.map +1 -1
  23. package/dist/cache/types.d.ts +1 -0
  24. package/dist/cache/types.d.ts.map +1 -1
  25. package/dist/docs/generators/http-generator.d.ts.map +1 -1
  26. package/dist/docs/generators/http-generator.js +0 -1
  27. package/dist/docs/generators/http-generator.js.map +1 -1
  28. package/dist/graphql/graphql.int.test.d.ts +10 -0
  29. package/dist/graphql/graphql.int.test.d.ts.map +1 -0
  30. package/dist/graphql/graphql.int.test.js +698 -0
  31. package/dist/graphql/graphql.int.test.js.map +1 -0
  32. package/dist/graphql/schema-generator.d.ts.map +1 -1
  33. package/dist/graphql/schema-generator.js +20 -7
  34. package/dist/graphql/schema-generator.js.map +1 -1
  35. package/dist/http/auth.d.ts.map +1 -1
  36. package/dist/http/auth.js +15 -1
  37. package/dist/http/auth.js.map +1 -1
  38. package/dist/http/http.int.test.d.ts +7 -0
  39. package/dist/http/http.int.test.d.ts.map +1 -0
  40. package/dist/http/http.int.test.js +604 -0
  41. package/dist/http/http.int.test.js.map +1 -0
  42. package/dist/http/index.d.ts +2 -0
  43. package/dist/http/index.d.ts.map +1 -1
  44. package/dist/http/index.js +2 -0
  45. package/dist/http/index.js.map +1 -1
  46. package/dist/http/oauth2.d.ts.map +1 -1
  47. package/dist/http/oauth2.js +39 -0
  48. package/dist/http/oauth2.js.map +1 -1
  49. package/dist/http/oidc.d.ts.map +1 -1
  50. package/dist/http/oidc.js +9 -1
  51. package/dist/http/oidc.js.map +1 -1
  52. package/dist/http/session-redis.d.ts +187 -0
  53. package/dist/http/session-redis.d.ts.map +1 -0
  54. package/dist/http/session-redis.int.test.d.ts +8 -0
  55. package/dist/http/session-redis.int.test.d.ts.map +1 -0
  56. package/dist/http/session-redis.int.test.js +492 -0
  57. package/dist/http/session-redis.int.test.js.map +1 -0
  58. package/dist/http/session-redis.js +320 -0
  59. package/dist/http/session-redis.js.map +1 -0
  60. package/dist/index.d.ts +2 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +25 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/mcp/cli.js +2 -1
  65. package/dist/mcp/cli.js.map +1 -1
  66. package/dist/mcp/docs/adapters.d.ts.map +1 -1
  67. package/dist/mcp/docs/adapters.js +175 -145
  68. package/dist/mcp/docs/adapters.js.map +1 -1
  69. package/dist/mcp/docs/interceptors.d.ts +1 -1
  70. package/dist/mcp/docs/interceptors.d.ts.map +1 -1
  71. package/dist/mcp/docs/interceptors.js +231 -305
  72. package/dist/mcp/docs/interceptors.js.map +1 -1
  73. package/dist/mcp/docs/patterns.d.ts.map +1 -1
  74. package/dist/mcp/docs/patterns.js +20 -18
  75. package/dist/mcp/docs/patterns.js.map +1 -1
  76. package/dist/mcp/docs/quickstart.d.ts +1 -1
  77. package/dist/mcp/docs/quickstart.d.ts.map +1 -1
  78. package/dist/mcp/docs/quickstart.js +48 -46
  79. package/dist/mcp/docs/quickstart.js.map +1 -1
  80. package/dist/mcp/server.d.ts +1 -1
  81. package/dist/mcp/server.d.ts.map +1 -1
  82. package/dist/mcp/server.js +6 -7
  83. package/dist/mcp/server.js.map +1 -1
  84. package/dist/mcp/version.d.ts +7 -0
  85. package/dist/mcp/version.d.ts.map +1 -0
  86. package/dist/mcp/version.js +20 -0
  87. package/dist/mcp/version.js.map +1 -0
  88. package/dist/middleware/auth/oauth2.d.ts +294 -0
  89. package/dist/middleware/auth/oauth2.d.ts.map +1 -0
  90. package/dist/middleware/auth/oauth2.int.test.d.ts +2 -0
  91. package/dist/middleware/auth/oauth2.int.test.d.ts.map +1 -0
  92. package/dist/middleware/auth/oauth2.int.test.js +714 -0
  93. package/dist/middleware/auth/oauth2.int.test.js.map +1 -0
  94. package/dist/middleware/auth/oauth2.js +671 -0
  95. package/dist/middleware/auth/oauth2.js.map +1 -0
  96. package/dist/middleware/auth.d.ts +2 -0
  97. package/dist/middleware/auth.d.ts.map +1 -1
  98. package/dist/middleware/auth.js +16 -0
  99. package/dist/middleware/auth.js.map +1 -1
  100. package/dist/middleware/index.d.ts +5 -2
  101. package/dist/middleware/index.d.ts.map +1 -1
  102. package/dist/middleware/index.js +4 -0
  103. package/dist/middleware/index.js.map +1 -1
  104. package/dist/middleware/interceptors/circuit-breaker.d.ts.map +1 -1
  105. package/dist/middleware/interceptors/circuit-breaker.js +0 -1
  106. package/dist/middleware/interceptors/circuit-breaker.js.map +1 -1
  107. package/dist/middleware/interceptors/envelope.d.ts +176 -0
  108. package/dist/middleware/interceptors/envelope.d.ts.map +1 -0
  109. package/dist/middleware/interceptors/envelope.int.test.d.ts +5 -0
  110. package/dist/middleware/interceptors/envelope.int.test.d.ts.map +1 -0
  111. package/dist/middleware/interceptors/envelope.int.test.js +409 -0
  112. package/dist/middleware/interceptors/envelope.int.test.js.map +1 -0
  113. package/dist/middleware/interceptors/envelope.js +294 -0
  114. package/dist/middleware/interceptors/envelope.js.map +1 -0
  115. package/dist/middleware/interceptors/index.d.ts +2 -0
  116. package/dist/middleware/interceptors/index.d.ts.map +1 -1
  117. package/dist/middleware/interceptors/index.js +2 -0
  118. package/dist/middleware/interceptors/index.js.map +1 -1
  119. package/dist/middleware/types.d.ts +25 -0
  120. package/dist/middleware/types.d.ts.map +1 -1
  121. package/dist/rate-limit/drivers/drivers.int.test.d.ts +7 -0
  122. package/dist/rate-limit/drivers/drivers.int.test.d.ts.map +1 -0
  123. package/dist/rate-limit/drivers/drivers.int.test.js +466 -0
  124. package/dist/rate-limit/drivers/drivers.int.test.js.map +1 -0
  125. package/dist/server/builder.d.ts.map +1 -1
  126. package/dist/server/builder.int.test.js +41 -0
  127. package/dist/server/builder.int.test.js.map +1 -1
  128. package/dist/server/builder.js +72 -15
  129. package/dist/server/builder.js.map +1 -1
  130. package/dist/server/channel-utils.d.ts +4 -1
  131. package/dist/server/channel-utils.d.ts.map +1 -1
  132. package/dist/server/channel-utils.js +12 -2
  133. package/dist/server/channel-utils.js.map +1 -1
  134. package/dist/server/errors.d.ts.map +1 -1
  135. package/dist/server/errors.js +0 -22
  136. package/dist/server/errors.js.map +1 -1
  137. package/dist/server/fs-routes/watcher.js +1 -1
  138. package/dist/server/fs-routes/watcher.js.map +1 -1
  139. package/dist/server/index.d.ts +1 -1
  140. package/dist/server/index.d.ts.map +1 -1
  141. package/dist/server/index.js.map +1 -1
  142. package/dist/server/types.d.ts +37 -33
  143. package/dist/server/types.d.ts.map +1 -1
  144. package/dist/tracing/interceptor.d.ts.map +1 -1
  145. package/dist/tracing/interceptor.js +4 -5
  146. package/dist/tracing/interceptor.js.map +1 -1
  147. package/dist/types/envelope.d.ts +1 -1
  148. package/dist/types/envelope.d.ts.map +1 -1
  149. package/dist/types/envelope.js.map +1 -1
  150. package/dist/types/handlers.d.ts +8 -0
  151. package/dist/types/handlers.d.ts.map +1 -1
  152. package/dist/ui/core/index.d.ts +7 -0
  153. package/dist/ui/core/index.d.ts.map +1 -0
  154. package/dist/ui/docs/generators/content-types.d.ts +10 -0
  155. package/dist/ui/docs/generators/content-types.d.ts.map +1 -0
  156. package/dist/ui/docs/generators/errors-types.d.ts +409 -0
  157. package/dist/ui/docs/generators/errors-types.d.ts.map +1 -0
  158. package/dist/ui/docs/generators/errors.d.ts +88 -0
  159. package/dist/ui/docs/generators/errors.d.ts.map +1 -0
  160. package/dist/ui/docs/generators/grpc-generator.d.ts +53 -0
  161. package/dist/ui/docs/generators/grpc-generator.d.ts.map +1 -0
  162. package/dist/ui/docs/generators/http-generator.d.ts +49 -0
  163. package/dist/ui/docs/generators/http-generator.d.ts.map +1 -0
  164. package/dist/ui/docs/generators/index.d.ts +17 -0
  165. package/dist/ui/docs/generators/index.d.ts.map +1 -0
  166. package/dist/ui/docs/generators/jsonrpc-generator.d.ts +53 -0
  167. package/dist/ui/docs/generators/jsonrpc-generator.d.ts.map +1 -0
  168. package/dist/ui/docs/generators/schema-converter.d.ts +117 -0
  169. package/dist/ui/docs/generators/schema-converter.d.ts.map +1 -0
  170. package/dist/ui/docs/generators/streams-generator.d.ts +85 -0
  171. package/dist/ui/docs/generators/streams-generator.d.ts.map +1 -0
  172. package/dist/ui/docs/generators/tcp-generator.d.ts +133 -0
  173. package/dist/ui/docs/generators/tcp-generator.d.ts.map +1 -0
  174. package/dist/ui/docs/generators/udp-generator.d.ts +119 -0
  175. package/dist/ui/docs/generators/udp-generator.d.ts.map +1 -0
  176. package/dist/ui/docs/generators/usd-generator.d.ts +182 -0
  177. package/dist/ui/docs/generators/usd-generator.d.ts.map +1 -0
  178. package/dist/ui/docs/generators/websocket-generator.d.ts +49 -0
  179. package/dist/ui/docs/generators/websocket-generator.d.ts.map +1 -0
  180. package/dist/ui/docs/index.d.ts +31 -0
  181. package/dist/ui/docs/index.d.ts.map +1 -0
  182. package/dist/ui/docs/usd-middleware.d.ts +157 -0
  183. package/dist/ui/docs/usd-middleware.d.ts.map +1 -0
  184. package/dist/ui/errors/factories.d.ts +142 -0
  185. package/dist/ui/errors/factories.d.ts.map +1 -0
  186. package/dist/ui/errors/index.d.ts +9 -0
  187. package/dist/ui/errors/index.d.ts.map +1 -0
  188. package/dist/ui/server/fs-routes/index.d.ts +66 -0
  189. package/dist/ui/server/fs-routes/index.d.ts.map +1 -0
  190. package/dist/ui/server/fs-routes/loader.d.ts +28 -0
  191. package/dist/ui/server/fs-routes/loader.d.ts.map +1 -0
  192. package/dist/ui/server/fs-routes/middleware-processor.d.ts +19 -0
  193. package/dist/ui/server/fs-routes/middleware-processor.d.ts.map +1 -0
  194. package/dist/ui/server/fs-routes/resources/index.d.ts +8 -0
  195. package/dist/ui/server/fs-routes/resources/index.d.ts.map +1 -0
  196. package/dist/ui/server/fs-routes/resources/loader.d.ts +16 -0
  197. package/dist/ui/server/fs-routes/resources/loader.d.ts.map +1 -0
  198. package/dist/ui/server/fs-routes/resources/types.d.ts +256 -0
  199. package/dist/ui/server/fs-routes/resources/types.d.ts.map +1 -0
  200. package/dist/ui/server/fs-routes/rest/index.d.ts +8 -0
  201. package/dist/ui/server/fs-routes/rest/index.d.ts.map +1 -0
  202. package/dist/ui/server/fs-routes/rest/loader.d.ts +11 -0
  203. package/dist/ui/server/fs-routes/rest/loader.d.ts.map +1 -0
  204. package/dist/ui/server/fs-routes/rest/types.d.ts +288 -0
  205. package/dist/ui/server/fs-routes/rest/types.d.ts.map +1 -0
  206. package/dist/ui/server/fs-routes/tcp/index.d.ts +8 -0
  207. package/dist/ui/server/fs-routes/tcp/index.d.ts.map +1 -0
  208. package/dist/ui/server/fs-routes/tcp/loader.d.ts +15 -0
  209. package/dist/ui/server/fs-routes/tcp/loader.d.ts.map +1 -0
  210. package/dist/ui/server/fs-routes/tcp/types.d.ts +215 -0
  211. package/dist/ui/server/fs-routes/tcp/types.d.ts.map +1 -0
  212. package/dist/ui/server/fs-routes/types.d.ts +437 -0
  213. package/dist/ui/server/fs-routes/types.d.ts.map +1 -0
  214. package/dist/ui/server/fs-routes/udp/index.d.ts +8 -0
  215. package/dist/ui/server/fs-routes/udp/index.d.ts.map +1 -0
  216. package/dist/ui/server/fs-routes/udp/loader.d.ts +15 -0
  217. package/dist/ui/server/fs-routes/udp/loader.d.ts.map +1 -0
  218. package/dist/ui/server/fs-routes/udp/types.d.ts +164 -0
  219. package/dist/ui/server/fs-routes/udp/types.d.ts.map +1 -0
  220. package/dist/ui/server/fs-routes/watcher.d.ts +34 -0
  221. package/dist/ui/server/fs-routes/watcher.d.ts.map +1 -0
  222. package/dist/ui/types/envelope.d.ts +1 -1
  223. package/dist/ui/types/envelope.d.ts.map +1 -1
  224. package/dist/ui/types/handlers.d.ts +8 -0
  225. package/dist/ui/types/handlers.d.ts.map +1 -1
  226. package/dist/ui/usd/builder/document.d.ts.map +1 -1
  227. package/dist/ui/usd/export/openapi.d.ts.map +1 -1
  228. package/dist/ui/usd/parser/normalize.d.ts.map +1 -1
  229. package/dist/ui/usd/spec/types.d.ts +14 -20
  230. package/dist/ui/usd/spec/types.d.ts.map +1 -1
  231. package/dist/ui/usd/utils/refs.d.ts.map +1 -1
  232. package/dist/ui/usd/validator/index.d.ts.map +1 -1
  233. package/dist/ui/usd/validator/schema.d.ts.map +1 -1
  234. package/dist/ui/usd/validator/semantic.d.ts.map +1 -1
  235. package/dist/ui/utils/logger.d.ts +15 -0
  236. package/dist/ui/utils/logger.d.ts.map +1 -0
  237. package/dist/usd/builder/document.d.ts.map +1 -1
  238. package/dist/usd/builder/document.js.map +1 -1
  239. package/dist/usd/export/openapi.d.ts.map +1 -1
  240. package/dist/usd/export/openapi.js +2 -4
  241. package/dist/usd/export/openapi.js.map +1 -1
  242. package/dist/usd/parser/normalize.d.ts.map +1 -1
  243. package/dist/usd/parser/normalize.js +0 -1
  244. package/dist/usd/parser/normalize.js.map +1 -1
  245. package/dist/usd/usd.int.test.d.ts +10 -0
  246. package/dist/usd/usd.int.test.d.ts.map +1 -0
  247. package/dist/usd/usd.int.test.js +719 -0
  248. package/dist/usd/usd.int.test.js.map +1 -0
  249. package/dist/usd/utils/refs.d.ts.map +1 -1
  250. package/dist/usd/validator/index.d.ts.map +1 -1
  251. package/dist/usd/validator/index.js.map +1 -1
  252. package/dist/usd/validator/schema.d.ts.map +1 -1
  253. package/dist/usd/validator/schema.js.map +1 -1
  254. package/dist/usd/validator/semantic.d.ts.map +1 -1
  255. package/dist/usd/validator/semantic.js.map +1 -1
  256. package/package.json +1 -1
  257. package/dist/middleware/rate-limit.d.ts +0 -105
  258. package/dist/middleware/rate-limit.d.ts.map +0 -1
  259. package/dist/middleware/rate-limit.int.test.d.ts +0 -5
  260. package/dist/middleware/rate-limit.int.test.d.ts.map +0 -1
  261. package/dist/middleware/rate-limit.int.test.js +0 -350
  262. package/dist/middleware/rate-limit.int.test.js.map +0 -1
  263. package/dist/middleware/rate-limit.js +0 -206
  264. package/dist/middleware/rate-limit.js.map +0 -1
  265. package/dist/openapi/index.d.ts +0 -9
  266. package/dist/openapi/index.d.ts.map +0 -1
  267. package/dist/openapi/index.js +0 -9
  268. package/dist/openapi/index.js.map +0 -1
@@ -0,0 +1,714 @@
1
+ /**
2
+ * OAuth2/OIDC Strategy Integration Tests
3
+ */
4
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
5
+ import { createOAuth2Strategy, createOIDCStrategy, createGoogleOAuth2Strategy, createGitHubOAuth2Strategy, createMicrosoftOAuth2Strategy, createAppleOAuth2Strategy, createFacebookOAuth2Strategy, generateState, generateNonce, clearDiscoveryCache, OAuth2Providers, } from './oauth2.js';
6
+ // Mock fetch globally
7
+ const mockFetch = vi.fn();
8
+ global.fetch = mockFetch;
9
+ // Helper to create test envelope
10
+ function createTestEnvelope(metadata = {}) {
11
+ return {
12
+ id: 'test-1',
13
+ procedure: 'test.procedure',
14
+ type: 'request',
15
+ payload: {},
16
+ metadata,
17
+ context: createTestContext(),
18
+ };
19
+ }
20
+ // Helper to create test context
21
+ function createTestContext() {
22
+ return {
23
+ requestId: 'req-123',
24
+ tracing: {
25
+ traceId: 'trace-123',
26
+ spanId: 'span-123',
27
+ },
28
+ signal: new AbortController().signal,
29
+ extensions: new Map(),
30
+ };
31
+ }
32
+ describe('OAuth2 Strategy', () => {
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+ clearDiscoveryCache();
36
+ });
37
+ afterEach(() => {
38
+ vi.restoreAllMocks();
39
+ });
40
+ describe('createOAuth2Strategy', () => {
41
+ it('should create strategy with Google provider preset', () => {
42
+ const strategy = createOAuth2Strategy({
43
+ provider: 'google',
44
+ clientId: 'client-id',
45
+ clientSecret: 'client-secret',
46
+ redirectUri: 'https://example.com/callback',
47
+ });
48
+ expect(strategy.name).toBe('oauth2:google');
49
+ expect(strategy.authenticate).toBeDefined();
50
+ expect(strategy.getAuthorizationUrl).toBeDefined();
51
+ expect(strategy.exchangeCode).toBeDefined();
52
+ expect(strategy.refreshToken).toBeDefined();
53
+ expect(strategy.getUserInfo).toBeDefined();
54
+ });
55
+ it('should create strategy with custom provider', () => {
56
+ const strategy = createOAuth2Strategy({
57
+ provider: 'custom',
58
+ clientId: 'client-id',
59
+ clientSecret: 'client-secret',
60
+ redirectUri: 'https://example.com/callback',
61
+ authorizationUrl: 'https://custom.auth/authorize',
62
+ tokenUrl: 'https://custom.auth/token',
63
+ });
64
+ expect(strategy.name).toBe('oauth2:custom');
65
+ });
66
+ it('should return null when no Authorization header present', async () => {
67
+ const strategy = createOAuth2Strategy({
68
+ provider: 'google',
69
+ clientId: 'client-id',
70
+ clientSecret: 'client-secret',
71
+ redirectUri: 'https://example.com/callback',
72
+ });
73
+ const envelope = createTestEnvelope();
74
+ const ctx = createTestContext();
75
+ const result = await strategy.authenticate(envelope, ctx);
76
+ expect(result).toBeNull();
77
+ });
78
+ it('should return null for non-Bearer tokens', async () => {
79
+ const strategy = createOAuth2Strategy({
80
+ provider: 'google',
81
+ clientId: 'client-id',
82
+ clientSecret: 'client-secret',
83
+ redirectUri: 'https://example.com/callback',
84
+ });
85
+ const envelope = createTestEnvelope({
86
+ authorization: 'Basic dXNlcjpwYXNz',
87
+ });
88
+ const ctx = createTestContext();
89
+ const result = await strategy.authenticate(envelope, ctx);
90
+ expect(result).toBeNull();
91
+ });
92
+ it('should validate token via introspection endpoint', async () => {
93
+ mockFetch.mockResolvedValueOnce({
94
+ ok: true,
95
+ json: async () => ({
96
+ active: true,
97
+ sub: 'user-123',
98
+ username: 'testuser',
99
+ scope: 'openid email profile',
100
+ }),
101
+ });
102
+ const strategy = createOAuth2Strategy({
103
+ provider: 'custom',
104
+ clientId: 'client-id',
105
+ clientSecret: 'client-secret',
106
+ redirectUri: 'https://example.com/callback',
107
+ authorizationUrl: 'https://auth.example.com/authorize',
108
+ tokenUrl: 'https://auth.example.com/token',
109
+ introspectionUrl: 'https://auth.example.com/introspect',
110
+ tokenValidation: 'introspection', // Must specify introspection
111
+ });
112
+ const envelope = createTestEnvelope({
113
+ authorization: 'Bearer valid-token',
114
+ });
115
+ const ctx = createTestContext();
116
+ const result = await strategy.authenticate(envelope, ctx);
117
+ expect(result).toEqual({
118
+ authenticated: true,
119
+ principal: 'user-123',
120
+ claims: {
121
+ active: true,
122
+ sub: 'user-123',
123
+ username: 'testuser',
124
+ scope: 'openid email profile',
125
+ accessToken: 'valid-token',
126
+ },
127
+ });
128
+ });
129
+ it('should return unauthenticated for inactive token', async () => {
130
+ mockFetch.mockResolvedValueOnce({
131
+ ok: true,
132
+ json: async () => ({
133
+ active: false,
134
+ }),
135
+ });
136
+ const strategy = createOAuth2Strategy({
137
+ provider: 'custom',
138
+ clientId: 'client-id',
139
+ clientSecret: 'client-secret',
140
+ redirectUri: 'https://example.com/callback',
141
+ authorizationUrl: 'https://auth.example.com/authorize',
142
+ tokenUrl: 'https://auth.example.com/token',
143
+ introspectionUrl: 'https://auth.example.com/introspect',
144
+ tokenValidation: 'introspection', // Must specify introspection
145
+ });
146
+ const envelope = createTestEnvelope({
147
+ authorization: 'Bearer invalid-token',
148
+ });
149
+ const ctx = createTestContext();
150
+ const result = await strategy.authenticate(envelope, ctx);
151
+ expect(result).toEqual({ authenticated: false });
152
+ });
153
+ });
154
+ describe('getAuthorizationUrl', () => {
155
+ it('should generate authorization URL with all parameters', () => {
156
+ const strategy = createOAuth2Strategy({
157
+ provider: 'google',
158
+ clientId: 'client-id',
159
+ clientSecret: 'client-secret',
160
+ redirectUri: 'https://example.com/callback',
161
+ scopes: ['openid', 'email', 'profile'],
162
+ });
163
+ const url = strategy.getAuthorizationUrl({
164
+ state: 'random-state',
165
+ nonce: 'random-nonce',
166
+ additionalParams: {
167
+ prompt: 'consent',
168
+ },
169
+ });
170
+ expect(url).toContain('https://accounts.google.com/o/oauth2/v2/auth');
171
+ expect(url).toContain('client_id=client-id');
172
+ expect(url).toContain('redirect_uri=https%3A%2F%2Fexample.com%2Fcallback');
173
+ expect(url).toContain('response_type=code');
174
+ expect(url).toContain('scope=openid+email+profile');
175
+ expect(url).toContain('state=random-state');
176
+ expect(url).toContain('nonce=random-nonce');
177
+ expect(url).toContain('prompt=consent');
178
+ });
179
+ it('should include PKCE parameters via additionalParams', () => {
180
+ const strategy = createOAuth2Strategy({
181
+ provider: 'google',
182
+ clientId: 'client-id',
183
+ clientSecret: 'client-secret',
184
+ redirectUri: 'https://example.com/callback',
185
+ });
186
+ const url = strategy.getAuthorizationUrl({
187
+ additionalParams: {
188
+ code_challenge: 'challenge-value',
189
+ code_challenge_method: 'S256',
190
+ },
191
+ });
192
+ expect(url).toContain('code_challenge=challenge-value');
193
+ expect(url).toContain('code_challenge_method=S256');
194
+ });
195
+ });
196
+ describe('exchangeCode', () => {
197
+ it('should exchange authorization code for tokens', async () => {
198
+ mockFetch.mockResolvedValueOnce({
199
+ ok: true,
200
+ json: async () => ({
201
+ access_token: 'access-token-123',
202
+ token_type: 'Bearer',
203
+ expires_in: 3600,
204
+ refresh_token: 'refresh-token-123',
205
+ scope: 'openid email profile',
206
+ id_token: 'id-token-123',
207
+ }),
208
+ });
209
+ const strategy = createOAuth2Strategy({
210
+ provider: 'google',
211
+ clientId: 'client-id',
212
+ clientSecret: 'client-secret',
213
+ redirectUri: 'https://example.com/callback',
214
+ });
215
+ const tokens = await strategy.exchangeCode('auth-code-123');
216
+ expect(tokens).toEqual({
217
+ accessToken: 'access-token-123',
218
+ tokenType: 'Bearer',
219
+ expiresIn: 3600,
220
+ refreshToken: 'refresh-token-123',
221
+ scope: 'openid email profile',
222
+ idToken: 'id-token-123',
223
+ });
224
+ });
225
+ it('should include required parameters in token request', async () => {
226
+ mockFetch.mockResolvedValueOnce({
227
+ ok: true,
228
+ json: async () => ({
229
+ access_token: 'access-token',
230
+ token_type: 'Bearer',
231
+ }),
232
+ });
233
+ const strategy = createOAuth2Strategy({
234
+ provider: 'google',
235
+ clientId: 'client-id',
236
+ clientSecret: 'client-secret',
237
+ redirectUri: 'https://example.com/callback',
238
+ });
239
+ await strategy.exchangeCode('auth-code');
240
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
241
+ method: 'POST',
242
+ body: expect.stringContaining('grant_type=authorization_code'),
243
+ }));
244
+ });
245
+ it('should throw on token exchange failure', async () => {
246
+ mockFetch.mockResolvedValueOnce({
247
+ ok: false,
248
+ status: 400,
249
+ text: async () => '{"error": "invalid_grant"}',
250
+ });
251
+ const strategy = createOAuth2Strategy({
252
+ provider: 'google',
253
+ clientId: 'client-id',
254
+ clientSecret: 'client-secret',
255
+ redirectUri: 'https://example.com/callback',
256
+ });
257
+ await expect(strategy.exchangeCode('invalid-code')).rejects.toThrow('Token exchange failed');
258
+ });
259
+ });
260
+ describe('refreshToken', () => {
261
+ it('should refresh access token', async () => {
262
+ mockFetch.mockResolvedValueOnce({
263
+ ok: true,
264
+ json: async () => ({
265
+ access_token: 'new-access-token',
266
+ token_type: 'Bearer',
267
+ expires_in: 3600,
268
+ }),
269
+ });
270
+ const strategy = createOAuth2Strategy({
271
+ provider: 'google',
272
+ clientId: 'client-id',
273
+ clientSecret: 'client-secret',
274
+ redirectUri: 'https://example.com/callback',
275
+ });
276
+ const tokens = await strategy.refreshToken('refresh-token-123');
277
+ expect(tokens.accessToken).toBe('new-access-token');
278
+ expect(tokens.refreshToken).toBe('refresh-token-123'); // Original preserved
279
+ });
280
+ it('should use new refresh token if provided', async () => {
281
+ mockFetch.mockResolvedValueOnce({
282
+ ok: true,
283
+ json: async () => ({
284
+ access_token: 'new-access-token',
285
+ token_type: 'Bearer',
286
+ refresh_token: 'new-refresh-token',
287
+ }),
288
+ });
289
+ const strategy = createOAuth2Strategy({
290
+ provider: 'google',
291
+ clientId: 'client-id',
292
+ clientSecret: 'client-secret',
293
+ redirectUri: 'https://example.com/callback',
294
+ });
295
+ const tokens = await strategy.refreshToken('old-refresh-token');
296
+ expect(tokens.refreshToken).toBe('new-refresh-token');
297
+ });
298
+ });
299
+ describe('getUserInfo', () => {
300
+ it('should fetch user info', async () => {
301
+ mockFetch.mockResolvedValueOnce({
302
+ ok: true,
303
+ json: async () => ({
304
+ sub: 'user-123',
305
+ email: 'user@example.com',
306
+ name: 'Test User',
307
+ picture: 'https://example.com/avatar.jpg',
308
+ }),
309
+ });
310
+ const strategy = createOAuth2Strategy({
311
+ provider: 'google',
312
+ clientId: 'client-id',
313
+ clientSecret: 'client-secret',
314
+ redirectUri: 'https://example.com/callback',
315
+ });
316
+ const userInfo = await strategy.getUserInfo('access-token');
317
+ expect(userInfo).toEqual({
318
+ sub: 'user-123',
319
+ email: 'user@example.com',
320
+ name: 'Test User',
321
+ picture: 'https://example.com/avatar.jpg',
322
+ });
323
+ });
324
+ it('should normalize GitHub response', async () => {
325
+ mockFetch.mockResolvedValueOnce({
326
+ ok: true,
327
+ json: async () => ({
328
+ id: 12345,
329
+ login: 'testuser',
330
+ name: 'Test User',
331
+ email: 'test@github.com',
332
+ avatar_url: 'https://github.com/avatar.jpg',
333
+ }),
334
+ });
335
+ const strategy = createOAuth2Strategy({
336
+ provider: 'github',
337
+ clientId: 'client-id',
338
+ clientSecret: 'client-secret',
339
+ redirectUri: 'https://example.com/callback',
340
+ });
341
+ const userInfo = await strategy.getUserInfo('access-token');
342
+ expect(userInfo.sub).toBe('12345'); // Normalized from id
343
+ expect(userInfo.name).toBe('Test User');
344
+ expect(userInfo.email).toBe('test@github.com');
345
+ expect(userInfo.picture).toBe('https://github.com/avatar.jpg');
346
+ });
347
+ it('should throw when userInfoUrl not configured', async () => {
348
+ const strategy = createOAuth2Strategy({
349
+ provider: 'custom',
350
+ clientId: 'client-id',
351
+ clientSecret: 'client-secret',
352
+ redirectUri: 'https://example.com/callback',
353
+ authorizationUrl: 'https://auth.example.com/authorize',
354
+ tokenUrl: 'https://auth.example.com/token',
355
+ // No userInfoUrl
356
+ });
357
+ await expect(strategy.getUserInfo('access-token')).rejects.toThrow('UserInfo endpoint not configured');
358
+ });
359
+ });
360
+ describe('revokeToken', () => {
361
+ it('should revoke token when revocationUrl configured', async () => {
362
+ mockFetch.mockResolvedValueOnce({ ok: true });
363
+ const strategy = createOAuth2Strategy({
364
+ provider: 'custom',
365
+ clientId: 'client-id',
366
+ clientSecret: 'client-secret',
367
+ redirectUri: 'https://example.com/callback',
368
+ authorizationUrl: 'https://auth.example.com/authorize',
369
+ tokenUrl: 'https://auth.example.com/token',
370
+ revocationUrl: 'https://auth.example.com/revoke',
371
+ });
372
+ await expect(strategy.revokeToken('access-token')).resolves.toBeUndefined();
373
+ expect(mockFetch).toHaveBeenCalledWith('https://auth.example.com/revoke', expect.objectContaining({
374
+ method: 'POST',
375
+ }));
376
+ });
377
+ it('should not have revokeToken when revocationUrl not configured', () => {
378
+ const strategy = createOAuth2Strategy({
379
+ provider: 'custom',
380
+ clientId: 'client-id',
381
+ clientSecret: 'client-secret',
382
+ redirectUri: 'https://example.com/callback',
383
+ authorizationUrl: 'https://auth.example.com/authorize',
384
+ tokenUrl: 'https://auth.example.com/token',
385
+ });
386
+ expect(strategy.revokeToken).toBeUndefined();
387
+ });
388
+ });
389
+ });
390
+ describe('OIDC Strategy', () => {
391
+ beforeEach(() => {
392
+ vi.clearAllMocks();
393
+ clearDiscoveryCache();
394
+ });
395
+ afterEach(() => {
396
+ vi.restoreAllMocks();
397
+ });
398
+ describe('createOIDCStrategy', () => {
399
+ it('should create strategy with lazy initialization', () => {
400
+ // No fetch needed during creation - discovery is lazy
401
+ const strategy = createOIDCStrategy({
402
+ issuer: 'https://auth.example.com',
403
+ clientId: 'client-id',
404
+ clientSecret: 'client-secret',
405
+ redirectUri: 'https://example.com/callback',
406
+ });
407
+ expect(strategy.name).toBe('oidc');
408
+ expect(strategy.authenticate).toBeDefined();
409
+ expect(strategy.getAuthorizationUrl).toBeDefined();
410
+ expect(strategy.exchangeCode).toBeDefined();
411
+ expect(strategy.validateIdToken).toBeDefined();
412
+ });
413
+ it('should expose discovery document after initialization', async () => {
414
+ // Discovery is fetched lazily when methods are called
415
+ // We can verify the strategy has the right interface
416
+ const strategy = createOIDCStrategy({
417
+ issuer: 'https://auth.example.com',
418
+ clientId: 'client-id',
419
+ clientSecret: 'client-secret',
420
+ redirectUri: 'https://example.com/callback',
421
+ });
422
+ // Before initialization, discovery is null
423
+ expect(strategy.discovery).toBeNull();
424
+ });
425
+ it('should cache discovery document', async () => {
426
+ mockFetch.mockResolvedValue({
427
+ ok: true,
428
+ json: async () => ({
429
+ issuer: 'https://auth.example.com',
430
+ authorization_endpoint: 'https://auth.example.com/authorize',
431
+ token_endpoint: 'https://auth.example.com/token',
432
+ userinfo_endpoint: 'https://auth.example.com/userinfo',
433
+ jwks_uri: 'https://auth.example.com/.well-known/jwks.json',
434
+ }),
435
+ });
436
+ const strategy1 = createOIDCStrategy({
437
+ issuer: 'https://auth.example.com',
438
+ clientId: 'client-id',
439
+ clientSecret: 'client-secret',
440
+ redirectUri: 'https://example.com/callback',
441
+ });
442
+ const strategy2 = createOIDCStrategy({
443
+ issuer: 'https://auth.example.com',
444
+ clientId: 'client-id-2',
445
+ clientSecret: 'client-secret-2',
446
+ redirectUri: 'https://example.com/callback2',
447
+ });
448
+ const envelope = createTestEnvelope();
449
+ const ctx = createTestContext();
450
+ // Trigger discovery for both
451
+ await strategy1.authenticate(envelope, ctx);
452
+ await strategy2.authenticate(envelope, ctx);
453
+ // Discovery should only be fetched once (cached)
454
+ expect(mockFetch).toHaveBeenCalledTimes(1);
455
+ });
456
+ it('should clear discovery cache', async () => {
457
+ mockFetch.mockResolvedValue({
458
+ ok: true,
459
+ json: async () => ({
460
+ issuer: 'https://auth.example.com',
461
+ authorization_endpoint: 'https://auth.example.com/authorize',
462
+ token_endpoint: 'https://auth.example.com/token',
463
+ userinfo_endpoint: 'https://auth.example.com/userinfo',
464
+ jwks_uri: 'https://auth.example.com/.well-known/jwks.json',
465
+ }),
466
+ });
467
+ const strategy1 = createOIDCStrategy({
468
+ issuer: 'https://auth.example.com',
469
+ clientId: 'client-id',
470
+ clientSecret: 'client-secret',
471
+ redirectUri: 'https://example.com/callback',
472
+ });
473
+ const envelope = createTestEnvelope();
474
+ const ctx = createTestContext();
475
+ // First discovery
476
+ await strategy1.authenticate(envelope, ctx);
477
+ expect(mockFetch).toHaveBeenCalledTimes(1);
478
+ // Clear cache
479
+ clearDiscoveryCache();
480
+ // Create new strategy and trigger discovery
481
+ const strategy2 = createOIDCStrategy({
482
+ issuer: 'https://auth.example.com',
483
+ clientId: 'client-id',
484
+ clientSecret: 'client-secret',
485
+ redirectUri: 'https://example.com/callback',
486
+ });
487
+ await strategy2.authenticate(envelope, ctx);
488
+ // Should fetch twice (cache was cleared)
489
+ expect(mockFetch).toHaveBeenCalledTimes(2);
490
+ });
491
+ it('should throw on discovery failure', async () => {
492
+ mockFetch.mockResolvedValueOnce({
493
+ ok: false,
494
+ status: 404,
495
+ });
496
+ const strategy = createOIDCStrategy({
497
+ issuer: 'https://invalid.example.com',
498
+ clientId: 'client-id',
499
+ clientSecret: 'client-secret',
500
+ redirectUri: 'https://example.com/callback',
501
+ });
502
+ const envelope = createTestEnvelope();
503
+ const ctx = createTestContext();
504
+ // Discovery failure happens on first authenticate
505
+ await expect(strategy.authenticate(envelope, ctx)).rejects.toThrow('OIDC discovery failed');
506
+ });
507
+ });
508
+ describe('validateIdToken', () => {
509
+ it('should decode and validate ID token claims', async () => {
510
+ mockFetch.mockResolvedValueOnce({
511
+ ok: true,
512
+ json: async () => ({
513
+ issuer: 'https://auth.example.com',
514
+ authorization_endpoint: 'https://auth.example.com/authorize',
515
+ token_endpoint: 'https://auth.example.com/token',
516
+ userinfo_endpoint: 'https://auth.example.com/userinfo',
517
+ jwks_uri: 'https://auth.example.com/.well-known/jwks.json',
518
+ }),
519
+ });
520
+ const strategy = createOIDCStrategy({
521
+ issuer: 'https://auth.example.com',
522
+ clientId: 'client-id',
523
+ clientSecret: 'client-secret',
524
+ redirectUri: 'https://example.com/callback',
525
+ });
526
+ // Create a mock JWT (header.payload.signature)
527
+ const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
528
+ const payload = Buffer.from(JSON.stringify({
529
+ iss: 'https://auth.example.com',
530
+ sub: 'user-123',
531
+ aud: 'client-id',
532
+ exp: Math.floor(Date.now() / 1000) + 3600,
533
+ iat: Math.floor(Date.now() / 1000),
534
+ email: 'user@example.com',
535
+ name: 'Test User',
536
+ })).toString('base64url');
537
+ const signature = 'fake-signature';
538
+ const idToken = `${header}.${payload}.${signature}`;
539
+ // validateIdToken is the method, not getIdTokenClaims
540
+ const claims = await strategy.validateIdToken(idToken);
541
+ expect(claims.sub).toBe('user-123');
542
+ expect(claims.email).toBe('user@example.com');
543
+ expect(claims.name).toBe('Test User');
544
+ });
545
+ });
546
+ });
547
+ describe('Provider Presets', () => {
548
+ it('should have all expected providers', () => {
549
+ expect(OAuth2Providers.google).toBeDefined();
550
+ expect(OAuth2Providers.github).toBeDefined();
551
+ expect(OAuth2Providers.microsoft).toBeDefined();
552
+ expect(OAuth2Providers.apple).toBeDefined();
553
+ expect(OAuth2Providers.facebook).toBeDefined();
554
+ });
555
+ it('should have correct Google URLs', () => {
556
+ expect(OAuth2Providers.google.authorizationUrl).toBe('https://accounts.google.com/o/oauth2/v2/auth');
557
+ expect(OAuth2Providers.google.tokenUrl).toBe('https://oauth2.googleapis.com/token');
558
+ expect(OAuth2Providers.google.userInfoUrl).toBe('https://openidconnect.googleapis.com/v1/userinfo');
559
+ });
560
+ it('should have correct GitHub URLs', () => {
561
+ expect(OAuth2Providers.github.authorizationUrl).toBe('https://github.com/login/oauth/authorize');
562
+ expect(OAuth2Providers.github.tokenUrl).toBe('https://github.com/login/oauth/access_token');
563
+ expect(OAuth2Providers.github.userInfoUrl).toBe('https://api.github.com/user');
564
+ });
565
+ });
566
+ describe('Provider Shortcut Functions', () => {
567
+ it('should create Google strategy', () => {
568
+ const strategy = createGoogleOAuth2Strategy({
569
+ clientId: 'client-id',
570
+ clientSecret: 'client-secret',
571
+ redirectUri: 'https://example.com/callback',
572
+ });
573
+ expect(strategy.name).toBe('oauth2:google');
574
+ });
575
+ it('should create GitHub strategy', () => {
576
+ const strategy = createGitHubOAuth2Strategy({
577
+ clientId: 'client-id',
578
+ clientSecret: 'client-secret',
579
+ redirectUri: 'https://example.com/callback',
580
+ });
581
+ expect(strategy.name).toBe('oauth2:github');
582
+ });
583
+ it('should create Microsoft strategy with tenant', () => {
584
+ const strategy = createMicrosoftOAuth2Strategy({
585
+ clientId: 'client-id',
586
+ clientSecret: 'client-secret',
587
+ redirectUri: 'https://example.com/callback',
588
+ tenant: 'my-tenant',
589
+ });
590
+ expect(strategy.name).toBe('oauth2:custom'); // Uses custom provider internally
591
+ });
592
+ it('should create Apple strategy', () => {
593
+ const strategy = createAppleOAuth2Strategy({
594
+ clientId: 'client-id',
595
+ clientSecret: 'client-secret',
596
+ redirectUri: 'https://example.com/callback',
597
+ });
598
+ expect(strategy.name).toBe('oauth2:apple');
599
+ });
600
+ it('should create Facebook strategy', () => {
601
+ const strategy = createFacebookOAuth2Strategy({
602
+ clientId: 'client-id',
603
+ clientSecret: 'client-secret',
604
+ redirectUri: 'https://example.com/callback',
605
+ });
606
+ expect(strategy.name).toBe('oauth2:facebook');
607
+ });
608
+ });
609
+ describe('Utility Functions', () => {
610
+ describe('generateState', () => {
611
+ it('should generate URL-safe random string', () => {
612
+ const state = generateState();
613
+ expect(state).toBeDefined();
614
+ expect(typeof state).toBe('string');
615
+ expect(state.length).toBeGreaterThan(0);
616
+ // Base64url should not contain +, /, or =
617
+ expect(state).not.toMatch(/[+/=]/);
618
+ });
619
+ it('should generate different values each time', () => {
620
+ const state1 = generateState();
621
+ const state2 = generateState();
622
+ expect(state1).not.toBe(state2);
623
+ });
624
+ it('should respect length parameter', () => {
625
+ const state16 = generateState(16);
626
+ const state64 = generateState(64);
627
+ // Base64url encoding produces ~4/3 chars per byte
628
+ expect(state16.length).toBeLessThan(state64.length);
629
+ });
630
+ });
631
+ describe('generateNonce', () => {
632
+ it('should generate URL-safe random string', () => {
633
+ const nonce = generateNonce();
634
+ expect(nonce).toBeDefined();
635
+ expect(typeof nonce).toBe('string');
636
+ expect(nonce.length).toBeGreaterThan(0);
637
+ // Base64url should not contain +, /, or =
638
+ expect(nonce).not.toMatch(/[+/=]/);
639
+ });
640
+ it('should generate different values each time', () => {
641
+ const nonce1 = generateNonce();
642
+ const nonce2 = generateNonce();
643
+ expect(nonce1).not.toBe(nonce2);
644
+ });
645
+ });
646
+ });
647
+ describe('Client Credentials in Body', () => {
648
+ beforeEach(() => {
649
+ vi.clearAllMocks();
650
+ });
651
+ it('should send credentials in body when clientCredentialsInBody is true', async () => {
652
+ mockFetch.mockResolvedValueOnce({
653
+ ok: true,
654
+ json: async () => ({
655
+ access_token: 'access-token',
656
+ token_type: 'Bearer',
657
+ }),
658
+ });
659
+ const strategy = createOAuth2Strategy({
660
+ provider: 'custom',
661
+ clientId: 'client-id',
662
+ clientSecret: 'client-secret',
663
+ redirectUri: 'https://example.com/callback',
664
+ authorizationUrl: 'https://auth.example.com/authorize',
665
+ tokenUrl: 'https://auth.example.com/token',
666
+ clientCredentialsInBody: true,
667
+ });
668
+ await strategy.exchangeCode('auth-code');
669
+ const [, requestInit] = mockFetch.mock.calls[0];
670
+ const body = requestInit.body;
671
+ expect(body).toContain('client_id=client-id');
672
+ expect(body).toContain('client_secret=client-secret');
673
+ expect(requestInit.headers).not.toHaveProperty('Authorization');
674
+ });
675
+ it('should send credentials in Authorization header by default', async () => {
676
+ mockFetch.mockResolvedValueOnce({
677
+ ok: true,
678
+ json: async () => ({
679
+ access_token: 'access-token',
680
+ token_type: 'Bearer',
681
+ }),
682
+ });
683
+ const strategy = createOAuth2Strategy({
684
+ provider: 'custom',
685
+ clientId: 'client-id',
686
+ clientSecret: 'client-secret',
687
+ redirectUri: 'https://example.com/callback',
688
+ authorizationUrl: 'https://auth.example.com/authorize',
689
+ tokenUrl: 'https://auth.example.com/token',
690
+ });
691
+ await strategy.exchangeCode('auth-code');
692
+ const [, requestInit] = mockFetch.mock.calls[0];
693
+ const headers = requestInit.headers;
694
+ expect(headers['Authorization']).toMatch(/^Basic /);
695
+ });
696
+ });
697
+ describe('Strategy Configuration', () => {
698
+ beforeEach(() => {
699
+ vi.clearAllMocks();
700
+ });
701
+ it('should respect timeout configuration', () => {
702
+ // The strategy accepts a timeout option
703
+ const strategy = createOAuth2Strategy({
704
+ provider: 'google',
705
+ clientId: 'client-id',
706
+ clientSecret: 'client-secret',
707
+ redirectUri: 'https://example.com/callback',
708
+ timeout: 1000,
709
+ });
710
+ // Strategy is created successfully with timeout
711
+ expect(strategy.name).toBe('oauth2:google');
712
+ });
713
+ });
714
+ //# sourceMappingURL=oauth2.int.test.js.map