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
JavaScript
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
;