@wovin/core 0.2.2 → 0.3.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.
- package/dist/applog/datom-types.d.ts.map +1 -1
- package/dist/applog.js +1 -1
- package/dist/blockstore.js +2 -0
- package/dist/blockstore.js.map +1 -1
- package/dist/{chunk-SHUHRHOT.js → chunk-2OXLPZQI.js} +10 -3
- package/dist/chunk-2OXLPZQI.js.map +1 -0
- package/dist/{chunk-3SUFNJEZ.js → chunk-2PJFLZRC.js} +7 -2
- package/dist/{chunk-3SUFNJEZ.js.map → chunk-2PJFLZRC.js.map} +1 -1
- package/dist/chunk-64EJIJAJ.js +17 -0
- package/dist/chunk-64EJIJAJ.js.map +1 -0
- package/dist/chunk-7QEGHKR4.js +17 -0
- package/dist/chunk-7QEGHKR4.js.map +1 -0
- package/dist/{chunk-OC6Z6CQW.js → chunk-EHO2BFFY.js} +2 -2
- package/dist/chunk-ICBK7NC4.js +27 -0
- package/dist/chunk-ICBK7NC4.js.map +1 -0
- package/dist/{chunk-22WDFLXO.js → chunk-OKXRRWNS.js} +3 -3
- package/dist/{chunk-6ALNRM3J.js → chunk-Q4EMPWA3.js} +15 -8
- package/dist/chunk-Q4EMPWA3.js.map +1 -0
- package/dist/{chunk-HUIQ54TT.js → chunk-VGIACGWX.js} +3 -3
- package/dist/{chunk-BLF5MAWU.js → chunk-WVW4YXB5.js} +2 -2
- package/dist/chunk-XF4DWOAE.js +25 -0
- package/dist/chunk-XF4DWOAE.js.map +1 -0
- package/dist/index.js +7 -7
- 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 +28 -7
- package/dist/ipns/ipns-record.d.ts.map +1 -1
- package/dist/ipns/ipns-w3name.d.ts +15 -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 +488 -8
- 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.js +3 -3
- 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/datom-types.ts +2 -2
- package/src/ipfs/car.ts +8 -2
- package/src/ipns/gateway-resolver.ts +63 -0
- package/src/ipns/ipns-record.ts +68 -17
- package/src/ipns/ipns-w3name.ts +103 -0
- package/src/ipns/ipns-watcher.ts +607 -0
- package/src/ipns.ts +3 -0
- package/src/pubsub/snap-push.ts +6 -5
- 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-6ALNRM3J.js.map +0 -1
- package/dist/chunk-SHUHRHOT.js.map +0 -1
- /package/dist/{chunk-OC6Z6CQW.js.map → chunk-EHO2BFFY.js.map} +0 -0
- /package/dist/{chunk-22WDFLXO.js.map → chunk-OKXRRWNS.js.map} +0 -0
- /package/dist/{chunk-HUIQ54TT.js.map → chunk-VGIACGWX.js.map} +0 -0
- /package/dist/{chunk-BLF5MAWU.js.map → chunk-WVW4YXB5.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,77 @@ 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
|
|
|
82
98
|
/**
|
|
83
99
|
* Create a signed IPNS record and publish to all configured targets.
|
|
84
|
-
*
|
|
85
|
-
*
|
|
100
|
+
*
|
|
101
|
+
* Sequence resolution: walks `targets` in order asking each one with a
|
|
102
|
+
* `resolveSequence` method. The first target to successfully return a value
|
|
103
|
+
* (including `null` for "never published") determines the next sequence.
|
|
104
|
+
* If every target throws or none supports `resolveSequence`, falls back to 0n.
|
|
105
|
+
*
|
|
106
|
+
* Publish fan-out: only targets with a `publish` method are called.
|
|
107
|
+
* Throws if every publish-capable target fails; partial failures are logged.
|
|
86
108
|
*/
|
|
87
109
|
export async function publishIPNSRecord(
|
|
88
110
|
privateKey: Uint8Array,
|
|
89
111
|
cid: CID,
|
|
90
112
|
targets: IPNSPublishTarget[],
|
|
91
|
-
sequenceServiceUrl = 'https://name.web3.storage',
|
|
92
113
|
): Promise<SignedIPNSRecord> {
|
|
93
114
|
const ipnsName = ipnsNameFromPrivateKey(privateKey)
|
|
94
|
-
const
|
|
95
|
-
const sequence = currentSeq != null ? currentSeq + 1n : 0n
|
|
115
|
+
const sequence = await pickNextSequence(ipnsName, targets)
|
|
96
116
|
const signed = await createSignedIPNSRecord(privateKey, cid, sequence)
|
|
97
117
|
|
|
118
|
+
const publishTargets = targets.filter(t => typeof t.publish === 'function')
|
|
119
|
+
if (publishTargets.length === 0) {
|
|
120
|
+
throw new Error('No publish-capable targets supplied to publishIPNSRecord')
|
|
121
|
+
}
|
|
122
|
+
|
|
98
123
|
const results = await Promise.allSettled(
|
|
99
|
-
|
|
124
|
+
publishTargets.map(t => t.publish!(ipnsName, signed.recordBytes)),
|
|
100
125
|
)
|
|
101
126
|
const failures = results
|
|
102
|
-
.map((r, i) => ({ r, name:
|
|
127
|
+
.map((r, i) => ({ r, name: publishTargets[i].name }))
|
|
103
128
|
.filter(({ r }) => r.status === 'rejected') as { r: PromiseRejectedResult; name: string }[]
|
|
104
129
|
|
|
105
|
-
if (failures.length > 0 && failures.length <
|
|
130
|
+
if (failures.length > 0 && failures.length < publishTargets.length) {
|
|
106
131
|
// Partial failure — log but don't throw
|
|
107
132
|
for (const { r, name } of failures) {
|
|
108
133
|
console.warn(`[publishIPNSRecord] target '${name}' failed:`, r.reason)
|
|
109
134
|
}
|
|
110
|
-
} else if (failures.length ===
|
|
135
|
+
} else if (failures.length === publishTargets.length) {
|
|
111
136
|
throw new Error(`All IPNS publish targets failed: ${failures.map(({ r, name }) => `${name}: ${r.reason}`).join('; ')}`)
|
|
112
137
|
}
|
|
113
138
|
|
|
114
139
|
return signed
|
|
115
140
|
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Walk targets, asking each to resolve the current IPNS sequence. First success wins.
|
|
144
|
+
* Falls back to 0n if no target can answer.
|
|
145
|
+
*/
|
|
146
|
+
async function pickNextSequence(
|
|
147
|
+
ipnsName: string,
|
|
148
|
+
targets: IPNSPublishTarget[],
|
|
149
|
+
): Promise<bigint> {
|
|
150
|
+
const capable = targets.filter(t => typeof t.resolveSequence === 'function')
|
|
151
|
+
if (capable.length === 0) {
|
|
152
|
+
return 0n
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const target of capable) {
|
|
156
|
+
try {
|
|
157
|
+
const current = await target.resolveSequence!(ipnsName)
|
|
158
|
+
return current == null ? 0n : current + 1n
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.warn(`[publishIPNSRecord] target '${target.name}' sequence resolve failed:`, err)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.warn(`[publishIPNSRecord] no target could resolve sequence for ${ipnsName} — starting at 0n`)
|
|
165
|
+
return 0n
|
|
166
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { IPNSPublishTarget } from '@wovin/core/ipns'
|
|
2
|
+
import { Logger } from 'besonders-logger'
|
|
3
|
+
import { base64pad } from 'multiformats/bases/base64'
|
|
4
|
+
import { CID } from 'multiformats/cid'
|
|
5
|
+
import * as W3Name from 'w3name'
|
|
6
|
+
|
|
7
|
+
const { WARN, LOG, DEBUG, ERROR } = Logger.setup(Logger.INFO)
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Try to resolve IPNS name to get existing revision.
|
|
11
|
+
* Returns null if record doesn't exist (404).
|
|
12
|
+
* Throws on network errors or server errors.
|
|
13
|
+
*
|
|
14
|
+
* This does a custom HTTP check first to distinguish 404 from network errors,
|
|
15
|
+
* then delegates to W3Name.resolve() for validation if record exists.
|
|
16
|
+
*/
|
|
17
|
+
async function tryResolveIPNS(ipns: W3Name.WritableName): Promise<W3Name.Revision | null> {
|
|
18
|
+
const url = `https://name.web3.storage/name/${ipns.toString()}`
|
|
19
|
+
|
|
20
|
+
let response: Response
|
|
21
|
+
try {
|
|
22
|
+
response = await fetch(url, { signal: AbortSignal.timeout(30_000) })
|
|
23
|
+
} catch (err) {
|
|
24
|
+
// Network error (no connection, DNS failure, etc.)
|
|
25
|
+
throw ERROR('[w3name] Network error resolving IPNS:', err)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 404 = record never published
|
|
29
|
+
if (response.status === 404) {
|
|
30
|
+
DEBUG('[w3name] IPNS record not found (never published):', ipns.toString())
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Other HTTP errors (5xx server error, etc.)
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw ERROR(`[w3name] HTTP ${response.status} resolving IPNS:`, response.statusText)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Success - use W3Name.resolve to get validated Revision
|
|
40
|
+
// (We could parse the record ourselves, but W3Name does validation/signature checks)
|
|
41
|
+
const existing = await W3Name.resolve(ipns)
|
|
42
|
+
return existing
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Publish CID to IPNS, automatically handling increment vs v0.
|
|
47
|
+
* Returns the revision for further processing (e.g., Kubo integration).
|
|
48
|
+
*/
|
|
49
|
+
export async function publishIPNS(ipnsPrivateKey: Uint8Array, cid: CID): Promise<W3Name.Revision> {
|
|
50
|
+
const TIMEOUT_MS = 30_000
|
|
51
|
+
const timeout = new Promise<never>((_, reject) =>
|
|
52
|
+
setTimeout(() => reject(new Error(`publishIPNS timed out after ${TIMEOUT_MS}ms`)), TIMEOUT_MS),
|
|
53
|
+
)
|
|
54
|
+
return Promise.race([_publishIPNSImpl(ipnsPrivateKey, cid), timeout])
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function _publishIPNSImpl(ipnsPrivateKey: Uint8Array, cid: CID): Promise<W3Name.Revision> {
|
|
58
|
+
const value = `/ipfs/${cid}`
|
|
59
|
+
const ipns = await W3Name.from(ipnsPrivateKey)
|
|
60
|
+
|
|
61
|
+
let revision: W3Name.Revision
|
|
62
|
+
const existing = await tryResolveIPNS(ipns)
|
|
63
|
+
|
|
64
|
+
if (existing) {
|
|
65
|
+
// Record exists - increment sequence number
|
|
66
|
+
revision = await W3Name.increment(existing, value)
|
|
67
|
+
DEBUG('[w3name] incrementing revision for', ipns.toString())
|
|
68
|
+
} else {
|
|
69
|
+
// First publish - use v0
|
|
70
|
+
revision = await W3Name.v0(ipns, value)
|
|
71
|
+
DEBUG('[w3name] creating initial revision for', ipns.toString())
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await W3Name.publish(revision, ipns.key)
|
|
75
|
+
DEBUG('[w3name] published', cid.toString(), 'to', ipns.toString())
|
|
76
|
+
|
|
77
|
+
return revision // Return for Kubo integration or other uses
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create an IPNSPublishTarget that publishes to W3Name service via HTTP POST.
|
|
82
|
+
*/
|
|
83
|
+
export function w3nameTarget(serviceUrl = 'https://name.web3.storage'): IPNSPublishTarget {
|
|
84
|
+
return {
|
|
85
|
+
name: 'w3name',
|
|
86
|
+
async publish(ipnsName: string, recordBytes: Uint8Array) {
|
|
87
|
+
const res = await fetch(`${serviceUrl}/name/${ipnsName}`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
body: base64pad.baseEncode(recordBytes),
|
|
90
|
+
})
|
|
91
|
+
if (!res.ok) throw new Error(`W3Name HTTP ${res.status}`)
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function generateIpnsKey() {
|
|
97
|
+
return W3Name.create() // Returns W3Name.WritableName type
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function getW3NamePublic(pk: Uint8Array) {
|
|
101
|
+
const ipns = await W3Name.from(pk)
|
|
102
|
+
return ipns.toString()
|
|
103
|
+
}
|