@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.
- package/dist/applog/applog-utils.d.ts +25 -1
- package/dist/applog/applog-utils.d.ts.map +1 -1
- package/dist/applog/datom-types.d.ts.map +1 -1
- package/dist/applog.js +5 -3
- package/dist/blockstore.js +2 -0
- package/dist/blockstore.js.map +1 -1
- package/dist/{chunk-SHUHRHOT.js → chunk-4MKPGQIM.js} +17 -18
- package/dist/chunk-4MKPGQIM.js.map +1 -0
- package/dist/chunk-64EJIJAJ.js +17 -0
- package/dist/chunk-64EJIJAJ.js.map +1 -0
- package/dist/{chunk-HUIQ54TT.js → chunk-6CSJTSQP.js} +3 -3
- package/dist/chunk-7QEGHKR4.js +17 -0
- package/dist/chunk-7QEGHKR4.js.map +1 -0
- package/dist/{chunk-BLF5MAWU.js → chunk-BIYQEX3N.js} +2 -2
- package/dist/{chunk-6ALNRM3J.js → chunk-H3JNNTVP.js} +21 -8
- package/dist/chunk-H3JNNTVP.js.map +1 -0
- package/dist/{chunk-OC6Z6CQW.js → chunk-H4YVJKB7.js} +2 -2
- package/dist/chunk-ICBK7NC4.js +27 -0
- package/dist/chunk-ICBK7NC4.js.map +1 -0
- package/dist/chunk-N5QPZNKD.js +288 -0
- package/dist/chunk-N5QPZNKD.js.map +1 -0
- package/dist/{chunk-3SUFNJEZ.js → chunk-N7SEGHU4.js} +7 -2
- package/dist/{chunk-3SUFNJEZ.js.map → chunk-N7SEGHU4.js.map} +1 -1
- package/dist/chunk-XF4DWOAE.js +25 -0
- package/dist/chunk-XF4DWOAE.js.map +1 -0
- package/dist/index.js +20 -10
- package/dist/ipfs/car.d.ts.map +1 -1
- package/dist/ipfs.js +4 -4
- package/dist/ipns/gateway-resolver.d.ts +21 -0
- package/dist/ipns/gateway-resolver.d.ts.map +1 -0
- package/dist/ipns/ipns-record.d.ts +46 -7
- package/dist/ipns/ipns-record.d.ts.map +1 -1
- package/dist/ipns/ipns-w3name.d.ts +45 -0
- package/dist/ipns/ipns-w3name.d.ts.map +1 -0
- package/dist/ipns/ipns-watcher.d.ts +190 -0
- package/dist/ipns/ipns-watcher.d.ts.map +1 -0
- package/dist/ipns.d.ts +3 -0
- package/dist/ipns.d.ts.map +1 -1
- package/dist/ipns.js +591 -14
- package/dist/ipns.js.map +1 -1
- package/dist/pubsub/snap-push.d.ts +2 -2
- package/dist/pubsub/snap-push.d.ts.map +1 -1
- package/dist/pubsub.js +4 -4
- package/dist/query/epoch-snapshot.d.ts +205 -0
- package/dist/query/epoch-snapshot.d.ts.map +1 -0
- package/dist/query.d.ts +1 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +12 -4
- package/dist/retrieve.js +4 -4
- package/dist/thread.js +1 -1
- package/dist/viewmodel/adapters/arktype.d.ts +33 -0
- package/dist/viewmodel/adapters/arktype.d.ts.map +1 -0
- package/dist/viewmodel/adapters/arktype.js +7 -0
- package/dist/viewmodel/adapters/arktype.js.map +1 -0
- package/dist/viewmodel/adapters/typebox.d.ts +35 -0
- package/dist/viewmodel/adapters/typebox.d.ts.map +1 -0
- package/dist/viewmodel/adapters/typebox.js +7 -0
- package/dist/viewmodel/adapters/typebox.js.map +1 -0
- package/dist/viewmodel/adapters/typia.d.ts +40 -0
- package/dist/viewmodel/adapters/typia.d.ts.map +1 -0
- package/dist/viewmodel/adapters/typia.js +7 -0
- package/dist/viewmodel/adapters/typia.js.map +1 -0
- package/dist/viewmodel/adapters/zod.d.ts +30 -0
- package/dist/viewmodel/adapters/zod.d.ts.map +1 -0
- package/dist/viewmodel/adapters/zod.js +7 -0
- package/dist/viewmodel/adapters/zod.js.map +1 -0
- package/dist/viewmodel/builder.d.ts +40 -0
- package/dist/viewmodel/builder.d.ts.map +1 -0
- package/dist/viewmodel/examples/all-adapters.d.ts +26 -0
- package/dist/viewmodel/examples/all-adapters.d.ts.map +1 -0
- package/dist/viewmodel/factory.d.ts +38 -0
- package/dist/viewmodel/factory.d.ts.map +1 -0
- package/dist/viewmodel/index.d.ts +10 -0
- package/dist/viewmodel/index.d.ts.map +1 -0
- package/dist/viewmodel/index.js +313 -0
- package/dist/viewmodel/index.js.map +1 -0
- package/dist/viewmodel/schema-adapter.d.ts +16 -0
- package/dist/viewmodel/schema-adapter.d.ts.map +1 -0
- package/dist/viewmodel/types.d.ts +97 -0
- package/dist/viewmodel/types.d.ts.map +1 -0
- package/package.json +29 -3
- package/src/applog/applog-utils.test.ts +43 -1
- package/src/applog/applog-utils.ts +38 -21
- package/src/applog/datom-types.ts +2 -2
- package/src/applog/object-values.test.ts +9 -9
- package/src/ipfs/car.ts +14 -2
- package/src/ipns/gateway-resolver.ts +63 -0
- package/src/ipns/ipns-record.ts +158 -29
- package/src/ipns/ipns-w3name.ts +207 -0
- package/src/ipns/ipns-watcher.ts +608 -0
- package/src/ipns.ts +3 -0
- package/src/pubsub/snap-push.ts +6 -5
- package/src/query/epoch-snapshot.test.ts +594 -0
- package/src/query/epoch-snapshot.ts +392 -0
- package/src/query.ts +1 -0
- package/src/viewmodel/adapters/arktype.ts +44 -0
- package/src/viewmodel/adapters/typebox.ts +59 -0
- package/src/viewmodel/adapters/typia.ts +50 -0
- package/src/viewmodel/adapters/zod.ts +55 -0
- package/src/viewmodel/builder.ts +71 -0
- package/src/viewmodel/examples/all-adapters.ts +206 -0
- package/src/viewmodel/factory.ts +330 -0
- package/src/viewmodel/index.ts +22 -0
- package/src/viewmodel/schema-adapter.ts +27 -0
- package/src/viewmodel/types.ts +152 -0
- package/dist/chunk-22WDFLXO.js +0 -138
- package/dist/chunk-22WDFLXO.js.map +0 -1
- package/dist/chunk-6ALNRM3J.js.map +0 -1
- package/dist/chunk-SHUHRHOT.js.map +0 -1
- /package/dist/{chunk-HUIQ54TT.js.map → chunk-6CSJTSQP.js.map} +0 -0
- /package/dist/{chunk-BLF5MAWU.js.map → chunk-BIYQEX3N.js.map} +0 -0
- /package/dist/{chunk-OC6Z6CQW.js.map → chunk-H4YVJKB7.js.map} +0 -0
package/src/ipns/ipns-record.ts
CHANGED
|
@@ -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
|
|
9
|
-
ipnsName: string
|
|
10
|
-
value: string
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
//
|
|
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
|
-
*
|
|
85
|
-
*
|
|
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
|
-
|
|
135
|
+
options?: PublishIPNSRecordOptions,
|
|
92
136
|
): Promise<SignedIPNSRecord> {
|
|
93
137
|
const ipnsName = ipnsNameFromPrivateKey(privateKey)
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
+
}
|