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.
- package/.babelrc-cjs +5 -0
- package/.babelrc-esm +5 -0
- package/README.md +1 -0
- package/dist/package.json +39 -0
- package/make.js +36 -0
- package/package.json +16 -17
- package/src/bin_to_str.js +46 -0
- package/src/collect-body-keys.js +436 -0
- package/src/commit.js +219 -0
- package/src/encode-array-item.js +112 -0
- package/src/encode-utils.js +191 -0
- package/src/encode.js +1256 -0
- package/src/erl_json.js +292 -0
- package/src/erl_str.js +1144 -0
- package/src/flat.js +250 -0
- package/src/http-message-signatures/httpbis.js +438 -0
- package/src/http-message-signatures/index.js +4 -0
- package/src/http-message-signatures/structured-header.js +105 -0
- package/src/httpsig.js +866 -0
- package/src/id.js +459 -0
- package/src/index.js +13 -0
- package/src/nocrypto.js +4 -0
- package/src/parser.js +171 -0
- package/src/send-utils.js +1132 -0
- package/src/send.js +142 -0
- package/src/signer-utils.js +375 -0
- package/src/signer.js +312 -0
- package/src/structured.js +496 -0
- package/src/test.js +2 -0
- package/src/utils.js +29 -0
- package/test/commit.test.js +41 -0
- package/test/erl_json.test.js +8 -0
- package/test/flat.test.js +27 -0
- package/test/httpsig.test.js +31 -0
- package/test/id.test.js +114 -0
- package/test/lib/all_cases.js +408 -0
- package/test/lib/cases.js +408 -0
- package/test/lib/erl_json_cases.js +161 -0
- package/test/lib/flat_cases.js +189 -0
- package/test/lib/gen.js +528 -0
- package/test/lib/httpsig_cases.js +313 -0
- package/test/lib/structured_cases.js +222 -0
- package/test/lib/test-utils.js +399 -0
- package/test/signer.test.js +48 -0
- package/test/structured.test.js +35 -0
- package/bin/install-deps +0 -0
- /package/{cjs → dist/cjs}/bin_to_str.js +0 -0
- /package/{cjs → dist/cjs}/collect-body-keys.js +0 -0
- /package/{cjs → dist/cjs}/commit.js +0 -0
- /package/{cjs → dist/cjs}/encode-array-item.js +0 -0
- /package/{cjs → dist/cjs}/encode-utils.js +0 -0
- /package/{cjs → dist/cjs}/encode.js +0 -0
- /package/{cjs → dist/cjs}/erl_json.js +0 -0
- /package/{cjs → dist/cjs}/erl_str.js +0 -0
- /package/{cjs → dist/cjs}/flat.js +0 -0
- /package/{cjs → dist/cjs}/http-message-signatures/httpbis.js +0 -0
- /package/{cjs → dist/cjs}/http-message-signatures/index.js +0 -0
- /package/{cjs → dist/cjs}/http-message-signatures/structured-header.js +0 -0
- /package/{cjs → dist/cjs}/httpsig.js +0 -0
- /package/{cjs → dist/cjs}/id.js +0 -0
- /package/{cjs → dist/cjs}/index.js +0 -0
- /package/{cjs → dist/cjs}/nocrypto.js +0 -0
- /package/{cjs → dist/cjs}/parser.js +0 -0
- /package/{cjs → dist/cjs}/send-utils.js +0 -0
- /package/{cjs → dist/cjs}/send.js +0 -0
- /package/{cjs → dist/cjs}/signer-utils.js +0 -0
- /package/{cjs → dist/cjs}/signer.js +0 -0
- /package/{cjs → dist/cjs}/structured.js +0 -0
- /package/{cjs → dist/cjs}/test.js +0 -0
- /package/{cjs → dist/cjs}/utils.js +0 -0
- /package/{esm → dist/esm}/bin_to_str.js +0 -0
- /package/{esm → dist/esm}/collect-body-keys.js +0 -0
- /package/{esm → dist/esm}/commit.js +0 -0
- /package/{esm → dist/esm}/encode-array-item.js +0 -0
- /package/{esm → dist/esm}/encode-utils.js +0 -0
- /package/{esm → dist/esm}/encode.js +0 -0
- /package/{esm → dist/esm}/erl_json.js +0 -0
- /package/{esm → dist/esm}/erl_str.js +0 -0
- /package/{esm → dist/esm}/flat.js +0 -0
- /package/{esm → dist/esm}/http-message-signatures/httpbis.js +0 -0
- /package/{esm → dist/esm}/http-message-signatures/index.js +0 -0
- /package/{esm → dist/esm}/http-message-signatures/structured-header.js +0 -0
- /package/{esm → dist/esm}/httpsig.js +0 -0
- /package/{esm → dist/esm}/id.js +0 -0
- /package/{esm → dist/esm}/index.js +0 -0
- /package/{esm → dist/esm}/nocrypto.js +0 -0
- /package/{esm → dist/esm}/package.json +0 -0
- /package/{esm → dist/esm}/parser.js +0 -0
- /package/{esm → dist/esm}/send-utils.js +0 -0
- /package/{esm → dist/esm}/send.js +0 -0
- /package/{esm → dist/esm}/signer-utils.js +0 -0
- /package/{esm → dist/esm}/signer.js +0 -0
- /package/{esm → dist/esm}/structured.js +0 -0
- /package/{esm → dist/esm}/test.js +0 -0
- /package/{esm → dist/esm}/utils.js +0 -0
package/.babelrc-cjs
ADDED
package/.babelrc-esm
ADDED
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|