├── FUNDING.json ├── .aegir.js ├── typedoc.json ├── CODE_OF_CONDUCT.md ├── .gitignore ├── tsconfig.json ├── test ├── fixtures │ ├── k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record │ ├── k51qzi5uqu5dm4tm0wt8srkg9h9suud4wuiwjimndrkydqm81cqtlb5ak6p7ku_v1.ipns-record │ ├── k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w_v1-v2.ipns-record │ ├── k51qzi5uqu5dlmit2tuwdvnx4sbnyqgmvbxftl0eo3f33wwtb9gr7yozae9kpw_v1-v2-broken-v1-value.ipns-record │ ├── k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c_v1-v2-broken-signature-v2.ipns-record │ ├── k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record │ └── records.ts ├── selector.spec.ts ├── conformance.spec.ts ├── utils.spec.ts ├── validator.spec.ts └── index.spec.ts ├── .github ├── workflows │ ├── semantic-pull-request.yml │ ├── stale.yml │ ├── generated-pr.yml │ └── js-test-and-release.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── open_an_issue.md ├── dependabot.yml └── config.yml ├── LICENSE-MIT ├── src ├── pb │ ├── ipns.proto │ └── ipns.ts ├── selector.ts ├── errors.ts ├── validator.ts ├── utils.ts └── index.ts ├── package.json ├── README.md ├── LICENSE-APACHE └── CHANGELOG.md /FUNDING.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0x7f330267969cf845a983a9d4e7b7dbcca5c700a5191269af377836d109e0bb69" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.aegir.js: -------------------------------------------------------------------------------- 1 | 2 | /** @type {import('aegir').PartialOptions} */ 3 | export default { 4 | build: { 5 | bundlesizeMax: '60KB' 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "readme": "none", 3 | "entryPoints": [ 4 | "./src/index.ts", 5 | "./src/selector.ts", 6 | "./src/validator.ts" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | This project follows the [`IPFS Community Code of Conduct`](https://github.com/ipfs/community/blob/master/code-of-conduct.md) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | .docs 5 | .coverage 6 | node_modules 7 | package-lock.json 8 | yarn.lock 9 | .vscode 10 | .tmp-compiled-docs 11 | tsconfig-doc-check.aegir.json 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "aegir/src/config/tsconfig.aegir.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src", 8 | "test" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipfs/js-ipns/HEAD/test/fixtures/k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record -------------------------------------------------------------------------------- /test/fixtures/k51qzi5uqu5dm4tm0wt8srkg9h9suud4wuiwjimndrkydqm81cqtlb5ak6p7ku_v1.ipns-record: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipfs/js-ipns/HEAD/test/fixtures/k51qzi5uqu5dm4tm0wt8srkg9h9suud4wuiwjimndrkydqm81cqtlb5ak6p7ku_v1.ipns-record -------------------------------------------------------------------------------- /test/fixtures/k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w_v1-v2.ipns-record: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipfs/js-ipns/HEAD/test/fixtures/k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w_v1-v2.ipns-record -------------------------------------------------------------------------------- /test/fixtures/k51qzi5uqu5dlmit2tuwdvnx4sbnyqgmvbxftl0eo3f33wwtb9gr7yozae9kpw_v1-v2-broken-v1-value.ipns-record: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipfs/js-ipns/HEAD/test/fixtures/k51qzi5uqu5dlmit2tuwdvnx4sbnyqgmvbxftl0eo3f33wwtb9gr7yozae9kpw_v1-v2-broken-v1-value.ipns-record -------------------------------------------------------------------------------- /test/fixtures/k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c_v1-v2-broken-signature-v2.ipns-record: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipfs/js-ipns/HEAD/test/fixtures/k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c_v1-v2-broken-signature-v2.ipns-record -------------------------------------------------------------------------------- /test/fixtures/k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipfs/js-ipns/HEAD/test/fixtures/k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record -------------------------------------------------------------------------------- /.github/workflows/semantic-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Semantic PR 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-semantic-pull-request.yml@v1 13 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close Stale Issues 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-stale-issue.yml@v1 15 | -------------------------------------------------------------------------------- /.github/workflows/generated-pr.yml: -------------------------------------------------------------------------------- 1 | name: Close Generated PRs 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-generated-pr.yml@v1 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Getting Help on IPFS 4 | url: https://ipfs.io/help 5 | about: All information about how and where to get help on IPFS. 6 | - name: IPFS Official Forum 7 | url: https://discuss.ipfs.io 8 | about: Please post general questions, support requests, and discussions here. 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 20 9 | commit-message: 10 | prefix: "deps" 11 | prefix-development: "chore" 12 | groups: 13 | interplanetary-deps: # Helia/libp2p deps 14 | patterns: 15 | - "*helia*" 16 | - "*libp2p*" 17 | - "*multiformats*" 18 | - "*blockstore*" 19 | - "*datastore*" 20 | kubo-deps: # kubo deps 21 | patterns: 22 | - "*kubo*" 23 | - "ipfsd-ctl" 24 | - package-ecosystem: "github-actions" 25 | directory: "/" 26 | schedule: 27 | interval: "weekly" 28 | commit-message: 29 | prefix: chore 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/open_an_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Open an issue 3 | about: Only for actionable issues relevant to this repository. 4 | title: '' 5 | labels: need/triage 6 | assignees: '' 7 | 8 | --- 9 | 20 | -------------------------------------------------------------------------------- /.github/workflows/js-test-and-release.yml: -------------------------------------------------------------------------------- 1 | name: test & maybe release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: write 12 | id-token: write 13 | packages: write 14 | pull-requests: write 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | js-test-and-release: 22 | uses: ipdxco/unified-github-workflows/.github/workflows/js-test-and-release.yml@v1.0 23 | secrets: 24 | DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} 25 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | UCI_GITHUB_TOKEN: ${{ secrets.UCI_GITHUB_TOKEN }} 28 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 29 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/pb/ipns.proto: -------------------------------------------------------------------------------- 1 | // https://github.com/ipfs/boxo/blob/main/ipns/pb/record.proto 2 | 3 | syntax = "proto3"; 4 | 5 | message IpnsEntry { 6 | enum ValidityType { 7 | // setting an EOL says "this record is valid until..." 8 | EOL = 0; 9 | } 10 | 11 | // legacy V1 copy of data[Value] 12 | optional bytes value = 1; 13 | 14 | // legacy V1 field, verify 'signatureV2' instead 15 | optional bytes signatureV1 = 2; 16 | 17 | // legacy V1 copies of data[ValidityType] and data[Validity] 18 | optional ValidityType validityType = 3; 19 | optional bytes validity = 4; 20 | 21 | // legacy V1 copy of data[Sequence] 22 | optional uint64 sequence = 5; 23 | 24 | // legacy V1 copy copy of data[TTL] 25 | optional uint64 ttl = 6; 26 | 27 | // Optional Public Key to be used for signature verification. 28 | // Used for big keys such as old RSA keys. Including the public key as part of 29 | // the record itself makes it verifiable in offline mode, without any additional lookup. 30 | // For newer Ed25519 keys, the public key is small enough that it can be embedded in the 31 | // IPNS Name itself, making this field unnecessary. 32 | optional bytes pubKey = 7; 33 | 34 | // (mandatory V2) signature of the IPNS record 35 | optional bytes signatureV2 = 8; 36 | 37 | // (mandatory V2) extensible record data in DAG-CBOR format 38 | optional bytes data = 9; 39 | } 40 | -------------------------------------------------------------------------------- /test/fixtures/records.ts: -------------------------------------------------------------------------------- 1 | import { peerIdFromString } from '@libp2p/peer-id' 2 | 3 | export const kuboRecord = { 4 | bytes: Uint8Array.from([10, 52, 47, 105, 112, 102, 115, 47, 81, 109, 97, 52, 115, 87, 121, 111, 84, 105, 74, 75, 89, 120, 49, 119, 84, 106, 107, 120, 87, 89, 55, 49, 100, 89, 103, 49, 111, 87, 103, 69, 55, 83, 69, 57, 111, 105, 84, 71, 113, 71, 110, 121, 111, 82, 18, 64, 178, 225, 212, 157, 188, 23, 25, 166, 9, 89, 255, 63, 227, 160, 140, 70, 192, 237, 178, 167, 94, 6, 112, 184, 106, 130, 89, 252, 141, 158, 84, 53, 65, 125, 253, 93, 255, 17, 28, 93, 9, 176, 232, 89, 51, 118, 104, 236, 126, 137, 136, 72, 0, 127, 101, 88, 178, 83, 115, 6, 30, 28, 140, 5, 24, 0, 34, 27, 50, 48, 50, 52, 45, 48, 49, 45, 49, 57, 84, 49, 54, 58, 51, 51, 58, 50, 48, 46, 56, 53, 56, 50, 48, 57, 90, 40, 0, 48, 128, 192, 226, 133, 227, 104, 66, 64, 133, 91, 52, 64, 253, 186, 129, 154, 218, 85, 188, 18, 104, 96, 180, 216, 254, 176, 210, 145, 130, 209, 176, 150, 134, 33, 59, 197, 162, 193, 15, 252, 71, 190, 240, 25, 3, 169, 60, 24, 236, 68, 218, 171, 61, 235, 157, 73, 215, 0, 51, 52, 24, 195, 90, 158, 245, 199, 172, 204, 12, 249, 89, 7, 74, 136, 1, 165, 99, 84, 84, 76, 27, 0, 0, 3, 70, 48, 184, 160, 0, 101, 86, 97, 108, 117, 101, 88, 52, 47, 105, 112, 102, 115, 47, 81, 109, 97, 52, 115, 87, 121, 111, 84, 105, 74, 75, 89, 120, 49, 119, 84, 106, 107, 120, 87, 89, 55, 49, 100, 89, 103, 49, 111, 87, 103, 69, 55, 83, 69, 57, 111, 105, 84, 71, 113, 71, 110, 121, 111, 82, 104, 83, 101, 113, 117, 101, 110, 99, 101, 0, 104, 86, 97, 108, 105, 100, 105, 116, 121, 88, 27, 50, 48, 50, 52, 45, 48, 49, 45, 49, 57, 84, 49, 54, 58, 51, 51, 58, 50, 48, 46, 56, 53, 56, 50, 48, 57, 90, 108, 86, 97, 108, 105, 100, 105, 116, 121, 84, 121, 112, 101, 0]), 5 | peerId: peerIdFromString('12D3KooWBT21CjaZgY3MvoFFwRJLBEqgk7zwa294Boh9wdX2RUX2'), 6 | value: '/ipfs/Qma4sWyoTiJKYx1wTjkxWY71dYg1oWgE7SE9oiTGqGnyoR' 7 | } 8 | -------------------------------------------------------------------------------- /src/selector.ts: -------------------------------------------------------------------------------- 1 | import NanoDate from 'timestamp-nano' 2 | import { IpnsEntry } from './pb/ipns.js' 3 | import { unmarshalIPNSRecord } from './utils.js' 4 | 5 | /** 6 | * Selects the latest valid IPNS record from an array of marshalled IPNS records. 7 | * 8 | * Records are sorted by: 9 | * 1. Sequence number (higher takes precedence) 10 | * 2. Validity time for EOL records with same sequence number (longer lived record takes precedence) 11 | * 12 | * @param key - The routing key for the IPNS record 13 | * @param data - Array of marshalled IPNS records to select from 14 | * @returns The index of the most valid record from the input array 15 | */ 16 | export function ipnsSelector (key: Uint8Array, data: Uint8Array[]): number { 17 | const entries = data.map((buf, index) => ({ 18 | record: unmarshalIPNSRecord(buf), 19 | index 20 | })) 21 | 22 | entries.sort((a, b) => { 23 | // Before we'd sort based on the signature version. Unmarshal now fails if 24 | // a record does not have SignatureV2, so that is no longer needed. V1-only 25 | // records haven't been issues in a long time. 26 | 27 | const aSeq = a.record.sequence 28 | const bSeq = b.record.sequence 29 | 30 | // choose later sequence number 31 | if (aSeq > bSeq) { 32 | return -1 33 | } else if (aSeq < bSeq) { 34 | return 1 35 | } 36 | 37 | if (a.record.validityType === IpnsEntry.ValidityType.EOL && b.record.validityType === IpnsEntry.ValidityType.EOL) { 38 | // choose longer lived record if sequence numbers the same 39 | const recordAValidityDate = NanoDate.fromString(a.record.validity).toDate() 40 | const recordBValidityDate = NanoDate.fromString(b.record.validity).toDate() 41 | 42 | if (recordAValidityDate.getTime() > recordBValidityDate.getTime()) { 43 | return -1 44 | } 45 | 46 | if (recordAValidityDate.getTime() < recordBValidityDate.getTime()) { 47 | return 1 48 | } 49 | } 50 | 51 | return 0 52 | }) 53 | 54 | return entries[0].index 55 | } 56 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export class SignatureCreationError extends Error { 2 | static name = 'SignatureCreationError' 3 | 4 | constructor (message = 'Record signature creation failed') { 5 | super(message) 6 | this.name = 'SignatureCreationError' 7 | } 8 | } 9 | 10 | export class SignatureVerificationError extends Error { 11 | static name = 'SignatureVerificationError' 12 | 13 | constructor (message = 'Record signature verification failed') { 14 | super(message) 15 | this.name = 'SignatureVerificationError' 16 | } 17 | } 18 | 19 | export class RecordExpiredError extends Error { 20 | static name = 'RecordExpiredError' 21 | 22 | constructor (message = 'Record has expired') { 23 | super(message) 24 | this.name = 'RecordExpiredError' 25 | } 26 | } 27 | 28 | export class UnsupportedValidityError extends Error { 29 | static name = 'UnsupportedValidityError' 30 | 31 | constructor (message = 'The validity type is unsupported') { 32 | super(message) 33 | this.name = 'UnsupportedValidityError' 34 | } 35 | } 36 | 37 | export class RecordTooLargeError extends Error { 38 | static name = 'RecordTooLargeError' 39 | 40 | constructor (message = 'The record is too large') { 41 | super(message) 42 | this.name = 'RecordTooLargeError' 43 | } 44 | } 45 | 46 | export class InvalidValueError extends Error { 47 | static name = 'InvalidValueError' 48 | 49 | constructor (message = 'Value must be a valid content path starting with /') { 50 | super(message) 51 | this.name = 'InvalidValueError' 52 | } 53 | } 54 | 55 | export class InvalidRecordDataError extends Error { 56 | static name = 'InvalidRecordDataError' 57 | 58 | constructor (message = 'Invalid record data') { 59 | super(message) 60 | this.name = 'InvalidRecordDataError' 61 | } 62 | } 63 | 64 | export class InvalidEmbeddedPublicKeyError extends Error { 65 | static name = 'InvalidEmbeddedPublicKeyError' 66 | 67 | constructor (message = 'Invalid embedded public key') { 68 | super(message) 69 | this.name = 'InvalidEmbeddedPublicKeyError' 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/selector.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import { generateKeyPair } from '@libp2p/crypto/keys' 4 | import { expect } from 'aegir/chai' 5 | import { createIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey } from '../src/index.js' 6 | import { ipnsSelector } from '../src/selector.js' 7 | import type { PrivateKey } from '@libp2p/interface' 8 | 9 | describe('selector', function () { 10 | this.timeout(20 * 1000) 11 | 12 | const contentPath = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' 13 | let privateKey: PrivateKey 14 | 15 | before(async () => { 16 | privateKey = await generateKeyPair('RSA', 2048) 17 | }) 18 | 19 | it('should use validator.select to select the record with the highest sequence number', async () => { 20 | const sequence = 0 21 | const lifetime = 1000000 22 | 23 | const record = await createIPNSRecord(privateKey, contentPath, sequence, lifetime) 24 | const newRecord = await createIPNSRecord(privateKey, contentPath, (sequence + 1), lifetime) 25 | 26 | const marshalledData = marshalIPNSRecord(record) 27 | const marshalledNewData = marshalIPNSRecord(newRecord) 28 | 29 | const key = multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()) 30 | 31 | let valid = ipnsSelector(key, [marshalledNewData, marshalledData]) 32 | expect(valid).to.equal(0) // new data is the selected one 33 | 34 | valid = ipnsSelector(key, [marshalledData, marshalledNewData]) 35 | expect(valid).to.equal(1) // new data is the selected one 36 | }) 37 | 38 | it('should use validator.select to select the record with the longest validity', async () => { 39 | const sequence = 0 40 | const lifetime = 1000000 41 | 42 | const record = await createIPNSRecord(privateKey, contentPath, sequence, lifetime) 43 | const newRecord = await createIPNSRecord(privateKey, contentPath, sequence, (lifetime + 1)) 44 | 45 | const marshalledData = marshalIPNSRecord(record) 46 | const marshalledNewData = marshalIPNSRecord(newRecord) 47 | 48 | const key = multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()) 49 | 50 | let valid = ipnsSelector(key, [marshalledNewData, marshalledData]) 51 | expect(valid).to.equal(0) // new data is the selected one 52 | 53 | valid = ipnsSelector(key, [marshalledData, marshalledNewData]) 54 | expect(valid).to.equal(1) // new data is the selected one 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # Configuration for welcome - https://github.com/behaviorbot/welcome 2 | 3 | # Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome 4 | # Comment to be posted to on first time issues 5 | newIssueWelcomeComment: > 6 | Thank you for submitting your first issue to this repository! A maintainer 7 | will be here shortly to triage and review. 8 | 9 | In the meantime, please double-check that you have provided all the 10 | necessary information to make this process easy! Any information that can 11 | help save additional round trips is useful! We currently aim to give 12 | initial feedback within **two business days**. If this does not happen, feel 13 | free to leave a comment. 14 | 15 | Please keep an eye on how this issue will be labeled, as labels give an 16 | overview of priorities, assignments and additional actions requested by the 17 | maintainers: 18 | 19 | - "Priority" labels will show how urgent this is for the team. 20 | - "Status" labels will show if this is ready to be worked on, blocked, or in progress. 21 | - "Need" labels will indicate if additional input or analysis is required. 22 | 23 | Finally, remember to use https://discuss.ipfs.io if you just need general 24 | support. 25 | 26 | # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome 27 | # Comment to be posted to on PRs from first time contributors in your repository 28 | newPRWelcomeComment: > 29 | Thank you for submitting this PR! 30 | 31 | A maintainer will be here shortly to review it. 32 | 33 | We are super grateful, but we are also overloaded! Help us by making sure 34 | that: 35 | 36 | * The context for this PR is clear, with relevant discussion, decisions 37 | and stakeholders linked/mentioned. 38 | 39 | * Your contribution itself is clear (code comments, self-review for the 40 | rest) and in its best form. Follow the [code contribution 41 | guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md#code-contribution-guidelines) 42 | if they apply. 43 | 44 | Getting other community members to do a review would be great help too on 45 | complex PRs (you can ask in the chats/forums). If you are unsure about 46 | something, just leave us a comment. 47 | 48 | Next steps: 49 | 50 | * A maintainer will triage and assign priority to this PR, commenting on 51 | any missing things and potentially assigning a reviewer for high 52 | priority items. 53 | 54 | * The PR gets reviews, discussed and approvals as needed. 55 | 56 | * The PR is merged by maintainers when it has been approved and comments addressed. 57 | 58 | We currently aim to provide initial feedback/triaging within **two business 59 | days**. Please keep an eye on any labelling actions, as these will indicate 60 | priorities and status of your contribution. 61 | 62 | We are very grateful for your contribution! 63 | 64 | 65 | # Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge 66 | # Comment to be posted to on pull requests merged by a first time user 67 | # Currently disabled 68 | #firstPRMergeComment: "" 69 | -------------------------------------------------------------------------------- /src/pb/ipns.ts: -------------------------------------------------------------------------------- 1 | import { decodeMessage, encodeMessage, enumeration, message } from 'protons-runtime' 2 | import type { Codec, DecodeOptions } from 'protons-runtime' 3 | import type { Uint8ArrayList } from 'uint8arraylist' 4 | 5 | export interface IpnsEntry { 6 | value?: Uint8Array 7 | signatureV1?: Uint8Array 8 | validityType?: IpnsEntry.ValidityType 9 | validity?: Uint8Array 10 | sequence?: bigint 11 | ttl?: bigint 12 | pubKey?: Uint8Array 13 | signatureV2?: Uint8Array 14 | data?: Uint8Array 15 | } 16 | 17 | export namespace IpnsEntry { 18 | export enum ValidityType { 19 | EOL = 'EOL' 20 | } 21 | 22 | enum __ValidityTypeValues { 23 | EOL = 0 24 | } 25 | 26 | export namespace ValidityType { 27 | export const codec = (): Codec => { 28 | return enumeration(__ValidityTypeValues) 29 | } 30 | } 31 | 32 | let _codec: Codec 33 | 34 | export const codec = (): Codec => { 35 | if (_codec == null) { 36 | _codec = message((obj, w, opts = {}) => { 37 | if (opts.lengthDelimited !== false) { 38 | w.fork() 39 | } 40 | 41 | if (obj.value != null) { 42 | w.uint32(10) 43 | w.bytes(obj.value) 44 | } 45 | 46 | if (obj.signatureV1 != null) { 47 | w.uint32(18) 48 | w.bytes(obj.signatureV1) 49 | } 50 | 51 | if (obj.validityType != null) { 52 | w.uint32(24) 53 | IpnsEntry.ValidityType.codec().encode(obj.validityType, w) 54 | } 55 | 56 | if (obj.validity != null) { 57 | w.uint32(34) 58 | w.bytes(obj.validity) 59 | } 60 | 61 | if (obj.sequence != null) { 62 | w.uint32(40) 63 | w.uint64(obj.sequence) 64 | } 65 | 66 | if (obj.ttl != null) { 67 | w.uint32(48) 68 | w.uint64(obj.ttl) 69 | } 70 | 71 | if (obj.pubKey != null) { 72 | w.uint32(58) 73 | w.bytes(obj.pubKey) 74 | } 75 | 76 | if (obj.signatureV2 != null) { 77 | w.uint32(66) 78 | w.bytes(obj.signatureV2) 79 | } 80 | 81 | if (obj.data != null) { 82 | w.uint32(74) 83 | w.bytes(obj.data) 84 | } 85 | 86 | if (opts.lengthDelimited !== false) { 87 | w.ldelim() 88 | } 89 | }, (reader, length, opts = {}) => { 90 | const obj: any = {} 91 | 92 | const end = length == null ? reader.len : reader.pos + length 93 | 94 | while (reader.pos < end) { 95 | const tag = reader.uint32() 96 | 97 | switch (tag >>> 3) { 98 | case 1: { 99 | obj.value = reader.bytes() 100 | break 101 | } 102 | case 2: { 103 | obj.signatureV1 = reader.bytes() 104 | break 105 | } 106 | case 3: { 107 | obj.validityType = IpnsEntry.ValidityType.codec().decode(reader) 108 | break 109 | } 110 | case 4: { 111 | obj.validity = reader.bytes() 112 | break 113 | } 114 | case 5: { 115 | obj.sequence = reader.uint64() 116 | break 117 | } 118 | case 6: { 119 | obj.ttl = reader.uint64() 120 | break 121 | } 122 | case 7: { 123 | obj.pubKey = reader.bytes() 124 | break 125 | } 126 | case 8: { 127 | obj.signatureV2 = reader.bytes() 128 | break 129 | } 130 | case 9: { 131 | obj.data = reader.bytes() 132 | break 133 | } 134 | default: { 135 | reader.skipType(tag & 7) 136 | break 137 | } 138 | } 139 | } 140 | 141 | return obj 142 | }) 143 | } 144 | 145 | return _codec 146 | } 147 | 148 | export const encode = (obj: Partial): Uint8Array => { 149 | return encodeMessage(obj, IpnsEntry.codec()) 150 | } 151 | 152 | export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions): IpnsEntry => { 153 | return decodeMessage(buf, IpnsEntry.codec(), opts) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /test/conformance.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import { peerIdFromCID } from '@libp2p/peer-id' 4 | import { expect } from 'aegir/chai' 5 | import loadFixture from 'aegir/fixtures' 6 | import { base36 } from 'multiformats/bases/base36' 7 | import { CID } from 'multiformats/cid' 8 | import { SignatureVerificationError } from '../src/errors.js' 9 | import { marshalIPNSRecord, unmarshalIPNSRecord } from '../src/index.js' 10 | import { validate } from '../src/validator.js' 11 | 12 | describe('conformance', function () { 13 | it('should reject a v1 only record', async () => { 14 | const buf = loadFixture('test/fixtures/k51qzi5uqu5dm4tm0wt8srkg9h9suud4wuiwjimndrkydqm81cqtlb5ak6p7ku_v1.ipns-record') 15 | 16 | expect(() => unmarshalIPNSRecord(buf)).to.throw(/Missing data or signatureV2/) 17 | .with.property('name', SignatureVerificationError.name) 18 | }) 19 | 20 | it('should validate a record with v1 and v2 signatures', async () => { 21 | const buf = loadFixture('test/fixtures/k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w_v1-v2.ipns-record') 22 | const record = unmarshalIPNSRecord(buf) 23 | 24 | const cid = CID.parse('k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w', base36) 25 | const peerId = peerIdFromCID(cid) 26 | 27 | if (peerId.publicKey == null) { 28 | throw new Error('Peer ID embedded in CID had no public key') 29 | } 30 | 31 | const publicKey = peerId.publicKey 32 | await validate(publicKey, buf) 33 | 34 | expect(record.value).to.equal('/ipfs/bafkqaddwgevxmmraojswg33smq') 35 | }) 36 | 37 | it('should reject a record with inconsistent value fields', async () => { 38 | const buf = loadFixture('test/fixtures/k51qzi5uqu5dlmit2tuwdvnx4sbnyqgmvbxftl0eo3f33wwtb9gr7yozae9kpw_v1-v2-broken-v1-value.ipns-record') 39 | 40 | expect(() => unmarshalIPNSRecord(buf)).to.throw(/Field "value" did not match/) 41 | .with.property('name', SignatureVerificationError.name) 42 | }) 43 | 44 | it('should reject a record with v1 and v2 signatures but invalid v2', async () => { 45 | const buf = loadFixture('test/fixtures/k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c_v1-v2-broken-signature-v2.ipns-record') 46 | const cid = CID.parse('k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c', base36) 47 | const peerId = peerIdFromCID(cid) 48 | 49 | if (peerId.publicKey == null) { 50 | throw new Error('Peer ID embedded in CID had no public key') 51 | } 52 | 53 | const publicKey = peerId.publicKey 54 | 55 | await expect(validate(publicKey, buf)).to.eventually.be.rejectedWith(/Record signature verification failed/) 56 | .with.property('name', SignatureVerificationError.name) 57 | }) 58 | 59 | it('should reject a record with v1 and v2 signatures but invalid v1', async () => { 60 | const buf = loadFixture('test/fixtures/k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record') 61 | const record = unmarshalIPNSRecord(buf) 62 | 63 | expect(record.value).to.equal('/ipfs/bafkqahtwgevxmmrao5uxi2bamjzg623fnyqhg2lhnzqxi5lsmuqhmmi') 64 | }) 65 | 66 | it('should validate a record with only v2 signature', async () => { 67 | const buf = loadFixture('test/fixtures/k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record') 68 | const record = unmarshalIPNSRecord(buf) 69 | 70 | const cid = CID.parse('k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f', base36) 71 | const peerId = peerIdFromCID(cid) 72 | 73 | if (peerId.publicKey == null) { 74 | throw new Error('Peer ID embedded in CID had no public key') 75 | } 76 | 77 | const publicKey = peerId.publicKey 78 | await validate(publicKey, buf) 79 | 80 | expect(record.value).to.equal('/ipfs/bafkqadtwgiww63tmpeqhezldn5zgi') 81 | }) 82 | 83 | it('should round trip fixtures', () => { 84 | const fixtures = [ 85 | 'test/fixtures/k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w_v1-v2.ipns-record', 86 | 'test/fixtures/k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c_v1-v2-broken-signature-v2.ipns-record', 87 | 'test/fixtures/k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record', 88 | 'test/fixtures/k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record' 89 | ] 90 | 91 | for (const fixture of fixtures) { 92 | const buf = loadFixture(fixture) 93 | const record = unmarshalIPNSRecord(buf) 94 | const marshalled = marshalIPNSRecord(record) 95 | 96 | expect(buf).to.equalBytes(marshalled) 97 | } 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /src/validator.ts: -------------------------------------------------------------------------------- 1 | import { publicKeyFromMultihash } from '@libp2p/crypto/keys' 2 | import { logger } from '@libp2p/logger' 3 | import NanoDate from 'timestamp-nano' 4 | import { equals as uint8ArrayEquals } from 'uint8arrays/equals' 5 | import { InvalidEmbeddedPublicKeyError, RecordExpiredError, RecordTooLargeError, SignatureVerificationError, UnsupportedValidityError } from './errors.js' 6 | import { IpnsEntry } from './pb/ipns.js' 7 | import { extractPublicKeyFromIPNSRecord, ipnsRecordDataForV2Sig, isCodec, multihashFromIPNSRoutingKey, multihashToIPNSRoutingKey, unmarshalIPNSRecord } from './utils.js' 8 | import type { IPNSRecord } from './index.js' 9 | import type { PublicKey } from '@libp2p/interface' 10 | 11 | const log = logger('ipns:validator') 12 | 13 | /** 14 | * Limit valid IPNS record sizes to 10kb 15 | */ 16 | const MAX_RECORD_SIZE = 1024 * 10 17 | 18 | /** 19 | * Validates the given IPNS Record against the given public key. We need a "raw" 20 | * record in order to be able to access to all of its fields. 21 | */ 22 | export async function validate (publicKey: PublicKey, marshalledRecord: Uint8Array): Promise { 23 | // unmarshal ensures that (1) SignatureV2 and Data are present, (2) that ValidityType 24 | // and Validity are of valid types and have a value, (3) that CBOR data matches protobuf 25 | // if it's a V1+V2 record. 26 | const record = unmarshalIPNSRecord(marshalledRecord) 27 | 28 | // Validate Signature V2 29 | let isValid 30 | 31 | try { 32 | const dataForSignature = ipnsRecordDataForV2Sig(record.data) 33 | isValid = await publicKey.verify(dataForSignature, record.signatureV2) 34 | } catch (err) { 35 | isValid = false 36 | } 37 | 38 | if (!isValid) { 39 | log.error('record signature verification failed') 40 | throw new SignatureVerificationError('Record signature verification failed') 41 | } 42 | 43 | // Validate according to the validity type 44 | if (record.validityType === IpnsEntry.ValidityType.EOL) { 45 | if (NanoDate.fromString(record.validity).toDate().getTime() < Date.now()) { 46 | log.error('record has expired') 47 | throw new RecordExpiredError('record has expired') 48 | } 49 | } else if (record.validityType != null) { 50 | log.error('the validity type is unsupported') 51 | throw new UnsupportedValidityError('The validity type is unsupported') 52 | } 53 | 54 | log('ipns record for %s is valid', record.value) 55 | } 56 | 57 | /** 58 | * Validate the given IPNS record against the given routing key. 59 | * 60 | * @see https://specs.ipfs.tech/ipns/ipns-record/#routing-record for the binary format of the routing key 61 | * 62 | * @param routingKey - The routing key in binary format: binary(ascii(IPNS_PREFIX) + multihash(public key)) 63 | * @param marshalledRecord - The marshalled record to validate. 64 | */ 65 | export async function ipnsValidator (routingKey: Uint8Array, marshalledRecord: Uint8Array): Promise { 66 | if (marshalledRecord.byteLength > MAX_RECORD_SIZE) { 67 | throw new RecordTooLargeError('The record is too large') 68 | } 69 | 70 | // try to extract public key from routing key 71 | const routingMultihash = multihashFromIPNSRoutingKey(routingKey) 72 | let routingPubKey: PublicKey | undefined 73 | 74 | // identity hash 75 | if (isCodec(routingMultihash, 0x0)) { 76 | routingPubKey = publicKeyFromMultihash(routingMultihash) 77 | } 78 | 79 | // extract public key from record 80 | const receivedRecord = unmarshalIPNSRecord(marshalledRecord) 81 | const recordPubKey = extractPublicKeyFromIPNSRecord(receivedRecord) ?? routingPubKey 82 | 83 | if (recordPubKey == null) { 84 | throw new InvalidEmbeddedPublicKeyError('Could not extract public key from IPNS record or routing key') 85 | } 86 | 87 | const expectedRoutingKey = multihashToIPNSRoutingKey(recordPubKey.toMultihash()) 88 | 89 | if (!uint8ArrayEquals(expectedRoutingKey, routingKey)) { 90 | throw new InvalidEmbeddedPublicKeyError('Embedded public key did not match routing key') 91 | } 92 | 93 | // Record validation 94 | await validate(recordPubKey, marshalledRecord) 95 | } 96 | 97 | /** 98 | * Returns the number of milliseconds until the record expires. 99 | * If the record is already expired, returns 0. 100 | * 101 | * @param record - The IPNS record to validate. 102 | * @returns The number of milliseconds until the record expires, or 0 if the record is already expired. 103 | */ 104 | export function validFor (record: IPNSRecord): number { 105 | if (record.validityType !== IpnsEntry.ValidityType.EOL) { 106 | throw new UnsupportedValidityError() 107 | } 108 | 109 | if (record.validity == null) { 110 | throw new UnsupportedValidityError() 111 | } 112 | 113 | const validUntil = NanoDate.fromString(record.validity).toDate().getTime() 114 | const now = Date.now() 115 | 116 | if (validUntil < now) { 117 | return 0 118 | } 119 | 120 | return validUntil - now 121 | } 122 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ipns", 3 | "version": "10.1.3", 4 | "description": "IPNS record definitions", 5 | "author": "Vasco Santos ", 6 | "license": "Apache-2.0 OR MIT", 7 | "homepage": "https://github.com/ipfs/js-ipns#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/ipfs/js-ipns.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/ipfs/js-ipns/issues" 14 | }, 15 | "publishConfig": { 16 | "access": "public", 17 | "provenance": true 18 | }, 19 | "keywords": [ 20 | "ipfs", 21 | "ipns" 22 | ], 23 | "type": "module", 24 | "types": "./dist/src/index.d.ts", 25 | "typesVersions": { 26 | "*": { 27 | "*": [ 28 | "*", 29 | "dist/*", 30 | "dist/src/*", 31 | "dist/src/*/index" 32 | ], 33 | "src/*": [ 34 | "*", 35 | "dist/*", 36 | "dist/src/*", 37 | "dist/src/*/index" 38 | ] 39 | } 40 | }, 41 | "files": [ 42 | "src", 43 | "dist", 44 | "!dist/test", 45 | "!**/*.tsbuildinfo" 46 | ], 47 | "exports": { 48 | ".": { 49 | "types": "./dist/src/index.d.ts", 50 | "import": "./dist/src/index.js" 51 | }, 52 | "./selector": { 53 | "types": "./dist/src/selector.d.ts", 54 | "import": "./dist/src/selector.js" 55 | }, 56 | "./validator": { 57 | "types": "./dist/src/validator.d.ts", 58 | "import": "./dist/src/validator.js" 59 | } 60 | }, 61 | "release": { 62 | "branches": [ 63 | "main" 64 | ], 65 | "plugins": [ 66 | [ 67 | "@semantic-release/commit-analyzer", 68 | { 69 | "preset": "conventionalcommits", 70 | "releaseRules": [ 71 | { 72 | "breaking": true, 73 | "release": "major" 74 | }, 75 | { 76 | "revert": true, 77 | "release": "patch" 78 | }, 79 | { 80 | "type": "feat", 81 | "release": "minor" 82 | }, 83 | { 84 | "type": "fix", 85 | "release": "patch" 86 | }, 87 | { 88 | "type": "docs", 89 | "release": "patch" 90 | }, 91 | { 92 | "type": "test", 93 | "release": "patch" 94 | }, 95 | { 96 | "type": "deps", 97 | "release": "patch" 98 | }, 99 | { 100 | "scope": "no-release", 101 | "release": false 102 | } 103 | ] 104 | } 105 | ], 106 | [ 107 | "@semantic-release/release-notes-generator", 108 | { 109 | "preset": "conventionalcommits", 110 | "presetConfig": { 111 | "types": [ 112 | { 113 | "type": "feat", 114 | "section": "Features" 115 | }, 116 | { 117 | "type": "fix", 118 | "section": "Bug Fixes" 119 | }, 120 | { 121 | "type": "chore", 122 | "section": "Trivial Changes" 123 | }, 124 | { 125 | "type": "docs", 126 | "section": "Documentation" 127 | }, 128 | { 129 | "type": "deps", 130 | "section": "Dependencies" 131 | }, 132 | { 133 | "type": "test", 134 | "section": "Tests" 135 | } 136 | ] 137 | } 138 | } 139 | ], 140 | "@semantic-release/changelog", 141 | "@semantic-release/npm", 142 | "@semantic-release/github", 143 | [ 144 | "@semantic-release/git", 145 | { 146 | "assets": [ 147 | "CHANGELOG.md", 148 | "package.json" 149 | ] 150 | } 151 | ] 152 | ] 153 | }, 154 | "scripts": { 155 | "clean": "aegir clean", 156 | "lint": "aegir lint", 157 | "dep-check": "aegir dep-check", 158 | "doc-check": "aegir doc-check", 159 | "build": "aegir build", 160 | "test": "aegir test", 161 | "test:node": "aegir test -t node --cov", 162 | "test:chrome": "aegir test -t browser --cov", 163 | "test:chrome-webworker": "aegir test -t webworker", 164 | "test:firefox": "aegir test -t browser -- --browser firefox", 165 | "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", 166 | "test:electron-main": "aegir test -t electron-main", 167 | "release": "aegir release", 168 | "generate": "protons src/pb/ipns.proto", 169 | "docs": "NODE_OPTIONS=--max_old_space_size=8192 aegir docs", 170 | "docs:no-publish": "NODE_OPTIONS=--max_old_space_size=8192 aegir docs --publish false" 171 | }, 172 | "dependencies": { 173 | "@libp2p/crypto": "^5.0.0", 174 | "@libp2p/interface": "^3.0.2", 175 | "@libp2p/logger": "^6.0.4", 176 | "cborg": "^4.2.3", 177 | "interface-datastore": "^9.0.2", 178 | "multiformats": "^13.2.2", 179 | "protons-runtime": "^5.5.0", 180 | "timestamp-nano": "^1.0.1", 181 | "uint8arraylist": "^2.4.8", 182 | "uint8arrays": "^5.1.0" 183 | }, 184 | "devDependencies": { 185 | "@libp2p/peer-id": "^6.0.3", 186 | "aegir": "^47.0.20", 187 | "protons": "^7.6.0" 188 | }, 189 | "sideEffects": false 190 | } 191 | -------------------------------------------------------------------------------- /test/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { peerIdFromString } from '@libp2p/peer-id' 2 | import { expect } from 'aegir/chai' 3 | import { CID } from 'multiformats/cid' 4 | import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' 5 | import { normalizeValue, multihashFromIPNSRoutingKey, multihashToIPNSRoutingKey, normalizeByteValue } from '../src/utils.js' 6 | import type { PeerId } from '@libp2p/interface' 7 | 8 | describe('utils', () => { 9 | describe('normalizeValue', () => { 10 | const cases: Record = { 11 | // CID input 12 | 'v0 CID': { 13 | input: CID.parse('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq'), 14 | output: '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua' 15 | }, 16 | 'v1 CID': { 17 | input: CID.parse('bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua'), 18 | output: '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua' 19 | }, 20 | 'v1 Libp2p Key CID': { 21 | input: CID.parse('bafzaajqaeqeaceralaazlm56u23dyhpm7ztoo5x4dcus2ghpqwedhoezk4h6yijbl6rq'), 22 | output: '/ipns/k73ap3wtp70r7cd9ofyhwgogv1j96huvtvfnsof5spyfaaopkxmonumi4fckgguqr' 23 | }, 24 | 25 | // path input 26 | '/ipfs/CID path': { 27 | input: '/ipfs/QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq/docs/readme.md', 28 | output: '/ipfs/QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq/docs/readme.md' 29 | }, 30 | '/ipns/CID path': { 31 | input: '/ipns/k51qzi5uqu5djni72pr40dt64kxlh0zb8baat8h7dtdvkov66euc2lho0oidr3', 32 | output: '/ipns/k51qzi5uqu5djni72pr40dt64kxlh0zb8baat8h7dtdvkov66euc2lho0oidr3' 33 | }, 34 | 35 | // peer id input 36 | 'Ed25519 PeerId': { 37 | input: peerIdFromString('12D3KooWKBpVwnRACfEsk6QME7dA5CZnFYVHQ7Zc927BEzuUekQe'), 38 | output: '/ipns/k51qzi5uqu5djni72pr40dt64kxlh0zb8baat8h7dtdvkov66euc2lho0oidr3' 39 | }, 40 | 'secp256k1 PeerId': { 41 | input: peerIdFromString('16Uiu2HAkyBsAs6fPyJYVNq3pUDFxyFnUPTQYL2JpLMEViMUwEnp2'), 42 | output: '/ipns/kzwfwjn5ji4pul2d7gonydo4rtncuequd647001hm1afmxxvhfs1pz9ckfrc1c3' 43 | }, 44 | 'RSA PeerId': { 45 | input: peerIdFromString('QmPofjNRgPN3ndH5RbcSr3X5EekvpCRsUw1E8ji8kJaQJa'), 46 | output: '/ipns/k2k4r8jyk192oxg2e40x4av8re5e6frptftrwrhu6k1ia6whqsew13f3' 47 | }, 48 | 49 | // string input 50 | 'string path': { 51 | input: '/hello', 52 | output: '/hello' 53 | } 54 | } 55 | 56 | Object.entries(cases).forEach(([name, { input, output }]) => { 57 | it(`should normalize a ${name}`, async () => { 58 | expect(normalizeValue(await input)).to.equal(output) 59 | }) 60 | }) 61 | }) 62 | 63 | describe('normalizeByteValue', () => { 64 | const cases: Record = { 65 | // Uint8Array input 66 | 'v0 CID bytes': { 67 | input: CID.parse('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq').bytes, 68 | output: '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua' 69 | }, 70 | 'v1 CID bytes': { 71 | input: CID.parse('bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua').bytes, 72 | output: '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua' 73 | }, 74 | 'v1 Libp2p Key CID bytes': { 75 | input: CID.parse('bafzaajqaeqeaceralaazlm56u23dyhpm7ztoo5x4dcus2ghpqwedhoezk4h6yijbl6rq').bytes, 76 | output: '/ipfs/bafzaajqaeqeaceralaazlm56u23dyhpm7ztoo5x4dcus2ghpqwedhoezk4h6yijbl6rq' 77 | }, 78 | 'string path Uint8Array': { 79 | input: uint8ArrayFromString('/hello'), 80 | output: '/hello' 81 | }, 82 | 'IPFS path v0 CID Uint8Array': { 83 | input: uint8ArrayFromString('/ipfs/QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq'), 84 | output: '/ipfs/QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq' 85 | }, 86 | 'IPFS path v1 CID Uint8Array': { 87 | input: uint8ArrayFromString('/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua'), 88 | output: '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua' 89 | }, 90 | 'IPFS path v1 Libp2p Key CID Uint8Array': { 91 | input: uint8ArrayFromString('/ipfs/bafzaajqaeqeaceralaazlm56u23dyhpm7ztoo5x4dcus2ghpqwedhoezk4h6yijbl6rq'), 92 | output: '/ipfs/bafzaajqaeqeaceralaazlm56u23dyhpm7ztoo5x4dcus2ghpqwedhoezk4h6yijbl6rq' 93 | } 94 | } 95 | 96 | Object.entries(cases).forEach(([name, { input, output }]) => { 97 | it(`should normalize a ${name}`, () => { 98 | expect(normalizeByteValue(input)).to.equal(output) 99 | }) 100 | }) 101 | }) 102 | 103 | describe('routing keys', () => { 104 | const cases: Record = { 105 | Ed25519: peerIdFromString('12D3KooWKBpVwnRACfEsk6QME7dA5CZnFYVHQ7Zc927BEzuUekQe'), 106 | secp256k1: peerIdFromString('16Uiu2HAkyBsAs6fPyJYVNq3pUDFxyFnUPTQYL2JpLMEViMUwEnp2'), 107 | RSA: peerIdFromString('QmPofjNRgPN3ndH5RbcSr3X5EekvpCRsUw1E8ji8kJaQJa') 108 | } 109 | 110 | Object.entries(cases).forEach(([name, input]) => { 111 | it(`should round trip a ${name} key`, async () => { 112 | const key = multihashToIPNSRoutingKey(input.toMultihash()) 113 | const output = multihashFromIPNSRoutingKey(key) 114 | 115 | expect(input.toMultihash().bytes).to.equalBytes(output.bytes) 116 | }) 117 | }) 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ipns 2 | 3 | [![ipfs.tech](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](https://ipfs.tech) 4 | [![Discuss](https://img.shields.io/discourse/https/discuss.ipfs.tech/posts.svg?style=flat-square)](https://discuss.ipfs.tech) 5 | [![codecov](https://img.shields.io/codecov/c/github/ipfs/js-ipns.svg?style=flat-square)](https://codecov.io/gh/ipfs/js-ipns) 6 | [![CI](https://img.shields.io/github/actions/workflow/status/ipfs/js-ipns/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/js-ipns/actions/workflows/js-test-and-release.yml?query=branch%3Amain) 7 | 8 | > IPNS record definitions 9 | 10 | # About 11 | 12 | 26 | 27 | Implements parsing and serialization of [IPNS Records](https://specs.ipfs.tech/ipns/ipns-record/). 28 | 29 | ## Example - Create record 30 | 31 | ```TypeScript 32 | import { createIPNSRecord } from 'ipns' 33 | import { generateKeyPair } from '@libp2p/crypto/keys' 34 | 35 | const privateKey = await generateKeyPair('Ed25519') 36 | const value = 'hello world' 37 | const sequenceNumber = 0 38 | const lifetime = 3_600_000 // ms, e.g. one hour 39 | 40 | const ipnsRecord = await createIPNSRecord(privateKey, value, sequenceNumber, lifetime) 41 | ``` 42 | 43 | ## Example - Validate record against public key 44 | 45 | ```TypeScript 46 | import { validate } from 'ipns/validator' 47 | import { generateKeyPair } from '@libp2p/crypto/keys' 48 | 49 | const privateKey = await generateKeyPair('Ed25519') 50 | const publicKey = privateKey.publicKey 51 | const marshalledRecord = Uint8Array.from([0, 1, 2, 3]) 52 | 53 | await validate(publicKey, marshalledRecord) 54 | // if no error thrown, the record is valid 55 | ``` 56 | 57 | ## Example - Validate record against routing key 58 | 59 | This is useful when validating IPNS names that use RSA keys, whose public key is embedded in the record (rather than in the routing key as with Ed25519). 60 | 61 | ```TypeScript 62 | import { ipnsValidator } from 'ipns/validator' 63 | import { multihashToIPNSRoutingKey } from 'ipns' 64 | import { generateKeyPair } from '@libp2p/crypto/keys' 65 | 66 | const privateKey = await generateKeyPair('Ed25519') 67 | const routingKey = multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()) 68 | const marshalledRecord = Uint8Array.from([0, 1, 2, 3]) 69 | 70 | await ipnsValidator(routingKey, marshalledRecord) 71 | ``` 72 | 73 | ## Example - Extract public key from record 74 | 75 | ```TypeScript 76 | import { extractPublicKeyFromIPNSRecord, createIPNSRecord } from 'ipns' 77 | import { generateKeyPair } from '@libp2p/crypto/keys' 78 | 79 | const privateKey = await generateKeyPair('Ed25519') 80 | const record = await createIPNSRecord(privateKey, 'hello world', 0, 3_600_000) 81 | 82 | const publicKey = await extractPublicKeyFromIPNSRecord(record) 83 | ``` 84 | 85 | ## Example - Marshal data with proto buffer 86 | 87 | ```TypeScript 88 | import { createIPNSRecord, marshalIPNSRecord } from 'ipns' 89 | import { generateKeyPair } from '@libp2p/crypto/keys' 90 | 91 | const privateKey = await generateKeyPair('Ed25519') 92 | const record = await createIPNSRecord(privateKey, 'hello world', 0, 3_600_000) 93 | // ... 94 | const marshalledData = marshalIPNSRecord(record) 95 | // ... 96 | ``` 97 | 98 | Returns the record data serialized. 99 | 100 | ## Example - Unmarshal data from proto buffer 101 | 102 | ```TypeScript 103 | import { unmarshalIPNSRecord } from 'ipns' 104 | 105 | const storedData = Uint8Array.from([0, 1, 2, 3, 4]) 106 | const ipnsRecord = unmarshalIPNSRecord(storedData) 107 | ``` 108 | 109 | Returns the `IPNSRecord` after being deserialized. 110 | 111 | # Install 112 | 113 | ```console 114 | $ npm i ipns 115 | ``` 116 | 117 | ## Browser ` 123 | ``` 124 | 125 | # API Docs 126 | 127 | - 128 | 129 | # License 130 | 131 | Licensed under either of 132 | 133 | - Apache 2.0, ([LICENSE-APACHE](https://github.com/ipfs/js-ipns/LICENSE-APACHE) / ) 134 | - MIT ([LICENSE-MIT](https://github.com/ipfs/js-ipns/LICENSE-MIT) / ) 135 | 136 | # Contribute 137 | 138 | Contributions welcome! Please check out [the issues](https://github.com/ipfs/js-ipns/issues). 139 | 140 | Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. 141 | 142 | Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). 143 | 144 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 145 | 146 | [![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) 147 | -------------------------------------------------------------------------------- /test/validator.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import { randomBytes } from '@libp2p/crypto' 4 | import { generateKeyPair, publicKeyToProtobuf } from '@libp2p/crypto/keys' 5 | import { expect } from 'aegir/chai' 6 | import { toString as uint8ArrayToString } from 'uint8arrays/to-string' 7 | import { InvalidEmbeddedPublicKeyError, RecordTooLargeError, SignatureVerificationError, UnsupportedValidityError } from '../src/errors.js' 8 | import { createIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey } from '../src/index.js' 9 | import { ipnsValidator, validFor } from '../src/validator.js' 10 | import type { PrivateKey } from '@libp2p/interface' 11 | 12 | describe('validator', function () { 13 | this.timeout(20 * 1000) 14 | 15 | const contentPath = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' 16 | let privateKey1: PrivateKey 17 | let privateKey2: PrivateKey 18 | 19 | before(async () => { 20 | privateKey1 = await generateKeyPair('RSA', 2048) 21 | privateKey2 = await generateKeyPair('RSA', 2048) 22 | }) 23 | 24 | it('should validate a (V2) record', async () => { 25 | const sequence = 0 26 | const validity = 1000000 27 | const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: false }) 28 | const marshalledData = marshalIPNSRecord(record) 29 | const key = multihashToIPNSRoutingKey(privateKey1.publicKey.toMultihash()) 30 | 31 | await ipnsValidator(key, marshalledData) 32 | }) 33 | 34 | it('should validate a (V1+V2) record', async () => { 35 | const sequence = 0 36 | const validity = 1000000 37 | const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: true }) 38 | const marshalledData = marshalIPNSRecord(record) 39 | const key = multihashToIPNSRoutingKey(privateKey1.publicKey.toMultihash()) 40 | 41 | await ipnsValidator(key, marshalledData) 42 | }) 43 | 44 | it('should use validator.validate to verify that a record is not valid', async () => { 45 | const sequence = 0 46 | const validity = 1000000 47 | 48 | const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity) 49 | 50 | // corrupt the record by changing the value to random bytes 51 | record.value = uint8ArrayToString(randomBytes(record.value?.length ?? 0)) 52 | const marshalledData = marshalIPNSRecord(record) 53 | 54 | const key = multihashToIPNSRoutingKey(privateKey1.publicKey.toMultihash()) 55 | 56 | await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected() 57 | .with.property('name', SignatureVerificationError.name) 58 | }) 59 | 60 | it('should use validator.validate to verify that a record is not valid when it is passed with the wrong IPNS key', async () => { 61 | const sequence = 0 62 | const validity = 1000000 63 | 64 | const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity) 65 | const marshalledData = marshalIPNSRecord(record) 66 | 67 | const key = multihashToIPNSRoutingKey(privateKey2.publicKey.toMultihash()) 68 | 69 | await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected() 70 | .with.property('name', InvalidEmbeddedPublicKeyError.name) 71 | }) 72 | 73 | it('should use validator.validate to verify that a record is not valid when the wrong key is embedded', async () => { 74 | const sequence = 0 75 | const validity = 1000000 76 | 77 | const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity) 78 | record.pubKey = publicKeyToProtobuf(privateKey2.publicKey) 79 | const marshalledData = marshalIPNSRecord(record) 80 | 81 | const key = multihashToIPNSRoutingKey(privateKey1.publicKey.toMultihash()) 82 | 83 | await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected() 84 | .with.property('name', InvalidEmbeddedPublicKeyError.name) 85 | }) 86 | 87 | it('should limit the size of incoming records', async () => { 88 | const marshalledData = new Uint8Array(1024 * 1024) 89 | const key = new Uint8Array() 90 | 91 | await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected() 92 | .with.property('name', RecordTooLargeError.name) 93 | }) 94 | 95 | describe('validFor', () => { 96 | it('should return the number of milliseconds until the record expires', async () => { 97 | const record = await createIPNSRecord(privateKey1, contentPath, 0, 1000000) 98 | const result = validFor(record) 99 | expect(result).to.be.greaterThan(0) 100 | }) 101 | 102 | it('should return 0 for expired records', async () => { 103 | const record = await createIPNSRecord(privateKey1, contentPath, 0, 0) 104 | 105 | expect(validFor(record)).to.equal(0) 106 | }) 107 | 108 | it('should throw UnsupportedValidityError for non-EOL validity types', async () => { 109 | const record = await createIPNSRecord(privateKey1, contentPath, 0, 1000000) 110 | record.validityType = 5 as any 111 | 112 | expect(() => validFor(record)).to.throw(UnsupportedValidityError) 113 | }) 114 | 115 | it('should throw UnsupportedValidityError for null validity', async () => { 116 | const record = await createIPNSRecord(privateKey1, contentPath, 0, 1000000) 117 | record.validityType = null as any 118 | 119 | expect(() => validFor(record)).to.throw(UnsupportedValidityError) 120 | }) 121 | 122 | it('should return correct milliseconds until expiration', async () => { 123 | const record = await createIPNSRecord(privateKey1, contentPath, 0, 5000) 124 | 125 | const result = validFor(record) 126 | 127 | expect(result).to.be.within(4900, 5000) 128 | }) 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { publicKeyFromProtobuf } from '@libp2p/crypto/keys' 2 | import { InvalidMultihashError } from '@libp2p/interface' 3 | import { logger } from '@libp2p/logger' 4 | import * as cborg from 'cborg' 5 | import { base36 } from 'multiformats/bases/base36' 6 | import { CID } from 'multiformats/cid' 7 | import * as Digest from 'multiformats/hashes/digest' 8 | import { concat as uint8ArrayConcat } from 'uint8arrays/concat' 9 | import { equals as uint8ArrayEquals } from 'uint8arrays/equals' 10 | import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' 11 | import { toString as uint8ArrayToString } from 'uint8arrays/to-string' 12 | import { InvalidRecordDataError, InvalidValueError, SignatureVerificationError, UnsupportedValidityError } from './errors.js' 13 | import { IpnsEntry } from './pb/ipns.js' 14 | import type { IPNSRecord, IPNSRecordV2, IPNSRecordData } from './index.js' 15 | import type { PublicKey } from '@libp2p/interface' 16 | import type { MultihashDigest } from 'multiformats/cid' 17 | 18 | const log = logger('ipns:utils') 19 | const IPNS_PREFIX = uint8ArrayFromString('/ipns/') 20 | const LIBP2P_CID_CODEC = 0x72 21 | const IDENTITY_CODEC = 0x0 22 | const SHA2_256_CODEC = 0x12 23 | 24 | /** 25 | * Extracts a public key from the passed PeerId, falling back to the pubKey 26 | * embedded in the ipns record 27 | */ 28 | export function extractPublicKeyFromIPNSRecord (record: IPNSRecord | IPNSRecordV2): PublicKey | undefined { 29 | let pubKey: PublicKey | undefined 30 | 31 | if (record.pubKey != null) { 32 | try { 33 | pubKey = publicKeyFromProtobuf(record.pubKey) 34 | } catch (err) { 35 | log.error(err) 36 | throw err 37 | } 38 | } 39 | 40 | if (pubKey != null) { 41 | return pubKey 42 | } 43 | } 44 | 45 | /** 46 | * Utility for creating the record data for being signed 47 | */ 48 | export function ipnsRecordDataForV1Sig (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array): Uint8Array { 49 | const validityTypeBuffer = uint8ArrayFromString(validityType) 50 | 51 | return uint8ArrayConcat([value, validity, validityTypeBuffer]) 52 | } 53 | 54 | /** 55 | * Utility for creating the record data for being signed 56 | */ 57 | export function ipnsRecordDataForV2Sig (data: Uint8Array): Uint8Array { 58 | const entryData = uint8ArrayFromString('ipns-signature:') 59 | 60 | return uint8ArrayConcat([entryData, data]) 61 | } 62 | 63 | export function marshalIPNSRecord (obj: IPNSRecord | IPNSRecordV2): Uint8Array { 64 | if ('signatureV1' in obj) { 65 | return IpnsEntry.encode({ 66 | value: uint8ArrayFromString(obj.value), 67 | signatureV1: obj.signatureV1, 68 | validityType: obj.validityType, 69 | validity: uint8ArrayFromString(obj.validity), 70 | sequence: obj.sequence, 71 | ttl: obj.ttl, 72 | pubKey: obj.pubKey, 73 | signatureV2: obj.signatureV2, 74 | data: obj.data 75 | }) 76 | } else { 77 | return IpnsEntry.encode({ 78 | pubKey: obj.pubKey, 79 | signatureV2: obj.signatureV2, 80 | data: obj.data 81 | }) 82 | } 83 | } 84 | 85 | export function unmarshalIPNSRecord (buf: Uint8Array): IPNSRecord { 86 | const message = IpnsEntry.decode(buf) 87 | 88 | // protobufjs returns bigints as numbers 89 | if (message.sequence != null) { 90 | message.sequence = BigInt(message.sequence) 91 | } 92 | 93 | // protobufjs returns bigints as numbers 94 | if (message.ttl != null) { 95 | message.ttl = BigInt(message.ttl) 96 | } 97 | 98 | // Check if we have the data field. If we don't, we fail. We've been producing 99 | // V1+V2 records for quite a while and we don't support V1-only records during 100 | // validation any more 101 | if (message.signatureV2 == null || message.data == null) { 102 | throw new SignatureVerificationError('Missing data or signatureV2') 103 | } 104 | 105 | const data = parseCborData(message.data) 106 | const value = normalizeByteValue(data.Value) 107 | const validity = uint8ArrayToString(data.Validity) 108 | 109 | if (message.value != null && message.signatureV1 != null) { 110 | // V1+V2 111 | validateCborDataMatchesPbData(message) 112 | 113 | return { 114 | value, 115 | validityType: IpnsEntry.ValidityType.EOL, 116 | validity, 117 | sequence: data.Sequence, 118 | ttl: data.TTL, 119 | pubKey: message.pubKey, 120 | signatureV1: message.signatureV1, 121 | signatureV2: message.signatureV2, 122 | data: message.data 123 | } 124 | } else if (message.signatureV2 != null) { 125 | // V2-only 126 | return { 127 | value, 128 | validityType: IpnsEntry.ValidityType.EOL, 129 | validity, 130 | sequence: data.Sequence, 131 | ttl: data.TTL, 132 | pubKey: message.pubKey, 133 | signatureV2: message.signatureV2, 134 | data: message.data 135 | } 136 | } else { 137 | throw new Error('invalid record: does not include signatureV1 or signatureV2') 138 | } 139 | } 140 | 141 | export function multihashToIPNSRoutingKey (digest: MultihashDigest<0x00 | 0x12>): Uint8Array { 142 | return uint8ArrayConcat([ 143 | IPNS_PREFIX, 144 | digest.bytes 145 | ]) 146 | } 147 | 148 | export function multihashFromIPNSRoutingKey (key: Uint8Array): MultihashDigest<0x00 | 0x12> { 149 | const digest = Digest.decode(key.slice(IPNS_PREFIX.length)) 150 | 151 | if (!isCodec(digest, IDENTITY_CODEC) && !isCodec(digest, SHA2_256_CODEC)) { 152 | throw new InvalidMultihashError('Multihash in IPNS key was not identity or sha2-256') 153 | } 154 | 155 | return digest 156 | } 157 | 158 | export function createCborData (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array, sequence: bigint, ttl: bigint): Uint8Array { 159 | let ValidityType 160 | 161 | if (validityType === IpnsEntry.ValidityType.EOL) { 162 | ValidityType = 0 163 | } else { 164 | throw new UnsupportedValidityError('The validity type is unsupported') 165 | } 166 | 167 | const data = { 168 | Value: value, 169 | Validity: validity, 170 | ValidityType, 171 | Sequence: sequence, 172 | TTL: ttl 173 | } 174 | 175 | return cborg.encode(data) 176 | } 177 | 178 | export function parseCborData (buf: Uint8Array): IPNSRecordData { 179 | const data = cborg.decode(buf) 180 | 181 | if (data.ValidityType === 0) { 182 | data.ValidityType = IpnsEntry.ValidityType.EOL 183 | } else { 184 | throw new UnsupportedValidityError('The validity type is unsupported') 185 | } 186 | 187 | if (Number.isInteger(data.Sequence)) { 188 | // sequence must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range 189 | data.Sequence = BigInt(data.Sequence) 190 | } 191 | 192 | if (Number.isInteger(data.TTL)) { 193 | // ttl must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range 194 | data.TTL = BigInt(data.TTL) 195 | } 196 | 197 | return data 198 | } 199 | 200 | export function normalizeByteValue (value: Uint8Array): string { 201 | const string = uint8ArrayToString(value).trim() 202 | 203 | // if we have a path, check it is a valid path 204 | if (string.startsWith('/')) { 205 | return string 206 | } 207 | 208 | // try parsing what we have as CID bytes or a CID string 209 | try { 210 | return `/ipfs/${CID.decode(value).toV1().toString()}` 211 | } catch { 212 | // fall through 213 | } 214 | 215 | try { 216 | return `/ipfs/${CID.parse(string).toV1().toString()}` 217 | } catch { 218 | // fall through 219 | } 220 | 221 | throw new InvalidValueError('Value must be a valid content path starting with /') 222 | } 223 | 224 | /** 225 | * Normalizes the given record value. It ensures it is a PeerID, a CID or a 226 | * string starting with '/'. PeerIDs become `/ipns/${cidV1Libp2pKey}`, 227 | * CIDs become `/ipfs/${cidAsV1}`. 228 | */ 229 | export function normalizeValue (value?: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string): string { 230 | if (value != null) { 231 | const cid = asCID(value) 232 | 233 | // if we have a CID, turn it into an ipfs path 234 | if (cid != null) { 235 | // PeerID encoded as a CID 236 | if (cid.code === LIBP2P_CID_CODEC) { 237 | return `/ipns/${cid.toString(base36)}` 238 | } 239 | 240 | return `/ipfs/${cid.toV1().toString()}` 241 | } 242 | 243 | if (hasBytes(value)) { 244 | return `/ipns/${base36.encode(value.bytes)}` 245 | } 246 | 247 | // if we have a path, check it is a valid path 248 | const string = value.toString().trim() 249 | 250 | if (string.startsWith('/') && string.length > 1) { 251 | return string 252 | } 253 | } 254 | 255 | throw new InvalidValueError('Value must be a valid content path starting with /') 256 | } 257 | 258 | function validateCborDataMatchesPbData (entry: IpnsEntry): void { 259 | if (entry.data == null) { 260 | throw new InvalidRecordDataError('Record data is missing') 261 | } 262 | 263 | const data = parseCborData(entry.data) 264 | 265 | if (!uint8ArrayEquals(data.Value, entry.value ?? new Uint8Array(0))) { 266 | throw new SignatureVerificationError('Field "value" did not match between protobuf and CBOR') 267 | } 268 | 269 | if (!uint8ArrayEquals(data.Validity, entry.validity ?? new Uint8Array(0))) { 270 | throw new SignatureVerificationError('Field "validity" did not match between protobuf and CBOR') 271 | } 272 | 273 | if (data.ValidityType !== entry.validityType) { 274 | throw new SignatureVerificationError('Field "validityType" did not match between protobuf and CBOR') 275 | } 276 | 277 | if (data.Sequence !== entry.sequence) { 278 | throw new SignatureVerificationError('Field "sequence" did not match between protobuf and CBOR') 279 | } 280 | 281 | if (data.TTL !== entry.ttl) { 282 | throw new SignatureVerificationError('Field "ttl" did not match between protobuf and CBOR') 283 | } 284 | } 285 | 286 | function hasBytes (obj?: any): obj is { bytes: Uint8Array } { 287 | return obj.bytes instanceof Uint8Array 288 | } 289 | 290 | function hasToCID (obj?: any): obj is { toCID(): CID } { 291 | return typeof obj?.toCID === 'function' 292 | } 293 | 294 | function asCID (obj?: any): CID | null { 295 | if (hasToCID(obj)) { 296 | return obj.toCID() 297 | } 298 | 299 | // try parsing as a CID string 300 | try { 301 | return CID.parse(obj) 302 | } catch { 303 | // fall through 304 | } 305 | 306 | return CID.asCID(obj) 307 | } 308 | 309 | export function isCodec (digest: MultihashDigest, codec: T): digest is MultihashDigest { 310 | return digest.code === codec 311 | } 312 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation 3 | * 4 | * Implements parsing and serialization of [IPNS Records](https://specs.ipfs.tech/ipns/ipns-record/). 5 | * 6 | * @example Create record 7 | * 8 | * ```TypeScript 9 | * import { createIPNSRecord } from 'ipns' 10 | * import { generateKeyPair } from '@libp2p/crypto/keys' 11 | * 12 | * const privateKey = await generateKeyPair('Ed25519') 13 | * const value = 'hello world' 14 | * const sequenceNumber = 0 15 | * const lifetime = 3_600_000 // ms, e.g. one hour 16 | * 17 | * const ipnsRecord = await createIPNSRecord(privateKey, value, sequenceNumber, lifetime) 18 | * ``` 19 | * 20 | * @example Validate record against public key 21 | * 22 | * ```TypeScript 23 | * import { validate } from 'ipns/validator' 24 | * import { generateKeyPair } from '@libp2p/crypto/keys' 25 | * 26 | * const privateKey = await generateKeyPair('Ed25519') 27 | * const publicKey = privateKey.publicKey 28 | * const marshalledRecord = Uint8Array.from([0, 1, 2, 3]) 29 | * 30 | * await validate(publicKey, marshalledRecord) 31 | * // if no error thrown, the record is valid 32 | * ``` 33 | * 34 | * @example Validate record against routing key 35 | * 36 | * This is useful when validating IPNS names that use RSA keys, whose public key is embedded in the record (rather than in the routing key as with Ed25519). 37 | * 38 | * ```TypeScript 39 | * import { ipnsValidator } from 'ipns/validator' 40 | * import { multihashToIPNSRoutingKey } from 'ipns' 41 | * import { generateKeyPair } from '@libp2p/crypto/keys' 42 | * 43 | * const privateKey = await generateKeyPair('Ed25519') 44 | * const routingKey = multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()) 45 | * const marshalledRecord = Uint8Array.from([0, 1, 2, 3]) 46 | * 47 | * await ipnsValidator(routingKey, marshalledRecord) 48 | * ``` 49 | * 50 | * @example Extract public key from record 51 | * 52 | * ```TypeScript 53 | * import { extractPublicKeyFromIPNSRecord, createIPNSRecord } from 'ipns' 54 | * import { generateKeyPair } from '@libp2p/crypto/keys' 55 | * 56 | * const privateKey = await generateKeyPair('Ed25519') 57 | * const record = await createIPNSRecord(privateKey, 'hello world', 0, 3_600_000) 58 | * 59 | * const publicKey = await extractPublicKeyFromIPNSRecord(record) 60 | * ``` 61 | * 62 | * @example Marshal data with proto buffer 63 | * 64 | * ```TypeScript 65 | * import { createIPNSRecord, marshalIPNSRecord } from 'ipns' 66 | * import { generateKeyPair } from '@libp2p/crypto/keys' 67 | * 68 | * const privateKey = await generateKeyPair('Ed25519') 69 | * const record = await createIPNSRecord(privateKey, 'hello world', 0, 3_600_000) 70 | * // ... 71 | * const marshalledData = marshalIPNSRecord(record) 72 | * // ... 73 | * ``` 74 | * 75 | * Returns the record data serialized. 76 | * 77 | * @example Unmarshal data from proto buffer 78 | * 79 | * ```TypeScript 80 | * import { unmarshalIPNSRecord } from 'ipns' 81 | * 82 | * const storedData = Uint8Array.from([0, 1, 2, 3, 4]) 83 | * const ipnsRecord = unmarshalIPNSRecord(storedData) 84 | * ``` 85 | * 86 | * Returns the `IPNSRecord` after being deserialized. 87 | */ 88 | 89 | import { publicKeyToProtobuf } from '@libp2p/crypto/keys' 90 | import { logger } from '@libp2p/logger' 91 | import NanoDate from 'timestamp-nano' 92 | import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' 93 | import { SignatureCreationError } from './errors.js' 94 | import { IpnsEntry } from './pb/ipns.js' 95 | import { createCborData, ipnsRecordDataForV1Sig, ipnsRecordDataForV2Sig, normalizeValue } from './utils.js' 96 | import type { PrivateKey, PublicKey } from '@libp2p/interface' 97 | import type { Key } from 'interface-datastore/key' 98 | import type { CID } from 'multiformats/cid' 99 | import type { MultihashDigest } from 'multiformats/hashes/interface' 100 | 101 | const log = logger('ipns') 102 | const DEFAULT_TTL_NS = 5 * 60 * 1e+9 // 5 Minutes or 300 Seconds, as suggested by https://specs.ipfs.tech/ipns/ipns-record/#ttl-uint64 103 | 104 | export const namespace = '/ipns/' 105 | export const namespaceLength = namespace.length 106 | 107 | export interface IPNSRecordV1V2 { 108 | /** 109 | * value of the record 110 | */ 111 | value: string 112 | 113 | /** 114 | * signature of the record 115 | */ 116 | signatureV1: Uint8Array 117 | 118 | /** 119 | * Type of validation being used 120 | */ 121 | validityType: IpnsEntry.ValidityType 122 | 123 | /** 124 | * expiration datetime for the record in RFC3339 format 125 | */ 126 | validity: string 127 | 128 | /** 129 | * number representing the version of the record 130 | */ 131 | sequence: bigint 132 | 133 | /** 134 | * ttl in nanoseconds 135 | */ 136 | ttl?: bigint 137 | 138 | /** 139 | * the public portion of the key that signed this record (only present if it was not embedded in the IPNS key) 140 | */ 141 | pubKey?: Uint8Array 142 | 143 | /** 144 | * the v2 signature of the record 145 | */ 146 | signatureV2: Uint8Array 147 | 148 | /** 149 | * extensible data 150 | */ 151 | data: Uint8Array 152 | } 153 | 154 | export interface IPNSRecordV2 { 155 | /** 156 | * value of the record 157 | */ 158 | value: string 159 | 160 | /** 161 | * the v2 signature of the record 162 | */ 163 | signatureV2: Uint8Array 164 | 165 | /** 166 | * Type of validation being used 167 | */ 168 | validityType: IpnsEntry.ValidityType 169 | 170 | /** 171 | * If the validity type is EOL, this is the expiration datetime for the record 172 | * in RFC3339 format 173 | */ 174 | validity: string 175 | 176 | /** 177 | * number representing the version of the record 178 | */ 179 | sequence: bigint 180 | 181 | /** 182 | * ttl in nanoseconds 183 | */ 184 | ttl?: bigint 185 | 186 | /** 187 | * the public portion of the key that signed this record (only present if it was not embedded in the IPNS key) 188 | */ 189 | pubKey?: Uint8Array 190 | 191 | /** 192 | * extensible data 193 | */ 194 | data: Uint8Array 195 | } 196 | 197 | export type IPNSRecord = IPNSRecordV1V2 | IPNSRecordV2 198 | 199 | export interface IPNSRecordData { 200 | Value: Uint8Array 201 | Validity: Uint8Array 202 | ValidityType: IpnsEntry.ValidityType 203 | Sequence: bigint 204 | TTL: bigint 205 | } 206 | 207 | export interface IDKeys { 208 | routingPubKey: Key 209 | pkKey: Key 210 | routingKey: Key 211 | ipnsKey: Key 212 | } 213 | 214 | export interface CreateOptions { 215 | ttlNs?: number | bigint 216 | v1Compatible?: boolean 217 | } 218 | 219 | export interface CreateV2OrV1Options { 220 | v1Compatible: true 221 | } 222 | 223 | export interface CreateV2Options { 224 | v1Compatible: false 225 | } 226 | 227 | const defaultCreateOptions: CreateOptions = { 228 | v1Compatible: true, 229 | ttlNs: DEFAULT_TTL_NS 230 | } 231 | 232 | /** 233 | * Creates a new IPNS record and signs it with the given private key. 234 | * The IPNS Record validity should follow the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision. 235 | * Note: This function does not embed the public key. If you want to do that, use `EmbedPublicKey`. 236 | * 237 | * The passed value can be a CID, a PublicKey or an arbitrary string path e.g. `/ipfs/...` or `/ipns/...`. 238 | * 239 | * CIDs will be converted to v1 and stored in the record as a string similar to: `/ipfs/${cid}` 240 | * PublicKeys will create recursive records, eg. the record value will be `/ipns/${cidV1Libp2pKey}` 241 | * String paths will be stored in the record as-is, but they must start with `"/"` 242 | * 243 | * @param {PrivateKey} privateKey - the private key for signing the record. 244 | * @param {CID | PublicKey | string} value - content to be stored in the record. 245 | * @param {number | bigint} seq - number representing the current version of the record. 246 | * @param {number} lifetime - lifetime of the record (in milliseconds). 247 | * @param {CreateOptions} options - additional create options. 248 | */ 249 | export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise 250 | export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise 251 | export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options: CreateOptions): Promise 252 | export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise { 253 | // Validity in ISOString with nanoseconds precision and validity type EOL 254 | const expirationDate = new NanoDate(Date.now() + Number(lifetime)) 255 | const validityType = IpnsEntry.ValidityType.EOL 256 | const ttlNs = BigInt(options.ttlNs ?? DEFAULT_TTL_NS) 257 | 258 | return _create(privateKey, value, seq, validityType, expirationDate.toString(), ttlNs, options) 259 | } 260 | 261 | /** 262 | * Same as create(), but instead of generating a new Date, it receives the intended expiration time 263 | * WARNING: nano precision is not standard, make sure the value in seconds is 9 orders of magnitude lesser than the one provided. 264 | * 265 | * The passed value can be a CID, a PublicKey or an arbitrary string path e.g. `/ipfs/...` or `/ipns/...`. 266 | * 267 | * CIDs will be converted to v1 and stored in the record as a string similar to: `/ipfs/${cid}` 268 | * PublicKeys will create recursive records, eg. the record value will be `/ipns/${cidV1Libp2pKey}` 269 | * String paths will be stored in the record as-is, but they must start with `"/"` 270 | * 271 | * @param {PrivateKey} privateKey - the private key for signing the record. 272 | * @param {CID | PublicKey | string} value - content to be stored in the record. 273 | * @param {number | bigint} seq - number representing the current version of the record. 274 | * @param {string} expiration - expiration datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision. 275 | * @param {CreateOptions} options - additional creation options. 276 | */ 277 | export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise 278 | export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options: CreateV2Options): Promise 279 | export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options: CreateOptions): Promise 280 | export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise { 281 | const expirationDate = NanoDate.fromString(expiration) 282 | const validityType = IpnsEntry.ValidityType.EOL 283 | const ttlNs = BigInt(options.ttlNs ?? DEFAULT_TTL_NS) 284 | 285 | return _create(privateKey, value, seq, validityType, expirationDate.toString(), ttlNs, options) 286 | } 287 | 288 | const _create = async (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, validity: string, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise => { 289 | seq = BigInt(seq) 290 | const isoValidity = uint8ArrayFromString(validity) 291 | const normalizedValue = normalizeValue(value) 292 | const encodedValue = uint8ArrayFromString(normalizedValue) 293 | const data = createCborData(encodedValue, validityType, isoValidity, seq, ttl) 294 | const sigData = ipnsRecordDataForV2Sig(data) 295 | const signatureV2 = await privateKey.sign(sigData) 296 | let pubKey: Uint8Array | undefined 297 | 298 | // if we cannot derive the public key from the PeerId (e.g. RSA PeerIDs), 299 | // we have to embed it in the IPNS record 300 | if (privateKey.type === 'RSA') { 301 | pubKey = publicKeyToProtobuf(privateKey.publicKey) 302 | } 303 | 304 | if (options.v1Compatible === true) { 305 | const signatureV1 = await signLegacyV1(privateKey, encodedValue, validityType, isoValidity) 306 | 307 | const record: IPNSRecord = { 308 | value: normalizedValue, 309 | signatureV1, 310 | validity, 311 | validityType, 312 | sequence: seq, 313 | ttl, 314 | signatureV2, 315 | data 316 | } 317 | 318 | if (pubKey != null) { 319 | record.pubKey = pubKey 320 | } 321 | 322 | return record 323 | } else { 324 | const record: IPNSRecordV2 = { 325 | value: normalizedValue, 326 | validity, 327 | validityType, 328 | sequence: seq, 329 | ttl, 330 | signatureV2, 331 | data 332 | } 333 | 334 | if (pubKey != null) { 335 | record.pubKey = pubKey 336 | } 337 | 338 | return record 339 | } 340 | } 341 | 342 | export { unmarshalIPNSRecord } from './utils.js' 343 | export { marshalIPNSRecord } from './utils.js' 344 | export { multihashToIPNSRoutingKey } from './utils.js' 345 | export { multihashFromIPNSRoutingKey } from './utils.js' 346 | export { extractPublicKeyFromIPNSRecord } from './utils.js' 347 | 348 | /** 349 | * Sign ipns record data using the legacy V1 signature scheme 350 | */ 351 | const signLegacyV1 = async (privateKey: PrivateKey, value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array): Promise => { 352 | try { 353 | const dataForSignature = ipnsRecordDataForV1Sig(value, validityType, validity) 354 | 355 | return await privateKey.sign(dataForSignature) 356 | } catch (error: any) { 357 | log.error('record signature creation failed', error) 358 | throw new SignatureCreationError('Record signature creation failed') 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /test/index.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import { randomBytes } from '@libp2p/crypto' 4 | import { generateKeyPair, publicKeyToProtobuf } from '@libp2p/crypto/keys' 5 | import { peerIdFromPrivateKey } from '@libp2p/peer-id' 6 | import { expect } from 'aegir/chai' 7 | import * as cbor from 'cborg' 8 | import { base36 } from 'multiformats/bases/base36' 9 | import { base58btc } from 'multiformats/bases/base58' 10 | import { CID } from 'multiformats/cid' 11 | import * as Digest from 'multiformats/hashes/digest' 12 | import { toString as uint8ArrayToString } from 'uint8arrays' 13 | import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' 14 | import { InvalidEmbeddedPublicKeyError, InvalidValueError, RecordExpiredError, SignatureVerificationError } from '../src/errors.js' 15 | import { createIPNSRecord, createIPNSRecordWithExpiration } from '../src/index.js' 16 | import { IpnsEntry } from '../src/pb/ipns.js' 17 | import { extractPublicKeyFromIPNSRecord, parseCborData, createCborData, ipnsRecordDataForV2Sig, marshalIPNSRecord, unmarshalIPNSRecord, multihashToIPNSRoutingKey, multihashFromIPNSRoutingKey } from '../src/utils.js' 18 | import { ipnsValidator } from '../src/validator.js' 19 | import { kuboRecord } from './fixtures/records.js' 20 | import type { PrivateKey } from '@libp2p/interface' 21 | 22 | describe('ipns', function () { 23 | this.timeout(20 * 1000) 24 | 25 | const contentPath = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' 26 | let privateKey: PrivateKey 27 | 28 | before(async () => { 29 | privateKey = await generateKeyPair('RSA', 2048) 30 | }) 31 | 32 | it('should create an ipns record (V1+V2) correctly', async () => { 33 | const sequence = 0 34 | const ttl = BigInt(5 * 60 * 1e+9) 35 | const validity = 1000000 36 | 37 | const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) 38 | 39 | expect(record.value).to.equal(contentPath) 40 | expect(record.validityType).to.equal(IpnsEntry.ValidityType.EOL) 41 | expect(record.validity).to.exist() 42 | expect(record.sequence).to.equal(BigInt(0)) 43 | expect(record.ttl).to.equal(ttl) 44 | expect(record.signatureV1).to.exist() 45 | expect(record.signatureV2).to.exist() 46 | expect(record.data).to.exist() 47 | 48 | // Protobuf must have all fields! 49 | const pb = IpnsEntry.decode(marshalIPNSRecord(record)) 50 | expect(pb.value).to.equalBytes(uint8ArrayFromString(contentPath)) 51 | expect(pb.validityType).to.equal(IpnsEntry.ValidityType.EOL) 52 | expect(pb.validity).to.exist() 53 | expect(pb.sequence).to.equal(BigInt(sequence)) 54 | expect(pb.ttl).to.equal(ttl) 55 | expect(pb.signatureV1).to.exist() 56 | expect(pb.signatureV2).to.exist() 57 | expect(pb.data).to.exist() 58 | 59 | // Protobuf.Data must have all fields and match! 60 | const data = parseCborData(pb.data ?? new Uint8Array(0)) 61 | expect(data.Value).to.equalBytes(pb.value) 62 | expect(data.ValidityType).to.equal(pb.validityType) 63 | expect(data.Validity).to.equalBytes(pb.validity) 64 | expect(data.Sequence).to.equal(pb.sequence) 65 | expect(data.TTL).to.equal(pb.ttl) 66 | }) 67 | 68 | it('should create an ipns record (V2) correctly', async () => { 69 | const sequence = 0 70 | const ttl = BigInt(5 * 60 * 1e+9) 71 | const validity = 1000000 72 | 73 | const record = await createIPNSRecord(privateKey, contentPath, sequence, validity, { v1Compatible: false }) 74 | 75 | expect(record.value).to.equal(contentPath) 76 | expect(record.validityType).to.equal(IpnsEntry.ValidityType.EOL) 77 | expect(record.validity).to.exist() 78 | expect(record.sequence).to.equal(BigInt(0)) 79 | expect(record.ttl).to.equal(ttl) 80 | expect(record.signatureV2).to.exist() 81 | expect(record).to.not.have.property('signatureV1') 82 | expect(record.data).to.exist() 83 | 84 | // PB must only have signature and data. 85 | const pb = IpnsEntry.decode(marshalIPNSRecord(record)) 86 | expect(pb.value).to.not.exist() 87 | expect(pb.validityType).to.not.exist() 88 | expect(pb.validity).to.not.exist() 89 | expect(pb.sequence).to.not.exist() 90 | expect(pb.ttl).to.not.exist() 91 | expect(pb.signatureV1).to.not.exist() 92 | expect(pb.signatureV2).to.exist() 93 | expect(pb.data).to.exist() 94 | 95 | // Protobuf.Data must have all fields and match! 96 | const data = parseCborData(pb.data ?? new Uint8Array(0)) 97 | expect(data.Value).to.equalBytes(uint8ArrayFromString(contentPath)) 98 | expect(data.ValidityType).to.equal(IpnsEntry.ValidityType.EOL) 99 | expect(data.Validity).to.exist() 100 | expect(data.Sequence).to.equal(BigInt(sequence)) 101 | expect(data.TTL).to.equal(ttl) 102 | }) 103 | 104 | it('should be able to create a record (V1+V2) with a fixed expiration', async () => { 105 | const sequence = 0 106 | const expiration = '2033-05-18T03:33:20.000000000Z' 107 | 108 | const record = await createIPNSRecordWithExpiration(privateKey, contentPath, sequence, expiration) 109 | const marshalledRecord = marshalIPNSRecord(record) 110 | 111 | await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalledRecord) 112 | 113 | const pb = IpnsEntry.decode(marshalledRecord) 114 | expect(pb).to.have.property('validity') 115 | expect(pb.validity).to.equalBytes(uint8ArrayFromString(expiration)) 116 | }) 117 | 118 | it('should be able to create a record (V2) with a fixed expiration', async () => { 119 | const sequence = 0 120 | const expiration = '2033-05-18T03:33:20.000000000Z' 121 | 122 | const record = await createIPNSRecordWithExpiration(privateKey, contentPath, sequence, expiration, { v1Compatible: false }) 123 | const marshalledRecord = marshalIPNSRecord(record) 124 | 125 | await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalledRecord) 126 | 127 | const pb = IpnsEntry.decode(marshalIPNSRecord(record)) 128 | expect(pb).to.not.have.property('validity') 129 | 130 | const data = parseCborData(pb.data ?? new Uint8Array(0)) 131 | expect(data.Validity).to.equalBytes(uint8ArrayFromString(expiration)) 132 | }) 133 | 134 | it('should be able to create a record (V1+V2) with a fixed ttl', async () => { 135 | const sequence = 0 136 | const ttl = BigInt(0.6e+12) 137 | const validity = 1000000 138 | 139 | const record = await createIPNSRecord(privateKey, contentPath, sequence, validity, { 140 | ttlNs: ttl 141 | }) 142 | const marshalledRecord = marshalIPNSRecord(record) 143 | 144 | await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalledRecord) 145 | 146 | const pb = IpnsEntry.decode(marshalledRecord) 147 | const data = parseCborData(pb.data ?? new Uint8Array(0)) 148 | expect(data.TTL).to.equal(ttl) 149 | }) 150 | 151 | it('should be able to create a record (V2) with a fixed ttl', async () => { 152 | const sequence = 0 153 | const ttl = BigInt(1.6e+12) 154 | const validity = 1000000 155 | 156 | const record = await createIPNSRecord(privateKey, contentPath, sequence, validity, { 157 | ttlNs: ttl, 158 | v1Compatible: false 159 | }) 160 | const marshalledRecord = marshalIPNSRecord(record) 161 | 162 | await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalledRecord) 163 | 164 | const pb = IpnsEntry.decode(marshalledRecord) 165 | expect(pb).to.not.have.property('ttl') 166 | 167 | const data = parseCborData(pb.data ?? new Uint8Array(0)) 168 | expect(data.TTL).to.equal(ttl) 169 | }) 170 | 171 | it('should create an ipns record (V1+V2) and validate it correctly', async () => { 172 | const sequence = 0 173 | const validity = 1000000 174 | 175 | const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) 176 | await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalIPNSRecord(record)) 177 | }) 178 | 179 | it('should create an ipns record (V2) and validate it correctly', async () => { 180 | const sequence = 0 181 | const validity = 1000000 182 | 183 | const record = await createIPNSRecord(privateKey, contentPath, sequence, validity, { v1Compatible: false }) 184 | await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalIPNSRecord(record)) 185 | }) 186 | 187 | it('should normalize value when creating an ipns record (arbitrary string path)', async () => { 188 | const inputValue = '/foo/bar/baz' 189 | const expectedValue = '/foo/bar/baz' 190 | const record = await createIPNSRecord(privateKey, inputValue, 0, 1000000) 191 | expect(record.value).to.equal(expectedValue) 192 | }) 193 | 194 | it('should normalize value when creating a recursive ipns record (Ed25519 public key)', async () => { 195 | const otherKey = await generateKeyPair('Ed25519') 196 | const expectedValue = `/ipns/${otherKey.publicKey.toCID().toString(base36)}` 197 | const record = await createIPNSRecord(privateKey, otherKey.publicKey, 0, 1000000) 198 | expect(record.value).to.equal(expectedValue) 199 | }) 200 | 201 | it('should normalize value when creating a recursive ipns record (RSA public key)', async () => { 202 | const otherKey = await generateKeyPair('RSA', 512) 203 | const expectedValue = `/ipns/${otherKey.publicKey.toCID().toString(base36)}` 204 | const record = await createIPNSRecord(privateKey, otherKey.publicKey, 0, 1000000) 205 | expect(record.value).to.equal(expectedValue) 206 | }) 207 | 208 | it('should normalize value when creating a recursive ipns record (peer id as CID)', async () => { 209 | const otherKey = await generateKeyPair('Ed25519') 210 | const peerId = peerIdFromPrivateKey(otherKey) 211 | const expectedValue = `/ipns/${peerId.toCID().toString(base36)}` 212 | const record = await createIPNSRecord(privateKey, peerId.toCID(), 0, 1000000) 213 | expect(record.value).to.equal(expectedValue) 214 | }) 215 | 216 | it('should normalize value when creating an ipns record (v0 cid)', async () => { 217 | const inputValue = CID.parse('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq') 218 | const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua' 219 | const record = await createIPNSRecord(privateKey, inputValue, 0, 1000000) 220 | expect(record.value).to.equal(expectedValue) 221 | }) 222 | 223 | it('should normalize value when creating an ipns record (v1 cid)', async () => { 224 | const inputValue = CID.parse('bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu') 225 | const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' 226 | const record = await createIPNSRecord(privateKey, inputValue, 0, 1000000) 227 | expect(record.value).to.equal(expectedValue) 228 | }) 229 | 230 | it('should normalize value when reading an ipns record (string v0 cid path)', async () => { 231 | const inputValue = '/ipfs/QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq' 232 | const expectedValue = '/ipfs/QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq' 233 | const record = await createIPNSRecord(privateKey, inputValue, 0, 1000000) 234 | 235 | const pb = IpnsEntry.decode(marshalIPNSRecord(record)) 236 | pb.data = createCborData(uint8ArrayFromString(inputValue), pb.validityType ?? IpnsEntry.ValidityType.EOL, pb.validity ?? new Uint8Array(0), pb.sequence ?? 0n, pb.ttl ?? 0n) 237 | pb.value = uint8ArrayFromString(inputValue) 238 | 239 | const modifiedRecord = unmarshalIPNSRecord(IpnsEntry.encode(pb)) 240 | expect(modifiedRecord.value).to.equal(expectedValue) 241 | }) 242 | 243 | it('should normalize value when reading an ipns record (string v1 cid path)', async () => { 244 | const inputValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' 245 | const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' 246 | const record = await createIPNSRecord(privateKey, inputValue, 0, 1000000) 247 | 248 | const pb = IpnsEntry.decode(marshalIPNSRecord(record)) 249 | pb.data = createCborData(uint8ArrayFromString(inputValue), pb.validityType ?? IpnsEntry.ValidityType.EOL, pb.validity ?? new Uint8Array(0), pb.sequence ?? 0n, pb.ttl ?? 0n) 250 | pb.value = uint8ArrayFromString(inputValue) 251 | 252 | const modifiedRecord = unmarshalIPNSRecord(IpnsEntry.encode(pb)) 253 | expect(modifiedRecord.value).to.equal(expectedValue) 254 | }) 255 | 256 | it('should fail to normalize non-path value', async () => { 257 | const inputValue = 'hello' 258 | 259 | await expect(createIPNSRecord(privateKey, inputValue, 0, 1000000)).to.eventually.be.rejected 260 | .with.property('name', InvalidValueError.name) 261 | }) 262 | 263 | it('should fail to normalize path value that is too short', async () => { 264 | const inputValue = '/' 265 | 266 | await expect(createIPNSRecord(privateKey, inputValue, 0, 1000000)).to.eventually.be.rejected 267 | .with.property('name', InvalidValueError.name) 268 | }) 269 | 270 | it('should fail to validate a v1 (deprecated legacy) message', async () => { 271 | const sequence = 0 272 | const validity = 1000000 273 | 274 | const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) 275 | const pb = IpnsEntry.decode(marshalIPNSRecord(record)) 276 | 277 | // remove the extra fields added for v2 sigs 278 | delete pb.data 279 | delete pb.signatureV2 280 | 281 | // confirm a v1 exists 282 | expect(pb).to.have.property('signatureV1') 283 | 284 | await expect(ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), IpnsEntry.encode(pb))).to.eventually.be.rejected() 285 | .with.property('name', SignatureVerificationError.name) 286 | }) 287 | 288 | it('should fail to validate a v2 without v2 signature (ignore v1)', async () => { 289 | const sequence = 0 290 | const validity = 1000000 291 | 292 | const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) 293 | const pb = IpnsEntry.decode(marshalIPNSRecord(record)) 294 | 295 | // remove v2 sig 296 | delete pb.signatureV2 297 | 298 | // confirm a v1 exists 299 | expect(pb).to.have.property('signatureV1') 300 | 301 | await expect(ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), IpnsEntry.encode(pb))).to.eventually.be.rejected() 302 | .with.property('name', SignatureVerificationError.name) 303 | }) 304 | 305 | it('should fail to validate a bad record', async () => { 306 | const sequence = 0 307 | const validity = 1000000 308 | 309 | const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) 310 | 311 | // corrupt the record by changing the value to random bytes 312 | record.value = uint8ArrayToString(randomBytes(46)) 313 | 314 | await expect(ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalIPNSRecord(record))).to.eventually.be.rejected() 315 | .with.property('name', SignatureVerificationError.name) 316 | }) 317 | 318 | it('should create an ipns record with a validity of 1 nanosecond correctly and it should not be valid 1ms later', async () => { 319 | const sequence = 0 320 | const validity = 0.00001 321 | 322 | const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) 323 | 324 | await new Promise(resolve => setTimeout(resolve, 1)) 325 | 326 | await expect(ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalIPNSRecord(record))).to.eventually.be.rejected() 327 | .with.property('name', RecordExpiredError.name) 328 | }) 329 | 330 | it('should create an ipns record, marshal and unmarshal it, as well as validate it correctly', async () => { 331 | const sequence = 0 332 | const validity = 1000000 333 | 334 | const createdRecord = await createIPNSRecord(privateKey, contentPath, sequence, validity) 335 | 336 | const marshalledData = marshalIPNSRecord(createdRecord) 337 | const unmarshalledData = unmarshalIPNSRecord(marshalledData) 338 | 339 | expect(createdRecord.value).to.equal(unmarshalledData.value) 340 | expect(createdRecord.validity.toString()).to.equal(unmarshalledData.validity.toString()) 341 | expect(createdRecord.validityType).to.equal(unmarshalledData.validityType) 342 | expect(createdRecord.signatureV1).to.equalBytes('signatureV1' in unmarshalledData ? unmarshalledData.signatureV1 : new Uint8Array(0)) 343 | expect(createdRecord.sequence).to.equal(unmarshalledData.sequence) 344 | expect(createdRecord.ttl).to.equal(unmarshalledData.ttl) 345 | expect(createdRecord.signatureV2).to.equalBytes(unmarshalledData.signatureV2) 346 | expect(createdRecord.data).to.equalBytes(unmarshalledData.data) 347 | 348 | await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalledData) 349 | }) 350 | 351 | it('should be able to turn routing key back into id', () => { 352 | const keys = [ 353 | 'QmQd5Enz5tzP8u5wHur8ADuJMbcNhEf86CkWkqRzoWUhst', 354 | 'QmW6mcoqDKJRch2oph2FmvZhPLJn6wPU648Vv9iMyMtmtG' 355 | ] 356 | 357 | keys.forEach(key => { 358 | const digest = Digest.decode(base58btc.decode(`z${key}`)) 359 | // @ts-expect-error digest may have the wrong hash type 360 | const routingKey = multihashToIPNSRoutingKey(digest) 361 | const id = multihashFromIPNSRoutingKey(routingKey) 362 | 363 | expect(base58btc.encode(id.bytes)).to.equal(`z${key}`) 364 | }) 365 | }) 366 | 367 | it('should be able to embed a public key in an ipns record', async () => { 368 | const sequence = 0 369 | const validity = 1000000 370 | 371 | const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) 372 | expect(record.pubKey).to.equalBytes(publicKeyToProtobuf(privateKey.publicKey)) 373 | 374 | const pb = IpnsEntry.decode(marshalIPNSRecord(record)) 375 | expect(pb.pubKey).to.equalBytes(publicKeyToProtobuf(privateKey.publicKey)) 376 | }) 377 | 378 | // It should have a public key embedded for newer ed25519 keys 379 | // https://github.com/ipfs/go-ipns/blob/d51115b4b14ed7fcca5472aadff0fee6772aca8c/ipns.go#L81 380 | // https://github.com/ipfs/go-ipns/blob/d51115b4b14ed7fcca5472aadff0fee6772aca8c/ipns_test.go 381 | // https://github.com/libp2p/go-libp2p-peer/blob/7f219a1e70011a258c5d3e502aef6896c60d03ce/peer.go#L80 382 | // IDFromEd25519PublicKey is not currently implement on js-libp2p-peer 383 | // https://github.com/libp2p/go-libp2p-peer/pull/30 384 | it('should be able to extract a public key directly from the peer', async () => { 385 | const sequence = 0 386 | const validity = 1000000 387 | 388 | const privateKey = await generateKeyPair('Ed25519') 389 | const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) 390 | 391 | expect(record).to.not.have.property('pubKey') // ed25519 keys should not be embedded 392 | }) 393 | 394 | it('validator with no valid public key should error', async () => { 395 | const sequence = 0 396 | const validity = 1000000 397 | 398 | const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) 399 | delete record.pubKey 400 | 401 | const marshalledData = marshalIPNSRecord(record) 402 | const key = multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()) 403 | 404 | await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected() 405 | .with.property('name', InvalidEmbeddedPublicKeyError.name) 406 | }) 407 | 408 | it('should be able to export a previously embedded public key from an ipns record', async () => { 409 | const sequence = 0 410 | const validity = 1000000 411 | 412 | const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) 413 | 414 | const publicKey = extractPublicKeyFromIPNSRecord(record) 415 | expect(publicKey?.equals(privateKey.publicKey)).to.be.true() 416 | }) 417 | 418 | it('should unmarshal a record with raw CID bytes', async () => { 419 | // we may encounter these in the wild due to older versions of this module 420 | // but IPNS records should have string path values 421 | 422 | // create a dummy record with an arbitrary string path 423 | const input = await createIPNSRecord(privateKey, '/foo', 0n, 10000, { 424 | v1Compatible: false 425 | }) 426 | 427 | // we will store the raw bytes from this CID 428 | const cid = CID.parse('bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu') 429 | 430 | // override data with raw CID bytes 431 | const data = cbor.decode(input.data) 432 | data.Value = cid.bytes 433 | input.data = cbor.encode(data) 434 | 435 | // re-sign record 436 | const sigData = ipnsRecordDataForV2Sig(input.data) 437 | input.signatureV2 = await privateKey.sign(sigData) 438 | 439 | const buf = marshalIPNSRecord(input) 440 | const record = unmarshalIPNSRecord(buf) 441 | 442 | expect(record).to.have.property('value', '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu') 443 | }) 444 | 445 | it('should round trip kubo records to bytes and back', async () => { 446 | // the IPNS spec gives an example for the Validity field as 447 | // 1970-01-01T00:00:00.000000001Z - e.g. nanosecond precision but Kubo only 448 | // uses microsecond precision. The value is a timestamp as defined by 449 | // rfc3339 which doesn't have a strong opinion on fractions of seconds so 450 | // both are valid but we must be able to round trip them intact. 451 | const unmarshalled = unmarshalIPNSRecord(kuboRecord.bytes) 452 | const remarhshalled = marshalIPNSRecord(unmarshalled) 453 | 454 | const reUnmarshalled = unmarshalIPNSRecord(remarhshalled) 455 | 456 | expect(unmarshalled).to.deep.equal(reUnmarshalled) 457 | expect(remarhshalled).to.equalBytes(kuboRecord.bytes) 458 | }) 459 | }) 460 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [10.1.3](https://github.com/ipfs/js-ipns/compare/v10.1.2...v10.1.3) (2025-10-14) 2 | 3 | ### Trivial Changes 4 | 5 | * add or force update .github/workflows/stale.yml ([#399](https://github.com/ipfs/js-ipns/issues/399)) ([2fd019a](https://github.com/ipfs/js-ipns/commit/2fd019acaa2122dc64c1a0a20e9eda3e6ebe8223)) 6 | 7 | ### Dependencies 8 | 9 | * bump the interplanetary-deps group across 1 directory with 4 updates ([#401](https://github.com/ipfs/js-ipns/issues/401)) ([7d7a2a8](https://github.com/ipfs/js-ipns/commit/7d7a2a8699bb938420076b4e4ac74ab9c13d8866)) 10 | 11 | ## [10.1.2](https://github.com/ipfs/js-ipns/compare/v10.1.1...v10.1.2) (2025-07-11) 12 | 13 | ### Documentation 14 | 15 | * update readme examples ([#398](https://github.com/ipfs/js-ipns/issues/398)) ([8fec3ef](https://github.com/ipfs/js-ipns/commit/8fec3efa09606549bbbbf1f27c038bfdc3de26a2)), closes [#315](https://github.com/ipfs/js-ipns/issues/315) [#314](https://github.com/ipfs/js-ipns/issues/314) 16 | 17 | ## [10.1.1](https://github.com/ipfs/js-ipns/compare/v10.1.0...v10.1.1) (2025-07-11) 18 | 19 | ### Dependencies 20 | 21 | * **dev:** bump aegir from 45.1.3 to 47.0.20 ([#397](https://github.com/ipfs/js-ipns/issues/397)) ([476d3e4](https://github.com/ipfs/js-ipns/commit/476d3e48cfdc5c2d515a98dbf24807d4a05136e9)) 22 | 23 | ## [10.1.0](https://github.com/ipfs/js-ipns/compare/v10.0.2...v10.1.0) (2025-07-11) 24 | 25 | ### Features 26 | 27 | * add validFor function to return the validity interval ([#388](https://github.com/ipfs/js-ipns/issues/388)) ([5b1c273](https://github.com/ipfs/js-ipns/commit/5b1c2731ac4ab989e4b6538de9d0097a97ff16bb)) 28 | 29 | ## [10.0.2](https://github.com/ipfs/js-ipns/compare/v10.0.1...v10.0.2) (2025-03-18) 30 | 31 | ### Bug Fixes 32 | 33 | * align implicit default ttl with specs (1h→5m) ([#336](https://github.com/ipfs/js-ipns/issues/336)) ([5677cf0](https://github.com/ipfs/js-ipns/commit/5677cf0c396d64af2f165b929cbd81ddb96f37b0)) 34 | 35 | ### Trivial Changes 36 | 37 | * Create FUNDING.json for OP RPF ([#335](https://github.com/ipfs/js-ipns/issues/335)) ([de2e786](https://github.com/ipfs/js-ipns/commit/de2e78629ae114adf02e680dbff5c49b3c4dc3a7)) 38 | 39 | ### Dependencies 40 | 41 | * **dev:** bump aegir from 44.1.4 to 45.0.8 ([#332](https://github.com/ipfs/js-ipns/issues/332)) ([c2d611a](https://github.com/ipfs/js-ipns/commit/c2d611aa508c10539ac5f354c19b9a378e71e759)) 42 | 43 | ## [10.0.1](https://github.com/ipfs/js-ipns/compare/v10.0.0...v10.0.1) (2025-02-11) 44 | 45 | ### Documentation 46 | 47 | * update api docs ([#333](https://github.com/ipfs/js-ipns/issues/333)) ([b73dac4](https://github.com/ipfs/js-ipns/commit/b73dac4773730902ee87ba5f7be6dbc22b2453e0)) 48 | 49 | ## [10.0.0](https://github.com/ipfs/js-ipns/compare/v9.1.2...v10.0.0) (2024-09-12) 50 | 51 | ### ⚠ BREAKING CHANGES 52 | 53 | * uses libp2p@2.x.x deps, operates on PrivateKey/PublicKeys instead of PeerIds 54 | 55 | ### Bug Fixes 56 | 57 | * update to libp2p@2.x.x deps ([#322](https://github.com/ipfs/js-ipns/issues/322)) ([316910c](https://github.com/ipfs/js-ipns/commit/316910cf83409bbcf7b681b82a3b3c4606908faf)) 58 | 59 | ## [9.1.2](https://github.com/ipfs/js-ipns/compare/v9.1.1...v9.1.2) (2024-09-12) 60 | 61 | ### Bug Fixes 62 | 63 | * add tests for utils ([#323](https://github.com/ipfs/js-ipns/issues/323)) ([3ec7233](https://github.com/ipfs/js-ipns/commit/3ec72338513f61cda5ae6fbe381de8529278c7d4)) 64 | 65 | ## [9.1.1](https://github.com/ipfs/js-ipns/compare/v9.1.0...v9.1.1) (2024-09-11) 66 | 67 | ### Dependencies 68 | 69 | * **dev:** bump aegir from 42.2.11 to 44.1.1 ([#321](https://github.com/ipfs/js-ipns/issues/321)) ([e06942b](https://github.com/ipfs/js-ipns/commit/e06942b1bf8a73ac0f30137a285146f03868152a)) 70 | 71 | ## [9.1.0](https://github.com/ipfs/js-ipns/compare/v9.0.0...v9.1.0) (2024-04-02) 72 | 73 | 74 | ### Features 75 | 76 | * change default TTL and add support for custom TTL ([#1](https://github.com/ipfs/js-ipns/issues/1)) ([#308](https://github.com/ipfs/js-ipns/issues/308)) ([d647529](https://github.com/ipfs/js-ipns/commit/d6475290941f356989847f6ab6b29c94a81039eb)), closes [/specs.ipfs.tech/ipns/ipns-record/#ttl-uint64](https://github.com/ipfs//specs.ipfs.tech/ipns/ipns-record//issues/ttl-uint64) [#310](https://github.com/ipfs/js-ipns/issues/310) 77 | 78 | 79 | ### Trivial Changes 80 | 81 | * add or force update .github/workflows/js-test-and-release.yml ([#311](https://github.com/ipfs/js-ipns/issues/311)) ([0c5f3e1](https://github.com/ipfs/js-ipns/commit/0c5f3e1b22b2c8b7c2653eb1dc4db35050f14ba4)) 82 | * add or force update .github/workflows/js-test-and-release.yml ([#312](https://github.com/ipfs/js-ipns/issues/312)) ([46a2b72](https://github.com/ipfs/js-ipns/commit/46a2b729fc984adea14d165dd800ec92abc45701)) 83 | * add or force update .github/workflows/js-test-and-release.yml ([#313](https://github.com/ipfs/js-ipns/issues/313)) ([e933496](https://github.com/ipfs/js-ipns/commit/e933496d93e553662b968019b57a2f8413fbefc6)) 84 | * Update .github/workflows/stale.yml [skip ci] ([16e0e10](https://github.com/ipfs/js-ipns/commit/16e0e10682fa9a663e0bb493a44d3e99a5200944)) 85 | 86 | ## [9.0.0](https://github.com/ipfs/js-ipns/compare/v8.0.4...v9.0.0) (2024-01-18) 87 | 88 | 89 | ### ⚠ BREAKING CHANGES 90 | 91 | * the validity field is now a string 92 | 93 | ### Bug Fixes 94 | 95 | * treat validity as opaque ([#307](https://github.com/ipfs/js-ipns/issues/307)) ([461190e](https://github.com/ipfs/js-ipns/commit/461190e215173e0ac2aad1dca107de5cb65a52ef)) 96 | 97 | ## [8.0.4](https://github.com/ipfs/js-ipns/compare/v8.0.3...v8.0.4) (2024-01-18) 98 | 99 | 100 | ### Bug Fixes 101 | 102 | * log type as string ([#306](https://github.com/ipfs/js-ipns/issues/306)) ([de68e4c](https://github.com/ipfs/js-ipns/commit/de68e4c0601702fb5d567a97e305b26f65c34fc2)) 103 | 104 | ## [8.0.3](https://github.com/ipfs/js-ipns/compare/v8.0.2...v8.0.3) (2024-01-16) 105 | 106 | 107 | ### Bug Fixes 108 | 109 | * mark package as side-effect free ([#305](https://github.com/ipfs/js-ipns/issues/305)) ([a389fe8](https://github.com/ipfs/js-ipns/commit/a389fe8f0e6dff4867ef22b6ddada43880476754)) 110 | 111 | ## [8.0.2](https://github.com/ipfs/js-ipns/compare/v8.0.1...v8.0.2) (2024-01-15) 112 | 113 | 114 | ### Dependencies 115 | 116 | * bump @libp2p/crypto from 3.0.4 to 4.0.0 ([#304](https://github.com/ipfs/js-ipns/issues/304)) ([ed83244](https://github.com/ipfs/js-ipns/commit/ed832448a9c903dc2ea0dd6158cc73211eacded7)) 117 | 118 | ## [8.0.1](https://github.com/ipfs/js-ipns/compare/v8.0.0...v8.0.1) (2024-01-12) 119 | 120 | 121 | ### Trivial Changes 122 | 123 | * Update .github/workflows/stale.yml [skip ci] ([e612553](https://github.com/ipfs/js-ipns/commit/e612553dab45bf102ef1f5b239fc4e13c0f96f3f)) 124 | 125 | 126 | ### Dependencies 127 | 128 | * **dev:** bump aegir from 41.3.5 to 42.1.1 ([#303](https://github.com/ipfs/js-ipns/issues/303)) ([9f0ab52](https://github.com/ipfs/js-ipns/commit/9f0ab52009d8163311fb116a87ebd5a3876c48c9)) 129 | 130 | ## [8.0.0](https://github.com/ipfs/js-ipns/compare/v7.0.2...v8.0.0) (2023-12-30) 131 | 132 | 133 | ### ⚠ BREAKING CHANGES 134 | 135 | * requires libp2p v1 or later 136 | 137 | ### Trivial Changes 138 | 139 | * rename master to main ([4f520b1](https://github.com/ipfs/js-ipns/commit/4f520b1946eacbed5ddb3dc567d2ba5423034ca0)) 140 | 141 | 142 | ### Dependencies 143 | 144 | * update libp2p deps to v1 ([#299](https://github.com/ipfs/js-ipns/issues/299)) ([5ae5b93](https://github.com/ipfs/js-ipns/commit/5ae5b934c391d652462765c102b6d01997a4d090)) 145 | 146 | ## [7.0.2](https://github.com/ipfs/js-ipns/compare/v7.0.1...v7.0.2) (2023-12-30) 147 | 148 | 149 | ### Dependencies 150 | 151 | * bump uint8arrays from 4.0.10 to 5.0.1 ([#297](https://github.com/ipfs/js-ipns/issues/297)) ([6043eab](https://github.com/ipfs/js-ipns/commit/6043eabe135c1659001a27657a602ca34c6ba3bc)) 152 | 153 | ## [7.0.1](https://github.com/ipfs/js-ipns/compare/v7.0.0...v7.0.1) (2023-09-15) 154 | 155 | 156 | ### Bug Fixes 157 | 158 | * add extra signature for empty options object ([#260](https://github.com/ipfs/js-ipns/issues/260)) ([ecbc699](https://github.com/ipfs/js-ipns/commit/ecbc699db81b0979520ed736df3550b2360d6d0e)) 159 | 160 | ## [7.0.0](https://github.com/ipfs/js-ipns/compare/v6.0.7...v7.0.0) (2023-09-15) 161 | 162 | 163 | ### ⚠ BREAKING CHANGES 164 | 165 | * all /ipns/* keys are now encoded as base36 encoded CIDv1 libp2p-cid 166 | 167 | ### Features 168 | 169 | * opt-in V2-only records, IPIP-428 verification ([#234](https://github.com/ipfs/js-ipns/issues/234)) ([df71fed](https://github.com/ipfs/js-ipns/commit/df71fedd29f15c4f5a93d3d6aaa4dc895e98ddc9)), closes [#217](https://github.com/ipfs/js-ipns/issues/217) 170 | 171 | ## [6.0.7](https://github.com/ipfs/js-ipns/compare/v6.0.6...v6.0.7) (2023-09-14) 172 | 173 | 174 | ### Bug Fixes 175 | 176 | * update libp2p interfaces to the latest version ([#259](https://github.com/ipfs/js-ipns/issues/259)) ([65f9d9b](https://github.com/ipfs/js-ipns/commit/65f9d9b328451d997ab99e3f5b7964ce4f7357f0)) 177 | 178 | 179 | ### Trivial Changes 180 | 181 | * update docs command ([#258](https://github.com/ipfs/js-ipns/issues/258)) ([705ef3e](https://github.com/ipfs/js-ipns/commit/705ef3efb94b76a598d9738612b8e83880eb87c9)) 182 | 183 | ## [6.0.6](https://github.com/ipfs/js-ipns/compare/v6.0.5...v6.0.6) (2023-09-14) 184 | 185 | 186 | ### Dependencies 187 | 188 | * bump cborg from 2.0.5 to 4.0.1 ([#257](https://github.com/ipfs/js-ipns/issues/257)) ([c51dc2f](https://github.com/ipfs/js-ipns/commit/c51dc2ff4d7b0e75ab51bb82f898e0b5f3acdd5d)) 189 | 190 | ## [6.0.5](https://github.com/ipfs/js-ipns/compare/v6.0.4...v6.0.5) (2023-08-24) 191 | 192 | 193 | ### Dependencies 194 | 195 | * bump @libp2p/peer-id from 2.0.4 to 3.0.2 ([#250](https://github.com/ipfs/js-ipns/issues/250)) ([69c52d7](https://github.com/ipfs/js-ipns/commit/69c52d70058172d6c61388b010f1e5c7f9b663e8)) 196 | 197 | ## [6.0.4](https://github.com/ipfs/js-ipns/compare/v6.0.3...v6.0.4) (2023-08-24) 198 | 199 | 200 | ### Trivial Changes 201 | 202 | * add or force update .github/workflows/js-test-and-release.yml ([#247](https://github.com/ipfs/js-ipns/issues/247)) ([3d3807f](https://github.com/ipfs/js-ipns/commit/3d3807f9a0ed5b1d5b06990c916a426d7c9200d0)) 203 | * delete templates [skip ci] ([#246](https://github.com/ipfs/js-ipns/issues/246)) ([c57dd46](https://github.com/ipfs/js-ipns/commit/c57dd46147816b78513ee0f22858fd8d12f2ec93)) 204 | * Update .github/workflows/stale.yml [skip ci] ([5139ee5](https://github.com/ipfs/js-ipns/commit/5139ee50b97670a1d713e891178cc6216f41a362)) 205 | * Update .github/workflows/stale.yml [skip ci] ([70735d1](https://github.com/ipfs/js-ipns/commit/70735d1d1f502f1b49b83433ca74ba30067fc23c)) 206 | 207 | 208 | ### Dependencies 209 | 210 | * bump @libp2p/crypto from 1.0.17 to 2.0.3 ([#249](https://github.com/ipfs/js-ipns/issues/249)) ([b12b1f7](https://github.com/ipfs/js-ipns/commit/b12b1f791eb0516a1357d95f91f3b1193013be08)) 211 | * bump @libp2p/logger from 2.1.1 to 3.0.2 ([#254](https://github.com/ipfs/js-ipns/issues/254)) ([73ba154](https://github.com/ipfs/js-ipns/commit/73ba154e73652731858cc836d2598f9bcaf4ac0b)) 212 | * bump cborg from 1.10.2 to 2.0.4 ([#252](https://github.com/ipfs/js-ipns/issues/252)) ([2c4d575](https://github.com/ipfs/js-ipns/commit/2c4d575aefd776dfe845699cfac6e5cab9800034)) 213 | * bump multiformats from 11.0.2 to 12.0.1 ([#229](https://github.com/ipfs/js-ipns/issues/229)) ([656fe3d](https://github.com/ipfs/js-ipns/commit/656fe3d13742fa1d6f33ce326631a2fd52dbf799)) 214 | * **dev:** bump @libp2p/peer-id-factory from 2.0.4 to 3.0.3 ([#251](https://github.com/ipfs/js-ipns/issues/251)) ([c6acf18](https://github.com/ipfs/js-ipns/commit/c6acf189dd81921e9416298e144e7047b0f175a2)) 215 | * **dev:** bump aegir from 39.0.13 to 40.0.11 ([#253](https://github.com/ipfs/js-ipns/issues/253)) ([45d81d7](https://github.com/ipfs/js-ipns/commit/45d81d743f33d638f624ae0e8a449f995c25578d)) 216 | 217 | ## [6.0.3](https://github.com/ipfs/js-ipns/compare/v6.0.2...v6.0.3) (2023-06-14) 218 | 219 | 220 | ### Bug Fixes 221 | 222 | * limit valid message size ([#226](https://github.com/ipfs/js-ipns/issues/226)) ([8a3e4f4](https://github.com/ipfs/js-ipns/commit/8a3e4f434701abf4ea997f50674a9659b2774c29)) 223 | 224 | 225 | ### Documentation 226 | 227 | * update readme and project ([#227](https://github.com/ipfs/js-ipns/issues/227)) ([3b587c2](https://github.com/ipfs/js-ipns/commit/3b587c2b7fa50501385a176a882e27d24ea6b6df)) 228 | 229 | ## [6.0.2](https://github.com/ipfs/js-ipns/compare/v6.0.1...v6.0.2) (2023-06-14) 230 | 231 | 232 | ### Bug Fixes 233 | 234 | * export extractPublicKey and update README ([#216](https://github.com/ipfs/js-ipns/issues/216)) ([eb34070](https://github.com/ipfs/js-ipns/commit/eb340700da56d89c0dc0e81d2da6dde1ed2d8ed9)), closes [#208](https://github.com/ipfs/js-ipns/issues/208) 235 | 236 | ## [6.0.1](https://github.com/ipfs/js-ipns/compare/v6.0.0...v6.0.1) (2023-06-14) 237 | 238 | 239 | ### Dependencies 240 | 241 | * **dev:** bump aegir from 38.1.8 to 39.0.10 ([#225](https://github.com/ipfs/js-ipns/issues/225)) ([1c9fce1](https://github.com/ipfs/js-ipns/commit/1c9fce138abaefbe6e704261aa10705ce9b39b1b)) 242 | 243 | ## [6.0.0](https://github.com/ipfs/js-ipns/compare/v5.0.2...v6.0.0) (2023-03-21) 244 | 245 | 246 | ### ⚠ BREAKING CHANGES 247 | 248 | * bump interface-datastore from 7.0.4 to 8.1.0 (#215) 249 | 250 | ### Dependencies 251 | 252 | * bump interface-datastore from 7.0.4 to 8.1.0 ([#215](https://github.com/ipfs/js-ipns/issues/215)) ([100799c](https://github.com/ipfs/js-ipns/commit/100799c97f177893417a70bcc8e5255013546031)) 253 | * bump protons-runtime from 4.0.2 to 5.0.0 ([#201](https://github.com/ipfs/js-ipns/issues/201)) ([60c3a15](https://github.com/ipfs/js-ipns/commit/60c3a156f78e407b2194ab51cc7c5ae9b3fc503d)) 254 | * **dev:** bump protons from 6.1.3 to 7.0.2 ([#204](https://github.com/ipfs/js-ipns/issues/204)) ([eb05501](https://github.com/ipfs/js-ipns/commit/eb05501647cc16efb34eb6a8ad7bc4263cdc84d9)) 255 | 256 | ## [5.0.2](https://github.com/ipfs/js-ipns/compare/v5.0.1...v5.0.2) (2023-03-21) 257 | 258 | 259 | ### Dependencies 260 | 261 | * **dev:** bump aegir from 37.12.1 to 38.1.7 ([#211](https://github.com/ipfs/js-ipns/issues/211)) ([9b7cca0](https://github.com/ipfs/js-ipns/commit/9b7cca02f4ed0a94ae23bd76a803508d4f75dbc9)) 262 | 263 | ## [5.0.1](https://github.com/ipfs/js-ipns/compare/v5.0.0...v5.0.1) (2023-01-07) 264 | 265 | 266 | ### Documentation 267 | 268 | * publish API docs ([#197](https://github.com/ipfs/js-ipns/issues/197)) ([ec88bf8](https://github.com/ipfs/js-ipns/commit/ec88bf8ce996fe049da336ef1ab7e1610706c1f6)) 269 | 270 | ## [5.0.0](https://github.com/ipfs/js-ipns/compare/v4.0.0...v5.0.0) (2023-01-07) 271 | 272 | 273 | ### ⚠ BREAKING CHANGES 274 | 275 | * update multiformats to v11 (#196) 276 | 277 | ### Dependencies 278 | 279 | * update multiformats to v11 ([#196](https://github.com/ipfs/js-ipns/issues/196)) ([e06d891](https://github.com/ipfs/js-ipns/commit/e06d891ecfff3001eb98226d6270b354c4ad4349)) 280 | 281 | ## [4.0.0](https://github.com/ipfs/js-ipns/compare/v3.0.0...v4.0.0) (2022-10-17) 282 | 283 | 284 | ### ⚠ BREAKING CHANGES 285 | 286 | * update multiformats, protons and uint8arrays (#189) 287 | 288 | ### Dependencies 289 | 290 | * update multiformats, protons and uint8arrays ([#189](https://github.com/ipfs/js-ipns/issues/189)) ([645c3b8](https://github.com/ipfs/js-ipns/commit/645c3b8c11c02ba43714b9806d0d2b0e0940217e)) 291 | 292 | ## [3.0.0](https://github.com/ipfs/js-ipns/compare/v2.0.3...v3.0.0) (2022-09-20) 293 | 294 | 295 | ### ⚠ BREAKING CHANGES 296 | 297 | * IPNS V1 signatures are ignored, records without V2 signature are no longer marked as Valid. 298 | 299 | ### Bug Fixes 300 | 301 | * require V2 signatures ([#180](https://github.com/ipfs/js-ipns/issues/180)) ([d522bcc](https://github.com/ipfs/js-ipns/commit/d522bccdacb645c887ca1ce566fe17eac1bcd1fd)) 302 | 303 | ## [2.0.3](https://github.com/ipfs/js-ipns/compare/v2.0.2...v2.0.3) (2022-08-14) 304 | 305 | 306 | ### Bug Fixes 307 | 308 | * ensure bigints are bigints ([#177](https://github.com/ipfs/js-ipns/issues/177)) ([4d1c0dd](https://github.com/ipfs/js-ipns/commit/4d1c0ddd6974f74494dd3b0a4defc56e51e75c3e)) 309 | 310 | ## [2.0.2](https://github.com/ipfs/js-ipns/compare/v2.0.1...v2.0.2) (2022-08-14) 311 | 312 | 313 | ### Dependencies 314 | 315 | * bump interface-datastore from 6.1.1 to 7.0.0 ([#176](https://github.com/ipfs/js-ipns/issues/176)) ([7f8caa0](https://github.com/ipfs/js-ipns/commit/7f8caa0538318d111da3b48e0503859308d161fd)) 316 | 317 | ## [2.0.1](https://github.com/ipfs/js-ipns/compare/v2.0.0...v2.0.1) (2022-08-11) 318 | 319 | 320 | ### Trivial Changes 321 | 322 | * Update .github/workflows/stale.yml [skip ci] ([116d5ec](https://github.com/ipfs/js-ipns/commit/116d5ecd8024a8edd70501e0b259bbe90587d089)) 323 | * update project config ([#174](https://github.com/ipfs/js-ipns/issues/174)) ([5200b95](https://github.com/ipfs/js-ipns/commit/5200b952699e0e32be0b62488d65ce1b3ec2e22a)) 324 | 325 | 326 | ### Dependencies 327 | 328 | * update protons to 5.1.0 ([#175](https://github.com/ipfs/js-ipns/issues/175)) ([4c50ec9](https://github.com/ipfs/js-ipns/commit/4c50ec9f750de1c154339396ebf30c9ea3c5fba7)) 329 | 330 | ## [2.0.0](https://github.com/ipfs/js-ipns/compare/v1.0.2...v2.0.0) (2022-06-28) 331 | 332 | 333 | ### ⚠ BREAKING CHANGES 334 | 335 | * update to new libp2p interface versions 336 | 337 | ### Features 338 | 339 | * update deps ([#163](https://github.com/ipfs/js-ipns/issues/163)) ([719925a](https://github.com/ipfs/js-ipns/commit/719925a8ae250bba1f0221613a7c430d82414391)) 340 | 341 | ### [1.0.2](https://github.com/ipfs/js-ipns/compare/v1.0.1...v1.0.2) (2022-05-25) 342 | 343 | 344 | ### Trivial Changes 345 | 346 | * **deps:** bump @libp2p/interfaces from 1.3.32 to 2.0.2 ([#159](https://github.com/ipfs/js-ipns/issues/159)) ([0f4bb9f](https://github.com/ipfs/js-ipns/commit/0f4bb9ff7f1831f080b79afb7d087416300e891c)) 347 | 348 | ### [1.0.1](https://github.com/ipfs/js-ipns/compare/v1.0.0...v1.0.1) (2022-05-10) 349 | 350 | 351 | ### Bug Fixes 352 | 353 | * encode enums correctly ([#156](https://github.com/ipfs/js-ipns/issues/156)) ([9267f06](https://github.com/ipfs/js-ipns/commit/9267f0645c980bf0727bf87a4316996a07cb2147)) 354 | 355 | ## [1.0.0](https://github.com/ipfs/js-ipns/compare/v0.16.0...v1.0.0) (2022-04-13) 356 | 357 | 358 | ### ⚠ BREAKING CHANGES 359 | 360 | * this module is now ESM-only 361 | 362 | ### Features 363 | 364 | * convert to typescript ([#154](https://github.com/ipfs/js-ipns/issues/154)) ([dd308f0](https://github.com/ipfs/js-ipns/commit/dd308f010311bd07a375270cf0ac505883bb740f)) 365 | 366 | # [0.16.0](https://github.com/ipfs/js-ipns/compare/v0.15.1...v0.16.0) (2021-12-02) 367 | 368 | 369 | ### chore 370 | 371 | * update deps ([#149](https://github.com/ipfs/js-ipns/issues/149)) ([87068ad](https://github.com/ipfs/js-ipns/commit/87068ad0c592f0eb481b7423598ad1054c462240)) 372 | 373 | 374 | ### BREAKING CHANGES 375 | 376 | * requires node 15+ 377 | 378 | 379 | 380 | ## [0.15.1](https://github.com/ipfs/js-ipns/compare/v0.15.0...v0.15.1) (2021-11-30) 381 | 382 | 383 | 384 | # [0.15.0](https://github.com/ipfs/js-ipns/compare/v0.14.1...v0.15.0) (2021-09-14) 385 | 386 | 387 | 388 | ## [0.14.1](https://github.com/ipfs/js-ipns/compare/v0.14.0...v0.14.1) (2021-09-10) 389 | 390 | 391 | ### chore 392 | 393 | * switch to ESM ([#136](https://github.com/ipfs/js-ipns/issues/136)) ([e4175cc](https://github.com/ipfs/js-ipns/commit/e4175ccf887d8ebc5590693759ef46b31a5ee18f)) 394 | 395 | 396 | ### BREAKING CHANGES 397 | 398 | * deep imports/requires are no longer possible 399 | 400 | 401 | 402 | # [0.14.0](https://github.com/ipfs/js-ipns/compare/v0.13.4...v0.14.0) (2021-09-02) 403 | 404 | 405 | ### Bug Fixes 406 | 407 | * update record selection rules ([#134](https://github.com/ipfs/js-ipns/issues/134)) ([fd1481a](https://github.com/ipfs/js-ipns/commit/fd1481a8fc00138d4543ca27050e080aefd8b31d)), closes [/github.com/ipfs/go-ipns/blob/a2d4e93f7e8ffc9f996471eb1a24ff12c8484120/ipns.go#L325-L362](https://github.com//github.com/ipfs/go-ipns/blob/a2d4e93f7e8ffc9f996471eb1a24ff12c8484120/ipns.go/issues/L325-L362) 408 | 409 | 410 | ### BREAKING CHANGES 411 | 412 | * extractPublicKey is now async 413 | 414 | 415 | 416 | ## [0.13.4](https://github.com/ipfs/js-ipns/compare/v0.13.2...v0.13.4) (2021-08-19) 417 | 418 | 419 | ### Bug Fixes 420 | 421 | * cbor keys should be pascal case ([#128](https://github.com/ipfs/js-ipns/issues/128)) ([1f8bcd3](https://github.com/ipfs/js-ipns/commit/1f8bcd379a7ba37daef0ceefc1ef69f4c15dab63)) 422 | 423 | 424 | 425 | ## [0.13.3](https://github.com/ipfs/js-ipns/compare/v0.13.2...v0.13.3) (2021-08-11) 426 | 427 | 428 | ### Bug Fixes 429 | 430 | * cbor keys should be pascal case ([#128](https://github.com/ipfs/js-ipns/issues/128)) ([1f8bcd3](https://github.com/ipfs/js-ipns/commit/1f8bcd379a7ba37daef0ceefc1ef69f4c15dab63)) 431 | 432 | 433 | 434 | ## [0.13.2](https://github.com/ipfs/js-ipns/compare/v0.12.0...v0.13.2) (2021-07-12) 435 | 436 | 437 | ### Bug Fixes 438 | 439 | * parse peer id from message correctly ([#127](https://github.com/ipfs/js-ipns/issues/127)) ([d7c8e51](https://github.com/ipfs/js-ipns/commit/d7c8e51c1505b1d75ef800d65c8896a3fe66d6d5)) 440 | 441 | 442 | ### chore 443 | 444 | * update deps ([#126](https://github.com/ipfs/js-ipns/issues/126)) ([063fc85](https://github.com/ipfs/js-ipns/commit/063fc85f831a33ebcbda45e2a9526712cc327d8c)) 445 | 446 | 447 | ### BREAKING CHANGES 448 | 449 | * uses new peer-id class and supporting ecosystem modules 450 | 451 | 452 | 453 | ## [0.13.1](https://github.com/ipfs/js-ipns/compare/v0.13.0...v0.13.1) (2021-07-10) 454 | 455 | 456 | 457 | # [0.13.0](https://github.com/ipfs/js-ipns/compare/v0.12.0...v0.13.0) (2021-07-09) 458 | 459 | 460 | ### chore 461 | 462 | * update deps ([#126](https://github.com/ipfs/js-ipns/issues/126)) ([063fc85](https://github.com/ipfs/js-ipns/commit/063fc85f831a33ebcbda45e2a9526712cc327d8c)) 463 | 464 | 465 | ### BREAKING CHANGES 466 | 467 | * uses new peer-id class and supporting ecosystem modules 468 | 469 | 470 | 471 | # [0.12.0](https://github.com/ipfs/js-ipns/compare/v0.11.0...v0.12.0) (2021-06-10) 472 | 473 | 474 | ### Features 475 | 476 | * validate v2 ipns signatures ([#121](https://github.com/ipfs/js-ipns/issues/121)) ([d1421f9](https://github.com/ipfs/js-ipns/commit/d1421f9389961ac6bf7b0ab4d80442fd7b78c14f)) 477 | 478 | 479 | 480 | # [0.11.0](https://github.com/ipfs/js-ipns/compare/v0.10.2...v0.11.0) (2021-04-21) 481 | 482 | 483 | ### Bug Fixes 484 | 485 | * ipns validate should return void ([#118](https://github.com/ipfs/js-ipns/issues/118)) ([67d0ad4](https://github.com/ipfs/js-ipns/commit/67d0ad40d10fced861b7b4825882f648b64528f1)) 486 | * specify pbjs root ([#119](https://github.com/ipfs/js-ipns/issues/119)) ([7cad961](https://github.com/ipfs/js-ipns/commit/7cad9611f87566047bc9e104be28970a877e9861)) 487 | 488 | 489 | ### BREAKING CHANGES 490 | 491 | * ipns validate function returns a void promise instead of boolean promise 492 | 493 | 494 | 495 | ## [0.10.2](https://github.com/ipfs/js-ipns/compare/v0.10.1...v0.10.2) (2021-04-15) 496 | 497 | 498 | 499 | ## [0.10.1](https://github.com/ipfs/js-ipns/compare/v0.10.0...v0.10.1) (2021-04-13) 500 | 501 | 502 | ### Bug Fixes 503 | 504 | * encode ipns key correctly ([#115](https://github.com/ipfs/js-ipns/issues/115)) ([a10889c](https://github.com/ipfs/js-ipns/commit/a10889c9bb0fdcb644081e7861d8a563249c6fd1)) 505 | 506 | 507 | 508 | # [0.10.0](https://github.com/ipfs/js-ipns/compare/v0.9.1...v0.10.0) (2021-03-10) 509 | 510 | 511 | ### Bug Fixes 512 | 513 | * remove value ambiguity ([#109](https://github.com/ipfs/js-ipns/issues/109)) ([5c589da](https://github.com/ipfs/js-ipns/commit/5c589da247d3103b2753eb435110ea6688e7bfa0)) 514 | 515 | 516 | ### BREAKING CHANGES 517 | 518 | * strings are no longer accepted as valid values to publish 519 | 520 | 521 | 522 | ## [0.9.1](https://github.com/ipfs/js-ipns/compare/v0.9.0...v0.9.1) (2021-03-09) 523 | 524 | 525 | ### Bug Fixes 526 | 527 | * add files list to package.json ([#108](https://github.com/ipfs/js-ipns/issues/108)) ([e990d8b](https://github.com/ipfs/js-ipns/commit/e990d8b3914f71356d5df3a8e3ad48eab4c05561)) 528 | 529 | 530 | 531 | # [0.9.0](https://github.com/ipfs/js-ipns/compare/v0.8.2...v0.9.0) (2021-03-05) 532 | 533 | 534 | ### Features 535 | 536 | * add types ([#106](https://github.com/ipfs/js-ipns/issues/106)) ([135552b](https://github.com/ipfs/js-ipns/commit/135552b3e0bcaa1b625d2b6789ff481e40b1c107)) 537 | 538 | 539 | 540 | ## [0.8.2](https://github.com/ipfs/js-ipns/compare/v0.8.1...v0.8.2) (2021-01-19) 541 | 542 | 543 | 544 | ## [0.8.1](https://github.com/ipfs/js-ipns/compare/v0.8.0...v0.8.1) (2020-12-22) 545 | 546 | 547 | 548 | 549 | # [0.8.0](https://github.com/ipfs/js-ipns/compare/v0.7.4...v0.8.0) (2020-08-14) 550 | 551 | 552 | ### Bug Fixes 553 | 554 | * replace node buffers with uint8arrays ([#67](https://github.com/ipfs/js-ipns/issues/67)) ([06ee535](https://github.com/ipfs/js-ipns/commit/06ee535)) 555 | 556 | 557 | ### BREAKING CHANGES 558 | 559 | * - All deps of this module use Uint8Arrays instead of Buffers 560 | - value and validity fields of IPNSEntries are now Uint8Arrays instead 561 | of Strings as they are `bytes` in the protobuf definition 562 | 563 | 564 | 565 | 566 | ## [0.7.4](https://github.com/ipfs/js-ipns/compare/v0.7.3...v0.7.4) (2020-08-06) 567 | 568 | 569 | 570 | 571 | ## [0.7.3](https://github.com/ipfs/js-ipns/compare/v0.7.2...v0.7.3) (2020-06-22) 572 | 573 | 574 | ### Bug Fixes 575 | 576 | * key encoding ([#48](https://github.com/ipfs/js-ipns/issues/48)) ([7c6c672](https://github.com/ipfs/js-ipns/commit/7c6c672)), closes [ipfs/js-ipfs#2930](https://github.com/ipfs/js-ipfs/issues/2930) 577 | 578 | 579 | 580 | 581 | ## [0.7.2](https://github.com/ipfs/js-ipns/compare/v0.7.1...v0.7.2) (2020-05-12) 582 | 583 | 584 | ### Bug Fixes 585 | 586 | * **ci:** add empty commit to fix lint checks on master ([929525f](https://github.com/ipfs/js-ipns/commit/929525f)) 587 | 588 | 589 | 590 | 591 | ## [0.7.1](https://github.com/ipfs/js-ipns/compare/v0.7.0...v0.7.1) (2020-04-24) 592 | 593 | 594 | ### Bug Fixes 595 | 596 | * add buffer and use multibase ([#34](https://github.com/ipfs/js-ipns/issues/34)) ([26eec66](https://github.com/ipfs/js-ipns/commit/26eec66)) 597 | 598 | 599 | 600 | 601 | # [0.7.0](https://github.com/ipfs/js-ipns/compare/v0.6.1...v0.7.0) (2019-12-18) 602 | 603 | 604 | ### Bug Fixes 605 | 606 | * remove unused left-pad ([#30](https://github.com/ipfs/js-ipns/issues/30)) ([2ab0fbd](https://github.com/ipfs/js-ipns/commit/2ab0fbd)) 607 | 608 | 609 | 610 | 611 | ## [0.6.1](https://github.com/ipfs/js-ipns/compare/v0.6.0...v0.6.1) (2019-09-25) 612 | 613 | 614 | 615 | 616 | # [0.6.0](https://github.com/ipfs/js-ipns/compare/v0.5.2...v0.6.0) (2019-07-19) 617 | 618 | 619 | ### Chores 620 | 621 | * convert from callbacks to async ([#19](https://github.com/ipfs/js-ipns/issues/19)) ([89e9903](https://github.com/ipfs/js-ipns/commit/89e9903)) 622 | 623 | 624 | ### BREAKING CHANGES 625 | 626 | * All places in the API that used callbacks are now replaced with async/await 627 | 628 | 629 | 630 | 631 | ## [0.5.2](https://github.com/ipfs/js-ipns/compare/v0.5.1...v0.5.2) (2019-05-23) 632 | 633 | 634 | ### Bug Fixes 635 | 636 | * month in RFC3339 util ([94bd20d](https://github.com/ipfs/js-ipns/commit/94bd20d)) 637 | * remove leftpad ([#22](https://github.com/ipfs/js-ipns/issues/22)) ([e04babc](https://github.com/ipfs/js-ipns/commit/e04babc)) 638 | 639 | 640 | 641 | 642 | ## [0.5.1](https://github.com/ipfs/js-ipns/compare/v0.4.4...v0.5.1) (2019-04-03) 643 | 644 | 645 | ### Bug Fixes 646 | 647 | * reduce bundle size ([#17](https://github.com/ipfs/js-ipns/issues/17)) ([a978c7d](https://github.com/ipfs/js-ipns/commit/a978c7d)) 648 | * verify public key exists in validator ([#21](https://github.com/ipfs/js-ipns/issues/21)) ([602e27f](https://github.com/ipfs/js-ipns/commit/602e27f)) 649 | 650 | 651 | ### BREAKING CHANGES 652 | 653 | * method createWithExpiration signature changed 654 | 655 | expiration param changed from time of the record (in nanoseconds) to datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision 656 | 657 | 658 | 659 | 660 | ## [0.5.0](https://github.com/ipfs/js-ipns/compare/v0.4.4...v0.5.0) (2019-01-10) 661 | 662 | 663 | ### Bug Fixes 664 | 665 | * reduce bundle size ([#17](https://github.com/ipfs/js-ipns/issues/17)) ([a978c7d](https://github.com/ipfs/js-ipns/commit/a978c7d)) 666 | 667 | 668 | ### BREAKING CHANGES 669 | 670 | * method createWithExpiration signature changed 671 | 672 | expiration param changed from time of the record (in nanoseconds) to datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision 673 | 674 | 675 | 676 | 677 | ## [0.4.4](https://github.com/ipfs/js-ipns/compare/v0.4.3...v0.4.4) (2019-01-04) 678 | 679 | 680 | 681 | 682 | ## [0.4.3](https://github.com/ipfs/js-ipns/compare/v0.4.2...v0.4.3) (2018-11-30) 683 | 684 | 685 | 686 | 687 | ## [0.4.2](https://github.com/ipfs/js-ipns/compare/v0.4.1...v0.4.2) (2018-11-29) 688 | 689 | 690 | ### Bug Fixes 691 | 692 | * validator select should return if no callback ([#15](https://github.com/ipfs/js-ipns/issues/15)) ([0845877](https://github.com/ipfs/js-ipns/commit/0845877)) 693 | 694 | 695 | 696 | 697 | ## [0.4.1](https://github.com/ipfs/js-ipns/compare/v0.4.0...v0.4.1) (2018-11-26) 698 | 699 | 700 | 701 | 702 | # [0.4.0](https://github.com/ipfs/js-ipns/compare/v0.3.0...v0.4.0) (2018-11-16) 703 | 704 | 705 | ### Bug Fixes 706 | 707 | * validator should create peer id ([#13](https://github.com/ipfs/js-ipns/issues/13)) ([e1c1332](https://github.com/ipfs/js-ipns/commit/e1c1332)) 708 | 709 | 710 | ### BREAKING CHANGES 711 | 712 | * having the libp2p-record protobuf definition compliant with go-libp2p-record. Author and signature were removed. 713 | 714 | 715 | 716 | 717 | # [0.3.0](https://github.com/ipfs/js-ipns/compare/v0.2.2...v0.3.0) (2018-10-26) 718 | 719 | 720 | 721 | 722 | ## [0.2.2](https://github.com/ipfs/js-ipns/compare/v0.2.1...v0.2.2) (2018-10-25) 723 | 724 | 725 | 726 | 727 | ## [0.2.1](https://github.com/ipfs/js-ipns/compare/v0.2.0...v0.2.1) (2018-09-24) 728 | 729 | 730 | ### Features 731 | 732 | * add creatWithExpiration function ([#9](https://github.com/ipfs/js-ipns/issues/9)) ([576bc1a](https://github.com/ipfs/js-ipns/commit/576bc1a)) 733 | 734 | 735 | 736 | 737 | # [0.2.0](https://github.com/ipfs/js-ipns/compare/v0.1.6...v0.2.0) (2018-09-20) 738 | 739 | 740 | 741 | 742 | ## [0.1.6](https://github.com/ipfs/js-ipns/compare/v0.1.5...v0.1.6) (2018-09-07) 743 | 744 | 745 | ### Bug Fixes 746 | 747 | * routing publick key format ([#8](https://github.com/ipfs/js-ipns/issues/8)) ([25c53cc](https://github.com/ipfs/js-ipns/commit/25c53cc)) 748 | 749 | 750 | 751 | 752 | ## [0.1.5](https://github.com/ipfs/js-ipns/compare/v0.1.4...v0.1.5) (2018-09-06) 753 | 754 | 755 | ### Bug Fixes 756 | 757 | * routing key format ([#7](https://github.com/ipfs/js-ipns/issues/7)) ([70036af](https://github.com/ipfs/js-ipns/commit/70036af)) 758 | 759 | 760 | 761 | 762 | ## [0.1.4](https://github.com/ipfs/js-ipns/compare/v0.1.2...v0.1.4) (2018-09-06) 763 | 764 | 765 | ### Bug Fixes 766 | 767 | * **security:** ensure validate is properly checking verify status ([33684e3](https://github.com/ipfs/js-ipns/commit/33684e3)) 768 | 769 | 770 | ### Features 771 | 772 | * add libp2p id key ([#6](https://github.com/ipfs/js-ipns/issues/6)) ([3d868fe](https://github.com/ipfs/js-ipns/commit/3d868fe)) 773 | * add records validator ([#5](https://github.com/ipfs/js-ipns/issues/5)) ([34468e1](https://github.com/ipfs/js-ipns/commit/34468e1)) 774 | 775 | 776 | 777 | 778 | ## [0.1.3](https://github.com/ipfs/js-ipns/compare/v0.1.2...v0.1.3) (2018-08-23) 779 | 780 | 781 | ### Bug Fixes 782 | 783 | * **security:** ensure validate is properly checking verify status ([33684e3](https://github.com/ipfs/js-ipns/commit/33684e3)) 784 | 785 | 786 | 787 | 788 | ## [0.1.2](https://github.com/ipfs/js-ipns/compare/v0.1.1...v0.1.2) (2018-08-09) 789 | 790 | 791 | ### Bug Fixes 792 | 793 | * local key for datastore ([439fcc0](https://github.com/ipfs/js-ipns/commit/439fcc0)) 794 | * readme license text ([981801e](https://github.com/ipfs/js-ipns/commit/981801e)) 795 | * typo in README ([d7c90ef](https://github.com/ipfs/js-ipns/commit/d7c90ef)) 796 | 797 | 798 | ### Features 799 | 800 | * add public key support ([#4](https://github.com/ipfs/js-ipns/issues/4)) ([e743632](https://github.com/ipfs/js-ipns/commit/e743632)) 801 | 802 | 803 | 804 | 805 | ## 0.1.1 (2018-06-29) 806 | 807 | 808 | ### Features 809 | 810 | * initial implementation ([b8eb65f](https://github.com/ipfs/js-ipns/commit/b8eb65f)) 811 | --------------------------------------------------------------------------------