UNPKG

w3name

Version:

The JavaScript API client for w3name

1 lines 15.6 kB
{"version":3,"file":"index.mjs","sources":["../src/index.ts"],"sourcesContent":["/**\n * A client library for the w3name - IPNS over HTTP API. It provides a\n * convenient interface for creating names, making revisions to name records,\n * and publishing and resolving them via the HTTP API.\n *\n * @example\n * ```js\n * import * as Name from 'w3name'\n *\n * const name = await Name.create()\n *\n * console.log('Name:', name.toString())\n * // e.g. k51qzi5uqu5di9agapykyjh3tqrf7i14a7fjq46oo0f6dxiimj62knq13059lt\n *\n * // The value to publish\n * const value = '/ipfs/bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui'\n * const revision = await Name.v0(name, value)\n *\n * // Publish the revision\n * await Name.publish(revision, name.key)\n *\n * // Resolve the latest value\n * await Name.resolve(name)\n * ```\n * @module\n */\n\nimport { generateKeyPair, publicKeyFromProtobuf, privateKeyFromProtobuf, privateKeyFromRaw } from '@libp2p/crypto/keys'\nimport { PrivateKey, PublicKey } from '@libp2p/interface'\nimport { Link } from 'multiformats'\nimport { base36 } from 'multiformats/bases/base36'\nimport { base64pad } from 'multiformats/bases/base64'\nimport { CID } from 'multiformats/cid'\nimport * as Digest from 'multiformats/hashes/digest'\nimport * as ipns from 'ipns'\nimport { validate } from 'ipns/validator'\nimport * as cbor from 'cborg'\nimport W3NameService from './service.js'\n\nconst libp2pKeyCode = 0x72\nconst ONE_YEAR = 1000 * 60 * 60 * 24 * 365\n\nconst defaultValidity = (): RFC3339DateString => new Date(Date.now() + ONE_YEAR).toISOString()\nconst defaultTTL = 5n * 60n * 1000n * 1000000n // 5 minutes in nanoseconds\nconst defaultService = new W3NameService()\n\n/**\n * Name is an IPNS key ID.\n *\n * Names can be used to retrieve the latest published value from the W3name service\n * using the {@link resolve} function.\n *\n * Note that `Name` contains only the public verification key and does not allow publishing\n * or updating records. To create or update a record, use the {@link WritableName} subclass.\n *\n * To convert from a string representation of a name to a `Name` object use the {@link parse} function.\n */\nexport class Name {\n /** @internal */\n _pubKey: PublicKey\n\n /** @internal */\n _cid: Link\n\n constructor (pubKey: PublicKey) {\n /**\n * @private\n */\n this._pubKey = pubKey\n this._cid = pubKey.toCID()\n }\n\n /**\n * A binary representation of the IPNS verification key.\n */\n get bytes (): Uint8Array {\n return this._cid.bytes\n }\n\n /**\n * @returns the string representation of the IPNS verification key (e.g. `k51qzi5uqu5di9agapykyjh3tqrf7i14a7fjq46oo0f6dxiimj62knq13059lt`)\n */\n toString (): string {\n return this._cid.toString(base36)\n }\n}\n\n/**\n * WritableName is a {@link Name} that has a signing key associated with it such that\n * new IPNS record {@link Revision}s can be created and signed for it.\n *\n * New `WritableName`s can be generated using the {@link create} function.\n *\n * To load a `WritableName` from a saved binary representation, see {@link from}.\n */\nexport class WritableName extends Name {\n /** @internal */\n _privKey: PrivateKey\n\n constructor (privKey: PrivateKey) {\n super(privKey.publicKey)\n /**\n * @private\n */\n this._privKey = privKey\n }\n\n /**\n * The private signing key, as a libp2p `PrivateKey` object.\n *\n * To save a key for later loading with {@link from}, write the\n * contents of `key.raw` somewhere safe.\n */\n get key (): PrivateKey {\n return this._privKey\n }\n}\n\n/**\n * Create a new name with associated signing key that can be used to create and\n * publish IPNS record revisions.\n */\nexport async function create (): Promise<WritableName> {\n const privKey = await generateKeyPair('Ed25519', 2048)\n return new WritableName(privKey)\n}\n\n/**\n * Parses a string-encoded {@link Name} to a {@link Name} object.\n *\n * Note that this returns a read-only {@link Name}, which can be used to {@link resolve} values\n * but cannot {@link publish} them.\n */\nexport function parse (name: string): Name {\n const keyCid = CID.parse(name, base36)\n if (keyCid.code !== libp2pKeyCode) {\n throw new Error(`Invalid key, expected ${libp2pKeyCode} codec code but got ${keyCid.code}`)\n }\n const pubKey = publicKeyFromProtobuf(Digest.decode(keyCid.multihash.bytes).bytes)\n return new Name(pubKey)\n}\n\n/**\n * Creates a {@link WritableName} from an existing signing key (private key).\n *\n * Expects the given `Uint8Array` to contain a binary representation of a\n * private signing key. Note that this is **not** the same as the output of\n * {@link Name#bytes | Name.bytes}, which always returns an encoding of the _public_ key,\n * even when the name in question is a {@link WritableName}.\n *\n * To save the key for a {@link WritableName} so that it can be used with this\n * function, use `key.raw`, for example:\n *\n * @example\n * ```js\n * import * as Name from 'w3name'\n * import fs from 'fs'\n *\n * async function example() {\n * const myName = await Name.create()\n *\n * // myName.key.raw can now be written to disk / database, etc.\n * await fs.promises.writeFile('myName.key', myName.key.raw)\n *\n * // let's pretend some time has passed and we want to load the\n * // key from disk:\n * const loadedBytes = await fs.promises.readFile('myName.key')\n * const myName2 = await Name.from(loadedBytes)\n *\n * // myName and myName2 can now be used interchangeably\n * }\n * ```\n *\n */\nexport async function from (key: Uint8Array): Promise<WritableName> {\n let privKey: PrivateKey\n try {\n privKey = privateKeyFromRaw(key)\n } catch {\n // legacy keys where key.bytes used to contain the PB encoded private key\n privKey = privateKeyFromProtobuf(key)\n }\n return new WritableName(privKey)\n}\n\n/**\n * Create an initial version of the IPNS record for the passed {@link Name}, set to the\n * passed value.\n *\n * Note that the returned {@link Revision} object must be {@link publish}ed before it\n * can be {@link resolve}d using the service.\n */\nexport async function v0 (name: Name, value: string): Promise<Revision> {\n return new Revision(name, value, 0n, defaultValidity())\n}\n\n/**\n * Create a {@link Revision} of the passed IPNS record by incrementing the sequence\n * number and changing the value.\n *\n * This returns a new {@link Revision} and does not alter the original `revision` argument.\n */\nexport async function increment (revision: Revision, value: string): Promise<Revision> {\n const seqno = revision.sequence + 1n\n return new Revision(revision.name, value, seqno, defaultValidity())\n}\n\nexport type RFC3339DateString = string\n\nexport interface RevisionOptions {\n /** TTL in nanoseconds, Default: 5m */\n ttl?: bigint\n}\n\n/**\n * A representation of a IPNS record that may be initial or revised.\n */\nexport class Revision {\n /** @internal */\n _name: Name\n\n /** @internal */\n _value: string\n\n /** @internal */\n _sequence: bigint\n\n /** @internal */\n _validity: string\n\n /** @internal */\n _ttl: bigint | undefined\n\n constructor (name: Name, value: string, sequence: bigint, validity: RFC3339DateString, options?: RevisionOptions) {\n this._name = name\n\n if (typeof value !== 'string') {\n throw new Error('invalid value')\n }\n this._value = value\n\n if (typeof sequence !== 'bigint') {\n throw new Error('invalid sequence number')\n }\n this._sequence = sequence\n\n if (typeof validity !== 'string' || Number.isNaN(new Date(validity).getTime())) {\n throw new Error('invalid validity')\n }\n this._validity = validity\n\n const ttl = options?.ttl ?? defaultTTL\n if (typeof ttl !== 'bigint') {\n throw new Error('invalid TTL')\n }\n this._ttl = ttl\n }\n\n get name (): Name {\n return this._name\n }\n\n get value (): string {\n return this._value\n }\n\n get sequence (): bigint {\n return this._sequence\n }\n\n /**\n * RFC3339 date string.\n */\n get validity (): RFC3339DateString {\n return this._validity\n }\n\n /** TTL in nanoseconds */\n get ttl (): bigint | undefined {\n return this._ttl\n }\n\n /**\n * Encodes a `Revision` to a binary representation and returns it as a `Uint8Array`.\n *\n * Note: if `revision.name` is a `WritableName` then signing key data will be\n * lost. i.e. the private key is not encoded.\n */\n static encode (revision: Revision): Uint8Array {\n return cbor.encode({\n name: revision._name.toString(),\n value: revision._value,\n sequence: revision._sequence,\n validity: revision._validity,\n ...(revision._ttl != null ? { ttl: revision._ttl } : {})\n })\n }\n\n /**\n * Decodes a `Revision` from a binary representation.\n *\n * @param bytes - a `Uint8Array` containing a binary encoding of a `Revision`, as produced by {@link #encode}.\n * @returns a {@link Revision} object\n * @throws if `bytes` does not contain a valid encoded `Revision`\n */\n static decode (bytes: Uint8Array): Revision {\n const raw = cbor.decode(bytes)\n const name = parse(raw.name)\n return new Revision(name, raw.value, BigInt(raw.sequence), raw.validity, { ttl: raw.ttl != null ? BigInt(raw.ttl) : undefined })\n }\n}\n\n/**\n * Publish a name {@link Revision} to W3name.\n *\n * Names should be {@link resolve}-able immediately via the w3name service, and will be\n * provided to the IPFS DHT network.\n *\n * Note that it may take a few seconds for the record to propagate and become available via\n * the IPFS DHT network and IPFS <-> HTTP gateways.\n */\nexport async function publish (revision: Revision, key: PrivateKey, service: W3NameService = defaultService): Promise<void> {\n const url = new URL(`name/${revision.name.toString()}`, service.endpoint)\n const entry = await ipns.createIPNSRecord(\n key,\n revision.value,\n revision.sequence,\n new Date(revision.validity).getTime() - Date.now(),\n { ttlNs: revision.ttl }\n )\n await service.waitForRateLimit()\n\n await maybeHandleError(fetch(url.toString(), {\n method: 'POST',\n body: base64pad.baseEncode(ipns.marshalIPNSRecord(entry))\n }))\n}\n\n/**\n * Resolve the current IPNS record revision for the passed name.\n *\n * Note that this will only resolve names published using the W3name service. Names published by\n * other IPNS implementations should be resolved using a DHT-backed implementation (e.g. kubo, js-ipfs, etc).\n */\nexport async function resolve (name: Name, service: W3NameService = defaultService): Promise<Revision> {\n const url = new URL(`name/${name.toString()}`, service.endpoint)\n await service.waitForRateLimit()\n\n const res: globalThis.Response = await maybeHandleError(fetch(url.toString()))\n const { record } = await res.json()\n\n const bytes = base64pad.baseDecode(record)\n const entry = ipns.unmarshalIPNSRecord(bytes)\n const keyCid = CID.decode(name.bytes)\n const pubKey = publicKeyFromProtobuf(Digest.decode(keyCid.multihash.bytes).bytes)\n\n await validate(pubKey, bytes)\n\n return new Revision(\n name,\n entry.value,\n entry.sequence,\n entry.validity,\n { ttl: entry.ttl }\n )\n}\n\nasync function maybeHandleError (resPromise: Promise<globalThis.Response>): Promise<globalThis.Response> {\n const res = await resPromise\n if (res.ok) return res\n let error\n try {\n const parsedError = await res.json()\n error = new Error(parsedError.message)\n } catch (jsonParseError) {\n error = new Error(`unexpected response from API, cannot parse error response. Received status: ${res.status}`)\n }\n throw error\n}\n"],"names":["defaultValidity","Date","now","toISOString","defaultTTL","defaultService","W3NameService","Name","_pubKey","_cid","constructor","pubKey","this","toCID","bytes","toString","base36","WritableName","_privKey","privKey","super","publicKey","key","async","create","generateKeyPair","parse","name","keyCid","CID","code","Error","publicKeyFromProtobuf","Digest","decode","multihash","from","privateKeyFromRaw","privateKeyFromProtobuf","v0","value","Revision","increment","revision","seqno","sequence","_name","_value","_sequence","_validity","_ttl","validity","options","Number","isNaN","getTime","ttl","static","cbor","encode","raw","BigInt","undefined","publish","service","url","URL","endpoint","entry","ipns","createIPNSRecord","ttlNs","waitForRateLimit","maybeHandleError","fetch","method","body","base64pad","baseEncode","marshalIPNSRecord","resolve","res","record","json","baseDecode","unmarshalIPNSRecord","validate","resPromise","ok","error","parsedError","message","jsonParseError","status"],"mappings":"ubAuCA,MAGMA,EAAkB,IAAyB,IAAIC,KAAKA,KAAKC,MAF9C,SAEgEC,cAC3EC,EAAa,GAAK,IAAM,MAAQ,SAChCC,EAAiB,IAAIC,QAadC,EAEXC,QAGAC,KAEAC,YAAaC,GAIXC,KAAKJ,QAAUG,EACfC,KAAKH,KAAOE,EAAOE,OACrB,CAKIC,YACF,OAAOF,KAAKH,KAAKK,KACnB,CAKAC,WACE,OAAOH,KAAKH,KAAKM,SAASC,EAC5B,EAWI,MAAOC,UAAqBV,EAEhCW,SAEAR,YAAaS,GACXC,MAAMD,EAAQE,WAIdT,KAAKM,SAAWC,CAClB,CAQIG,UACF,OAAOV,KAAKM,QACd,EAOKK,eAAeC,IACpB,MAAML,QAAgBM,EAAgB,UAAW,MACjD,OAAO,IAAIR,EAAaE,EAC1B,CAQM,SAAUO,EAAOC,GACrB,MAAMC,EAASC,EAAIH,MAAMC,EAAMX,GAC/B,GAhGoB,MAgGhBY,EAAOE,KACT,MAAM,IAAIC,MAAM,gDAA6DH,EAAOE,QAEtF,MAAMnB,EAASqB,EAAsBC,EAAOC,OAAON,EAAOO,UAAUrB,OAAOA,OAC3E,OAAO,IAAIP,EAAKI,EAClB,CAkCOY,eAAea,EAAMd,GAC1B,IAAIH,EACJ,IACEA,EAAUkB,EAAkBf,EAI7B,CAHC,MAEAH,EAAUmB,EAAuBhB,EAClC,CACD,OAAO,IAAIL,EAAaE,EAC1B,CASOI,eAAegB,EAAIZ,EAAYa,GACpC,OAAO,IAAIC,EAASd,EAAMa,EAAO,GAAIxC,IACvC,CAQOuB,eAAemB,EAAWC,EAAoBH,GACnD,MAAMI,EAAQD,EAASE,SAAW,GAClC,OAAO,IAAIJ,EAASE,EAAShB,KAAMa,EAAOI,EAAO5C,IACnD,OAYayC,EAEXK,MAGAC,OAGAC,UAGAC,UAGAC,KAEAxC,YAAaiB,EAAYa,EAAeK,EAAkBM,EAA6BC,GAGrF,GAFAxC,KAAKkC,MAAQnB,EAEQ,iBAAVa,EACT,MAAM,IAAIT,MAAM,iBAIlB,GAFAnB,KAAKmC,OAASP,EAEU,iBAAbK,EACT,MAAM,IAAId,MAAM,2BAIlB,GAFAnB,KAAKoC,UAAYH,EAEO,iBAAbM,GAAyBE,OAAOC,MAAM,IAAIrD,KAAKkD,GAAUI,WAClE,MAAM,IAAIxB,MAAM,oBAElBnB,KAAKqC,UAAYE,EAEjB,MAAMK,EAAMJ,GAASI,KAAOpD,EAC5B,GAAmB,iBAARoD,EACT,MAAM,IAAIzB,MAAM,eAElBnB,KAAKsC,KAAOM,CACd,CAEI7B,WACF,OAAOf,KAAKkC,KACd,CAEIN,YACF,OAAO5B,KAAKmC,MACd,CAEIF,eACF,OAAOjC,KAAKoC,SACd,CAKIG,eACF,OAAOvC,KAAKqC,SACd,CAGIO,UACF,OAAO5C,KAAKsC,IACd,CAQAO,cAAed,GACb,OAAOe,EAAKC,OAAO,CACjBhC,KAAMgB,EAASG,MAAM/B,WACrByB,MAAOG,EAASI,OAChBF,SAAUF,EAASK,UACnBG,SAAUR,EAASM,aACE,MAAjBN,EAASO,KAAe,CAAEM,IAAKb,EAASO,MAAS,CAAA,GAEzD,CASAO,cAAe3C,GACb,MAAM8C,EAAMF,EAAKxB,OAAOpB,GAClBa,EAAOD,EAAMkC,EAAIjC,MACvB,OAAO,IAAIc,EAASd,EAAMiC,EAAIpB,MAAOqB,OAAOD,EAAIf,UAAWe,EAAIT,SAAU,CAAEK,IAAgB,MAAXI,EAAIJ,IAAcK,OAAOD,EAAIJ,UAAOM,GACtH,EAYKvC,eAAewC,EAASpB,EAAoBrB,EAAiB0C,EAAyB3D,GAC3F,MAAM4D,EAAM,IAAIC,IAAI,QAAQvB,EAAShB,KAAKZ,aAAciD,EAAQG,UAC1DC,QAAcC,EAAKC,iBACvBhD,EACAqB,EAASH,MACTG,EAASE,SACT,IAAI5C,KAAK0C,EAASQ,UAAUI,UAAYtD,KAAKC,MAC7C,CAAEqE,MAAO5B,EAASa,YAEdQ,EAAQQ,yBAERC,EAAiBC,MAAMT,EAAIlD,WAAY,CAC3C4D,OAAQ,OACRC,KAAMC,EAAUC,WAAWT,EAAKU,kBAAkBX,MAEtD,CAQO7C,eAAeyD,EAASrD,EAAYqC,EAAyB3D,GAClE,MAAM4D,EAAM,IAAIC,IAAI,QAAQvC,EAAKZ,aAAciD,EAAQG,gBACjDH,EAAQQ,mBAEd,MAAMS,QAAiCR,EAAiBC,MAAMT,EAAIlD,cAC5DmE,OAAEA,SAAiBD,EAAIE,OAEvBrE,EAAQ+D,EAAUO,WAAWF,GAC7Bd,EAAQC,EAAKgB,oBAAoBvE,GACjCc,EAASC,EAAIK,OAAOP,EAAKb,OACzBH,EAASqB,EAAsBC,EAAOC,OAAON,EAAOO,UAAUrB,OAAOA,OAI3E,aAFMwE,EAAS3E,EAAQG,GAEhB,IAAI2B,EACTd,EACAyC,EAAM5B,MACN4B,EAAMvB,SACNuB,EAAMjB,SACN,CAAEK,IAAKY,EAAMZ,KAEjB,CAEAjC,eAAekD,EAAkBc,GAC/B,MAAMN,QAAYM,EAClB,GAAIN,EAAIO,GAAI,OAAOP,EACnB,IAAIQ,EACJ,IACE,MAAMC,QAAoBT,EAAIE,OAC9BM,EAAQ,IAAI1D,MAAM2D,EAAYC,QAG/B,CAFC,MAAOC,GACPH,EAAQ,IAAI1D,MAAM,+EAA+EkD,EAAIY,SACtG,CACD,MAAMJ,CACR"}