@wovin/connect-web3storage 0.1.35 → 0.2.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/chunk-6224SQ5A.js +2 -0
- package/dist/chunk-6224SQ5A.js.map +1 -0
- package/dist/chunk-LDGLUYRV.js +2 -0
- package/dist/chunk-LDGLUYRV.js.map +1 -0
- package/dist/chunk-SW4VIQUZ.js +2 -0
- package/dist/{chunk-XCHCNDQW.min.js.map → chunk-SW4VIQUZ.js.map} +1 -1
- package/dist/{chunk-BFHWRINC.min.js → chunk-U6PL7AYU.js} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/ipns.js +2 -0
- package/dist/retrieve.js +2 -0
- package/dist/store.js +1 -0
- package/dist/utils.js +2 -0
- package/dist/watch.js +2 -0
- package/package.json +13 -12
- package/src/index.ts +5 -0
- package/src/ipns.ts +103 -0
- package/src/retrieve.ts +97 -0
- package/src/store.ts +20 -0
- package/src/utils.ts +5 -0
- package/src/watch.ts +572 -0
- package/dist/chunk-6K4NRK7Q.min.js +0 -2
- package/dist/chunk-6K4NRK7Q.min.js.map +0 -1
- package/dist/chunk-D2SZILOK.min.js +0 -2
- package/dist/chunk-L7VH6KZJ.min.js +0 -3
- package/dist/chunk-L7VH6KZJ.min.js.map +0 -1
- package/dist/chunk-UKHFBNOE.min.js +0 -29
- package/dist/chunk-UKHFBNOE.min.js.map +0 -1
- package/dist/chunk-W6NJ5ZSK.min.js +0 -33
- package/dist/chunk-W6NJ5ZSK.min.js.map +0 -1
- package/dist/chunk-XCHCNDQW.min.js +0 -2
- package/dist/index.min.js +0 -2
- package/dist/ipns.min.js +0 -2
- package/dist/retrieve.min.js +0 -2
- package/dist/store.min.js +0 -1
- package/dist/utils.min.js +0 -2
- package/dist/watch.min.js +0 -2
- package/dist/watch.min.js.map +0 -1
- /package/dist/{chunk-BFHWRINC.min.js.map → chunk-U6PL7AYU.js.map} +0 -0
- /package/dist/{chunk-D2SZILOK.min.js.map → index.js.map} +0 -0
- /package/dist/{index.min.js.map → ipns.js.map} +0 -0
- /package/dist/{ipns.min.js.map → retrieve.js.map} +0 -0
- /package/dist/{retrieve.min.js.map → store.js.map} +0 -0
- /package/dist/{store.min.js.map → utils.js.map} +0 -0
- /package/dist/{utils.min.js.map → watch.js.map} +0 -0
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{Logger as v}from"besonders-logger";import{CID as m}from"multiformats/cid";import I from"partysocket/ws";var{WARN:c,LOG:l,DEBUG:o,ERROR:S}=v.setup(v.INFO),w="wss://name.web3.storage/name",f="https://name.web3.storage/name";function d(a){try{let e=a.startsWith("/ipfs/")?a.slice(6):a;return m.parse(e)}catch{return o("[parseCidFromIpnsValue] failed to parse:",a),null}}function C(a,e){let n=`${w}/${a}/watch`;o("[watchNameRaw] connecting to",n);let r=new WebSocket(n);return r.onopen=()=>{l("[watchNameRaw] connected to",a),e.onOpen?.()},r.onmessage=s=>{try{let t=JSON.parse(s.data);o("[watchNameRaw] received update for",a,t),e.onUpdate(t)}catch(t){c("[watchNameRaw] failed to parse message:",s.data,t),e.onError?.(t instanceof Error?t:new Error(String(t)))}},r.onerror=s=>{c("[watchNameRaw] error for",a,s),e.onError?.(s)},r.onclose=s=>{o("[watchNameRaw] closed for",a,"code:",s.code),e.onClose?.(s)},{close:()=>{o("[watchNameRaw] closing connection for",a),r.close()},ws:r}}var W=36e5,u=class{name;ws;lastKnownValue=null;options;isFirstConnect=!0;livenessTimer=null;connectedAt=null;lastMessageAt=null;constructor(e,n){this.name=e,this.options=n;let r=`${w}/${e}/watch`;o("[IpnsWatcher] creating for",e),this.ws=new I(r,[],{maxReconnectionDelay:9e5,minReconnectionDelay:5e3,reconnectionDelayGrowFactor:2,maxRetries:1/0,...n.wsOptions}),this.ws.onopen=()=>{l("[IpnsWatcher] connected to",e),this.connectedAt=new Date,n.onConnected?.(),this.isFirstConnect&&(n.fetchInitialState??!1)?this.checkForMissedUpdates():!this.isFirstConnect&&(n.catchUpOnReconnect??!0)&&this.checkForMissedUpdates(),this.isFirstConnect=!1,n.livenessCheck!==!1&&this.startLivenessCheck()},this.ws.onmessage=s=>{this.lastMessageAt=new Date;try{let t=JSON.parse(s.data);o("[IpnsWatcher] received update for",e,t);let i=this.lastKnownValue,h=t.value!==i,p=d(t.value);if(!h&&!n.includeUnchanged){o("[IpnsWatcher] skipping unchanged value for",e);return}this.lastKnownValue=t.value;let g={value:t.value,cid:p,lastValue:i,isNew:h,record:t};n.onUpdate(g)}catch(t){c("[IpnsWatcher] failed to parse message:",s.data,t),n.onError?.(t instanceof Error?t:new Error(String(t)))}},this.ws.onerror=s=>{let t=s instanceof ErrorEvent?s.message:"WebSocket error";t==="Unexpected EOF"?l("[IpnsWatcher] error for",e,":",t,"(auto-reconnect enabled)"):c("[IpnsWatcher] error for",e,":",t),t!=="Unexpected EOF"&&n.onError?.(s)},this.ws.onclose=()=>{o("[IpnsWatcher] disconnected from",e),this.stopLivenessCheck(),this.connectedAt=null,this.lastMessageAt=null,n.onDisconnected?.()}}async checkForMissedUpdates(){try{o("[IpnsWatcher] checking for missed updates for",this.name);let e=await fetch(`${f}/${this.name}`);if(!e.ok){if(e.status===404){o("[IpnsWatcher] IPNS not yet published:",this.name);return}throw new Error(`HTTP ${e.status}: ${e.statusText}`)}let n=await e.json(),r=this.lastKnownValue,s=n.value!==r,t=d(n.value);if(!s&&!this.options.includeUnchanged){o("[IpnsWatcher] no new updates for",this.name);return}l(r===null?"[IpnsWatcher] fetched initial state for":"[IpnsWatcher] caught missed update for",this.name,{previous:r,current:n.value}),this.lastKnownValue=n.value;let h={value:n.value,cid:t,lastValue:r,isNew:s,record:n};this.options.onUpdate(h)}catch(e){c("[IpnsWatcher] failed to check for missed updates:",this.name,e),this.options.onError?.(e instanceof Error?e:new Error(String(e)))}}startLivenessCheck(){this.stopLivenessCheck();let e=this.options.livenessCheckInterval??W;o("[IpnsWatcher] starting liveness check for",this.name,"interval:",e),this.livenessTimer=setInterval(()=>{this.performLivenessCheck()},e)}stopLivenessCheck(){this.livenessTimer!==null&&(o("[IpnsWatcher] stopping liveness check for",this.name),clearInterval(this.livenessTimer),this.livenessTimer=null)}async performLivenessCheck(){try{o("[IpnsWatcher] performing liveness check for",this.name);let e=await fetch(`${f}/${this.name}`);if(!e.ok){if(e.status===404){if(this.lastKnownValue===null){o("[IpnsWatcher] liveness check OK (both null) for",this.name);return}c("[IpnsWatcher] liveness check inconsistent (we have value, HTTP 404) for",this.name);return}throw new Error(`HTTP ${e.status}: ${e.statusText}`)}let n=await e.json();if(n.value===this.lastKnownValue){o("[IpnsWatcher] liveness check OK for",this.name);return}let r=new Date,s=this.lastMessageAt?r.getTime()-this.lastMessageAt.getTime():this.connectedAt?r.getTime()-this.connectedAt.getTime():0,t={connectedAt:this.connectedAt??r,lastMessageAt:this.lastMessageAt,silenceDuration:s,staleValue:this.lastKnownValue,currentValue:n.value};c("[IpnsWatcher] stale connection detected for",this.name,{connectedAt:t.connectedAt.toISOString(),lastMessageAt:t.lastMessageAt?.toISOString()??"never",silenceDuration:`${Math.round(s/1e3)}s`,staleValue:t.staleValue,currentValue:t.currentValue}),this.options.onStaleConnection?.(t);let i=this.lastKnownValue,h=d(n.value);this.lastKnownValue=n.value;let p={value:n.value,cid:h,lastValue:i,isNew:!0,record:n};this.options.onUpdate(p),l("[IpnsWatcher] forcing reconnect due to stale connection for",this.name),this.ws.reconnect()}catch(e){c("[IpnsWatcher] liveness check failed for",this.name,e)}}start(){l("[IpnsWatcher] starting watcher for",this.name),this.ws.reconnect()}stop(){this.close()}close(){l("[IpnsWatcher] closing watcher for",this.name),this.stopLivenessCheck(),this.ws.close()}get lastValue(){return this.lastKnownValue}get readyState(){return this.ws.readyState}};function N(a,e){return new u(a,e)}async function*U(a,e){let n=[],r=null,s=null,t=new u(a,{onUpdate:i=>{n.push(i),r?.()},onError:i=>{s=i instanceof Error?i:new Error("WebSocket error"),r?.()}});e?.addEventListener("abort",()=>{t.close()});try{for(;!e?.aborted;)n.length>0?yield n.shift():s?(c("[watchNameIterator] error occurred, continuing:",s),s=null):(await new Promise(i=>{r=i}),r=null)}finally{t.close()}}export{C as a,u as b,N as c,U as d};
|
|
2
|
+
//# sourceMappingURL=chunk-6224SQ5A.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/watch.ts"],"sourcesContent":["import { Logger } from 'besonders-logger'\nimport { CID } from 'multiformats/cid'\nimport ReconnectingWebSocket, { type Options as PartysocketOptions } from 'partysocket/ws'\n\nexport type { PartysocketOptions }\n\nconst { WARN, LOG, DEBUG, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line unused-imports/no-unused-vars\n\nconst NAME_WS_URL = 'wss://name.web3.storage/name'\nconst NAME_HTTP_URL = 'https://name.web3.storage/name'\n\nexport interface W3NameRecord {\n\tvalue: string // e.g. \"/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi\"\n\tseq?: number\n\tvalidity?: string\n}\n\n/**\n * Debug info provided when a stale WebSocket connection is detected.\n */\nexport interface StaleConnectionInfo {\n\t/** When the WebSocket connection was established */\n\tconnectedAt: Date\n\t/** When we last received a WebSocket message */\n\tlastMessageAt: Date | null\n\t/** How long since last message (ms) */\n\tsilenceDuration: number\n\t/** The stale value from WebSocket */\n\tstaleValue: string | null\n\t/** The current value from HTTP */\n\tcurrentValue: string\n}\n\n/**\n * Enriched IPNS update with parsed CID and change detection.\n * Backwards compatible - `.value` still works as before.\n */\nexport interface IpnsUpdate {\n\t/** Raw IPNS value string (e.g. '/ipfs/bafy...') — same as W3NameRecord.value */\n\tvalue: string\n\t/** Parsed CID if value is valid IPFS path, null otherwise */\n\tcid: CID | null\n\t/** Previous value (null on first update) */\n\tlastValue: string | null\n\t/** Whether this is a change from lastValue */\n\tisNew: boolean\n\t/** Original W3NameRecord for access to seq/validity */\n\trecord: W3NameRecord\n}\n\n/**\n * Parse CID from IPNS value string (e.g. \"/ipfs/bafybeig...\")\n * @returns CID if valid, null otherwise\n */\nfunction parseCidFromIpnsValue(value: string): CID | null {\n\ttry {\n\t\t// Strip /ipfs/ prefix if present\n\t\tconst cidStr = value.startsWith('/ipfs/') ? value.slice(6) : value\n\t\treturn CID.parse(cidStr)\n\t} catch {\n\t\tDEBUG('[parseCidFromIpnsValue] failed to parse:', value)\n\t\treturn null\n\t}\n}\n\nexport interface WatchRawOptions {\n\t/** Called when the IPNS record is updated */\n\tonUpdate: (record: W3NameRecord) => void\n\t/** Called when an error occurs */\n\tonError?: (error: Event | Error) => void\n\t/** Called when the connection is opened */\n\tonOpen?: () => void\n\t/** Called when the connection is closed */\n\tonClose?: (event: CloseEvent) => void\n}\n\nexport interface WatchRawSubscription {\n\t/** Close the WebSocket connection */\n\tclose: () => void\n\t/** The underlying WebSocket instance */\n\tws: WebSocket\n}\n\n/**\n * Low-level WebSocket watcher for IPNS (no reconnect logic).\n * Use this when you want full control over connection lifecycle.\n * For most cases, prefer `watchName` or `IpnsWatcher` which handle reconnection.\n *\n * @param name - The IPNS name/key to watch\n * @param options - Callback options\n * @returns Subscription with close() and ws\n *\n * @example\n * ```ts\n * const sub = watchNameRaw('k51qzi5u...', {\n * onUpdate: (record) => console.log('Update:', record.value),\n * onClose: () => console.log('Disconnected - handle reconnect yourself'),\n * })\n * ```\n */\nexport function watchNameRaw(name: string, options: WatchRawOptions): WatchRawSubscription {\n\tconst url = `${NAME_WS_URL}/${name}/watch`\n\tDEBUG('[watchNameRaw] connecting to', url)\n\n\tconst ws = new WebSocket(url)\n\n\tws.onopen = () => {\n\t\tLOG('[watchNameRaw] connected to', name)\n\t\toptions.onOpen?.()\n\t}\n\n\tws.onmessage = (event) => {\n\t\ttry {\n\t\t\tconst record: W3NameRecord = JSON.parse(event.data)\n\t\t\tDEBUG('[watchNameRaw] received update for', name, record)\n\t\t\toptions.onUpdate(record)\n\t\t} catch (err) {\n\t\t\tWARN('[watchNameRaw] failed to parse message:', event.data, err)\n\t\t\toptions.onError?.(err instanceof Error ? err : new Error(String(err)))\n\t\t}\n\t}\n\n\tws.onerror = (event) => {\n\t\tWARN('[watchNameRaw] error for', name, event)\n\t\toptions.onError?.(event)\n\t}\n\n\tws.onclose = (event) => {\n\t\tDEBUG('[watchNameRaw] closed for', name, 'code:', event.code)\n\t\toptions.onClose?.(event)\n\t}\n\n\treturn {\n\t\tclose: () => {\n\t\t\tDEBUG('[watchNameRaw] closing connection for', name)\n\t\t\tws.close()\n\t\t},\n\t\tws,\n\t}\n}\n\nexport interface IpnsWatcherOptions {\n\t/** Called when the IPNS record is updated (enriched payload with CID and change detection) */\n\tonUpdate: (update: IpnsUpdate) => void | Promise<void>\n\t/** Called when an error occurs */\n\tonError?: (error: Error | Event) => void\n\t/** Called when the connection is opened/reconnected */\n\tonConnected?: () => void\n\t/** Called when the connection is closed */\n\tonDisconnected?: () => void\n\t/** Fetch current IPNS state on first connect (default: false) */\n\tfetchInitialState?: boolean\n\t/** Fetch current IPNS state on reconnect to catch missed updates (default: true) */\n\tcatchUpOnReconnect?: boolean\n\t/** If true, call onUpdate even when value hasn't changed (default: false) */\n\tincludeUnchanged?: boolean\n\t/**\n\t * Enable periodic liveness checks via HTTP to detect zombie connections (default: true).\n\t * When enabled, periodically fetches current IPNS value and forces reconnect if it\n\t * differs from the last WebSocket update.\n\t */\n\tlivenessCheck?: boolean\n\t/**\n\t * Liveness check interval in milliseconds (default: 3600000 = 1 hour).\n\t * Only used when livenessCheck is enabled.\n\t */\n\tlivenessCheckInterval?: number\n\t/**\n\t * Called when a stale connection is detected (WebSocket missed updates).\n\t * Provides debug info about the connection state.\n\t */\n\tonStaleConnection?: (info: StaleConnectionInfo) => void\n\t/**\n\t * Partysocket options (passed through to ReconnectingWebSocket).\n\t * Useful options: startClosed, maxReconnectionDelay, minReconnectionDelay, etc.\n\t * @see https://github.com/partykit/partykit/tree/main/packages/partysocket\n\t */\n\twsOptions?: PartysocketOptions\n}\n\n/**\n * Robust IPNS watcher with auto-reconnect and catch-up logic.\n * Uses partysocket for reliable WebSocket reconnection.\n *\n * @example\n * ```ts\n * const watcher = new IpnsWatcher('k51qzi5uqu...', {\n * onUpdate: (update) => console.log('New CID:', update.cid?.toString()),\n * onError: (err) => console.error('Error:', err),\n * })\n *\n * // Later, to stop watching:\n * watcher.close()\n * ```\n */\nconst DEFAULT_LIVENESS_INTERVAL = 3600000 // 1 hour\n\nexport class IpnsWatcher {\n\tprivate name: string\n\tprivate ws: ReconnectingWebSocket\n\tprivate lastKnownValue: string | null = null\n\tprivate options: IpnsWatcherOptions\n\tprivate isFirstConnect = true\n\tprivate livenessTimer: ReturnType<typeof setInterval> | null = null\n\tprivate connectedAt: Date | null = null\n\tprivate lastMessageAt: Date | null = null\n\n\tconstructor(name: string, options: IpnsWatcherOptions) {\n\t\tthis.name = name\n\t\tthis.options = options\n\n\t\tconst url = `${NAME_WS_URL}/${name}/watch`\n\t\tDEBUG('[IpnsWatcher] creating for', name)\n\n\t\tthis.ws = new ReconnectingWebSocket(url, [], {\n\t\t\tmaxReconnectionDelay: 900000, // 15min\n\t\t\tminReconnectionDelay: 5000,\n\t\t\treconnectionDelayGrowFactor: 2,\n\t\t\tmaxRetries: Infinity,\n\t\t\t...options.wsOptions,\n\t\t})\n\n\t\tthis.ws.onopen = () => {\n\t\t\tLOG('[IpnsWatcher] connected to', name)\n\t\t\tthis.connectedAt = new Date()\n\t\t\toptions.onConnected?.()\n\n\t\t\t// Check for current state on first connect if requested\n\t\t\tif (this.isFirstConnect && (options.fetchInitialState ?? false)) {\n\t\t\t\tthis.checkForMissedUpdates()\n\t\t\t}\n\t\t\t// Check for missed updates on reconnect\n\t\t\telse if (!this.isFirstConnect && (options.catchUpOnReconnect ?? true)) {\n\t\t\t\tthis.checkForMissedUpdates()\n\t\t\t}\n\n\t\t\tthis.isFirstConnect = false\n\n\t\t\t// Start liveness checking (default: enabled)\n\t\t\tif (options.livenessCheck !== false) {\n\t\t\t\tthis.startLivenessCheck()\n\t\t\t}\n\t\t}\n\n\t\tthis.ws.onmessage = (event) => {\n\t\t\tthis.lastMessageAt = new Date()\n\t\t\ttry {\n\t\t\t\tconst record: W3NameRecord = JSON.parse(event.data as string)\n\t\t\t\tDEBUG('[IpnsWatcher] received update for', name, record)\n\n\t\t\t\tconst lastValue = this.lastKnownValue\n\t\t\t\tconst isNew = record.value !== lastValue\n\t\t\t\tconst cid = parseCidFromIpnsValue(record.value)\n\n\t\t\t\t// Skip unchanged values unless includeUnchanged is set\n\t\t\t\tif (!isNew && !options.includeUnchanged) {\n\t\t\t\t\tDEBUG('[IpnsWatcher] skipping unchanged value for', name)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// Update lastKnownValue after the skip check\n\t\t\t\tthis.lastKnownValue = record.value\n\n\t\t\t\tconst update: IpnsUpdate = {\n\t\t\t\t\tvalue: record.value,\n\t\t\t\t\tcid,\n\t\t\t\t\tlastValue,\n\t\t\t\t\tisNew,\n\t\t\t\t\trecord,\n\t\t\t\t}\n\n\t\t\t\tvoid options.onUpdate(update)\n\t\t\t} catch (err) {\n\t\t\t\tWARN('[IpnsWatcher] failed to parse message:', event.data, err)\n\t\t\t\toptions.onError?.(err instanceof Error ? err : new Error(String(err)))\n\t\t\t}\n\t\t}\n\n\t\tthis.ws.onerror = (event) => {\n\t\t\t// Extract meaningful error info instead of logging entire ErrorEvent\n\t\t\tconst errorMsg = event instanceof ErrorEvent ? event.message : 'WebSocket error'\n\n\t\t\t// \"Unexpected EOF\" is a normal disconnection - partysocket will auto-reconnect\n\t\t\t// Log at INFO level as it's expected behavior, not an error\n\t\t\tif (errorMsg === 'Unexpected EOF') {\n\t\t\t\tLOG('[IpnsWatcher] error for', name, ':', errorMsg, '(auto-reconnect enabled)')\n\t\t\t} else {\n\t\t\t\tWARN('[IpnsWatcher] error for', name, ':', errorMsg)\n\t\t\t}\n\n\t\t\t// Still call the error handler for unexpected errors\n\t\t\tif (errorMsg !== 'Unexpected EOF') {\n\t\t\t\toptions.onError?.(event)\n\t\t\t}\n\t\t}\n\n\t\tthis.ws.onclose = () => {\n\t\t\tDEBUG('[IpnsWatcher] disconnected from', name)\n\t\t\tthis.stopLivenessCheck()\n\t\t\tthis.connectedAt = null\n\t\t\tthis.lastMessageAt = null\n\t\t\toptions.onDisconnected?.()\n\t\t}\n\t}\n\n\t/**\n\t * Resolve current IPNS value via HTTP API to catch missed updates\n\t */\n\tprivate async checkForMissedUpdates(): Promise<void> {\n\t\ttry {\n\t\t\tDEBUG('[IpnsWatcher] checking for missed updates for', this.name)\n\t\t\tconst response = await fetch(`${NAME_HTTP_URL}/${this.name}`)\n\n\t\t\tif (!response.ok) {\n\t\t\t\tif (response.status === 404) {\n\t\t\t\t\tDEBUG('[IpnsWatcher] IPNS not yet published:', this.name)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthrow new Error(`HTTP ${response.status}: ${response.statusText}`)\n\t\t\t}\n\n\t\t\tconst record: W3NameRecord = await response.json()\n\t\t\tconst lastValue = this.lastKnownValue\n\t\t\tconst isNew = record.value !== lastValue\n\t\t\tconst cid = parseCidFromIpnsValue(record.value)\n\n\t\t\t// Skip unchanged values unless includeUnchanged is set\n\t\t\tif (!isNew && !this.options.includeUnchanged) {\n\t\t\t\tDEBUG('[IpnsWatcher] no new updates for', this.name)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst logMsg = lastValue === null\n\t\t\t\t? '[IpnsWatcher] fetched initial state for'\n\t\t\t\t: '[IpnsWatcher] caught missed update for'\n\t\t\tLOG(logMsg, this.name, {\n\t\t\t\tprevious: lastValue,\n\t\t\t\tcurrent: record.value,\n\t\t\t})\n\n\t\t\t// Update lastKnownValue after the skip check\n\t\t\tthis.lastKnownValue = record.value\n\n\t\t\tconst update: IpnsUpdate = {\n\t\t\t\tvalue: record.value,\n\t\t\t\tcid,\n\t\t\t\tlastValue,\n\t\t\t\tisNew,\n\t\t\t\trecord,\n\t\t\t}\n\n\t\t\tvoid this.options.onUpdate(update)\n\t\t} catch (err) {\n\t\t\tWARN('[IpnsWatcher] failed to check for missed updates:', this.name, err)\n\t\t\tthis.options.onError?.(err instanceof Error ? err : new Error(String(err)))\n\t\t}\n\t}\n\n\t/**\n\t * Start periodic liveness checks to detect zombie connections.\n\t */\n\tprivate startLivenessCheck(): void {\n\t\tthis.stopLivenessCheck() // Clear any existing timer\n\t\tconst interval = this.options.livenessCheckInterval ?? DEFAULT_LIVENESS_INTERVAL\n\t\tDEBUG('[IpnsWatcher] starting liveness check for', this.name, 'interval:', interval)\n\n\t\tthis.livenessTimer = setInterval(() => {\n\t\t\tvoid this.performLivenessCheck()\n\t\t}, interval)\n\t}\n\n\t/**\n\t * Stop periodic liveness checks.\n\t */\n\tprivate stopLivenessCheck(): void {\n\t\tif (this.livenessTimer !== null) {\n\t\t\tDEBUG('[IpnsWatcher] stopping liveness check for', this.name)\n\t\t\tclearInterval(this.livenessTimer)\n\t\t\tthis.livenessTimer = null\n\t\t}\n\t}\n\n\t/**\n\t * Perform a single liveness check via HTTP.\n\t * If the HTTP value differs from lastKnownValue, the connection is stale.\n\t */\n\tprivate async performLivenessCheck(): Promise<void> {\n\t\ttry {\n\t\t\tDEBUG('[IpnsWatcher] performing liveness check for', this.name)\n\t\t\tconst response = await fetch(`${NAME_HTTP_URL}/${this.name}`)\n\n\t\t\tif (!response.ok) {\n\t\t\t\tif (response.status === 404) {\n\t\t\t\t\t// IPNS not published - if we also have null, that's consistent\n\t\t\t\t\tif (this.lastKnownValue === null) {\n\t\t\t\t\t\tDEBUG('[IpnsWatcher] liveness check OK (both null) for', this.name)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\t// We have a value but HTTP says 404 - this shouldn't happen normally\n\t\t\t\t\tWARN('[IpnsWatcher] liveness check inconsistent (we have value, HTTP 404) for', this.name)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthrow new Error(`HTTP ${response.status}: ${response.statusText}`)\n\t\t\t}\n\n\t\t\tconst record: W3NameRecord = await response.json()\n\n\t\t\t// Check if values match\n\t\t\tif (record.value === this.lastKnownValue) {\n\t\t\t\tDEBUG('[IpnsWatcher] liveness check OK for', this.name)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Stale connection detected!\n\t\t\tconst now = new Date()\n\t\t\tconst silenceDuration = this.lastMessageAt\n\t\t\t\t? now.getTime() - this.lastMessageAt.getTime()\n\t\t\t\t: this.connectedAt\n\t\t\t\t\t? now.getTime() - this.connectedAt.getTime()\n\t\t\t\t\t: 0\n\n\t\t\tconst staleInfo: StaleConnectionInfo = {\n\t\t\t\tconnectedAt: this.connectedAt ?? now,\n\t\t\t\tlastMessageAt: this.lastMessageAt,\n\t\t\t\tsilenceDuration,\n\t\t\t\tstaleValue: this.lastKnownValue,\n\t\t\t\tcurrentValue: record.value,\n\t\t\t}\n\n\t\t\tWARN('[IpnsWatcher] stale connection detected for', this.name, {\n\t\t\t\tconnectedAt: staleInfo.connectedAt.toISOString(),\n\t\t\t\tlastMessageAt: staleInfo.lastMessageAt?.toISOString() ?? 'never',\n\t\t\t\tsilenceDuration: `${Math.round(silenceDuration / 1000)}s`,\n\t\t\t\tstaleValue: staleInfo.staleValue,\n\t\t\t\tcurrentValue: staleInfo.currentValue,\n\t\t\t})\n\n\t\t\t// Notify via callback\n\t\t\tthis.options.onStaleConnection?.(staleInfo)\n\n\t\t\t// Fire immediate update with the current value\n\t\t\tconst lastValue = this.lastKnownValue\n\t\t\tconst cid = parseCidFromIpnsValue(record.value)\n\t\t\tthis.lastKnownValue = record.value\n\n\t\t\tconst update: IpnsUpdate = {\n\t\t\t\tvalue: record.value,\n\t\t\t\tcid,\n\t\t\t\tlastValue,\n\t\t\t\tisNew: true,\n\t\t\t\trecord,\n\t\t\t}\n\t\t\tvoid this.options.onUpdate(update)\n\n\t\t\t// Force reconnect to get a fresh connection\n\t\t\tLOG('[IpnsWatcher] forcing reconnect due to stale connection for', this.name)\n\t\t\tthis.ws.reconnect()\n\t\t} catch (err) {\n\t\t\t// Don't treat HTTP errors as stale - could be network issue\n\t\t\tWARN('[IpnsWatcher] liveness check failed for', this.name, err)\n\t\t}\n\t}\n\n\t/**\n\t * Manually start/reconnect the WebSocket.\n\t * Only needed if you used `wsOptions: { startClosed: true }`.\n\t */\n\tstart(): void {\n\t\tLOG('[IpnsWatcher] starting watcher for', this.name)\n\t\tthis.ws.reconnect()\n\t}\n\n\t/**\n\t * Alias for close() - for backward compatibility\n\t */\n\tstop(): void {\n\t\tthis.close()\n\t}\n\n\t/**\n\t * Close the WebSocket connection and stop watching\n\t */\n\tclose(): void {\n\t\tLOG('[IpnsWatcher] closing watcher for', this.name)\n\t\tthis.stopLivenessCheck()\n\t\tthis.ws.close()\n\t}\n\n\t/**\n\t * Get the last known IPNS value\n\t */\n\tget lastValue(): string | null {\n\t\treturn this.lastKnownValue\n\t}\n\n\t/**\n\t * Get the WebSocket ready state\n\t */\n\tget readyState(): number {\n\t\treturn this.ws.readyState\n\t}\n}\n\n/**\n * Create an IPNS watcher with auto-reconnect and catch-up logic.\n * Convenience function that creates and returns an IpnsWatcher instance.\n *\n * @param name - The IPNS name/key to watch (e.g. \"k51qzi5u...\")\n * @param options - Callback options for handling events\n * @returns An IpnsWatcher instance with close() method\n */\nexport function watchName(name: string, options: IpnsWatcherOptions): IpnsWatcher {\n\treturn new IpnsWatcher(name, options)\n}\n\n/**\n * Watch an IPNS name and return updates as an async iterator.\n * Includes auto-reconnect - iterator continues through disconnections.\n *\n * @param name - The IPNS name/key to watch\n * @param signal - Optional AbortSignal to stop the watch\n *\n * @example\n * ```ts\n * const controller = new AbortController()\n * for await (const update of watchNameIterator('k51qzi5u...', controller.signal)) {\n * console.log('Update:', update.cid?.toString())\n * }\n * ```\n */\nexport async function* watchNameIterator(\n\tname: string,\n\tsignal?: AbortSignal,\n): AsyncGenerator<IpnsUpdate, void, unknown> {\n\tconst queue: IpnsUpdate[] = []\n\tlet resolve: (() => void) | null = null\n\tlet error: Error | null = null\n\n\tconst watcher = new IpnsWatcher(name, {\n\t\tonUpdate: (update) => {\n\t\t\tqueue.push(update)\n\t\t\tresolve?.()\n\t\t},\n\t\tonError: (err) => {\n\t\t\terror = err instanceof Error ? err : new Error('WebSocket error')\n\t\t\tresolve?.()\n\t\t},\n\t})\n\n\tsignal?.addEventListener('abort', () => {\n\t\twatcher.close()\n\t})\n\n\ttry {\n\t\twhile (!signal?.aborted) {\n\t\t\tif (queue.length > 0) {\n\t\t\t\tyield queue.shift()!\n\t\t\t} else if (error) {\n\t\t\t\t// Log error but continue - partysocket will reconnect\n\t\t\t\tWARN('[watchNameIterator] error occurred, continuing:', error)\n\t\t\t\terror = null\n\t\t\t} else {\n\t\t\t\tawait new Promise<void>((r) => {\n\t\t\t\t\tresolve = r\n\t\t\t\t})\n\t\t\t\tresolve = null\n\t\t\t}\n\t\t}\n\t} finally {\n\t\twatcher.close()\n\t}\n}\n"],"mappings":"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","names":["Logger","CID","ReconnectingWebSocket","WARN","LOG","DEBUG","ERROR","NAME_WS_URL","NAME_HTTP_URL","parseCidFromIpnsValue","value","cidStr","watchNameRaw","name","options","url","ws","event","record","err","DEFAULT_LIVENESS_INTERVAL","IpnsWatcher","lastValue","isNew","cid","update","errorMsg","response","interval","now","silenceDuration","staleInfo","watchName","watchNameIterator","signal","queue","resolve","error","watcher","r"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{Logger as m}from"besonders-logger";import{base64pad as c}from"multiformats/bases/base64";import*as e from"w3name";var{WARN:g,LOG:f,DEBUG:s,ERROR:u}=m.setup(m.INFO);async function l(r){let i=`https://name.web3.storage/name/${r.toString()}`,n;try{n=await fetch(i,{signal:AbortSignal.timeout(3e4)})}catch(o){throw u("[w3name] Network error resolving IPNS:",o)}if(n.status===404)return s("[w3name] IPNS record not found (never published):",r.toString()),null;if(!n.ok)throw u(`[w3name] HTTP ${n.status} resolving IPNS:`,n.statusText);return await e.resolve(r)}async function S(r,i){let t=new Promise((o,a)=>setTimeout(()=>a(new Error("publishIPNS timed out after 30000ms")),3e4));return Promise.race([p(r,i),t])}async function p(r,i){let n=`/ipfs/${i}`,t=await e.from(r),o,a=await l(t);return a?(o=await e.increment(a,n),s("[w3name] incrementing revision for",t.toString())):(o=await e.v0(t,n),s("[w3name] creating initial revision for",t.toString())),await e.publish(o,t.key),s("[w3name] published",i.toString(),"to",t.toString()),o}function P(r="https://name.web3.storage"){return{name:"w3name",async publish(i,n){let t=await fetch(`${r}/name/${i}`,{method:"POST",body:c.baseEncode(n)});if(!t.ok)throw new Error(`W3Name HTTP ${t.status}`)}}}async function h(){return e.create()}async function I(r){return(await e.from(r)).toString()}export{S as a,P as b,h as c,I as d};
|
|
2
|
+
//# sourceMappingURL=chunk-LDGLUYRV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ipns.ts"],"sourcesContent":["import type { IPNSPublishTarget } from '@wovin/core/ipns'\nimport { Logger } from 'besonders-logger'\nimport { base64pad } from 'multiformats/bases/base64'\nimport { CID } from 'multiformats/cid'\nimport * as W3Name from 'w3name'\n\nconst { WARN, LOG, DEBUG, ERROR } = Logger.setup(Logger.INFO)\n\n/**\n * Try to resolve IPNS name to get existing revision.\n * Returns null if record doesn't exist (404).\n * Throws on network errors or server errors.\n *\n * This does a custom HTTP check first to distinguish 404 from network errors,\n * then delegates to W3Name.resolve() for validation if record exists.\n */\nasync function tryResolveIPNS(ipns: W3Name.WritableName): Promise<W3Name.Revision | null> {\n\tconst url = `https://name.web3.storage/name/${ipns.toString()}`\n\n\tlet response: Response\n\ttry {\n\t\tresponse = await fetch(url, { signal: AbortSignal.timeout(30_000) })\n\t} catch (err) {\n\t\t// Network error (no connection, DNS failure, etc.)\n\t\tthrow ERROR('[w3name] Network error resolving IPNS:', err)\n\t}\n\n\t// 404 = record never published\n\tif (response.status === 404) {\n\t\tDEBUG('[w3name] IPNS record not found (never published):', ipns.toString())\n\t\treturn null\n\t}\n\n\t// Other HTTP errors (5xx server error, etc.)\n\tif (!response.ok) {\n\t\tthrow ERROR(`[w3name] HTTP ${response.status} resolving IPNS:`, response.statusText)\n\t}\n\n\t// Success - use W3Name.resolve to get validated Revision\n\t// (We could parse the record ourselves, but W3Name does validation/signature checks)\n\tconst existing = await W3Name.resolve(ipns)\n\treturn existing\n}\n\n/**\n * Publish CID to IPNS, automatically handling increment vs v0.\n * Returns the revision for further processing (e.g., Kubo integration).\n */\nexport async function publishIPNS(ipnsPrivateKey: Uint8Array, cid: CID): Promise<W3Name.Revision> {\n\tconst TIMEOUT_MS = 30_000\n\tconst timeout = new Promise<never>((_, reject) =>\n\t\tsetTimeout(() => reject(new Error(`publishIPNS timed out after ${TIMEOUT_MS}ms`)), TIMEOUT_MS),\n\t)\n\treturn Promise.race([_publishIPNSImpl(ipnsPrivateKey, cid), timeout])\n}\n\nasync function _publishIPNSImpl(ipnsPrivateKey: Uint8Array, cid: CID): Promise<W3Name.Revision> {\n\tconst value = `/ipfs/${cid}`\n\tconst ipns = await W3Name.from(ipnsPrivateKey)\n\n\tlet revision: W3Name.Revision\n\tconst existing = await tryResolveIPNS(ipns)\n\n\tif (existing) {\n\t\t// Record exists - increment sequence number\n\t\trevision = await W3Name.increment(existing, value)\n\t\tDEBUG('[w3name] incrementing revision for', ipns.toString())\n\t} else {\n\t\t// First publish - use v0\n\t\trevision = await W3Name.v0(ipns, value)\n\t\tDEBUG('[w3name] creating initial revision for', ipns.toString())\n\t}\n\n\tawait W3Name.publish(revision, ipns.key)\n\tDEBUG('[w3name] published', cid.toString(), 'to', ipns.toString())\n\n\treturn revision // Return for Kubo integration or other uses\n}\n\n/**\n * Create an IPNSPublishTarget that publishes to W3Name service via HTTP POST.\n */\nexport function w3nameTarget(serviceUrl = 'https://name.web3.storage'): IPNSPublishTarget {\n\treturn {\n\t\tname: 'w3name',\n\t\tasync publish(ipnsName: string, recordBytes: Uint8Array) {\n\t\t\tconst res = await fetch(`${serviceUrl}/name/${ipnsName}`, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tbody: base64pad.baseEncode(recordBytes),\n\t\t\t})\n\t\t\tif (!res.ok) throw new Error(`W3Name HTTP ${res.status}`)\n\t\t},\n\t}\n}\n\nexport async function generateIpnsKey() {\n\treturn W3Name.create() // Returns W3Name.WritableName type\n}\n\nexport async function getW3NamePublic(pk: Uint8Array) {\n\tconst ipns = await W3Name.from(pk)\n\treturn ipns.toString()\n}\n"],"mappings":"AACA,OAAS,UAAAA,MAAc,mBACvB,OAAS,aAAAC,MAAiB,4BAE1B,UAAYC,MAAY,SAExB,GAAM,CAAE,KAAAC,EAAM,IAAAC,EAAK,MAAAC,EAAO,MAAAC,CAAM,EAAIN,EAAO,MAAMA,EAAO,IAAI,EAU5D,eAAeO,EAAeC,EAA4D,CACzF,IAAMC,EAAM,kCAAkCD,EAAK,SAAS,CAAC,GAEzDE,EACJ,GAAI,CACHA,EAAW,MAAM,MAAMD,EAAK,CAAE,OAAQ,YAAY,QAAQ,GAAM,CAAE,CAAC,CACpE,OAASE,EAAK,CAEb,MAAML,EAAM,yCAA0CK,CAAG,CAC1D,CAGA,GAAID,EAAS,SAAW,IACvB,OAAAL,EAAM,oDAAqDG,EAAK,SAAS,CAAC,EACnE,KAIR,GAAI,CAACE,EAAS,GACb,MAAMJ,EAAM,iBAAiBI,EAAS,MAAM,mBAAoBA,EAAS,UAAU,EAMpF,OADiB,MAAa,UAAQF,CAAI,CAE3C,CAMA,eAAsBI,EAAYC,EAA4BC,EAAoC,CAEjG,IAAMC,EAAU,IAAI,QAAe,CAACC,EAAGC,IACtC,WAAW,IAAMA,EAAO,IAAI,MAAM,qCAA6C,CAAC,EAAG,GAAU,CAC9F,EACA,OAAO,QAAQ,KAAK,CAACC,EAAiBL,EAAgBC,CAAG,EAAGC,CAAO,CAAC,CACrE,CAEA,eAAeG,EAAiBL,EAA4BC,EAAoC,CAC/F,IAAMK,EAAQ,SAASL,CAAG,GACpBN,EAAO,MAAa,OAAKK,CAAc,EAEzCO,EACEC,EAAW,MAAMd,EAAeC,CAAI,EAE1C,OAAIa,GAEHD,EAAW,MAAa,YAAUC,EAAUF,CAAK,EACjDd,EAAM,qCAAsCG,EAAK,SAAS,CAAC,IAG3DY,EAAW,MAAa,KAAGZ,EAAMW,CAAK,EACtCd,EAAM,yCAA0CG,EAAK,SAAS,CAAC,GAGhE,MAAa,UAAQY,EAAUZ,EAAK,GAAG,EACvCH,EAAM,qBAAsBS,EAAI,SAAS,EAAG,KAAMN,EAAK,SAAS,CAAC,EAE1DY,CACR,CAKO,SAASE,EAAaC,EAAa,4BAAgD,CACzF,MAAO,CACN,KAAM,SACN,MAAM,QAAQC,EAAkBC,EAAyB,CACxD,IAAMC,EAAM,MAAM,MAAM,GAAGH,CAAU,SAASC,CAAQ,GAAI,CACzD,OAAQ,OACR,KAAMvB,EAAU,WAAWwB,CAAW,CACvC,CAAC,EACD,GAAI,CAACC,EAAI,GAAI,MAAM,IAAI,MAAM,eAAeA,EAAI,MAAM,EAAE,CACzD,CACD,CACD,CAEA,eAAsBC,GAAkB,CACvC,OAAc,SAAO,CACtB,CAEA,eAAsBC,EAAgBC,EAAgB,CAErD,OADa,MAAa,OAAKA,CAAE,GACrB,SAAS,CACtB","names":["Logger","base64pad","W3Name","WARN","LOG","DEBUG","ERROR","tryResolveIPNS","ipns","url","response","err","publishIPNS","ipnsPrivateKey","cid","timeout","_","reject","_publishIPNSImpl","value","revision","existing","w3nameTarget","serviceUrl","ipnsName","recordBytes","res","generateIpnsKey","getW3NamePublic","pk"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/retrieve.ts"],"sourcesContent":["import { CarReader } from '@ipld/car'\n// import { create } from '@web3-storage/w3up-client'\nimport { Applog, CidString, sortApplogsByTs } from '@wovin/core/applog'\nimport { Logger } from 'besonders-logger'\nimport { CID } from 'multiformats/cid'\n// import * as W3Name from 'w3name'\nimport { ThreadInMemory } from '@wovin/core/thread'\n\nconst { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line no-unused-vars\n\n// let w3sReadonly: Web3Storage = new Web3Storage({ token: 'FAKE' })\n// const client = await create()\n\n// const GATEWAYS = [\n// \t'https://cloudflare-ipfs.com/ipfs/CID/?format=car&dag-scope=all',\n// \t// \"https://cloudflare-ipfs.com/ipfs/CID/?format=car&dag-scope=block\",\n// \t// \"https://cloudflare-ipfs.com/ipfs/CID/?format=car&dag-scope=entity\",\n// \t'https://CID.ipfs.dweb.link/?format=car&dag-scope=all',\n// \t'https://CID.ipfs.dweb.link/?format=car&dag-scope=block',\n// \t'https://CID.ipfs.dweb.link/?format=car&dag-scope=entity',\n// \t'https://dweb.link/ipfs/CID/?format=car&dag-scope=all',\n// \t'https://dweb.link/ipfs/CID/?format=car&dag-scope=block',\n// \t'https://dweb.link/ipfs/CID/?format=car&dag-scope=entity',\n// \t'https://ipfs.4everland.io/ipfs/CID/?format=car&dag-scope=all',\n// \t'https://ipfs.4everland.io/ipfs/CID/?format=car&dag-scope=block',\n// \t'https://ipfs.4everland.io/ipfs/CID/?format=car&dag-scope=entity',\n// \t'https://ipfs.io/ipfs/CID/?format=car&dag-scope=all',\n// \t'https://ipfs.io/ipfs/CID/?format=car&dag-scope=block',\n// \t'https://ipfs.io/ipfs/CID/?format=car&dag-scope=entity',\n// ]\n\n// export async function retrieveThread(\n// \tpubID: CidString,\n// \t{ readOnly = true, pinnedCID }: { readOnly?: boolean; pinnedCID?: CidString } = {},\n// ) {\n// \tlet cid: CID\n// \tif (pinnedCID) {\n// \t\tcid = CID.parse(pinnedCID)\n// \t} else {\n// \t\ttry {\n// \t\t\tcid = await resolveIPNS(/* parseW3Name( */ pubID /* ) */)\n// \t\t\tDEBUG(`Resolved pub to CID`, cid.toString())\n// \t\t} catch (err) {\n// \t\t\tthrow ERROR(`Failed to resolve IPNS ${pubID}:`, err)\n// \t\t}\n// \t}\n// \tconst car = await retrieveCar(cid.toString())\n// \tconst { applogs: maybeEncrypted } = await decodeCarToApplogs(car)\n// \tconst encrypted = maybeEncrypted.filter(log => log instanceof Uint8Array) as Uint8Array[]\n// \tif (encrypted.length) WARN(`Found ${encrypted.length} encrypted logs - skipping`) // TODO: decryption\n// \tconst applogs = maybeEncrypted.filter(log => !(log instanceof Uint8Array)) as Applog[]\n// \tsortApplogsByTs(applogs)\n// \tconst thread = new ThreadInMemory(applogs, [], `preview-${pubID}`, readOnly)\n// \treturn { cid, thread }\n// \t// return thread\n// }\n\n// export async function resolveIPNS(name: CidString /* W3Name.Name */): Promise<CID> {\n// \tconst response = await fetch(`https://name.web3.storage/name/${name}`)\n// \tconst result = await response.json()\n// \t// return result\n// \t// const revision = await W3Name.resolve(name)\n// \t// DEBUG('[w3name] resolved', name.toString(), 'to', revision)\n// \tif (!result.value.startsWith('/ipfs/')) {\n// \t\tconsole.warn('IPNS value is not ipfs:', result)\n// \t}\n// \treturn CID.parse(result.value.replace('/ipfs/', ''))\n// }\n\n// export async function retrieveCar(cid: CidString): Promise<CarReader> {\n// \t// Fetch the CAR file from web3.storage\n// \tDEBUG('Retrieving:', cid)\n// \t// const response = await w3sReadonly.get(cid)\n// \t// const response = await fetch(`https://saturn.ms/ipfs/${cid}?format=car&dag-scope=all`)\n// \tconst response = await fetch(`https://cloudflare-ipfs.com/ipfs/${cid}?format=car&dag-scope=all`) // HACK: w3s is broken\n// \tif (!response?.ok || !response?.body) {\n// \t\tthrow new Error(`unexpected response ${response?.statusText}`)\n// \t}\n\n// \t// The data is an AsyncIterable<Uint8Array>, convert it to an AsyncIterable for the CarReader\n// \tconst data: AsyncIterable<Uint8Array> = threadToIterable(response!.body.getReader())\n\n// \t// return a CarReader from the CAR data\n// \treturn await CarReader.fromIterable(data)\n// }\n\n// function threadToIterable(bodyReader: any): AsyncIterable<Uint8Array> {\n// \treturn (async function*() {\n// \t\twhile (true) {\n// \t\t\tconst { done, value } = await bodyReader.read()\n// \t\t\tif (done) {\n// \t\t\t\tbreak\n// \t\t\t}\n// \t\t\tyield value\n// \t\t}\n// \t})()\n// }\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/retrieve.ts"],"sourcesContent":["import { CarReader } from '@ipld/car'\n// import { create } from '@web3-storage/w3up-client'\nimport { Applog, CidString, sortApplogsByTs } from '@wovin/core/applog'\nimport { Logger } from 'besonders-logger'\nimport { CID } from 'multiformats/cid'\n// import * as W3Name from 'w3name'\nimport { ThreadInMemory } from '@wovin/core/thread'\n\nconst { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line no-unused-vars\n\n// let w3sReadonly: Web3Storage = new Web3Storage({ token: 'FAKE' })\n// const client = await create()\n\n// const GATEWAYS = [\n// \t'https://cloudflare-ipfs.com/ipfs/CID/?format=car&dag-scope=all',\n// \t// \"https://cloudflare-ipfs.com/ipfs/CID/?format=car&dag-scope=block\",\n// \t// \"https://cloudflare-ipfs.com/ipfs/CID/?format=car&dag-scope=entity\",\n// \t'https://CID.ipfs.dweb.link/?format=car&dag-scope=all',\n// \t'https://CID.ipfs.dweb.link/?format=car&dag-scope=block',\n// \t'https://CID.ipfs.dweb.link/?format=car&dag-scope=entity',\n// \t'https://dweb.link/ipfs/CID/?format=car&dag-scope=all',\n// \t'https://dweb.link/ipfs/CID/?format=car&dag-scope=block',\n// \t'https://dweb.link/ipfs/CID/?format=car&dag-scope=entity',\n// \t'https://ipfs.4everland.io/ipfs/CID/?format=car&dag-scope=all',\n// \t'https://ipfs.4everland.io/ipfs/CID/?format=car&dag-scope=block',\n// \t'https://ipfs.4everland.io/ipfs/CID/?format=car&dag-scope=entity',\n// \t'https://ipfs.io/ipfs/CID/?format=car&dag-scope=all',\n// \t'https://ipfs.io/ipfs/CID/?format=car&dag-scope=block',\n// \t'https://ipfs.io/ipfs/CID/?format=car&dag-scope=entity',\n// ]\n\n// export async function retrieveThread(\n// \tpubID: CidString,\n// \t{ readOnly = true, pinnedCID }: { readOnly?: boolean; pinnedCID?: CidString } = {},\n// ) {\n// \tlet cid: CID\n// \tif (pinnedCID) {\n// \t\tcid = CID.parse(pinnedCID)\n// \t} else {\n// \t\ttry {\n// \t\t\tcid = await resolveIPNS(/* parseW3Name( */ pubID /* ) */)\n// \t\t\tDEBUG(`Resolved pub to CID`, cid.toString())\n// \t\t} catch (err) {\n// \t\t\tthrow ERROR(`Failed to resolve IPNS ${pubID}:`, err)\n// \t\t}\n// \t}\n// \tconst car = await retrieveCar(cid.toString())\n// \tconst { applogs: maybeEncrypted } = await decodeCarToApplogs(car)\n// \tconst encrypted = maybeEncrypted.filter(log => log instanceof Uint8Array) as Uint8Array[]\n// \tif (encrypted.length) WARN(`Found ${encrypted.length} encrypted logs - skipping`) // TODO: decryption\n// \tconst applogs = maybeEncrypted.filter(log => !(log instanceof Uint8Array)) as Applog[]\n// \tsortApplogsByTs(applogs)\n// \tconst thread = new ThreadInMemory(applogs, [], `preview-${pubID}`, readOnly)\n// \treturn { cid, thread }\n// \t// return thread\n// }\n\n// export async function resolveIPNS(name: CidString /* W3Name.Name */): Promise<CID> {\n// \tconst response = await fetch(`https://name.web3.storage/name/${name}`)\n// \tconst result = await response.json()\n// \t// return result\n// \t// const revision = await W3Name.resolve(name)\n// \t// DEBUG('[w3name] resolved', name.toString(), 'to', revision)\n// \tif (!result.value.startsWith('/ipfs/')) {\n// \t\tconsole.warn('IPNS value is not ipfs:', result)\n// \t}\n// \treturn CID.parse(result.value.replace('/ipfs/', ''))\n// }\n\n// export async function retrieveCar(cid: CidString): Promise<CarReader> {\n// \t// Fetch the CAR file from web3.storage\n// \tDEBUG('Retrieving:', cid)\n// \t// const response = await w3sReadonly.get(cid)\n// \t// const response = await fetch(`https://saturn.ms/ipfs/${cid}?format=car&dag-scope=all`)\n// \tconst response = await fetch(`https://cloudflare-ipfs.com/ipfs/${cid}?format=car&dag-scope=all`) // HACK: w3s is broken\n// \tif (!response?.ok || !response?.body) {\n// \t\tthrow new Error(`unexpected response ${response?.statusText}`)\n// \t}\n\n// \t// The data is an AsyncIterable<Uint8Array>, convert it to an AsyncIterable for the CarReader\n// \tconst data: AsyncIterable<Uint8Array> = threadToIterable(response!.body.getReader())\n\n// \t// return a CarReader from the CAR data\n// \treturn await CarReader.fromIterable(data)\n// }\n\n// function threadToIterable(bodyReader: any): AsyncIterable<Uint8Array> {\n// \treturn (async function*() {\n// \t\twhile (true) {\n// \t\t\tconst { done, value } = await bodyReader.read()\n// \t\t\tif (done) {\n// \t\t\t\tbreak\n// \t\t\t}\n// \t\t\tyield value\n// \t\t}\n// \t})()\n// }\n"],"mappings":"AAGA,OAAS,UAAAA,MAAc,mBAKvB,GAAM,CAAE,KAAAC,EAAM,IAAAC,EAAK,MAAAC,EAAO,QAAAC,EAAS,MAAAC,CAAM,EAAIL,EAAO,MAAMA,EAAO,IAAI","names":["Logger","WARN","LOG","DEBUG","VERBOSE","ERROR"]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
var r=()=>{throw new Error("pls no parseW3Name")};export{r as a};
|
|
2
|
-
//# sourceMappingURL=chunk-
|
|
2
|
+
//# sourceMappingURL=chunk-U6PL7AYU.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export * from './retrieve';
|
|
2
|
-
export * from './utils';
|
|
3
|
-
export * from './watch';
|
|
4
|
-
export * from './ipns';
|
|
1
|
+
export * from './retrieve.ts';
|
|
2
|
+
export * from './utils.ts';
|
|
3
|
+
export * from './watch.ts';
|
|
4
|
+
export * from './ipns.ts';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAE7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as p,b as t,c as x,d as a}from"./chunk-LDGLUYRV.js";import"./chunk-SW4VIQUZ.js";import{a as o}from"./chunk-U6PL7AYU.js";import{a as r,b as e,c as f,d as m}from"./chunk-6224SQ5A.js";export{e as IpnsWatcher,x as generateIpnsKey,a as getW3NamePublic,o as parseW3Name,p as publishIPNS,t as w3nameTarget,f as watchName,m as watchNameIterator,r as watchNameRaw};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
package/dist/ipns.js
ADDED
package/dist/retrieve.js
ADDED
package/dist/store.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=store.js.map
|
package/dist/utils.js
ADDED
package/dist/watch.js
ADDED
package/package.json
CHANGED
|
@@ -1,39 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wovin/connect-web3storage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main": "./dist/index.
|
|
6
|
-
"browser": "./dist/index.
|
|
7
|
-
"module": "./dist/index.
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"browser": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
-
"import": "./dist/index.
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
12
|
"types": "./dist/index.d.ts"
|
|
13
13
|
},
|
|
14
14
|
"./ipns": {
|
|
15
|
-
"import": "./dist/ipns.
|
|
15
|
+
"import": "./dist/ipns.js",
|
|
16
16
|
"types": "./dist/ipns.d.ts"
|
|
17
17
|
},
|
|
18
18
|
"./retrieve": {
|
|
19
|
-
"import": "./dist/retrieve.
|
|
19
|
+
"import": "./dist/retrieve.js",
|
|
20
20
|
"types": "./dist/retrieve.d.ts"
|
|
21
21
|
},
|
|
22
22
|
"./utils": {
|
|
23
|
-
"import": "./dist/utils.
|
|
23
|
+
"import": "./dist/utils.js",
|
|
24
24
|
"types": "./dist/utils.d.ts"
|
|
25
25
|
},
|
|
26
26
|
"./store": {
|
|
27
|
-
"import": "./dist/store.
|
|
27
|
+
"import": "./dist/store.js",
|
|
28
28
|
"types": "./dist/store.d.ts"
|
|
29
29
|
},
|
|
30
30
|
"./watch": {
|
|
31
|
-
"import": "./dist/watch.
|
|
31
|
+
"import": "./dist/watch.js",
|
|
32
32
|
"types": "./dist/watch.d.ts"
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
|
-
"./dist/"
|
|
36
|
+
"./dist/",
|
|
37
|
+
"./src/"
|
|
37
38
|
],
|
|
38
39
|
"esm.sh": {
|
|
39
40
|
"bundle": false
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
"multiformats": "^13.0.1",
|
|
45
46
|
"partysocket": "^1.0.2",
|
|
46
47
|
"w3name": "^1.0.8",
|
|
47
|
-
"@wovin/core": "^0.
|
|
48
|
+
"@wovin/core": "^0.2.0"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"concurrently": "^8.2.2",
|
package/src/index.ts
ADDED
package/src/ipns.ts
ADDED
|
@@ -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
|
+
}
|
package/src/retrieve.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { CarReader } from '@ipld/car'
|
|
2
|
+
// import { create } from '@web3-storage/w3up-client'
|
|
3
|
+
import { Applog, CidString, sortApplogsByTs } from '@wovin/core/applog'
|
|
4
|
+
import { Logger } from 'besonders-logger'
|
|
5
|
+
import { CID } from 'multiformats/cid'
|
|
6
|
+
// import * as W3Name from 'w3name'
|
|
7
|
+
import { ThreadInMemory } from '@wovin/core/thread'
|
|
8
|
+
|
|
9
|
+
const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line no-unused-vars
|
|
10
|
+
|
|
11
|
+
// let w3sReadonly: Web3Storage = new Web3Storage({ token: 'FAKE' })
|
|
12
|
+
// const client = await create()
|
|
13
|
+
|
|
14
|
+
// const GATEWAYS = [
|
|
15
|
+
// 'https://cloudflare-ipfs.com/ipfs/CID/?format=car&dag-scope=all',
|
|
16
|
+
// // "https://cloudflare-ipfs.com/ipfs/CID/?format=car&dag-scope=block",
|
|
17
|
+
// // "https://cloudflare-ipfs.com/ipfs/CID/?format=car&dag-scope=entity",
|
|
18
|
+
// 'https://CID.ipfs.dweb.link/?format=car&dag-scope=all',
|
|
19
|
+
// 'https://CID.ipfs.dweb.link/?format=car&dag-scope=block',
|
|
20
|
+
// 'https://CID.ipfs.dweb.link/?format=car&dag-scope=entity',
|
|
21
|
+
// 'https://dweb.link/ipfs/CID/?format=car&dag-scope=all',
|
|
22
|
+
// 'https://dweb.link/ipfs/CID/?format=car&dag-scope=block',
|
|
23
|
+
// 'https://dweb.link/ipfs/CID/?format=car&dag-scope=entity',
|
|
24
|
+
// 'https://ipfs.4everland.io/ipfs/CID/?format=car&dag-scope=all',
|
|
25
|
+
// 'https://ipfs.4everland.io/ipfs/CID/?format=car&dag-scope=block',
|
|
26
|
+
// 'https://ipfs.4everland.io/ipfs/CID/?format=car&dag-scope=entity',
|
|
27
|
+
// 'https://ipfs.io/ipfs/CID/?format=car&dag-scope=all',
|
|
28
|
+
// 'https://ipfs.io/ipfs/CID/?format=car&dag-scope=block',
|
|
29
|
+
// 'https://ipfs.io/ipfs/CID/?format=car&dag-scope=entity',
|
|
30
|
+
// ]
|
|
31
|
+
|
|
32
|
+
// export async function retrieveThread(
|
|
33
|
+
// pubID: CidString,
|
|
34
|
+
// { readOnly = true, pinnedCID }: { readOnly?: boolean; pinnedCID?: CidString } = {},
|
|
35
|
+
// ) {
|
|
36
|
+
// let cid: CID
|
|
37
|
+
// if (pinnedCID) {
|
|
38
|
+
// cid = CID.parse(pinnedCID)
|
|
39
|
+
// } else {
|
|
40
|
+
// try {
|
|
41
|
+
// cid = await resolveIPNS(/* parseW3Name( */ pubID /* ) */)
|
|
42
|
+
// DEBUG(`Resolved pub to CID`, cid.toString())
|
|
43
|
+
// } catch (err) {
|
|
44
|
+
// throw ERROR(`Failed to resolve IPNS ${pubID}:`, err)
|
|
45
|
+
// }
|
|
46
|
+
// }
|
|
47
|
+
// const car = await retrieveCar(cid.toString())
|
|
48
|
+
// const { applogs: maybeEncrypted } = await decodeCarToApplogs(car)
|
|
49
|
+
// const encrypted = maybeEncrypted.filter(log => log instanceof Uint8Array) as Uint8Array[]
|
|
50
|
+
// if (encrypted.length) WARN(`Found ${encrypted.length} encrypted logs - skipping`) // TODO: decryption
|
|
51
|
+
// const applogs = maybeEncrypted.filter(log => !(log instanceof Uint8Array)) as Applog[]
|
|
52
|
+
// sortApplogsByTs(applogs)
|
|
53
|
+
// const thread = new ThreadInMemory(applogs, [], `preview-${pubID}`, readOnly)
|
|
54
|
+
// return { cid, thread }
|
|
55
|
+
// // return thread
|
|
56
|
+
// }
|
|
57
|
+
|
|
58
|
+
// export async function resolveIPNS(name: CidString /* W3Name.Name */): Promise<CID> {
|
|
59
|
+
// const response = await fetch(`https://name.web3.storage/name/${name}`)
|
|
60
|
+
// const result = await response.json()
|
|
61
|
+
// // return result
|
|
62
|
+
// // const revision = await W3Name.resolve(name)
|
|
63
|
+
// // DEBUG('[w3name] resolved', name.toString(), 'to', revision)
|
|
64
|
+
// if (!result.value.startsWith('/ipfs/')) {
|
|
65
|
+
// console.warn('IPNS value is not ipfs:', result)
|
|
66
|
+
// }
|
|
67
|
+
// return CID.parse(result.value.replace('/ipfs/', ''))
|
|
68
|
+
// }
|
|
69
|
+
|
|
70
|
+
// export async function retrieveCar(cid: CidString): Promise<CarReader> {
|
|
71
|
+
// // Fetch the CAR file from web3.storage
|
|
72
|
+
// DEBUG('Retrieving:', cid)
|
|
73
|
+
// // const response = await w3sReadonly.get(cid)
|
|
74
|
+
// // const response = await fetch(`https://saturn.ms/ipfs/${cid}?format=car&dag-scope=all`)
|
|
75
|
+
// const response = await fetch(`https://cloudflare-ipfs.com/ipfs/${cid}?format=car&dag-scope=all`) // HACK: w3s is broken
|
|
76
|
+
// if (!response?.ok || !response?.body) {
|
|
77
|
+
// throw new Error(`unexpected response ${response?.statusText}`)
|
|
78
|
+
// }
|
|
79
|
+
|
|
80
|
+
// // The data is an AsyncIterable<Uint8Array>, convert it to an AsyncIterable for the CarReader
|
|
81
|
+
// const data: AsyncIterable<Uint8Array> = threadToIterable(response!.body.getReader())
|
|
82
|
+
|
|
83
|
+
// // return a CarReader from the CAR data
|
|
84
|
+
// return await CarReader.fromIterable(data)
|
|
85
|
+
// }
|
|
86
|
+
|
|
87
|
+
// function threadToIterable(bodyReader: any): AsyncIterable<Uint8Array> {
|
|
88
|
+
// return (async function*() {
|
|
89
|
+
// while (true) {
|
|
90
|
+
// const { done, value } = await bodyReader.read()
|
|
91
|
+
// if (done) {
|
|
92
|
+
// break
|
|
93
|
+
// }
|
|
94
|
+
// yield value
|
|
95
|
+
// }
|
|
96
|
+
// })()
|
|
97
|
+
// }
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// import { build } from 'ucan-storage/ucan-storage'
|
|
2
|
+
|
|
3
|
+
// // The DID for the storage service. In real code, you should obtain this from the service you're targetting.
|
|
4
|
+
// const serviceDID = 'did:key:a-fake-service-did'
|
|
5
|
+
|
|
6
|
+
// async function createRequestToken(parentUCAN, issuerKeyPair) {
|
|
7
|
+
// // we want to include the capabilities of the parent token in our request token
|
|
8
|
+
// // so we validate the parent token to extract the payload and copy over the capabilities
|
|
9
|
+
// const { payload } = await validate(parentUCAN)
|
|
10
|
+
|
|
11
|
+
// // the `att` field contains the capabilities we need for uploading
|
|
12
|
+
// const { att } = payload
|
|
13
|
+
|
|
14
|
+
// return build({
|
|
15
|
+
// issuer: issuerKeyPair,
|
|
16
|
+
// audience: serviceDID,
|
|
17
|
+
// capabilities: att,
|
|
18
|
+
// proofs: [parentUcan],
|
|
19
|
+
// })
|
|
20
|
+
// }
|