wao 0.40.2 → 0.41.0

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 (74) hide show
  1. package/cjs/accounts-web.js +25 -0
  2. package/cjs/accounts.js +38 -0
  3. package/cjs/adaptor-base.js +504 -287
  4. package/cjs/adaptor-cf.js +42 -0
  5. package/cjs/ao-loader.js +1 -1
  6. package/cjs/ao.js +18 -6
  7. package/cjs/aoconnect-base.js +1010 -546
  8. package/cjs/aoconnect-cf.js +24 -0
  9. package/cjs/ar-remote.js +277 -0
  10. package/cjs/armem-base.js +822 -211
  11. package/cjs/armem-cf.js +128 -0
  12. package/cjs/armem.js +11 -5
  13. package/cjs/bar.js +511 -173
  14. package/cjs/car.js +37 -0
  15. package/cjs/cf-env.js +54 -0
  16. package/cjs/cf.js +76 -0
  17. package/cjs/cli.js +12 -6
  18. package/cjs/create.js +1 -1
  19. package/cjs/devs.js +53 -0
  20. package/cjs/dodb.js +116 -0
  21. package/cjs/hb.js +136 -53
  22. package/cjs/hyperbeam.js +85 -44
  23. package/cjs/keygen.js +94 -0
  24. package/cjs/run.js +4 -1
  25. package/cjs/server.js +40 -9
  26. package/cjs/storage-multi.js +525 -0
  27. package/cjs/tgql-d1.js +664 -0
  28. package/cjs/tgql.js +293 -172
  29. package/cjs/workspace/.claude/agents/tester.md +2 -2
  30. package/cjs/workspace/.claude/skills/build/SKILL.md +2 -2
  31. package/cjs/workspace/.claude/skills/test/SKILL.md +3 -2
  32. package/cjs/workspace/CLAUDE.md +2 -2
  33. package/cjs/workspace/README.md +1 -1
  34. package/cjs/workspace/docs/debug.md +9 -4
  35. package/cjs/workspace/docs/hyperbeam-devices.md +50 -1
  36. package/cjs/workspace/package.json +3 -3
  37. package/esm/accounts-web.js +14 -0
  38. package/esm/accounts.js +27 -0
  39. package/esm/adaptor-base.js +129 -31
  40. package/esm/adaptor-cf.js +11 -0
  41. package/esm/ao-loader.js +1 -1
  42. package/esm/ao.js +21 -2
  43. package/esm/aoconnect-base.js +248 -7
  44. package/esm/aoconnect-cf.js +9 -0
  45. package/esm/ar-remote.js +87 -0
  46. package/esm/armem-base.js +304 -53
  47. package/esm/armem-cf.js +67 -0
  48. package/esm/armem.js +7 -2
  49. package/esm/bar.js +126 -16
  50. package/esm/car.js +10 -0
  51. package/esm/cf-env.js +29 -0
  52. package/esm/cf.js +11 -0
  53. package/esm/cli.js +6 -2
  54. package/esm/create.js +1 -1
  55. package/esm/devs.js +15 -0
  56. package/esm/dodb.js +26 -0
  57. package/esm/hb.js +93 -16
  58. package/esm/hyperbeam.js +68 -30
  59. package/esm/keygen.js +47 -0
  60. package/esm/run.js +4 -1
  61. package/esm/server.js +29 -9
  62. package/esm/storage-multi.js +183 -0
  63. package/esm/tgql-d1.js +407 -0
  64. package/esm/tgql.js +29 -10
  65. package/esm/workspace/.claude/agents/tester.md +2 -2
  66. package/esm/workspace/.claude/skills/build/SKILL.md +2 -2
  67. package/esm/workspace/.claude/skills/test/SKILL.md +3 -2
  68. package/esm/workspace/CLAUDE.md +2 -2
  69. package/esm/workspace/README.md +1 -1
  70. package/esm/workspace/docs/debug.md +9 -4
  71. package/esm/workspace/docs/hyperbeam-devices.md +50 -1
  72. package/esm/workspace/package.json +3 -3
  73. package/package.json +10 -3
  74. package/postinstall.cjs +84 -0
