hbsig 0.3.2 → 0.3.3

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 (95) hide show
  1. package/.babelrc-cjs +5 -0
  2. package/.babelrc-esm +5 -0
  3. package/README.md +1 -0
  4. package/dist/package.json +39 -0
  5. package/make.js +36 -0
  6. package/package.json +16 -17
  7. package/src/bin_to_str.js +46 -0
  8. package/src/collect-body-keys.js +436 -0
  9. package/src/commit.js +219 -0
  10. package/src/encode-array-item.js +112 -0
  11. package/src/encode-utils.js +191 -0
  12. package/src/encode.js +1256 -0
  13. package/src/erl_json.js +292 -0
  14. package/src/erl_str.js +1144 -0
  15. package/src/flat.js +250 -0
  16. package/src/http-message-signatures/httpbis.js +438 -0
  17. package/src/http-message-signatures/index.js +4 -0
  18. package/src/http-message-signatures/structured-header.js +105 -0
  19. package/src/httpsig.js +866 -0
  20. package/src/id.js +459 -0
  21. package/src/index.js +13 -0
  22. package/src/nocrypto.js +4 -0
  23. package/src/parser.js +171 -0
  24. package/src/send-utils.js +1132 -0
  25. package/src/send.js +142 -0
  26. package/src/signer-utils.js +375 -0
  27. package/src/signer.js +312 -0
  28. package/src/structured.js +496 -0
  29. package/src/test.js +2 -0
  30. package/src/utils.js +29 -0
  31. package/test/commit.test.js +41 -0
  32. package/test/erl_json.test.js +8 -0
  33. package/test/flat.test.js +27 -0
  34. package/test/httpsig.test.js +31 -0
  35. package/test/id.test.js +114 -0
  36. package/test/lib/all_cases.js +408 -0
  37. package/test/lib/cases.js +408 -0
  38. package/test/lib/erl_json_cases.js +161 -0
  39. package/test/lib/flat_cases.js +189 -0
  40. package/test/lib/gen.js +528 -0
  41. package/test/lib/httpsig_cases.js +313 -0
  42. package/test/lib/structured_cases.js +222 -0
  43. package/test/lib/test-utils.js +399 -0
  44. package/test/signer.test.js +48 -0
  45. package/test/structured.test.js +35 -0
  46. package/bin/install-deps +0 -0
  47. /package/{cjs → dist/cjs}/bin_to_str.js +0 -0
  48. /package/{cjs → dist/cjs}/collect-body-keys.js +0 -0
  49. /package/{cjs → dist/cjs}/commit.js +0 -0
  50. /package/{cjs → dist/cjs}/encode-array-item.js +0 -0
  51. /package/{cjs → dist/cjs}/encode-utils.js +0 -0
  52. /package/{cjs → dist/cjs}/encode.js +0 -0
  53. /package/{cjs → dist/cjs}/erl_json.js +0 -0
  54. /package/{cjs → dist/cjs}/erl_str.js +0 -0
  55. /package/{cjs → dist/cjs}/flat.js +0 -0
  56. /package/{cjs → dist/cjs}/http-message-signatures/httpbis.js +0 -0
  57. /package/{cjs → dist/cjs}/http-message-signatures/index.js +0 -0
  58. /package/{cjs → dist/cjs}/http-message-signatures/structured-header.js +0 -0
  59. /package/{cjs → dist/cjs}/httpsig.js +0 -0
  60. /package/{cjs → dist/cjs}/id.js +0 -0
  61. /package/{cjs → dist/cjs}/index.js +0 -0
  62. /package/{cjs → dist/cjs}/nocrypto.js +0 -0
  63. /package/{cjs → dist/cjs}/parser.js +0 -0
  64. /package/{cjs → dist/cjs}/send-utils.js +0 -0
  65. /package/{cjs → dist/cjs}/send.js +0 -0
  66. /package/{cjs → dist/cjs}/signer-utils.js +0 -0
  67. /package/{cjs → dist/cjs}/signer.js +0 -0
  68. /package/{cjs → dist/cjs}/structured.js +0 -0
  69. /package/{cjs → dist/cjs}/test.js +0 -0
  70. /package/{cjs → dist/cjs}/utils.js +0 -0
  71. /package/{esm → dist/esm}/bin_to_str.js +0 -0
  72. /package/{esm → dist/esm}/collect-body-keys.js +0 -0
  73. /package/{esm → dist/esm}/commit.js +0 -0
  74. /package/{esm → dist/esm}/encode-array-item.js +0 -0
  75. /package/{esm → dist/esm}/encode-utils.js +0 -0
  76. /package/{esm → dist/esm}/encode.js +0 -0
  77. /package/{esm → dist/esm}/erl_json.js +0 -0
  78. /package/{esm → dist/esm}/erl_str.js +0 -0
  79. /package/{esm → dist/esm}/flat.js +0 -0
  80. /package/{esm → dist/esm}/http-message-signatures/httpbis.js +0 -0
  81. /package/{esm → dist/esm}/http-message-signatures/index.js +0 -0
  82. /package/{esm → dist/esm}/http-message-signatures/structured-header.js +0 -0
  83. /package/{esm → dist/esm}/httpsig.js +0 -0
  84. /package/{esm → dist/esm}/id.js +0 -0
  85. /package/{esm → dist/esm}/index.js +0 -0
  86. /package/{esm → dist/esm}/nocrypto.js +0 -0
  87. /package/{esm → dist/esm}/package.json +0 -0
  88. /package/{esm → dist/esm}/parser.js +0 -0
  89. /package/{esm → dist/esm}/send-utils.js +0 -0
  90. /package/{esm → dist/esm}/send.js +0 -0
  91. /package/{esm → dist/esm}/signer-utils.js +0 -0
  92. /package/{esm → dist/esm}/signer.js +0 -0
  93. /package/{esm → dist/esm}/structured.js +0 -0
  94. /package/{esm → dist/esm}/test.js +0 -0
  95. /package/{esm → dist/esm}/utils.js +0 -0
