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.
Files changed (94) 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 -16
  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/{cjs → dist/cjs}/bin_to_str.js +0 -0
  47. /package/{cjs → dist/cjs}/collect-body-keys.js +0 -0
  48. /package/{cjs → dist/cjs}/commit.js +0 -0
  49. /package/{cjs → dist/cjs}/encode-array-item.js +0 -0
  50. /package/{cjs → dist/cjs}/encode-utils.js +0 -0
  51. /package/{cjs → dist/cjs}/encode.js +0 -0
  52. /package/{cjs → dist/cjs}/erl_json.js +0 -0
  53. /package/{cjs → dist/cjs}/erl_str.js +0 -0
  54. /package/{cjs → dist/cjs}/flat.js +0 -0
  55. /package/{cjs → dist/cjs}/http-message-signatures/httpbis.js +0 -0
  56. /package/{cjs → dist/cjs}/http-message-signatures/index.js +0 -0
  57. /package/{cjs → dist/cjs}/http-message-signatures/structured-header.js +0 -0
  58. /package/{cjs → dist/cjs}/httpsig.js +0 -0
  59. /package/{cjs → dist/cjs}/id.js +0 -0
  60. /package/{cjs → dist/cjs}/index.js +0 -0
  61. /package/{cjs → dist/cjs}/nocrypto.js +0 -0
  62. /package/{cjs → dist/cjs}/parser.js +0 -0
  63. /package/{cjs → dist/cjs}/send-utils.js +0 -0
  64. /package/{cjs → dist/cjs}/send.js +0 -0
  65. /package/{cjs → dist/cjs}/signer-utils.js +0 -0
  66. /package/{cjs → dist/cjs}/signer.js +0 -0
  67. /package/{cjs → dist/cjs}/structured.js +0 -0
  68. /package/{cjs → dist/cjs}/test.js +0 -0
  69. /package/{cjs → dist/cjs}/utils.js +0 -0
  70. /package/{esm → dist/esm}/bin_to_str.js +0 -0
  71. /package/{esm → dist/esm}/collect-body-keys.js +0 -0
  72. /package/{esm → dist/esm}/commit.js +0 -0
  73. /package/{esm → dist/esm}/encode-array-item.js +0 -0
  74. /package/{esm → dist/esm}/encode-utils.js +0 -0
  75. /package/{esm → dist/esm}/encode.js +0 -0
  76. /package/{esm → dist/esm}/erl_json.js +0 -0
  77. /package/{esm → dist/esm}/erl_str.js +0 -0
  78. /package/{esm → dist/esm}/flat.js +0 -0
  79. /package/{esm → dist/esm}/http-message-signatures/httpbis.js +0 -0
  80. /package/{esm → dist/esm}/http-message-signatures/index.js +0 -0
  81. /package/{esm → dist/esm}/http-message-signatures/structured-header.js +0 -0
  82. /package/{esm → dist/esm}/httpsig.js +0 -0
  83. /package/{esm → dist/esm}/id.js +0 -0
  84. /package/{esm → dist/esm}/index.js +0 -0
  85. /package/{esm → dist/esm}/nocrypto.js +0 -0
  86. /package/{esm → dist/esm}/package.json +0 -0
  87. /package/{esm → dist/esm}/parser.js +0 -0
  88. /package/{esm → dist/esm}/send-utils.js +0 -0
  89. /package/{esm → dist/esm}/send.js +0 -0
  90. /package/{esm → dist/esm}/signer-utils.js +0 -0
  91. /package/{esm → dist/esm}/signer.js +0 -0
  92. /package/{esm → dist/esm}/structured.js +0 -0
  93. /package/{esm → dist/esm}/test.js +0 -0
  94. /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
+ }