package/esm/bar.js CHANGED
@@ -27,6 +27,10 @@ class AR extends MAR {
27
27
  this.mem = opt.mem ?? new opt.ArMem()
28
28
  this.gql = new GQL({ mem: this.mem })
29
29
  this.arweave = this.mem.arweave
30
+ // Block batching (DB path only)
31
+ this._pendingBatch = []
32
+ this._flushTimer = null
33
+ this._batchWindow = opt.batchWindow ?? 0 // ms, 0 = disabled
30
34
  }
31
35
  isHttpMsg(item) {
32
36
  if (typeof item === "object" && item !== null) {
@@ -77,6 +81,21 @@ class AR extends MAR {
77
81
  }
78
82
 
79
83
  async postItems(items, jwk) {
84
+ // Remote mode: forward bundle to main AR via HTTP
85
+ if (this.mem._remote) {
86
+ let err = null
87
+ ;({ err, jwk } = await this.checkWallet({ jwk }))
88
+ if (err) return { err }
89
+ if (!is(Array, items)) items = [items]
90
+ const bundle = await bundleAndSignData(items, new ArweaveSigner(jwk))
91
+ const tx = await this.mem.arweave.createTransaction(
92
+ { data: bundle.binary },
93
+ jwk
94
+ )
95
+ tx.addTag("Bundle-Format", "binary")
96
+ tx.addTag("Bundle-Version", "2.0.0")
97
+ return await this.postTx(tx, jwk, items.map(i => ({ id: i.id })))
98
+ }
80
99
  let err = null
81
100
  ;({ err, jwk } = await this.checkWallet({ jwk }))
82
101
  if (err) return { err }
@@ -90,6 +109,10 @@ class AR extends MAR {
90
109
  if (t.name === "Content-Type") data_type = t.value
91
110
  const owner = await this.owner(di)
92
111
  await this.mem.set({ key: di.owner, address: owner }, "addrmap", owner)
112
+ // D1: write addrmap
113
+ if (this.mem.db?.d1WriteAddrmap) {
114
+ try { await this.mem.db.d1WriteAddrmap(owner, { key: di.owner, address: owner }) } catch (e) {}
115
+ }
93
116
  let _item = {
94
117
  _data: { size: data_size, type: data_type },
95
118
  anchor: di.anchor,
@@ -101,6 +124,12 @@ class AR extends MAR {
101
124
  tags: di.tags,
102
125
  data: di.data,
103
126
  }
127
+ // Extract raw data to R2
128
+ if (_item.data && this.mem.db?.r2PutTxData) {
129
+ await this.mem.db.r2PutTxData(_item.id, _item.data)
130
+ _item._r2_data = true
131
+ _item.data = ""
132
+ }
104
133
  await this.mem.set(_item, "txs", await di.id)
105
134
  _items.push(_item)
106
135
  }
@@ -115,19 +144,41 @@ class AR extends MAR {
115
144
  }
116
145
 
117
146
  async postTx(tx, jwk, items = []) {
147
+ // Remote mode: sign locally, POST to main AR
148
+ if (this.mem._remote) {
149
+ let err = null
150
+ ;({ err, jwk } = await this.checkWallet({ jwk }))
151
+ if (err) return { err }
152
+ if (!tx.id) await this.mem.arweave.transactions.sign(tx, jwk)
153
+ const res = await this.mem._remote.postTx(tx)
154
+ return { res: { id: tx.id, status: 200 }, err: null, id: tx.id }
155
+ }
118
156
  let err = null
119
157
  ;({ err, jwk } = await this.checkWallet({ jwk }))
120
158
  if (err) return { err }
121
159
 
122
160
  let res = null
123
161
  if (!tx.id) await this.mem.arweave.transactions.sign(tx, jwk)
124
- let height = (await this.mem.get("height")) + 1
125
- await this.mem.set(height, "height")
162
+ let height
163
+ if (this.mem._d1Ready) {
164
+ // D1 ready: increment in-memory, flush every 10 txs
165
+ this.mem.height = (this.mem.height ?? 0) + 1
166
+ height = this.mem.height
167
+ if (height % 10 === 0) {
168
+ await this.mem.set(height, "height")
169
+ }
170
+ } else {
171
+ height = (await this.mem.get("height")) + 1
172
+ await this.mem.set(height, "height")
173
+ }
174
+ let previous = this.mem._d1Ready
175
+ ? (this.mem.lastBlockId || "")
176
+ : (last(await this.mem.get("blocks")) ?? "")
126
177
  let block = {
127
178
  id: tx.id,
128
179
  timestamp: Date.now(),
129
180
  height,
130
- previous: last(await this.mem.get("blocks")) ?? "",
181
+ previous,
131
182
  txs: [],
132
183
  }
133
184
  let msg = null
@@ -144,6 +195,7 @@ class AR extends MAR {
144
195
  "Process",
145
196
  "Module",
146
197
  "Scheduler-Location",
198
+ "Scheduler-Transfer",
147
199
  "Attestation",
148
200
  "Available",
149
201
  ])
@@ -157,7 +209,12 @@ class AR extends MAR {
157
209
  }
158
210
  block.txs.push(item.id)
159
211
  _txs.block = block.id
160
- await this.mem.set({ bundle: tx.id }, "txs", item.id)
212
+ // Re-store item with parent/bundledIn/block, but drop the raw
213
+ // DataItem binary (item.item) to save space — data is in R2 or
214
+ // can be reconstructed from the wrapper bundle tx.
215
+ const slim = { ..._txs, bundle: tx.id }
216
+ delete slim.item
217
+ await this.mem.set(slim, "txs", item.id)
161
218
  }
162
219
  }
163
220
  let _tags = []
@@ -180,19 +237,64 @@ class AR extends MAR {
180
237
  }
181
238
  tx.tags = _tags
182
239
  tx.owner = await this.arweave.wallets.jwkToAddress({ n: tx.owner })
240
+ tx.recipient = tx.target || ""
183
241
  let _txs = tx
184
242
  block.txs.push(tx.id)
185
243
  _txs.block = block.id
244
+ // Extract raw data to R2
245
+ if (_txs.data && this.mem.db?.r2PutTxData) {
246
+ await this.mem.db.r2PutTxData(tx.id, _txs.data)
247
+ _txs._r2_data = true
248
+ _txs.data = ""
249
+ }
186
250
  await this.mem.set(_txs, "txs", tx.id)
187
- let blocks = await this.mem.get("blocks")
188
- blocks.push(block.id)
189
- await this.mem.set(blocks, "blocks")
251
+ // Update block tracking — always maintain blocks array + blockmap
252
+ // so the O(n) scan fallback in tgql.js works even if D1 is stale
253
+ if (this.mem._d1Ready) {
254
+ await this.mem.set(block.id, "lastBlockId")
255
+ this.mem.lastBlockId = block.id
256
+ }
257
+ this.mem.blocks ??= []
258
+ this.mem.blocks.push(block.id)
259
+ await this.mem.set(this.mem.blocks, "blocks")
190
260
  await this.mem.set(block, "blockmap", block.id)
191
261
 
192
262
  if (jwk) {
193
263
  const owner = await this.arweave.wallets.jwkToAddress(jwk)
194
264
  await this.mem.set({ address: owner, key: jwk.n }, "addrmap", owner)
265
+ // D1: write addrmap
266
+ if (this.mem.db?.d1WriteAddrmap) {
267
+ try { await this.mem.db.d1WriteAddrmap(owner, { address: owner, key: jwk.n }) } catch (e) {}
268
+ }
269
+ }
270
+
271
+ // D1 write: block + txs + tags (only when D1 schema is verified ready)
272
+ if (this.mem._d1Ready && this.mem.db?.d1WriteBlock) {
273
+ const mainTx = {
274
+ id: tx.id,
275
+ owner: tx.owner,
276
+ recipient: tx.recipient || "",
277
+ anchor: tx.anchor || "",
278
+ signature: tx.signature || "",
279
+ tags: _tags,
280
+ _data: tx._data || { size: tx.data ? String(tx.data.length) : "0", type: "" },
281
+ bundledIn: tx.bundledIn,
282
+ parent: tx.parent,
283
+ }
284
+ if (this._batchWindow > 0) {
285
+ this._pendingBatch.push({ block, mainTx, items: [...items] })
286
+ if (!this._flushTimer) {
287
+ this._flushTimer = setTimeout(() => this._flushBatch(), this._batchWindow)
288
+ }
289
+ } else {
290
+ await this.mem.db.d1WriteBlock(block)
291
+ await this.mem.db.d1WriteTx(mainTx, block.id, block.height)
292
+ for (const item of items) {
293
+ await this.mem.db.d1WriteTx(item, block.id, block.height)
294
+ }
295
+ }
195
296
  }
297
+
196
298
  res = { id: tx.id, status: 200, statusText: "200" }
197
299
  if (this.log) {
198
300
  if (msg) {
@@ -206,6 +308,21 @@ class AR extends MAR {
206
308
  return { res, err, id: tx.id }
207
309
  }
208
310
 
311
+ async _flushBatch() {
312
+ this._flushTimer = null
313
+ if (!this._pendingBatch.length) return
314
+ const batch = this._pendingBatch.splice(0)
315
+ for (const { block, mainTx, items } of batch) {
316
+ await this.mem.db.d1WriteBlock(block)
317
+ await this.mem.db.d1WriteTx(mainTx, block.id, block.height)
318
+ for (const item of items) {
319
+ await this.mem.db.d1WriteTx(item, block.id, block.height)
320
+ }
321
+ }
322
+ // Flush height to DO after batch
323
+ await this.mem.set(this.mem.height, "height")
324
+ }
325
+
209
326
  async tx(id) {
210
327
  return await this.mem.getTx(id)
211
328
  }
@@ -219,15 +336,8 @@ class AR extends MAR {
219
336
  }
220
337
  let tx = await this.mem.getTx(id)
221
338
  let _data = tx?.data ?? null
222
- if (tx?.format === 2 && _data) {
223
- _data = Buffer.from(_data, "base64")
224
- } else if (_data) {
225
- // need to check production
226
- if (tx._data.type === "") {
227
- _data = tobuff(_data)
228
- } else {
229
- _data = Buffer.from(base64url.decode(_data))
230
- }
339
+ if (_data && is(String, _data)) {
340
+ _data = tobuff(_data)
231
341
  }
232
342
  let isBuf = is(Uint8Array, _data) || is(ArrayBuffer, _data)
233
343
  let isStr = is(String, _data)
package/esm/car.js ADDED
@@ -0,0 +1,10 @@
1
+ import BAR from "./bar.js"
2
+ import ArMem from "./armem-cf.js"
3
+
4
+ class AR extends BAR {
5
+ constructor(opt = {}) {
6
+ super({ ...opt, ArMem })
7
+ }
8
+ }
9
+
10
+ export default AR
package/esm/cf-env.js ADDED
@@ -0,0 +1,29 @@
1
+ // CF Workers polyfill: make emscripten (ao-loader) detect "web" environment.
2
+ // Must be imported BEFORE ao-loader.js so it runs first.
3
+ if (typeof globalThis.window === "undefined") {
4
+ globalThis.window = globalThis
5
+ }
6
+
7
+ // CF Workers blocks WebAssembly.instantiate(buffer, imports) — only pre-compiled
8
+ // WebAssembly.Module objects are allowed. Patch the API so that when a pre-compiled
9
+ // module is queued (via cfWasmReady), emscripten's default async loading path works
10
+ // unchanged, preserving Asyncify and all other internals.
11
+ const _wasmInstantiate = WebAssembly.instantiate.bind(WebAssembly)
12
+ let _cfWasmModule = null
13
+
14
+ export function cfWasmReady(mod) {
15
+ _cfWasmModule = mod
16
+ }
17
+
18
+ WebAssembly.instantiate = async function (source, imports) {
19
+ if (
20
+ _cfWasmModule &&
21
+ (source instanceof ArrayBuffer || ArrayBuffer.isView(source))
22
+ ) {
23
+ const mod = _cfWasmModule
24
+ _cfWasmModule = null
25
+ const instance = await _wasmInstantiate(mod, imports)
26
+ return { instance, module: mod }
27
+ }
28
+ return _wasmInstantiate(source, imports)
29
+ }
package/esm/cf.js ADDED
@@ -0,0 +1,11 @@
1
+ import { acc, mu, su, cu } from "./accounts-web.js"
2
+ import { connect } from "./aoconnect-cf.js"
3
+ import Adaptor from "./adaptor-cf.js"
4
+ import AO from "./wao.js"
5
+ import AR from "./car.js"
6
+ import GQL from "./tgql.js"
7
+ import ArMem from "./armem-cf.js"
8
+ import { dirname } from "./utils.js"
9
+ const scheduler = "_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA"
10
+
11
+ export { GQL, ArMem, AO, AR, connect, acc, mu, su, cu, scheduler, Adaptor }
package/esm/cli.js CHANGED
@@ -192,7 +192,7 @@ const ask = question =>
192
192
 
193
193
 
194
194
  const HB_REPO = "https://github.com/permaweb/HyperBEAM.git"
195
- const HB_TAG = "v0.9-milestone-3-beta-3"
195
+ const HB_TAG = "v0.9-FINAL"
196
196
 
197
197
  const which = async cmd => {
198
198
  try {
@@ -527,10 +527,14 @@ else {
527
527
  process.exit(2)
528
528
  }
529
529
 
530
+ // Node 24+ enables wasm-memory64 by default and rejects the
531
+ // experimental flag at startup; older Node still needs it.
532
+ const _nodeMajor = parseInt((process.versions.node || "0").split(".")[0], 10)
533
+ const _wasm64Flag = _nodeMajor >= 24 ? "" : "--experimental-wasm-memory64"
530
534
  pm2.start(
531
535
  {
532
536
  script: resolve(await dirname(), cmd.script),
533
- nodeArgs: "--experimental-wasm-memory64",
537
+ nodeArgs: _wasm64Flag,
534
538
  instances: 1,
535
539
  force: true,
536
540
  args: args,
package/esm/create.js CHANGED
@@ -141,7 +141,7 @@ const ask = (question) => new Promise(res => {
141
141
  })
142
142
 
143
143
  const HB_REPO = "https://github.com/permaweb/HyperBEAM.git"
144
- const HB_TAG = "v0.9-milestone-3-beta-3"
144
+ const HB_TAG = "v0.9-FINAL"
145
145
 
146
146
  const yellow = t => `\x1b[33m${t}\x1b[39m`
147
147
 
package/esm/devs.js CHANGED
@@ -40,6 +40,7 @@ export default {
40
40
  p4: { name: "p4@1.0", module: "dev_p4" },
41
41
  "node-process": { name: "node-process@1.0", module: "dev_node_process" },
42
42
  "simple-pay": { name: "simple-pay@1.0", module: "dev_simple_pay" },
43
+ metering: { name: "metering@1.0", module: "dev_metering" },
43
44
 
44
45
  // tested
45
46
  cron: { name: "cron@1.0", module: "dev_cron" },
@@ -51,12 +52,16 @@ export default {
51
52
  "local-name": { name: "local-name@1.0", module: "dev_local_name" },
52
53
  lookup: { name: "lookup@1.0", module: "dev_lookup" },
53
54
  name: { name: "name@1.0", module: "dev_name" },
55
+ location: { name: "location@1.0", module: "dev_location" },
56
+ trie: { name: "trie@1.0", module: "dev_trie" },
54
57
 
55
58
  // others
56
59
  compute: { name: "compute@1.0", module: "dev_cu" },
57
60
  dedup: { name: "dedup@1.0", module: "dev_dedup" },
58
61
  manifest: { name: "manifest@1.0", module: "dev_manifest" },
59
62
  monitor: { name: "monitor@1.0", module: "dev_monitor" },
63
+ blacklist: { name: "blacklist@1.0", module: "dev_blacklist" },
64
+ "rate-limit": { name: "rate-limit@1.0", module: "dev_rate_limit" },
60
65
 
61
66
  // advanced
62
67
  snp: { name: "snp@1.0", module: "dev_snp" },
@@ -68,4 +73,14 @@ export default {
68
73
  hyperbuddy: { name: "hyperbuddy@1.0", module: "dev_hyperbuddy" },
69
74
  ans104: { name: "ans104@1.0", module: "dev_codec_ans104" },
70
75
  cacheviz: { name: "cacheviz@1.0", module: "dev_cacheviz" },
76
+
77
+ // v0.9-FINAL additions
78
+ arweave: { name: "arweave@2.9", module: "dev_arweave" },
79
+ "b32-name": { name: "b32-name@1.0", module: "dev_b32_name" },
80
+ bundler: { name: "bundler@1.0", module: "dev_bundler" },
81
+ copycat: { name: "copycat@1.0", module: "dev_copycat" },
82
+ gzip: { name: "gzip@1.0", module: "dev_gzip" },
83
+ secret: { name: "secret@1.0", module: "dev_secret" },
84
+ tx: { name: "tx@1.0", module: "dev_codec_tx" },
85
+ whois: { name: "whois@1.0", module: "dev_whois" },
71
86
  }
package/esm/dodb.js ADDED
@@ -0,0 +1,26 @@
1
+ // Strip functions and other non-serializable values before storing.
2
+ // LMDB silently drops these; DO storage would throw on them.
3
+ // The codebase handles missing `handle` by recreating it on read.
4
+ function sanitize(val) {
5
+ if (val == null) return val
6
+ if (typeof val === "function") return null
7
+ if (typeof val !== "object") return val
8
+ if (val instanceof Uint8Array || val instanceof ArrayBuffer) return val
9
+ if (ArrayBuffer.isView(val)) return val
10
+ if (Array.isArray(val)) return val.map(sanitize)
11
+ const out = {}
12
+ for (const k of Object.keys(val)) {
13
+ const v = val[k]
14
+ if (typeof v !== "function") out[k] = sanitize(v)
15
+ }
16
+ return out
17
+ }
18
+
19
+ export default (storage) => ({
20
+ put: async (key, val) => await storage.put(key, sanitize(val)),
21
+ get: async (key) => await storage.get(key) ?? null,
22
+ getKeys: async ({ start, end }) => {
23
+ const map = await storage.list({ start, end })
24
+ return [...map.keys()]
25
+ },
26
+ })
package/esm/hb.js CHANGED
@@ -17,6 +17,83 @@ import aos_wamr from "./aos_wamr.js"
17
17
  import { ArweaveSigner } from "@ar.io/sdk"
18
18
  import { createData } from "@dha-team/arbundles"
19
19
 
20
+ // Convert objects whose keys are sequential numeric strings (e.g.
21
+ // `{1: "a", 2: "b"}`) into arrays. v0.9-FINAL's multipart re-encoder treats
22
+ // numbered TABMs as lists; JS hbsig only emits `.="list"` in ao-types when
23
+ // the value is an Array, so without this conversion the content-digest of
24
+ // the request body diverges from HB's recomputation and rsa-pss verification
25
+ // fails with `process_not_verified`.
26
+ const normalizeNumberedObjects = v => {
27
+ if (v === null || v === undefined) return v
28
+ if (Array.isArray(v)) return v.map(normalizeNumberedObjects)
29
+ if (Buffer.isBuffer(v) || v instanceof Blob) return v
30
+ if (typeof v !== "object") return v
31
+ const keys = Object.keys(v)
32
+ if (
33
+ keys.length > 0 &&
34
+ keys.every(k => /^[1-9][0-9]*$/.test(k)) &&
35
+ keys
36
+ .map(k => parseInt(k, 10))
37
+ .sort((a, b) => a - b)
38
+ .every((n, i) => n === i + 1)
39
+ ) {
40
+ return keys
41
+ .map(k => parseInt(k, 10))
42
+ .sort((a, b) => a - b)
43
+ .map(i => normalizeNumberedObjects(v[String(i)]))
44
+ }
45
+ const out = {}
46
+ for (const [k, val] of Object.entries(v)) out[k] = normalizeNumberedObjects(val)
47
+ return out
48
+ }
49
+
50
+ // v0.9-FINAL returns Messages with lowercase fields (data, target, anchor, from)
51
+ // where legacy AOS-format consumers expect capitalised (Data, Target, Anchor,
52
+ // From). Also lower-cases tag {name, value} pairs and may emit them as a
53
+ // numbered-key map under "tags" rather than an array under "Tags". Normalise
54
+ // so existing callers that read `Messages[0].Data` keep working.
55
+ const _normalizeLegacyResult = res => {
56
+ if (!res || typeof res !== "object") return res
57
+ const _msgs = res.Messages ?? res.messages
58
+ if (!Array.isArray(_msgs)) return res
59
+ const _tagsArray = t => {
60
+ if (!t) return []
61
+ if (Array.isArray(t)) {
62
+ return t.map(x => ({
63
+ name: x?.name ?? x?.Name,
64
+ value: x?.value ?? x?.Value,
65
+ })).filter(x => x.name != null)
66
+ }
67
+ if (typeof t === "object") {
68
+ return Object.keys(t)
69
+ .filter(k => /^\d+$/.test(k))
70
+ .sort((a, b) => Number(a) - Number(b))
71
+ .map(k => {
72
+ const x = t[k]
73
+ return {
74
+ name: x?.name ?? x?.Name,
75
+ value: x?.value ?? x?.Value,
76
+ }
77
+ })
78
+ .filter(x => x.name != null)
79
+ }
80
+ return []
81
+ }
82
+ const Messages = _msgs.map(m => {
83
+ if (!m || typeof m !== "object") return m
84
+ const tags = _tagsArray(m.Tags ?? m.tags)
85
+ return {
86
+ ...m,
87
+ Data: m.Data ?? m.data,
88
+ Target: m.Target ?? m.target,
89
+ Anchor: m.Anchor ?? m.anchor,
90
+ From: m.From ?? m.from,
91
+ Tags: tags,
92
+ }
93
+ })
94
+ return { ...res, Messages }
95
+ }
96
+
20
97
  const toMsg = async req => {
21
98
  let msg = {}
22
99
  req?.headers?.forEach((v, k) => {
@@ -251,24 +328,17 @@ class HB {
251
328
  async computeLegacy({ pid, slot }) {
252
329
  // Match master: compute and parse results.json.body
253
330
  const json = await this.compute({ pid, slot })
331
+ let parsed = json
254
332
  if (json?.results?.json?.body) {
255
- return JSON.parse(json.results.json.body)
256
- }
257
- // Fallback: try compute/results/json/body structure
258
- if (json?.["compute/results/json"]?.body) {
259
- return JSON.parse(json["compute/results/json"].body)
260
- }
261
- // Remote nodes return results.raw with CU format directly
262
- if (json?.results?.raw) {
333
+ parsed = JSON.parse(json.results.json.body)
334
+ } else if (json?.["compute/results/json"]?.body) {
335
+ parsed = JSON.parse(json["compute/results/json"].body)
336
+ } else if (json?.results?.raw) {
263
337
  const raw = typeof json.results.raw === "string"
264
338
  ? JSON.parse(json.results.raw) : json.results.raw
265
- if (raw?.Messages || raw?.Output) return raw
266
- }
267
- // Another fallback: check if it's the raw CU format
268
- if (json?.Messages || json?.Output) {
269
- return json
339
+ if (raw?.Messages || raw?.Output) parsed = raw
270
340
  }
271
- return json
341
+ return _normalizeLegacyResult(parsed)
272
342
  }
273
343
 
274
344
  async cacheScript(data, type = "application/lua") {
@@ -448,6 +518,13 @@ class HB {
448
518
  }
449
519
  async spawn(tags = {}) {
450
520
  await this.setInfo()
521
+ // v0.9-FINAL multipart re-encoding sees a numbered-key object as a list
522
+ // (TABM with .="list" in ao-types). JS hbsig's structured codec encodes
523
+ // a plain numeric-keyed object without that marker, so HB's recomputed
524
+ // content-digest doesn't match what JS signed and verify fails. Pre-
525
+ // normalize any numbered-key object value into an array so the array
526
+ // path (which already emits .="list") is used.
527
+ tags = normalizeNumberedObjects(tags)
451
528
  let res = null
452
529
  if (this.format === "ans104") {
453
530
  res = await this.post104({
@@ -462,7 +539,7 @@ class HB {
462
539
  })
463
540
  return { res, pid: res.out.process }
464
541
  } else {
465
- // Use httpsig-signed multipart POST (beta3-compatible approach)
542
+ // Use httpsig-signed multipart POST
466
543
  const spawnTags = mergeLeft(tags, {
467
544
  "random-seed": seed(16),
468
545
  type: "Process",
@@ -742,7 +819,7 @@ class HB {
742
819
  i++
743
820
  }
744
821
  }
745
- // Add accept-bundle header to get inline data instead of links (beta3 compatibility)
822
+ // Add accept-bundle header to get inline data instead of links
746
823
  const url = `${this.url}${path}${_json}${_params}`
747
824
  let response
748
825
  for (let attempt = 0; attempt < 3; attempt++) {