UNPKG

nft.storage

Version:

A client library for the https://nft.storage/ service. It provides a convenient interface for working with the HTTP API from a web browser or Node.js

443 lines (406 loc) 13 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var pack = require('ipfs-car/pack'); require('multiformats/cid'); var Block = require('multiformats/block'); var sha2 = require('multiformats/hashes/sha2'); var dagCbor = require('@ipld/dag-cbor'); require('@web-std/fetch'); var formData = require('@web-std/form-data'); require('@web-std/blob'); var file = require('@web-std/file'); var fs = require('ipfs-car/blockstore/fs'); var gateway = require('./gateway.cjs'); var bsCarReader = require('./bs-car-reader.cjs'); function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n['default'] = e; return Object.freeze(n); } var Block__namespace = /*#__PURE__*/_interopNamespace(Block); var dagCbor__namespace = /*#__PURE__*/_interopNamespace(dagCbor); /** * @typedef {import('./gateway.js').GatewayURLOptions} EmbedOptions * @typedef {import('./lib/interface.js').TokenInput} TokenInput * @typedef {import('ipfs-car/blockstore').Blockstore} Blockstore */ /** * @template T * @typedef {import('./lib/interface.js').Encoded<T, [[Blob, URL]]>} EncodedBlobUrl */ /** * @template G * @typedef {import('./lib/interface.js').Encoded<G, [[Blob, Blob]]>} EncodedBlobBlob */ /** * @template {import('./lib/interface.js').TokenInput} T * @typedef {import('./lib/interface.js').Token<T>} TokenType */ /** * @template {TokenInput} T * @implements {TokenType<T>} */ class Token { /** * @param {import('./lib/interface.js').CIDString} ipnft * @param {import('./lib/interface.js').EncodedURL} url * @param {import('./lib/interface.js').Encoded<T, [[Blob, URL]]>} data */ constructor(ipnft, url, data) { /** @readonly */ this.ipnft = ipnft; /** @readonly */ this.url = url; /** @readonly */ this.data = data; Object.defineProperties(this, { ipnft: { enumerable: true, writable: false }, url: { enumerable: true, writable: false }, data: { enumerable: false, writable: false }, }); } /** * @returns {import('./lib/interface.js').Encoded<T, [[Blob, URL]]>} */ embed() { return Token.embed(this) } /** * @template {TokenInput} T * @param {{data: import('./lib/interface.js').Encoded<T, [[Blob, URL]]>}} token * @returns {import('./lib/interface.js').Encoded<T, [[Blob, URL]]>} */ static embed({ data }) { return embed(data, { gateway: gateway.GATEWAY }) } /** * Takes token input, encodes it as a DAG, wraps it in a CAR and creates a new * Token instance from it. Where values are discovered `Blob` (or `File`) * objects in the given input, they are replaced with IPFS URLs (an `ipfs://` * prefixed CID with an optional path). * * @example * ```js * const cat = new File(['...'], 'cat.png') * const kitty = new File(['...'], 'kitty.png') * const { token, car } = await Token.encode({ * name: 'hello' * image: cat * properties: { * extra: { * image: kitty * } * } * }) * ``` * * @template {TokenInput} T * @param {T} input * @returns {Promise<{ cid: CID, token: TokenType<T>, car: import('./lib/interface.js').CarReader }>} */ static async encode(input) { const blockstore = new fs.FsBlockStore(); const [blobs, meta] = mapTokenInputBlobs(input); /** @type {EncodedBlobUrl<T>} */ const data = JSON.parse(JSON.stringify(meta)); /** @type {import('./lib/interface.js').Encoded<T, [[Blob, CID]]>} */ const dag = JSON.parse(JSON.stringify(meta)); for (const [dotPath, blob] of blobs.entries()) { /** @type {string|undefined} */ // @ts-ignore blob may be a File! const name = blob.name || 'blob'; /** @type {import('./platform.js').ReadableStream} */ let content; // FIXME: should not be necessary to await arrayBuffer()! // Node.js 20 hangs reading the stream (it never ends) but in // older node versions and the browser it is fine to use blob.stream(). /* c8 ignore next 5 */ if (parseInt(globalThis.process?.versions?.node) > 18) { content = new Uint8Array(await blob.arrayBuffer()); } else { content = blob.stream(); } const { root: cid } = await pack.pack({ input: [{ path: name, content }], blockstore, wrapWithDirectory: true, }); const href = new URL(`ipfs://${cid}/${name}`); const path = dotPath.split('.'); setIn(data, path, href); setIn(dag, path, cid); } const { root: metadataJsonCid } = await pack.pack({ input: [{ path: 'metadata.json', content: JSON.stringify(data) }], blockstore, wrapWithDirectory: false, }); const block = await Block__namespace.encode({ value: { ...dag, 'metadata.json': metadataJsonCid, type: 'nft', }, codec: dagCbor__namespace, hasher: sha2.sha256, }); await blockstore.put(block.cid, block.bytes); return { cid: block.cid, token: new Token( block.cid.toString(), `ipfs://${block.cid}/metadata.json`, data ), car: new bsCarReader.BlockstoreCarReader(1, [block.cid], blockstore), } } } /** * @template T * @param {EncodedBlobUrl<T>} input * @param {EmbedOptions} options * @returns {EncodedBlobUrl<T>} */ const embed = (input, options) => mapWith(input, isURL, embedURL, options); /** * @template {TokenInput} T * @param {import('./lib/interface.js').EncodedToken<T>} value * @param {Set<string>} paths - Paths were to expect EncodedURLs * @returns {Token<T>} */ const decode = ({ ipnft, url, data }, paths) => new Token(ipnft, url, mapWith(data, isEncodedURL, decodeURL, paths)); /** * @param {any} value * @returns {value is URL} */ const isURL = (value) => value instanceof URL; /** * @template State * @param {State} state * @param {import('./lib/interface.js').EncodedURL} url * @returns {[State, URL]} */ const decodeURL = (state, url) => [state, new URL(url)]; /** * @param {EmbedOptions} context * @param {URL} url * @returns {[EmbedOptions, URL]} */ const embedURL = (context, url) => [context, gateway.toGatewayURL(url, context)]; /** * @param {any} value * @returns {value is object} */ const isObject = (value) => typeof value === 'object' && value != null; /** * @param {any} value * @param {Set<string>} assetPaths * @param {PropertyKey[]} path * @returns {value is import('./lib/interface.js').EncodedURL} */ const isEncodedURL = (value, assetPaths, path) => typeof value === 'string' && assetPaths.has(path.join('.')); /** * Takes token input and encodes it into * [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) * object where form field values are discovered `Blob` (or `File`) objects in * the given token and field keys are `.` joined paths where they were discoverd * in the token. Additionally encoded `FormData` will also have a field * named `meta` containing JSON serialized token with blobs and file values * `null` set to null (this allows backend to injest all of the files from * `multipart/form-data` request and update provided "meta" data with * corresponding file ipfs:// URLs) * * @example * ```js * const cat = new File([], 'cat.png') * const kitty = new File([], 'kitty.png') * const form = encode({ * name: 'hello' * image: cat * properties: { * extra: { * image: kitty * } * } * }) * [...form.entries()] //> * // [ * // ['image', cat], * // ['properties.extra.image', kitty], * // ['meta', '{"name":"hello",image:null,"properties":{"extra":{"kitty": null}}}'] * // ] * ``` * * @template {TokenInput} T * @param {EncodedBlobBlob<T>} input * @returns {FormData} */ const encode = (input) => { const [map, meta] = mapValueWith(input, isBlob, encodeBlob, new Map(), []); const form = new formData.FormData(); for (const [k, v] of map.entries()) { form.set(k, v); } form.set('meta', JSON.stringify(meta)); return form }; /** * @param {Map<string, Blob>} data * @param {Blob} blob * @param {PropertyKey[]} path * @returns {[Map<string, Blob>, void]} */ const encodeBlob = (data, blob, path) => { data.set(path.join('.'), blob); return [data, undefined] }; /** * @param {any} value * @returns {value is Blob} */ const isBlob = (value) => value instanceof file.Blob; /** * @template {TokenInput} T * @param {EncodedBlobBlob<T>} input */ const mapTokenInputBlobs = (input) => { return mapValueWith(input, isBlob, encodeBlob, new Map(), []) }; /** * Substitues values in the given `input` that match `p(value) == true` with * `f(value, context, path)` where `context` is whatever you pass (usually * a mutable state) and `path` is a array of keys / indexes where the value * was encountered. * * @template T, I, X, O, State * @param {import('./lib/interface.js').Encoded<T, [[I, X]]>} input - Arbitrary input. * @param {(input:any, state:State, path:PropertyKey[]) => input is X} p - Predicate function to determine * which values to swap. * @param {(state:State, input:X, path:PropertyKey[]) => [State, O]} f - Function * that swaps matching values. * @param {State} state - Some additional context you need in the process. * likey you'll start with `[]`. * @returns {import('./lib/interface.js').Encoded<T, [[I, O]]>} */ const mapWith = (input, p, f, state) => { const [, output] = mapValueWith(input, p, f, state, []); return output }; /** * @template T, I, X, O, State * @param {import('./lib/interface.js').Encoded<T, [[I, X]]>} input - Arbitrary input. * @param {(input:any, state:State, path:PropertyKey[]) => input is X} p - Predicate function to determine * which values to swap. * @param {(state:State, input:X, path:PropertyKey[]) => [State, O]} f - Function * that swaps matching values. * @param {State} state - Some additional context you need in the process. * @param {PropertyKey[]} path - Path where the value was encountered. Most * likey you'll start with `[]`. * @returns {[State, import('./lib/interface.js').Encoded<T, [[I, O]]>]} */ const mapValueWith = (input, p, f, state, path) => p(input, state, path) ? f(state, input, path) : Array.isArray(input) ? mapArrayWith(input, p, f, state, path) : isObject(input) ? mapObjectWith(input, p, f, state, path) : [state, /** @type {any} */ (input)]; /** * Just like `mapWith` except * * @template State, T, I, X, O * @param {import('./lib/interface.js').Encoded<T, [[I, X]]>} input * @param {(input:any, state:State, path:PropertyKey[]) => input is X} p * @param {(state: State, input:X, path:PropertyKey[]) => [State, O]} f * @param {State} init * @param {PropertyKey[]} path * @returns {[State, import('./lib/interface.js').Encoded<T, [[I, O]]>]} */ const mapObjectWith = (input, p, f, init, path) => { let state = init; const output = /** @type {import('./lib/interface.js').Encoded<T, [[I, O]]>} */ ({}); for (const [key, value] of Object.entries(input)) { const [next, out] = mapValueWith(value, p, f, state, [...path, key]); // @ts-ignore output[key] = out; state = next; } return [state, output] }; /** * Just like `mapWith` except for Arrays. * * @template I, X, O, State * @template {any[]} T * @param {T} input * @param {(input:any, state:State, path:PropertyKey[]) => input is X} p * @param {(state: State, input:X, path:PropertyKey[]) => [State, O]} f * @param {State} init * @param {PropertyKey[]} path * @returns {[State, import('./lib/interface.js').Encoded<T, [[I, O]]>]} */ const mapArrayWith = (input, p, f, init, path) => { const output = /** @type {unknown[]} */ ([]); let state = init; for (const [index, element] of input.entries()) { const [next, out] = mapValueWith(element, p, f, state, [...path, index]); output[index] = out; state = next; } return [ state, /** @type {import('./lib/interface.js').Encoded<T, [[I, O]]>} */ (output), ] }; /** * Sets a given `value` at the given `path` on a passed `object`. * * @example * ```js * const obj = { a: { b: { c: 1 }}} * setIn(obj, ['a', 'b', 'c'], 5) * obj.a.b.c //> 5 * ``` * * @template V * @param {any} object * @param {string[]} path * @param {V} value */ const setIn = (object, path, value) => { const n = path.length - 1; let target = object; for (let [index, key] of path.entries()) { if (index === n) { target[key] = value; } else { target = target[key]; } } }; exports.Token = Token; exports.decode = decode; exports.embed = embed; exports.encode = encode; exports.mapWith = mapWith; //# sourceMappingURL=token.cjs.map