hbsig 0.3.1 → 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 -16
- 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/{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/src/commit.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { id, base, hashpath, rsaid } from "./id.js"
|
|
2
|
+
import { toAddr } from "./utils.js"
|
|
3
|
+
import { extractPubKey } from "./signer-utils.js"
|
|
4
|
+
import { verify } from "./signer-utils.js"
|
|
5
|
+
import crypto from "crypto"
|
|
6
|
+
|
|
7
|
+
// Helper to compute SHA-256 content-digest in RFC 9530 format
|
|
8
|
+
const computeContentDigest = (body) => {
|
|
9
|
+
let bodyBuffer
|
|
10
|
+
if (Buffer.isBuffer(body)) {
|
|
11
|
+
bodyBuffer = body
|
|
12
|
+
} else if (body instanceof Blob) {
|
|
13
|
+
return null // Can't compute synchronously for Blob
|
|
14
|
+
} else if (typeof body === "string") {
|
|
15
|
+
bodyBuffer = Buffer.from(body, "binary")
|
|
16
|
+
} else {
|
|
17
|
+
bodyBuffer = Buffer.from(String(body), "binary")
|
|
18
|
+
}
|
|
19
|
+
const hash = crypto.createHash("sha256").update(bodyBuffer).digest("base64")
|
|
20
|
+
return `sha-256=:${hash}:`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Helper to build ao-types string from an object
|
|
24
|
+
const buildAoTypes = (obj) => {
|
|
25
|
+
const types = []
|
|
26
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
27
|
+
if (typeof value === "number") {
|
|
28
|
+
types.push(`${key}="${Number.isInteger(value) ? "integer" : "float"}"`)
|
|
29
|
+
} else if (typeof value === "boolean") {
|
|
30
|
+
types.push(`${key}="atom"`)
|
|
31
|
+
} else if (value === null) {
|
|
32
|
+
types.push(`${key}="atom"`)
|
|
33
|
+
} else if (typeof value === "symbol") {
|
|
34
|
+
// Symbols are Erlang atoms
|
|
35
|
+
types.push(`${key}="atom"`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return types.length > 0 ? types.join(", ") : null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// todo: handle @
|
|
42
|
+
export const commit = async (obj, opts) => {
|
|
43
|
+
const msg = await opts.signer(obj, opts)
|
|
44
|
+
const {
|
|
45
|
+
decodedSignatureInput: { components },
|
|
46
|
+
} = await verify(msg)
|
|
47
|
+
|
|
48
|
+
let body = {}
|
|
49
|
+
|
|
50
|
+
// Check for inline-body-key (indicates a field was moved to HTTP body during encoding)
|
|
51
|
+
const inlineBodyKey = msg.headers["inline-body-key"] || msg.headers["ao-body-key"]
|
|
52
|
+
|
|
53
|
+
// Body field names that HyperBEAM's inline_key() recognizes natively.
|
|
54
|
+
// For these, normalize_for_encoding() re-derives ao-body-key automatically.
|
|
55
|
+
// For custom names (e.g., "json"), we must keep ao-body-key in committed list
|
|
56
|
+
// so HyperBEAM knows which field to inline during verification.
|
|
57
|
+
const NATIVE_BODY_KEYS = new Set(["body", "data"])
|
|
58
|
+
const isNativeBodyKey = !inlineBodyKey || NATIVE_BODY_KEYS.has(inlineBodyKey)
|
|
59
|
+
|
|
60
|
+
// Build body from committed components.
|
|
61
|
+
// IMPORTANT: Use original typed values from obj (preserves integers, booleans, etc.)
|
|
62
|
+
// instead of string values from headers. This is critical because:
|
|
63
|
+
// 1. HTTP headers are always strings (e.g., quantity: 100 → "100")
|
|
64
|
+
// 2. We include ao-types to tell HyperBEAM the original types
|
|
65
|
+
// 3. HyperBEAM applies ao-types BEFORE signature verification
|
|
66
|
+
// 4. If we send strings, HyperBEAM converts to integers, breaking signature
|
|
67
|
+
// 5. If we send original integers, HyperBEAM re-encodes them as strings for verification
|
|
68
|
+
//
|
|
69
|
+
// Always skip content-digest and inline-body-key (transport artifacts re-derived by HyperBEAM).
|
|
70
|
+
// Skip ao-body-key only for native body keys (HyperBEAM re-derives it).
|
|
71
|
+
// Keep ao-body-key for custom body keys (HyperBEAM needs it to find the body field).
|
|
72
|
+
|
|
73
|
+
// Create case-insensitive lookup maps
|
|
74
|
+
const headerLookup = new Map()
|
|
75
|
+
for (const [k, v] of Object.entries(msg.headers)) {
|
|
76
|
+
headerLookup.set(k.toLowerCase(), v)
|
|
77
|
+
}
|
|
78
|
+
// Case-insensitive lookup for original object values (preserves types)
|
|
79
|
+
const objLookup = new Map()
|
|
80
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
81
|
+
objLookup.set(k.toLowerCase(), v)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const v of components) {
|
|
85
|
+
const key = v === "@path" ? "path" : v
|
|
86
|
+
if (key === "content-length") continue
|
|
87
|
+
if (key === "content-digest") continue
|
|
88
|
+
if (key === "inline-body-key") continue
|
|
89
|
+
if (isNativeBodyKey && key === "ao-body-key") continue
|
|
90
|
+
|
|
91
|
+
// Prefer original value from input object (preserves integer/boolean/etc. types)
|
|
92
|
+
// Fall back to header string value if not found in original object
|
|
93
|
+
const originalValue = objLookup.get(key.toLowerCase())
|
|
94
|
+
if (originalValue !== undefined) {
|
|
95
|
+
body[key] = originalValue
|
|
96
|
+
} else {
|
|
97
|
+
const headerValue = headerLookup.get(key)
|
|
98
|
+
if (headerValue !== undefined) {
|
|
99
|
+
body[key] = headerValue
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Handle body resolution - restore the inlined field to its AO-Core name
|
|
105
|
+
let bodyContent = null
|
|
106
|
+
if (msg.body) {
|
|
107
|
+
if (msg.body instanceof Blob) {
|
|
108
|
+
const arrayBuffer = await msg.body.arrayBuffer()
|
|
109
|
+
bodyContent = Buffer.from(arrayBuffer)
|
|
110
|
+
} else {
|
|
111
|
+
bodyContent = msg.body
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Put body content under the original field name (e.g., "data", "json")
|
|
115
|
+
if (inlineBodyKey) {
|
|
116
|
+
body[inlineBodyKey] = bodyContent
|
|
117
|
+
} else {
|
|
118
|
+
body.body = bodyContent
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Include non-committed fields from the original object as JSON values.
|
|
123
|
+
// This covers: (1) body-key fields (arrays/objects encoded as multipart,
|
|
124
|
+
// which can't be included as raw multipart in JSON), and (2) any other
|
|
125
|
+
// fields excluded from signing. These fields are unsigned but present
|
|
126
|
+
// so HyperBEAM can parse them directly as JSON types.
|
|
127
|
+
// Use case-insensitive matching: signing normalizes keys to lowercase,
|
|
128
|
+
// but the original object may use mixed case (e.g., "To" vs "to").
|
|
129
|
+
const committedSetLower = new Set(components.map(v => (v === "@path" ? "path" : v).toLowerCase()))
|
|
130
|
+
// Also track lowercase keys already in body to prevent duplicates
|
|
131
|
+
const bodyKeysLower = new Set(Object.keys(body).map(k => k.toLowerCase()))
|
|
132
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
133
|
+
// Skip HTTP method (not a data field)
|
|
134
|
+
if (key === "method") continue
|
|
135
|
+
// Skip if already in committed set (was signed)
|
|
136
|
+
if (committedSetLower.has(key.toLowerCase())) continue
|
|
137
|
+
// Skip if already in body (duplicate prevention)
|
|
138
|
+
if (bodyKeysLower.has(key.toLowerCase())) continue
|
|
139
|
+
if (value === undefined) continue
|
|
140
|
+
body[key] = value
|
|
141
|
+
bodyKeysLower.add(key.toLowerCase())
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Include ao-types so HyperBEAM knows how to convert string values to proper types.
|
|
145
|
+
// First check if it was set by the signer, otherwise compute it from the original object
|
|
146
|
+
let aoTypes = msg.headers["ao-types"]
|
|
147
|
+
if (!aoTypes) {
|
|
148
|
+
// Compute ao-types from the original typed values in obj
|
|
149
|
+
aoTypes = buildAoTypes(obj)
|
|
150
|
+
}
|
|
151
|
+
if (aoTypes) {
|
|
152
|
+
body["ao-types"] = aoTypes
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const rsaId = rsaid(msg.headers)
|
|
156
|
+
const pub = extractPubKey(msg.headers)
|
|
157
|
+
const pubKeyBase64 = pub.toString("base64")
|
|
158
|
+
const committer = toAddr(pubKeyBase64)
|
|
159
|
+
|
|
160
|
+
// Extract keyid from signature-input to ensure it matches what was signed
|
|
161
|
+
// Format: sig-xxx=("field1"...);alg="...";keyid="..."
|
|
162
|
+
// The keyid may already have a scheme prefix (e.g., "publickey:base64data")
|
|
163
|
+
const extractKeyidFromSigInput = (sigInput) => {
|
|
164
|
+
if (!sigInput) return null
|
|
165
|
+
const match = sigInput.match(/keyid="([^"]+)"/)
|
|
166
|
+
if (!match) return null
|
|
167
|
+
// Return as-is - the keyid already includes the prefix from signing
|
|
168
|
+
return match[1]
|
|
169
|
+
}
|
|
170
|
+
const keyid = extractKeyidFromSigInput(msg.headers["signature-input"]) || `publickey:${pubKeyBase64}`
|
|
171
|
+
|
|
172
|
+
// Build the list of committed fields.
|
|
173
|
+
// Always transform HTTPSig transport keys to AO-Core keys:
|
|
174
|
+
// - Replace content-digest with the body field name (HyperBEAM re-derives content-digest)
|
|
175
|
+
// - For native body keys ("body", "data"): also remove ao-body-key (HyperBEAM re-derives it)
|
|
176
|
+
// - For custom body keys (e.g., "json"): keep ao-body-key (HyperBEAM needs it to find the field)
|
|
177
|
+
let committedFields = components.map(v => v === "@path" ? "path" : v)
|
|
178
|
+
if (committedFields.includes("content-digest") && (inlineBodyKey || msg.body)) {
|
|
179
|
+
const bodyFieldName = inlineBodyKey || "body"
|
|
180
|
+
committedFields = committedFields.filter(k => k !== "content-digest")
|
|
181
|
+
if (!committedFields.includes(bodyFieldName)) {
|
|
182
|
+
committedFields.push(bodyFieldName)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (isNativeBodyKey) {
|
|
186
|
+
// For native body keys, ao-body-key is a transport artifact - remove it
|
|
187
|
+
committedFields = committedFields.filter(k => k !== "ao-body-key")
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Extract just the base64 signature data from the header format "sig-xxx=:base64data:"
|
|
191
|
+
// HyperBEAM expects raw base64 without colons (uses b64fast:encode/decode)
|
|
192
|
+
const extractSignature = (sigHeader) => {
|
|
193
|
+
if (!sigHeader) return sigHeader
|
|
194
|
+
// Match the base64 data between colons after the label
|
|
195
|
+
const match = sigHeader.match(/=:([^:]+):/)
|
|
196
|
+
return match ? match[1] : sigHeader
|
|
197
|
+
}
|
|
198
|
+
const rawSignature = extractSignature(msg.headers.signature)
|
|
199
|
+
const meta = {
|
|
200
|
+
type: "rsa-pss-sha512",
|
|
201
|
+
alg: "rsa-pss-sha512",
|
|
202
|
+
"commitment-device": "httpsig@1.0",
|
|
203
|
+
keyid: keyid,
|
|
204
|
+
committed: committedFields
|
|
205
|
+
}
|
|
206
|
+
const sigs = {
|
|
207
|
+
signature: rawSignature,
|
|
208
|
+
"signature-input": msg.headers["signature-input"],
|
|
209
|
+
}
|
|
210
|
+
// Only include the RSA commitment - HMAC commitments are created by HyperBEAM
|
|
211
|
+
// when needed and require the server's HMAC key which we don't have
|
|
212
|
+
const committed = {
|
|
213
|
+
commitments: {
|
|
214
|
+
[rsaId]: { ...meta, committer, ...sigs },
|
|
215
|
+
},
|
|
216
|
+
...body,
|
|
217
|
+
}
|
|
218
|
+
return committed
|
|
219
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { toBuffer, formatFloat, isBytes, isPojo } from "./encode-utils.js"
|
|
2
|
+
|
|
3
|
+
// Helper to generate the correct number of backslashes for a given nesting level
|
|
4
|
+
function getBackslashes(level) {
|
|
5
|
+
return "\\".repeat(Math.pow(2, level) - 1)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Helper to encode primitive values at a specific nesting level
|
|
9
|
+
function encodePrimitiveAtLevel(value, level) {
|
|
10
|
+
const bs = getBackslashes(level)
|
|
11
|
+
|
|
12
|
+
if (typeof value === "number") {
|
|
13
|
+
if (Number.isInteger(value)) {
|
|
14
|
+
return `${bs}"(ao-type-integer) ${value}${bs}"`
|
|
15
|
+
} else {
|
|
16
|
+
return `${bs}"(ao-type-float) ${formatFloat(value)}${bs}"`
|
|
17
|
+
}
|
|
18
|
+
} else if (typeof value === "string") {
|
|
19
|
+
return `${bs}"${value}${bs}"`
|
|
20
|
+
} else if (value === null) {
|
|
21
|
+
const innerBs = getBackslashes(level + 1)
|
|
22
|
+
return `${bs}"(ao-type-atom) ${innerBs}"null${innerBs}"${bs}"`
|
|
23
|
+
} else if (value === undefined) {
|
|
24
|
+
const innerBs = getBackslashes(level + 1)
|
|
25
|
+
return `${bs}"(ao-type-atom) ${innerBs}"undefined${innerBs}"${bs}"`
|
|
26
|
+
} else if (typeof value === "symbol") {
|
|
27
|
+
const desc = value.description || "Symbol.for()"
|
|
28
|
+
const innerBs = getBackslashes(level + 1)
|
|
29
|
+
return `${bs}"(ao-type-atom) ${innerBs}"${desc}${innerBs}"${bs}"`
|
|
30
|
+
} else if (typeof value === "boolean") {
|
|
31
|
+
const innerBs = getBackslashes(level + 1)
|
|
32
|
+
return `${bs}"(ao-type-atom) ${innerBs}"${value}${innerBs}"${bs}"`
|
|
33
|
+
} else {
|
|
34
|
+
return `${bs}"${String(value)}${bs}"`
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Recursive function to handle arrays at any nesting level
|
|
39
|
+
function encodeArrayAtLevel(items, level) {
|
|
40
|
+
// The original code only goes 3 levels deep for arrays
|
|
41
|
+
if (level >= 3) {
|
|
42
|
+
const bs = getBackslashes(level)
|
|
43
|
+
const stringItems = items
|
|
44
|
+
.map(item => {
|
|
45
|
+
if (typeof item === "number") {
|
|
46
|
+
if (Number.isInteger(item)) {
|
|
47
|
+
return `${bs}"(ao-type-integer) ${item}${bs}"`
|
|
48
|
+
} else {
|
|
49
|
+
return `${bs}"(ao-type-float) ${formatFloat(item)}${bs}"`
|
|
50
|
+
}
|
|
51
|
+
} else if (typeof item === "string") {
|
|
52
|
+
return `${bs}"${item}${bs}"`
|
|
53
|
+
} else {
|
|
54
|
+
return `${bs}"${String(item)}${bs}"`
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
.join(", ")
|
|
58
|
+
return `${getBackslashes(level - 1)}"(ao-type-list) ${stringItems}${getBackslashes(level - 1)}"`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const encodedItems = items
|
|
62
|
+
.map(item => {
|
|
63
|
+
if (Array.isArray(item)) {
|
|
64
|
+
return encodeArrayAtLevel(item, level + 1)
|
|
65
|
+
} else {
|
|
66
|
+
return encodePrimitiveAtLevel(item, level)
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
.join(", ")
|
|
70
|
+
|
|
71
|
+
if (level === 0) {
|
|
72
|
+
return `"(ao-type-list) ${encodedItems}"`
|
|
73
|
+
} else {
|
|
74
|
+
const bs = getBackslashes(level - 1)
|
|
75
|
+
return `${bs}"(ao-type-list) ${encodedItems}${bs}"`
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default function encodeArrayItem(item) {
|
|
80
|
+
if (typeof item === "number") {
|
|
81
|
+
if (Number.isInteger(item)) {
|
|
82
|
+
return `"(ao-type-integer) ${item}"`
|
|
83
|
+
} else {
|
|
84
|
+
return `"(ao-type-float) ${formatFloat(item)}"`
|
|
85
|
+
}
|
|
86
|
+
} else if (typeof item === "string") {
|
|
87
|
+
return `"${item}"`
|
|
88
|
+
} else if (item === null) {
|
|
89
|
+
return `"(ao-type-atom) \\"null\\""`
|
|
90
|
+
} else if (item === undefined) {
|
|
91
|
+
return `"(ao-type-atom) \\"undefined\\""`
|
|
92
|
+
} else if (typeof item === "symbol") {
|
|
93
|
+
const desc = item.description || "Symbol.for()"
|
|
94
|
+
return `"(ao-type-atom) \\"${desc}\\""`
|
|
95
|
+
} else if (typeof item === "boolean") {
|
|
96
|
+
return `"(ao-type-atom) \\"${item}\\""`
|
|
97
|
+
} else if (Array.isArray(item)) {
|
|
98
|
+
return encodeArrayAtLevel(item, 1)
|
|
99
|
+
} else if (isBytes(item)) {
|
|
100
|
+
const buffer = toBuffer(item)
|
|
101
|
+
if (buffer.length === 0 || buffer.byteLength === 0) {
|
|
102
|
+
return `""`
|
|
103
|
+
}
|
|
104
|
+
return `"(ao-type-binary)"`
|
|
105
|
+
} else if (isPojo(item)) {
|
|
106
|
+
const json = JSON.stringify(item)
|
|
107
|
+
const escaped = json.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
108
|
+
return `"(ao-type-map) ${escaped}"`
|
|
109
|
+
} else {
|
|
110
|
+
return `"${String(item)}"`
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { hash } from "fast-sha256"
|
|
2
|
+
|
|
3
|
+
export function isBytes(value) {
|
|
4
|
+
return (
|
|
5
|
+
value instanceof ArrayBuffer ||
|
|
6
|
+
ArrayBuffer.isView(value) ||
|
|
7
|
+
Buffer.isBuffer(value) ||
|
|
8
|
+
(value &&
|
|
9
|
+
typeof value === "object" &&
|
|
10
|
+
value.type === "Buffer" &&
|
|
11
|
+
Array.isArray(value.data))
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isPojo(value) {
|
|
16
|
+
return (
|
|
17
|
+
!isBytes(value) &&
|
|
18
|
+
!Array.isArray(value) &&
|
|
19
|
+
!(value instanceof Blob) &&
|
|
20
|
+
typeof value === "object" &&
|
|
21
|
+
value !== null
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function hasNewline(value) {
|
|
26
|
+
if (typeof value === "string") return value.includes("\n")
|
|
27
|
+
if (value instanceof Blob) {
|
|
28
|
+
value = await value.text()
|
|
29
|
+
return value.includes("\n")
|
|
30
|
+
}
|
|
31
|
+
if (isBytes(value)) return Buffer.from(value).includes("\n")
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function sha256(data) {
|
|
36
|
+
let uint8Array
|
|
37
|
+
if (data instanceof ArrayBuffer) {
|
|
38
|
+
uint8Array = new Uint8Array(data)
|
|
39
|
+
} else if (data instanceof Uint8Array) {
|
|
40
|
+
uint8Array = data
|
|
41
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
42
|
+
uint8Array = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
43
|
+
} else {
|
|
44
|
+
throw new Error("sha256 expects ArrayBuffer or ArrayBufferView")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const hashResult = hash(uint8Array)
|
|
48
|
+
return hashResult.buffer.slice(
|
|
49
|
+
hashResult.byteOffset,
|
|
50
|
+
hashResult.byteOffset + hashResult.byteLength
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function formatFloat(num) {
|
|
55
|
+
let exp = num.toExponential(20)
|
|
56
|
+
exp = exp.replace(/e\+(\d)$/, "e+0$1")
|
|
57
|
+
exp = exp.replace(/e-(\d)$/, "e-0$1")
|
|
58
|
+
return exp
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function hasNonAscii(str) {
|
|
62
|
+
return /[^\x00-\x7F]/.test(str)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function toBuffer(value) {
|
|
66
|
+
if (Buffer.isBuffer(value)) {
|
|
67
|
+
return value
|
|
68
|
+
} else if (
|
|
69
|
+
value &&
|
|
70
|
+
typeof value === "object" &&
|
|
71
|
+
value.type === "Buffer" &&
|
|
72
|
+
Array.isArray(value.data)
|
|
73
|
+
) {
|
|
74
|
+
return Buffer.from(value.data)
|
|
75
|
+
} else if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
|
|
76
|
+
return Buffer.from(value)
|
|
77
|
+
} else {
|
|
78
|
+
return Buffer.from(value)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Navigate through a path to get a value from nested object/array
|
|
83
|
+
export function getValueByPath(obj, path) {
|
|
84
|
+
const pathParts = path.split("/")
|
|
85
|
+
let value = obj
|
|
86
|
+
for (const part of pathParts) {
|
|
87
|
+
if (/^\d+$/.test(part)) {
|
|
88
|
+
value = value[parseInt(part) - 1]
|
|
89
|
+
} else {
|
|
90
|
+
value = value[part]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return value
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Get the ao-type for a value
|
|
97
|
+
// Note: dev_codec_structured.erl only supports: integer, float, atom, list, map
|
|
98
|
+
// Do NOT use empty-binary, empty-list, empty-message as they cause binary_to_existing_atom errors
|
|
99
|
+
export function getAoType(value) {
|
|
100
|
+
if (
|
|
101
|
+
typeof value === "boolean" ||
|
|
102
|
+
value === null ||
|
|
103
|
+
value === undefined ||
|
|
104
|
+
typeof value === "symbol"
|
|
105
|
+
) {
|
|
106
|
+
return "atom"
|
|
107
|
+
} else if (typeof value === "number") {
|
|
108
|
+
return Number.isInteger(value) ? "integer" : "float"
|
|
109
|
+
} else if (typeof value === "string" && value.length === 0) {
|
|
110
|
+
// Empty strings become empty binaries naturally - no type annotation needed
|
|
111
|
+
return null
|
|
112
|
+
} else if (isBytes(value) && (value.length === 0 || value.byteLength === 0)) {
|
|
113
|
+
// Empty buffers become empty binaries naturally - no type annotation needed
|
|
114
|
+
return null
|
|
115
|
+
} else if (Array.isArray(value) && value.length === 0) {
|
|
116
|
+
// Use "list" for empty arrays (structured codec compatible)
|
|
117
|
+
return "list"
|
|
118
|
+
} else if (Array.isArray(value)) {
|
|
119
|
+
return "list"
|
|
120
|
+
} else if (isPojo(value) && Object.keys(value).length === 0) {
|
|
121
|
+
// Use "map" for empty objects (structured codec compatible)
|
|
122
|
+
return "map"
|
|
123
|
+
}
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check if a value is empty
|
|
128
|
+
export function isEmpty(value) {
|
|
129
|
+
return (
|
|
130
|
+
(Array.isArray(value) && value.length === 0) ||
|
|
131
|
+
(isPojo(value) && Object.keys(value).length === 0) ||
|
|
132
|
+
(isBytes(value) && (value.length === 0 || value.byteLength === 0)) ||
|
|
133
|
+
(typeof value === "string" && value.length === 0)
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Convert primitive values to their encoded string representation
|
|
138
|
+
export function encodePrimitiveContent(value) {
|
|
139
|
+
if (typeof value === "string") {
|
|
140
|
+
return value
|
|
141
|
+
} else if (typeof value === "boolean") {
|
|
142
|
+
return `"${value}"`
|
|
143
|
+
} else if (typeof value === "number") {
|
|
144
|
+
return String(value)
|
|
145
|
+
} else if (value === null) {
|
|
146
|
+
return '"null"'
|
|
147
|
+
} else if (value === undefined) {
|
|
148
|
+
return '"undefined"'
|
|
149
|
+
} else if (typeof value === "symbol") {
|
|
150
|
+
return `"${value.description || "Symbol.for()"}"`
|
|
151
|
+
}
|
|
152
|
+
return value
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Sort type annotations by their numeric prefix
|
|
156
|
+
export function sortTypeAnnotations(types) {
|
|
157
|
+
return types.sort((a, b) => {
|
|
158
|
+
const aNum = parseInt(a.split("=")[0])
|
|
159
|
+
const bNum = parseInt(b.split("=")[0])
|
|
160
|
+
return aNum - bNum
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Analyze array contents
|
|
165
|
+
export function analyzeArray(array) {
|
|
166
|
+
return {
|
|
167
|
+
hasObjects: array.some(item => isPojo(item)),
|
|
168
|
+
hasArrays: array.some(item => Array.isArray(item)),
|
|
169
|
+
hasEmptyStrings: array.some(
|
|
170
|
+
item => typeof item === "string" && item === ""
|
|
171
|
+
),
|
|
172
|
+
hasEmptyObjects: array.some(
|
|
173
|
+
item => isPojo(item) && Object.keys(item).length === 0
|
|
174
|
+
),
|
|
175
|
+
hasOnlyEmptyElements:
|
|
176
|
+
array.length > 0 &&
|
|
177
|
+
array.every(
|
|
178
|
+
item =>
|
|
179
|
+
(Array.isArray(item) && item.length === 0) ||
|
|
180
|
+
(isPojo(item) && Object.keys(item).length === 0) ||
|
|
181
|
+
(typeof item === "string" && item === "")
|
|
182
|
+
),
|
|
183
|
+
hasOnlyNonEmptyObjects:
|
|
184
|
+
array.length > 0 &&
|
|
185
|
+
array.every(item => isPojo(item) && Object.keys(item).length > 0),
|
|
186
|
+
hasObjectsWithOnlyEmptyValues: array.some(item => {
|
|
187
|
+
if (!isPojo(item) || Object.keys(item).length === 0) return false
|
|
188
|
+
return Object.values(item).every(v => isEmpty(v))
|
|
189
|
+
}),
|
|
190
|
+
}
|
|
191
|
+
}
|