@nocobase/plugin-idp-oauth 2.1.0-alpha.17 → 2.1.0-alpha.18

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 (214) hide show
  1. package/build.config.ts +1 -1
  2. package/dist/externalVersion.js +4 -4
  3. package/dist/node_modules/light-my-request/package.json +1 -1
  4. package/dist/node_modules/undici/LICENSE +21 -0
  5. package/dist/node_modules/undici/README.md +741 -0
  6. package/dist/node_modules/undici/docs/docs/api/Agent.md +84 -0
  7. package/dist/node_modules/undici/docs/docs/api/BalancedPool.md +99 -0
  8. package/dist/node_modules/undici/docs/docs/api/CacheStorage.md +30 -0
  9. package/dist/node_modules/undici/docs/docs/api/CacheStore.md +164 -0
  10. package/dist/node_modules/undici/docs/docs/api/Client.md +285 -0
  11. package/dist/node_modules/undici/docs/docs/api/ClientStats.md +27 -0
  12. package/dist/node_modules/undici/docs/docs/api/Connector.md +115 -0
  13. package/dist/node_modules/undici/docs/docs/api/ContentType.md +57 -0
  14. package/dist/node_modules/undici/docs/docs/api/Cookies.md +101 -0
  15. package/dist/node_modules/undici/docs/docs/api/Debug.md +62 -0
  16. package/dist/node_modules/undici/docs/docs/api/DiagnosticsChannel.md +315 -0
  17. package/dist/node_modules/undici/docs/docs/api/Dispatcher.md +1392 -0
  18. package/dist/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +159 -0
  19. package/dist/node_modules/undici/docs/docs/api/Errors.md +49 -0
  20. package/dist/node_modules/undici/docs/docs/api/EventSource.md +45 -0
  21. package/dist/node_modules/undici/docs/docs/api/Fetch.md +60 -0
  22. package/dist/node_modules/undici/docs/docs/api/GlobalInstallation.md +139 -0
  23. package/dist/node_modules/undici/docs/docs/api/H2CClient.md +263 -0
  24. package/dist/node_modules/undici/docs/docs/api/MockAgent.md +603 -0
  25. package/dist/node_modules/undici/docs/docs/api/MockCallHistory.md +197 -0
  26. package/dist/node_modules/undici/docs/docs/api/MockCallHistoryLog.md +43 -0
  27. package/dist/node_modules/undici/docs/docs/api/MockClient.md +81 -0
  28. package/dist/node_modules/undici/docs/docs/api/MockErrors.md +12 -0
  29. package/dist/node_modules/undici/docs/docs/api/MockPool.md +555 -0
  30. package/dist/node_modules/undici/docs/docs/api/Pool.md +84 -0
  31. package/dist/node_modules/undici/docs/docs/api/PoolStats.md +35 -0
  32. package/dist/node_modules/undici/docs/docs/api/ProxyAgent.md +229 -0
  33. package/dist/node_modules/undici/docs/docs/api/RedirectHandler.md +93 -0
  34. package/dist/node_modules/undici/docs/docs/api/RetryAgent.md +50 -0
  35. package/dist/node_modules/undici/docs/docs/api/RetryHandler.md +118 -0
  36. package/dist/node_modules/undici/docs/docs/api/RoundRobinPool.md +145 -0
  37. package/dist/node_modules/undici/docs/docs/api/SnapshotAgent.md +616 -0
  38. package/dist/node_modules/undici/docs/docs/api/Socks5ProxyAgent.md +274 -0
  39. package/dist/node_modules/undici/docs/docs/api/Util.md +25 -0
  40. package/dist/node_modules/undici/docs/docs/api/WebSocket.md +141 -0
  41. package/dist/node_modules/undici/docs/docs/api/api-lifecycle.md +91 -0
  42. package/dist/node_modules/undici/docs/docs/best-practices/client-certificate.md +64 -0
  43. package/dist/node_modules/undici/docs/docs/best-practices/crawling.md +58 -0
  44. package/dist/node_modules/undici/docs/docs/best-practices/mocking-request.md +190 -0
  45. package/dist/node_modules/undici/docs/docs/best-practices/proxy.md +127 -0
  46. package/dist/node_modules/undici/docs/docs/best-practices/undici-vs-builtin-fetch.md +224 -0
  47. package/dist/node_modules/undici/docs/docs/best-practices/writing-tests.md +63 -0
  48. package/dist/node_modules/undici/index-fetch.js +65 -0
  49. package/dist/node_modules/undici/index.d.ts +3 -0
  50. package/dist/node_modules/undici/index.js +234 -0
  51. package/dist/node_modules/undici/lib/api/abort-signal.js +59 -0
  52. package/dist/node_modules/undici/lib/api/api-connect.js +110 -0
  53. package/dist/node_modules/undici/lib/api/api-pipeline.js +252 -0
  54. package/dist/node_modules/undici/lib/api/api-request.js +214 -0
  55. package/dist/node_modules/undici/lib/api/api-stream.js +209 -0
  56. package/dist/node_modules/undici/lib/api/api-upgrade.js +111 -0
  57. package/dist/node_modules/undici/lib/api/index.js +7 -0
  58. package/dist/node_modules/undici/lib/api/readable.js +580 -0
  59. package/dist/node_modules/undici/lib/cache/memory-cache-store.js +234 -0
  60. package/dist/node_modules/undici/lib/cache/sqlite-cache-store.js +461 -0
  61. package/dist/node_modules/undici/lib/core/connect.js +137 -0
  62. package/dist/node_modules/undici/lib/core/constants.js +143 -0
  63. package/dist/node_modules/undici/lib/core/diagnostics.js +227 -0
  64. package/dist/node_modules/undici/lib/core/errors.js +477 -0
  65. package/dist/node_modules/undici/lib/core/request.js +438 -0
  66. package/dist/node_modules/undici/lib/core/socks5-client.js +407 -0
  67. package/dist/node_modules/undici/lib/core/socks5-utils.js +203 -0
  68. package/dist/node_modules/undici/lib/core/symbols.js +75 -0
  69. package/dist/node_modules/undici/lib/core/tree.js +160 -0
  70. package/dist/node_modules/undici/lib/core/util.js +992 -0
  71. package/dist/node_modules/undici/lib/dispatcher/agent.js +158 -0
  72. package/dist/node_modules/undici/lib/dispatcher/balanced-pool.js +219 -0
  73. package/dist/node_modules/undici/lib/dispatcher/client-h1.js +1610 -0
  74. package/dist/node_modules/undici/lib/dispatcher/client-h2.js +995 -0
  75. package/dist/node_modules/undici/lib/dispatcher/client.js +659 -0
  76. package/dist/node_modules/undici/lib/dispatcher/dispatcher-base.js +165 -0
  77. package/dist/node_modules/undici/lib/dispatcher/dispatcher.js +48 -0
  78. package/dist/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js +146 -0
  79. package/dist/node_modules/undici/lib/dispatcher/fixed-queue.js +135 -0
  80. package/dist/node_modules/undici/lib/dispatcher/h2c-client.js +51 -0
  81. package/dist/node_modules/undici/lib/dispatcher/pool-base.js +214 -0
  82. package/dist/node_modules/undici/lib/dispatcher/pool.js +118 -0
  83. package/dist/node_modules/undici/lib/dispatcher/proxy-agent.js +318 -0
  84. package/dist/node_modules/undici/lib/dispatcher/retry-agent.js +35 -0
  85. package/dist/node_modules/undici/lib/dispatcher/round-robin-pool.js +137 -0
  86. package/dist/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +249 -0
  87. package/dist/node_modules/undici/lib/encoding/index.js +33 -0
  88. package/dist/node_modules/undici/lib/global.js +50 -0
  89. package/dist/node_modules/undici/lib/handler/cache-handler.js +578 -0
  90. package/dist/node_modules/undici/lib/handler/cache-revalidation-handler.js +124 -0
  91. package/dist/node_modules/undici/lib/handler/decorator-handler.js +67 -0
  92. package/dist/node_modules/undici/lib/handler/deduplication-handler.js +460 -0
  93. package/dist/node_modules/undici/lib/handler/redirect-handler.js +238 -0
  94. package/dist/node_modules/undici/lib/handler/retry-handler.js +394 -0
  95. package/dist/node_modules/undici/lib/handler/unwrap-handler.js +100 -0
  96. package/dist/node_modules/undici/lib/handler/wrap-handler.js +105 -0
  97. package/dist/node_modules/undici/lib/interceptor/cache.js +495 -0
  98. package/dist/node_modules/undici/lib/interceptor/decompress.js +259 -0
  99. package/dist/node_modules/undici/lib/interceptor/deduplicate.js +117 -0
  100. package/dist/node_modules/undici/lib/interceptor/dns.js +571 -0
  101. package/dist/node_modules/undici/lib/interceptor/dump.js +112 -0
  102. package/dist/node_modules/undici/lib/interceptor/redirect.js +21 -0
  103. package/dist/node_modules/undici/lib/interceptor/response-error.js +95 -0
  104. package/dist/node_modules/undici/lib/interceptor/retry.js +19 -0
  105. package/dist/node_modules/undici/lib/llhttp/.gitkeep +0 -0
  106. package/dist/node_modules/undici/lib/llhttp/constants.d.ts +195 -0
  107. package/dist/node_modules/undici/lib/llhttp/constants.js +531 -0
  108. package/dist/node_modules/undici/lib/llhttp/llhttp-wasm.js +15 -0
  109. package/dist/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js +15 -0
  110. package/dist/node_modules/undici/lib/llhttp/utils.d.ts +2 -0
  111. package/dist/node_modules/undici/lib/llhttp/utils.js +12 -0
  112. package/dist/node_modules/undici/lib/mock/mock-agent.js +232 -0
  113. package/dist/node_modules/undici/lib/mock/mock-call-history.js +248 -0
  114. package/dist/node_modules/undici/lib/mock/mock-client.js +68 -0
  115. package/dist/node_modules/undici/lib/mock/mock-errors.js +29 -0
  116. package/dist/node_modules/undici/lib/mock/mock-interceptor.js +209 -0
  117. package/dist/node_modules/undici/lib/mock/mock-pool.js +68 -0
  118. package/dist/node_modules/undici/lib/mock/mock-symbols.js +32 -0
  119. package/dist/node_modules/undici/lib/mock/mock-utils.js +486 -0
  120. package/dist/node_modules/undici/lib/mock/pending-interceptors-formatter.js +43 -0
  121. package/dist/node_modules/undici/lib/mock/snapshot-agent.js +353 -0
  122. package/dist/node_modules/undici/lib/mock/snapshot-recorder.js +588 -0
  123. package/dist/node_modules/undici/lib/mock/snapshot-utils.js +158 -0
  124. package/dist/node_modules/undici/lib/util/cache.js +407 -0
  125. package/dist/node_modules/undici/lib/util/date.js +653 -0
  126. package/dist/node_modules/undici/lib/util/promise.js +28 -0
  127. package/dist/node_modules/undici/lib/util/runtime-features.js +124 -0
  128. package/dist/node_modules/undici/lib/util/stats.js +32 -0
  129. package/dist/node_modules/undici/lib/util/timers.js +425 -0
  130. package/dist/node_modules/undici/lib/web/cache/cache.js +864 -0
  131. package/dist/node_modules/undici/lib/web/cache/cachestorage.js +152 -0
  132. package/dist/node_modules/undici/lib/web/cache/util.js +45 -0
  133. package/dist/node_modules/undici/lib/web/cookies/constants.js +12 -0
  134. package/dist/node_modules/undici/lib/web/cookies/index.js +199 -0
  135. package/dist/node_modules/undici/lib/web/cookies/parse.js +322 -0
  136. package/dist/node_modules/undici/lib/web/cookies/util.js +282 -0
  137. package/dist/node_modules/undici/lib/web/eventsource/eventsource-stream.js +399 -0
  138. package/dist/node_modules/undici/lib/web/eventsource/eventsource.js +501 -0
  139. package/dist/node_modules/undici/lib/web/eventsource/util.js +29 -0
  140. package/dist/node_modules/undici/lib/web/fetch/LICENSE +21 -0
  141. package/dist/node_modules/undici/lib/web/fetch/body.js +509 -0
  142. package/dist/node_modules/undici/lib/web/fetch/constants.js +131 -0
  143. package/dist/node_modules/undici/lib/web/fetch/data-url.js +596 -0
  144. package/dist/node_modules/undici/lib/web/fetch/formdata-parser.js +575 -0
  145. package/dist/node_modules/undici/lib/web/fetch/formdata.js +259 -0
  146. package/dist/node_modules/undici/lib/web/fetch/global.js +40 -0
  147. package/dist/node_modules/undici/lib/web/fetch/headers.js +719 -0
  148. package/dist/node_modules/undici/lib/web/fetch/index.js +2397 -0
  149. package/dist/node_modules/undici/lib/web/fetch/request.js +1115 -0
  150. package/dist/node_modules/undici/lib/web/fetch/response.js +641 -0
  151. package/dist/node_modules/undici/lib/web/fetch/util.js +1520 -0
  152. package/dist/node_modules/undici/lib/web/infra/index.js +229 -0
  153. package/dist/node_modules/undici/lib/web/subresource-integrity/Readme.md +9 -0
  154. package/dist/node_modules/undici/lib/web/subresource-integrity/subresource-integrity.js +307 -0
  155. package/dist/node_modules/undici/lib/web/webidl/index.js +1006 -0
  156. package/dist/node_modules/undici/lib/web/websocket/connection.js +329 -0
  157. package/dist/node_modules/undici/lib/web/websocket/constants.js +126 -0
  158. package/dist/node_modules/undici/lib/web/websocket/events.js +331 -0
  159. package/dist/node_modules/undici/lib/web/websocket/frame.js +133 -0
  160. package/dist/node_modules/undici/lib/web/websocket/permessage-deflate.js +118 -0
  161. package/dist/node_modules/undici/lib/web/websocket/receiver.js +450 -0
  162. package/dist/node_modules/undici/lib/web/websocket/sender.js +109 -0
  163. package/dist/node_modules/undici/lib/web/websocket/stream/websocketerror.js +104 -0
  164. package/dist/node_modules/undici/lib/web/websocket/stream/websocketstream.js +497 -0
  165. package/dist/node_modules/undici/lib/web/websocket/util.js +347 -0
  166. package/dist/node_modules/undici/lib/web/websocket/websocket.js +751 -0
  167. package/dist/node_modules/undici/package.json +152 -0
  168. package/dist/node_modules/undici/scripts/strip-comments.js +10 -0
  169. package/dist/node_modules/undici/types/README.md +6 -0
  170. package/dist/node_modules/undici/types/agent.d.ts +32 -0
  171. package/dist/node_modules/undici/types/api.d.ts +43 -0
  172. package/dist/node_modules/undici/types/balanced-pool.d.ts +30 -0
  173. package/dist/node_modules/undici/types/cache-interceptor.d.ts +179 -0
  174. package/dist/node_modules/undici/types/cache.d.ts +36 -0
  175. package/dist/node_modules/undici/types/client-stats.d.ts +15 -0
  176. package/dist/node_modules/undici/types/client.d.ts +123 -0
  177. package/dist/node_modules/undici/types/connector.d.ts +36 -0
  178. package/dist/node_modules/undici/types/content-type.d.ts +21 -0
  179. package/dist/node_modules/undici/types/cookies.d.ts +30 -0
  180. package/dist/node_modules/undici/types/diagnostics-channel.d.ts +74 -0
  181. package/dist/node_modules/undici/types/dispatcher.d.ts +273 -0
  182. package/dist/node_modules/undici/types/env-http-proxy-agent.d.ts +22 -0
  183. package/dist/node_modules/undici/types/errors.d.ts +177 -0
  184. package/dist/node_modules/undici/types/eventsource.d.ts +66 -0
  185. package/dist/node_modules/undici/types/fetch.d.ts +231 -0
  186. package/dist/node_modules/undici/types/formdata.d.ts +114 -0
  187. package/dist/node_modules/undici/types/global-dispatcher.d.ts +9 -0
  188. package/dist/node_modules/undici/types/global-origin.d.ts +7 -0
  189. package/dist/node_modules/undici/types/h2c-client.d.ts +73 -0
  190. package/dist/node_modules/undici/types/handlers.d.ts +14 -0
  191. package/dist/node_modules/undici/types/header.d.ts +160 -0
  192. package/dist/node_modules/undici/types/index.d.ts +91 -0
  193. package/dist/node_modules/undici/types/interceptors.d.ts +80 -0
  194. package/dist/node_modules/undici/types/mock-agent.d.ts +68 -0
  195. package/dist/node_modules/undici/types/mock-call-history.d.ts +111 -0
  196. package/dist/node_modules/undici/types/mock-client.d.ts +27 -0
  197. package/dist/node_modules/undici/types/mock-errors.d.ts +12 -0
  198. package/dist/node_modules/undici/types/mock-interceptor.d.ts +94 -0
  199. package/dist/node_modules/undici/types/mock-pool.d.ts +27 -0
  200. package/dist/node_modules/undici/types/patch.d.ts +29 -0
  201. package/dist/node_modules/undici/types/pool-stats.d.ts +19 -0
  202. package/dist/node_modules/undici/types/pool.d.ts +41 -0
  203. package/dist/node_modules/undici/types/proxy-agent.d.ts +29 -0
  204. package/dist/node_modules/undici/types/readable.d.ts +68 -0
  205. package/dist/node_modules/undici/types/retry-agent.d.ts +8 -0
  206. package/dist/node_modules/undici/types/retry-handler.d.ts +125 -0
  207. package/dist/node_modules/undici/types/round-robin-pool.d.ts +41 -0
  208. package/dist/node_modules/undici/types/snapshot-agent.d.ts +109 -0
  209. package/dist/node_modules/undici/types/socks5-proxy-agent.d.ts +25 -0
  210. package/dist/node_modules/undici/types/util.d.ts +18 -0
  211. package/dist/node_modules/undici/types/utility.d.ts +7 -0
  212. package/dist/node_modules/undici/types/webidl.d.ts +347 -0
  213. package/dist/node_modules/undici/types/websocket.d.ts +188 -0
  214. package/package.json +2 -2