package/.babelrc-cjs ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "presets": [
3
+ ["@babel/preset-env", { "modules": "commonjs" }]
4
+ ]
5
+ }
package/.babelrc-esm ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "presets": [
3
+ ["@babel/preset-env", { "modules": false }]
4
+ ]
5
+ }
package/README.md ADDED
@@ -0,0 +1 @@
1
+ [Documentation](../docs/docs/pages/api/hbsig.mdx)
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "hbsig",
3
+ "version": "0.3.1",
4
+ "main": "cjs/index.js",
5
+ "module": "esm/index.js",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./cjs/index.js",
10
+ "import": "./esm/index.js"
11
+ },
12
+ "./nocrypto": {
13
+ "require": "./cjs/nocrypto.js",
14
+ "import": "./esm/nocrypto.js"
15
+ }
16
+ },
17
+ "devDependencies": {
18
+ "@babel/cli": "^7.24.8",
19
+ "@babel/core": "^7.25.2",
20
+ "@babel/plugin-transform-modules-commonjs": "^7.24.8",
21
+ "@babel/preset-env": "^7.25.3",
22
+ "wao": "^0.33.0"
23
+ },
24
+ "dependencies": {
25
+ "@permaweb/aoconnect": "^0.0.85",
26
+ "base64url": "^3.0.1",
27
+ "fast-sha256": "^1.3.0",
28
+ "hbsig": "^0.1.5",
29
+ "ramda": "^0.31.3",
30
+ "structured-headers": "1.0.1"
31
+ },
32
+ "bin": {
33
+ "wao": "./cjs/cli.js",
34
+ "wao-esm": "./esm/cli.js"
35
+ },
36
+ "scripts": {
37
+ "server": "node --experimental-wasm-memory64 cjs/run.js"
38
+ }
39
+ }
package/make.js ADDED
@@ -0,0 +1,36 @@
1
+ import { writeFileSync, readFileSync } from "fs"
2
+ import { resolve } from "path"
3
+
4
+ const packageJson = resolve(import.meta.dirname, "package.json")
5
+ const packageJsonDist = resolve(import.meta.dirname, "dist/package.json")
6
+ const packageJsonDist2 = resolve(import.meta.dirname, "dist/esm/package.json")
7
+ const json = JSON.parse(readFileSync(packageJson, "utf8"))
8
+ delete json.type
9
+ delete json.scripts
10
+ json.main = "cjs/index.js"
11
+ json.module = "esm/index.js"
12
+ // Fix exports paths for dist package (remove dist/ prefix)
13
+ json.exports = {
14
+ ".": {
15
+ "require": "./cjs/index.js",
16
+ "import": "./esm/index.js"
17
+ },
18
+ "./nocrypto": {
19
+ "require": "./cjs/nocrypto.js",
20
+ "import": "./esm/nocrypto.js"
21
+ }
22
+ }
23
+ json.bin = {
24
+ wao: "./cjs/cli.js",
25
+ "wao-esm": "./esm/cli.js",
26
+ }
27
+ json.scripts = {
28
+ server: "node --experimental-wasm-memory64 cjs/run.js",
29
+ }
30
+
31
+ writeFileSync(packageJsonDist, JSON.stringify(json, undefined, 2))
32
+
33
+ const json2 = {
34
+ type: "module",
35
+ }
36
+ writeFileSync(packageJsonDist2, JSON.stringify(json2, undefined, 2))
package/package.json CHANGED
@@ -1,17 +1,25 @@
1
1
  {
2
2
  "name": "hbsig",
3
- "version": "0.3.2",
4
- "main": "cjs/index.js",
5
- "module": "esm/index.js",
3
+ "version": "0.3.3",
4
+ "type": "module",
5
+ "main": "dist/cjs/index.js",
6
+ "module": "dist/esm/index.js",
7
+ "scripts": {
8
+ "build:cjs": "babel src --out-dir dist/cjs --config-file ./.babelrc-cjs",
9
+ "build": "rm -rf dist && npm run build:cjs && cp src -rf dist/esm && node make.js && cp .npmignore dist/",
10
+ "test": "node --test --test-concurrency=1",
11
+ "test-only": "node --test-only --test-concurrency=1",
12
+ "test-all": "node --test --test-concurrency=1 test/**/*.test.js"
13
+ },
6
14
  "license": "MIT",
7
15
  "exports": {
8
16
  ".": {
9
- "require": "./cjs/index.js",
10
- "import": "./esm/index.js"
17
+ "require": "./dist/cjs/index.js",
18
+ "import": "./dist/esm/index.js"
11
19
  },
12
20
  "./nocrypto": {
13
- "require": "./cjs/nocrypto.js",
14
- "import": "./esm/nocrypto.js"
21
+ "require": "./dist/cjs/nocrypto.js",
22
+ "import": "./dist/esm/nocrypto.js"
15
23
  }
16
24
  },
17
25
  "devDependencies": {
@@ -25,16 +33,7 @@
25
33
  "@permaweb/aoconnect": "^0.0.85",
26
34
  "base64url": "^3.0.1",
27
35
  "fast-sha256": "^1.3.0",
28
- "hbsig": "^0.1.5",
29
36
  "ramda": "^0.31.3",
30
37
  "structured-headers": "1.0.1"
31
- },
32
- "bin": {
33
- "wao": "./cjs/cli.js",
34
- "wao-esm": "./esm/cli.js"
35
- },
36
- "scripts": {
37
- "server": "node --experimental-wasm-memory64 cjs/run.js",
38
- "preinstall": "./bin/install-deps"
39
38
  }
40
- }
39
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Convert a Buffer to string if it contains valid UTF-8 text
3
+ * @param {Buffer} buffer - Buffer to check and potentially convert
4
+ * @returns {string|Buffer} - String if valid UTF-8, otherwise original Buffer
5
+ */
6
+ export default function bin_to_str(buffer) {
7
+ if (!Buffer.isBuffer(buffer)) {
8
+ return buffer
9
+ }
10
+
11
+ // Empty buffer stays as buffer
12
+ if (buffer.length === 0) {
13
+ return buffer
14
+ }
15
+
16
+ try {
17
+ const str = buffer.toString("utf8")
18
+ // Check if it's valid UTF-8 by seeing if it round-trips correctly
19
+ if (Buffer.from(str, "utf8").equals(buffer)) {
20
+ // Additional check: ensure all characters are printable or common whitespace
21
+ // This prevents converting binary data that happens to be valid UTF-8
22
+ let isPrintable = true
23
+ for (let i = 0; i < str.length; i++) {
24
+ const code = str.charCodeAt(i)
25
+ // Allow printable ASCII (32-126) and common whitespace (tab, newline, carriage return)
26
+ if (
27
+ !(code >= 32 && code <= 126) &&
28
+ code !== 9 &&
29
+ code !== 10 &&
30
+ code !== 13
31
+ ) {
32
+ isPrintable = false
33
+ break
34
+ }
35
+ }
36
+
37
+ if (isPrintable) {
38
+ return str
39
+ }
40
+ }
41
+ } catch (e) {
42
+ // Not valid UTF-8, return original buffer
43
+ }
44
+
45
+ return buffer
46
+ }
@@ -0,0 +1,436 @@
1
+ import {
2
+ hasNonAscii,
3
+ sha256,
4
+ hasNewline,
5
+ isBytes,
6
+ isPojo,
7
+ } from "./encode-utils.js"
8
+
9
+ // Helper functions
10
+ const isEmpty = value => {
11
+ if (typeof value === "string") return value === ""
12
+ if (Array.isArray(value)) return value.length === 0
13
+ if (isPojo(value)) return Object.keys(value).length === 0
14
+ if (isBytes(value)) return value.length === 0 || value.byteLength === 0
15
+ return false
16
+ }
17
+
18
+ const hasOnlyEmptyValues = obj => {
19
+ if (!isPojo(obj)) return false
20
+ return Object.values(obj).every(
21
+ v =>
22
+ (typeof v === "string" && v === "") ||
23
+ (Array.isArray(v) && v.length === 0) ||
24
+ (isPojo(v) && Object.keys(v).length === 0)
25
+ )
26
+ }
27
+
28
+ const isSimpleValue = value => {
29
+ return (
30
+ typeof value === "string" ||
31
+ typeof value === "number" ||
32
+ typeof value === "boolean" ||
33
+ value === null ||
34
+ value === undefined ||
35
+ typeof value === "symbol"
36
+ )
37
+ }
38
+
39
+ const getValueByPath = (obj, path) => {
40
+ const parts = path.split("/")
41
+ let value = obj
42
+ for (const part of parts) {
43
+ if (/^\d+$/.test(part)) {
44
+ value = value[parseInt(part) - 1]
45
+ } else {
46
+ value = value[part]
47
+ }
48
+ }
49
+ return value
50
+ }
51
+
52
+ const canArrayBeInHeader = array => {
53
+ // Empty arrays can be in headers
54
+ if (array.length === 0) return true
55
+
56
+ // Arrays with objects must go to body
57
+ if (array.some(item => isPojo(item))) return false
58
+
59
+ // Arrays with binary data must go to body
60
+ if (array.some(item => isBytes(item) && item.length > 0)) return false
61
+
62
+ // Arrays with non-ASCII strings must go to body
63
+ if (array.some(item => typeof item === "string" && hasNonAscii(item)))
64
+ return false
65
+
66
+ // Arrays with nested arrays must go to body (to match original behavior)
67
+ if (array.some(item => Array.isArray(item))) return false
68
+
69
+ // Arrays with nested arrays that have objects must go to body
70
+ if (
71
+ array.some(
72
+ item => Array.isArray(item) && item.some(subItem => isPojo(subItem))
73
+ )
74
+ )
75
+ return false
76
+
77
+ // Simple arrays of primitives can stay in headers
78
+ return true
79
+ }
80
+
81
+ // Array analysis helper
82
+ const analyzeArray = array => {
83
+ const analysis = {
84
+ hasObjects: false,
85
+ hasArrays: false,
86
+ hasNonObjects: false,
87
+ hasArraysOfObjects: false,
88
+ hasEmptyStrings: false,
89
+ hasEmptyObjects: false,
90
+ hasNonEmptyObjects: false,
91
+ hasObjectsWithOnlyEmptyValues: false,
92
+ hasOnlyEmptyElements: true,
93
+ }
94
+
95
+ for (const item of array) {
96
+ if (isPojo(item)) {
97
+ analysis.hasObjects = true
98
+ if (Object.keys(item).length === 0) {
99
+ analysis.hasEmptyObjects = true
100
+ } else {
101
+ analysis.hasNonEmptyObjects = true
102
+ if (hasOnlyEmptyValues(item)) {
103
+ analysis.hasObjectsWithOnlyEmptyValues = true
104
+ }
105
+ }
106
+ } else if (Array.isArray(item)) {
107
+ analysis.hasArrays = true
108
+ if (item.some(subItem => isPojo(subItem))) {
109
+ analysis.hasArraysOfObjects = true
110
+ }
111
+ } else {
112
+ analysis.hasNonObjects = true
113
+ }
114
+
115
+ if (typeof item === "string" && item === "") {
116
+ analysis.hasEmptyStrings = true
117
+ }
118
+
119
+ if (!isEmpty(item)) {
120
+ analysis.hasOnlyEmptyElements = false
121
+ }
122
+ }
123
+
124
+ return analysis
125
+ }
126
+
127
+ // Body key collector class
128
+ class BodyKeyCollector {
129
+ constructor(obj) {
130
+ this.obj = obj
131
+ this.keys = []
132
+ }
133
+
134
+ collect() {
135
+ this.processRootObject()
136
+ return this.deduplicateAndFilter()
137
+ }
138
+
139
+ // Process top-level object
140
+ processRootObject() {
141
+ const objKeys = Object.keys(this.obj)
142
+
143
+ for (const [key, value] of Object.entries(this.obj)) {
144
+ if (this.isSpecialDataBodyField(key, value, objKeys)) {
145
+ this.keys.push(key)
146
+ } else if (Array.isArray(value) && value.length > 0) {
147
+ // Check if array can stay in header
148
+ if (!canArrayBeInHeader(value)) {
149
+ this.processRootArray(key, value)
150
+ }
151
+ } else if (isPojo(value)) {
152
+ this.processRootNestedObject(key, value)
153
+ } else if (this.needsBodyKey(key, value)) {
154
+ this.keys.push(key)
155
+ }
156
+ }
157
+ }
158
+
159
+ // Check if field is special data/body field
160
+ isSpecialDataBodyField(key, value, objKeys) {
161
+ if (
162
+ (key === "data" || key === "body") &&
163
+ isSimpleValue(value) &&
164
+ objKeys.length > 1
165
+ ) {
166
+ const otherKey = key === "data" ? "body" : "data"
167
+ const otherValue = this.obj[otherKey]
168
+
169
+ if (
170
+ otherValue &&
171
+ isPojo(otherValue) &&
172
+ Object.keys(otherValue).length > 0
173
+ ) {
174
+ return false
175
+ }
176
+ return true
177
+ }
178
+ return false
179
+ }
180
+
181
+ // Check if value needs a body key
182
+ needsBodyKey(key, value) {
183
+ return (
184
+ (isBytes(value) && value.length > 0) ||
185
+ (typeof value === "string" && value.includes("\n")) ||
186
+ (typeof value === "string" && hasNonAscii(value))
187
+ )
188
+ }
189
+
190
+ // Process root-level arrays
191
+ processRootArray(key, array) {
192
+ const analysis = analyzeArray(array)
193
+ let bodyPartCounter = 1
194
+
195
+ if (analysis.hasArraysOfObjects) {
196
+ // Handle arrays of arrays containing objects
197
+ array.forEach((item, index) => {
198
+ if (Array.isArray(item)) {
199
+ item.forEach((subItem, subIndex) => {
200
+ if (isPojo(subItem)) {
201
+ this.keys.push(`${key}/${index + 1}/${subIndex + 1}`)
202
+ }
203
+ })
204
+ }
205
+ })
206
+ this.keys.push(key)
207
+ } else if (
208
+ analysis.hasObjects &&
209
+ (analysis.hasEmptyStrings || analysis.hasEmptyObjects) &&
210
+ !analysis.hasObjectsWithOnlyEmptyValues
211
+ ) {
212
+ // Mixed array: only non-empty objects get parts
213
+ array.forEach(item => {
214
+ if (isPojo(item) && Object.keys(item).length > 0) {
215
+ const path = `${key}/${bodyPartCounter}`
216
+ this.keys.push(path)
217
+ this.addNestedObjectPaths(item, path)
218
+ }
219
+ bodyPartCounter++
220
+ })
221
+ this.keys.push(key)
222
+ } else if (analysis.hasObjects) {
223
+ // Regular array with objects
224
+ const skipEmptyObjects = analysis.hasOnlyEmptyElements
225
+
226
+ array.forEach(item => {
227
+ if (isPojo(item)) {
228
+ if (!(skipEmptyObjects && Object.keys(item).length === 0)) {
229
+ const path = `${key}/${bodyPartCounter}`
230
+ this.keys.push(path)
231
+ if (Object.keys(item).length > 0) {
232
+ this.addNestedObjectPaths(item, path)
233
+ }
234
+ }
235
+ } else if (typeof item === "string" && item === "") {
236
+ this.keys.push(`${key}/${bodyPartCounter}`)
237
+ }
238
+ bodyPartCounter++
239
+ })
240
+
241
+ // Add main array key conditionally
242
+ if (!analysis.hasObjectsWithOnlyEmptyValues || analysis.hasNonObjects) {
243
+ if (!analysis.hasOnlyEmptyElements) {
244
+ this.keys.push(key)
245
+ }
246
+ }
247
+ } else {
248
+ // Simple array without objects
249
+ const hasOnlyEmptyArraysOrObjects = array.every(
250
+ item =>
251
+ isEmpty(item) &&
252
+ (Array.isArray(item) || isPojo(item) || typeof item === "string")
253
+ )
254
+
255
+ if (hasOnlyEmptyArraysOrObjects || !array.every(isEmpty)) {
256
+ this.keys.push(key)
257
+ }
258
+ }
259
+ }
260
+
261
+ // Add paths for nested objects within an object
262
+ addNestedObjectPaths(obj, basePath) {
263
+ for (const [nestedKey, nestedValue] of Object.entries(obj)) {
264
+ if (isPojo(nestedValue) && Object.keys(nestedValue).length > 0) {
265
+ this.keys.push(`${basePath}/${nestedKey}`)
266
+ }
267
+ }
268
+ }
269
+
270
+ // Process root-level nested object
271
+ processRootNestedObject(key, obj) {
272
+ // Check for arrays with only empty elements
273
+ for (const [k, v] of Object.entries(obj)) {
274
+ if (Array.isArray(v) && v.length > 0 && v.every(isEmpty)) {
275
+ this.keys.push(`${key}/${k}`)
276
+ }
277
+ }
278
+
279
+ // Traverse the object
280
+ this.traverse(obj, key)
281
+ }
282
+
283
+ // Traverse nested structures
284
+ traverse(current, path) {
285
+ let hasSimpleFields = false
286
+ const nestedPaths = []
287
+ let hasArraysWithObjects = false
288
+
289
+ // First pass: analyze structure
290
+ for (const [key, value] of Object.entries(current)) {
291
+ const fullPath = path ? `${path}/${key}` : key
292
+ const result = this.analyzeFieldInTraversal(value, fullPath, nestedPaths)
293
+
294
+ hasSimpleFields = hasSimpleFields || result.hasSimpleFields
295
+ hasArraysWithObjects = hasArraysWithObjects || result.hasArraysWithObjects
296
+ }
297
+
298
+ // Add current path if needed
299
+ if (hasSimpleFields || (hasArraysWithObjects && path)) {
300
+ this.keys.push(path)
301
+ }
302
+
303
+ // Second pass: check for arrays with only empty elements
304
+ for (const [key, value] of Object.entries(current)) {
305
+ const fullPath = path ? `${path}/${key}` : key
306
+ if (Array.isArray(value) && value.length > 0 && value.every(isEmpty)) {
307
+ this.keys.push(fullPath)
308
+ }
309
+ }
310
+
311
+ // Traverse nested objects
312
+ for (const nestedPath of nestedPaths) {
313
+ const nestedObj = getValueByPath(this.obj, nestedPath)
314
+ if (isPojo(nestedObj)) {
315
+ this.traverse(nestedObj, nestedPath)
316
+ }
317
+ }
318
+ }
319
+
320
+ // Analyze a field during traversal
321
+ analyzeFieldInTraversal(value, fullPath, nestedPaths) {
322
+ let hasSimpleFields = false
323
+ let hasArraysWithObjects = false
324
+
325
+ if (Array.isArray(value)) {
326
+ if (value.length === 0) {
327
+ hasSimpleFields = true
328
+ } else {
329
+ const analysis = analyzeArray(value)
330
+
331
+ if (analysis.hasObjects) {
332
+ hasArraysWithObjects = true
333
+ this.processArrayInTraversal(value, fullPath, nestedPaths, analysis)
334
+
335
+ if (analysis.hasNonObjects) {
336
+ hasSimpleFields = true
337
+ // Only add to keys if array can't be in header
338
+ if (!canArrayBeInHeader(value)) {
339
+ this.keys.push(fullPath)
340
+ }
341
+ }
342
+ } else {
343
+ hasSimpleFields = true
344
+ }
345
+ }
346
+ } else if (isPojo(value)) {
347
+ if (Object.keys(value).length === 0) {
348
+ hasSimpleFields = true
349
+ } else if (hasOnlyEmptyValues(value)) {
350
+ this.keys.push(fullPath)
351
+ } else {
352
+ const hasArraysWithOnlyEmptyElements = Object.entries(value).some(
353
+ ([k, v]) => Array.isArray(v) && v.length > 0 && v.every(isEmpty)
354
+ )
355
+
356
+ if (hasArraysWithOnlyEmptyElements) {
357
+ this.keys.push(fullPath)
358
+ }
359
+ nestedPaths.push(fullPath)
360
+ }
361
+ } else if ((isBytes(value) && value.length > 0) || isSimpleValue(value)) {
362
+ hasSimpleFields = true
363
+ }
364
+
365
+ return { hasSimpleFields, hasArraysWithObjects }
366
+ }
367
+
368
+ // Process arrays found during traversal
369
+ processArrayInTraversal(array, fullPath, nestedPaths, analysis) {
370
+ if (
371
+ (analysis.hasEmptyStrings || analysis.hasEmptyObjects) &&
372
+ analysis.hasNonEmptyObjects
373
+ ) {
374
+ // Special case: only non-empty objects get parts
375
+ array.forEach((item, index) => {
376
+ if (isPojo(item) && Object.keys(item).length > 0) {
377
+ const itemPath = `${fullPath}/${index + 1}`
378
+ this.keys.push(itemPath)
379
+ nestedPaths.push(itemPath)
380
+ }
381
+ })
382
+ } else if (
383
+ analysis.hasObjectsWithOnlyEmptyValues &&
384
+ !analysis.hasNonObjects
385
+ ) {
386
+ // Objects with only empty values
387
+ array.forEach((item, index) => {
388
+ if (isPojo(item)) {
389
+ const itemPath = `${fullPath}/${index + 1}`
390
+ this.keys.push(itemPath)
391
+ if (Object.keys(item).length > 0) {
392
+ nestedPaths.push(itemPath)
393
+ }
394
+ }
395
+ })
396
+ } else {
397
+ // Normal case
398
+ array.forEach((item, index) => {
399
+ if (isPojo(item)) {
400
+ const itemPath = `${fullPath}/${index + 1}`
401
+ this.keys.push(itemPath)
402
+ if (Object.keys(item).length > 0) {
403
+ nestedPaths.push(itemPath)
404
+ }
405
+ }
406
+ })
407
+ }
408
+ }
409
+
410
+ // Deduplicate and filter results
411
+ deduplicateAndFilter() {
412
+ const uniqueKeys = [...new Set(this.keys)]
413
+
414
+ return uniqueKeys.filter(k => {
415
+ if (!k) return false
416
+
417
+ // Check if this is a path to an element inside an array with only empty elements
418
+ const parts = k.split("/")
419
+ if (parts.length >= 2 && /^\d+$/.test(parts[parts.length - 1])) {
420
+ const arrayPath = parts.slice(0, -1).join("/")
421
+ const arrayValue = getValueByPath(this.obj, arrayPath)
422
+
423
+ if (Array.isArray(arrayValue) && arrayValue.every(isEmpty)) {
424
+ return false
425
+ }
426
+ }
427
+
428
+ return true
429
+ })
430
+ }
431
+ }
432
+
433
+ export default function collectBodyKeys(obj, prefix = "") {
434
+ const collector = new BodyKeyCollector(obj)
435
+ return collector.collect()
436
+ }