diff --git a/src/bee.ts b/src/bee.ts index b086d101..e46e1239 100644 --- a/src/bee.ts +++ b/src/bee.ts @@ -30,6 +30,7 @@ import { assertPssMessageHandler, assertPublicKey, assertReference, + assertReferenceOrEns, assertRequestOptions, assertUploadOptions, makeTagUid, @@ -37,7 +38,17 @@ import { import { setJsonData, getJsonData } from './feed/json' import { makeCollectionFromFileList, assertCollection } from './utils/collection' import { makeCollectionFromFS } from './utils/collection.node' -import { AllTagsOptions, CHUNK_SIZE, Collection, Ky, Readable, RequestOptions, SPAN_SIZE, UploadResult } from './types' +import { + AllTagsOptions, + CHUNK_SIZE, + Collection, + Ky, + Readable, + ReferenceOrEns, + RequestOptions, + SPAN_SIZE, + UploadResult, +} from './types' import type { Options as KyOptions } from 'ky-universal' @@ -161,14 +172,16 @@ export class Bee { /** * Download data as a byte array * - * @param reference Bee data reference + * @param reference Bee data reference in hex string (either 64 or 128 chars long) or ENS domain. * @param options Options that affects the request behavior + * @throws TypeError if some of the input parameters is not expected type + * @throws BeeArgumentError if there is passed ENS domain with invalid unicode characters * @see [Bee docs - Upload and download](https://docs.ethswarm.org/docs/access-the-swarm/upload-and-download) * @see [Bee API reference - `GET /bytes`](https://docs.ethswarm.org/api/#tag/Bytes/paths/~1bytes~1{reference}/get) */ - async downloadData(reference: Reference | string, options?: RequestOptions): Promise { + async downloadData(reference: ReferenceOrEns | string, options?: RequestOptions): Promise { assertRequestOptions(options) - assertReference(reference) + assertReferenceOrEns(reference) return bytes.download(this.getKy(options), reference) } @@ -176,17 +189,19 @@ export class Bee { /** * Download data as a Readable stream * - * @param reference Bee data reference + * @param reference Bee data reference in hex string (either 64 or 128 chars long) or ENS domain. * @param options Options that affects the request behavior + * @throws TypeError if some of the input parameters is not expected type + * @throws BeeArgumentError if there is passed ENS domain with invalid unicode characters * @see [Bee docs - Upload and download](https://docs.ethswarm.org/docs/access-the-swarm/upload-and-download) * @see [Bee API reference - `GET /bytes`](https://docs.ethswarm.org/api/#tag/Bytes/paths/~1bytes~1{reference}/get) */ async downloadReadableData( - reference: Reference | string, + reference: ReferenceOrEns | string, options?: RequestOptions, ): Promise> { assertRequestOptions(options) - assertReference(reference) + assertReferenceOrEns(reference) return bytes.downloadReadable(this.getKy(options), reference) } @@ -225,14 +240,16 @@ export class Bee { /** * Download chunk as a byte array * - * @param reference Bee chunk reference + * @param reference Bee chunk reference in hex string (either 64 or 128 chars long) or ENS domain. * @param options Options that affects the request behavior + * @throws TypeError if some of the input parameters is not expected type + * @throws BeeArgumentError if there is passed ENS domain with invalid unicode characters * @see [Bee docs - Upload and download](https://docs.ethswarm.org/docs/access-the-swarm/upload-and-download) * @see [Bee API reference - `GET /chunks`](https://docs.ethswarm.org/api/#tag/Chunk/paths/~1chunks~1{reference}/get) */ - async downloadChunk(reference: Reference | string, options?: RequestOptions): Promise { + async downloadChunk(reference: ReferenceOrEns | string, options?: RequestOptions): Promise { assertRequestOptions(options) - assertReference(reference) + assertReferenceOrEns(reference) return chunk.download(this.getKy(options), reference) } @@ -289,17 +306,18 @@ export class Bee { /** * Download single file. * - * @param reference Bee file reference + * @param reference Bee file reference in hex string (either 64 or 128 chars long) or ENS domain. * @param path If reference points to manifest, then this parameter defines path to the file * @param options Options that affects the request behavior - * + * @throws TypeError if some of the input parameters is not expected type + * @throws BeeArgumentError if there is passed ENS domain with invalid unicode characters * @see Data * @see [Bee docs - Upload and download](https://docs.ethswarm.org/docs/access-the-swarm/upload-and-download) * @see [Bee API reference - `GET /bzz`](https://docs.ethswarm.org/api/#tag/Collection/paths/~1bzz~1{reference}~1{path}/get) */ - async downloadFile(reference: Reference | string, path = '', options?: RequestOptions): Promise> { + async downloadFile(reference: ReferenceOrEns | string, path = '', options?: RequestOptions): Promise> { assertRequestOptions(options) - assertReference(reference) + assertReferenceOrEns(reference) return bzz.downloadFile(this.getKy(options), reference, path) } @@ -307,20 +325,22 @@ export class Bee { /** * Download single file as a readable stream * - * @param reference Hash reference to file + * @param reference Bee file reference in hex string (either 64 or 128 chars long) or ENS domain. * @param path If reference points to manifest / collections, then this parameter defines path to the file * @param options Options that affects the request behavior + * @throws TypeError if some of the input parameters is not expected type + * @throws BeeArgumentError if there is passed ENS domain with invalid unicode characters * * @see [Bee docs - Upload and download](https://docs.ethswarm.org/docs/access-the-swarm/upload-and-download) * @see [Bee API reference - `GET /bzz`](https://docs.ethswarm.org/api/#tag/Collection/paths/~1bzz~1{reference}~1{path}/get) */ async downloadReadableFile( - reference: Reference | string, + reference: ReferenceOrEns | string, path = '', options?: RequestOptions, ): Promise>> { assertRequestOptions(options) - assertReference(reference) + assertReferenceOrEns(reference) return bzz.downloadFileReadable(this.getKy(options), reference, path) } @@ -566,9 +586,10 @@ export class Bee { * * **Warning! Not allowed when node is in Gateway mode!** * - * @param reference Bee data reference + * @param reference Bee data reference in hex string (either 64 or 128 chars long) or ENS domain. * @param options Options that affects the request behavior - * @throws TypeError if reference is in not correct format + * @throws TypeError if some of the input parameters is not expected type + * @throws BeeArgumentError if there is passed ENS domain with invalid unicode characters * * @see [Bee docs - Pinning](https://docs.ethswarm.org/docs/access-the-swarm/pinning) */ @@ -582,16 +603,17 @@ export class Bee { /** * Instructs the Bee node to reupload a locally pinned data into the network. * - * @param reference + * @param reference Bee data reference to be re-uploaded in hex string (either 64 or 128 chars long) or ENS domain. * @param options Options that affects the request behavior * @throws BeeArgumentError if the reference is not locally pinned - * @throws TypeError if reference is in not correct format + * @throws TypeError if some of the input parameters is not expected type + * @throws BeeArgumentError if there is passed ENS domain with invalid unicode characters * * @see [Bee API reference - `PUT /stewardship`](https://docs.ethswarm.org/api/#tag/Stewardship/paths/~1stewardship~1{reference}/put) */ - async reuploadPinnedData(reference: Reference | string, options?: RequestOptions): Promise { + async reuploadPinnedData(reference: ReferenceOrEns | string, options?: RequestOptions): Promise { assertRequestOptions(options) - assertReference(reference) + assertReferenceOrEns(reference) await stewardship.reupload(this.getKy(options), reference) } @@ -599,14 +621,16 @@ export class Bee { /** * Checks if content specified by reference is retrievable from the network. * - * @param reference The checked content + * @param reference Bee data reference to be checked in hex string (either 64 or 128 chars long) or ENS domain. * @param options Options that affects the request behavior + * @throws TypeError if some of the input parameters is not expected type + * @throws BeeArgumentError if there is passed ENS domain with invalid unicode characters * * @see [Bee API reference - `GET /stewardship`](https://docs.ethswarm.org/api/#tag/Stewardship/paths/~1stewardship~1{reference}/get) */ - async isReferenceRetrievable(reference: Reference | string, options?: RequestOptions): Promise { + async isReferenceRetrievable(reference: ReferenceOrEns | string, options?: RequestOptions): Promise { assertRequestOptions(options) - assertReference(reference) + assertReferenceOrEns(reference) return stewardship.isRetrievable(this.getKy(options), reference) } diff --git a/src/modules/bytes.ts b/src/modules/bytes.ts index 53397880..a329cf5b 100644 --- a/src/modules/bytes.ts +++ b/src/modules/bytes.ts @@ -1,4 +1,4 @@ -import type { BatchId, Data, Ky, Reference, UploadOptions } from '../types' +import type { BatchId, Data, Ky, Reference, ReferenceOrEns, UploadOptions } from '../types' import { prepareData } from '../utils/data' import { extractUploadHeaders } from '../utils/headers' import { http } from '../utils/http' @@ -45,7 +45,7 @@ export async function upload( * @param ky * @param hash Bee content reference */ -export async function download(ky: Ky, hash: Reference): Promise { +export async function download(ky: Ky, hash: ReferenceOrEns): Promise { const response = await http(ky, { responseType: 'arraybuffer', path: `${endpoint}/${hash}`, @@ -60,7 +60,7 @@ export async function download(ky: Ky, hash: Reference): Promise { * @param ky * @param hash Bee content reference */ -export async function downloadReadable(ky: Ky, hash: Reference): Promise> { +export async function downloadReadable(ky: Ky, hash: ReferenceOrEns): Promise> { const response = await http>(ky, { responseType: 'stream', path: `${endpoint}/${hash}`, diff --git a/src/modules/bzz.ts b/src/modules/bzz.ts index 413774bd..e19c4e35 100644 --- a/src/modules/bzz.ts +++ b/src/modules/bzz.ts @@ -8,6 +8,7 @@ import { Ky, Readable, Reference, + ReferenceOrEns, UploadHeaders, UploadResult, } from '../types' @@ -83,7 +84,7 @@ export async function uploadFile( * @param hash Bee file or collection hash * @param path If hash is collection then this defines path to a single file in the collection */ -export async function downloadFile(ky: Ky, hash: string, path = ''): Promise> { +export async function downloadFile(ky: Ky, hash: ReferenceOrEns, path = ''): Promise> { const response = await http(ky, { method: 'GET', responseType: 'arraybuffer', @@ -106,7 +107,7 @@ export async function downloadFile(ky: Ky, hash: string, path = ''): Promise>> { const response = await http>(ky, { diff --git a/src/modules/chunk.ts b/src/modules/chunk.ts index 0be7e4a2..0bd2c1f9 100644 --- a/src/modules/chunk.ts +++ b/src/modules/chunk.ts @@ -1,4 +1,4 @@ -import type { BatchId, Data, Ky, Reference, ReferenceResponse, UploadOptions } from '../types' +import type { BatchId, Data, Ky, Reference, ReferenceOrEns, ReferenceResponse, UploadOptions } from '../types' import { extractUploadHeaders } from '../utils/headers' import { http } from '../utils/http' import { wrapBytesWithHelpers } from '../utils/bytes' @@ -44,7 +44,7 @@ export async function upload( * @param hash Bee content reference * */ -export async function download(ky: Ky, hash: string): Promise { +export async function download(ky: Ky, hash: ReferenceOrEns): Promise { const response = await http(ky, { responseType: 'arraybuffer', path: `${endpoint}/${hash}`, diff --git a/src/modules/stewardship.ts b/src/modules/stewardship.ts index 413237f3..3ce5c5e9 100644 --- a/src/modules/stewardship.ts +++ b/src/modules/stewardship.ts @@ -1,4 +1,4 @@ -import type { Ky, Reference } from '../types' +import type { Ky, ReferenceOrEns } from '../types' import { http } from '../utils/http' const stewardshipEndpoint = 'stewardship' @@ -10,7 +10,7 @@ const stewardshipEndpoint = 'stewardship' * @param options * @throws BeeResponseError if not locally pinned or invalid data */ -export async function reupload(ky: Ky, reference: Reference): Promise { +export async function reupload(ky: Ky, reference: ReferenceOrEns): Promise { await http(ky, { method: 'put', path: `${stewardshipEndpoint}/${reference}`, @@ -21,7 +21,7 @@ interface IsRetrievableResponse { isRetrievable: boolean } -export async function isRetrievable(ky: Ky, reference: Reference): Promise { +export async function isRetrievable(ky: Ky, reference: ReferenceOrEns): Promise { const response = await http(ky, { method: 'get', responseType: 'json', diff --git a/src/types/index.ts b/src/types/index.ts index 57b6c6d7..46fd41c7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -60,6 +60,12 @@ export const FEED_INDEX_HEX_LENGTH = 16 */ export type Reference = HexString | HexString +/** + * Type that represents either Swarm's reference in hex string or ESN domain (something.eth). + * This is the type used on all the download functions. + */ +export type ReferenceOrEns = Reference | string + export type PlainBytesReference = Bytes export type EncryptedBytesReference = Bytes export type BytesReference = PlainBytesReference | EncryptedBytesReference diff --git a/src/utils/type.ts b/src/utils/type.ts index 77b334db..7269ffcc 100644 --- a/src/utils/type.ts +++ b/src/utils/type.ts @@ -24,10 +24,11 @@ import { RequestOptions, PostageBatchOptions, CashoutOptions, + ReferenceOrEns, } from '../types' import { BeeArgumentError } from './error' import { isFile } from './file' -import { assertHexString, assertPrefixedHexString } from './hex' +import { assertHexString, assertPrefixedHexString, isHexString } from './hex' import { isReadable } from './stream' export function isUint8Array(obj: unknown): obj is Uint8Array { @@ -100,6 +101,45 @@ export function assertReference(value: unknown): asserts value is Reference { } } +export function assertReferenceOrEns(value: unknown): asserts value is ReferenceOrEns { + if (typeof value !== 'string') { + throw new TypeError('ReferenceOrEns has to be a string!') + } + + if (isHexString(value)) { + assertReference(value) + + return + } + + /** + * a.asdf - VALID + * test.eth - VALID + * ADAM.ETH - VALID + * ADAM UHLIR.ETH - INVALID + * test.whatever.eth - VALID + * -adg.ets - INVALID + * adg-.ets - INVALID + * as-a.com - VALID + * ethswarm.org - VALID + * http://asdf.asf - INVALID + * řš+ýí.šě+ř.čě - VALID + * tsg.asg?asg - INVALID + * tsg.asg:1599 - INVALID + * ethswarm.something- - INVALID + * ethswarm.-something - INVALID + * ethswarm.some-thing - VALID + */ + const DOMAIN_REGEX = /^(?:(?!-)[^.\/?:\s]{1,63}(? void): voi }) } +export function testReferenceOrEnsAssertions(executor: (input: unknown) => void): void { + it('should throw exception for bad ReferenceOrEns', async () => { + await expect(() => executor(1)).rejects.toThrow(TypeError) + await expect(() => executor(true)).rejects.toThrow(TypeError) + await expect(() => executor({})).rejects.toThrow(TypeError) + await expect(() => executor(null)).rejects.toThrow(TypeError) + await expect(() => executor(undefined)).rejects.toThrow(TypeError) + await expect(() => executor([])).rejects.toThrow(TypeError) + + // Not an valid hexstring (ZZZ) + await expect(() => executor('ZZZfb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cdd')).rejects.toThrow( + TypeError, + ) + + // Prefixed hexstring is not accepted + await expect(() => executor('0x634fb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cdd')).rejects.toThrow( + TypeError, + ) + + // Length mismatch + await expect(() => executor('4fb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cdd')).rejects.toThrow( + TypeError, + ) + + // ENS with invalid characters + await expect(() => executor('')).rejects.toThrow(TypeError) + await expect(() => executor('some space.eth')).rejects.toThrow(TypeError) + await expect(() => executor('-example.eth')).rejects.toThrow(TypeError) + await expect(() => executor('http://example.eth')).rejects.toThrow(TypeError) + }) +} + export function testAddressPrefixAssertions(executor: (input: unknown) => void): void { it('should throw exception for bad AddressPrefix', async () => { await expect(() => executor(1)).rejects.toThrow(TypeError) diff --git a/test/unit/bee-class.spec.ts b/test/unit/bee-class.spec.ts index 033f1df8..595b0b00 100644 --- a/test/unit/bee-class.spec.ts +++ b/test/unit/bee-class.spec.ts @@ -16,6 +16,7 @@ import { testBatchId, testChunkHash, testIdentity, + testJsonEns, testJsonHash, testJsonPayload, testJsonStringPayload, @@ -38,6 +39,7 @@ import { testEthAddressAssertions, testMakeSignerAssertions, testRequestOptionsAssertions, + testReferenceOrEnsAssertions, } from './assertions' import { FeedType } from '../../src/feed/type' import { isStrictlyObject } from '../../src/utils/type' @@ -115,7 +117,7 @@ describe('Bee class', () => { }) describe('downloadData', () => { - testReferenceAssertions(async (input: unknown) => { + testReferenceOrEnsAssertions(async (input: unknown) => { const bee = new Bee(MOCK_SERVER_URL) return bee.downloadData(input as string) @@ -126,6 +128,20 @@ describe('Bee class', () => { return bee.downloadData(testChunkHash, input as RequestOptions) }) + + it('should accept valid ENS domain', async () => { + downloadDataMock(testJsonEns).reply(200, testJsonStringPayload) + + const bee = new Bee(MOCK_SERVER_URL) + expect((await bee.downloadData(testJsonEns)).text()).toEqual(testJsonStringPayload) + }) + + it('should accept valid ENS subdomain', async () => { + downloadDataMock(`subdomain.${testJsonEns}`).reply(200, testJsonStringPayload) + + const bee = new Bee(MOCK_SERVER_URL) + expect((await bee.downloadData(`subdomain.${testJsonEns}`)).text()).toEqual(testJsonStringPayload) + }) }) describe('chunk', () => { @@ -147,7 +163,7 @@ describe('Bee class', () => { }) describe('downloadReadableData', () => { - testReferenceAssertions(async (input: unknown) => { + testReferenceOrEnsAssertions(async (input: unknown) => { const bee = new Bee(MOCK_SERVER_URL) return bee.downloadReadableData(input as string) @@ -193,7 +209,7 @@ describe('Bee class', () => { }) describe('downloadFile', () => { - testReferenceAssertions(async (input: unknown) => { + testReferenceOrEnsAssertions(async (input: unknown) => { const bee = new Bee(MOCK_SERVER_URL) return bee.downloadFile(input as string) @@ -207,7 +223,7 @@ describe('Bee class', () => { }) describe('downloadReadableFile', () => { - testReferenceAssertions(async (input: unknown) => { + testReferenceOrEnsAssertions(async (input: unknown) => { const bee = new Bee(MOCK_SERVER_URL) return bee.downloadReadableFile(input as string) @@ -466,7 +482,7 @@ describe('Bee class', () => { return bee.reuploadPinnedData(testChunkHash, input as RequestOptions) }) - testReferenceAssertions(async (input: unknown) => { + testReferenceOrEnsAssertions(async (input: unknown) => { const bee = new Bee(MOCK_SERVER_URL) return bee.reuploadPinnedData(input as string) diff --git a/test/utils.ts b/test/utils.ts index b4d643f3..05646750 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -444,6 +444,7 @@ export const testBatchId = 'ca6357a08e317d15ec560fef34e4c45f8f19f01c372aa70f1da7 export const testJsonPayload = [{ some: 'object' }] export const testJsonStringPayload = JSON.stringify(testJsonPayload) export const testJsonHash = '872a858115b8bee4408b1427b49e472883fdc2512d5a8f2d428b97ecc8f7ccfa' +export const testJsonEns = 'testing.eth' export const testIdentity = { privateKey: '634fb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cdd' as HexString,