@@ -0,0 +1,588 @@
1
+ 'use strict'
2
+
3
+ const { writeFile, readFile, mkdir } = require('node:fs/promises')
4
+ const { dirname, resolve } = require('node:path')
5
+ const { setTimeout, clearTimeout } = require('node:timers')
6
+ const { InvalidArgumentError, UndiciError } = require('../core/errors')
7
+ const { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require('./snapshot-utils')
8
+
9
+ /**
10
+ * @typedef {Object} SnapshotRequestOptions
11
+ * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.)
12
+ * @property {string} path - Request path
13
+ * @property {string} origin - Request origin (base URL)
14
+ * @property {import('./snapshot-utils').Headers|import('./snapshot-utils').UndiciHeaders} headers - Request headers
15
+ * @property {import('./snapshot-utils').NormalizedHeaders} _normalizedHeaders - Request headers as a lowercase object
16
+ * @property {string|Buffer} [body] - Request body (optional)
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} SnapshotEntryRequest
21
+ * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.)
22
+ * @property {string} url - Full URL of the request
23
+ * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized headers as a lowercase object
24
+ * @property {string|Buffer} [body] - Request body (optional)
25
+ */
26
+
27
+ /**
28
+ * @typedef {Object} SnapshotEntryResponse
29
+ * @property {number} statusCode - HTTP status code of the response
30
+ * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized response headers as a lowercase object
31
+ * @property {string} body - Response body as a base64url encoded string
32
+ * @property {Object} [trailers] - Optional response trailers
33
+ */
34
+
35
+ /**
36
+ * @typedef {Object} SnapshotEntry
37
+ * @property {SnapshotEntryRequest} request - The request object
38
+ * @property {Array<SnapshotEntryResponse>} responses - Array of response objects
39
+ * @property {number} callCount - Number of times this snapshot has been called
40
+ * @property {string} timestamp - ISO timestamp of when the snapshot was created
41
+ */
42
+
43
+ /**
44
+ * @typedef {Object} SnapshotRecorderMatchOptions
45
+ * @property {Array<string>} [matchHeaders=[]] - Headers to match (empty array means match all headers)
46
+ * @property {Array<string>} [ignoreHeaders=[]] - Headers to ignore for matching
47
+ * @property {Array<string>} [excludeHeaders=[]] - Headers to exclude from matching
48
+ * @property {boolean} [matchBody=true] - Whether to match request body
49
+ * @property {boolean} [matchQuery=true] - Whether to match query properties
50
+ * @property {boolean} [caseSensitive=false] - Whether header matching is case-sensitive
51
+ */
52
+
53
+ /**
54
+ * @typedef {Object} SnapshotRecorderOptions
55
+ * @property {string} [snapshotPath] - Path to save/load snapshots
56
+ * @property {import('./snapshot-utils').SnapshotMode} [mode='record'] - Mode: 'record' or 'playback'
57
+ * @property {number} [maxSnapshots=Infinity] - Maximum number of snapshots to keep
58
+ * @property {boolean} [autoFlush=false] - Whether to automatically flush snapshots to disk
59
+ * @property {number} [flushInterval=30000] - Auto-flush interval in milliseconds (default: 30 seconds)
60
+ * @property {Array<string|RegExp>} [excludeUrls=[]] - URLs to exclude from recording
61
+ * @property {function} [shouldRecord=null] - Function to filter requests for recording
62
+ * @property {function} [shouldPlayback=null] - Function to filter requests
63
+ */
64
+
65
+ /**
66
+ * @typedef {Object} SnapshotFormattedRequest
67
+ * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.)
68
+ * @property {string} url - Full URL of the request (with query parameters if matchQuery is true)
69
+ * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized headers as a lowercase object
70
+ * @property {string} body - Request body (optional, only if matchBody is true)
71
+ */
72
+
73
+ /**
74
+ * @typedef {Object} SnapshotInfo
75
+ * @property {string} hash - Hash key for the snapshot
76
+ * @property {SnapshotEntryRequest} request - The request object
77
+ * @property {number} responseCount - Number of responses recorded for this request
78
+ * @property {number} callCount - Number of times this snapshot has been called
79
+ * @property {string} timestamp - ISO timestamp of when the snapshot was created
80
+ */
81
+
82
+ /**
83
+ * Formats a request for consistent snapshot storage
84
+ * Caches normalized headers to avoid repeated processing
85
+ *
86
+ * @param {SnapshotRequestOptions} opts - Request options
87
+ * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached header sets for performance
88
+ * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers and body
89
+ * @returns {SnapshotFormattedRequest} - Formatted request object
90
+ */
91
+ function formatRequestKey (opts, headerFilters, matchOptions = {}) {
92
+ const url = new URL(opts.path, opts.origin)
93
+
94
+ // Cache normalized headers if not already done
95
+ const normalized = opts._normalizedHeaders || normalizeHeaders(opts.headers)
96
+ if (!opts._normalizedHeaders) {
97
+ opts._normalizedHeaders = normalized
98
+ }
99
+
100
+ return {
101
+ method: opts.method || 'GET',
102
+ url: matchOptions.matchQuery !== false ? url.toString() : `${url.origin}${url.pathname}`,
103
+ headers: filterHeadersForMatching(normalized, headerFilters, matchOptions),
104
+ body: matchOptions.matchBody !== false && opts.body ? String(opts.body) : ''
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Filters headers based on matching configuration
110
+ *
111
+ * @param {import('./snapshot-utils').Headers} headers - Headers to filter
112
+ * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached sets for ignore, exclude, and match headers
113
+ * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers
114
+ */
115
+ function filterHeadersForMatching (headers, headerFilters, matchOptions = {}) {
116
+ if (!headers || typeof headers !== 'object') return {}
117
+
118
+ const {
119
+ caseSensitive = false
120
+ } = matchOptions
121
+
122
+ const filtered = {}
123
+ const { ignore, exclude, match } = headerFilters
124
+
125
+ for (const [key, value] of Object.entries(headers)) {
126
+ const headerKey = caseSensitive ? key : key.toLowerCase()
127
+
128
+ // Skip if in exclude list (for security)
129
+ if (exclude.has(headerKey)) continue
130
+
131
+ // Skip if in ignore list (for matching)
132
+ if (ignore.has(headerKey)) continue
133
+
134
+ // If matchHeaders is specified, only include those headers
135
+ if (match.size !== 0) {
136
+ if (!match.has(headerKey)) continue
137
+ }
138
+
139
+ filtered[headerKey] = value
140
+ }
141
+
142
+ return filtered
143
+ }
144
+
145
+ /**
146
+ * Filters headers for storage (only excludes sensitive headers)
147
+ *
148
+ * @param {import('./snapshot-utils').Headers} headers - Headers to filter
149
+ * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached sets for ignore, exclude, and match headers
150
+ * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers
151
+ */
152
+ function filterHeadersForStorage (headers, headerFilters, matchOptions = {}) {
153
+ if (!headers || typeof headers !== 'object') return {}
154
+
155
+ const {
156
+ caseSensitive = false
157
+ } = matchOptions
158
+
159
+ const filtered = {}
160
+ const { exclude: excludeSet } = headerFilters
161
+
162
+ for (const [key, value] of Object.entries(headers)) {
163
+ const headerKey = caseSensitive ? key : key.toLowerCase()
164
+
165
+ // Skip if in exclude list (for security)
166
+ if (excludeSet.has(headerKey)) continue
167
+
168
+ filtered[headerKey] = value
169
+ }
170
+
171
+ return filtered
172
+ }
173
+
174
+ /**
175
+ * Creates a hash key for request matching
176
+ * Properly orders headers to avoid conflicts and uses crypto hashing when available
177
+ *
178
+ * @param {SnapshotFormattedRequest} formattedRequest - Request object
179
+ * @returns {string} - Base64url encoded hash of the request
180
+ */
181
+ function createRequestHash (formattedRequest) {
182
+ const parts = [
183
+ formattedRequest.method,
184
+ formattedRequest.url
185
+ ]
186
+
187
+ // Process headers in a deterministic way to avoid conflicts
188
+ if (formattedRequest.headers && typeof formattedRequest.headers === 'object') {
189
+ const headerKeys = Object.keys(formattedRequest.headers).sort()
190
+ for (const key of headerKeys) {
191
+ const values = Array.isArray(formattedRequest.headers[key])
192
+ ? formattedRequest.headers[key]
193
+ : [formattedRequest.headers[key]]
194
+
195
+ // Add header name
196
+ parts.push(key)
197
+
198
+ // Add all values for this header, sorted for consistency
199
+ for (const value of values.sort()) {
200
+ parts.push(String(value))
201
+ }
202
+ }
203
+ }
204
+
205
+ // Add body
206
+ parts.push(formattedRequest.body)
207
+
208
+ const content = parts.join('|')
209
+
210
+ return hashId(content)
211
+ }
212
+
213
+ class SnapshotRecorder {
214
+ /** @type {NodeJS.Timeout | null} */
215
+ #flushTimeout
216
+
217
+ /** @type {import('./snapshot-utils').IsUrlExcluded} */
218
+ #isUrlExcluded
219
+
220
+ /** @type {Map<string, SnapshotEntry>} */
221
+ #snapshots = new Map()
222
+
223
+ /** @type {string|undefined} */
224
+ #snapshotPath
225
+
226
+ /** @type {number} */
227
+ #maxSnapshots = Infinity
228
+
229
+ /** @type {boolean} */
230
+ #autoFlush = false
231
+
232
+ /** @type {import('./snapshot-utils').HeaderFilters} */
233
+ #headerFilters
234
+
235
+ /**
236
+ * Creates a new SnapshotRecorder instance
237
+ * @param {SnapshotRecorderOptions&SnapshotRecorderMatchOptions} [options={}] - Configuration options for the recorder
238
+ */
239
+ constructor (options = {}) {
240
+ this.#snapshotPath = options.snapshotPath
241
+ this.#maxSnapshots = options.maxSnapshots || Infinity
242
+ this.#autoFlush = options.autoFlush || false
243
+ this.flushInterval = options.flushInterval || 30000 // 30 seconds default
244
+ this._flushTimer = null
245
+
246
+ // Matching configuration
247
+ /** @type {Required<SnapshotRecorderMatchOptions>} */
248
+ this.matchOptions = {
249
+ matchHeaders: options.matchHeaders || [], // empty means match all headers
250
+ ignoreHeaders: options.ignoreHeaders || [],
251
+ excludeHeaders: options.excludeHeaders || [],
252
+ matchBody: options.matchBody !== false, // default: true
253
+ matchQuery: options.matchQuery !== false, // default: true
254
+ caseSensitive: options.caseSensitive || false
255
+ }
256
+
257
+ // Cache processed header sets to avoid recreating them on every request
258
+ this.#headerFilters = createHeaderFilters(this.matchOptions)
259
+
260
+ // Request filtering callbacks
261
+ this.shouldRecord = options.shouldRecord || (() => true) // function(requestOpts) -> boolean
262
+ this.shouldPlayback = options.shouldPlayback || (() => true) // function(requestOpts) -> boolean
263
+
264
+ // URL pattern filtering
265
+ this.#isUrlExcluded = isUrlExcludedFactory(options.excludeUrls) // Array of regex patterns or strings
266
+
267
+ // Start auto-flush timer if enabled
268
+ if (this.#autoFlush && this.#snapshotPath) {
269
+ this.#startAutoFlush()
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Records a request-response interaction
275
+ * @param {SnapshotRequestOptions} requestOpts - Request options
276
+ * @param {SnapshotEntryResponse} response - Response data to record
277
+ * @return {Promise<void>} - Resolves when the recording is complete
278
+ */
279
+ async record (requestOpts, response) {
280
+ // Check if recording should be filtered out
281
+ if (!this.shouldRecord(requestOpts)) {
282
+ return // Skip recording
283
+ }
284
+
285
+ // Check URL exclusion patterns
286
+ if (this.isUrlExcluded(requestOpts)) {
287
+ return // Skip recording
288
+ }
289
+
290
+ const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions)
291
+ const hash = createRequestHash(request)
292
+
293
+ // Extract response data - always store body as base64
294
+ const normalizedHeaders = normalizeHeaders(response.headers)
295
+
296
+ /** @type {SnapshotEntryResponse} */
297
+ const responseData = {
298
+ statusCode: response.statusCode,
299
+ headers: filterHeadersForStorage(normalizedHeaders, this.#headerFilters, this.matchOptions),
300
+ body: Buffer.isBuffer(response.body)
301
+ ? response.body.toString('base64')
302
+ : Buffer.from(String(response.body || '')).toString('base64'),
303
+ trailers: response.trailers
304
+ }
305
+
306
+ // Remove oldest snapshot if we exceed maxSnapshots limit
307
+ if (this.#snapshots.size >= this.#maxSnapshots && !this.#snapshots.has(hash)) {
308
+ const oldestKey = this.#snapshots.keys().next().value
309
+ this.#snapshots.delete(oldestKey)
310
+ }
311
+
312
+ // Support sequential responses - if snapshot exists, add to responses array
313
+ const existingSnapshot = this.#snapshots.get(hash)
314
+ if (existingSnapshot && existingSnapshot.responses) {
315
+ existingSnapshot.responses.push(responseData)
316
+ existingSnapshot.timestamp = new Date().toISOString()
317
+ } else {
318
+ this.#snapshots.set(hash, {
319
+ request,
320
+ responses: [responseData], // Always store as array for consistency
321
+ callCount: 0,
322
+ timestamp: new Date().toISOString()
323
+ })
324
+ }
325
+
326
+ // Auto-flush if enabled
327
+ if (this.#autoFlush && this.#snapshotPath) {
328
+ this.#scheduleFlush()
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Checks if a URL should be excluded from recording/playback
334
+ * @param {SnapshotRequestOptions} requestOpts - Request options to check
335
+ * @returns {boolean} - True if URL is excluded
336
+ */
337
+ isUrlExcluded (requestOpts) {
338
+ const url = new URL(requestOpts.path, requestOpts.origin).toString()
339
+ return this.#isUrlExcluded(url)
340
+ }
341
+
342
+ /**
343
+ * Finds a matching snapshot for the given request
344
+ * Returns the appropriate response based on call count for sequential responses
345
+ *
346
+ * @param {SnapshotRequestOptions} requestOpts - Request options to match
347
+ * @returns {SnapshotEntry&Record<'response', SnapshotEntryResponse>|undefined} - Matching snapshot response or undefined if not found
348
+ */
349
+ findSnapshot (requestOpts) {
350
+ // Check if playback should be filtered out
351
+ if (!this.shouldPlayback(requestOpts)) {
352
+ return undefined // Skip playback
353
+ }
354
+
355
+ // Check URL exclusion patterns
356
+ if (this.isUrlExcluded(requestOpts)) {
357
+ return undefined // Skip playback
358
+ }
359
+
360
+ const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions)
361
+ const hash = createRequestHash(request)
362
+ const snapshot = this.#snapshots.get(hash)
363
+
364
+ if (!snapshot) return undefined
365
+
366
+ // Handle sequential responses
367
+ const currentCallCount = snapshot.callCount || 0
368
+ const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1)
369
+ snapshot.callCount = currentCallCount + 1
370
+
371
+ return {
372
+ ...snapshot,
373
+ response: snapshot.responses[responseIndex]
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Loads snapshots from file
379
+ * @param {string} [filePath] - Optional file path to load snapshots from
380
+ * @return {Promise<void>} - Resolves when snapshots are loaded
381
+ */
382
+ async loadSnapshots (filePath) {
383
+ const path = filePath || this.#snapshotPath
384
+ if (!path) {
385
+ throw new InvalidArgumentError('Snapshot path is required')
386
+ }
387
+
388
+ try {
389
+ const data = await readFile(resolve(path), 'utf8')
390
+ const parsed = JSON.parse(data)
391
+
392
+ // Convert array format back to Map
393
+ if (Array.isArray(parsed)) {
394
+ this.#snapshots.clear()
395
+ for (const { hash, snapshot } of parsed) {
396
+ this.#snapshots.set(hash, snapshot)
397
+ }
398
+ } else {
399
+ // Legacy object format
400
+ this.#snapshots = new Map(Object.entries(parsed))
401
+ }
402
+ } catch (error) {
403
+ if (error.code === 'ENOENT') {
404
+ // File doesn't exist yet - that's ok for recording mode
405
+ this.#snapshots.clear()
406
+ } else {
407
+ throw new UndiciError(`Failed to load snapshots from ${path}`, { cause: error })
408
+ }
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Saves snapshots to file
414
+ *
415
+ * @param {string} [filePath] - Optional file path to save snapshots
416
+ * @returns {Promise<void>} - Resolves when snapshots are saved
417
+ */
418
+ async saveSnapshots (filePath) {
419
+ const path = filePath || this.#snapshotPath
420
+ if (!path) {
421
+ throw new InvalidArgumentError('Snapshot path is required')
422
+ }
423
+
424
+ const resolvedPath = resolve(path)
425
+
426
+ // Ensure directory exists
427
+ await mkdir(dirname(resolvedPath), { recursive: true })
428
+
429
+ // Convert Map to serializable format
430
+ const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({
431
+ hash,
432
+ snapshot
433
+ }))
434
+
435
+ await writeFile(resolvedPath, JSON.stringify(data, null, 2), { flush: true })
436
+ }
437
+
438
+ /**
439
+ * Clears all recorded snapshots
440
+ * @returns {void}
441
+ */
442
+ clear () {
443
+ this.#snapshots.clear()
444
+ }
445
+
446
+ /**
447
+ * Gets all recorded snapshots
448
+ * @return {Array<SnapshotEntry>} - Array of all recorded snapshots
449
+ */
450
+ getSnapshots () {
451
+ return Array.from(this.#snapshots.values())
452
+ }
453
+
454
+ /**
455
+ * Gets snapshot count
456
+ * @return {number} - Number of recorded snapshots
457
+ */
458
+ size () {
459
+ return this.#snapshots.size
460
+ }
461
+
462
+ /**
463
+ * Resets call counts for all snapshots (useful for test cleanup)
464
+ * @returns {void}
465
+ */
466
+ resetCallCounts () {
467
+ for (const snapshot of this.#snapshots.values()) {
468
+ snapshot.callCount = 0
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Deletes a specific snapshot by request options
474
+ * @param {SnapshotRequestOptions} requestOpts - Request options to match
475
+ * @returns {boolean} - True if snapshot was deleted, false if not found
476
+ */
477
+ deleteSnapshot (requestOpts) {
478
+ const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions)
479
+ const hash = createRequestHash(request)
480
+ return this.#snapshots.delete(hash)
481
+ }
482
+
483
+ /**
484
+ * Gets information about a specific snapshot
485
+ * @param {SnapshotRequestOptions} requestOpts - Request options to match
486
+ * @returns {SnapshotInfo|null} - Snapshot information or null if not found
487
+ */
488
+ getSnapshotInfo (requestOpts) {
489
+ const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions)
490
+ const hash = createRequestHash(request)
491
+ const snapshot = this.#snapshots.get(hash)
492
+
493
+ if (!snapshot) return null
494
+
495
+ return {
496
+ hash,
497
+ request: snapshot.request,
498
+ responseCount: snapshot.responses ? snapshot.responses.length : (snapshot.response ? 1 : 0), // .response for legacy snapshots
499
+ callCount: snapshot.callCount || 0,
500
+ timestamp: snapshot.timestamp
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Replaces all snapshots with new data (full replacement)
506
+ * @param {Array<{hash: string; snapshot: SnapshotEntry}>|Record<string, SnapshotEntry>} snapshotData - New snapshot data to replace existing ones
507
+ * @returns {void}
508
+ */
509
+ replaceSnapshots (snapshotData) {
510
+ this.#snapshots.clear()
511
+
512
+ if (Array.isArray(snapshotData)) {
513
+ for (const { hash, snapshot } of snapshotData) {
514
+ this.#snapshots.set(hash, snapshot)
515
+ }
516
+ } else if (snapshotData && typeof snapshotData === 'object') {
517
+ // Legacy object format
518
+ this.#snapshots = new Map(Object.entries(snapshotData))
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Starts the auto-flush timer
524
+ * @returns {void}
525
+ */
526
+ #startAutoFlush () {
527
+ return this.#scheduleFlush()
528
+ }
529
+
530
+ /**
531
+ * Stops the auto-flush timer
532
+ * @returns {void}
533
+ */
534
+ #stopAutoFlush () {
535
+ if (this.#flushTimeout) {
536
+ clearTimeout(this.#flushTimeout)
537
+ // Ensure any pending flush is completed
538
+ this.saveSnapshots().catch(() => {
539
+ // Ignore flush errors
540
+ })
541
+ this.#flushTimeout = null
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Schedules a flush (debounced to avoid excessive writes)
547
+ */
548
+ #scheduleFlush () {
549
+ this.#flushTimeout = setTimeout(() => {
550
+ this.saveSnapshots().catch(() => {
551
+ // Ignore flush errors
552
+ })
553
+ if (this.#autoFlush) {
554
+ this.#flushTimeout?.refresh()
555
+ } else {
556
+ this.#flushTimeout = null
557
+ }
558
+ }, 1000) // 1 second debounce
559
+ }
560
+
561
+ /**
562
+ * Cleanup method to stop timers
563
+ * @returns {void}
564
+ */
565
+ destroy () {
566
+ this.#stopAutoFlush()
567
+ if (this.#flushTimeout) {
568
+ clearTimeout(this.#flushTimeout)
569
+ this.#flushTimeout = null
570
+ }
571
+ }
572
+
573
+ /**
574
+ * Async close method that saves all recordings and performs cleanup
575
+ * @returns {Promise<void>}
576
+ */
577
+ async close () {
578
+ // Save any pending recordings if we have a snapshot path
579
+ if (this.#snapshotPath && this.#snapshots.size !== 0) {
580
+ await this.saveSnapshots()
581
+ }
582
+
583
+ // Perform cleanup
584
+ this.destroy()
585
+ }
586
+ }
587
+
588
+ module.exports = { SnapshotRecorder, formatRequestKey, createRequestHash, filterHeadersForMatching, filterHeadersForStorage, createHeaderFilters }