@wovin/core 0.2.2 → 0.3.2

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 (112) hide show
  1. package/dist/applog/applog-utils.d.ts +25 -1
  2. package/dist/applog/applog-utils.d.ts.map +1 -1
  3. package/dist/applog/datom-types.d.ts.map +1 -1
  4. package/dist/applog.js +5 -3
  5. package/dist/blockstore.js +2 -0
  6. package/dist/blockstore.js.map +1 -1
  7. package/dist/{chunk-SHUHRHOT.js → chunk-4MKPGQIM.js} +17 -18
  8. package/dist/chunk-4MKPGQIM.js.map +1 -0
  9. package/dist/chunk-64EJIJAJ.js +17 -0
  10. package/dist/chunk-64EJIJAJ.js.map +1 -0
  11. package/dist/{chunk-HUIQ54TT.js → chunk-6CSJTSQP.js} +3 -3
  12. package/dist/chunk-7QEGHKR4.js +17 -0
  13. package/dist/chunk-7QEGHKR4.js.map +1 -0
  14. package/dist/{chunk-BLF5MAWU.js → chunk-BIYQEX3N.js} +2 -2
  15. package/dist/{chunk-6ALNRM3J.js → chunk-H3JNNTVP.js} +21 -8
  16. package/dist/chunk-H3JNNTVP.js.map +1 -0
  17. package/dist/{chunk-OC6Z6CQW.js → chunk-H4YVJKB7.js} +2 -2
  18. package/dist/chunk-ICBK7NC4.js +27 -0
  19. package/dist/chunk-ICBK7NC4.js.map +1 -0
  20. package/dist/chunk-N5QPZNKD.js +288 -0
  21. package/dist/chunk-N5QPZNKD.js.map +1 -0
  22. package/dist/{chunk-3SUFNJEZ.js → chunk-N7SEGHU4.js} +7 -2
  23. package/dist/{chunk-3SUFNJEZ.js.map → chunk-N7SEGHU4.js.map} +1 -1
  24. package/dist/chunk-XF4DWOAE.js +25 -0
  25. package/dist/chunk-XF4DWOAE.js.map +1 -0
  26. package/dist/index.js +20 -10
  27. package/dist/ipfs/car.d.ts.map +1 -1
  28. package/dist/ipfs.js +4 -4
  29. package/dist/ipns/gateway-resolver.d.ts +21 -0
  30. package/dist/ipns/gateway-resolver.d.ts.map +1 -0
  31. package/dist/ipns/ipns-record.d.ts +46 -7
  32. package/dist/ipns/ipns-record.d.ts.map +1 -1
  33. package/dist/ipns/ipns-w3name.d.ts +45 -0
  34. package/dist/ipns/ipns-w3name.d.ts.map +1 -0
  35. package/dist/ipns/ipns-watcher.d.ts +190 -0
  36. package/dist/ipns/ipns-watcher.d.ts.map +1 -0
  37. package/dist/ipns.d.ts +3 -0
  38. package/dist/ipns.d.ts.map +1 -1
  39. package/dist/ipns.js +591 -14
  40. package/dist/ipns.js.map +1 -1
  41. package/dist/pubsub/snap-push.d.ts +2 -2
  42. package/dist/pubsub/snap-push.d.ts.map +1 -1
  43. package/dist/pubsub.js +4 -4
  44. package/dist/query/epoch-snapshot.d.ts +205 -0
  45. package/dist/query/epoch-snapshot.d.ts.map +1 -0
  46. package/dist/query.d.ts +1 -0
  47. package/dist/query.d.ts.map +1 -1
  48. package/dist/query.js +12 -4
  49. package/dist/retrieve.js +4 -4
  50. package/dist/thread.js +1 -1
  51. package/dist/viewmodel/adapters/arktype.d.ts +33 -0
  52. package/dist/viewmodel/adapters/arktype.d.ts.map +1 -0
  53. package/dist/viewmodel/adapters/arktype.js +7 -0
  54. package/dist/viewmodel/adapters/arktype.js.map +1 -0
  55. package/dist/viewmodel/adapters/typebox.d.ts +35 -0
  56. package/dist/viewmodel/adapters/typebox.d.ts.map +1 -0
  57. package/dist/viewmodel/adapters/typebox.js +7 -0
  58. package/dist/viewmodel/adapters/typebox.js.map +1 -0
  59. package/dist/viewmodel/adapters/typia.d.ts +40 -0
  60. package/dist/viewmodel/adapters/typia.d.ts.map +1 -0
  61. package/dist/viewmodel/adapters/typia.js +7 -0
  62. package/dist/viewmodel/adapters/typia.js.map +1 -0
  63. package/dist/viewmodel/adapters/zod.d.ts +30 -0
  64. package/dist/viewmodel/adapters/zod.d.ts.map +1 -0
  65. package/dist/viewmodel/adapters/zod.js +7 -0
  66. package/dist/viewmodel/adapters/zod.js.map +1 -0
  67. package/dist/viewmodel/builder.d.ts +40 -0
  68. package/dist/viewmodel/builder.d.ts.map +1 -0
  69. package/dist/viewmodel/examples/all-adapters.d.ts +26 -0
  70. package/dist/viewmodel/examples/all-adapters.d.ts.map +1 -0
  71. package/dist/viewmodel/factory.d.ts +38 -0
  72. package/dist/viewmodel/factory.d.ts.map +1 -0
  73. package/dist/viewmodel/index.d.ts +10 -0
  74. package/dist/viewmodel/index.d.ts.map +1 -0
  75. package/dist/viewmodel/index.js +313 -0
  76. package/dist/viewmodel/index.js.map +1 -0
  77. package/dist/viewmodel/schema-adapter.d.ts +16 -0
  78. package/dist/viewmodel/schema-adapter.d.ts.map +1 -0
  79. package/dist/viewmodel/types.d.ts +97 -0
  80. package/dist/viewmodel/types.d.ts.map +1 -0
  81. package/package.json +29 -3
  82. package/src/applog/applog-utils.test.ts +43 -1
  83. package/src/applog/applog-utils.ts +38 -21
  84. package/src/applog/datom-types.ts +2 -2
  85. package/src/applog/object-values.test.ts +9 -9
  86. package/src/ipfs/car.ts +14 -2
  87. package/src/ipns/gateway-resolver.ts +63 -0
  88. package/src/ipns/ipns-record.ts +158 -29
  89. package/src/ipns/ipns-w3name.ts +207 -0
  90. package/src/ipns/ipns-watcher.ts +608 -0
  91. package/src/ipns.ts +3 -0
  92. package/src/pubsub/snap-push.ts +6 -5
  93. package/src/query/epoch-snapshot.test.ts +594 -0
  94. package/src/query/epoch-snapshot.ts +392 -0
  95. package/src/query.ts +1 -0
  96. package/src/viewmodel/adapters/arktype.ts +44 -0
  97. package/src/viewmodel/adapters/typebox.ts +59 -0
  98. package/src/viewmodel/adapters/typia.ts +50 -0
  99. package/src/viewmodel/adapters/zod.ts +55 -0
  100. package/src/viewmodel/builder.ts +71 -0
  101. package/src/viewmodel/examples/all-adapters.ts +206 -0
  102. package/src/viewmodel/factory.ts +330 -0
  103. package/src/viewmodel/index.ts +22 -0
  104. package/src/viewmodel/schema-adapter.ts +27 -0
  105. package/src/viewmodel/types.ts +152 -0
  106. package/dist/chunk-22WDFLXO.js +0 -138
  107. package/dist/chunk-22WDFLXO.js.map +0 -1
  108. package/dist/chunk-6ALNRM3J.js.map +0 -1
  109. package/dist/chunk-SHUHRHOT.js.map +0 -1
  110. /package/dist/{chunk-HUIQ54TT.js.map → chunk-6CSJTSQP.js.map} +0 -0
  111. /package/dist/{chunk-BLF5MAWU.js.map → chunk-BIYQEX3N.js.map} +0 -0
  112. /package/dist/{chunk-OC6Z6CQW.js.map → chunk-H4YVJKB7.js.map} +0 -0
