From 89f7d22962dd2b5ac58cec9d287d78b52e0f6be9 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Sat, 30 Mar 2024 19:44:27 +0100 Subject: [PATCH 1/7] feat: Implement private shares --- packages/nest/package.json | 7 + packages/nest/src/exchange-key.ts | 88 +++++++ packages/nest/src/index.ts | 1 + packages/nest/src/mutations.ts | 1 + packages/nest/src/path.ts | 103 +++++++++ packages/nest/src/root-tree.ts | 10 + packages/nest/src/root-tree/basic.ts | 145 +++++++++++- packages/nest/src/transaction.ts | 332 +++++++++++++++++++++++++-- packages/nest/src/types.ts | 2 +- packages/nest/test/class.test.ts | 42 ++++ 10 files changed, 700 insertions(+), 31 deletions(-) create mode 100644 packages/nest/src/exchange-key.ts diff --git a/packages/nest/package.json b/packages/nest/package.json index 71e39ea..2189b79 100644 --- a/packages/nest/package.json +++ b/packages/nest/package.json @@ -36,6 +36,10 @@ "types": "./dist/src/events.d.ts", "default": "./dist/src/events.js" }, + "./exchange-key": { + "types": "./dist/src/exchange-key.d.ts", + "default": "./dist/src/exchange-key.js" + }, "./path": { "types": "./dist/src/path.d.ts", "default": "./dist/src/path.js" @@ -80,6 +84,9 @@ "events": [ "dist/src/events" ], + "exchange-key": [ + "dist/src/exchange-key" + ], "path": [ "dist/src/path" ], diff --git a/packages/nest/src/exchange-key.ts b/packages/nest/src/exchange-key.ts new file mode 100644 index 0000000..9308348 --- /dev/null +++ b/packages/nest/src/exchange-key.ts @@ -0,0 +1,88 @@ +import * as Uint8Arr from 'uint8arrays' +import { webcrypto } from './crypto.js' + +export class ExchangeKey { + key: CryptoKey + + constructor(key: CryptoKey) { + this.key = key + } + + // CONSTRUCTORS + + static async fromModulus(modulus: Uint8Array): Promise { + const keyData = { + kty: 'RSA', + n: Uint8Arr.toString(modulus, 'base64url'), + e: Uint8Arr.toString(new Uint8Array([0x01, 0x00, 0x01]), 'base64url'), + alg: 'RSA-OAEP-256', + ext: true, + } + + const key = await webcrypto.subtle.importKey( + 'jwk', + keyData, + { + name: 'RSA-OAEP', + hash: { name: 'SHA-256' }, + }, + false, + ['encrypt'] + ) + + return new ExchangeKey(key) + } + + // RELATED + + static async decrypt( + data: Uint8Array, + privateKey: CryptoKey + ): Promise { + const buffer = await webcrypto.subtle.decrypt( + { + name: 'RSA-OAEP', + }, + privateKey, + data + ) + + return new Uint8Array(buffer) + } + + static async generate(): Promise { + return await webcrypto.subtle.generateKey( + { + name: 'RSA-OAEP', + modulusLength: 2048, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: { name: 'SHA-256' }, + }, + true, + ['decrypt'] + ) + } + + // INSTANCE METHODS + + async encrypt(data: Uint8Array): Promise { + const encryptedData = await webcrypto.subtle.encrypt( + { + name: 'RSA-OAEP', + }, + this.key, + data + ) + + return new Uint8Array(encryptedData) + } + + async publicKeyModulus(): Promise { + const key = await webcrypto.subtle.exportKey('jwk', this.key) + if (key.n === undefined) throw new Error('key.n is undefined') + return Uint8Arr.fromString(key.n, 'base64url') + } +} + +// @ts-expect-error Required by the `wnfs` package +globalThis.ExchangeKey = ExchangeKey diff --git a/packages/nest/src/index.ts b/packages/nest/src/index.ts index 301bdf0..f1f480c 100644 --- a/packages/nest/src/index.ts +++ b/packages/nest/src/index.ts @@ -4,6 +4,7 @@ export * from './app-info.js' export * from './class.js' export * from './errors.js' export * from './events.js' +export * from './exchange-key.js' export * from './root-tree.js' export * from './root-tree/basic.js' export * from './transaction.js' diff --git a/packages/nest/src/mutations.ts b/packages/nest/src/mutations.ts index 448e17b..7eff837 100644 --- a/packages/nest/src/mutations.ts +++ b/packages/nest/src/mutations.ts @@ -20,6 +20,7 @@ import { searchLatest } from './common.js' export const TYPES: Record = { ADDED_OR_UPDATED: 'added-or-updated', REMOVED: 'removed', + SHARED: 'shared', } // PUBLIC diff --git a/packages/nest/src/path.ts b/packages/nest/src/path.ts index e3dfa81..0666587 100644 --- a/packages/nest/src/path.ts +++ b/packages/nest/src/path.ts @@ -3,9 +3,11 @@ import type { AppInfo } from './app-info.js' export enum RootBranch { + DID = 'did', Exchange = 'exchange', Private = 'private', Public = 'public', + Tally = 'tally', Unix = 'unix', Version = 'version', } @@ -75,6 +77,7 @@ export type Distinctive

= DistinctivePath

/** * Utility function to create a `DirectoryPath` * + * @param args * @group 🪺 :: START HERE */ export function directory

( @@ -87,6 +90,10 @@ export function directory( ...args: SegmentsNonEmpty ): DirectoryPath export function directory(...args: Segments): DirectoryPath +/** + * + * @param args + */ export function directory(...args: Segments): DirectoryPath { if (args.some((p) => p.includes('/'))) { throw new Error('Forward slashes `/` are not allowed') @@ -97,6 +104,7 @@ export function directory(...args: Segments): DirectoryPath { /** * Utility function to create a `FilePath` * + * @param args * @group 🪺 :: START HERE */ export function file

( @@ -104,6 +112,10 @@ export function file

( ): FilePath> export function file(...args: SegmentsNonEmpty): FilePath export function file(...args: Segments): FilePath +/** + * + * @param args + */ export function file(...args: Segments): FilePath { if (args.some((p) => p.includes('/'))) { throw new Error('Forward slashes `/` are not allowed') @@ -114,6 +126,8 @@ export function file(...args: Segments): FilePath { /** * Utility function to create a path based on the given `Kind` * + * @param kind + * @param args * @group 🪺 :: START HERE */ export function fromKind

( @@ -157,6 +171,11 @@ export function fromKind( kind: Kind, ...args: Segments ): DistinctivePath +/** + * + * @param kind + * @param args + */ export function fromKind( kind: Kind, ...args: Segments @@ -176,6 +195,8 @@ export function root(): DirectoryPath { /** * Utility function create an app data path. * + * @param partition + * @param app * @group 🪺 :: START HERE */ export function appData

( @@ -197,6 +218,12 @@ export function appData

( app: AppInfo, suffix: DistinctivePath ): DistinctivePath> +/** + * + * @param partition + * @param app + * @param suffix + */ export function appData

( partition: P, app: AppInfo, @@ -216,6 +243,7 @@ export function appData

( * * Leading forward slashes are removed too, so you can pass absolute paths. * + * @param path * @group POSIX */ export function fromPosix(path: string): DistinctivePath { @@ -231,6 +259,9 @@ export function fromPosix(path: string): DistinctivePath { * Directories will have the format `path/to/dir/` and * files will have the format `path/to/file`. * + * @param path + * @param options + * @param options.absolute * @group POSIX */ export function toPosix( @@ -248,6 +279,9 @@ export function toPosix( /** * Combine two `DistinctivePath`s. + * + * @param a + * @param b */ export function combine

( a: DirectoryPath>, @@ -309,6 +343,11 @@ export function combine( a: DirectoryPath, b: DistinctivePath ): DistinctivePath +/** + * + * @param a + * @param b + */ export function combine( a: DirectoryPath, b: DistinctivePath @@ -318,6 +357,8 @@ export function combine( /** * Is this `DistinctivePath` a directory? + * + * @param path */ export function isDirectory

( path: DistinctivePath

@@ -327,6 +368,8 @@ export function isDirectory

( /** * Is this `DistinctivePath` a file? + * + * @param path */ export function isFile

(path: DistinctivePath

): path is FilePath

{ return 'file' in path @@ -334,6 +377,9 @@ export function isFile

(path: DistinctivePath

): path is FilePath

{ /** * Is this `DistinctivePath` on the given `RootBranch`? + * + * @param rootBranch + * @param path */ export function isOnRootBranch( rootBranch: RootBranch, @@ -344,6 +390,9 @@ export function isOnRootBranch( /** * Is this `DistinctivePath` of the given `Partition`? + * + * @param partition + * @param path */ export function isPartition( partition: Partition, @@ -354,6 +403,8 @@ export function isPartition( /** * Is this a partitioned `DistinctivePath`? + * + * @param path */ export function isPartitioned

( path: DistinctivePath @@ -366,6 +417,8 @@ export function isPartitioned

( /** * Is this partitioned `DistinctivePath` non-empty? + * + * @param path */ export function isPartitionedNonEmpty

( path: DistinctivePath @@ -375,6 +428,8 @@ export function isPartitionedNonEmpty

( /** * Is this `DirectoryPath` a root directory? + * + * @param path */ export function isRootDirectory(path: DirectoryPath): boolean { return path.directory.length === 0 @@ -382,6 +437,9 @@ export function isRootDirectory(path: DirectoryPath): boolean { /** * Check if two `DistinctivePath` have the same `Partition`. + * + * @param a + * @param b */ export function isSamePartition( a: DistinctivePath, @@ -392,6 +450,9 @@ export function isSamePartition( /** * Check if two `DistinctivePath` are of the same kind. + * + * @param a + * @param b */ export function isSameKind( a: DistinctivePath, @@ -404,6 +465,8 @@ export function isSameKind( /** * What `Kind` of path are we dealing with? + * + * @param path */ export function kind

(path: DistinctivePath

): Kind { if (isDirectory(path)) return Kind.Directory @@ -412,6 +475,8 @@ export function kind

(path: DistinctivePath

): Kind { /** * What's the length of a path? + * + * @param path */ export function length(path: DistinctivePath): number { return unwrap(path).length @@ -419,6 +484,9 @@ export function length(path: DistinctivePath): number { /** * Map a `DistinctivePath`. + * + * @param fn + * @param path */ export function map( fn: (p: A) => B, @@ -431,6 +499,8 @@ export function map( /** * Get the parent directory of a `DistinctivePath`. + * + * @param path */ export function parent( path: DistinctivePath<[Partition, Segment, Segment, ...Segments]> @@ -455,6 +525,10 @@ export function parent(path: DistinctivePath<[]>): undefined export function parent( path: DistinctivePath ): DirectoryPath | undefined +/** + * + * @param path + */ export function parent( path: DistinctivePath ): DirectoryPath | undefined { @@ -465,6 +539,8 @@ export function parent( /** * Remove the `Partition` of a `DistinctivePath` (ie. the top-level directory) + * + * @param path */ export function removePartition( path: DistinctivePath @@ -496,6 +572,11 @@ export function replaceTerminus( path: DistinctivePath, terminus: string ): DistinctivePath +/** + * + * @param path + * @param terminus + */ export function replaceTerminus( path: DistinctivePath | DistinctivePath, terminus: string @@ -503,6 +584,10 @@ export function replaceTerminus( return combine(parent(path), fromKind(kind(path), terminus)) } +/** + * + * @param path + */ export function rootBranch( path: DistinctivePath ): { branch: RootBranch; rest: Segments } | undefined { @@ -539,6 +624,8 @@ export function rootBranch( /** * Get the last part of the path. + * + * @param path */ export function terminus( path: DistinctivePath> @@ -546,6 +633,10 @@ export function terminus( export function terminus(path: DistinctivePath>): string export function terminus(path: DistinctivePath): string export function terminus(path: DistinctivePath): string | undefined +/** + * + * @param path + */ export function terminus(path: DistinctivePath): string | undefined { const u = unwrap(path) if (u.length === 0) return undefined @@ -554,6 +645,8 @@ export function terminus(path: DistinctivePath): string | undefined { /** * Unwrap a `DistinctivePath`. + * + * @param path */ export function unwrap

(path: DistinctivePath

): P { if (isDirectory(path)) { @@ -567,6 +660,9 @@ export function unwrap

(path: DistinctivePath

): P { /** * Utility function to prefix a path with a `Partition`. + * + * @param partition + * @param path */ export function withPartition

( partition: P, @@ -592,6 +688,11 @@ export function withPartition

( partition: P, path: DistinctivePath ): DistinctivePath> +/** + * + * @param partition + * @param path + */ export function withPartition

( partition: P, path: DistinctivePath @@ -603,6 +704,8 @@ export function withPartition

( /** * Render a raw `Path` to a string for logging purposes. + * + * @param path */ export function log(path: Segments): string { return `[ ${path.join(', ')} ]` diff --git a/packages/nest/src/root-tree.ts b/packages/nest/src/root-tree.ts index 84def0e..a9b9304 100644 --- a/packages/nest/src/root-tree.ts +++ b/packages/nest/src/root-tree.ts @@ -8,17 +8,27 @@ import type { Modification } from './types.js' * The tree that ties different file systems together. */ export abstract class RootTree { + abstract did(): string | undefined + abstract replaceDID(did: string): Promise + + abstract exchangeRoot(): PublicDirectory + abstract replaceExchangeRoot(dir: PublicDirectory): Promise + abstract privateForest(): PrivateForest abstract replacePrivateForest( forest: PrivateForest, modifications: Modification[] ): Promise + abstract publicRoot(): PublicDirectory abstract replacePublicRoot( dir: PublicDirectory, modifications: Modification[] ): Promise + abstract shareCounter(): number + abstract increaseShareCounter(): Promise + abstract clone(): RootTree abstract store(): Promise diff --git a/packages/nest/src/root-tree/basic.ts b/packages/nest/src/root-tree/basic.ts index 78eefe6..154e8e2 100644 --- a/packages/nest/src/root-tree/basic.ts +++ b/packages/nest/src/root-tree/basic.ts @@ -2,6 +2,7 @@ import type { PBLink, PBNode } from '@ipld/dag-pb' import type { Blockstore } from 'interface-blockstore' import * as DagPB from '@ipld/dag-pb' +import * as JsonCodec from 'multiformats/codecs/json' import * as Raw from 'multiformats/codecs/raw' import * as Uint8Arrays from 'uint8arrays' @@ -25,37 +26,47 @@ import { webcrypto } from '../crypto.js' export class BasicRootTree implements RootTree { readonly #blockstore: Blockstore + readonly #did: string | undefined readonly #exchangeRoot: PublicDirectory readonly #privateForest: PrivateForest readonly #publicRoot: PublicDirectory + readonly #shareCounter: number readonly #unix: PBNode readonly #version: string constructor({ blockstore, + did, exchangeRoot, publicRoot, privateForest, + shareCounter, unix, version, }: { blockstore: Blockstore + did: string | undefined exchangeRoot: PublicDirectory privateForest: PrivateForest publicRoot: PublicDirectory + shareCounter: number unix: PBNode version: string }) { this.#blockstore = blockstore + this.#did = did this.#exchangeRoot = exchangeRoot this.#privateForest = privateForest this.#publicRoot = publicRoot + this.#shareCounter = shareCounter this.#unix = unix this.#version = version } /** * Create a new root tree. + * + * @param blockstore */ static async create(blockstore: Blockstore): Promise { const currentTime = new Date() @@ -63,9 +74,11 @@ export class BasicRootTree implements RootTree { return new BasicRootTree({ blockstore, + did: undefined, exchangeRoot: new PublicDirectory(currentTime), publicRoot: new PublicDirectory(currentTime), privateForest: await createPrivateForest(), + shareCounter: 0, unix: Unix.createDirectory(currentTime), version: Version.latest, }) @@ -73,6 +86,9 @@ export class BasicRootTree implements RootTree { /** * Load an existing root tree. + * + * @param blockstore + * @param cid */ static async fromCID( blockstore: Blockstore, @@ -85,21 +101,34 @@ export class BasicRootTree implements RootTree { const links = await linksFromCID(cid, blockstore) // Retrieve all pieces + /** + * + * @param name + * @param present + * @param missing + */ async function handleLink( name: string, present: (cid: CID) => Promise, missing: () => T | Promise ): Promise { if (links[name] === undefined) { - console.warn( - `Missing '${name}' link in the root tree from '${cid.toString()}'. Creating a new link.` - ) return await missing() } return await present(links[name]) } + const did = await handleLink( + RootBranch.DID, + async (cid) => { + const block = await blockstore.get(cid) + return new TextDecoder().decode(Raw.decode(block)) + }, + // eslint-disable-next-line unicorn/no-useless-undefined + () => undefined + ) + const exchangeRoot = await handleLink( RootBranch.Exchange, async (cid) => await PublicDirectory.load(cid.bytes, wnfsStore), @@ -124,10 +153,20 @@ export class BasicRootTree implements RootTree { () => Unix.createDirectory(currentTime) ) + const shareCounter = await handleLink( + RootBranch.Tally, + async (cid) => { + const block = await blockstore.get(cid) + return await JsonCodec.decode(block) + }, + () => 0 + ) + const version = await handleLink( RootBranch.Version, async (cid) => { - return new TextDecoder().decode(Raw.decode(await blockstore.get(cid))) + const block = await blockstore.get(cid) + return new TextDecoder().decode(Raw.decode(block)) }, () => Version.latest ) @@ -136,14 +175,52 @@ export class BasicRootTree implements RootTree { return new BasicRootTree({ blockstore, + did, exchangeRoot, publicRoot, privateForest, + shareCounter, unix, version, }) } + did(): string | undefined { + return this.#did + } + + async replaceDID(did: string): Promise { + return new BasicRootTree({ + blockstore: this.#blockstore, + + did, + exchangeRoot: this.#exchangeRoot, + publicRoot: this.#publicRoot, + privateForest: this.#privateForest, + shareCounter: this.#shareCounter, + unix: this.#unix, + version: this.#version, + }) + } + + exchangeRoot(): PublicDirectory { + return this.#exchangeRoot + } + + async replaceExchangeRoot(dir: PublicDirectory): Promise { + return new BasicRootTree({ + blockstore: this.#blockstore, + + did: this.#did, + exchangeRoot: dir, + publicRoot: this.#publicRoot, + privateForest: this.#privateForest, + shareCounter: this.#shareCounter, + unix: this.#unix, + version: this.#version, + }) + } + privateForest(): PrivateForest { return this.#privateForest } @@ -155,9 +232,11 @@ export class BasicRootTree implements RootTree { return new BasicRootTree({ blockstore: this.#blockstore, + did: this.#did, exchangeRoot: this.#exchangeRoot, publicRoot: this.#publicRoot, privateForest: forest, + shareCounter: this.#shareCounter, unix: this.#unix, version: this.#version, }) @@ -174,9 +253,11 @@ export class BasicRootTree implements RootTree { const treeWithNewPublicRoot = new BasicRootTree({ blockstore: this.#blockstore, + did: this.#did, exchangeRoot: this.#exchangeRoot, publicRoot: dir, privateForest: this.#privateForest, + shareCounter: this.#shareCounter, unix: this.#unix, version: this.#version, }) @@ -215,21 +296,43 @@ export class BasicRootTree implements RootTree { return new BasicRootTree({ blockstore: this.#blockstore, + did: this.#did, exchangeRoot: this.#exchangeRoot, publicRoot: dir, privateForest: this.#privateForest, + shareCounter: this.#shareCounter, unix: unixTree, version: this.#version, }) } + shareCounter(): number { + return this.#shareCounter + } + + async increaseShareCounter(): Promise { + return new BasicRootTree({ + blockstore: this.#blockstore, + + did: this.#did, + exchangeRoot: this.#exchangeRoot, + publicRoot: this.#publicRoot, + privateForest: this.#privateForest, + shareCounter: this.shareCounter() + 1, + unix: this.#unix, + version: this.#version, + }) + } + clone(): RootTree { return new BasicRootTree({ blockstore: this.#blockstore, + did: this.#did, exchangeRoot: this.#exchangeRoot, publicRoot: this.#publicRoot, privateForest: this.#privateForest, + shareCounter: this.#shareCounter, unix: this.#unix, version: this.#version, }) @@ -248,14 +351,30 @@ export class BasicRootTree implements RootTree { this.#blockstore ) + const tally = await Store.store( + JsonCodec.encode(this.#shareCounter), + JsonCodec.code, + this.#blockstore + ) + const version = await Store.store( Raw.encode(new TextEncoder().encode(this.#version)), Raw.code, this.#blockstore ) + // DID + const did = + this.#did === undefined + ? undefined + : await Store.store( + Raw.encode(new TextEncoder().encode(this.#did)), + Raw.code, + this.#blockstore + ) + // Store root tree - const links = [ + let links = [ { Name: RootBranch.Exchange, Hash: CID.decode(exchangeRoot as Uint8Array), @@ -268,6 +387,10 @@ export class BasicRootTree implements RootTree { Name: RootBranch.Public, Hash: CID.decode(publicRoot as Uint8Array), }, + { + Name: RootBranch.Tally, + Hash: tally, + }, { Name: RootBranch.Unix, Hash: unixTree, @@ -278,6 +401,15 @@ export class BasicRootTree implements RootTree { }, ] + if (did !== undefined) + links = [ + ...links, + { + Name: RootBranch.DID, + Hash: did, + }, + ] + const node = DagPB.createNode(new Uint8Array([8, 1]), links) // Fin @@ -317,6 +449,9 @@ async function createPrivateForest(): Promise { /** * Retrieve the links of a root tree. + * + * @param cid + * @param blockstore */ export async function linksFromCID( cid: CID, diff --git a/packages/nest/src/transaction.ts b/packages/nest/src/transaction.ts index 7da8466..874c383 100644 --- a/packages/nest/src/transaction.ts +++ b/packages/nest/src/transaction.ts @@ -2,7 +2,19 @@ import type { PrivateForest, PrivateNode } from 'wnfs' import type { Blockstore } from 'interface-blockstore' import { CID } from 'multiformats/cid' -import { AccessKey, PublicFile } from 'wnfs' +import { + AccessKey, + Name, + NameAccumulator, + PublicFile, + createShareName, + findLatestShareCounter, + receiveShare, + share, +} from 'wnfs' + +import type { Rng } from './rng.js' +import type { RootTree } from './root-tree.js' import * as Path from './path.js' import * as Mutations from './mutations.js' @@ -22,8 +34,7 @@ import { addOrIncreaseNameNumber, searchLatest } from './common.js' import { dataFromBytes, dataToBytes } from './data.js' import { partition as determinePartition, findPrivateNode } from './mounts.js' -import type { Rng } from './rng.js' -import type { RootTree } from './root-tree.js' +import { BasicRootTree } from './root-tree/basic.js' import type { AnySupportedDataType, @@ -39,8 +50,11 @@ import type { import type { MountedPrivateNodes, PrivateNodeQueryResult, + WnfsPublicResult, } from './types/internal.js' +import { ExchangeKey } from './exchange-key.js' + // CLASS export class TransactionContext { @@ -56,7 +70,14 @@ export class TransactionContext { path: Path.Distinctive> }> - /** @internal */ + /** + * @param blockstore + * @param onCommit + * @param privateNodes + * @param rng + * @param rootTree + * @internal + */ constructor( blockstore: Blockstore, onCommit: CommitVerifier, @@ -73,7 +94,10 @@ export class TransactionContext { this.#modifications = new Set() } - /** @internal */ + /** + * @param context + * @internal + */ static async commit(context: TransactionContext): Promise< | { modifications: Array<{ @@ -123,29 +147,38 @@ export class TransactionContext { // Fin return { - modifications: modifications, + modifications, privateNodes: context.#privateNodes, - rootTree: rootTree, + rootTree, } } // QUERIES - /** @group Querying */ + /** + * @param path + * @group Querying + */ async contentCID( path: Path.File> ): Promise { return await References.contentCID(this.#blockstore, this.#rootTree, path) } - /** @group Querying */ + /** + * @param path + * @group Querying + */ async capsuleCID( path: Path.Distinctive> ): Promise { return await References.capsuleCID(this.#blockstore, this.#rootTree, path) } - /** @group Querying */ + /** + * @param path + * @group Querying + */ async capsuleKey( path: Path.Distinctive> ): Promise { @@ -190,7 +223,10 @@ export class TransactionContext { }) } - /** @group Querying */ + /** + * @param path + * @group Querying + */ async exists( path: Path.Distinctive> ): Promise { @@ -245,7 +281,7 @@ export class TransactionContext { | { capsuleKey: Uint8Array }, - dataType: DataType, + dataType: D, options?: { offset?: number; length?: number } ): Promise> async read( @@ -307,7 +343,10 @@ export class TransactionContext { return dataFromBytes(dataType, bytes) } - /** @group Querying */ + /** + * @param path + * @group Querying + */ async size(path: Path.File>): Promise { return await this.#query(path, { public: Queries.publicSize(), @@ -317,7 +356,11 @@ export class TransactionContext { // MUTATIONS - /** @group Mutating */ + /** + * @param fromParam + * @param toParam + * @group Mutating + */ async copy( fromParam: Path.Distinctive>, toParam: @@ -349,7 +392,10 @@ export class TransactionContext { /** @group Mutating */ cp = this.copy // eslint-disable-line @typescript-eslint/unbound-method - /** @group Mutating */ + /** + * @param path + * @group Mutating + */ async createDirectory( path: Path.Directory> ): Promise<{ path: Path.Directory> }> { @@ -358,11 +404,16 @@ export class TransactionContext { return await this.createDirectory(newPath) } else { await this.ensureDirectory(path) - return { path: path } + return { path } } } - /** @group Mutating */ + /** + * @param path + * @param dataType + * @param data + * @group Mutating + */ async createFile( path: Path.File>, dataType: DataType, @@ -373,11 +424,14 @@ export class TransactionContext { return await this.createFile(newPath, dataType, data) } else { await this.write(path, dataType, data) - return { path: path } + return { path } } } - /** @group Mutating */ + /** + * @param path + * @group Mutating + */ async ensureDirectory( path: Path.Directory> ): Promise { @@ -407,7 +461,11 @@ export class TransactionContext { /** @group Mutating */ mkdir = this.ensureDirectory // eslint-disable-line @typescript-eslint/unbound-method - /** @group Mutating */ + /** + * @param fromParam + * @param toParam + * @group Mutating + */ async move( fromParam: Path.Distinctive>, toParam: @@ -428,7 +486,10 @@ export class TransactionContext { /** @group Mutating */ mv = this.move // eslint-disable-line @typescript-eslint/unbound-method - /** @group Mutating */ + /** + * @param path + * @group Mutating + */ async remove( path: Path.Distinctive> ): Promise { @@ -458,7 +519,11 @@ export class TransactionContext { /** @group Mutating */ rm = this.remove // eslint-disable-line @typescript-eslint/unbound-method - /** @group Mutating */ + /** + * @param path + * @param newName + * @group Mutating + */ async rename( path: Path.Distinctive>, newName: string @@ -469,7 +534,12 @@ export class TransactionContext { await this.move(fromPath, toPath) } - /** @group Mutating */ + /** + * @param path + * @param dataType + * @param data + * @group Mutating + */ async write( path: Path.File>, dataType: DataType, @@ -499,6 +569,218 @@ export class TransactionContext { } } + // IDENTITY + + async assignIdentifier(did: string): Promise { + this.#rootTree = await this.#rootTree.replaceDID(did) + } + + identifier(): string | undefined { + return this.#rootTree.did() + } + + // SHARING + + /** + * Register an exchange key. + * + * @param name A name for the key + * @param publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes + * + * @group Sharing + */ + async registerExchangeKey( + name: string, + publicKey: CryptoKey | Uint8Array + ): Promise { + const publicDir = this.#rootTree.exchangeRoot() + const publicKeyResult = + publicKey instanceof CryptoKey + ? await new ExchangeKey(publicKey).publicKeyModulus() + : publicKey + + // Add public key to exchange root + const result: WnfsPublicResult = await publicDir.write( + [name, 'v1.exchange_key'], + publicKeyResult, + new Date(), + Store.wnfs(this.#blockstore) + ) + + // Replace public root + this.#rootTree = await this.#rootTree.replaceExchangeRoot(result.rootDir) + + // Return public key + return publicKeyResult + } + + /** + * Load a shared item. + * + * NOTE: A share can only be received if the exchange key was registered + * and the receiver is in possession of the associated private key. + * + * @param itemName + * @param sharerDataRoot The data root CID from the sharer + * @param publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes + * + * @param opts Optional overrides + * @param opts.sharerBlockstore Specify what blockstore to use to load the sharer's file system + * @param opts.sharerRootTreeClass Specify what root tree class was used for the sharer's file system + * @param privateKey + */ + async receive( + itemName: string, + sharerDataRoot: CID, + publicKey: CryptoKey | Uint8Array, + privateKey: CryptoKey, + opts: { + sharerBlockstore?: Blockstore + sharerRootTreeClass?: typeof RootTree + } = {} + ): Promise { + const publicKeyResult = + publicKey instanceof CryptoKey + ? await new ExchangeKey(publicKey).publicKeyModulus() + : publicKey + + const sharerBlockstore = opts.sharerBlockstore ?? this.#blockstore + const sharerRootTreeClass = opts.sharerRootTreeClass ?? BasicRootTree + const sharerRootTree = await sharerRootTreeClass.fromCID( + sharerBlockstore, + sharerDataRoot + ) + + const sharerForest = sharerRootTree.privateForest() + const sharerCounter = sharerRootTree.shareCounter() + const sharerIdentifier = sharerRootTree.did() + if (sharerIdentifier === undefined) + throw new Error("The sharer's file system is missing an identifier") + + // TODO: Check if this works, not sure if `findLatestShareCounter` returns a result object or not + const shareNumber: bigint = await findLatestShareCounter( + 0, + sharerCounter < 1 ? 1 : sharerCounter, + publicKeyResult, + sharerIdentifier, + sharerForest, + Store.wnfs(sharerBlockstore) + ) + + // Determine share name + const shareLabel = createShareName( + Number(shareNumber), + sharerIdentifier, + publicKeyResult, + sharerForest + ) + + const shareLabelSerialized = shareLabel + .toNameAccumulator(sharerForest) + .toBytes() + const shareLabelDeserialized = new Name( + NameAccumulator.fromBytes(shareLabelSerialized) + ) + + // Load shared private node + const sharedNode: PrivateNode = await receiveShare( + shareLabelDeserialized, + { + decrypt: async (data: Uint8Array) => + await ExchangeKey.decrypt(data, privateKey), + }, + sharerForest, + Store.wnfs(sharerBlockstore) + ) + + // Create transaction context + const path = Path.fromKind( + sharedNode.isFile() ? Path.Kind.File : Path.Kind.Directory, + itemName + ) + + return new TransactionContext( + sharerBlockstore, + async () => ({ commit: false }), + { [Path.toPosix(path, { absolute: true })]: { path, node: sharedNode } }, + this.#rng, + sharerRootTree + ) + } + + /** + * Share a private file or directory. + * + * NOTE: A share can only be received if the exchange key was registered + * and the receiver is in possession of the associated private key. + * + * @param path Path to the private file or directory to share (with 'private' prefix) + * @param receiverDataRoot Data root CID of the receiver + * @param opts Optional overrides + * @param opts.receiverBlockstore Specify what blockstore to use to load the receiver's file system + * @param opts.receiverRootTreeClass Specify what root tree class was used for the receiver's file system + * + * @group Sharing + */ + async share( + path: Path.Distinctive>, + receiverDataRoot: CID, + opts: { + receiverBlockstore?: Blockstore + receiverRootTreeClass?: typeof RootTree + } = {} + ): Promise { + const did = this.identifier() + + if (did === undefined) + throw new Error( + "Identifier wasn't set yet. Set one first using `assignIdentifier`." + ) + + // Access key + const key = await this.capsuleKey(path) + if (key === undefined) throw new Error('Nothing exists at the given path.') + + // Counter + const counter = this.#rootTree.shareCounter() + + // Determine exchange root CID + const receiverBlockstore = opts.receiverBlockstore ?? this.#blockstore + const receiverRootTreeClass = opts.receiverRootTreeClass ?? BasicRootTree + const receiverRootTree = await receiverRootTreeClass.fromCID( + receiverBlockstore, + receiverDataRoot + ) + + const exchangeRoot: Uint8Array = await receiverRootTree + .exchangeRoot() + .store(Store.wnfs(receiverBlockstore)) + + // Create share + const forest: PrivateForest = await share( + AccessKey.fromBytes(key), + counter, + did, + exchangeRoot, + this.#rootTree.privateForest(), + Store.wnfs(this.#blockstore) + ) + + // Update counter + await this.#rootTree.increaseShareCounter() + + // Modification + const change = { + type: Mutations.TYPES.Shared, + path, + } + + this.#modifications.add(change) + + // Replace root tree + this.#rootTree = await this.#rootTree.replacePrivateForest(forest, [change]) + } + // ㊙️ ▒▒ QUERIES async #query( @@ -587,7 +869,7 @@ export class TransactionContext { ): Promise { const mod = { type: mutType, - path: path, + path, } const result = await mut({ @@ -613,7 +895,7 @@ export class TransactionContext { const priv = findPrivateNode(path, this.#privateNodes) const change = { type: mutType, - path: path, + path, } // Perform mutation diff --git a/packages/nest/src/types.ts b/packages/nest/src/types.ts index 18490cc..6e89963 100644 --- a/packages/nest/src/types.ts +++ b/packages/nest/src/types.ts @@ -48,7 +48,7 @@ export type MutationResult< ? PrivateMutationResult : never -export type MutationType = 'added-or-updated' | 'removed' +export type MutationType = 'added-or-updated' | 'removed' | 'shared' export type PartitionDiscovery

= P extends Path.Public ? { diff --git a/packages/nest/test/class.test.ts b/packages/nest/test/class.test.ts index 512d149..66d74ab 100644 --- a/packages/nest/test/class.test.ts +++ b/packages/nest/test/class.test.ts @@ -10,6 +10,7 @@ import { MemoryBlockstore } from 'blockstore-core/memory' import * as Path from '../src/path.js' import type { Modification } from '../src/types.js' +import { ExchangeKey } from '../src/exchange-key.js' import { FileSystem } from '../src/class.js' import { @@ -1127,4 +1128,45 @@ describe('File System Class', () => { assert.equal(result, 'no-op') }) + + // SHARING + + it('can share a file using a transaction and receive it', async () => { + const keypair = await ExchangeKey.generate() + + const receiverFs = await FileSystem.create({ + blockstore, + ...fsOpts, + }) + + await receiverFs.transaction(async (t) => { + await t.registerExchangeKey('device', keypair.publicKey) + }) + + const receiverDataRoot = await receiverFs.calculateDataRoot() + + await fs.transaction(async (t) => { + const path = Path.file('private', 'fileToShare') + await t.assignIdentifier('did:test:1') + await t.write(path, 'utf8', '🔒') + await t.share(path, receiverDataRoot) + }) + + const sharerDataRoot = await fs.calculateDataRoot() + + let content = '' + + await receiverFs.transaction(async (t) => { + const ctx = await t.receive( + 'fileToShare', + sharerDataRoot, + keypair.publicKey, + keypair.privateKey + ) + + content = await ctx.read(Path.file('private', 'fileToShare'), 'utf8') + }) + + assert.equal(content, '🔒') + }) }) From d1a510459aef0d6b400a50b522fd5d2c3961063a Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Sat, 30 Mar 2024 21:54:37 +0100 Subject: [PATCH 2/7] feat: Add share class methods --- examples/audio/package.json | 3 +- examples/demo/package.json | 1 - examples/web3storage/package.json | 3 +- examples/web3storage/rsbuild.config.ts | 6 +- packages/nest/src/class.ts | 191 +++++++++++++++++++++++-- packages/nest/src/transaction.ts | 73 +++++----- packages/nest/test/class.test.ts | 39 +++++ 7 files changed, 264 insertions(+), 52 deletions(-) diff --git a/examples/audio/package.json b/examples/audio/package.json index a4b978e..3d6933a 100644 --- a/examples/audio/package.json +++ b/examples/audio/package.json @@ -1,7 +1,6 @@ { - "name": "demo", + "name": "audio", "type": "module", - "version": "1.0.0", "private": true, "description": "", "author": "Steven Vandevelde (tokono.ma)", diff --git a/examples/demo/package.json b/examples/demo/package.json index 3a91669..b414c44 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -1,7 +1,6 @@ { "name": "demo", "type": "module", - "version": "1.0.0", "private": true, "description": "", "author": "Steven Vandevelde (tokono.ma)", diff --git a/examples/web3storage/package.json b/examples/web3storage/package.json index 65b2542..0874cef 100644 --- a/examples/web3storage/package.json +++ b/examples/web3storage/package.json @@ -1,7 +1,6 @@ { - "name": "demo", + "name": "web3storage", "type": "module", - "version": "1.0.0", "private": true, "description": "", "author": "Steven Vandevelde (tokono.ma)", diff --git a/examples/web3storage/rsbuild.config.ts b/examples/web3storage/rsbuild.config.ts index 7d9c18b..e8a32fc 100644 --- a/examples/web3storage/rsbuild.config.ts +++ b/examples/web3storage/rsbuild.config.ts @@ -1,7 +1,7 @@ -import { defineConfig } from "@rsbuild/core"; +import { defineConfig } from '@rsbuild/core' export default defineConfig({ html: { - template: "./src/index.html", + template: './src/index.html', }, -}); +}) diff --git a/packages/nest/src/class.ts b/packages/nest/src/class.ts index dc75d3a..a03f4f2 100644 --- a/packages/nest/src/class.ts +++ b/packages/nest/src/class.ts @@ -80,7 +80,13 @@ export class FileSystem { #privateNodes: MountedPrivateNodes = {} #rootTree: RootTree - /** @hidden */ + /** + * @param blockstore + * @param onCommit + * @param rootTree + * @param settleTimeBeforePublish + * @hidden + */ constructor( blockstore: Blockstore, onCommit: CommitVerifier | undefined, @@ -113,6 +119,7 @@ export class FileSystem { /** * Creates a file system with an empty public tree & an empty private tree at the root. * + * @param opts * @group 🪺 :: START HERE */ static async create(opts: FileSystemOptions): Promise { @@ -131,6 +138,8 @@ export class FileSystem { /** * Loads an existing file system from a CID. * + * @param cid + * @param opts * @group 🪺 :: START HERE */ static async fromCID(cid: CID, opts: FileSystemOptions): Promise { @@ -156,6 +165,8 @@ export class FileSystem { /** * {@inheritDoc Emittery.on} * + * @param eventName + * @param listener * @group Events */ on = ( @@ -166,6 +177,7 @@ export class FileSystem { /** * {@inheritDoc Emittery.onAny} * + * @param listener * @group Events */ onAny = ( @@ -178,6 +190,8 @@ export class FileSystem { /** * {@inheritDoc Emittery.off} * + * @param eventName + * @param listener * @group Events */ off = ( @@ -190,6 +204,7 @@ export class FileSystem { /** * {@inheritDoc Emittery.offAny} * + * @param listener * @group Events */ offAny = ( @@ -204,6 +219,7 @@ export class FileSystem { /** * {@inheritDoc Emittery.once} * + * @param eventName * @group Events */ once = ( @@ -222,6 +238,7 @@ export class FileSystem { /** * {@inheritDoc Emittery.events} * + * @param eventName * @group Events */ events = ( @@ -234,6 +251,9 @@ export class FileSystem { /** * Mount a private node onto the file system. * + * @param node + * @param node.path + * @param node.capsuleKey * @group Mounting */ async mountPrivateNode(node: { @@ -253,6 +273,7 @@ export class FileSystem { * When a `capsuleKey` is not given, * it will create the given path instead of trying to load it. * + * @param nodes * @group Mounting */ async mountPrivateNodes( @@ -336,6 +357,7 @@ export class FileSystem { /** * Unmount a private node from the file system. * + * @param path * @group Mounting */ unmountPrivateNode(path: Path.Distinctive): void { @@ -346,28 +368,40 @@ export class FileSystem { // QUERY // ----- - /** @group Querying */ + /** + * @param path + * @group Querying + */ async contentCID( path: Path.File> ): Promise { return await this.#transactionContext().contentCID(path) } - /** @group Querying */ + /** + * @param path + * @group Querying + */ async capsuleCID( path: Path.Distinctive> ): Promise { return await this.#transactionContext().capsuleCID(path) } - /** @group Querying */ + /** + * @param path + * @group Querying + */ async capsuleKey( path: Path.Distinctive> ): Promise { return await this.#transactionContext().capsuleKey(path) } - /** @group Querying */ + /** + * @param path + * @group Querying + */ async exists( path: Path.Distinctive> ): Promise { @@ -426,7 +460,10 @@ export class FileSystem { ) } - /** @group Querying */ + /** + * @param path + * @group Querying + */ async size(path: Path.File>): Promise { return await this.#transactionContext().size(path) } @@ -575,7 +612,11 @@ export class FileSystem { /** @group Mutating */ mv = this.move // eslint-disable-line @typescript-eslint/unbound-method - /** @group Mutating */ + /** + * @param path + * @param mutationOptions + * @group Mutating + */ async remove( path: Path.Distinctive>, mutationOptions: MutationOptions = {} @@ -638,10 +679,144 @@ export class FileSystem { ) } + // IDENTITY + // -------- + + async assignIdentifier( + did: string, + mutationOptions: MutationOptions = {} + ): Promise<{ dataRoot: CID }> { + const transactionResult = await this.transaction(async (t) => { + await t.assignIdentifier(did) + }, mutationOptions) + + if (transactionResult === 'no-op') { + throw new Error( + 'The transaction was a no-op, most likely as a result of the commit not being approved by the `onCommit` verifier.' + ) + } + + const dataRoot = transactionResult.dataRoot + return { dataRoot } + } + + identifier(): string | undefined { + return this.#rootTree.did() + } + + // SHARING + // ------- + + /** + * Load a shared item. + * + * NOTE: A share can only be received if the exchange key was registered + * and the receiver is in possession of the associated private key. + * + * @param itemName + * @param sharerDataRoot The data root CID from the sharer + * @param publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes + * @param privateKey A RSA-OAEP-256 private key in the form of a `CryptoKey` + * @param opts Optional overrides + * @param opts.sharerBlockstore Specify what blockstore to use to load the sharer's file system + * @param opts.sharerRootTreeClass Specify what root tree class was used for the sharer's file system + * + * @group Sharing + */ + async receive( + itemName: string, + sharerDataRoot: CID, + publicKey: CryptoKey | Uint8Array, + privateKey: CryptoKey, + opts: { + sharerBlockstore?: Blockstore + sharerRootTreeClass?: typeof RootTree + } = {} + ): Promise<{ + share: TransactionContext + }> { + const share = await this.#transactionContext().receive( + itemName, + sharerDataRoot, + publicKey, + privateKey, + opts + ) + + return { + share, + } + } + + /** + * Register an exchange key. + * + * @param name A name for the key + * @param publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes + * @param mutationOptions Mutation options + * + * @group Sharing + */ + async registerExchangeKey( + name: string, + publicKey: CryptoKey | Uint8Array, + mutationOptions: MutationOptions = {} + ): Promise<{ dataRoot: CID }> { + const transactionResult = await this.transaction(async (t) => { + await t.registerExchangeKey(name, publicKey) + }, mutationOptions) + + if (transactionResult === 'no-op') { + throw new Error( + 'The transaction was a no-op, most likely as a result of the commit not being approved by the `onCommit` verifier.' + ) + } + + const dataRoot = transactionResult.dataRoot + return { dataRoot } + } + + /** + * Share a private file or directory. + * + * NOTE: A share can only be received if the exchange key was registered + * and the receiver is in possession of the associated private key. + * + * @param path Path to the private file or directory to share (with 'private' prefix) + * @param receiverDataRoot Data root CID of the receiver + * @param opts Optional overrides + * @param opts.receiverBlockstore Specify what blockstore to use to load the receiver's file system + * @param opts.receiverRootTreeClass Specify what root tree class was used for the receiver's file system + * @param opts.mutationOptions Mutation options + * + * @group Sharing + */ + async share( + path: Path.Distinctive>, + receiverDataRoot: CID, + opts: { + mutationOptions?: MutationOptions + receiverBlockstore?: Blockstore + receiverRootTreeClass?: typeof RootTree + } = {} + ): Promise> { + return await this.#infusedTransaction( + async (t) => { + await t.share(path, receiverDataRoot, opts) + }, + path, + opts.mutationOptions + ) + } + // TRANSACTIONS // ------------ - /** @group Transacting */ + /** + * @param handler + * @param mutationOptions + * @group Transacting + */ async transaction( handler: (t: TransactionContext) => Promise, mutationOptions: MutationOptions = {} diff --git a/packages/nest/src/transaction.ts b/packages/nest/src/transaction.ts index 874c383..a79c8b2 100644 --- a/packages/nest/src/transaction.ts +++ b/packages/nest/src/transaction.ts @@ -581,39 +581,6 @@ export class TransactionContext { // SHARING - /** - * Register an exchange key. - * - * @param name A name for the key - * @param publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes - * - * @group Sharing - */ - async registerExchangeKey( - name: string, - publicKey: CryptoKey | Uint8Array - ): Promise { - const publicDir = this.#rootTree.exchangeRoot() - const publicKeyResult = - publicKey instanceof CryptoKey - ? await new ExchangeKey(publicKey).publicKeyModulus() - : publicKey - - // Add public key to exchange root - const result: WnfsPublicResult = await publicDir.write( - [name, 'v1.exchange_key'], - publicKeyResult, - new Date(), - Store.wnfs(this.#blockstore) - ) - - // Replace public root - this.#rootTree = await this.#rootTree.replaceExchangeRoot(result.rootDir) - - // Return public key - return publicKeyResult - } - /** * Load a shared item. * @@ -623,11 +590,12 @@ export class TransactionContext { * @param itemName * @param sharerDataRoot The data root CID from the sharer * @param publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes - * + * @param privateKey A RSA-OAEP-256 private key in the form of a `CryptoKey` * @param opts Optional overrides * @param opts.sharerBlockstore Specify what blockstore to use to load the sharer's file system * @param opts.sharerRootTreeClass Specify what root tree class was used for the sharer's file system - * @param privateKey + * + * @group Sharing */ async receive( itemName: string, @@ -657,7 +625,7 @@ export class TransactionContext { if (sharerIdentifier === undefined) throw new Error("The sharer's file system is missing an identifier") - // TODO: Check if this works, not sure if `findLatestShareCounter` returns a result object or not + // Find the share number const shareNumber: bigint = await findLatestShareCounter( 0, sharerCounter < 1 ? 1 : sharerCounter, @@ -708,6 +676,39 @@ export class TransactionContext { ) } + /** + * Register an exchange key. + * + * @param name A name for the key + * @param publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes + * + * @group Sharing + */ + async registerExchangeKey( + name: string, + publicKey: CryptoKey | Uint8Array + ): Promise { + const publicDir = this.#rootTree.exchangeRoot() + const publicKeyResult = + publicKey instanceof CryptoKey + ? await new ExchangeKey(publicKey).publicKeyModulus() + : publicKey + + // Add public key to exchange root + const result: WnfsPublicResult = await publicDir.write( + [name, 'v1.exchange_key'], + publicKeyResult, + new Date(), + Store.wnfs(this.#blockstore) + ) + + // Replace public root + this.#rootTree = await this.#rootTree.replaceExchangeRoot(result.rootDir) + + // Return public key + return publicKeyResult + } + /** * Share a private file or directory. * diff --git a/packages/nest/test/class.test.ts b/packages/nest/test/class.test.ts index 66d74ab..f5b7c96 100644 --- a/packages/nest/test/class.test.ts +++ b/packages/nest/test/class.test.ts @@ -1169,4 +1169,43 @@ describe('File System Class', () => { assert.equal(content, '🔒') }) + + it('can share a file and receive it', async () => { + const keypair = await ExchangeKey.generate() + + const receiverFs = await FileSystem.create({ + blockstore, + ...fsOpts, + }) + + // Register exchange key + const a = await receiverFs.registerExchangeKey('device', keypair.publicKey) + const receiverDataRoot = a.dataRoot + + // Assign sharer identifier & create share + const path = Path.file('private', 'nested', 'level 2', 'fileToShare') + await fs.assignIdentifier('did:test:1') + await fs.write(path, 'utf8', '🔒') + const b = await fs.share( + Path.directory('private', 'nested'), + receiverDataRoot + ) + + // Receive share + const sharerDataRoot = b.dataRoot + const { share } = await receiverFs.receive( + 'directory', + sharerDataRoot, + keypair.publicKey, + keypair.privateKey + ) + + const content = await share.read( + Path.file('private', 'directory', 'level 2', 'fileToShare'), + 'utf8' + ) + + // Assert + assert.equal(content, '🔒') + }) }) From 055e3511b588e9df91f40c79e6872c6b1ff5bd9a Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Sat, 30 Mar 2024 23:41:25 +0100 Subject: [PATCH 3/7] feat: Add Share class --- packages/nest/README.md | 33 +++++++- packages/nest/package.json | 3 +- packages/nest/src/class.ts | 6 +- packages/nest/src/share.ts | 136 +++++++++++++++++++++++++++++++ packages/nest/src/transaction.ts | 10 +-- packages/nest/test/class.test.ts | 8 +- 6 files changed, 180 insertions(+), 16 deletions(-) create mode 100644 packages/nest/src/share.ts diff --git a/packages/nest/README.md b/packages/nest/README.md index 5caeb4c..82a7e06 100644 --- a/packages/nest/README.md +++ b/packages/nest/README.md @@ -8,8 +8,9 @@ A layer around the `wnfs` package that provides a `FileSystem` class, a root tre - A file system class that allows for an easy-to-use mutable API. - A root tree, holding references to all the needed individual parts (public fs, private forest, exchange, etc) -- A unix-fs compatibility layer for the public file system (allows for public files to be viewed through, for example, IPFS gateways) - A mounting system for private nodes, mount specific paths. +- A unix-fs compatibility layer for the public file system (allows for public files to be viewed through, for example, IPFS gateways) +- Private data sharing helpers - Provides a transaction system, rewinding the state if an error occurs. - Creates a private forest automatically with a RSA modules using the Web Crypto API (supported on multiple platforms) - Ability to verify commits to the file system. If a commit, aka. modification, is not verified, it will result in a no-op. @@ -120,6 +121,36 @@ fs.rename fs.write ``` +## Identity + +```ts +fs.identity() +fs.assignIdentity('did') +``` + +## Private Data Sharing + +Flow: + +1. The receiver of a share register their exchange key. An app could do this automatically when the app starts, or at some other time. +2. The data root of the receiver is passed to the sharer. Ideally this is done through some naming system. For example, you use DNS to map a username to the data root (eg. `TXT file-system.tokono.ma` could resolve to the data root, a CID). That said, this could also be done without a naming system, maybe by presenting a QR code. +3. The sharer creates the share. +4. This step is the reverse of step 2, where we pass the sharer's data root to the receiver. +5. Use the shared item. + +```ts +// Step 1 & 2 +const { dataRoot } = await fs.registerExchangeKey('key-id', publicKey) +const receiverDataRoot = dataRoot +// Step 3 & 4 +const { dataRoot } = await fs.share(pathToPrivateItem, receiverDataRoot) +const sharerDataRoot = dataRoot +// Step 5 +const { share } = await fs.receive(sharerDataRoot, publicKey, privateKey) + +await share.read('utf8') +``` + ## Transactions ```ts diff --git a/packages/nest/package.json b/packages/nest/package.json index 2189b79..09159d0 100644 --- a/packages/nest/package.json +++ b/packages/nest/package.json @@ -149,7 +149,8 @@ "mocha": true }, "ignorePatterns": [ - "dist" + "dist", + "README.md" ], "rules": { "@typescript-eslint/no-unused-vars": [ diff --git a/packages/nest/src/class.ts b/packages/nest/src/class.ts index a03f4f2..28bc6d4 100644 --- a/packages/nest/src/class.ts +++ b/packages/nest/src/class.ts @@ -43,6 +43,8 @@ import * as Rng from './rng.js' import * as Store from './store.js' import type { RootTree } from './root-tree.js' +import type { Share } from './share.js' + import type { Partition, Partitioned, @@ -724,7 +726,6 @@ export class FileSystem { * @group Sharing */ async receive( - itemName: string, sharerDataRoot: CID, publicKey: CryptoKey | Uint8Array, privateKey: CryptoKey, @@ -733,10 +734,9 @@ export class FileSystem { sharerRootTreeClass?: typeof RootTree } = {} ): Promise<{ - share: TransactionContext + share: Share }> { const share = await this.#transactionContext().receive( - itemName, sharerDataRoot, publicKey, privateKey, diff --git a/packages/nest/src/share.ts b/packages/nest/src/share.ts new file mode 100644 index 0000000..c96d7d7 --- /dev/null +++ b/packages/nest/src/share.ts @@ -0,0 +1,136 @@ +import type { Blockstore } from 'interface-blockstore' + +import * as Queries from './queries.js' +import * as Path from './path.js' + +import type { Segments } from './path.js' +import type { Rng } from './rng.js' +import type { RootTree } from './root-tree.js' + +import type { + AnySupportedDataType, + DataForType, + DataType, + DirectoryItem, + DirectoryItemWithKind, +} from './types.js' + +import type { MountedPrivateNodes } from './types/internal.js' + +import { dataFromBytes } from './data.js' + +// CLASS + +export class Share { + readonly #blockstore: Blockstore + readonly #privateNodes: MountedPrivateNodes + readonly #rootTree: RootTree + readonly #rng: Rng + + /** + * @param blockstore + * @param privateNodes + * @param rng + * @param rootTree + * @internal + */ + constructor( + blockstore: Blockstore, + privateNodes: MountedPrivateNodes, + rng: Rng, + rootTree: RootTree + ) { + this.#blockstore = blockstore + this.#privateNodes = privateNodes + this.#rng = rng + this.#rootTree = rootTree + } + + // QUERIES + + /** + * @param path + * @group Querying + */ + async exists(path?: Path.Distinctive): Promise { + return await this.#query(path ?? Path.root(), Queries.privateExists()) + } + + /** @group Querying */ + async listDirectory( + path: Path.Directory, + listOptions: { withItemKind: true } + ): Promise + async listDirectory( + path: Path.Directory, + listOptions: { withItemKind: false } + ): Promise + async listDirectory(): Promise + async listDirectory(path: Path.Directory): Promise + async listDirectory( + path?: Path.Directory, + listOptions?: { withItemKind: boolean } + ): Promise + async listDirectory( + path?: Path.Directory, + listOptions?: { withItemKind: boolean } + ): Promise { + return await this.#query( + path ?? Path.root(), + listOptions?.withItemKind === true + ? Queries.privateListDirectoryWithKind() + : Queries.privateListDirectory() + ) + } + + /** @group Querying */ + ls = this.listDirectory // eslint-disable-line @typescript-eslint/unbound-method + + /** @group Querying */ + async read( + dataType: D, + path?: Path.File, + options?: { offset?: number; length?: number } + ): Promise> + async read( + dataType: DataType, + path?: Path.File, + options?: { offset?: number; length?: number } + ): Promise> { + const bytes = await this.#query( + path ?? Path.root(), + Queries.privateRead(options) + ) + return dataFromBytes(dataType, bytes) + } + + /** + * @param path + * @group Querying + */ + async size(path?: Path.File): Promise { + return await this.#query(path ?? Path.root(), Queries.privateSize()) + } + + // ㊙️ + + async #query( + path: Path.Distinctive, + query: Queries.Private + ): Promise { + return await Queries.privateQuery( + Path.withPartition('private', path), + query, + this.#privateContext() + ) + } + + #privateContext(): Queries.PrivateContext { + return { + blockstore: this.#blockstore, + privateNodes: this.#privateNodes, + rng: this.#rng, + rootTree: this.#rootTree, + } + } +} diff --git a/packages/nest/src/transaction.ts b/packages/nest/src/transaction.ts index a79c8b2..b0ef1f3 100644 --- a/packages/nest/src/transaction.ts +++ b/packages/nest/src/transaction.ts @@ -54,6 +54,7 @@ import type { } from './types/internal.js' import { ExchangeKey } from './exchange-key.js' +import { Share } from './share.js' // CLASS @@ -598,7 +599,6 @@ export class TransactionContext { * @group Sharing */ async receive( - itemName: string, sharerDataRoot: CID, publicKey: CryptoKey | Uint8Array, privateKey: CryptoKey, @@ -606,7 +606,7 @@ export class TransactionContext { sharerBlockstore?: Blockstore sharerRootTreeClass?: typeof RootTree } = {} - ): Promise { + ): Promise { const publicKeyResult = publicKey instanceof CryptoKey ? await new ExchangeKey(publicKey).publicKeyModulus() @@ -663,13 +663,11 @@ export class TransactionContext { // Create transaction context const path = Path.fromKind( - sharedNode.isFile() ? Path.Kind.File : Path.Kind.Directory, - itemName + sharedNode.isFile() ? Path.Kind.File : Path.Kind.Directory ) - return new TransactionContext( + return new Share( sharerBlockstore, - async () => ({ commit: false }), { [Path.toPosix(path, { absolute: true })]: { path, node: sharedNode } }, this.#rng, sharerRootTree diff --git a/packages/nest/test/class.test.ts b/packages/nest/test/class.test.ts index f5b7c96..0e7454a 100644 --- a/packages/nest/test/class.test.ts +++ b/packages/nest/test/class.test.ts @@ -1158,13 +1158,12 @@ describe('File System Class', () => { await receiverFs.transaction(async (t) => { const ctx = await t.receive( - 'fileToShare', sharerDataRoot, keypair.publicKey, keypair.privateKey ) - content = await ctx.read(Path.file('private', 'fileToShare'), 'utf8') + content = await ctx.read('utf8') }) assert.equal(content, '🔒') @@ -1194,15 +1193,14 @@ describe('File System Class', () => { // Receive share const sharerDataRoot = b.dataRoot const { share } = await receiverFs.receive( - 'directory', sharerDataRoot, keypair.publicKey, keypair.privateKey ) const content = await share.read( - Path.file('private', 'directory', 'level 2', 'fileToShare'), - 'utf8' + 'utf8', + Path.file('level 2', 'fileToShare') ) // Assert From 0f322fe10c85349b12cb1338d88c205bb240bb77 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Sun, 31 Mar 2024 14:45:20 +0200 Subject: [PATCH 4/7] feat!: Create private node using exchange key pair --- packages/nest/README.md | 15 +- packages/nest/src/class.ts | 297 +++++++++++++++++++++---------- packages/nest/src/sharing.ts | 99 +++++++++++ packages/nest/src/transaction.ts | 141 +++++++-------- packages/nest/test/class.test.ts | 178 ++++++++++++++++-- 5 files changed, 548 insertions(+), 182 deletions(-) create mode 100644 packages/nest/src/sharing.ts diff --git a/packages/nest/README.md b/packages/nest/README.md index 82a7e06..22f38b2 100644 --- a/packages/nest/README.md +++ b/packages/nest/README.md @@ -43,7 +43,7 @@ const fs = await FileSystem.create({ }) // Create the private node of which we'll keep the encryption key around. -const { capsuleKey } = await fs.mountPrivateNode({ +const { capsuleKey } = await fs.createPrivateNode({ path: Path.root(), // ie. root private directory }) @@ -146,11 +146,22 @@ const receiverDataRoot = dataRoot const { dataRoot } = await fs.share(pathToPrivateItem, receiverDataRoot) const sharerDataRoot = dataRoot // Step 5 -const { share } = await fs.receive(sharerDataRoot, publicKey, privateKey) +const { share } = await fs.receive(sharerDataRoot, { publicKey, privateKey }) await share.read('utf8') ``` +## Manage private node using exchange key pair + +Instead of keeping the symmetric capsule key around we can use an exchange key pair to mount a private node. This basically creates a share for ourselves. + +```ts +await fs.createPrivateNode({ + path: Path.root(), + exchangeKeyPair: { publicKey, privateKey }, // 🔑 Pass in key pair here +}) +``` + ## Transactions ```ts diff --git a/packages/nest/src/class.ts b/packages/nest/src/class.ts index 28bc6d4..e664c72 100644 --- a/packages/nest/src/class.ts +++ b/packages/nest/src/class.ts @@ -53,10 +53,11 @@ import type { Public, } from './path.js' -import { searchLatest } from './common.js' -import { partition as determinePartition, findPrivateNode } from './mounts.js' import { TransactionContext } from './transaction.js' import { BasicRootTree } from './root-tree/basic.js' +import { searchLatest } from './common.js' +import { partition as determinePartition, findPrivateNode } from './mounts.js' +import { loadShare } from './sharing.js' // OPTIONS @@ -251,109 +252,207 @@ export class FileSystem { // ------ /** - * Mount a private node onto the file system. + * Create a new private node and mount it. * * @param node * @param node.path - * @param node.capsuleKey + * @param node.exchangeKeyPair + * @param node.exchangeKeyPair.publicKey + * @param node.exchangeKeyPair.privateKey + * @param mutationOptions * @group Mounting */ - async mountPrivateNode(node: { - path: Path.Distinctive - capsuleKey?: Uint8Array - }): Promise<{ - path: Path.Distinctive + async createPrivateNode( + node: { + path: Path.Distinctive + exchangeKeyPair?: { + publicKey: CryptoKey | Uint8Array + privateKey: CryptoKey + } + }, + mutationOptions?: MutationOptions + ): Promise<{ + path: Path.Distinctive> capsuleKey: Uint8Array }> { - const mounts = await this.mountPrivateNodes([node]) - return mounts[0] + const { path } = node + const absolutePosixPath = Path.toPosix(path, { absolute: true }) + + if (this.#privateNodes[absolutePosixPath] !== undefined) { + throw new Error( + 'A private node is already mounted at this path, unmount it first if you want to replace it.' + ) + } + + // Share to self, pt. 1 + if (node.exchangeKeyPair !== undefined) { + const isRegistered = await this.isExchangeKeyRegistered( + node.exchangeKeyPair.publicKey + ) + + if (!isRegistered) + throw new Error( + 'Register the exchange key first using `registerExchange`' + ) + } + + // Create + const privateNode = Path.isFile(path) + ? new PrivateFile( + this.#rootTree.privateForest().emptyName(), + new Date(), + this.#rng + ).asNode() + : new PrivateDirectory( + this.#rootTree.privateForest().emptyName(), + new Date(), + this.#rng + ).asNode() + + // Store + const storeResult = await privateNode.store( + this.#rootTree.privateForest(), + Store.wnfs(this.#blockstore), + this.#rng + ) + + const [accessKey, privateForest] = storeResult + + // Update root tree + const pathWithPartition = Path.withPartition('private', path) + const modification: Modification = { + path: pathWithPartition, + type: 'added-or-updated', + } + + this.#rootTree = await this.#rootTree.replacePrivateForest( + privateForest as PrivateForest, + [modification] + ) + + // Mount + this.#privateNodes = { + ...this.#privateNodes, + [absolutePosixPath]: { node: privateNode, path }, + } + + // Emit events + const dataRoot = await this.calculateDataRoot() + + await this.#eventEmitter.emit('commit', { + dataRoot, + modifications: [modification], + }) + + // Publish + if ( + mutationOptions?.skipPublish === false || + mutationOptions?.skipPublish === undefined + ) { + await this.#publish(dataRoot, [modification]) + } + + // Share to self, pt. 2 + if (node.exchangeKeyPair !== undefined) { + await this.share(pathWithPartition, dataRoot, { mutationOptions }) + } + + // Fin + return { + path: pathWithPartition, + capsuleKey: accessKey.toBytes(), + } + } + + /** + * Mount a private node onto the file system. + * + * @param node + * @group Mounting + */ + async mountPrivateNode( + node: + | { + path: Path.Distinctive + capsuleKey: Uint8Array + } + | { + path: Path.Distinctive + exchangeKeyPair: { + publicKey: CryptoKey | Uint8Array + privateKey: CryptoKey + } + } + ): Promise { + await this.mountPrivateNodes([node]) } /** * Mount private nodes onto the file system. * - * When a `capsuleKey` is not given, - * it will create the given path instead of trying to load it. + * This supports two scenarios: + * - Load a private node using a capsule key + * - Load a private node using an exchange key pair (this is a share made to one of your own exchange keys) * * @param nodes * @group Mounting */ async mountPrivateNodes( - nodes: Array<{ - path: Path.Distinctive - capsuleKey?: Uint8Array - }> - ): Promise< - Array<{ - path: Path.Distinctive - capsuleKey: Uint8Array - }> - > { - const newNodes = await Promise.all( - nodes.map( - async ({ path, capsuleKey }): Promise<[string, MountedPrivateNode]> => { - let privateNode: PrivateNode - - if (capsuleKey === null || capsuleKey === undefined) { - privateNode = Path.isFile(path) - ? new PrivateFile( - this.#rootTree.privateForest().emptyName(), - new Date(), - this.#rng - ).asNode() - : new PrivateDirectory( - this.#rootTree.privateForest().emptyName(), - new Date(), - this.#rng - ).asNode() - } else { - const accessKey = AccessKey.fromBytes(capsuleKey) - privateNode = await PrivateNode.load( - accessKey, - this.#rootTree.privateForest(), - Store.wnfs(this.#blockstore) - ) + nodes: Array< + | { + path: Path.Distinctive + capsuleKey: Uint8Array + } + | { + path: Path.Distinctive + exchangeKeyPair: { + publicKey: CryptoKey | Uint8Array + privateKey: CryptoKey } + } + > + ): Promise { + let newNodes: Array<[string, MountedPrivateNode]> = [] + + await Promise.all( + nodes.map(async (args): Promise => { + let privateNode: PrivateNode | undefined + + const { path } = args + + if ('capsuleKey' in args) { + const accessKey = AccessKey.fromBytes(args.capsuleKey) + privateNode = await PrivateNode.load( + accessKey, + this.#rootTree.privateForest(), + Store.wnfs(this.#blockstore) + ) + } else if ('exchangeKeyPair' in args) { + privateNode = await loadShare( + await this.calculateDataRoot(), + args.exchangeKeyPair, + { + sharerBlockstore: this.#blockstore, + } + ).then((a) => a.sharedNode) + } - return [ - // Use absolute paths so that you can retrieve the root: privateNodes["/"] - Path.toPosix(path, { absolute: true }), - { node: privateNode, path }, + if (privateNode !== undefined) + newNodes = [ + ...newNodes, + [ + // Use absolute paths so that we can retrieve the root: privateNodes["/"] + Path.toPosix(path, { absolute: true }), + { node: privateNode, path }, + ], ] - } - ) + }) ) this.#privateNodes = { ...this.#privateNodes, ...Object.fromEntries(newNodes), } - - return await Promise.all( - newNodes.map(async ([_, n]: [string, MountedPrivateNode]) => { - const storeResult = await n.node.store( - this.#rootTree.privateForest(), - Store.wnfs(this.#blockstore), - this.#rng - ) - const [accessKey, privateForest] = storeResult - - this.#rootTree = await this.#rootTree.replacePrivateForest( - privateForest as PrivateForest, - [ - { - path: Path.withPartition('private', n.path), - type: 'added-or-updated', - }, - ] - ) - - return { - path: n.path, - capsuleKey: accessKey.toBytes(), - } - }) - ) } /** @@ -364,7 +463,7 @@ export class FileSystem { */ unmountPrivateNode(path: Path.Distinctive): void { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this.#privateNodes[Path.toPosix(path)] + delete this.#privateNodes[Path.toPosix(path, { absolute: true })] } // QUERY @@ -709,6 +808,20 @@ export class FileSystem { // SHARING // ------- + /** + * Check if an exchange key was already registered. + * + * @param exchangePublicKey + * @group Sharing + */ + async isExchangeKeyRegistered( + exchangePublicKey: CryptoKey | Uint8Array + ): Promise { + return await this.#transactionContext().isExchangeKeyRegistered( + exchangePublicKey + ) + } + /** * Load a shared item. * @@ -717,8 +830,9 @@ export class FileSystem { * * @param itemName * @param sharerDataRoot The data root CID from the sharer - * @param publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes - * @param privateKey A RSA-OAEP-256 private key in the form of a `CryptoKey` + * @param exchangeKeyPair A RSA-OAEP-256 key pair + * @param exchangeKeyPair.publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes + * @param exchangeKeyPair.privateKey A RSA-OAEP-256 private key in the form of a `CryptoKey` * @param opts Optional overrides * @param opts.sharerBlockstore Specify what blockstore to use to load the sharer's file system * @param opts.sharerRootTreeClass Specify what root tree class was used for the sharer's file system @@ -727,8 +841,10 @@ export class FileSystem { */ async receive( sharerDataRoot: CID, - publicKey: CryptoKey | Uint8Array, - privateKey: CryptoKey, + exchangeKeyPair: { + publicKey: CryptoKey | Uint8Array + privateKey: CryptoKey + }, opts: { sharerBlockstore?: Blockstore sharerRootTreeClass?: typeof RootTree @@ -738,8 +854,7 @@ export class FileSystem { }> { const share = await this.#transactionContext().receive( sharerDataRoot, - publicKey, - privateKey, + exchangeKeyPair, opts ) @@ -751,19 +866,19 @@ export class FileSystem { /** * Register an exchange key. * - * @param name A name for the key - * @param publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes + * @param name A name for the key (using an existing name overrides the old key) + * @param exchangePublicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes * @param mutationOptions Mutation options * * @group Sharing */ async registerExchangeKey( name: string, - publicKey: CryptoKey | Uint8Array, + exchangePublicKey: CryptoKey | Uint8Array, mutationOptions: MutationOptions = {} ): Promise<{ dataRoot: CID }> { const transactionResult = await this.transaction(async (t) => { - await t.registerExchangeKey(name, publicKey) + await t.registerExchangeKey(name, exchangePublicKey) }, mutationOptions) if (transactionResult === 'no-op') { @@ -792,7 +907,7 @@ export class FileSystem { * @group Sharing */ async share( - path: Path.Distinctive>, + path: Path.Distinctive>, receiverDataRoot: CID, opts: { mutationOptions?: MutationOptions diff --git a/packages/nest/src/sharing.ts b/packages/nest/src/sharing.ts new file mode 100644 index 0000000..286e1f5 --- /dev/null +++ b/packages/nest/src/sharing.ts @@ -0,0 +1,99 @@ +import type { PrivateNode } from 'wnfs' +import type { Blockstore } from 'interface-blockstore' +import type { CID } from 'multiformats/cid' + +import { + Name, + NameAccumulator, + createShareName, + findLatestShareCounter, + receiveShare, +} from 'wnfs' + +import type { RootTree } from './root-tree.js' + +import * as Store from './store.js' + +import { BasicRootTree } from './root-tree/basic.js' +import { ExchangeKey } from './exchange-key.js' + +/** + * + * @param sharerDataRoot + * @param exchangeKeyPair + * @param exchangeKeyPair.publicKey + * @param exchangeKeyPair.privateKey + * @param opts + * @param opts.sharerBlockstore + * @param opts.sharerRootTreeClass + */ +export async function loadShare( + sharerDataRoot: CID, + exchangeKeyPair: { + publicKey: CryptoKey | Uint8Array + privateKey: CryptoKey + }, + opts: { + sharerBlockstore: Blockstore + sharerRootTreeClass?: typeof RootTree + } +): Promise<{ + sharedNode: PrivateNode + sharerRootTree: RootTree +}> { + const publicKeyResult = + exchangeKeyPair.publicKey instanceof CryptoKey + ? await new ExchangeKey(exchangeKeyPair.publicKey).publicKeyModulus() + : exchangeKeyPair.publicKey + + const sharerBlockstore = opts.sharerBlockstore + const sharerRootTreeClass = opts.sharerRootTreeClass ?? BasicRootTree + const sharerRootTree = await sharerRootTreeClass.fromCID( + sharerBlockstore, + sharerDataRoot + ) + + const sharerForest = sharerRootTree.privateForest() + const sharerCounter = sharerRootTree.shareCounter() + const sharerIdentifier = sharerRootTree.did() + if (sharerIdentifier === undefined) + throw new Error("The sharer's file system is missing an identifier") + + // Find the share number + const shareNumber: bigint = await findLatestShareCounter( + 0, + sharerCounter < 1 ? 1 : sharerCounter, + publicKeyResult, + sharerIdentifier, + sharerForest, + Store.wnfs(sharerBlockstore) + ) + + // Determine share name + const shareLabel = createShareName( + Number(shareNumber), + sharerIdentifier, + publicKeyResult, + sharerForest + ) + + const shareLabelSerialized = shareLabel + .toNameAccumulator(sharerForest) + .toBytes() + const shareLabelDeserialized = new Name( + NameAccumulator.fromBytes(shareLabelSerialized) + ) + + // Load shared private node + const sharedNode: PrivateNode = await receiveShare( + shareLabelDeserialized, + { + decrypt: async (data: Uint8Array) => + await ExchangeKey.decrypt(data, exchangeKeyPair.privateKey), + }, + sharerForest, + Store.wnfs(sharerBlockstore) + ) + + return { sharedNode, sharerRootTree } +} diff --git a/packages/nest/src/transaction.ts b/packages/nest/src/transaction.ts index b0ef1f3..dc14993 100644 --- a/packages/nest/src/transaction.ts +++ b/packages/nest/src/transaction.ts @@ -1,17 +1,9 @@ import type { PrivateForest, PrivateNode } from 'wnfs' import type { Blockstore } from 'interface-blockstore' +import * as Uint8Arr from 'uint8arrays' import { CID } from 'multiformats/cid' -import { - AccessKey, - Name, - NameAccumulator, - PublicFile, - createShareName, - findLatestShareCounter, - receiveShare, - share, -} from 'wnfs' +import { AccessKey, PublicFile, share } from 'wnfs' import type { Rng } from './rng.js' import type { RootTree } from './root-tree.js' @@ -30,11 +22,13 @@ import type { Public, } from './path.js' +import { BasicRootTree } from './root-tree/basic.js' +import { ExchangeKey } from './exchange-key.js' +import { Share } from './share.js' import { addOrIncreaseNameNumber, searchLatest } from './common.js' - import { dataFromBytes, dataToBytes } from './data.js' import { partition as determinePartition, findPrivateNode } from './mounts.js' -import { BasicRootTree } from './root-tree/basic.js' +import { loadShare } from './sharing.js' import type { AnySupportedDataType, @@ -53,9 +47,6 @@ import type { WnfsPublicResult, } from './types/internal.js' -import { ExchangeKey } from './exchange-key.js' -import { Share } from './share.js' - // CLASS export class TransactionContext { @@ -582,6 +573,43 @@ export class TransactionContext { // SHARING + /** + * Check if an exchange key was already registered. + * + * @param exchangePublicKey + * @group Sharing + */ + async isExchangeKeyRegistered( + exchangePublicKey: CryptoKey | Uint8Array + ): Promise { + const publicKey = + exchangePublicKey instanceof CryptoKey + ? await new ExchangeKey(exchangePublicKey).publicKeyModulus() + : exchangePublicKey + + const exr = this.#rootTree.exchangeRoot() + const wnfsStore = Store.wnfs(this.#blockstore) + const items: DirectoryItem[] = await exr.ls([], wnfsStore) + + let isRegistered = false + + for await (const item of items) { + const nestedItems = await exr.ls([item.name], wnfsStore) + for await (const nestedItem of nestedItems) { + const bytes: Uint8Array = await exr.read( + [item.name, nestedItem.name], + wnfsStore + ) + if (Uint8Arr.compare(publicKey, bytes) === 0) { + isRegistered = true + break + } + } + } + + return isRegistered + } + /** * Load a shared item. * @@ -590,8 +618,9 @@ export class TransactionContext { * * @param itemName * @param sharerDataRoot The data root CID from the sharer - * @param publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes - * @param privateKey A RSA-OAEP-256 private key in the form of a `CryptoKey` + * @param exchangeKeyPair A RSA-OAEP-256 key pair + * @param exchangeKeyPair.publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes + * @param exchangeKeyPair.privateKey A RSA-OAEP-256 private key in the form of a `CryptoKey` * @param opts Optional overrides * @param opts.sharerBlockstore Specify what blockstore to use to load the sharer's file system * @param opts.sharerRootTreeClass Specify what root tree class was used for the sharer's file system @@ -600,68 +629,26 @@ export class TransactionContext { */ async receive( sharerDataRoot: CID, - publicKey: CryptoKey | Uint8Array, - privateKey: CryptoKey, + exchangeKeyPair: { + publicKey: CryptoKey | Uint8Array + privateKey: CryptoKey + }, opts: { sharerBlockstore?: Blockstore sharerRootTreeClass?: typeof RootTree } = {} ): Promise { - const publicKeyResult = - publicKey instanceof CryptoKey - ? await new ExchangeKey(publicKey).publicKeyModulus() - : publicKey - const sharerBlockstore = opts.sharerBlockstore ?? this.#blockstore - const sharerRootTreeClass = opts.sharerRootTreeClass ?? BasicRootTree - const sharerRootTree = await sharerRootTreeClass.fromCID( - sharerBlockstore, - sharerDataRoot - ) - - const sharerForest = sharerRootTree.privateForest() - const sharerCounter = sharerRootTree.shareCounter() - const sharerIdentifier = sharerRootTree.did() - if (sharerIdentifier === undefined) - throw new Error("The sharer's file system is missing an identifier") - - // Find the share number - const shareNumber: bigint = await findLatestShareCounter( - 0, - sharerCounter < 1 ? 1 : sharerCounter, - publicKeyResult, - sharerIdentifier, - sharerForest, - Store.wnfs(sharerBlockstore) - ) - - // Determine share name - const shareLabel = createShareName( - Number(shareNumber), - sharerIdentifier, - publicKeyResult, - sharerForest - ) - - const shareLabelSerialized = shareLabel - .toNameAccumulator(sharerForest) - .toBytes() - const shareLabelDeserialized = new Name( - NameAccumulator.fromBytes(shareLabelSerialized) - ) - - // Load shared private node - const sharedNode: PrivateNode = await receiveShare( - shareLabelDeserialized, + const { sharedNode, sharerRootTree } = await loadShare( + sharerDataRoot, + exchangeKeyPair, { - decrypt: async (data: Uint8Array) => - await ExchangeKey.decrypt(data, privateKey), - }, - sharerForest, - Store.wnfs(sharerBlockstore) + sharerBlockstore, + sharerRootTreeClass: opts.sharerRootTreeClass, + } ) - // Create transaction context + // Create share context const path = Path.fromKind( sharedNode.isFile() ? Path.Kind.File : Path.Kind.Directory ) @@ -677,20 +664,20 @@ export class TransactionContext { /** * Register an exchange key. * - * @param name A name for the key - * @param publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes + * @param name A name for the key (using an existing name overrides the old key) + * @param exchangePublicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes * * @group Sharing */ async registerExchangeKey( name: string, - publicKey: CryptoKey | Uint8Array + exchangePublicKey: CryptoKey | Uint8Array ): Promise { const publicDir = this.#rootTree.exchangeRoot() const publicKeyResult = - publicKey instanceof CryptoKey - ? await new ExchangeKey(publicKey).publicKeyModulus() - : publicKey + exchangePublicKey instanceof CryptoKey + ? await new ExchangeKey(exchangePublicKey).publicKeyModulus() + : exchangePublicKey // Add public key to exchange root const result: WnfsPublicResult = await publicDir.write( @@ -722,7 +709,7 @@ export class TransactionContext { * @group Sharing */ async share( - path: Path.Distinctive>, + path: Path.Distinctive>, receiverDataRoot: CID, opts: { receiverBlockstore?: Blockstore diff --git a/packages/nest/test/class.test.ts b/packages/nest/test/class.test.ts index 0e7454a..06df36a 100644 --- a/packages/nest/test/class.test.ts +++ b/packages/nest/test/class.test.ts @@ -42,7 +42,11 @@ describe('File System Class', () => { ...fsOpts, }) - _mounts = await fs.mountPrivateNodes([{ path: Path.root() }]) + _mounts = [ + await fs.createPrivateNode({ + path: Path.root(), + }), + ] }) // LOADING @@ -1031,6 +1035,15 @@ describe('File System Class', () => { it("doesn't publish when asked not to do so", async () => { let published = false + fs = await FileSystem.create({ + blockstore, + ...fsOpts, + }) + + _mounts = [ + await fs.createPrivateNode({ path: Path.root() }, { skipPublish: true }), + ] + fs.on('publish', () => { published = true }) @@ -1120,7 +1133,7 @@ describe('File System Class', () => { onCommit: async (_modifications: Modification[]) => ({ commit: false }), }) - _mounts = await fs.mountPrivateNodes([{ path: Path.root() }]) + _mounts = [await fs.createPrivateNode({ path: Path.root() })] const result = await fs.transaction(async (t) => { await t.write(Path.file('private', 'file'), 'utf8', '💃') @@ -1157,11 +1170,7 @@ describe('File System Class', () => { let content = '' await receiverFs.transaction(async (t) => { - const ctx = await t.receive( - sharerDataRoot, - keypair.publicKey, - keypair.privateKey - ) + const ctx = await t.receive(sharerDataRoot, keypair) content = await ctx.read('utf8') }) @@ -1192,11 +1201,7 @@ describe('File System Class', () => { // Receive share const sharerDataRoot = b.dataRoot - const { share } = await receiverFs.receive( - sharerDataRoot, - keypair.publicKey, - keypair.privateKey - ) + const { share } = await receiverFs.receive(sharerDataRoot, keypair) const content = await share.read( 'utf8', @@ -1206,4 +1211,153 @@ describe('File System Class', () => { // Assert assert.equal(content, '🔒') }) + + it('can mount a share made to self', async () => { + const keypair = await ExchangeKey.generate() + + // Register exchange key + await fs.registerExchangeKey('device', keypair.publicKey) + + // Assign sharer identifier & create share + const path = Path.file('private', 'nested', 'level 2', 'fileToShare') + await fs.assignIdentifier('did:test:2') + await fs.write(path, 'utf8', '🔒') + await fs.share( + Path.directory('private', 'nested'), + await fs.calculateDataRoot() + ) + + // Unmount root test node, otherwise we'd search from the root, + // instead of the new mount. + fs.unmountPrivateNode(Path.root()) + + // Mount node + await fs.mountPrivateNode({ + path: Path.directory('shared', 'item'), + exchangeKeyPair: keypair, + }) + + const content = await fs.read( + Path.file('private', 'shared', 'item', 'level 2', 'fileToShare'), + 'utf8' + ) + + // Assert + assert.equal(content, '🔒') + }) + + it('can mount the root node shared to self', async () => { + const keypair = await ExchangeKey.generate() + + // Register exchange key + await fs.registerExchangeKey('device', keypair.publicKey) + + // Assign sharer identifier & create share + const path = Path.file('private', 'nested', 'level 2', 'fileToShare') + await fs.assignIdentifier('did:test:3') + await fs.write(path, 'utf8', '🔒') + await fs.share(Path.directory('private'), await fs.calculateDataRoot()) + + // Unmount existing root node + fs.unmountPrivateNode(Path.root()) + + // Remount root node using exchange key pair + await fs.mountPrivateNode({ + path: Path.root(), + exchangeKeyPair: keypair, + }) + + // Check content + const content = await fs.read( + Path.file('private', 'nested', 'level 2', 'fileToShare'), + 'utf8' + ) + + assert.equal(content, '🔒') + + // Test mutations + await fs.write( + Path.file('private', 'nested', 'level 2', 'fileToShare'), + 'utf8', + '🚀' + ) + + const content2 = await fs.read( + Path.file('private', 'nested', 'level 2', 'fileToShare'), + 'utf8' + ) + + assert.equal(content2, '🚀') + + await fs.write(Path.file('private', 'dir', 'test'), 'utf8', '✌️') + + const content3 = await fs.read(Path.file('private', 'dir', 'test'), 'utf8') + + assert.equal(content3, '✌️') + }) + + it('can create and load a private node using an exchange key', async () => { + const keypair = await ExchangeKey.generate() + + fs = await FileSystem.create({ + blockstore, + ...fsOpts, + }) + + try { + await fs.createPrivateNode({ + path: Path.root(), + exchangeKeyPair: keypair, + }) + } catch (error) { + assert(error instanceof Error) + } + + await fs.assignIdentifier('did:test:4') + await fs.registerExchangeKey('device', keypair.publicKey) + + _mounts = [ + await fs.createPrivateNode({ + path: Path.root(), + exchangeKeyPair: keypair, + }), + ] + + await fs.write( + Path.file('private', 'nested', 'level 2', 'fileToShare'), + 'utf8', + '🚀' + ) + + const content2 = await fs.read( + Path.file('private', 'nested', 'level 2', 'fileToShare'), + 'utf8' + ) + + assert.equal(content2, '🚀') + + const { dataRoot } = await fs.write( + Path.file('private', 'dir', 'test'), + 'utf8', + '✌️' + ) + + // Try to load a new instance + const fsInstance = await FileSystem.fromCID(dataRoot, { + blockstore, + ...fsOpts, + }) + + await fsInstance.mountPrivateNode({ + path: Path.root(), + exchangeKeyPair: keypair, + }) + + const content = await fsInstance.read( + Path.file('private', 'dir', 'test'), + 'utf8' + ) + + assert.equal(content, '✌️') + }) }) From e05dd8edd5baad922ee084abcd1c376c4729242a Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Mon, 1 Apr 2024 14:16:12 +0200 Subject: [PATCH 5/7] chore: Several share improvements --- packages/nest/README.md | 39 +++-- packages/nest/src/class.ts | 76 +++++++-- packages/nest/src/share.ts | 15 ++ packages/nest/src/sharing.ts | 31 +++- packages/nest/src/transaction.ts | 46 ++++- packages/nest/test/class.test.ts | 140 +++++++++++++++- pnpm-lock.yaml | 278 ++++++++++++++++++++++++++++++- 7 files changed, 572 insertions(+), 53 deletions(-) diff --git a/packages/nest/README.md b/packages/nest/README.md index 22f38b2..1e67d97 100644 --- a/packages/nest/README.md +++ b/packages/nest/README.md @@ -10,9 +10,10 @@ A layer around the `wnfs` package that provides a `FileSystem` class, a root tre - A root tree, holding references to all the needed individual parts (public fs, private forest, exchange, etc) - A mounting system for private nodes, mount specific paths. - A unix-fs compatibility layer for the public file system (allows for public files to be viewed through, for example, IPFS gateways) -- Private data sharing helpers +- Private data sharing + helpers - Provides a transaction system, rewinding the state if an error occurs. -- Creates a private forest automatically with a RSA modules using the Web Crypto API (supported on multiple platforms) +- Creates a private forest automatically with a RSA modulus using the Web Crypto API (supported on multiple platforms) +- In addition to the default symmetric key, use an RSA-OAEP asymmetric key to mount a private node (essentially sharing to self). Can be used to load a private directory, or file, using a passkey + the PRF extension. - Ability to verify commits to the file system. If a commit, aka. modification, is not verified, it will result in a no-op. - And more: typed paths, events, path helpers, data casting, … @@ -121,11 +122,11 @@ fs.rename fs.write ``` -## Identity +## Identifier ```ts -fs.identity() -fs.assignIdentity('did') +fs.identifier() +fs.assignIdentifier('did') ``` ## Private Data Sharing @@ -134,32 +135,44 @@ Flow: 1. The receiver of a share register their exchange key. An app could do this automatically when the app starts, or at some other time. 2. The data root of the receiver is passed to the sharer. Ideally this is done through some naming system. For example, you use DNS to map a username to the data root (eg. `TXT file-system.tokono.ma` could resolve to the data root, a CID). That said, this could also be done without a naming system, maybe by presenting a QR code. -3. The sharer creates the share. -4. This step is the reverse of step 2, where we pass the sharer's data root to the receiver. -5. Use the shared item. +3. Make sure the sharer's file system has an identity assigned. +4. The sharer creates the share. +5. This step is the reverse of step 2, where we pass the sharer's data root to the receiver. +6. Use the shared item. ```ts -// Step 1 & 2 +// Step 1 & 2 (Receiver) const { dataRoot } = await fs.registerExchangeKey('key-id', publicKey) const receiverDataRoot = dataRoot -// Step 3 & 4 + +// Step 3, 4 & 5 (Sharer) +await fs.assignIdentifier('did') + const { dataRoot } = await fs.share(pathToPrivateItem, receiverDataRoot) const sharerDataRoot = dataRoot -// Step 5 -const { share } = await fs.receive(sharerDataRoot, { publicKey, privateKey }) + +// Step 6 (Receiver) +const share = await fs.receive(sharerDataRoot, { publicKey, privateKey }) await share.read('utf8') ``` ## Manage private node using exchange key pair -Instead of keeping the symmetric capsule key around we can use an exchange key pair to mount a private node. This basically creates a share for ourselves. +Instead of keeping the (symmetric) capsule key around we can use an (asymmetric) exchange key pair to mount a private node. This basically creates a share for ourselves. ```ts +// 🚀 Create & mount await fs.createPrivateNode({ path: Path.root(), exchangeKeyPair: { publicKey, privateKey }, // 🔑 Pass in key pair here }) + +// 🧳 Load +await fs.mountPrivateNode({ + path: Path.root(), + exchangeKeyPair: { publicKey, privateKey }, +}) ``` ## Transactions diff --git a/packages/nest/src/class.ts b/packages/nest/src/class.ts index e664c72..b7476c1 100644 --- a/packages/nest/src/class.ts +++ b/packages/nest/src/class.ts @@ -262,6 +262,29 @@ export class FileSystem { * @param mutationOptions * @group Mounting */ + async createPrivateNode( + node: { + path: Path.Distinctive + exchangeKeyPair: { + publicKey: CryptoKey | Uint8Array + privateKey: CryptoKey + } + }, + mutationOptions?: MutationOptions + ): Promise<{ + path: Path.Distinctive> + capsuleKey: Uint8Array + shareId: string + }> + async createPrivateNode( + node: { + path: Path.Distinctive + }, + mutationOptions?: MutationOptions + ): Promise<{ + path: Path.Distinctive> + capsuleKey: Uint8Array + }> async createPrivateNode( node: { path: Path.Distinctive @@ -274,6 +297,7 @@ export class FileSystem { ): Promise<{ path: Path.Distinctive> capsuleKey: Uint8Array + shareId?: string }> { const { path } = node const absolutePosixPath = Path.toPosix(path, { absolute: true }) @@ -354,7 +378,15 @@ export class FileSystem { // Share to self, pt. 2 if (node.exchangeKeyPair !== undefined) { - await this.share(pathWithPartition, dataRoot, { mutationOptions }) + const { shareId } = await this.share(pathWithPartition, dataRoot, { + mutationOptions, + }) + + return { + path: pathWithPartition, + capsuleKey: accessKey.toBytes(), + shareId, + } } // Fin @@ -382,6 +414,7 @@ export class FileSystem { publicKey: CryptoKey | Uint8Array privateKey: CryptoKey } + shareId?: string } ): Promise { await this.mountPrivateNodes([node]) @@ -409,6 +442,7 @@ export class FileSystem { publicKey: CryptoKey | Uint8Array privateKey: CryptoKey } + shareId?: string } > ): Promise { @@ -432,6 +466,7 @@ export class FileSystem { await this.calculateDataRoot(), args.exchangeKeyPair, { + shareId: args.shareId, sharerBlockstore: this.#blockstore, } ).then((a) => a.sharedNode) @@ -780,8 +815,8 @@ export class FileSystem { ) } - // IDENTITY - // -------- + // IDENTIFIER + // ---------- async assignIdentifier( did: string, @@ -828,14 +863,14 @@ export class FileSystem { * NOTE: A share can only be received if the exchange key was registered * and the receiver is in possession of the associated private key. * - * @param itemName * @param sharerDataRoot The data root CID from the sharer * @param exchangeKeyPair A RSA-OAEP-256 key pair * @param exchangeKeyPair.publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes * @param exchangeKeyPair.privateKey A RSA-OAEP-256 private key in the form of a `CryptoKey` * @param opts Optional overrides - * @param opts.sharerBlockstore Specify what blockstore to use to load the sharer's file system - * @param opts.sharerRootTreeClass Specify what root tree class was used for the sharer's file system + * @param opts.shareId Specify what shareId to use, otherwise this'll load the last share that was made to the given exchange key. + * @param opts.sharerBlockstore Specify what blockstore to use to load the sharer's file system. + * @param opts.sharerRootTreeClass Specify what root tree class was used for the sharer's file system. * * @group Sharing */ @@ -846,21 +881,16 @@ export class FileSystem { privateKey: CryptoKey }, opts: { + shareId?: string sharerBlockstore?: Blockstore sharerRootTreeClass?: typeof RootTree } = {} - ): Promise<{ - share: Share - }> { - const share = await this.#transactionContext().receive( + ): Promise { + return await this.#transactionContext().receive( sharerDataRoot, exchangeKeyPair, opts ) - - return { - share, - } } /** @@ -914,14 +944,26 @@ export class FileSystem { receiverBlockstore?: Blockstore receiverRootTreeClass?: typeof RootTree } = {} - ): Promise> { - return await this.#infusedTransaction( + ): Promise<{ shareId: string } & MutationResult> { + let shareId: string | undefined + + const result = await this.#infusedTransaction( async (t) => { - await t.share(path, receiverDataRoot, opts) + const shareResult = await t.share(path, receiverDataRoot, opts) + shareId = shareResult.shareId }, path, opts.mutationOptions ) + + if (shareId === undefined) { + throw new Error('`shareId` was not set') + } + + return { + ...result, + shareId, + } } // TRANSACTIONS diff --git a/packages/nest/src/share.ts b/packages/nest/src/share.ts index c96d7d7..64e0274 100644 --- a/packages/nest/src/share.ts +++ b/packages/nest/src/share.ts @@ -1,4 +1,5 @@ import type { Blockstore } from 'interface-blockstore' +import type { PrivateNode } from 'wnfs' import * as Queries from './queries.js' import * as Path from './path.js' @@ -22,12 +23,15 @@ import { dataFromBytes } from './data.js' // CLASS export class Share { + readonly id: string + readonly #blockstore: Blockstore readonly #privateNodes: MountedPrivateNodes readonly #rootTree: RootTree readonly #rng: Rng /** + * @param id * @param blockstore * @param privateNodes * @param rng @@ -35,17 +39,28 @@ export class Share { * @internal */ constructor( + id: string, blockstore: Blockstore, privateNodes: MountedPrivateNodes, rng: Rng, rootTree: RootTree ) { + this.id = id this.#blockstore = blockstore this.#privateNodes = privateNodes this.#rng = rng this.#rootTree = rootTree } + // EXPORT + + export(): PrivateNode { + const node = this.#privateNodes['/'] + if (node === undefined) + throw new Error('Expected a node to be mounted at root') + return node.node + } + // QUERIES /** diff --git a/packages/nest/src/sharing.ts b/packages/nest/src/sharing.ts index 286e1f5..985af41 100644 --- a/packages/nest/src/sharing.ts +++ b/packages/nest/src/sharing.ts @@ -24,6 +24,7 @@ import { ExchangeKey } from './exchange-key.js' * @param exchangeKeyPair.publicKey * @param exchangeKeyPair.privateKey * @param opts + * @param opts.shareId * @param opts.sharerBlockstore * @param opts.sharerRootTreeClass */ @@ -34,10 +35,12 @@ export async function loadShare( privateKey: CryptoKey }, opts: { + shareId?: string sharerBlockstore: Blockstore sharerRootTreeClass?: typeof RootTree } ): Promise<{ + shareId: string sharedNode: PrivateNode sharerRootTree: RootTree }> { @@ -60,14 +63,20 @@ export async function loadShare( throw new Error("The sharer's file system is missing an identifier") // Find the share number - const shareNumber: bigint = await findLatestShareCounter( - 0, - sharerCounter < 1 ? 1 : sharerCounter, - publicKeyResult, - sharerIdentifier, - sharerForest, - Store.wnfs(sharerBlockstore) - ) + const shareNumber: undefined | number | bigint = + opts.shareId === undefined + ? await findLatestShareCounter( + 0, + sharerCounter < 1 ? 1 : sharerCounter, + publicKeyResult, + sharerIdentifier, + sharerForest, + Store.wnfs(sharerBlockstore) + ) + : Number.parseInt(opts.shareId) + + if (shareNumber === undefined) + throw new Error('Failed to determine share number') // Determine share name const shareLabel = createShareName( @@ -95,5 +104,9 @@ export async function loadShare( Store.wnfs(sharerBlockstore) ) - return { sharedNode, sharerRootTree } + return { + shareId: Number(shareNumber).toString(), + sharedNode, + sharerRootTree, + } } diff --git a/packages/nest/src/transaction.ts b/packages/nest/src/transaction.ts index dc14993..4aa39c3 100644 --- a/packages/nest/src/transaction.ts +++ b/packages/nest/src/transaction.ts @@ -561,7 +561,7 @@ export class TransactionContext { } } - // IDENTITY + // IDENTIFIER async assignIdentifier(did: string): Promise { this.#rootTree = await this.#rootTree.replaceDID(did) @@ -616,14 +616,14 @@ export class TransactionContext { * NOTE: A share can only be received if the exchange key was registered * and the receiver is in possession of the associated private key. * - * @param itemName * @param sharerDataRoot The data root CID from the sharer * @param exchangeKeyPair A RSA-OAEP-256 key pair * @param exchangeKeyPair.publicKey A RSA-OAEP-256 public key in the form of a `CryptoKey` or its modulus bytes * @param exchangeKeyPair.privateKey A RSA-OAEP-256 private key in the form of a `CryptoKey` * @param opts Optional overrides - * @param opts.sharerBlockstore Specify what blockstore to use to load the sharer's file system - * @param opts.sharerRootTreeClass Specify what root tree class was used for the sharer's file system + * @param opts.shareId Specify what shareId to use, otherwise this'll load the last share that was made to the given exchange key. + * @param opts.sharerBlockstore Specify what blockstore to use to load the sharer's file system. + * @param opts.sharerRootTreeClass Specify what root tree class was used for the sharer's file system. * * @group Sharing */ @@ -634,16 +634,18 @@ export class TransactionContext { privateKey: CryptoKey }, opts: { + shareId?: string sharerBlockstore?: Blockstore sharerRootTreeClass?: typeof RootTree } = {} ): Promise { const sharerBlockstore = opts.sharerBlockstore ?? this.#blockstore - const { sharedNode, sharerRootTree } = await loadShare( + const { shareId, sharedNode, sharerRootTree } = await loadShare( sharerDataRoot, exchangeKeyPair, { sharerBlockstore, + shareId: opts.shareId, sharerRootTreeClass: opts.sharerRootTreeClass, } ) @@ -654,6 +656,7 @@ export class TransactionContext { ) return new Share( + shareId, sharerBlockstore, { [Path.toPosix(path, { absolute: true })]: { path, node: sharedNode } }, this.#rng, @@ -715,7 +718,7 @@ export class TransactionContext { receiverBlockstore?: Blockstore receiverRootTreeClass?: typeof RootTree } = {} - ): Promise { + ): Promise<{ shareId: string }> { const did = this.identifier() if (did === undefined) @@ -738,9 +741,29 @@ export class TransactionContext { receiverDataRoot ) + const receiverWnfsBlockstore = Store.wnfs(receiverBlockstore) const exchangeRoot: Uint8Array = await receiverRootTree .exchangeRoot() - .store(Store.wnfs(receiverBlockstore)) + .store(receiverWnfsBlockstore) + + // Create a "merged" blockstore + const sharerBlockstore = Store.wnfs(this.#blockstore) + + const mergedBlockstore = { + async getBlock(cid: Uint8Array): Promise { + if (await sharerBlockstore.hasBlock(cid)) + return await sharerBlockstore.getBlock(cid) + return await receiverWnfsBlockstore.getBlock(cid) + }, + + async hasBlock(cid: Uint8Array): Promise { + if (await sharerBlockstore.hasBlock(cid)) return true + return await receiverWnfsBlockstore.hasBlock(cid) + }, + + putBlockKeyed: sharerBlockstore.putBlockKeyed.bind(sharerBlockstore), + putBlock: sharerBlockstore.putBlock?.bind(sharerBlockstore), + } // Create share const forest: PrivateForest = await share( @@ -749,11 +772,11 @@ export class TransactionContext { did, exchangeRoot, this.#rootTree.privateForest(), - Store.wnfs(this.#blockstore) + mergedBlockstore ) // Update counter - await this.#rootTree.increaseShareCounter() + this.#rootTree = await this.#rootTree.increaseShareCounter() // Modification const change = { @@ -765,6 +788,11 @@ export class TransactionContext { // Replace root tree this.#rootTree = await this.#rootTree.replacePrivateForest(forest, [change]) + + // Fin + return { + shareId: counter.toString(), + } } // ㊙️ ▒▒ QUERIES diff --git a/packages/nest/test/class.test.ts b/packages/nest/test/class.test.ts index 06df36a..13e5d35 100644 --- a/packages/nest/test/class.test.ts +++ b/packages/nest/test/class.test.ts @@ -1201,7 +1201,7 @@ describe('File System Class', () => { // Receive share const sharerDataRoot = b.dataRoot - const { share } = await receiverFs.receive(sharerDataRoot, keypair) + const share = await receiverFs.receive(sharerDataRoot, keypair) const content = await share.read( 'utf8', @@ -1360,4 +1360,142 @@ describe('File System Class', () => { assert.equal(content, '✌️') }) + + it('can share multiple items and still load the older ones', async () => { + const keypairA = await ExchangeKey.generate() + const keypairB = await ExchangeKey.generate() + + const receiverFs = await FileSystem.create({ + blockstore, + ...fsOpts, + }) + + // Register exchange key + await receiverFs.registerExchangeKey('device-a', keypairA.publicKey) + await receiverFs.registerExchangeKey('device-b', keypairB.publicKey) + const receiverDataRoot = await receiverFs.calculateDataRoot() + + // Assign sharer identifier & create share + await fs.assignIdentifier('did:test:5') + + await fs.write(Path.file('private', 'file 1'), 'utf8', '🔐 1') + await fs.write(Path.file('private', 'file 2'), 'utf8', '🔒 2') + + const { shareId } = await fs.share( + Path.file('private', 'file 1'), + receiverDataRoot + ) + + await fs.share(Path.file('private', 'file 2'), receiverDataRoot) + + // Receive shares + const sharerDataRoot = await fs.calculateDataRoot() + + const content1 = await receiverFs + .receive(sharerDataRoot, keypairA, { shareId }) + .then(async (share) => await share.read('utf8')) + + const content2 = await receiverFs + .receive(sharerDataRoot, keypairB) + .then(async (share) => await share.read('utf8')) + + // Assert + assert.equal(content1, '🔐 1') + assert.equal(content2, '🔒 2') + }) + + it('can create multiple private nodes with an exchange key and still mount the older one', async () => { + const keypair = await ExchangeKey.generate() + + fs = await FileSystem.create({ + blockstore, + ...fsOpts, + }) + + await fs.assignIdentifier('did:test:6') + await fs.registerExchangeKey('device', keypair.publicKey) + + const first = await fs.createPrivateNode({ + path: Path.directory('first'), + exchangeKeyPair: keypair, + }) + + const _second = await fs.createPrivateNode({ + path: Path.directory('second'), + exchangeKeyPair: keypair, + }) + + await fs.write( + Path.file('private', 'first', 'nested', 'file'), + 'utf8', + '🔑' + ) + + await fs.write(Path.file('private', 'second', 'file'), 'utf8', '🔒') + + const fsInstance = await FileSystem.fromCID(await fs.calculateDataRoot(), { + blockstore, + }) + + await fsInstance.mountPrivateNode({ + path: Path.directory('first'), + exchangeKeyPair: keypair, + shareId: first.shareId, + }) + + await fsInstance.mountPrivateNode({ + path: Path.directory('second'), + exchangeKeyPair: keypair, + }) + + const contents = await fsInstance.read( + Path.file('private', 'first', 'nested', 'file'), + 'utf8' + ) + + assert.equal(contents, '🔑') + + const contents2 = await fsInstance.read( + Path.file('private', 'second', 'file'), + 'utf8' + ) + + assert.equal(contents2, '🔒') + }) + + it('can create a private node using an exchange key and then later mount it using the capsule key', async () => { + const keypair = await ExchangeKey.generate() + + fs = await FileSystem.create({ + blockstore, + ...fsOpts, + }) + + await fs.assignIdentifier('did:test:7') + await fs.registerExchangeKey('device', keypair.publicKey) + + await fs.createPrivateNode({ + path: Path.root(), + exchangeKeyPair: keypair, + }) + + const { capsuleKey, dataRoot } = await fs.write( + Path.file('private', 'file'), + 'utf8', + '👀' + ) + + const fsInstance = await FileSystem.fromCID(dataRoot, { + blockstore, + }) + + await fsInstance.mountPrivateNode({ + path: Path.root(), + capsuleKey, + }) + + const contents = await fs.read(Path.file('private', 'file'), 'utf8') + + assert.equal(contents, '👀') + }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d22e1f2..cc58702 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,6 +116,34 @@ importers: specifier: ^3.3.0 version: 3.3.0(vite@5.2.2) + examples/passkey: + dependencies: + '@wnfs-wg/nest': + specifier: '*' + version: link:../../packages/nest + idb-keyval: + specifier: ^6.2.1 + version: 6.2.1 + iso-base: + specifier: ^4.0.0 + version: 4.0.0 + iso-passkeys: + specifier: ^0.2.2 + version: 0.2.2 + devDependencies: + '@rsbuild/core': + specifier: ^0.5.2 + version: 0.5.2 + '@types/node': + specifier: ^20.11.30 + version: 20.11.30 + '@types/qrcode': + specifier: ^1.5.5 + version: 1.5.5 + typescript: + specifier: 5.4.3 + version: 5.4.3 + examples/web3storage: dependencies: '@picocss/pico': @@ -458,6 +486,54 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true + /@cbor-extract/cbor-extract-darwin-arm64@2.2.0: + resolution: {integrity: sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@cbor-extract/cbor-extract-darwin-x64@2.2.0: + resolution: {integrity: sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@cbor-extract/cbor-extract-linux-arm64@2.2.0: + resolution: {integrity: sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@cbor-extract/cbor-extract-linux-arm@2.2.0: + resolution: {integrity: sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@cbor-extract/cbor-extract-linux-x64@2.2.0: + resolution: {integrity: sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@cbor-extract/cbor-extract-win32-x64@2.2.0: + resolution: {integrity: sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@chainsafe/is-ip@2.0.2: resolution: {integrity: sha512-ndGqEMG1W5WkGagaqOZHpPU172AGdxr+LD15sv3WIUvT5oCFUrG1Y0CW/v2Egwj4JXEvSibaIIIqImsm98y1nA==} @@ -1378,6 +1454,10 @@ packages: resolution: {integrity: sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==} dev: false + /@noble/ed25519@2.1.0: + resolution: {integrity: sha512-KM4qTyXPinyCgMzeYJH/UudpdL+paJXtY3CHtHYZQtBkS8MZoPr4rOikZllIutJe0d06QDQKisyn02gxZ8TcQA==} + dev: false + /@noble/hashes@1.3.3: resolution: {integrity: sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==} engines: {node: '>= 16'} @@ -1409,6 +1489,33 @@ packages: fastq: 1.17.1 dev: true + /@peculiar/asn1-ecc@2.3.8: + resolution: {integrity: sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ==} + dependencies: + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/asn1-x509': 2.3.8 + asn1js: 3.0.5 + tslib: 2.6.2 + dev: false + + /@peculiar/asn1-schema@2.3.8: + resolution: {integrity: sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==} + dependencies: + asn1js: 3.0.5 + pvtsutils: 1.3.5 + tslib: 2.6.2 + dev: false + + /@peculiar/asn1-x509@2.3.8: + resolution: {integrity: sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw==} + dependencies: + '@peculiar/asn1-schema': 2.3.8 + asn1js: 3.0.5 + ipaddr.js: 2.1.0 + pvtsutils: 1.3.5 + tslib: 2.6.2 + dev: false + /@perma/map@1.0.3: resolution: {integrity: sha512-Bf5njk0fnJGTFE2ETntq0N1oJ6YdCPIpTDn3R3KYZJQdeYSOCNL7mBrFlGnbqav8YQhJA/p81pvHINX9vAtHkQ==} dependencies: @@ -1940,6 +2047,10 @@ packages: resolution: {integrity: sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==} dev: false + /@types/retry@0.12.2: + resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} + dev: false + /@types/semver@7.5.8: resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} dev: true @@ -2621,6 +2732,15 @@ packages: engines: {node: '>=8'} dev: true + /asn1js@3.0.5: + resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} + engines: {node: '>=12.0.0'} + dependencies: + pvtsutils: 1.3.5 + pvutils: 1.1.3 + tslib: 2.6.2 + dev: false + /assert@2.1.0: resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} dependencies: @@ -2816,6 +2936,28 @@ packages: resolution: {integrity: sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==} dev: true + /cbor-extract@2.2.0: + resolution: {integrity: sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==} + hasBin: true + requiresBuild: true + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.0 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.0 + '@cbor-extract/cbor-extract-linux-arm': 2.2.0 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.0 + '@cbor-extract/cbor-extract-linux-x64': 2.2.0 + '@cbor-extract/cbor-extract-win32-x64': 2.2.0 + dev: false + optional: true + + /cbor-x@1.5.9: + resolution: {integrity: sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ==} + optionalDependencies: + cbor-extract: 2.2.0 + dev: false + /cborg@4.1.3: resolution: {integrity: sha512-I8sAcVtiarz0dZ4IYixNUaL2hIl9cMDjo1ytI57F5fUlekTEO5Im8aXbAvsuayeP76hHSPRMwos0AUuntHJjqQ==} hasBin: true @@ -3006,6 +3148,21 @@ packages: semver: 7.6.0 dev: false + /conf@12.0.0: + resolution: {integrity: sha512-fIWyWUXrJ45cHCIQX+Ck1hrZDIf/9DR0P0Zewn3uNht28hbt5OfGUq8rRWsxi96pZWPyBEd0eY9ama01JTaknA==} + engines: {node: '>=18'} + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + atomically: 2.0.2 + debounce-fn: 5.1.2 + dot-prop: 8.0.2 + env-paths: 3.0.0 + json-schema-typed: 8.0.1 + semver: 7.6.0 + uint8array-extras: 0.3.0 + dev: false + /convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true @@ -3157,6 +3314,11 @@ packages: object-keys: 1.1.1 dev: true + /delay@6.0.0: + resolution: {integrity: sha512-2NJozoOHQ4NuZuVIr5CWd0iiLVIRSDepakaovIN+9eIDHEhdCAEvSy2cuf1DCrPPQLvHmbqTHODlhHg8UCy4zw==} + engines: {node: '>=16'} + dev: false + /depcheck@1.4.7: resolution: {integrity: sha512-1lklS/bV5chOxwNKA/2XUUk/hPORp8zihZsXflr8x0kLwmcZ9Y9BsS6Hs3ssvA+2wUVbG0U2Ciqvm1SokNjPkA==} engines: {node: '>=10'} @@ -3198,6 +3360,17 @@ packages: engines: {node: '>=0.10.0'} dev: true + /detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + requiresBuild: true + dev: false + optional: true + + /did-resolver@4.1.0: + resolution: {integrity: sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA==} + dev: false + /diff@5.0.0: resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} engines: {node: '>=0.3.1'} @@ -3268,6 +3441,13 @@ packages: type-fest: 2.19.0 dev: false + /dot-prop@8.0.2: + resolution: {integrity: sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ==} + engines: {node: '>=16'} + dependencies: + type-fest: 3.13.1 + dev: false + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true @@ -3309,7 +3489,7 @@ packages: resolution: {integrity: sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==} engines: {node: '>=10.13.0'} dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 tapable: 2.2.1 dev: true @@ -4651,6 +4831,11 @@ packages: side-channel: 1.0.6 dev: true + /ipaddr.js@2.1.0: + resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==} + engines: {node: '>= 10'} + dev: false + /ipfs-unixfs-exporter@13.5.0: resolution: {integrity: sha512-s1eWXzoyhQFNEAB1p+QE3adjhW+lBdgpORmmjiCLiruHs5z7T5zsAgRVcWpM8LWYhq2flRtJHObb7Hg73J+oLQ==} dependencies: @@ -4896,6 +5081,11 @@ packages: engines: {node: '>= 0.4'} dev: true + /is-network-error@1.1.0: + resolution: {integrity: sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==} + engines: {node: '>=16'} + dev: false + /is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -5012,11 +5202,54 @@ packages: bigint-mod-arith: 3.3.1 dev: false + /iso-base@4.0.0: + resolution: {integrity: sha512-Vf+6r7XlP+LQD0HSR0ZBLSj/mwuP+59ElbgMKT+iFSnkBW2RzRohQScgKfPCYbaGZAjEdbi40eYq28E/RAnrVA==} + dependencies: + base-x: 4.0.0 + bigint-mod-arith: 3.3.1 + dev: false + + /iso-did@1.6.0: + resolution: {integrity: sha512-DBq2MzTMGMJgYs/3jhj+h/EuDuQs7NWUYwNtXpBgbTRl6CVqsu0nAHqzxIzaZYJbb05msnq7KUwMCZmLCQ9asA==} + dependencies: + did-resolver: 4.1.0 + iso-base: 2.0.1 + iso-web: 1.0.5 + multiformats: 13.1.0 + dev: false + + /iso-kv@3.0.2: + resolution: {integrity: sha512-DL4TNf1SRVskOKRsEk2QjMHsLUWh1H+iV4LPc9dMDfi1wcb/HlkGl+9ETphk3FN5ToL6l6CUKdeHGYyOPdmMHg==} + dependencies: + conf: 12.0.0 + idb-keyval: 6.2.1 + kysely: 0.27.3 + dev: false + + /iso-passkeys@0.2.2: + resolution: {integrity: sha512-CsuztK2+FOmlq7LVNhwLYiDDeVzBqCL/iHFWzpY8YhiNgiBMVi8Vtxb5pev780QWgKKh3XmLPPH/rgaZ2XuIPQ==} + dependencies: + '@noble/ed25519': 2.1.0 + '@peculiar/asn1-ecc': 2.3.8 + '@peculiar/asn1-schema': 2.3.8 + cbor-x: 1.5.9 + iso-base: 2.0.1 + iso-did: 1.6.0 + dev: false + /iso-url@1.2.1: resolution: {integrity: sha512-9JPDgCN4B7QPkLtYAAOrEuAWvP9rWvR5offAr0/SeF046wIkglqH3VXgYYP6NcsKslH80UIVgmPqNe3j7tG2ng==} engines: {node: '>=12'} dev: false + /iso-web@1.0.5: + resolution: {integrity: sha512-ZRZ5BgGAvwau+WtNLZXp1byawaMPFUFnTwVHLXKFXIKxz0CD9hkEkSe505kxcNEui0TqVEQBOwg4806au1djGg==} + dependencies: + delay: 6.0.0 + iso-kv: 3.0.2 + p-retry: 6.2.0 + dev: false + /iso-websocket@0.2.0: resolution: {integrity: sha512-imBalzmPSq0C9CfMouimB2kZ5X1qS4Yai8kGTQdluGRb0T0iu+BkPcakFelh4FIlTM8y6+BNuCEGog3lf8HC4A==} dependencies: @@ -5276,6 +5509,11 @@ packages: engines: {node: '>=6'} dev: true + /kysely@0.27.3: + resolution: {integrity: sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==} + engines: {node: '>=14.0.0'} + dev: false + /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -5669,6 +5907,15 @@ packages: whatwg-url: 5.0.0 dev: false + /node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + requiresBuild: true + dependencies: + detect-libc: 2.0.3 + dev: false + optional: true + /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true @@ -5909,6 +6156,15 @@ packages: retry: 0.13.1 dev: false + /p-retry@6.2.0: + resolution: {integrity: sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==} + engines: {node: '>=16.17'} + dependencies: + '@types/retry': 0.12.2 + is-network-error: 1.1.0 + retry: 0.13.1 + dev: false + /p-timeout@6.1.2: resolution: {integrity: sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==} engines: {node: '>=14.16'} @@ -6173,6 +6429,17 @@ packages: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} dev: true + /pvtsutils@1.3.5: + resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==} + dependencies: + tslib: 2.6.2 + dev: false + + /pvutils@1.1.3: + resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} + engines: {node: '>=6.0.0'} + dev: false + /qrcode@1.5.3: resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==} engines: {node: '>=10.13.0'} @@ -6904,7 +7171,6 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - dev: true /tsutils-etc@1.4.2(tsutils@3.21.0)(typescript@5.3.3): resolution: {integrity: sha512-2Dn5SxTDOu6YWDNKcx1xu2YUy6PUeKrWZB/x2cQ8vY2+iz3JRembKn/iZ0JLT1ZudGNwQQvtFX9AwvRHbXuPUg==} @@ -6974,7 +7240,6 @@ packages: /type-fest@3.13.1: resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} engines: {node: '>=14.16'} - dev: true /type-fest@4.13.1: resolution: {integrity: sha512-ASMgM+Vf2cLwDMt1KXSkMUDSYCxtckDJs8zsaVF/mYteIsiARKCVtyXtcK38mIKbLTctZP8v6GMqdNaeI3fo7g==} @@ -7092,6 +7357,11 @@ packages: uint8arraylist: 2.4.8 uint8arrays: 5.0.3 + /uint8array-extras@0.3.0: + resolution: {integrity: sha512-erJsJwQ0tKdwuqI0359U8ijkFmfiTcq25JvvzRVc1VP+2son1NJRXhxcAKJmAW3ajM8JSGAfsAXye8g4s+znxA==} + engines: {node: '>=18'} + dev: false + /uint8arraylist@2.4.8: resolution: {integrity: sha512-vc1PlGOzglLF0eae1M8mLRTBivsvrGsdmJ5RbK3e+QRvRLOZfZhQROTwH/OfyF3+ZVUg9/8hE8bmKP2CvP9quQ==} dependencies: @@ -7253,7 +7523,7 @@ packages: engines: {node: '>=10.13.0'} dependencies: glob-to-regexp: 0.4.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 dev: true /webidl-conversions@3.0.1: From 2414180c58bb1029686f4eac1ec3025a9c2f45e3 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Thu, 4 Apr 2024 18:56:27 +0200 Subject: [PATCH 6/7] refactor!: Reduce complexity of paths --- packages/nest/README.md | 14 +- packages/nest/package.json | 2 +- packages/nest/src/class.ts | 152 +++---- packages/nest/src/common.ts | 26 +- packages/nest/src/data/sample.ts | 15 +- packages/nest/src/errors.ts | 18 +- packages/nest/src/mounts.ts | 36 +- packages/nest/src/path.ts | 570 ++++------------------- packages/nest/src/queries.ts | 76 +++- packages/nest/src/references.ts | 16 +- packages/nest/src/root-tree/basic.ts | 11 +- packages/nest/src/share.ts | 23 +- packages/nest/src/transaction.ts | 133 +++--- packages/nest/src/types.ts | 12 +- packages/nest/src/types/internal.ts | 2 +- packages/nest/src/unix.ts | 66 ++- packages/nest/test/class.test.ts | 645 ++++++++++----------------- packages/nest/test/helpers/index.ts | 57 ++- packages/nest/test/path.test.ts | 377 ---------------- 19 files changed, 706 insertions(+), 1545 deletions(-) delete mode 100644 packages/nest/test/path.test.ts diff --git a/packages/nest/README.md b/packages/nest/README.md index 1e67d97..01c57f5 100644 --- a/packages/nest/README.md +++ b/packages/nest/README.md @@ -25,17 +25,15 @@ pnpm install @wnfs-wg/nest ## Usage +Scenario 1:
+🚀 Create a new file system, create a new file and read it back. + ```ts import { FileSystem, Path } from '@wnfs-wg/nest' // Provide some block store of the `Blockstore` type from the `interface-blockstore` package import { IDBBlockstore } from 'blockstore-idb' -``` -Scenario 1:
-🚀 Create a new file system, create a new file and read it back. - -```ts const blockstore = new IDBBlockstore('path/to/store') await blockstore.open() @@ -49,9 +47,9 @@ const { capsuleKey } = await fs.createPrivateNode({ }) // Write & Read -await fs.write(Path.file('private', 'file'), 'utf8', '🪺') +await fs.write(['private', 'file'], 'utf8', '🪺') -const contents = await fs.read(Path.file('private', 'file'), 'utf8') +const contents = await fs.read(['private', 'file'], 'utf8') ``` Scenario 2:
@@ -68,7 +66,7 @@ of our root tree, the pointer to our file system. let fsPointer: CID = await fs.calculateDataRoot() // When we make a modification to the file system a verification is performed. -await fs.write(Path.file('private', 'file'), 'utf8', '🪺') +await fs.write(['private', 'file'], 'utf8', '🪺') // If the commit is approved, the changes are reflected in the file system and // the `commit` and `publish` events are emitted. diff --git a/packages/nest/package.json b/packages/nest/package.json index 09159d0..9d78239 100644 --- a/packages/nest/package.json +++ b/packages/nest/package.json @@ -106,7 +106,7 @@ "src" ], "scripts": { - "lint": "tsc --build && eslint . --quiet --ignore-pattern='README.md' && prettier --check '**/*.{js,ts,yml,json}' --ignore-path ../../.gitignore", + "lint": "tsc --build && eslint . --quiet && prettier --check '**/*.{js,ts,yml,json}' --ignore-path ../../.gitignore", "build": "tsc --build", "test": "pnpm run test:node && pnpm run test:browser", "test:node": "mocha 'test/**/!(*.browser).test.ts' --bail --timeout 30000", diff --git a/packages/nest/src/class.ts b/packages/nest/src/class.ts index b7476c1..eb20354 100644 --- a/packages/nest/src/class.ts +++ b/packages/nest/src/class.ts @@ -264,42 +264,45 @@ export class FileSystem { */ async createPrivateNode( node: { - path: Path.Distinctive + path: Path.Segments exchangeKeyPair: { publicKey: CryptoKey | Uint8Array privateKey: CryptoKey } + kind?: Path.Kind }, mutationOptions?: MutationOptions ): Promise<{ - path: Path.Distinctive> + path: Path.Partitioned capsuleKey: Uint8Array shareId: string }> async createPrivateNode( node: { - path: Path.Distinctive + path: Path.Segments + kind?: Path.Kind }, mutationOptions?: MutationOptions ): Promise<{ - path: Path.Distinctive> + path: Path.Partitioned capsuleKey: Uint8Array }> async createPrivateNode( node: { - path: Path.Distinctive + path: Path.Segments exchangeKeyPair?: { publicKey: CryptoKey | Uint8Array privateKey: CryptoKey } + kind?: Path.Kind }, mutationOptions?: MutationOptions ): Promise<{ - path: Path.Distinctive> + path: Path.Partitioned capsuleKey: Uint8Array shareId?: string }> { - const { path } = node + const { kind, path } = node const absolutePosixPath = Path.toPosix(path, { absolute: true }) if (this.#privateNodes[absolutePosixPath] !== undefined) { @@ -321,17 +324,18 @@ export class FileSystem { } // Create - const privateNode = Path.isFile(path) - ? new PrivateFile( - this.#rootTree.privateForest().emptyName(), - new Date(), - this.#rng - ).asNode() - : new PrivateDirectory( - this.#rootTree.privateForest().emptyName(), - new Date(), - this.#rng - ).asNode() + const privateNode = + kind === Path.Kind.File + ? new PrivateFile( + this.#rootTree.privateForest().emptyName(), + new Date(), + this.#rng + ).asNode() + : new PrivateDirectory( + this.#rootTree.privateForest().emptyName(), + new Date(), + this.#rng + ).asNode() // Store const storeResult = await privateNode.store( @@ -405,11 +409,11 @@ export class FileSystem { async mountPrivateNode( node: | { - path: Path.Distinctive + path: Path.Segments capsuleKey: Uint8Array } | { - path: Path.Distinctive + path: Path.Segments exchangeKeyPair: { publicKey: CryptoKey | Uint8Array privateKey: CryptoKey @@ -433,11 +437,11 @@ export class FileSystem { async mountPrivateNodes( nodes: Array< | { - path: Path.Distinctive + path: Path.Segments capsuleKey: Uint8Array } | { - path: Path.Distinctive + path: Path.Segments exchangeKeyPair: { publicKey: CryptoKey | Uint8Array privateKey: CryptoKey @@ -496,7 +500,7 @@ export class FileSystem { * @param path * @group Mounting */ - unmountPrivateNode(path: Path.Distinctive): void { + unmountPrivateNode(path: Path.Segments): void { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.#privateNodes[Path.toPosix(path, { absolute: true })] } @@ -508,9 +512,7 @@ export class FileSystem { * @param path * @group Querying */ - async contentCID( - path: Path.File> - ): Promise { + async contentCID(path: Partitioned): Promise { return await this.#transactionContext().contentCID(path) } @@ -518,9 +520,7 @@ export class FileSystem { * @param path * @group Querying */ - async capsuleCID( - path: Path.Distinctive> - ): Promise { + async capsuleCID(path: Partitioned): Promise { return await this.#transactionContext().capsuleCID(path) } @@ -529,7 +529,7 @@ export class FileSystem { * @group Querying */ async capsuleKey( - path: Path.Distinctive> + path: Partitioned ): Promise { return await this.#transactionContext().capsuleKey(path) } @@ -538,26 +538,26 @@ export class FileSystem { * @param path * @group Querying */ - async exists( - path: Path.Distinctive> - ): Promise { + async exists(path: Partitioned): Promise { return await this.#transactionContext().exists(path) } /** @group Querying */ async listDirectory( - path: Path.Directory>, + path: Partitioned, listOptions: { withItemKind: true } ): Promise async listDirectory( - path: Path.Directory>, + path: Partitioned, listOptions: { withItemKind: false } ): Promise + async listDirectory(path: Partitioned): Promise async listDirectory( - path: Path.Directory> - ): Promise + path: Partitioned, + listOptions?: { withItemKind: boolean } + ): Promise async listDirectory( - path: Path.Directory>, + path: Partitioned, listOptions?: { withItemKind: boolean } ): Promise { return await this.#transactionContext().listDirectory(path, listOptions) @@ -569,7 +569,7 @@ export class FileSystem { /** @group Querying */ async read( path: - | Path.File> + | PartitionedNonEmpty | { contentCID: CID } | { capsuleCID: CID } | { @@ -580,7 +580,7 @@ export class FileSystem { ): Promise> async read( path: - | Path.File> + | PartitionedNonEmpty | { contentCID: CID } | { capsuleCID: CID } | { @@ -600,7 +600,7 @@ export class FileSystem { * @param path * @group Querying */ - async size(path: Path.File>): Promise { + async size(path: PartitionedNonEmpty): Promise { return await this.#transactionContext().size(path) } @@ -609,15 +609,13 @@ export class FileSystem { /** @group Mutating */ async copy( - from: Path.Distinctive>, - to: Path.File> | Path.Directory>, + from: PartitionedNonEmpty, + to: Partitioned, mutationOptions?: MutationOptions ): Promise> async copy( - from: Path.Distinctive>, - to: - | Path.File> - | Path.Directory>, + from: PartitionedNonEmpty, + to: Partitioned, mutationOptions: MutationOptions = {} ): Promise> { return await this.#infusedTransaction( @@ -634,19 +632,14 @@ export class FileSystem { /** @group Mutating */ async createDirectory

( - path: Path.Directory>, + path: PartitionedNonEmpty

, mutationOptions?: MutationOptions - ): Promise< - MutationResult> }> - > + ): Promise }>> async createDirectory( - path: Path.Directory>, + path: PartitionedNonEmpty, mutationOptions: MutationOptions = {} ): Promise< - MutationResult< - Partition, - { path: Path.Directory> } - > + MutationResult }> > { let finalPath = path @@ -667,23 +660,18 @@ export class FileSystem { /** @group Mutating */ async createFile

( - path: Path.File>, + path: PartitionedNonEmpty

, dataType: DataType, data: DataForType, mutationOptions?: MutationOptions - ): Promise< - MutationResult> }> - > + ): Promise }>> async createFile( - path: Path.File>, + path: PartitionedNonEmpty, dataType: DataType, data: DataForType, mutationOptions: MutationOptions = {} ): Promise< - MutationResult< - Partition, - { path: Path.File> } - > + MutationResult }> > { let finalPath = path @@ -704,11 +692,11 @@ export class FileSystem { /** @group Mutating */ async ensureDirectory

( - path: Path.Directory>, + path: PartitionedNonEmpty

, mutationOptions?: MutationOptions ): Promise> async ensureDirectory( - path: Path.Directory>, + path: PartitionedNonEmpty, mutationOptions: MutationOptions = {} ): Promise> { return await this.#infusedTransaction( @@ -725,15 +713,13 @@ export class FileSystem { /** @group Mutating */ async move( - from: Path.Distinctive>, - to: Path.File> | Path.Directory>, + from: PartitionedNonEmpty, + to: PartitionedNonEmpty | Partitioned, mutationOptions?: MutationOptions ): Promise> async move( - from: Path.Distinctive>, - to: - | Path.File> - | Path.Directory>, + from: PartitionedNonEmpty, + to: PartitionedNonEmpty | Partitioned, mutationOptions: MutationOptions = {} ): Promise> { return await this.#infusedTransaction( @@ -754,7 +740,7 @@ export class FileSystem { * @group Mutating */ async remove( - path: Path.Distinctive>, + path: PartitionedNonEmpty, mutationOptions: MutationOptions = {} ): Promise { const transactionResult = await this.transaction(async (t) => { @@ -775,12 +761,12 @@ export class FileSystem { /** @group Mutating */ async rename

( - path: Path.Distinctive>, + path: PartitionedNonEmpty

, newName: string, mutationOptions?: MutationOptions ): Promise> async rename( - path: Path.Distinctive>, + path: PartitionedNonEmpty, newName: string, mutationOptions: MutationOptions = {} ): Promise> { @@ -795,13 +781,13 @@ export class FileSystem { /** @group Mutating */ async write

( - path: Path.File>, + path: PartitionedNonEmpty

, dataType: DataType, data: DataForType, mutationOptions?: MutationOptions ): Promise> async write( - path: Path.File>, + path: PartitionedNonEmpty, dataType: DataType, data: DataForType, mutationOptions: MutationOptions = {} @@ -937,7 +923,7 @@ export class FileSystem { * @group Sharing */ async share( - path: Path.Distinctive>, + path: Partitioned, receiverDataRoot: CID, opts: { mutationOptions?: MutationOptions @@ -1033,22 +1019,22 @@ export class FileSystem { async #infusedTransaction( handler: (t: TransactionContext) => Promise, - path: Path.Distinctive>, + path: Partitioned, mutationOptions?: MutationOptions ): Promise async #infusedTransaction( handler: (t: TransactionContext) => Promise, - path: Path.Distinctive>, + path: Partitioned, mutationOptions?: MutationOptions ): Promise async #infusedTransaction( handler: (t: TransactionContext) => Promise, - path: Path.Distinctive>, + path: Partitioned, mutationOptions?: MutationOptions ): Promise> async #infusedTransaction( handler: (t: TransactionContext) => Promise, - path: Path.Distinctive>, + path: Partitioned, mutationOptions: MutationOptions = {} ): Promise> { const transactionResult = await this.transaction(handler, mutationOptions) diff --git a/packages/nest/src/common.ts b/packages/nest/src/common.ts index a2eb251..abeb4e0 100644 --- a/packages/nest/src/common.ts +++ b/packages/nest/src/common.ts @@ -1,16 +1,16 @@ import * as Path from './path.js' +/** + * + * @param path + */ export function addOrIncreaseNameNumber( - path: Path.Directory> -): Path.Directory> -export function addOrIncreaseNameNumber( - path: Path.File> -): Path.File> -export function addOrIncreaseNameNumber( - path: Path.Distinctive> -): Path.Distinctive> { - const regex = Path.isFile(path) ? /( \((\d+)\))?(\.[^$]+)?$/ : /( \((\d+)\))$/ + path: Path.PartitionedNonEmpty +): Path.PartitionedNonEmpty { const terminus = Path.terminus(path) + const regex = terminus.includes('.') + ? /( \((\d+)\))?(\.[^$]+)?$/ + : /( \((\d+)\))$/ const suffixMatches = terminus.match(regex) return Path.replaceTerminus( @@ -26,10 +26,14 @@ export function addOrIncreaseNameNumber( ) } +/** + * + * @param path + */ export function pathSegmentsWithoutPartition( - path: Path.Distinctive> + path: Path.Partitioned ): Path.Segments { - return Path.unwrap(Path.removePartition(path)) + return Path.removePartition(path) } /** diff --git a/packages/nest/src/data/sample.ts b/packages/nest/src/data/sample.ts index 8e56386..25dc105 100644 --- a/packages/nest/src/data/sample.ts +++ b/packages/nest/src/data/sample.ts @@ -1,19 +1,20 @@ -import * as Path from '../path.js' import type { FileSystem } from '../class.js' /** * Adds some sample to the file system. + * + * @param fs */ export async function addSampleData(fs: FileSystem): Promise { - await fs.mkdir(Path.directory('private', 'Apps')) - await fs.mkdir(Path.directory('private', 'Audio')) - await fs.mkdir(Path.directory('private', 'Documents')) - await fs.mkdir(Path.directory('private', 'Photos')) - await fs.mkdir(Path.directory('private', 'Video')) + await fs.mkdir(['private', 'Apps']) + await fs.mkdir(['private', 'Audio']) + await fs.mkdir(['private', 'Documents']) + await fs.mkdir(['private', 'Photos']) + await fs.mkdir(['private', 'Video']) // Files await fs.write( - Path.file('private', 'Welcome.txt'), + ['private', 'Welcome.txt'], 'utf8', 'Welcome to your personal transportable encrypted file system 👋' ) diff --git a/packages/nest/src/errors.ts b/packages/nest/src/errors.ts index f2281da..7da9929 100644 --- a/packages/nest/src/errors.ts +++ b/packages/nest/src/errors.ts @@ -1,9 +1,11 @@ import * as Path from './path.js' -export function throwNoAccess( - path: Path.DistinctivePath, - accessType?: string -): never { +/** + * + * @param path + * @param accessType + */ +export function throwNoAccess(path: Path.Segments, accessType?: string): never { throw new Error( `Expected to have ${ typeof accessType === 'string' ? accessType + ' ' : '' @@ -11,9 +13,11 @@ export function throwNoAccess( ) } -export function throwInvalidPartition( - path: Path.Distinctive -): never { +/** + * + * @param path + */ +export function throwInvalidPartition(path: Path.Segments): never { throw new Error( `Expected either a public or private path, got '${Path.toPosix(path)}'` ) diff --git a/packages/nest/src/mounts.ts b/packages/nest/src/mounts.ts index 4a7abe0..1606f10 100644 --- a/packages/nest/src/mounts.ts +++ b/packages/nest/src/mounts.ts @@ -19,21 +19,18 @@ import { throwInvalidPartition, throwNoAccess } from './errors.js' * * Starts from the path `/` and works up to given path, * which could be a file or directory path. + * + * @param path + * @param privateNodes */ export function findPrivateNode( - path: Path.Distinctive>, + path: Partitioned, privateNodes: MountedPrivateNodes ): PrivateNodeQueryResult { - const pathKind = Path.kind(path) - const pathWithoutPartition = Path.removePartition(path) - const pathSegments = Path.unwrap(pathWithoutPartition) + const pathSegments = Path.removePartition(path) for (let i = 0; i <= pathSegments.length; i++) { - const path = Path.fromKind( - i === pathSegments.length ? pathKind : Path.Kind.Directory, - ...pathSegments.slice(0, i) - ) - + const path = pathSegments.slice(0, i) const result: MountedPrivateNode | undefined = privateNodes[Path.toPosix(path, { absolute: true })] @@ -49,25 +46,28 @@ export function findPrivateNode( } export function partition

( - path: Path.Distinctive> + path: PartitionedNonEmpty

): PartitionDiscoveryNonEmpty

export function partition

( - path: Path.Distinctive> + path: Partitioned

): PartitionDiscovery

-export function partition(path: Path.Distinctive>): { +/** + * + * @param path + */ +export function partition(path: Partitioned): { name: 'public' | 'private' - path: Path.Distinctive> + path: Partitioned segments: Path.Segments } { - const unwrapped = Path.unwrap(path) - const rest = unwrapped.slice(1) + const rest = path.slice(1) - switch (unwrapped[0]) { + switch (path[0]) { case 'public': { - return { name: 'public', path: path, segments: rest } + return { name: 'public', path, segments: rest } } case 'private': { - return { name: 'private', path: path, segments: rest } + return { name: 'private', path, segments: rest } } default: { throwInvalidPartition(path) diff --git a/packages/nest/src/path.ts b/packages/nest/src/path.ts index 0666587..c4a72ba 100644 --- a/packages/nest/src/path.ts +++ b/packages/nest/src/path.ts @@ -1,7 +1,5 @@ // 🧩 -import type { AppInfo } from './app-info.js' - export enum RootBranch { DID = 'did', Exchange = 'exchange', @@ -38,205 +36,43 @@ export type Public = 'public' | RootBranch.Public */ export type Partition = Private | Public +export function priv(...args: SegmentsNonEmpty): PartitionedNonEmpty +export function priv(...args: Segments): Partitioned /** - * A directory path. - */ -export interface DirectoryPath

{ - directory: P -} - -/** - * A file path. - */ -export interface FilePath

{ - file: P -} - -/** - * A file or directory path. - */ -export type DistinctivePath

= DirectoryPath

| FilePath

- -/** - * Alias for `DirectoryPath` - */ -export type Directory

= DirectoryPath

- -/** - * Alias for `FilePath` - */ -export type File

= FilePath

- -/** - * Alias for `DistinctivePath` - */ -export type Distinctive

= DistinctivePath

- -// CREATION - -/** - * Utility function to create a `DirectoryPath` + * Utility function to create a private path. * * @param args * @group 🪺 :: START HERE */ -export function directory

( - ...args: PartitionedNonEmpty

-): DirectoryPath> -export function directory

( - ...args: Partitioned

-): DirectoryPath> -export function directory( - ...args: SegmentsNonEmpty -): DirectoryPath -export function directory(...args: Segments): DirectoryPath -/** - * - * @param args - */ -export function directory(...args: Segments): DirectoryPath { - if (args.some((p) => p.includes('/'))) { - throw new Error('Forward slashes `/` are not allowed') - } - return { directory: args } +export function priv(...args: Segments): Partitioned { + return ['private', ...args] } +export function pub(...args: SegmentsNonEmpty): PartitionedNonEmpty +export function pub(...args: Segments): Partitioned /** - * Utility function to create a `FilePath` + * Utility function to create a public path. * * @param args * @group 🪺 :: START HERE */ -export function file

( - ...args: PartitionedNonEmpty

-): FilePath> -export function file(...args: SegmentsNonEmpty): FilePath -export function file(...args: Segments): FilePath -/** - * - * @param args - */ -export function file(...args: Segments): FilePath { - if (args.some((p) => p.includes('/'))) { - throw new Error('Forward slashes `/` are not allowed') - } - return { file: args } +export function pub(...args: Segments): Partitioned { + return ['public', ...args] } /** - * Utility function to create a path based on the given `Kind` + * Utility function to create a root path (aka. an empty array) * - * @param kind - * @param args * @group 🪺 :: START HERE */ -export function fromKind

( - kind: Kind.Directory, - ...args: PartitionedNonEmpty

-): DirectoryPath> -export function fromKind

( - kind: Kind.Directory, - ...args: Partitioned

-): DirectoryPath> -export function fromKind( - kind: Kind.Directory, - ...args: SegmentsNonEmpty -): DirectoryPath -export function fromKind( - kind: Kind.Directory, - ...args: Segments -): DirectoryPath -export function fromKind

( - kind: Kind.File, - ...args: PartitionedNonEmpty

-): FilePath> -export function fromKind( - kind: Kind.File, - ...args: SegmentsNonEmpty -): FilePath -export function fromKind(kind: Kind.File, ...args: Segments): FilePath -export function fromKind

( - kind: Kind, - ...args: PartitionedNonEmpty

-): DistinctivePath> -export function fromKind

( - kind: Kind, - ...args: Partitioned

-): DistinctivePath> -export function fromKind( - kind: Kind, - ...args: SegmentsNonEmpty -): DistinctivePath -export function fromKind( - kind: Kind, - ...args: Segments -): DistinctivePath -/** - * - * @param kind - * @param args - */ -export function fromKind( - kind: Kind, - ...args: Segments -): DistinctivePath { - return kind === Kind.Directory ? directory(...args) : file(...args) -} - -/** - * Utility function to create a root `DirectoryPath` - * - * @group 🪺 :: START HERE - */ -export function root(): DirectoryPath { - return { directory: [] } -} - -/** - * Utility function create an app data path. - * - * @param partition - * @param app - * @group 🪺 :: START HERE - */ -export function appData

( - partition: P, - app: AppInfo -): DirectoryPath> -export function appData

( - partition: P, - app: AppInfo, - suffix: FilePath -): FilePath> -export function appData

( - partition: P, - app: AppInfo, - suffix: DirectoryPath -): DirectoryPath> -export function appData

( - partition: P, - app: AppInfo, - suffix: DistinctivePath -): DistinctivePath> -/** - * - * @param partition - * @param app - * @param suffix - */ -export function appData

( - partition: P, - app: AppInfo, - suffix?: DistinctivePath -): DistinctivePath> { - const appDir = directory(partition, 'Apps', app.creator, app.name) - return suffix === undefined ? appDir : combine(appDir, suffix) +export function root(): [] { + return [] } // POSIX /** - * Transform a string into a `DistinctivePath`. + * Transform a POSIX string into a path. * * Directories should have the format `path/to/dir/` and * files should have the format `path/to/file`. @@ -246,15 +82,15 @@ export function appData

( * @param path * @group POSIX */ -export function fromPosix(path: string): DistinctivePath { +export function fromPosix(path: string): Segments { const split = path.replace(/^\/+/, '').split('/') - if (path.endsWith('/')) return { directory: split.slice(0, -1) } + if (path.endsWith('/')) return split.slice(0, -1) else if (path === '') return root() - return { file: split } + return split } /** - * Transform a `DistinctivePath` into a string. + * Transform a path into a POSIX string. * * Directories will have the format `path/to/dir/` and * files will have the format `path/to/file`. @@ -262,326 +98,160 @@ export function fromPosix(path: string): DistinctivePath { * @param path * @param options * @param options.absolute + * @param options.directory * @group POSIX */ export function toPosix( - path: DistinctivePath, - options?: { absolute?: boolean } + path: Segments, + options?: { absolute?: boolean; directory?: boolean } ): string { const prefix = options?.absolute === true ? '/' : '' - const joinedPath = unwrap(path).join('/') - if (isDirectory(path)) + const joinedPath = path.join('/') + if (options?.directory === true) return prefix + joinedPath + (joinedPath.length > 0 ? '/' : '') return prefix + joinedPath } // 🛠️ -/** - * Combine two `DistinctivePath`s. - * - * @param a - * @param b - */ -export function combine

( - a: DirectoryPath>, - b: FilePath -): FilePath> -export function combine

( - a: DirectoryPath>, - b: FilePath -): FilePath> -export function combine

( - a: DirectoryPath>, - b: FilePath -): FilePath> -export function combine( - a: DirectoryPath, - b: FilePath -): FilePath -export function combine( - a: DirectoryPath, - b: FilePath -): FilePath -export function combine

( - a: DirectoryPath>, - b: DirectoryPath -): DirectoryPath> -export function combine

( - a: DirectoryPath>, - b: DirectoryPath -): DirectoryPath> export function combine

( - a: DirectoryPath>, - b: DirectoryPath -): DirectoryPath> -export function combine( - a: DirectoryPath, - b: DirectoryPath -): DirectoryPath -export function combine( - a: DirectoryPath, - b: DirectoryPath -): DirectoryPath + a: PartitionedNonEmpty

, + b: Segments +): PartitionedNonEmpty

export function combine

( - a: DirectoryPath>, - b: DistinctivePath -): DistinctivePath> + a: Partitioned

, + b: SegmentsNonEmpty +): PartitionedNonEmpty

export function combine

( - a: DirectoryPath>, - b: DistinctivePath -): DistinctivePath> -export function combine

( - a: DirectoryPath>, - b: DistinctivePath -): DistinctivePath> -export function combine( - a: DirectoryPath, - b: DistinctivePath -): DistinctivePath -export function combine( - a: DirectoryPath, - b: DistinctivePath -): DistinctivePath + a: Partitioned

, + b: Segments +): Partitioned

+export function combine(a: Segments, b: SegmentsNonEmpty): SegmentsNonEmpty +export function combine(a: Segments, b: Segments): Segments /** + * Combine two `DistinctivePath`s. * * @param a * @param b */ -export function combine( - a: DirectoryPath, - b: DistinctivePath -): DistinctivePath { - return map((p) => [...unwrap(a), ...p], b) -} - -/** - * Is this `DistinctivePath` a directory? - * - * @param path - */ -export function isDirectory

( - path: DistinctivePath

-): path is DirectoryPath

{ - return 'directory' in path -} - -/** - * Is this `DistinctivePath` a file? - * - * @param path - */ -export function isFile

(path: DistinctivePath

): path is FilePath

{ - return 'file' in path +export function combine(a: Segments, b: Segments): Segments { + return [...a, ...b] } /** - * Is this `DistinctivePath` on the given `RootBranch`? + * Is this path on the given `RootBranch`? * * @param rootBranch * @param path */ export function isOnRootBranch( rootBranch: RootBranch, - path: DistinctivePath + path: Segments ): boolean { - return unwrap(path)[0] === rootBranch + return path[0] === rootBranch } /** - * Is this `DistinctivePath` of the given `Partition`? + * Is this path of the given `Partition`? * * @param partition * @param path */ -export function isPartition( - partition: Partition, - path: DistinctivePath -): boolean { - return unwrap(path)[0] === partition +export function isPartition(partition: Partition, path: Segments): boolean { + return path[0] === partition } /** - * Is this a partitioned `DistinctivePath`? + * Is this a partitioned path? * * @param path */ export function isPartitioned

( - path: DistinctivePath -): path is DistinctivePath> { - const soCalledPartition = unwrap(path)[0] + path: Segments +): path is Partitioned

{ + const soCalledPartition = path[0] return [RootBranch.Private, RootBranch.Public, 'private', 'public'].includes( soCalledPartition ) } /** - * Is this partitioned `DistinctivePath` non-empty? + * Is this partitioned path non-empty? * * @param path */ export function isPartitionedNonEmpty

( - path: DistinctivePath -): path is DistinctivePath> { - return isPartitioned(path) && length(path) > 1 + path: Segments +): path is PartitionedNonEmpty

{ + return isPartitioned(path) && path.length > 1 } /** - * Is this `DirectoryPath` a root directory? + * Is this path a root path? * * @param path */ -export function isRootDirectory(path: DirectoryPath): boolean { - return path.directory.length === 0 -} - -/** - * Check if two `DistinctivePath` have the same `Partition`. - * - * @param a - * @param b - */ -export function isSamePartition( - a: DistinctivePath, - b: DistinctivePath -): boolean { - return unwrap(a)[0] === unwrap(b)[0] +export function isRoot(path: Segments): boolean { + return path.length === 0 } /** - * Check if two `DistinctivePath` are of the same kind. + * Check if two paths have the same `Partition`. * * @param a * @param b */ -export function isSameKind( - a: DistinctivePath, - b: DistinctivePath -): boolean { - if (isDirectory(a) && isDirectory(b)) return true - else if (isFile(a) && isFile(b)) return true - else return false -} - -/** - * What `Kind` of path are we dealing with? - * - * @param path - */ -export function kind

(path: DistinctivePath

): Kind { - if (isDirectory(path)) return Kind.Directory - return Kind.File -} - -/** - * What's the length of a path? - * - * @param path - */ -export function length(path: DistinctivePath): number { - return unwrap(path).length -} - -/** - * Map a `DistinctivePath`. - * - * @param fn - * @param path - */ -export function map( - fn: (p: A) => B, - path: DistinctivePath -): DistinctivePath { - if (isDirectory(path)) return { directory: fn(path.directory) } - else if (isFile(path)) return { file: fn(path.file) } - return path +export function isSamePartition(a: Segments, b: Segments): boolean { + return a[0] === b[0] } -/** - * Get the parent directory of a `DistinctivePath`. - * - * @param path - */ -export function parent( - path: DistinctivePath<[Partition, Segment, Segment, ...Segments]> -): DirectoryPath> -export function parent( - path: DistinctivePath<[Segment, Segment, Segment, ...Segments]> -): DirectoryPath -export function parent( - path: DistinctivePath> -): DirectoryPath> export function parent( - path: DistinctivePath<[Partition, Segment]> -): DirectoryPath> + path: [Partition, Segment, Segment, ...Segments] +): PartitionedNonEmpty export function parent( - path: DistinctivePath> -): DirectoryPath + path: [Segment, Segment, Segment, ...Segments] +): SegmentsNonEmpty export function parent( - path: DistinctivePath -): DirectoryPath -export function parent(path: DistinctivePath<[Segment]>): DirectoryPath<[]> -export function parent(path: DistinctivePath<[]>): undefined -export function parent( - path: DistinctivePath -): DirectoryPath | undefined + path: PartitionedNonEmpty +): Partitioned +export function parent(path: [Partition, Segment]): Partitioned +export function parent(path: Partitioned): Segments +export function parent(path: SegmentsNonEmpty): Segments +export function parent(path: [Segment]): [] +export function parent(path: []): undefined +export function parent(path: Segments): Segments | undefined /** + * Get the parent directory of a path. * * @param path */ -export function parent( - path: DistinctivePath -): DirectoryPath | undefined { - return isDirectory(path) && isRootDirectory(path) - ? undefined - : directory(...unwrap(path).slice(0, -1)) +export function parent(path: Segments): Segments | undefined { + return path.slice(0, -1) } /** - * Remove the `Partition` of a `DistinctivePath` (ie. the top-level directory) + * Remove the `Partition` of a path (ie. the top-level directory) * * @param path */ -export function removePartition( - path: DistinctivePath -): DistinctivePath { - return map((p) => (isDirectory(path) || p.length > 1 ? p.slice(1) : p), path) +export function removePartition(path: Segments): Segments { + return isPartitioned(path) ? path.slice(1) : path } export function replaceTerminus( - path: FilePath>, - terminus: string -): FilePath> -export function replaceTerminus( - path: DirectoryPath>, - terminus: string -): DirectoryPath> -export function replaceTerminus( - path: DistinctivePath>, - terminus: string -): DistinctivePath> -export function replaceTerminus( - path: FilePath, - terminus: string -): FilePath -export function replaceTerminus( - path: DirectoryPath, + path: PartitionedNonEmpty, terminus: string -): DirectoryPath -export function replaceTerminus( - path: DistinctivePath, - terminus: string -): DistinctivePath +): PartitionedNonEmpty /** * * @param path * @param terminus */ export function replaceTerminus( - path: DistinctivePath | DistinctivePath, + path: SegmentsNonEmpty, terminus: string -): DistinctivePath { - return combine(parent(path), fromKind(kind(path), terminus)) +): SegmentsNonEmpty { + return combine(parent(path), [terminus]) } /** @@ -589,11 +259,10 @@ export function replaceTerminus( * @param path */ export function rootBranch( - path: DistinctivePath + path: Segments ): { branch: RootBranch; rest: Segments } | undefined { - const unwrapped = unwrap(path) - const firstSegment = unwrapped[0] - const rest = unwrapped.slice(1) + const firstSegment = path[0] + const rest = path.slice(1) switch (firstSegment) { case RootBranch.Exchange: { @@ -622,82 +291,39 @@ export function rootBranch( } } +export function terminus(path: PartitionedNonEmpty): string +export function terminus(path: Partitioned): string +export function terminus(path: SegmentsNonEmpty): string +export function terminus(path: Segments): string | undefined /** * Get the last part of the path. * * @param path */ -export function terminus( - path: DistinctivePath> -): string -export function terminus(path: DistinctivePath>): string -export function terminus(path: DistinctivePath): string -export function terminus(path: DistinctivePath): string | undefined -/** - * - * @param path - */ -export function terminus(path: DistinctivePath): string | undefined { - const u = unwrap(path) - if (u.length === 0) return undefined - return u.at(-1) +export function terminus(path: Segments): string | undefined { + if (path.length === 0) return undefined + return path.at(-1) } -/** - * Unwrap a `DistinctivePath`. - * - * @param path - */ -export function unwrap

(path: DistinctivePath

): P { - if (isDirectory(path)) { - return path.directory - } else if (isFile(path)) { - return path.file - } - - throw new Error('Path is neither a directory or a file') -} - -/** - * Utility function to prefix a path with a `Partition`. - * - * @param partition - * @param path - */ export function withPartition

( partition: P, - path: DirectoryPath -): DirectoryPath> + path: SegmentsNonEmpty +): PartitionedNonEmpty

export function withPartition

( partition: P, - path: DirectoryPath -): DirectoryPath> -export function withPartition

( - partition: P, - path: FilePath -): FilePath> -export function withPartition

( - partition: P, - path: FilePath -): FilePath> -export function withPartition

( - partition: P, - path: DistinctivePath -): DistinctivePath> -export function withPartition

( - partition: P, - path: DistinctivePath -): DistinctivePath> + path: Segments +): Partitioned

/** + * Utility function to prefix a path with a `Partition`. * * @param partition * @param path */ export function withPartition

( partition: P, - path: DistinctivePath -): DistinctivePath> { - return combine(directory(partition), path) + path: Segments +): Partitioned

{ + return [partition, ...path] } // 🔬 diff --git a/packages/nest/src/queries.ts b/packages/nest/src/queries.ts index 4d1b069..2844819 100644 --- a/packages/nest/src/queries.ts +++ b/packages/nest/src/queries.ts @@ -37,14 +37,20 @@ export interface PublicParams { export type Public = (params: PublicParams) => Promise export type PublicContext = Omit +/** + * + * @param path + * @param qry + * @param context + */ export async function publicQuery( - path: Path.Distinctive>, + path: Partitioned, qry: Public, context: PublicContext ): Promise { return await qry({ blockstore: context.blockstore, - pathSegments: Path.unwrap(Path.removePartition(path)), + pathSegments: Path.removePartition(path), rootTree: context.rootTree, }) } @@ -59,6 +65,21 @@ export const publicExists = () => { } } +export const publicItemKind = () => { + return async (params: PublicParams): Promise => { + const result = await params.rootTree + .publicRoot() + .getNode(params.pathSegments, Store.wnfs(params.blockstore)) + + if (result !== null && result !== undefined) + return (result as PublicNode).isFile() + ? Path.Kind.File + : Path.Kind.Directory + + return undefined + } +} + export const publicListDirectory = () => { return async (params: PublicParams): Promise => { return await params.rootTree @@ -92,9 +113,9 @@ export const publicListDirectoryWithKind = () => { return { ...item, kind, - path: Path.combine( - Path.directory('public', ...params.pathSegments), - Path.fromKind(kind, item.name) + path: Path.withPartition( + 'public', + Path.combine(params.pathSegments, [item.name]) ), } }) @@ -166,8 +187,14 @@ export type PrivateParams = { export type Private = (params: PrivateParams) => Promise export type PrivateContext = Omit +/** + * + * @param path + * @param qry + * @param context + */ export async function privateQuery( - path: Path.Distinctive>, + path: Partitioned, qry: Private, context: PrivateContext ): Promise { @@ -200,6 +227,28 @@ export const privateExists = () => { } } +export const privateItemKind = () => { + return async (params: PrivateParams): Promise => { + if (params.node.isFile()) return Path.Kind.File + + const result = await params.node + .asDir() + .getNode( + params.remainder, + searchLatest(), + params.rootTree.privateForest(), + Store.wnfs(params.blockstore) + ) + + if (result !== null && result !== undefined) + return (result as PrivateNode).isFile() + ? Path.Kind.File + : Path.Kind.Directory + + return undefined + } +} + export const privateListDirectory = () => { return async (params: PrivateParams): Promise => { if (params.node.isFile()) throw new Error('Cannot list a file') @@ -240,14 +289,10 @@ export const privateListDirectoryWithKind = () => { ) .then((a) => a.result) - const parentPath = Path.combine( - Path.directory('private', ...Path.unwrap(params.path)), - Path.directory(...params.remainder) - ) - - if (!Path.isDirectory(parentPath)) { - throw new Error("Didn't expect a file path") - } + const parentPath = Path.withPartition('private', [ + ...params.path, + ...params.remainder, + ]) const promises = items.map( async (item: DirectoryItem): Promise => { @@ -263,7 +308,7 @@ export const privateListDirectoryWithKind = () => { return { ...item, kind, - path: Path.combine(parentPath, Path.fromKind(kind, item.name)), + path: Path.combine(parentPath, [item.name]), } } ) @@ -327,7 +372,6 @@ export const privateReadFromAccessKey = ( if (node.isFile() === true) { const file: PrivateFile = node.asFile() - // TODO: Respect the offset and length options when available in rs-wnfs return await file.readAt( options?.offset ?? 0, options?.length ?? undefined, diff --git a/packages/nest/src/references.ts b/packages/nest/src/references.ts index ed3c436..c497281 100644 --- a/packages/nest/src/references.ts +++ b/packages/nest/src/references.ts @@ -9,10 +9,16 @@ import type { RootTree } from './root-tree.js' import * as Store from './store.js' import { pathSegmentsWithoutPartition } from './common.js' +/** + * + * @param blockstore + * @param rootTree + * @param path + */ export async function contentCID( blockstore: Blockstore, rootTree: RootTree, - path: Path.File> + path: Path.Partitioned ): Promise { const wnfsBlockstore = Store.wnfs(blockstore) const result = await rootTree @@ -30,10 +36,16 @@ export async function contentCID( : undefined } +/** + * + * @param blockstore + * @param rootTree + * @param path + */ export async function capsuleCID( blockstore: Blockstore, rootTree: RootTree, - path: Path.Distinctive> + path: Path.Partitioned ): Promise { const wnfsBlockstore = Store.wnfs(blockstore) const result = await rootTree diff --git a/packages/nest/src/root-tree/basic.ts b/packages/nest/src/root-tree/basic.ts index 154e8e2..93eb2df 100644 --- a/packages/nest/src/root-tree/basic.ts +++ b/packages/nest/src/root-tree/basic.ts @@ -7,7 +7,7 @@ import * as Raw from 'multiformats/codecs/raw' import * as Uint8Arrays from 'uint8arrays' import { CID } from 'multiformats/cid' -import { PrivateForest, PublicDirectory } from 'wnfs' +import { PrivateForest, PublicDirectory, type PublicNode } from 'wnfs' import * as Path from '../path.js' import * as References from '../references.js' @@ -275,8 +275,14 @@ export class BasicRootTree implements RootTree { return await Unix.removeNodeFromTree(oldRoot, path, this.#blockstore) } + const node: PublicNode = await dir.getNode( + path, + Store.wnfs(this.#blockstore) + ) + const itemKind = node.isFile() ? Path.Kind.File : Path.Kind.Directory + const contentCID = - Path.isFile(mod.path) && + itemKind === Path.Kind.File && Path.isPartitionedNonEmpty(mod.path) ? await References.contentCID( this.#blockstore, @@ -286,6 +292,7 @@ export class BasicRootTree implements RootTree { : undefined return await Unix.insertNodeIntoTree( + itemKind, oldRoot, path, this.#blockstore, diff --git a/packages/nest/src/share.ts b/packages/nest/src/share.ts index 64e0274..8286e39 100644 --- a/packages/nest/src/share.ts +++ b/packages/nest/src/share.ts @@ -67,27 +67,27 @@ export class Share { * @param path * @group Querying */ - async exists(path?: Path.Distinctive): Promise { + async exists(path?: Segments): Promise { return await this.#query(path ?? Path.root(), Queries.privateExists()) } /** @group Querying */ async listDirectory( - path: Path.Directory, + path: Segments, listOptions: { withItemKind: true } ): Promise async listDirectory( - path: Path.Directory, + path: Segments, listOptions: { withItemKind: false } ): Promise async listDirectory(): Promise - async listDirectory(path: Path.Directory): Promise + async listDirectory(path: Segments): Promise async listDirectory( - path?: Path.Directory, + path?: Segments, listOptions?: { withItemKind: boolean } ): Promise async listDirectory( - path?: Path.Directory, + path?: Segments, listOptions?: { withItemKind: boolean } ): Promise { return await this.#query( @@ -104,12 +104,12 @@ export class Share { /** @group Querying */ async read( dataType: D, - path?: Path.File, + path?: Segments, options?: { offset?: number; length?: number } ): Promise> async read( dataType: DataType, - path?: Path.File, + path?: Segments, options?: { offset?: number; length?: number } ): Promise> { const bytes = await this.#query( @@ -123,16 +123,13 @@ export class Share { * @param path * @group Querying */ - async size(path?: Path.File): Promise { + async size(path?: Segments): Promise { return await this.#query(path ?? Path.root(), Queries.privateSize()) } // ㊙️ - async #query( - path: Path.Distinctive, - query: Queries.Private - ): Promise { + async #query(path: Segments, query: Queries.Private): Promise { return await Queries.privateQuery( Path.withPartition('private', path), query, diff --git a/packages/nest/src/transaction.ts b/packages/nest/src/transaction.ts index 4aa39c3..8213192 100644 --- a/packages/nest/src/transaction.ts +++ b/packages/nest/src/transaction.ts @@ -59,7 +59,7 @@ export class TransactionContext { readonly #modifications: Set<{ type: MutationType - path: Path.Distinctive> + path: Partitioned }> /** @@ -93,7 +93,7 @@ export class TransactionContext { static async commit(context: TransactionContext): Promise< | { modifications: Array<{ - path: Path.Distinctive> + path: Partitioned type: MutationType }> privateNodes: MountedPrivateNodes @@ -117,7 +117,7 @@ export class TransactionContext { } const maybeNode = findPrivateNode( - mod.path as Path.Distinctive>, + mod.path as Path.Partitioned, context.#privateNodes ) @@ -151,9 +151,7 @@ export class TransactionContext { * @param path * @group Querying */ - async contentCID( - path: Path.File> - ): Promise { + async contentCID(path: Partitioned): Promise { return await References.contentCID(this.#blockstore, this.#rootTree, path) } @@ -161,9 +159,7 @@ export class TransactionContext { * @param path * @group Querying */ - async capsuleCID( - path: Path.Distinctive> - ): Promise { + async capsuleCID(path: Partitioned): Promise { return await References.capsuleCID(this.#blockstore, this.#rootTree, path) } @@ -172,7 +168,7 @@ export class TransactionContext { * @group Querying */ async capsuleKey( - path: Path.Distinctive> + path: Partitioned ): Promise { let priv: PrivateNodeQueryResult @@ -219,9 +215,7 @@ export class TransactionContext { * @param path * @group Querying */ - async exists( - path: Path.Distinctive> - ): Promise { + async exists(path: Partitioned): Promise { return await this.#query(path, { public: Queries.publicExists(), private: Queries.privateExists(), @@ -230,22 +224,20 @@ export class TransactionContext { /** @group Querying */ async listDirectory( - path: Path.Directory>, + path: Partitioned, listOptions: { withItemKind: true } ): Promise async listDirectory( - path: Path.Directory>, + path: Partitioned, listOptions: { withItemKind: false } ): Promise + async listDirectory(path: Partitioned): Promise async listDirectory( - path: Path.Directory> - ): Promise - async listDirectory( - path: Path.Directory>, + path: Partitioned, listOptions?: { withItemKind: boolean } ): Promise async listDirectory( - path: Path.Directory>, + path: Partitioned, listOptions?: { withItemKind: boolean } ): Promise { if (listOptions?.withItemKind === true) { @@ -267,7 +259,7 @@ export class TransactionContext { /** @group Querying */ async read( arg: - | Path.File> + | PartitionedNonEmpty | { contentCID: CID } | { capsuleCID: CID } | { @@ -278,7 +270,7 @@ export class TransactionContext { ): Promise> async read( arg: - | Path.File> + | PartitionedNonEmpty | { contentCID: CID } | { capsuleCID: CID } | { @@ -321,7 +313,7 @@ export class TransactionContext { AccessKey.fromBytes(arg.capsuleKey), options )(this.#privateContext()) - } else if ('file' in arg || 'directory' in arg) { + } else if (Array.isArray(arg)) { // Public or private from path bytes = await this.#query(arg, { public: Queries.publicRead(options), @@ -329,7 +321,7 @@ export class TransactionContext { }) } else { // ⚠️ - throw new Error('Invalid argument') + throw new TypeError('Invalid argument') } return dataFromBytes(dataType, bytes) @@ -339,7 +331,7 @@ export class TransactionContext { * @param path * @group Querying */ - async size(path: Path.File>): Promise { + async size(path: PartitionedNonEmpty): Promise { return await this.#query(path, { public: Queries.publicSize(), private: Queries.privateSize(), @@ -354,23 +346,21 @@ export class TransactionContext { * @group Mutating */ async copy( - fromParam: Path.Distinctive>, - toParam: - | Path.File> - | Path.Directory> + fromParam: PartitionedNonEmpty, + toParam: PartitionedNonEmpty | Partitioned ): Promise { const from = fromParam - let to = toParam + const to = toParam - if (Path.isDirectory(fromParam) && Path.isFile(toParam)) - throw new Error('Cannot copy a directory to a file') - if (Path.isFile(fromParam) && Path.isDirectory(toParam)) - to = Path.combine(toParam, Path.file(Path.terminus(from))) + const kind = await this.#query(from, { + public: Queries.publicItemKind(), + private: Queries.privateItemKind(), + }) - if (Path.isFile(from) && Path.isFile(to)) { + if (kind === Path.Kind.File && Path.isPartitionedNonEmpty(to)) { await this.#manualCopyFile(from, to) return - } else if (Path.isDirectory(from) && Path.isDirectory(to)) { + } else if (kind === Path.Kind.Directory) { await this.#manualCopyDirectory(from, to) return } @@ -389,8 +379,8 @@ export class TransactionContext { * @group Mutating */ async createDirectory( - path: Path.Directory> - ): Promise<{ path: Path.Directory> }> { + path: PartitionedNonEmpty + ): Promise<{ path: PartitionedNonEmpty }> { if (await this.exists(path)) { const newPath = addOrIncreaseNameNumber(path) return await this.createDirectory(newPath) @@ -407,10 +397,10 @@ export class TransactionContext { * @group Mutating */ async createFile( - path: Path.File>, + path: PartitionedNonEmpty, dataType: DataType, data: DataForType - ): Promise<{ path: Path.File> }> { + ): Promise<{ path: PartitionedNonEmpty }> { if (await this.exists(path)) { const newPath = addOrIncreaseNameNumber(path) return await this.createFile(newPath, dataType, data) @@ -424,9 +414,7 @@ export class TransactionContext { * @param path * @group Mutating */ - async ensureDirectory( - path: Path.Directory> - ): Promise { + async ensureDirectory(path: PartitionedNonEmpty): Promise { const partition = determinePartition(path) switch (partition.name) { @@ -459,18 +447,11 @@ export class TransactionContext { * @group Mutating */ async move( - fromParam: Path.Distinctive>, - toParam: - | Path.File> - | Path.Directory> + fromParam: PartitionedNonEmpty, + toParam: PartitionedNonEmpty | Partitioned ): Promise { const from = fromParam - let to = toParam - - if (Path.isDirectory(fromParam) && Path.isFile(toParam)) - throw new Error('Cannot move a directory to a file') - if (Path.isFile(fromParam) && Path.isDirectory(toParam)) - to = Path.combine(toParam, Path.file(Path.terminus(from))) + const to = toParam await this.#manualMove(from, to) } @@ -482,9 +463,7 @@ export class TransactionContext { * @param path * @group Mutating */ - async remove( - path: Path.Distinctive> - ): Promise { + async remove(path: PartitionedNonEmpty): Promise { const partition = determinePartition(path) switch (partition.name) { @@ -517,7 +496,7 @@ export class TransactionContext { * @group Mutating */ async rename( - path: Path.Distinctive>, + path: PartitionedNonEmpty, newName: string ): Promise { const fromPath = path @@ -533,7 +512,7 @@ export class TransactionContext { * @group Mutating */ async write( - path: Path.File>, + path: PartitionedNonEmpty, dataType: DataType, data: DataForType ): Promise { @@ -651,14 +630,10 @@ export class TransactionContext { ) // Create share context - const path = Path.fromKind( - sharedNode.isFile() ? Path.Kind.File : Path.Kind.Directory - ) - return new Share( shareId, sharerBlockstore, - { [Path.toPosix(path, { absolute: true })]: { path, node: sharedNode } }, + { '/': { path: Path.root(), node: sharedNode } }, this.#rng, sharerRootTree ) @@ -712,7 +687,7 @@ export class TransactionContext { * @group Sharing */ async share( - path: Path.Distinctive>, + path: Partitioned, receiverDataRoot: CID, opts: { receiverBlockstore?: Blockstore @@ -798,7 +773,7 @@ export class TransactionContext { // ㊙️ ▒▒ QUERIES async #query( - path: Path.Distinctive>, + path: Partitioned, queryFunctions: { public: Queries.Public private: Queries.Private @@ -828,15 +803,15 @@ export class TransactionContext { // ㊙️ ▒▒ MUTATIONS async #manualCopyFile( - from: Path.File>, - to: Path.File> + from: PartitionedNonEmpty, + to: PartitionedNonEmpty ): Promise { await this.write(to, 'bytes', await this.read(from, 'bytes')) } async #manualCopyDirectory( - from: Path.Directory>, - to: Path.Directory> + from: PartitionedNonEmpty, + to: Partitioned ): Promise { if (Path.isPartitionedNonEmpty(to)) await this.ensureDirectory(to) @@ -854,12 +829,12 @@ export class TransactionContext { item.kind === 'directory' ? await this.#manualCopyDirectory( - Path.combine(from, Path.directory(item.name)), - Path.combine(to, Path.directory(item.name)) + Path.combine(from, [item.name]), + Path.combine(to, [item.name]) ) : await this.#manualCopyFile( - Path.combine(from, Path.file(item.name)), - Path.combine(to, Path.file(item.name)) + Path.combine(from, [item.name]), + Path.combine(to, [item.name]) ) }, Promise.resolve() @@ -867,17 +842,15 @@ export class TransactionContext { } async #manualMove( - from: Path.Distinctive>, - to: - | Path.File> - | Path.Directory> + from: PartitionedNonEmpty, + to: PartitionedNonEmpty | Partitioned ): Promise { await this.copy(from, to) await this.remove(from) } async #publicMutation( - path: Path.Distinctive>, + path: Partitioned, mut: Mutations.Public, mutType: MutationType ): Promise { @@ -888,7 +861,7 @@ export class TransactionContext { const result = await mut({ blockstore: this.#blockstore, - pathSegments: Path.unwrap(Path.removePartition(path)), + pathSegments: Path.removePartition(path), rootTree: this.#rootTree, }) @@ -902,7 +875,7 @@ export class TransactionContext { } async #privateMutation( - path: Path.Distinctive>, + path: Partitioned, mut: Mutations.Private, mutType: MutationType ): Promise { diff --git a/packages/nest/src/types.ts b/packages/nest/src/types.ts index 6e89963..4314e7b 100644 --- a/packages/nest/src/types.ts +++ b/packages/nest/src/types.ts @@ -27,11 +27,11 @@ export interface DirectoryItem { export type DirectoryItemWithKind = DirectoryItem & { kind: Path.Kind - path: Path.Distinctive> + path: Path.PartitionedNonEmpty } export interface Modification { - path: Path.Distinctive> + path: Path.Partitioned type: MutationType } @@ -53,13 +53,13 @@ export type MutationType = 'added-or-updated' | 'removed' | 'shared' export type PartitionDiscovery

= P extends Path.Public ? { name: 'public' - path: Path.File> + path: Path.Partitioned segments: Path.Segments } : P extends Path.Private ? { name: 'private' - path: Path.File> + path: Path.Partitioned segments: Path.Segments } : never @@ -68,13 +68,13 @@ export type PartitionDiscoveryNonEmpty

= P extends Path.Public ? { name: 'public' - path: Path.File> + path: Path.PartitionedNonEmpty segments: Path.Segments } : P extends Path.Private ? { name: 'private' - path: Path.File> + path: Path.PartitionedNonEmpty segments: Path.Segments } : never diff --git a/packages/nest/src/types/internal.ts b/packages/nest/src/types/internal.ts index 8d175b9..1748ee1 100644 --- a/packages/nest/src/types/internal.ts +++ b/packages/nest/src/types/internal.ts @@ -12,7 +12,7 @@ export type MountedPrivateNodes = Record export interface MountedPrivateNode { node: PrivateNode - path: Path.Distinctive + path: Path.Segments } export type PrivateNodeQueryResult = MountedPrivateNode & { diff --git a/packages/nest/src/unix.ts b/packages/nest/src/unix.ts index 8bf2dcb..adc5346 100644 --- a/packages/nest/src/unix.ts +++ b/packages/nest/src/unix.ts @@ -14,6 +14,9 @@ import * as Store from './store.js' /** * Create a UnixFS directory. + * + * @param currentTime + * @param links */ export function createDirectory( currentTime: Date, @@ -29,6 +32,12 @@ export function createDirectory( /** * Get the bytes of a UnixFS file. + * + * @param cid + * @param store + * @param options + * @param options.offset + * @param options.length */ export async function exportFile( cid: CID, @@ -51,6 +60,9 @@ export async function exportFile( /** * Get the CID for some file bytes. + * + * @param bytes + * @param store */ export async function importFile( bytes: Uint8Array, @@ -63,29 +75,35 @@ export async function importFile( /** * Insert a node into UnixFS tree, creating directories when needed * and overwriting content. + * + * @param itemKind + * @param node + * @param path + * @param store + * @param fileCID */ export async function insertNodeIntoTree( + itemKind: Path.Kind, node: PBNode, - path: Path.Distinctive, + path: Path.Segments, store: Blockstore, fileCID?: CID ): Promise { - const pathKind = Path.kind(path) - const pathParts = Path.unwrap(path) - const name = pathParts[0] + const name = path[0] const link = node.Links.find((l) => l.Name === name) // Directory // --------- - if (Path.length(path) > 1) { + if (path.length > 1) { const dirNode: PBNode = link?.Hash === undefined ? createDirectory(new Date()) : await load(link.Hash, store) const newDirNode = await insertNodeIntoTree( + itemKind, dirNode, - Path.fromKind(pathKind, ...pathParts.slice(1)), + path.slice(1), store, fileCID ) @@ -107,7 +125,7 @@ export async function insertNodeIntoTree( // Last part of path // ----------------- // Directory - if (pathKind === 'directory') { + if (itemKind === Path.Kind.Directory) { if (link !== undefined) return node const dirNode = createDirectory(new Date()) @@ -131,6 +149,9 @@ export async function insertNodeIntoTree( /** * Load a UnixFS node. + * + * @param cid + * @param store */ export async function load(cid: CID, store: Blockstore): Promise { return DagPB.decode(await store.get(cid)) @@ -138,20 +159,22 @@ export async function load(cid: CID, store: Blockstore): Promise { /** * Remove a node from a UnixFS tree. + * + * @param node + * @param path + * @param store */ export async function removeNodeFromTree( node: PBNode, - path: Path.Distinctive, + path: Path.Segments, store: Blockstore ): Promise { - const pathKind = Path.kind(path) - const pathParts = Path.unwrap(path) - const name = pathParts[0] + const name = path[0] const link = node.Links.find((l) => l.Name === name) // Directory // --------- - if (Path.length(path) > 1) { + if (path.length > 1) { let dirNode: PBNode if (link?.Hash === undefined) { @@ -162,7 +185,7 @@ export async function removeNodeFromTree( const newDirNode = await removeNodeFromTree( dirNode, - Path.fromKind(pathKind, ...pathParts.slice(1)), + Path.removePartition(path), store ) @@ -188,14 +211,31 @@ export async function removeNodeFromTree( // ㊙️ +/** + * + * @param links + * @param name + * @param hash + */ function addLink(links: PBLink[], name: string, hash: CID): PBLink[] { return [...links, DagPB.createLink(name, 0, hash)].sort(linkSorter) } +/** + * + * @param links + * @param name + * @param hash + */ function replaceLinkHash(links: PBLink[], name: string, hash: CID): PBLink[] { return links.map((l) => (l.Name === name ? { ...l, Hash: hash } : l)) } +/** + * + * @param a + * @param b + */ function linkSorter(a: PBLink, b: PBLink): number { if ((a.Name ?? '') > (b.Name ?? '')) return 1 if ((a.Name ?? '') < (b.Name ?? '')) return -1 diff --git a/packages/nest/test/class.test.ts b/packages/nest/test/class.test.ts index 13e5d35..3aa7aa7 100644 --- a/packages/nest/test/class.test.ts +++ b/packages/nest/test/class.test.ts @@ -23,7 +23,7 @@ describe('File System Class', () => { let blockstore: Blockstore let fs: FileSystem let _mounts: Array<{ - path: Path.Distinctive + path: Path.Segments capsuleKey: Uint8Array }> @@ -53,8 +53,8 @@ describe('File System Class', () => { // ------- it('loads a file system and capsule keys + content cids', async () => { - const publicPath = Path.file('public', 'nested-public', 'public.txt') - const privatePath = Path.file('private', 'nested-private', 'private.txt') + const publicPath = Path.pub('nested-public', 'public.txt') + const privatePath = Path.priv('nested-private', 'private.txt') await fs.write(publicPath, 'utf8', 'public') const { capsuleKey, dataRoot } = await fs.write( @@ -81,19 +81,19 @@ describe('File System Class', () => { }) it('loads a file system and capsule keys + content cids after multiple changes', async () => { - const publicPath = Path.file('public', 'nested-public', 'public.txt') - const privatePath = Path.file('private', 'nested-private', 'private.txt') + const publicPath = Path.pub('nested-public', 'public.txt') + const privatePath = Path.priv('nested-private', 'private.txt') await fs.write(publicPath, 'utf8', 'public') await fs.write(privatePath, 'utf8', 'private') - await fs.write(Path.file('public', 'part.two'), 'utf8', 'public-2') + await fs.write(Path.pub('part.two'), 'utf8', 'public-2') const { dataRoot } = await fs.write( - Path.file('private', 'part.two'), + Path.priv('part.two'), 'utf8', 'private-2' ) - const capsuleKey = await fs.capsuleKey(Path.directory('private')) + const capsuleKey = await fs.capsuleKey(Path.priv()) const loadedFs = await FileSystem.fromCID(dataRoot, { blockstore, @@ -111,8 +111,8 @@ describe('File System Class', () => { }) it('loads a private file system given an older capsule key', async () => { - const privatePath = Path.file('private', 'nested-private', 'private.txt') - const oldCapsuleKey = await fs.capsuleKey(Path.directory('private')) + const privatePath = Path.priv('nested-private', 'private.txt') + const oldCapsuleKey = await fs.capsuleKey(Path.priv()) const { dataRoot } = await fs.write(privatePath, 'utf8', 'private') @@ -140,7 +140,7 @@ describe('File System Class', () => { // ----------------- it('writes and reads public files', async () => { - const path = Path.file('public', 'a') + const path = Path.pub('a') const bytes = new TextEncoder().encode('🚀') await fs.write(path, 'bytes', bytes) @@ -150,7 +150,7 @@ describe('File System Class', () => { }) it('writes and reads private files', async () => { - const path = Path.file('private', 'a') + const path = Path.priv('a') await fs.write(path, 'json', { foo: 'bar', a: 1 }) @@ -158,8 +158,8 @@ describe('File System Class', () => { }) it('writes and reads deeply nested files', async () => { - const pathPublic = Path.file('public', 'a', 'b', 'c.txt') - const pathPrivate = Path.file('private', 'a', 'b', 'c.txt') + const pathPublic = Path.pub('a', 'b', 'c.txt') + const pathPrivate = Path.priv('a', 'b', 'c.txt') await fs.write(pathPublic, 'utf8', '🌍') await fs.write(pathPrivate, 'utf8', '🔐') @@ -176,41 +176,38 @@ describe('File System Class', () => { }) it('creates files', async () => { - await fs.write(Path.file('private', 'File'), 'utf8', '🧞') - await fs.createFile(Path.file('private', 'File'), 'utf8', '🧞') + await fs.write(Path.priv('File'), 'utf8', '🧞') + await fs.createFile(Path.priv('File'), 'utf8', '🧞') - assert.equal(await fs.exists(Path.file('private', 'File (1)')), true) + assert.equal(await fs.exists(Path.priv('File (1)')), true) - await fs.createFile(Path.file('private', 'File'), 'utf8', '🧞') + await fs.createFile(Path.priv('File'), 'utf8', '🧞') - assert.equal(await fs.exists(Path.file('private', 'File (2)')), true) + assert.equal(await fs.exists(Path.priv('File (2)')), true) - await fs.createFile(Path.file('private', 'File (1)'), 'utf8', '🧞') + await fs.createFile(Path.priv('File (1)'), 'utf8', '🧞') - assert.equal(await fs.read(Path.file('private', 'File (3)'), 'utf8'), '🧞') + assert.equal(await fs.read(Path.priv('File (3)'), 'utf8'), '🧞') }) it('creates files with extensions', async () => { - await fs.write(Path.file('private', 'File.7z'), 'utf8', '🧞') - await fs.createFile(Path.file('private', 'File.7z'), 'utf8', '🧞') + await fs.write(Path.priv('File.7z'), 'utf8', '🧞') + await fs.createFile(Path.priv('File.7z'), 'utf8', '🧞') - assert.equal(await fs.exists(Path.file('private', 'File (1).7z')), true) + assert.equal(await fs.exists(Path.priv('File (1).7z')), true) - await fs.createFile(Path.file('private', 'File.7z'), 'utf8', '🧞') + await fs.createFile(Path.priv('File.7z'), 'utf8', '🧞') - assert.equal(await fs.exists(Path.file('private', 'File (2).7z')), true) + assert.equal(await fs.exists(Path.priv('File (2).7z')), true) - await fs.createFile(Path.file('private', 'File (1).7z'), 'utf8', '🧞') + await fs.createFile(Path.priv('File (1).7z'), 'utf8', '🧞') - assert.equal( - await fs.read(Path.file('private', 'File (3).7z'), 'utf8'), - '🧞' - ) + assert.equal(await fs.read(Path.priv('File (3).7z'), 'utf8'), '🧞') }) it('retrieves public content using a CID', async () => { const { contentCID, capsuleCID } = await fs.write( - Path.file('public', 'file'), + Path.pub('file'), 'utf8', '🌍' ) @@ -221,18 +218,14 @@ describe('File System Class', () => { }) it('retrieves private content using a capsule key', async () => { - const { capsuleKey } = await fs.write( - Path.file('private', 'file'), - 'utf8', - '🔐' - ) + const { capsuleKey } = await fs.write(Path.priv('file'), 'utf8', '🔐') assert.equal(await fs.read({ capsuleKey }, 'utf8'), '🔐') }) it('can read partial public content bytes', async () => { const { contentCID, capsuleCID } = await fs.write( - Path.file('public', 'file'), + Path.pub('file'), 'bytes', new Uint8Array([16, 24, 32]) ) @@ -254,7 +247,7 @@ describe('File System Class', () => { it('can read partial utf8 public content', async () => { const { contentCID, capsuleCID } = await fs.write( - Path.file('public', 'file'), + Path.pub('file'), 'utf8', 'abc' ) @@ -272,7 +265,7 @@ describe('File System Class', () => { it('can read partial private content bytes', async () => { const { capsuleKey } = await fs.write( - Path.file('private', 'file'), + Path.priv('file'), 'bytes', new Uint8Array([16, 24, 32]) ) @@ -286,11 +279,7 @@ describe('File System Class', () => { }) it('can read partial utf8 private content', async () => { - const { capsuleKey } = await fs.write( - Path.file('private', 'file'), - 'utf8', - 'abc' - ) + const { capsuleKey } = await fs.write(Path.priv('file'), 'utf8', 'abc') assert.equal( await fs.read({ capsuleKey }, 'utf8', { offset: 1, length: 1 }), @@ -302,57 +291,46 @@ describe('File System Class', () => { // ----------- it('ensures directories and checks for existence', async () => { - await fs.ensureDirectory(Path.directory('public', 'a')) - await fs.ensureDirectory(Path.directory('public', 'a', 'b')) - await fs.ensureDirectory(Path.directory('public', 'a', 'b', 'c')) + await fs.ensureDirectory(Path.pub('a')) + await fs.ensureDirectory(Path.pub('a', 'b')) + await fs.ensureDirectory(Path.pub('a', 'b', 'c')) - await fs.ensureDirectory(Path.directory('private', 'a')) - await fs.ensureDirectory(Path.directory('private', 'a', 'b')) - await fs.ensureDirectory(Path.directory('private', 'a', 'b', 'c')) + await fs.ensureDirectory(Path.priv('a')) + await fs.ensureDirectory(Path.priv('a', 'b')) + await fs.ensureDirectory(Path.priv('a', 'b', 'c')) - assert.equal(await fs.exists(Path.directory('public', 'a')), true) - assert.equal(await fs.exists(Path.directory('public', 'a', 'b')), true) - assert.equal(await fs.exists(Path.directory('public', 'a', 'b', 'c')), true) + assert.equal(await fs.exists(Path.pub('a')), true) + assert.equal(await fs.exists(Path.pub('a', 'b')), true) + assert.equal(await fs.exists(Path.pub('a', 'b', 'c')), true) - assert.equal(await fs.exists(Path.directory('private', 'a')), true) - assert.equal(await fs.exists(Path.directory('private', 'a', 'b')), true) - assert.equal( - await fs.exists(Path.directory('private', 'a', 'b', 'c')), - true - ) + assert.equal(await fs.exists(Path.priv('a')), true) + assert.equal(await fs.exists(Path.priv('a', 'b')), true) + assert.equal(await fs.exists(Path.priv('a', 'b', 'c')), true) // Does not throw for existing dirs - await fs.ensureDirectory(Path.directory('public', 'a')) - await fs.ensureDirectory(Path.directory('public', 'a', 'b')) + await fs.ensureDirectory(Path.pub('a')) + await fs.ensureDirectory(Path.pub('a', 'b')) - await fs.ensureDirectory(Path.directory('private', 'a')) - await fs.ensureDirectory(Path.directory('private', 'a', 'b')) + await fs.ensureDirectory(Path.priv('a')) + await fs.ensureDirectory(Path.priv('a', 'b')) - await assertUnixFsDirectory( - { blockstore }, - fs, - Path.directory('public', 'a') - ) - await assertUnixFsDirectory( - { blockstore }, - fs, - Path.directory('public', 'a', 'b') - ) + await assertUnixFsDirectory({ blockstore }, fs, Path.pub('a')) + await assertUnixFsDirectory({ blockstore }, fs, Path.pub('a', 'b')) }) it('lists public directories', async () => { - await fs.ensureDirectory(Path.directory('public', 'a')) - await fs.write(Path.file('public', 'a-file'), 'utf8', '🧞') - await fs.ensureDirectory(Path.directory('public', 'a', 'b')) - await fs.write(Path.file('public', 'a', 'b-file'), 'utf8', '💃') + await fs.ensureDirectory(Path.pub('a')) + await fs.write(Path.pub('a-file'), 'utf8', '🧞') + await fs.ensureDirectory(Path.pub('a', 'b')) + await fs.write(Path.pub('a', 'b-file'), 'utf8', '💃') - const a = await fs.listDirectory(Path.directory('public')) + const a = await fs.listDirectory(Path.pub()) assert.deepEqual( a.map((i) => i.name), ['a', 'a-file'] ) - const b = await fs.listDirectory(Path.directory('public', 'a')) + const b = await fs.listDirectory(Path.pub('a')) assert.deepEqual( b.map((i) => i.name), ['b', 'b-file'] @@ -360,17 +338,17 @@ describe('File System Class', () => { }) it('lists public directories with item kind', async () => { - const pathDirA = Path.directory('public', 'a') - const pathFileA = Path.file('public', 'a-file') - const pathDirB = Path.directory('public', 'a', 'b') - const pathFileB = Path.file('public', 'a', 'b-file') + const pathDirA = Path.pub('a') + const pathFileA = Path.pub('a-file') + const pathDirB = Path.pub('a', 'b') + const pathFileB = Path.pub('a', 'b-file') await fs.ensureDirectory(pathDirA) await fs.write(pathFileA, 'utf8', '🧞') await fs.ensureDirectory(pathDirB) await fs.write(pathFileB, 'utf8', '💃') - const a = await fs.listDirectory(Path.directory('public'), { + const a = await fs.listDirectory(Path.pub(), { withItemKind: true, }) assert.deepEqual( @@ -382,7 +360,7 @@ describe('File System Class', () => { [pathDirA, pathFileA] ) - const b = await fs.listDirectory(Path.directory('public', 'a'), { + const b = await fs.listDirectory(Path.pub('a'), { withItemKind: true, }) assert.deepEqual( @@ -396,18 +374,18 @@ describe('File System Class', () => { }) it('lists private directories', async () => { - await fs.ensureDirectory(Path.directory('private', 'a')) - await fs.write(Path.file('private', 'a-file'), 'utf8', '🧞') - await fs.ensureDirectory(Path.directory('private', 'a', 'b')) - await fs.write(Path.file('private', 'a', 'b-file'), 'utf8', '💃') + await fs.ensureDirectory(Path.priv('a')) + await fs.write(Path.priv('a-file'), 'utf8', '🧞') + await fs.ensureDirectory(Path.priv('a', 'b')) + await fs.write(Path.priv('a', 'b-file'), 'utf8', '💃') - const a = await fs.listDirectory(Path.directory('private')) + const a = await fs.listDirectory(Path.priv()) assert.deepEqual( a.map((i) => i.name), ['a', 'a-file'] ) - const b = await fs.listDirectory(Path.directory('private', 'a')) + const b = await fs.listDirectory(Path.priv('a')) assert.deepEqual( b.map((i) => i.name), ['b', 'b-file'] @@ -415,17 +393,17 @@ describe('File System Class', () => { }) it('lists private directories with item kind', async () => { - const pathDirA = Path.directory('private', 'a') - const pathFileA = Path.file('private', 'a-file') - const pathDirB = Path.directory('private', 'a', 'b') - const pathFileB = Path.file('private', 'a', 'b-file') + const pathDirA = Path.priv('a') + const pathFileA = Path.priv('a-file') + const pathDirB = Path.priv('a', 'b') + const pathFileB = Path.priv('a', 'b-file') await fs.ensureDirectory(pathDirA) await fs.write(pathFileA, 'utf8', '🧞') await fs.ensureDirectory(pathDirB) await fs.write(pathFileB, 'utf8', '💃') - const a = await fs.listDirectory(Path.directory('private'), { + const a = await fs.listDirectory(Path.priv(), { withItemKind: true, }) assert.deepEqual( @@ -437,7 +415,7 @@ describe('File System Class', () => { [pathDirA, pathFileA] ) - const b = await fs.listDirectory(Path.directory('private', 'a'), { + const b = await fs.listDirectory(Path.priv('a'), { withItemKind: true, }) assert.deepEqual( @@ -451,58 +429,25 @@ describe('File System Class', () => { }) it('creates directories', async () => { - await fs.ensureDirectory(Path.directory('private', 'Directory')) - await fs.createDirectory(Path.directory('private', 'Directory')) - - assert.equal( - await fs.exists(Path.directory('private', 'Directory (1)')), - true - ) - - await fs.createDirectory(Path.directory('private', 'Directory')) - - assert.equal( - await fs.exists(Path.directory('private', 'Directory (2)')), - true - ) - - await fs.createDirectory(Path.directory('private', 'Directory (1)')) - - assert.equal( - await fs.exists(Path.directory('private', 'Directory (3)')), - true - ) - }) - - it('creates directories with extensions', async () => { - await fs.ensureDirectory(Path.directory('private', 'Directory.7z')) - await fs.createDirectory(Path.directory('private', 'Directory.7z')) + await fs.ensureDirectory(Path.priv('Directory')) + await fs.createDirectory(Path.priv('Directory')) - assert.equal( - await fs.exists(Path.directory('private', 'Directory.7z (1)')), - true - ) + assert.equal(await fs.exists(Path.priv('Directory (1)')), true) - await fs.createDirectory(Path.directory('private', 'Directory.7z')) + await fs.createDirectory(Path.priv('Directory')) - assert.equal( - await fs.exists(Path.directory('private', 'Directory.7z (2)')), - true - ) + assert.equal(await fs.exists(Path.priv('Directory (2)')), true) - await fs.createDirectory(Path.directory('private', 'Directory.7z (1)')) + await fs.createDirectory(Path.priv('Directory (1)')) - assert.equal( - await fs.exists(Path.directory('private', 'Directory.7z (3)')), - true - ) + assert.equal(await fs.exists(Path.priv('Directory (3)')), true) }) // CIDS & REFS // ----------- it('can get a content CID for an existing public file', async () => { - const path = Path.file('public', 'a', 'b', 'file') + const path = Path.pub('a', 'b', 'file') const { contentCID } = await fs.write(path, 'utf8', '💃') const cid = await fs.contentCID(path) @@ -511,7 +456,7 @@ describe('File System Class', () => { }) it('can get a capsule CID for an existing public file', async () => { - const path = Path.file('public', 'a', 'b', 'file') + const path = Path.pub('a', 'b', 'file') const { capsuleCID } = await fs.write(path, 'utf8', '💃') const cid = await fs.capsuleCID(path) @@ -520,7 +465,7 @@ describe('File System Class', () => { }) it('can get a capsule CID for an existing public directory', async () => { - const path = Path.directory('public', 'a', 'b', 'directory') + const path = Path.pub('a', 'b', 'directory') const { capsuleCID } = await fs.ensureDirectory(path) const cid = await fs.capsuleCID(path) @@ -529,7 +474,7 @@ describe('File System Class', () => { }) it('can get a capsule key for an existing private file', async () => { - const path = Path.file('private', 'a', 'b', 'file') + const path = Path.priv('a', 'b', 'file') const { capsuleKey } = await fs.write(path, 'utf8', '💃') const key = await fs.capsuleKey(path) @@ -541,7 +486,7 @@ describe('File System Class', () => { }) it('can get a capsule CID for an existing private directory', async () => { - const path = Path.directory('private', 'a', 'b', 'directory') + const path = Path.priv('a', 'b', 'directory') const { capsuleKey } = await fs.ensureDirectory(path) const key = await fs.capsuleKey(path) @@ -553,7 +498,7 @@ describe('File System Class', () => { }) it('can get a capsule CID for a mounted private directory', async () => { - const path = Path.directory('private') + const path = Path.priv() const key = await fs.capsuleKey(path) assert.notEqual( @@ -566,7 +511,7 @@ describe('File System Class', () => { // ---- it('returns the size of public files', async () => { - const path = Path.file('public', 'file') + const path = Path.pub('file') await fs.write(path, 'bytes', new Uint8Array([1, 2, 3])) const size = await fs.size(path) @@ -575,7 +520,7 @@ describe('File System Class', () => { }) it('returns the size of private files', async () => { - const path = Path.file('private', 'file') + const path = Path.priv('file') await fs.write(path, 'bytes', new Uint8Array([1, 2, 3, 4])) const size = await fs.size(path) @@ -587,7 +532,7 @@ describe('File System Class', () => { // ------ it('removes public files', async () => { - const path = Path.file('public', 'a', 'b', 'file') + const path = Path.pub('a', 'b', 'file') await fs.write(path, 'utf8', '💃') await fs.remove(path) @@ -598,7 +543,7 @@ describe('File System Class', () => { }) it('removes private files', async () => { - const path = Path.file('private', 'a', 'b', 'file') + const path = Path.priv('a', 'b', 'file') await fs.write(path, 'utf8', '💃') await fs.remove(path) @@ -607,7 +552,7 @@ describe('File System Class', () => { }) it('removes public directories', async () => { - const path = Path.directory('public', 'a', 'b', 'directory') + const path = Path.pub('a', 'b', 'directory') await fs.ensureDirectory(path) await fs.remove(path) @@ -618,7 +563,7 @@ describe('File System Class', () => { }) it('removes private directories', async () => { - const path = Path.directory('private', 'a', 'b', 'directory') + const path = Path.priv('a', 'b', 'directory') await fs.ensureDirectory(path) await fs.remove(path) @@ -630,8 +575,8 @@ describe('File System Class', () => { // ------- it('copies public files', async () => { - const fromPath = Path.file('public', 'a', 'b', 'file') - const toPath = Path.file('public', 'a', 'b', 'c', 'd', 'file') + const fromPath = Path.pub('a', 'b', 'file') + const toPath = Path.pub('a', 'b', 'c', 'd', 'file') await fs.write(fromPath, 'utf8', '💃') await fs.copy(fromPath, toPath) @@ -640,10 +585,10 @@ describe('File System Class', () => { }) it('copies public files into a directory that already exists', async () => { - await fs.ensureDirectory(Path.directory('public', 'a', 'b', 'c', 'd')) + await fs.ensureDirectory(Path.pub('a', 'b', 'c', 'd')) - const fromPath = Path.file('public', 'a', 'b', 'file') - const toPath = Path.file('public', 'a', 'b', 'c', 'd', 'file') + const fromPath = Path.pub('a', 'b', 'file') + const toPath = Path.pub('a', 'b', 'c', 'd', 'file') await fs.write(fromPath, 'utf8', '💃') await fs.copy(fromPath, toPath) @@ -652,8 +597,8 @@ describe('File System Class', () => { }) it('copies private files', async () => { - const fromPath = Path.file('private', 'a', 'b', 'file') - const toPath = Path.file('private', 'a', 'b', 'c', 'd', 'file') + const fromPath = Path.priv('a', 'b', 'file') + const toPath = Path.priv('a', 'b', 'c', 'd', 'file') await fs.write(fromPath, 'utf8', '💃') await fs.copy(fromPath, toPath) @@ -662,10 +607,10 @@ describe('File System Class', () => { }) it('copies private files into a directory that already exists', async () => { - await fs.ensureDirectory(Path.directory('private', 'a', 'b', 'c', 'd')) + await fs.ensureDirectory(Path.priv('a', 'b', 'c', 'd')) - const fromPath = Path.file('private', 'a', 'b', 'file') - const toPath = Path.file('private', 'a', 'b', 'c', 'd', 'file') + const fromPath = Path.priv('a', 'b', 'file') + const toPath = Path.priv('a', 'b', 'c', 'd', 'file') await fs.write(fromPath, 'utf8', '💃') await fs.copy(fromPath, toPath) @@ -674,103 +619,71 @@ describe('File System Class', () => { }) it('copies public directories', async () => { - const fromPath = Path.directory('public', 'b', 'c') - const toPath = Path.directory('public', 'a', 'b', 'c', 'd', 'e') + const fromPath = Path.pub('b', 'c') + const toPath = Path.pub('a', 'b', 'c', 'd', 'e') - await fs.write(Path.combine(fromPath, Path.file('file')), 'utf8', '💃') - await fs.write( - Path.combine(fromPath, Path.file('nested', 'file')), - 'utf8', - '🧞' - ) + await fs.write(Path.combine(fromPath, ['file']), 'utf8', '💃') + await fs.write(Path.combine(fromPath, ['nested', 'file']), 'utf8', '🧞') + await fs.ensureDirectory(Path.combine(fromPath, ['nested-empty'])) await fs.ensureDirectory( - Path.combine(fromPath, Path.directory('nested-empty')) - ) - await fs.ensureDirectory( - Path.combine(fromPath, Path.directory('nested-2', 'deeply-nested')) + Path.combine(fromPath, ['nested-2', 'deeply-nested']) ) await fs.copy(fromPath, toPath) - assert.equal( - await fs.read(Path.combine(toPath, Path.file('file')), 'utf8'), - '💃' - ) + assert.equal(await fs.read(Path.combine(toPath, ['file']), 'utf8'), '💃') assert.equal( - await fs.read(Path.combine(toPath, Path.file('nested', 'file')), 'utf8'), + await fs.read(Path.combine(toPath, ['nested', 'file']), 'utf8'), '🧞' ) - assert.equal( - await fs.exists(Path.combine(toPath, Path.directory('nested-empty'))), - true - ) + assert.equal(await fs.exists(Path.combine(toPath, ['nested-empty'])), true) assert.equal( - await fs.exists( - Path.combine(toPath, Path.directory('nested-2', 'deeply-nested')) - ), + await fs.exists(Path.combine(toPath, ['nested-2', 'deeply-nested'])), true ) - await fs.copy(Path.directory('public', 'a', 'b'), Path.directory('public')) + await fs.copy(Path.pub('a', 'b'), Path.pub()) assert.equal( - await fs.exists( - Path.directory('public', 'b', 'c', 'nested-2', 'deeply-nested') - ), + await fs.exists(Path.pub('b', 'c', 'nested-2', 'deeply-nested')), true ) }) it('copies private directories', async () => { - const fromPath = Path.directory('private', 'b', 'c') - const toPath = Path.directory('private', 'a', 'b', 'c', 'd', 'e') + const fromPath = Path.priv('b', 'c') + const toPath = Path.priv('a', 'b', 'c', 'd', 'e') - await fs.write(Path.combine(fromPath, Path.file('file')), 'utf8', '💃') - await fs.write( - Path.combine(fromPath, Path.file('nested', 'file')), - 'utf8', - '🧞' - ) - await fs.ensureDirectory( - Path.combine(fromPath, Path.directory('nested-empty')) - ) + await fs.write(Path.combine(fromPath, ['file']), 'utf8', '💃') + await fs.write(Path.combine(fromPath, ['nested', 'file']), 'utf8', '🧞') + await fs.ensureDirectory(Path.combine(fromPath, ['nested-empty'])) await fs.ensureDirectory( - Path.combine(fromPath, Path.directory('nested-2', 'deeply-nested')) + Path.combine(fromPath, ['nested-2', 'deeply-nested']) ) await fs.copy(fromPath, toPath) - assert.equal( - await fs.read(Path.combine(toPath, Path.file('file')), 'utf8'), - '💃' - ) + assert.equal(await fs.read(Path.combine(toPath, ['file']), 'utf8'), '💃') assert.equal( - await fs.read(Path.combine(toPath, Path.file('nested', 'file')), 'utf8'), + await fs.read(Path.combine(toPath, ['nested', 'file']), 'utf8'), '🧞' ) - assert.equal( - await fs.exists(Path.combine(toPath, Path.directory('nested-empty'))), - true - ) + assert.equal(await fs.exists(Path.combine(toPath, ['nested-empty'])), true) assert.equal( - await fs.exists( - Path.combine(toPath, Path.directory('nested-2', 'deeply-nested')) - ), + await fs.exists(Path.combine(toPath, ['nested-2', 'deeply-nested'])), true ) - await fs.copy(Path.directory('private', 'a'), Path.directory('private')) + await fs.copy(Path.priv('a'), Path.priv()) assert.equal( - await fs.exists( - Path.directory('private', 'b', 'c', 'nested-2', 'deeply-nested') - ), + await fs.exists(Path.priv('b', 'c', 'nested-2', 'deeply-nested')), true ) }) @@ -779,8 +692,8 @@ describe('File System Class', () => { // ------ it('moves public files', async () => { - const fromPath = Path.file('public', 'a', 'b', 'file') - const toPath = Path.file('public', 'a', 'b', 'c', 'd', 'file') + const fromPath = Path.pub('a', 'b', 'file') + const toPath = Path.pub('a', 'b', 'c', 'd', 'file') await fs.write(fromPath, 'utf8', '💃') await fs.move(fromPath, toPath) @@ -790,8 +703,8 @@ describe('File System Class', () => { }) it('moves private files', async () => { - const fromPath = Path.file('private', 'a', 'b', 'file') - const toPath = Path.file('private', 'a', 'b', 'c', 'd', 'file') + const fromPath = Path.priv('a', 'b', 'file') + const toPath = Path.priv('a', 'b', 'c', 'd', 'file') await fs.write(fromPath, 'utf8', '💃') await fs.move(fromPath, toPath) @@ -801,150 +714,100 @@ describe('File System Class', () => { }) it('moves public directories', async () => { - const fromPath = Path.directory('public', 'b', 'c') - const toPath = Path.directory('public', 'a', 'b', 'c', 'd', 'e') + const fromPath = Path.pub('b', 'c') + const toPath = Path.pub('a', 'b', 'c', 'd', 'e') - await fs.write(Path.combine(fromPath, Path.file('file')), 'utf8', '💃') - await fs.write( - Path.combine(fromPath, Path.file('nested', 'file')), - 'utf8', - '🧞' - ) - await fs.ensureDirectory( - Path.combine(fromPath, Path.directory('nested-empty')) - ) + await fs.write(Path.combine(fromPath, ['file']), 'utf8', '💃') + await fs.write(Path.combine(fromPath, ['nested', 'file']), 'utf8', '🧞') + await fs.ensureDirectory(Path.combine(fromPath, ['nested-empty'])) await fs.ensureDirectory( - Path.combine(fromPath, Path.directory('nested-2', 'deeply-nested')) + Path.combine(fromPath, ['nested-2', 'deeply-nested']) ) await fs.move(fromPath, toPath) - assert.equal( - await fs.read(Path.combine(toPath, Path.file('file')), 'utf8'), - '💃' - ) + assert.equal(await fs.read(Path.combine(toPath, ['file']), 'utf8'), '💃') assert.equal( - await fs.read(Path.combine(toPath, Path.file('nested', 'file')), 'utf8'), + await fs.read(Path.combine(toPath, ['nested', 'file']), 'utf8'), '🧞' ) - assert.equal( - await fs.exists(Path.combine(toPath, Path.directory('nested-empty'))), - true - ) + assert.equal(await fs.exists(Path.combine(toPath, ['nested-empty'])), true) assert.equal( - await fs.exists( - Path.combine(toPath, Path.directory('nested-2', 'deeply-nested')) - ), + await fs.exists(Path.combine(toPath, ['nested-2', 'deeply-nested'])), true ) assert.equal(await fs.exists(fromPath), false) - await fs.move(Path.directory('public', 'a'), Path.directory('public')) + await fs.move(Path.pub('a'), Path.pub()) assert.equal( - await fs.exists( - Path.directory('public', 'b', 'c', 'nested-2', 'deeply-nested') - ), + await fs.exists(Path.pub('b', 'c', 'nested-2', 'deeply-nested')), false ) - assert.equal(await fs.exists(Path.directory('public', 'a')), false) + assert.equal(await fs.exists(Path.pub('a')), false) assert.equal( await fs.exists( - Path.directory( - 'public', - 'a', - 'b', - 'c', - 'd', - 'e', - 'nested-2', - 'deeply-nested' - ) + Path.pub('a', 'b', 'c', 'd', 'e', 'nested-2', 'deeply-nested') ), false ) }) it('moves private directories', async () => { - const fromPath = Path.directory('private', 'b', 'c') - const toPath = Path.directory('private', 'a', 'b', 'c', 'd', 'e') + const fromPath = Path.priv('b', 'c') + const toPath = Path.priv('a', 'b', 'c', 'd', 'e') - await fs.write(Path.combine(fromPath, Path.file('file')), 'utf8', '💃') - await fs.write( - Path.combine(fromPath, Path.file('nested', 'file')), - 'utf8', - '🧞' - ) + await fs.write(Path.combine(fromPath, ['file']), 'utf8', '💃') + await fs.write(Path.combine(fromPath, ['nested', 'file']), 'utf8', '🧞') + await fs.ensureDirectory(Path.combine(fromPath, ['nested-empty'])) await fs.ensureDirectory( - Path.combine(fromPath, Path.directory('nested-empty')) - ) - await fs.ensureDirectory( - Path.combine(fromPath, Path.directory('nested-2', 'deeply-nested')) + Path.combine(fromPath, ['nested-2', 'deeply-nested']) ) await fs.move(fromPath, toPath) - assert.equal( - await fs.read(Path.combine(toPath, Path.file('file')), 'utf8'), - '💃' - ) + assert.equal(await fs.read(Path.combine(toPath, ['file']), 'utf8'), '💃') assert.equal( - await fs.read(Path.combine(toPath, Path.file('nested', 'file')), 'utf8'), + await fs.read(Path.combine(toPath, ['nested', 'file']), 'utf8'), '🧞' ) - assert.equal( - await fs.exists(Path.combine(toPath, Path.directory('nested-empty'))), - true - ) + assert.equal(await fs.exists(Path.combine(toPath, ['nested-empty'])), true) assert.equal( - await fs.exists( - Path.combine(toPath, Path.directory('nested-2', 'deeply-nested')) - ), + await fs.exists(Path.combine(toPath, ['nested-2', 'deeply-nested'])), true ) assert.equal(await fs.exists(fromPath), false) - await fs.move(Path.directory('private', 'a'), Path.directory('private')) + await fs.move(Path.priv('a'), Path.priv()) assert.equal( - await fs.exists( - Path.directory('public', 'b', 'c', 'nested-2', 'deeply-nested') - ), + await fs.exists(Path.pub('b', 'c', 'nested-2', 'deeply-nested')), false ) - assert.equal(await fs.exists(Path.directory('public', 'a')), false) + assert.equal(await fs.exists(Path.pub('a')), false) assert.equal( await fs.exists( - Path.directory( - 'public', - 'a', - 'b', - 'c', - 'd', - 'e', - 'nested-2', - 'deeply-nested' - ) + Path.pub('a', 'b', 'c', 'd', 'e', 'nested-2', 'deeply-nested') ), false ) }) it('moves a public file to the private partition', async () => { - const fromPath = Path.file('public', 'a', 'b', 'file') - const toPath = Path.file('private', 'a', 'b', 'c', 'd', 'file') + const fromPath = Path.pub('a', 'b', 'file') + const toPath = Path.priv('a', 'b', 'c', 'd', 'file') await fs.write(fromPath, 'utf8', '💃') await fs.move(fromPath, toPath) @@ -954,8 +817,8 @@ describe('File System Class', () => { }) it('moves a private file to the public partition', async () => { - const fromPath = Path.file('private', 'a', 'b', 'file') - const toPath = Path.file('public', 'a', 'b', 'c', 'd', 'file') + const fromPath = Path.priv('a', 'b', 'file') + const toPath = Path.pub('a', 'b', 'c', 'd', 'file') await fs.write(fromPath, 'utf8', '💃') await fs.move(fromPath, toPath) @@ -968,39 +831,39 @@ describe('File System Class', () => { // -------- it('renames public files', async () => { - await fs.write(Path.file('public', 'a'), 'bytes', new Uint8Array()) - await fs.rename(Path.file('public', 'a'), 'b') + await fs.write(Path.pub('a'), 'bytes', new Uint8Array()) + await fs.rename(Path.pub('a'), 'b') - assert.equal(await fs.exists(Path.file('public', 'a')), false) + assert.equal(await fs.exists(Path.pub('a')), false) - assert.equal(await fs.exists(Path.file('public', 'b')), true) + assert.equal(await fs.exists(Path.pub('b')), true) }) it('renames private files', async () => { - await fs.write(Path.file('private', 'a'), 'bytes', new Uint8Array()) - await fs.rename(Path.file('private', 'a'), 'b') + await fs.write(Path.priv('a'), 'bytes', new Uint8Array()) + await fs.rename(Path.priv('a'), 'b') - assert.equal(await fs.exists(Path.file('private', 'a')), false) + assert.equal(await fs.exists(Path.priv('a')), false) - assert.equal(await fs.exists(Path.file('private', 'b')), true) + assert.equal(await fs.exists(Path.priv('b')), true) }) it('renames public directories', async () => { - await fs.ensureDirectory(Path.directory('public', 'a')) - await fs.rename(Path.directory('public', 'a'), 'b') + await fs.ensureDirectory(Path.pub('a')) + await fs.rename(Path.pub('a'), 'b') - assert.equal(await fs.exists(Path.directory('public', 'a')), false) + assert.equal(await fs.exists(Path.pub('a')), false) - assert.equal(await fs.exists(Path.directory('public', 'b')), true) + assert.equal(await fs.exists(Path.pub('b')), true) }) it('renames private directories', async () => { - await fs.ensureDirectory(Path.directory('private', 'a')) - await fs.rename(Path.directory('private', 'a'), 'b') + await fs.ensureDirectory(Path.priv('a')) + await fs.rename(Path.priv('a'), 'b') - assert.equal(await fs.exists(Path.directory('private', 'a')), false) + assert.equal(await fs.exists(Path.priv('a')), false) - assert.equal(await fs.exists(Path.directory('private', 'b')), true) + assert.equal(await fs.exists(Path.priv('b')), true) }) // PUBLISHING @@ -1018,15 +881,11 @@ describe('File System Class', () => { .then(resolve, reject) }) - await fs.write(Path.file('private', 'a'), 'bytes', new Uint8Array()) - await fs.write(Path.file('private', 'b'), 'bytes', new Uint8Array()) - await fs.write(Path.file('private', 'c'), 'bytes', new Uint8Array()) + await fs.write(Path.priv('a'), 'bytes', new Uint8Array()) + await fs.write(Path.priv('b'), 'bytes', new Uint8Array()) + await fs.write(Path.priv('c'), 'bytes', new Uint8Array()) - const d = await fs.write( - Path.file('private', 'd'), - 'bytes', - new Uint8Array() - ) + const d = await fs.write(Path.priv('d'), 'bytes', new Uint8Array()) const result = await promise assert.equal(result.toString(), d.dataRoot.toString()) @@ -1048,22 +907,20 @@ describe('File System Class', () => { published = true }) - await fs.mkdir(Path.directory('private', 'dir'), { skipPublish: true }) - await fs.write(Path.file('public', 'file'), 'bytes', new Uint8Array(), { + await fs.mkdir(Path.priv('dir'), { skipPublish: true }) + await fs.write(Path.pub('file'), 'bytes', new Uint8Array(), { skipPublish: true, }) - await fs.cp(Path.file('public', 'file'), Path.file('private', 'file'), { + await fs.cp(Path.pub('file'), Path.priv('file'), { skipPublish: true, }) - await fs.mv( - Path.file('private', 'file'), - Path.file('private', 'dir', 'file'), - { skipPublish: true } - ) - await fs.rename(Path.file('private', 'dir', 'file'), 'renamed', { + await fs.mv(Path.priv('file'), Path.priv('dir', 'file'), { + skipPublish: true, + }) + await fs.rename(Path.priv('dir', 'file'), 'renamed', { skipPublish: true, }) - await fs.rm(Path.file('private', 'dir', 'renamed'), { skipPublish: true }) + await fs.rm(Path.priv('dir', 'renamed'), { skipPublish: true }) await new Promise((resolve) => setTimeout(resolve, fsOpts.settleTimeBeforePublish * 1.5) @@ -1086,7 +943,7 @@ describe('File System Class', () => { }) const mutationResult = await fs.write( - Path.file('private', 'file'), + Path.priv('file'), 'bytes', new Uint8Array() ) @@ -1100,27 +957,27 @@ describe('File System Class', () => { it('commits a transaction', async () => { await fs.transaction(async (t) => { - await t.write(Path.file('private', 'file'), 'utf8', '💃') + await t.write(Path.priv('file'), 'utf8', '💃') await t.write( - Path.file('public', 'file'), + Path.pub('file'), 'bytes', - await t.read(Path.file('private', 'file'), 'bytes') + await t.read(Path.priv('file'), 'bytes') ) }) - assert.equal(await fs.read(Path.file('public', 'file'), 'utf8'), '💃') + assert.equal(await fs.read(Path.pub('file'), 'utf8'), '💃') }) it("doesn't commit a transaction when an error occurs inside of the transaction", async () => { await fs .transaction(async (t) => { - await t.write(Path.file('private', 'file'), 'utf8', '💃') + await t.write(Path.priv('file'), 'utf8', '💃') throw new Error('Whoops') }) .catch((_error) => {}) try { - await fs.read(Path.file('private', 'file'), 'utf8') + await fs.read(Path.priv('file'), 'utf8') } catch (error) { assert(error) } @@ -1136,7 +993,7 @@ describe('File System Class', () => { _mounts = [await fs.createPrivateNode({ path: Path.root() })] const result = await fs.transaction(async (t) => { - await t.write(Path.file('private', 'file'), 'utf8', '💃') + await t.write(Path.priv('file'), 'utf8', '💃') }) assert.equal(result, 'no-op') @@ -1159,7 +1016,7 @@ describe('File System Class', () => { const receiverDataRoot = await receiverFs.calculateDataRoot() await fs.transaction(async (t) => { - const path = Path.file('private', 'fileToShare') + const path = Path.priv('fileToShare') await t.assignIdentifier('did:test:1') await t.write(path, 'utf8', '🔒') await t.share(path, receiverDataRoot) @@ -1191,22 +1048,16 @@ describe('File System Class', () => { const receiverDataRoot = a.dataRoot // Assign sharer identifier & create share - const path = Path.file('private', 'nested', 'level 2', 'fileToShare') + const path = Path.priv('nested', 'level 2', 'fileToShare') await fs.assignIdentifier('did:test:1') await fs.write(path, 'utf8', '🔒') - const b = await fs.share( - Path.directory('private', 'nested'), - receiverDataRoot - ) + const b = await fs.share(Path.priv('nested'), receiverDataRoot) // Receive share const sharerDataRoot = b.dataRoot const share = await receiverFs.receive(sharerDataRoot, keypair) - const content = await share.read( - 'utf8', - Path.file('level 2', 'fileToShare') - ) + const content = await share.read('utf8', ['level 2', 'fileToShare']) // Assert assert.equal(content, '🔒') @@ -1219,13 +1070,10 @@ describe('File System Class', () => { await fs.registerExchangeKey('device', keypair.publicKey) // Assign sharer identifier & create share - const path = Path.file('private', 'nested', 'level 2', 'fileToShare') + const path = Path.priv('nested', 'level 2', 'fileToShare') await fs.assignIdentifier('did:test:2') await fs.write(path, 'utf8', '🔒') - await fs.share( - Path.directory('private', 'nested'), - await fs.calculateDataRoot() - ) + await fs.share(Path.priv('nested'), await fs.calculateDataRoot()) // Unmount root test node, otherwise we'd search from the root, // instead of the new mount. @@ -1233,12 +1081,12 @@ describe('File System Class', () => { // Mount node await fs.mountPrivateNode({ - path: Path.directory('shared', 'item'), + path: ['shared', 'item'], exchangeKeyPair: keypair, }) const content = await fs.read( - Path.file('private', 'shared', 'item', 'level 2', 'fileToShare'), + Path.priv('shared', 'item', 'level 2', 'fileToShare'), 'utf8' ) @@ -1253,10 +1101,10 @@ describe('File System Class', () => { await fs.registerExchangeKey('device', keypair.publicKey) // Assign sharer identifier & create share - const path = Path.file('private', 'nested', 'level 2', 'fileToShare') + const path = Path.priv('nested', 'level 2', 'fileToShare') await fs.assignIdentifier('did:test:3') await fs.write(path, 'utf8', '🔒') - await fs.share(Path.directory('private'), await fs.calculateDataRoot()) + await fs.share(Path.priv(), await fs.calculateDataRoot()) // Unmount existing root node fs.unmountPrivateNode(Path.root()) @@ -1269,29 +1117,25 @@ describe('File System Class', () => { // Check content const content = await fs.read( - Path.file('private', 'nested', 'level 2', 'fileToShare'), + Path.priv('nested', 'level 2', 'fileToShare'), 'utf8' ) assert.equal(content, '🔒') // Test mutations - await fs.write( - Path.file('private', 'nested', 'level 2', 'fileToShare'), - 'utf8', - '🚀' - ) + await fs.write(Path.priv('nested', 'level 2', 'fileToShare'), 'utf8', '🚀') const content2 = await fs.read( - Path.file('private', 'nested', 'level 2', 'fileToShare'), + Path.priv('nested', 'level 2', 'fileToShare'), 'utf8' ) assert.equal(content2, '🚀') - await fs.write(Path.file('private', 'dir', 'test'), 'utf8', '✌️') + await fs.write(Path.priv('dir', 'test'), 'utf8', '✌️') - const content3 = await fs.read(Path.file('private', 'dir', 'test'), 'utf8') + const content3 = await fs.read(Path.priv('dir', 'test'), 'utf8') assert.equal(content3, '✌️') }) @@ -1323,24 +1167,16 @@ describe('File System Class', () => { }), ] - await fs.write( - Path.file('private', 'nested', 'level 2', 'fileToShare'), - 'utf8', - '🚀' - ) + await fs.write(Path.priv('nested', 'level 2', 'fileToShare'), 'utf8', '🚀') const content2 = await fs.read( - Path.file('private', 'nested', 'level 2', 'fileToShare'), + Path.priv('nested', 'level 2', 'fileToShare'), 'utf8' ) assert.equal(content2, '🚀') - const { dataRoot } = await fs.write( - Path.file('private', 'dir', 'test'), - 'utf8', - '✌️' - ) + const { dataRoot } = await fs.write(Path.priv('dir', 'test'), 'utf8', '✌️') // Try to load a new instance const fsInstance = await FileSystem.fromCID(dataRoot, { @@ -1353,10 +1189,7 @@ describe('File System Class', () => { exchangeKeyPair: keypair, }) - const content = await fsInstance.read( - Path.file('private', 'dir', 'test'), - 'utf8' - ) + const content = await fsInstance.read(Path.priv('dir', 'test'), 'utf8') assert.equal(content, '✌️') }) @@ -1378,15 +1211,12 @@ describe('File System Class', () => { // Assign sharer identifier & create share await fs.assignIdentifier('did:test:5') - await fs.write(Path.file('private', 'file 1'), 'utf8', '🔐 1') - await fs.write(Path.file('private', 'file 2'), 'utf8', '🔒 2') + await fs.write(Path.priv('file 1'), 'utf8', '🔐 1') + await fs.write(Path.priv('file 2'), 'utf8', '🔒 2') - const { shareId } = await fs.share( - Path.file('private', 'file 1'), - receiverDataRoot - ) + const { shareId } = await fs.share(Path.priv('file 1'), receiverDataRoot) - await fs.share(Path.file('private', 'file 2'), receiverDataRoot) + await fs.share(Path.priv('file 2'), receiverDataRoot) // Receive shares const sharerDataRoot = await fs.calculateDataRoot() @@ -1416,49 +1246,42 @@ describe('File System Class', () => { await fs.registerExchangeKey('device', keypair.publicKey) const first = await fs.createPrivateNode({ - path: Path.directory('first'), + path: ['first'], exchangeKeyPair: keypair, }) const _second = await fs.createPrivateNode({ - path: Path.directory('second'), + path: ['second'], exchangeKeyPair: keypair, }) - await fs.write( - Path.file('private', 'first', 'nested', 'file'), - 'utf8', - '🔑' - ) + await fs.write(Path.priv('first', 'nested', 'file'), 'utf8', '🔑') - await fs.write(Path.file('private', 'second', 'file'), 'utf8', '🔒') + await fs.write(Path.priv('second', 'file'), 'utf8', '🔒') const fsInstance = await FileSystem.fromCID(await fs.calculateDataRoot(), { blockstore, }) await fsInstance.mountPrivateNode({ - path: Path.directory('first'), + path: ['first'], exchangeKeyPair: keypair, shareId: first.shareId, }) await fsInstance.mountPrivateNode({ - path: Path.directory('second'), + path: ['second'], exchangeKeyPair: keypair, }) const contents = await fsInstance.read( - Path.file('private', 'first', 'nested', 'file'), + Path.priv('first', 'nested', 'file'), 'utf8' ) assert.equal(contents, '🔑') - const contents2 = await fsInstance.read( - Path.file('private', 'second', 'file'), - 'utf8' - ) + const contents2 = await fsInstance.read(Path.priv('second', 'file'), 'utf8') assert.equal(contents2, '🔒') }) @@ -1480,7 +1303,7 @@ describe('File System Class', () => { }) const { capsuleKey, dataRoot } = await fs.write( - Path.file('private', 'file'), + Path.priv('file'), 'utf8', '👀' ) @@ -1494,7 +1317,7 @@ describe('File System Class', () => { capsuleKey, }) - const contents = await fs.read(Path.file('private', 'file'), 'utf8') + const contents = await fs.read(Path.priv('file'), 'utf8') assert.equal(contents, '👀') }) diff --git a/packages/nest/test/helpers/index.ts b/packages/nest/test/helpers/index.ts index ef590d5..6ef62ff 100644 --- a/packages/nest/test/helpers/index.ts +++ b/packages/nest/test/helpers/index.ts @@ -13,32 +13,33 @@ import * as Path from '../../src/path.js' // PATHS +/** + * + * @param partition + */ export function arbitraryDirectoryPath

( partition: P -): fc.Arbitrary>> { +): fc.Arbitrary> { return fc .array(arbitraryPathSegment(), { minLength: 1, maxLength: 8 }) - .map((array) => { - const path: Path.Directory> = { - directory: [partition, ...array] as any, - } - return path - }) + .map((array) => [partition, ...array] as Path.PartitionedNonEmpty

) } +/** + * + * @param partition + */ export function arbitraryFilePath

( partition: P -): fc.Arbitrary>> { +): fc.Arbitrary> { return fc .array(arbitraryPathSegment(), { minLength: 1, maxLength: 8 }) - .map((array) => { - const path: Path.File> = { - file: [partition, ...array] as any, - } - return path - }) + .map((array) => [partition, ...array] as Path.PartitionedNonEmpty

) } +/** + * + */ export function arbitraryPathSegment(): fc.Arbitrary { return fc.oneof( fc.webSegment().filter((segment) => segment.length > 0), @@ -48,10 +49,17 @@ export function arbitraryPathSegment(): fc.Arbitrary { // UNIX +/** + * + * @param opts + * @param opts.blockstore + * @param fs + * @param path + */ export async function assertUnixFsDirectory( opts: { blockstore: Blockstore }, fs: FileSystem, - path: Path.Directory> + path: Path.Partitioned ): Promise { const dataRoot = await fs.calculateDataRoot() @@ -69,10 +77,18 @@ export async function assertUnixFsDirectory( assert.equal(entry.type, 'directory') } +/** + * + * @param opts + * @param opts.blockstore + * @param fs + * @param path + * @param bytes + */ export async function assertUnixFsFile( opts: { blockstore: Blockstore }, fs: FileSystem, - path: Path.File>, + path: Path.Partitioned, bytes: Uint8Array ): Promise { const dataRoot = await fs.calculateDataRoot() @@ -94,10 +110,17 @@ export async function assertUnixFsFile( assert.equal(Uint8Arrays.equals(unixBytes, bytes), true) } +/** + * + * @param opts + * @param opts.blockstore + * @param fs + * @param path + */ export async function assertUnixNodeRemoval( opts: { blockstore: Blockstore }, fs: FileSystem, - path: Path.Distinctive> + path: Path.Partitioned ): Promise { const dataRoot = await fs.calculateDataRoot() diff --git a/packages/nest/test/path.test.ts b/packages/nest/test/path.test.ts deleted file mode 100644 index 7b33336..0000000 --- a/packages/nest/test/path.test.ts +++ /dev/null @@ -1,377 +0,0 @@ -import { strict as assert } from 'assert' -import * as fc from 'fast-check' - -import type { DirectoryPath, FilePath } from '../src/path.js' -import * as Path from '../src/path.js' -import { RootBranch } from '../src/path.js' - -describe('Path functions', () => { - // CREATION - - it('creates directory paths', () => { - fc.assert( - fc.property(fc.array(fc.hexaString()), (data) => { - assert.deepEqual(Path.directory(...data), { directory: data }) - }) - ) - - assert.throws(() => Path.directory('/')) - - // Type testing - const _a: Path.Directory> = - Path.directory('private') - const _b: Path.Directory> = - Path.directory('public', 'a') - const _c: Path.Directory = Path.directory( - 'private', - 'a', - 'b' - ) - }) - - it('creates file paths', () => { - fc.assert( - fc.property(fc.array(fc.hexaString()), (data) => { - assert.deepEqual(Path.file(...data), { file: data }) - }) - ) - - assert.throws(() => Path.file('/')) - - // Type testing - const _a: Path.File> = Path.file( - 'private', - 'a' - ) - const _b: Path.File = Path.file('private', 'a', 'b') - }) - - it('creates directory paths with fromKind', () => { - fc.assert( - fc.property(fc.array(fc.hexaString()), (data) => { - assert.deepEqual(Path.fromKind(Path.Kind.Directory, ...data), { - directory: data, - }) - }) - ) - - // Type testing - const _a: Path.Directory> = Path.fromKind( - Path.Kind.Directory, - 'private' - ) - const _b: Path.Directory> = - Path.fromKind(Path.Kind.Directory, 'public', 'a') - const _c: Path.Directory = Path.fromKind( - Path.Kind.Directory, - 'private', - 'a', - 'b' - ) - }) - - it('creates file paths with fromKind', () => { - fc.assert( - fc.property(fc.array(fc.hexaString()), (data) => { - assert.deepEqual(Path.fromKind(Path.Kind.File, ...data), { file: data }) - }) - ) - - // Type testing - const _a: Path.File> = Path.fromKind( - Path.Kind.File, - 'private', - 'a' - ) - const _b: Path.File = Path.fromKind( - Path.Kind.File, - 'private', - 'a', - 'b' - ) - }) - - // POSIX - - it('creates a path from a POSIX formatted string', () => { - assert.deepEqual(Path.fromPosix('foo/bar/'), { directory: ['foo', 'bar'] }) - - assert.deepEqual(Path.fromPosix('/foo/bar/'), { directory: ['foo', 'bar'] }) - - assert.deepEqual(Path.fromPosix('/'), { directory: [] }) - - assert.deepEqual(Path.fromPosix('foo/bar'), { file: ['foo', 'bar'] }) - - assert.deepEqual(Path.fromPosix('/foo/bar'), { file: ['foo', 'bar'] }) - }) - - it('converts a path to the POSIX format', () => { - assert.equal(Path.toPosix({ directory: ['foo', 'bar'] }), 'foo/bar/') - - assert.equal(Path.toPosix({ directory: [] }), '') - - assert.equal(Path.toPosix({ file: ['foo', 'bar'] }), 'foo/bar') - }) - - // 🛠 - - it('can create app-data paths', () => { - const appInfo = { - name: 'Tests', - creator: 'WNFS WG', - } - - const root: DirectoryPath> = - Path.appData('private', appInfo) - - assert.deepEqual(root, { - directory: [RootBranch.Private, 'Apps', appInfo.creator, appInfo.name], - }) - - const dir: DirectoryPath> = - Path.appData('private', appInfo, Path.directory('a')) - - assert.deepEqual(dir, { - directory: [ - RootBranch.Private, - 'Apps', - appInfo.creator, - appInfo.name, - 'a', - ], - }) - - const file: FilePath> = Path.appData( - 'public', - appInfo, - Path.file('a') - ) - - assert.deepEqual(file, { - file: [RootBranch.Public, 'Apps', appInfo.creator, appInfo.name, 'a'], - }) - }) - - it('can be combined', () => { - const dir: DirectoryPath = Path.combine( - Path.directory('a'), - Path.directory('b') - ) - - assert.deepEqual(dir, { directory: ['a', 'b'] }) - - const file: FilePath = Path.combine( - Path.directory('a'), - Path.file('b') - ) - - assert.deepEqual(file, { file: ['a', 'b'] }) - - // Type testing - const _a: DirectoryPath> = - Path.combine(Path.directory('private'), Path.directory('a')) - - const _aa: FilePath> = Path.combine( - Path.directory('public'), - Path.file('a') - ) - - const _b: DirectoryPath> = Path.combine( - Path.directory('private'), - Path.directory() - ) - - const _bb: FilePath> = Path.combine( - Path.directory('public'), - Path.file() - ) - - const _c: DirectoryPath> = - Path.combine(Path.directory('private'), Path.directory('a')) - - const _cc: FilePath> = Path.combine( - Path.directory('public'), - Path.file('a') - ) - }) - - it('supports isOnRootBranch', () => { - assert.equal( - Path.isOnRootBranch( - RootBranch.Private, - Path.directory(RootBranch.Private, 'a') - ), - true - ) - - assert.equal( - Path.isOnRootBranch( - RootBranch.Public, - Path.directory(RootBranch.Private, 'a') - ), - false - ) - }) - - it('supports isDirectory', () => { - assert.equal(Path.isDirectory(Path.directory(RootBranch.Private)), true) - - assert.equal(Path.isDirectory(Path.file('foo')), false) - }) - - it('supports isFile', () => { - assert.equal(Path.isFile(Path.file('foo')), true) - - assert.equal(Path.isFile(Path.directory(RootBranch.Private)), false) - }) - - it('supports isRootDirectory', () => { - assert.equal(Path.isRootDirectory(Path.root()), true) - - assert.equal(Path.isRootDirectory(Path.directory()), true) - - assert.equal( - Path.isRootDirectory(Path.directory(RootBranch.Private)), - false - ) - }) - - it('supports isSamePartition', () => { - assert.equal( - Path.isSamePartition( - Path.directory(RootBranch.Private), - Path.directory(RootBranch.Private) - ), - true - ) - - assert.equal( - Path.isSamePartition( - Path.directory(RootBranch.Private), - Path.directory(RootBranch.Public) - ), - false - ) - }) - - it('supports isSameKind', () => { - assert.equal(Path.isSameKind(Path.directory(), Path.file()), false) - - assert.equal(Path.isSameKind(Path.file(), Path.directory()), false) - - assert.equal(Path.isSameKind(Path.directory(), Path.directory()), true) - - assert.equal(Path.isSameKind(Path.file(), Path.file()), true) - }) - - it('has kind', () => { - assert.equal(Path.kind(Path.directory()), Path.Kind.Directory) - - assert.equal(Path.kind(Path.file()), Path.Kind.File) - }) - - it('supports map', () => { - assert.deepEqual( - Path.map((p) => [...p, 'bar'], Path.directory('foo')), // eslint-disable-line unicorn/no-array-method-this-argument - { directory: ['foo', 'bar'] } - ) - - assert.deepEqual( - Path.map((p) => [...p, 'bar'], Path.file('foo')), // eslint-disable-line unicorn/no-array-method-this-argument - { file: ['foo', 'bar'] } - ) - }) - - it('supports parent', () => { - assert.deepEqual(Path.parent(Path.directory('foo')), Path.root()) - - assert.deepEqual(Path.parent(Path.file('foo')), Path.root()) - - assert.equal(Path.parent(Path.root()), undefined) - - // Type testing - const _a: DirectoryPath> = - Path.parent({ - directory: ['private', 'a', 'b'], - }) - - const _a_: DirectoryPath = Path.parent({ - directory: ['random', 'a', 'b'], - }) - - const _b: DirectoryPath> = Path.parent({ - directory: ['private', 'a'], - }) - - const _b_: DirectoryPath = Path.parent({ - directory: ['random', 'a'], - }) - - const _c: DirectoryPath = Path.parent({ - directory: ['private'], - }) - - const _c_: DirectoryPath = Path.parent({ - directory: ['random'], - }) - - // const _x: undefined = Path.parent({ - // directory: [], - // }) - }) - - it('supports removePartition', () => { - assert.deepEqual(Path.removePartition(Path.directory('foo')), { - directory: [], - }) - - assert.deepEqual( - Path.removePartition(Path.directory('foo', 'bar')), - Path.directory('bar') - ) - }) - - it('supports replaceTerminus', () => { - assert.deepEqual( - Path.replaceTerminus(Path.file('private', 'a', 'b'), 'c'), - Path.file('private', 'a', 'c') - ) - - // Type testing - const _a: DirectoryPath> = - Path.replaceTerminus( - { - directory: ['private', 'a'], - }, - 'b' - ) - - const _b: FilePath> = - Path.replaceTerminus( - { - file: ['private', 'a'], - }, - 'b' - ) - - const _c: DirectoryPath = Path.replaceTerminus( - { - directory: ['a'], - }, - 'b' - ) - - const _d: FilePath = Path.replaceTerminus( - { - file: ['a'], - }, - 'b' - ) - }) - - it('correctly unwraps', () => { - assert.deepEqual(Path.unwrap(Path.directory('foo')), ['foo']) - - assert.deepEqual(Path.unwrap(Path.file('foo')), ['foo']) - }) -}) From 748bd2f7d151b79e550932eebc8e7c69cbc497da Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Fri, 5 Apr 2024 14:35:30 +0200 Subject: [PATCH 7/7] chore: Update lock file --- pnpm-lock.yaml | 274 +------------------------------------------------ 1 file changed, 2 insertions(+), 272 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc58702..a89ca5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,34 +116,6 @@ importers: specifier: ^3.3.0 version: 3.3.0(vite@5.2.2) - examples/passkey: - dependencies: - '@wnfs-wg/nest': - specifier: '*' - version: link:../../packages/nest - idb-keyval: - specifier: ^6.2.1 - version: 6.2.1 - iso-base: - specifier: ^4.0.0 - version: 4.0.0 - iso-passkeys: - specifier: ^0.2.2 - version: 0.2.2 - devDependencies: - '@rsbuild/core': - specifier: ^0.5.2 - version: 0.5.2 - '@types/node': - specifier: ^20.11.30 - version: 20.11.30 - '@types/qrcode': - specifier: ^1.5.5 - version: 1.5.5 - typescript: - specifier: 5.4.3 - version: 5.4.3 - examples/web3storage: dependencies: '@picocss/pico': @@ -486,54 +458,6 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true - /@cbor-extract/cbor-extract-darwin-arm64@2.2.0: - resolution: {integrity: sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@cbor-extract/cbor-extract-darwin-x64@2.2.0: - resolution: {integrity: sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@cbor-extract/cbor-extract-linux-arm64@2.2.0: - resolution: {integrity: sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@cbor-extract/cbor-extract-linux-arm@2.2.0: - resolution: {integrity: sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@cbor-extract/cbor-extract-linux-x64@2.2.0: - resolution: {integrity: sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@cbor-extract/cbor-extract-win32-x64@2.2.0: - resolution: {integrity: sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - /@chainsafe/is-ip@2.0.2: resolution: {integrity: sha512-ndGqEMG1W5WkGagaqOZHpPU172AGdxr+LD15sv3WIUvT5oCFUrG1Y0CW/v2Egwj4JXEvSibaIIIqImsm98y1nA==} @@ -1454,10 +1378,6 @@ packages: resolution: {integrity: sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==} dev: false - /@noble/ed25519@2.1.0: - resolution: {integrity: sha512-KM4qTyXPinyCgMzeYJH/UudpdL+paJXtY3CHtHYZQtBkS8MZoPr4rOikZllIutJe0d06QDQKisyn02gxZ8TcQA==} - dev: false - /@noble/hashes@1.3.3: resolution: {integrity: sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==} engines: {node: '>= 16'} @@ -1489,33 +1409,6 @@ packages: fastq: 1.17.1 dev: true - /@peculiar/asn1-ecc@2.3.8: - resolution: {integrity: sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ==} - dependencies: - '@peculiar/asn1-schema': 2.3.8 - '@peculiar/asn1-x509': 2.3.8 - asn1js: 3.0.5 - tslib: 2.6.2 - dev: false - - /@peculiar/asn1-schema@2.3.8: - resolution: {integrity: sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==} - dependencies: - asn1js: 3.0.5 - pvtsutils: 1.3.5 - tslib: 2.6.2 - dev: false - - /@peculiar/asn1-x509@2.3.8: - resolution: {integrity: sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw==} - dependencies: - '@peculiar/asn1-schema': 2.3.8 - asn1js: 3.0.5 - ipaddr.js: 2.1.0 - pvtsutils: 1.3.5 - tslib: 2.6.2 - dev: false - /@perma/map@1.0.3: resolution: {integrity: sha512-Bf5njk0fnJGTFE2ETntq0N1oJ6YdCPIpTDn3R3KYZJQdeYSOCNL7mBrFlGnbqav8YQhJA/p81pvHINX9vAtHkQ==} dependencies: @@ -2047,10 +1940,6 @@ packages: resolution: {integrity: sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==} dev: false - /@types/retry@0.12.2: - resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} - dev: false - /@types/semver@7.5.8: resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} dev: true @@ -2732,15 +2621,6 @@ packages: engines: {node: '>=8'} dev: true - /asn1js@3.0.5: - resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} - engines: {node: '>=12.0.0'} - dependencies: - pvtsutils: 1.3.5 - pvutils: 1.1.3 - tslib: 2.6.2 - dev: false - /assert@2.1.0: resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} dependencies: @@ -2936,28 +2816,6 @@ packages: resolution: {integrity: sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==} dev: true - /cbor-extract@2.2.0: - resolution: {integrity: sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==} - hasBin: true - requiresBuild: true - dependencies: - node-gyp-build-optional-packages: 5.1.1 - optionalDependencies: - '@cbor-extract/cbor-extract-darwin-arm64': 2.2.0 - '@cbor-extract/cbor-extract-darwin-x64': 2.2.0 - '@cbor-extract/cbor-extract-linux-arm': 2.2.0 - '@cbor-extract/cbor-extract-linux-arm64': 2.2.0 - '@cbor-extract/cbor-extract-linux-x64': 2.2.0 - '@cbor-extract/cbor-extract-win32-x64': 2.2.0 - dev: false - optional: true - - /cbor-x@1.5.9: - resolution: {integrity: sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ==} - optionalDependencies: - cbor-extract: 2.2.0 - dev: false - /cborg@4.1.3: resolution: {integrity: sha512-I8sAcVtiarz0dZ4IYixNUaL2hIl9cMDjo1ytI57F5fUlekTEO5Im8aXbAvsuayeP76hHSPRMwos0AUuntHJjqQ==} hasBin: true @@ -3148,21 +3006,6 @@ packages: semver: 7.6.0 dev: false - /conf@12.0.0: - resolution: {integrity: sha512-fIWyWUXrJ45cHCIQX+Ck1hrZDIf/9DR0P0Zewn3uNht28hbt5OfGUq8rRWsxi96pZWPyBEd0eY9ama01JTaknA==} - engines: {node: '>=18'} - dependencies: - ajv: 8.12.0 - ajv-formats: 2.1.1(ajv@8.12.0) - atomically: 2.0.2 - debounce-fn: 5.1.2 - dot-prop: 8.0.2 - env-paths: 3.0.0 - json-schema-typed: 8.0.1 - semver: 7.6.0 - uint8array-extras: 0.3.0 - dev: false - /convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true @@ -3314,11 +3157,6 @@ packages: object-keys: 1.1.1 dev: true - /delay@6.0.0: - resolution: {integrity: sha512-2NJozoOHQ4NuZuVIr5CWd0iiLVIRSDepakaovIN+9eIDHEhdCAEvSy2cuf1DCrPPQLvHmbqTHODlhHg8UCy4zw==} - engines: {node: '>=16'} - dev: false - /depcheck@1.4.7: resolution: {integrity: sha512-1lklS/bV5chOxwNKA/2XUUk/hPORp8zihZsXflr8x0kLwmcZ9Y9BsS6Hs3ssvA+2wUVbG0U2Ciqvm1SokNjPkA==} engines: {node: '>=10'} @@ -3360,17 +3198,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} - engines: {node: '>=8'} - requiresBuild: true - dev: false - optional: true - - /did-resolver@4.1.0: - resolution: {integrity: sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA==} - dev: false - /diff@5.0.0: resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} engines: {node: '>=0.3.1'} @@ -3441,13 +3268,6 @@ packages: type-fest: 2.19.0 dev: false - /dot-prop@8.0.2: - resolution: {integrity: sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ==} - engines: {node: '>=16'} - dependencies: - type-fest: 3.13.1 - dev: false - /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true @@ -4831,11 +4651,6 @@ packages: side-channel: 1.0.6 dev: true - /ipaddr.js@2.1.0: - resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==} - engines: {node: '>= 10'} - dev: false - /ipfs-unixfs-exporter@13.5.0: resolution: {integrity: sha512-s1eWXzoyhQFNEAB1p+QE3adjhW+lBdgpORmmjiCLiruHs5z7T5zsAgRVcWpM8LWYhq2flRtJHObb7Hg73J+oLQ==} dependencies: @@ -5081,11 +4896,6 @@ packages: engines: {node: '>= 0.4'} dev: true - /is-network-error@1.1.0: - resolution: {integrity: sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==} - engines: {node: '>=16'} - dev: false - /is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -5202,54 +5012,11 @@ packages: bigint-mod-arith: 3.3.1 dev: false - /iso-base@4.0.0: - resolution: {integrity: sha512-Vf+6r7XlP+LQD0HSR0ZBLSj/mwuP+59ElbgMKT+iFSnkBW2RzRohQScgKfPCYbaGZAjEdbi40eYq28E/RAnrVA==} - dependencies: - base-x: 4.0.0 - bigint-mod-arith: 3.3.1 - dev: false - - /iso-did@1.6.0: - resolution: {integrity: sha512-DBq2MzTMGMJgYs/3jhj+h/EuDuQs7NWUYwNtXpBgbTRl6CVqsu0nAHqzxIzaZYJbb05msnq7KUwMCZmLCQ9asA==} - dependencies: - did-resolver: 4.1.0 - iso-base: 2.0.1 - iso-web: 1.0.5 - multiformats: 13.1.0 - dev: false - - /iso-kv@3.0.2: - resolution: {integrity: sha512-DL4TNf1SRVskOKRsEk2QjMHsLUWh1H+iV4LPc9dMDfi1wcb/HlkGl+9ETphk3FN5ToL6l6CUKdeHGYyOPdmMHg==} - dependencies: - conf: 12.0.0 - idb-keyval: 6.2.1 - kysely: 0.27.3 - dev: false - - /iso-passkeys@0.2.2: - resolution: {integrity: sha512-CsuztK2+FOmlq7LVNhwLYiDDeVzBqCL/iHFWzpY8YhiNgiBMVi8Vtxb5pev780QWgKKh3XmLPPH/rgaZ2XuIPQ==} - dependencies: - '@noble/ed25519': 2.1.0 - '@peculiar/asn1-ecc': 2.3.8 - '@peculiar/asn1-schema': 2.3.8 - cbor-x: 1.5.9 - iso-base: 2.0.1 - iso-did: 1.6.0 - dev: false - /iso-url@1.2.1: resolution: {integrity: sha512-9JPDgCN4B7QPkLtYAAOrEuAWvP9rWvR5offAr0/SeF046wIkglqH3VXgYYP6NcsKslH80UIVgmPqNe3j7tG2ng==} engines: {node: '>=12'} dev: false - /iso-web@1.0.5: - resolution: {integrity: sha512-ZRZ5BgGAvwau+WtNLZXp1byawaMPFUFnTwVHLXKFXIKxz0CD9hkEkSe505kxcNEui0TqVEQBOwg4806au1djGg==} - dependencies: - delay: 6.0.0 - iso-kv: 3.0.2 - p-retry: 6.2.0 - dev: false - /iso-websocket@0.2.0: resolution: {integrity: sha512-imBalzmPSq0C9CfMouimB2kZ5X1qS4Yai8kGTQdluGRb0T0iu+BkPcakFelh4FIlTM8y6+BNuCEGog3lf8HC4A==} dependencies: @@ -5509,11 +5276,6 @@ packages: engines: {node: '>=6'} dev: true - /kysely@0.27.3: - resolution: {integrity: sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==} - engines: {node: '>=14.0.0'} - dev: false - /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -5907,15 +5669,6 @@ packages: whatwg-url: 5.0.0 dev: false - /node-gyp-build-optional-packages@5.1.1: - resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} - hasBin: true - requiresBuild: true - dependencies: - detect-libc: 2.0.3 - dev: false - optional: true - /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true @@ -6156,15 +5909,6 @@ packages: retry: 0.13.1 dev: false - /p-retry@6.2.0: - resolution: {integrity: sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==} - engines: {node: '>=16.17'} - dependencies: - '@types/retry': 0.12.2 - is-network-error: 1.1.0 - retry: 0.13.1 - dev: false - /p-timeout@6.1.2: resolution: {integrity: sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==} engines: {node: '>=14.16'} @@ -6429,17 +6173,6 @@ packages: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} dev: true - /pvtsutils@1.3.5: - resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==} - dependencies: - tslib: 2.6.2 - dev: false - - /pvutils@1.1.3: - resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} - engines: {node: '>=6.0.0'} - dev: false - /qrcode@1.5.3: resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==} engines: {node: '>=10.13.0'} @@ -7171,6 +6904,7 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: true /tsutils-etc@1.4.2(tsutils@3.21.0)(typescript@5.3.3): resolution: {integrity: sha512-2Dn5SxTDOu6YWDNKcx1xu2YUy6PUeKrWZB/x2cQ8vY2+iz3JRembKn/iZ0JLT1ZudGNwQQvtFX9AwvRHbXuPUg==} @@ -7240,6 +6974,7 @@ packages: /type-fest@3.13.1: resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} engines: {node: '>=14.16'} + dev: true /type-fest@4.13.1: resolution: {integrity: sha512-ASMgM+Vf2cLwDMt1KXSkMUDSYCxtckDJs8zsaVF/mYteIsiARKCVtyXtcK38mIKbLTctZP8v6GMqdNaeI3fo7g==} @@ -7357,11 +7092,6 @@ packages: uint8arraylist: 2.4.8 uint8arrays: 5.0.3 - /uint8array-extras@0.3.0: - resolution: {integrity: sha512-erJsJwQ0tKdwuqI0359U8ijkFmfiTcq25JvvzRVc1VP+2son1NJRXhxcAKJmAW3ajM8JSGAfsAXye8g4s+znxA==} - engines: {node: '>=18'} - dev: false - /uint8arraylist@2.4.8: resolution: {integrity: sha512-vc1PlGOzglLF0eae1M8mLRTBivsvrGsdmJ5RbK3e+QRvRLOZfZhQROTwH/OfyF3+ZVUg9/8hE8bmKP2CvP9quQ==} dependencies: