@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.
Files changed (98) hide show
  1. package/dist/applog/datom-types.d.ts.map +1 -1
  2. package/dist/applog.js +1 -1
  3. package/dist/blockstore.js +2 -0
  4. package/dist/blockstore.js.map +1 -1
  5. package/dist/{chunk-SHUHRHOT.js → chunk-2OXLPZQI.js} +10 -3
  6. package/dist/chunk-2OXLPZQI.js.map +1 -0
  7. package/dist/{chunk-3SUFNJEZ.js → chunk-2PJFLZRC.js} +7 -2
  8. package/dist/{chunk-3SUFNJEZ.js.map → chunk-2PJFLZRC.js.map} +1 -1
  9. package/dist/chunk-64EJIJAJ.js +17 -0
  10. package/dist/chunk-64EJIJAJ.js.map +1 -0
  11. package/dist/chunk-7QEGHKR4.js +17 -0
  12. package/dist/chunk-7QEGHKR4.js.map +1 -0
  13. package/dist/{chunk-OC6Z6CQW.js → chunk-EHO2BFFY.js} +2 -2
  14. package/dist/chunk-ICBK7NC4.js +27 -0
  15. package/dist/chunk-ICBK7NC4.js.map +1 -0
  16. package/dist/{chunk-22WDFLXO.js → chunk-OKXRRWNS.js} +3 -3
  17. package/dist/{chunk-6ALNRM3J.js → chunk-Q4EMPWA3.js} +15 -8
  18. package/dist/chunk-Q4EMPWA3.js.map +1 -0
  19. package/dist/{chunk-HUIQ54TT.js → chunk-VGIACGWX.js} +3 -3
  20. package/dist/{chunk-BLF5MAWU.js → chunk-WVW4YXB5.js} +2 -2
  21. package/dist/chunk-XF4DWOAE.js +25 -0
  22. package/dist/chunk-XF4DWOAE.js.map +1 -0
  23. package/dist/index.js +7 -7
  24. package/dist/ipfs/car.d.ts.map +1 -1
  25. package/dist/ipfs.js +4 -4
  26. package/dist/ipns/gateway-resolver.d.ts +21 -0
  27. package/dist/ipns/gateway-resolver.d.ts.map +1 -0
  28. package/dist/ipns/ipns-record.d.ts +28 -7
  29. package/dist/ipns/ipns-record.d.ts.map +1 -1
  30. package/dist/ipns/ipns-w3name.d.ts +15 -0
  31. package/dist/ipns/ipns-w3name.d.ts.map +1 -0
  32. package/dist/ipns/ipns-watcher.d.ts +190 -0
  33. package/dist/ipns/ipns-watcher.d.ts.map +1 -0
  34. package/dist/ipns.d.ts +3 -0
  35. package/dist/ipns.d.ts.map +1 -1
  36. package/dist/ipns.js +488 -8
  37. package/dist/ipns.js.map +1 -1
  38. package/dist/pubsub/snap-push.d.ts +2 -2
  39. package/dist/pubsub/snap-push.d.ts.map +1 -1
  40. package/dist/pubsub.js +4 -4
  41. package/dist/query.js +3 -3
  42. package/dist/retrieve.js +4 -4
  43. package/dist/thread.js +1 -1
  44. package/dist/viewmodel/adapters/arktype.d.ts +33 -0
  45. package/dist/viewmodel/adapters/arktype.d.ts.map +1 -0
  46. package/dist/viewmodel/adapters/arktype.js +7 -0
  47. package/dist/viewmodel/adapters/arktype.js.map +1 -0
  48. package/dist/viewmodel/adapters/typebox.d.ts +35 -0
  49. package/dist/viewmodel/adapters/typebox.d.ts.map +1 -0
  50. package/dist/viewmodel/adapters/typebox.js +7 -0
  51. package/dist/viewmodel/adapters/typebox.js.map +1 -0
  52. package/dist/viewmodel/adapters/typia.d.ts +40 -0
  53. package/dist/viewmodel/adapters/typia.d.ts.map +1 -0
  54. package/dist/viewmodel/adapters/typia.js +7 -0
  55. package/dist/viewmodel/adapters/typia.js.map +1 -0
  56. package/dist/viewmodel/adapters/zod.d.ts +30 -0
  57. package/dist/viewmodel/adapters/zod.d.ts.map +1 -0
  58. package/dist/viewmodel/adapters/zod.js +7 -0
  59. package/dist/viewmodel/adapters/zod.js.map +1 -0
  60. package/dist/viewmodel/builder.d.ts +40 -0
  61. package/dist/viewmodel/builder.d.ts.map +1 -0
  62. package/dist/viewmodel/examples/all-adapters.d.ts +26 -0
  63. package/dist/viewmodel/examples/all-adapters.d.ts.map +1 -0
  64. package/dist/viewmodel/factory.d.ts +38 -0
  65. package/dist/viewmodel/factory.d.ts.map +1 -0
  66. package/dist/viewmodel/index.d.ts +10 -0
  67. package/dist/viewmodel/index.d.ts.map +1 -0
  68. package/dist/viewmodel/index.js +313 -0
  69. package/dist/viewmodel/index.js.map +1 -0
  70. package/dist/viewmodel/schema-adapter.d.ts +16 -0
  71. package/dist/viewmodel/schema-adapter.d.ts.map +1 -0
  72. package/dist/viewmodel/types.d.ts +97 -0
  73. package/dist/viewmodel/types.d.ts.map +1 -0
  74. package/package.json +29 -3
  75. package/src/applog/datom-types.ts +2 -2
  76. package/src/ipfs/car.ts +8 -2
  77. package/src/ipns/gateway-resolver.ts +63 -0
  78. package/src/ipns/ipns-record.ts +68 -17
  79. package/src/ipns/ipns-w3name.ts +103 -0
  80. package/src/ipns/ipns-watcher.ts +607 -0
  81. package/src/ipns.ts +3 -0
  82. package/src/pubsub/snap-push.ts +6 -5
  83. package/src/viewmodel/adapters/arktype.ts +44 -0
  84. package/src/viewmodel/adapters/typebox.ts +59 -0
  85. package/src/viewmodel/adapters/typia.ts +50 -0
  86. package/src/viewmodel/adapters/zod.ts +55 -0
  87. package/src/viewmodel/builder.ts +71 -0
  88. package/src/viewmodel/examples/all-adapters.ts +206 -0
  89. package/src/viewmodel/factory.ts +330 -0
  90. package/src/viewmodel/index.ts +22 -0
  91. package/src/viewmodel/schema-adapter.ts +27 -0
  92. package/src/viewmodel/types.ts +152 -0
  93. package/dist/chunk-6ALNRM3J.js.map +0 -1
  94. package/dist/chunk-SHUHRHOT.js.map +0 -1
  95. /package/dist/{chunk-OC6Z6CQW.js.map → chunk-EHO2BFFY.js.map} +0 -0
  96. /package/dist/{chunk-22WDFLXO.js.map → chunk-OKXRRWNS.js.map} +0 -0
  97. /package/dist/{chunk-HUIQ54TT.js.map → chunk-VGIACGWX.js.map} +0 -0
  98. /package/dist/{chunk-BLF5MAWU.js.map → chunk-WVW4YXB5.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,77 @@ 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
 
82
98
  /**
83
99
  * 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.
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 currentSeq = await resolveIPNSSequence(sequenceServiceUrl, ipnsName)
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
- targets.map(t => t.publish(ipnsName, signed.recordBytes))
124
+ publishTargets.map(t => t.publish!(ipnsName, signed.recordBytes)),
100
125
  )
101
126
  const failures = results
102
- .map((r, i) => ({ r, name: targets[i].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 < targets.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 === targets.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
+ }