@@ -5,9 +5,9 @@ import { base64pad } from 'multiformats/bases/base64'
5
5
  import type { CID } from 'multiformats/cid'
6
6
 
7
7
  export interface SignedIPNSRecord {
8
- recordBytes: Uint8Array // marshalled protobuf, signed — the wire format
9
- ipnsName: string // k51... string
10
- value: string // /ipfs/<cid>
8
+ recordBytes: Uint8Array // marshalled protobuf, signed — the wire format
9
+ ipnsName: string // k51... string
10
+ value: string // /ipfs/<cid>
11
11
  sequence: bigint
12
12
  }
13
13
 
@@ -37,16 +37,31 @@ export async function createSignedIPNSRecord(
37
37
 
38
38
  export { unmarshalIPNSRecord }
39
39
 
40
- /** A target that can receive a signed IPNS record */
40
+ /**
41
+ * A target that can receive a signed IPNS record, advertise its current
42
+ * sequence, or both.
43
+ *
44
+ * - `publish` is called by `publishIPNSRecord` to actually store the record
45
+ * on the target's backing service. Targets without `publish` are skipped
46
+ * during the fan-out (e.g. a sequence-only source).
47
+ * - `resolveSequence` is consulted by `publishIPNSRecord` to compute the
48
+ * next IPNS sequence. The first target to return a value (including
49
+ * `null` for "never published") wins. Throw to indicate a transient
50
+ * error — the caller will try the next target.
51
+ *
52
+ * Most real targets (e.g. a storage connector) provide both. A simple
53
+ * "track sequence in localStorage" target only needs `resolveSequence`.
54
+ */
41
55
  export interface IPNSPublishTarget {
42
56
  name: string
43
- publish(ipnsName: string, recordBytes: Uint8Array): Promise<void>
57
+ publish?(ipnsName: string, recordBytes: Uint8Array): Promise<void>
58
+ resolveSequence?(ipnsName: string): Promise<bigint | null>
44
59
  }
45
60
 
46
61
  /**
47
- * Resolve current IPNS sequence number from a naming service.
62
+ * Resolve the current IPNS sequence from a generic naming service.
48
63
  * Returns null if the name was never published (404).
49
- * Throws on network/server errors.
64
+ * Throws on network/server errors so the caller can try a different target.
50
65
  */
51
66
  export async function resolveIPNSSequence(
52
67
  nameServiceUrl: string,
@@ -75,41 +90,155 @@ export async function resolveIPNSSequence(
75
90
  return entry.sequence
76
91
  }
77
92
 
78
- // Some servers return value but not raw record — can't get sequence
93
+ // Server only returned a value, no raw record — can't know the exact sequence.
94
+ // Most services still accept this as "previous published"; treat as seq=0n.
79
95
  return 0n
80
96
  }
81
97
 
98
+ /** Regex to detect Kubo IPNS sequence conflict in error messages */
99
+ const SEQ_CONFLICT_RE = /existing IPNS record has sequence (\d+) >= new record sequence (\d+)/
100
+
101
+ /**
102
+ * Options for {@link publishIPNSRecord}.
103
+ */
104
+ export interface PublishIPNSRecordOptions {
105
+ /**
106
+ * Max number of times to bump the sequence on "existing >= new" conflict.
107
+ * Defaults to 3. Each bump re-signs the record with a higher sequence
108
+ * and retries all targets.
109
+ */
110
+ maxSequenceBumps?: number
111
+ }
112
+
82
113
  /**
83
114
  * Create a signed IPNS record and publish to all configured targets.
84
- * Resolves sequence from sequenceServiceUrl, creates record once, fans out.
85
- * Throws if ALL targets fail; warns on partial failure.
115
+ *
116
+ * Sequence resolution: walks `targets` in order asking each one with a
117
+ * `resolveSequence` method. The first target to successfully return a value
118
+ * (including `null` for "never published") determines the next sequence.
119
+ * If every target throws or none supports `resolveSequence`, falls back to 0n.
120
+ *
121
+ * Publish fan-out: only targets with a `publish` method are called.
122
+ * Throws if every publish-capable target fails; partial failures are logged.
123
+ *
124
+ * **Sequence-coherence retry:** if a publish target rejects with the Kubo
125
+ * "existing IPNS record has sequence X >= new record sequence Y" error,
126
+ * the function parses `X`, bumps the sequence to `max(X + 1n, Y + 1n)`,
127
+ * re-signs the record, and retries (up to `maxSequenceBumps` times).
128
+ * This is the "increment and clobber" mechanism that recovers from
129
+ * sequence drift without requiring server-side `--force` support.
86
130
  */
87
131
  export async function publishIPNSRecord(
88
132
  privateKey: Uint8Array,
89
133
  cid: CID,
90
134
  targets: IPNSPublishTarget[],
91
- sequenceServiceUrl = 'https://name.web3.storage',
135
+ options?: PublishIPNSRecordOptions,
92
136
  ): Promise<SignedIPNSRecord> {
93
137
  const ipnsName = ipnsNameFromPrivateKey(privateKey)
94
- const currentSeq = await resolveIPNSSequence(sequenceServiceUrl, ipnsName)
95
- const sequence = currentSeq != null ? currentSeq + 1n : 0n
96
- const signed = await createSignedIPNSRecord(privateKey, cid, sequence)
97
-
98
- const results = await Promise.allSettled(
99
- targets.map(t => t.publish(ipnsName, signed.recordBytes))
100
- )
101
- const failures = results
102
- .map((r, i) => ({ r, name: targets[i].name }))
103
- .filter(({ r }) => r.status === 'rejected') as { r: PromiseRejectedResult; name: string }[]
104
-
105
- if (failures.length > 0 && failures.length < targets.length) {
106
- // Partial failure log but don't throw
107
- for (const { r, name } of failures) {
108
- console.warn(`[publishIPNSRecord] target '${name}' failed:`, r.reason)
138
+ const maxBumps = options?.maxSequenceBumps ?? 3
139
+
140
+ const publishTargets = targets.filter(t => typeof t.publish === 'function')
141
+ if (publishTargets.length === 0) {
142
+ throw new Error('No publish-capable targets supplied to publishIPNSRecord')
143
+ }
144
+
145
+ let sequence: bigint | undefined
146
+ let bumpFloor: bigint | undefined
147
+
148
+ for (let attempt = 0; ; attempt++) {
149
+ // Compute next sequence on first pass or after a conflict bump
150
+ if (sequence === undefined) {
151
+ sequence = await pickNextSequence(ipnsName, targets)
152
+ }
153
+ if (bumpFloor !== undefined) {
154
+ // bumpFloor was set on previous attempt from a conflict error
155
+ if (bumpFloor > sequence) sequence = bumpFloor
156
+ bumpFloor = undefined
157
+ }
158
+
159
+ const signed = await createSignedIPNSRecord(privateKey, cid, sequence)
160
+
161
+ const results = await Promise.allSettled(
162
+ publishTargets.map(t => t.publish!(ipnsName, signed.recordBytes)),
163
+ )
164
+ const failures = results
165
+ .map((r, i) => ({ r, name: publishTargets[i].name }))
166
+ .filter(({ r }) => r.status === 'rejected') as { r: PromiseRejectedResult; name: string }[]
167
+
168
+ // All succeeded — done
169
+ if (failures.length === 0) {
170
+ return signed
171
+ }
172
+
173
+ // Check if any failure is a sequence conflict we can recover from
174
+ const conflictReasons = failures.filter(({ r }) =>
175
+ SEQ_CONFLICT_RE.test(typeof r.reason === 'string' ? r.reason : String(r.reason)),
176
+ )
177
+
178
+ if (conflictReasons.length > 0 && attempt < maxBumps) {
179
+ // Parse the highest existing sequence from each conflict error
180
+ let bump = sequence
181
+ for (const { r } of conflictReasons) {
182
+ const msg = typeof r.reason === 'string' ? r.reason : String(r.reason)
183
+ const m = msg.match(SEQ_CONFLICT_RE)
184
+ if (m) {
185
+ const existing = BigInt(m[1])
186
+ if (existing >= bump) bump = existing + 1n
187
+ }
188
+ }
189
+ if (bump > sequence) {
190
+ console.warn(
191
+ `[publishIPNSRecord] Sequence conflict: server seq ${bump - 1n} >= current ${sequence}, ` +
192
+ `bumping to ${bump} (attempt ${attempt + 1}/${maxBumps})`,
193
+ )
194
+ sequence = bump
195
+ continue
196
+ }
109
197
  }
110
- } else if (failures.length === targets.length) {
111
- throw new Error(`All IPNS publish targets failed: ${failures.map(({ r, name }) => `${name}: ${r.reason}`).join('; ')}`)
198
+
199
+ // Partial failure (some targets succeeded) log and return the signed record
200
+ if (failures.length < publishTargets.length) {
201
+ for (const { r, name } of failures) {
202
+ console.warn(`[publishIPNSRecord] target '${name}' failed:`, r.reason)
203
+ }
204
+ return signed
205
+ }
206
+
207
+ // Every publish-capable target failed and we cannot recover
208
+ if (conflictReasons.length > 0) {
209
+ throw new Error(
210
+ `IPNS sequence conflict not resolved after ${Math.min(attempt + 1, maxBumps)} bumps. ` +
211
+ `All publish targets failed: ${failures.map(({ r, name }) => `${name}: ${r.reason}`).join('; ')}`,
212
+ )
213
+ }
214
+ throw new Error(
215
+ `All IPNS publish targets failed: ${failures.map(({ r, name }) => `${name}: ${r.reason}`).join('; ')}`,
216
+ )
112
217
  }
218
+ }
113
219
 
114
- return signed
220
+ /**
221
+ * Walk targets, asking each to resolve the current IPNS sequence. First success wins.
222
+ * Falls back to 0n if no target can answer.
223
+ */
224
+ async function pickNextSequence(
225
+ ipnsName: string,
226
+ targets: IPNSPublishTarget[],
227
+ ): Promise<bigint> {
228
+ const capable = targets.filter(t => typeof t.resolveSequence === 'function')
229
+ if (capable.length === 0) {
230
+ return 0n
231
+ }
232
+
233
+ for (const target of capable) {
234
+ try {
235
+ const current = await target.resolveSequence!(ipnsName)
236
+ return current == null ? 0n : current + 1n
237
+ } catch (err) {
238
+ console.warn(`[publishIPNSRecord] target '${target.name}' sequence resolve failed:`, err)
239
+ }
240
+ }
241
+
242
+ console.warn(`[publishIPNSRecord] no target could resolve sequence for ${ipnsName} — starting at 0n`)
243
+ return 0n
115
244
  }
@@ -0,0 +1,207 @@
1
+ import type { IPNSPublishTarget } from '@wovin/core/ipns'
2
+ import { Logger } from 'besonders-logger'
3
+ import { base64pad } from 'multiformats/bases/base64'
4
+ import { resolveIPNSSequence, unmarshalIPNSRecord } from './ipns-record.ts'
5
+ import { CID } from 'multiformats/cid'
6
+ import * as W3Name from 'w3name'
7
+
8
+ const { WARN, LOG, DEBUG, ERROR } = Logger.setup(Logger.INFO)
9
+
10
+ /**
11
+ * Try to resolve IPNS name to get existing revision.
12
+ * Returns null if record doesn't exist (404).
13
+ * Throws on network errors or server errors.
14
+ *
15
+ * This does a custom HTTP check first to distinguish 404 from network errors,
16
+ * then delegates to W3Name.resolve() for validation if record exists.
17
+ */
18
+ async function tryResolveIPNS(ipns: W3Name.WritableName): Promise<W3Name.Revision | null> {
19
+ const url = `https://name.web3.storage/name/${ipns.toString()}`
20
+
21
+ let response: Response
22
+ try {
23
+ response = await fetch(url, { signal: AbortSignal.timeout(30_000) })
24
+ } catch (err) {
25
+ // Network error (no connection, DNS failure, etc.)
26
+ throw ERROR('[w3name] Network error resolving IPNS:', err)
27
+ }
28
+
29
+ // 404 = record never published
30
+ if (response.status === 404) {
31
+ DEBUG('[w3name] IPNS record not found (never published):', ipns.toString())
32
+ return null
33
+ }
34
+
35
+ // Other HTTP errors (5xx server error, etc.)
36
+ if (!response.ok) {
37
+ throw ERROR(`[w3name] HTTP ${response.status} resolving IPNS:`, response.statusText)
38
+ }
39
+
40
+ // Success - use W3Name.resolve to get validated Revision
41
+ // (We could parse the record ourselves, but W3Name does validation/signature checks)
42
+ const existing = await W3Name.resolve(ipns)
43
+ return existing
44
+ }
45
+
46
+ /**
47
+ * Publish CID to IPNS, automatically handling increment vs v0.
48
+ * Returns the revision for further processing (e.g., Kubo integration).
49
+ */
50
+ export async function publishIPNS(ipnsPrivateKey: Uint8Array, cid: CID): Promise<W3Name.Revision> {
51
+ const TIMEOUT_MS = 30_000
52
+ const timeout = new Promise<never>((_, reject) =>
53
+ setTimeout(() => reject(new Error(`publishIPNS timed out after ${TIMEOUT_MS}ms`)), TIMEOUT_MS),
54
+ )
55
+ return Promise.race([_publishIPNSImpl(ipnsPrivateKey, cid), timeout])
56
+ }
57
+
58
+ async function _publishIPNSImpl(ipnsPrivateKey: Uint8Array, cid: CID): Promise<W3Name.Revision> {
59
+ const value = `/ipfs/${cid}`
60
+ const ipns = await W3Name.from(ipnsPrivateKey)
61
+
62
+ let revision: W3Name.Revision
63
+ const existing = await tryResolveIPNS(ipns)
64
+
65
+ if (existing) {
66
+ // Record exists - increment sequence number
67
+ revision = await W3Name.increment(existing, value)
68
+ DEBUG('[w3name] incrementing revision for', ipns.toString())
69
+ } else {
70
+ // First publish - use v0
71
+ revision = await W3Name.v0(ipns, value)
72
+ DEBUG('[w3name] creating initial revision for', ipns.toString())
73
+ }
74
+
75
+ await W3Name.publish(revision, ipns.key)
76
+ DEBUG('[w3name] published', cid.toString(), 'to', ipns.toString())
77
+
78
+ return revision // Return for Kubo integration or other uses
79
+ }
80
+
81
+ // ─────────────────────────────────────────────────────────────────────────────
82
+ // zt.ax naming service target (primary)
83
+ // ─────────────────────────────────────────────────────────────────────────────
84
+
85
+ /**
86
+ * Resolve the current IPNS sequence from a zt.ax-compatible naming service.
87
+ *
88
+ * The naming service API uses the simple W3NameRecord format:
89
+ * `GET <baseUrl>/<ipnsName>` → `{ value: "/ipfs/<cid>", seq?: number, validity?: string }`
90
+ *
91
+ * Also supports the legacy w3name record format (`{ record: "<base64>", value: "..." }`)
92
+ * for backward compatibility with hybrid services.
93
+ */
94
+ export async function resolveZtaxSequence(
95
+ baseUrl: string,
96
+ ipnsName: string,
97
+ ): Promise<bigint | null> {
98
+ const url = `${baseUrl.replace(/\/+$/, '')}/${ipnsName}`
99
+ let response: Response
100
+ try {
101
+ response = await fetch(url)
102
+ } catch (err) {
103
+ throw new Error(`Network error resolving sequence from ${baseUrl}: ${err}`)
104
+ }
105
+
106
+ if (response.status === 404) return null
107
+ if (!response.ok) {
108
+ throw new Error(`HTTP ${response.status} resolving sequence from ${baseUrl}: ${response.statusText}`)
109
+ }
110
+
111
+ const body: Record<string, unknown> = await response.json()
112
+
113
+ // Format 1: simple W3NameRecord with seq number
114
+ if (typeof body.seq === 'number') {
115
+ return BigInt(body.seq)
116
+ }
117
+
118
+ // Format 2: legacy w3name record with base64-encoded protobuf
119
+ if (typeof body.record === 'string') {
120
+ try {
121
+ const bytes = base64pad.baseDecode(body.record)
122
+ const entry = unmarshalIPNSRecord(bytes)
123
+ return entry.sequence
124
+ } catch {
125
+ WARN('[resolveZtaxSequence] failed to unmarshal record for', ipnsName)
126
+ return null
127
+ }
128
+ }
129
+
130
+ // No sequence info — treat as never published or zero
131
+ return null
132
+ }
133
+
134
+ /**
135
+ * Create an IPNSPublishTarget that publishes to a zt.ax naming service.
136
+ *
137
+ * Uses HTTP POST to publish signed IPNS records and HTTP GET to resolve
138
+ * the current sequence number, following the same wire format as the
139
+ * standard w3name API but at a configurable base URL.
140
+ *
141
+ * The naming service is expected to support:
142
+ * - `POST <baseUrl>/<ipnsName>` — publish signed IPNS record (base64-encoded body)
143
+ * - `GET <baseUrl>/<ipnsName>` — resolve current record (`{ value, seq?, record? }`)
144
+ *
145
+ * @param baseUrl - Base URL of the naming service (default: https://ipfs.zt.ax)
146
+ */
147
+ export function ztaxTarget(baseUrl = 'https://ipfs.zt.ax'): IPNSPublishTarget {
148
+ const cleanBase = baseUrl.replace(/\/+$/, '')
149
+ return {
150
+ name: 'zt.ax',
151
+ async publish(ipnsName: string, recordBytes: Uint8Array) {
152
+ const url = `${cleanBase}/${ipnsName}`
153
+ DEBUG(`[ztaxTarget] POST ${url}`)
154
+ const res = await fetch(url, {
155
+ method: 'POST',
156
+ body: base64pad.baseEncode(recordBytes),
157
+ })
158
+ if (!res.ok) {
159
+ let detail = ''
160
+ try {
161
+ const body = await res.json() as Record<string, unknown>
162
+ detail = body.message ? ` — ${body.message}` : ''
163
+ } catch {
164
+ detail = res.statusText ? ` — ${res.statusText}` : ''
165
+ }
166
+ throw new Error(`zt.ax HTTP ${res.status}${detail}`)
167
+ }
168
+ },
169
+ async resolveSequence(ipnsName: string): Promise<bigint | null> {
170
+ return resolveZtaxSequence(cleanBase, ipnsName)
171
+ },
172
+ }
173
+ }
174
+
175
+ // ─────────────────────────────────────────────────────────────────────────────
176
+ // w3name naming service target (deprecated — read‑only backup)
177
+ // ─────────────────────────────────────────────────────────────────────────────
178
+
179
+ /**
180
+ * Create an IPNSPublishTarget for the w3name service.
181
+ *
182
+ * @deprecated w3name is shut down for publishing. This target is kept as a
183
+ * read‑only backup for sequence resolution only (no publish method).
184
+ * Use {@link ztaxTarget} for all publishing.
185
+ *
186
+ * @param serviceUrl - Base URL of the w3name-compatible service (default: https://name.web3.storage)
187
+ */
188
+ export function w3nameTarget(serviceUrl = 'https://name.web3.storage'): IPNSPublishTarget {
189
+ return {
190
+ name: 'w3name',
191
+ // No publish() — w3name is read-only; publishing goes through ztaxTarget
192
+ async resolveSequence(ipnsName: string): Promise<bigint | null> {
193
+ return resolveIPNSSequence(serviceUrl, ipnsName)
194
+ },
195
+ }
196
+ }
197
+
198
+ // ─────────────────────────────────────────────────────────────────────────────
199
+
200
+ export async function generateIpnsKey() {
201
+ return W3Name.create() // Returns W3Name.WritableName type
202
+ }
203
+
204
+ export async function getW3NamePublic(pk: Uint8Array) {
205
+ const ipns = await W3Name.from(pk)
206
+ return ipns.toString()
207
+ }