diff --git a/src/auth.ts b/src/auth.ts index 15b8648d..da1332d2 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -2,31 +2,37 @@ import { Cabinet } from "./repositories/cabinet.js" import { Account, Agent, Authority, Identifier } from "./components.js" import { AnnexParentType } from "./components/account/implementation.js" +import { Names } from "./repositories/names.js" ////////////// // REGISTER // ////////////// export const register = ( - { account, agent, authority, identifier, cabinet }: { + { account, agent, authority, identifier, cabinet, names }: { account: Account.Implementation agent: Agent.Implementation authority: Authority.Implementation identifier: Identifier.Implementation cabinet: Cabinet + names: Names } ) => async (formValues: Record): Promise< { registered: true } | { registered: false; reason: string } > => { - // Do delegation from identifier to agent - const agentDelegation = await authority.clerk.tickets.misc.identifierToAgentDelegation(identifier, agent) - await cabinet.addTicket("misc", agentDelegation) - // Call account register implementation - const result = await account.register(formValues, agentDelegation) + const result = await account.register(identifier, names, formValues) if (result.registered) { + // Do delegation from identifier to agent + const agentDelegation = await authority.clerk.tickets.misc.identifierToAgentDelegation( + identifier, + agent, + result.tickets + ) + + await cabinet.addTicket("agent", agentDelegation) await cabinet.addTickets("account", result.tickets) return { registered: true } } else { diff --git a/src/authority/query.ts b/src/authority/query.ts index 0a21ef3f..78dc0d2b 100644 --- a/src/authority/query.ts +++ b/src/authority/query.ts @@ -1,4 +1,4 @@ -import { isString } from "../common/type-checks.js" +import { isObject, isString } from "../common/type-checks.js" import * as Path from "../path/index.js" //////// @@ -22,7 +22,7 @@ export type FileSystemAbility = typeof ALLOWED_FILE_SYSTEM_ABILITIES[number] export type FileSystemQuery = { query: "fileSystem" ability: FileSystemAbility - did: string + id: { did: string } | { name: string } path: Path.Distinctive> } @@ -35,28 +35,28 @@ export const account: AccountQuery = { } export const fileSystem = { - rootAccess(did: string): FileSystemQuery[] { + rootAccess(id: { did: string } | { name: string }): FileSystemQuery[] { return [{ query: "fileSystem", ability: "*", - did, + id, path: Path.directory("public"), }, { query: "fileSystem", ability: "*", - did, + id, path: Path.directory("private"), }] }, limitedAccess( ability: typeof ALLOWED_FILE_SYSTEM_ABILITIES[number], - did: string, + id: { did: string } | { name: string }, path: Path.Distinctive> ): FileSystemQuery { return { query: "fileSystem", - did, ability, + id, path, } }, @@ -77,7 +77,7 @@ export function isContained({ parent, child }: { parent: Query; child: Query }): ? true : (child.ability === "*" ? false : parent.ability === child.ability) - const did = parent.did === child.did + const id = JSON.stringify(parent.id) === JSON.stringify(child.id) const unwrappedParentPath = Path.unwrap(parent.path) const path = Path.unwrap(child.path).reduce( @@ -89,7 +89,7 @@ export function isContained({ parent, child }: { parent: Query; child: Query }): true ) - return ability && did && path + return ability && id && path } // ? @@ -139,14 +139,20 @@ function fileSystemQueryFromJSON(obj: Record): FileSystemQuery throw new Error(`Expected a path with a partition (private or public), got: ${obj.path}`) } - if (!isString(obj.did)) { - throw new Error("Expected a `did` property") + if (!isObject(obj.id)) { + throw new Error("Expected a `id` object") } + if (!isString(obj.id.did) || !isString(obj.id.name)) { + throw new Error("Expected the `id` object to have a `did` or a `name` property") + } + + obj.id.did + return { query: "fileSystem", ability: obj.ability as FileSystemAbility, - did: obj.did, + id: obj.id.did ? { did: obj.id.did } : { name: obj.id.name }, path: partitionedPath, } } @@ -162,7 +168,7 @@ export function toJSON(query: Query): object { return { query: query.query, ability: query.ability, - did: query.did, + id: query.id, path: Path.toPosix(query.path), } } diff --git a/src/components/account/fission/data-root.ts b/src/components/account/fission/data-root.ts index 048c3fc8..27c9d72d 100644 --- a/src/components/account/fission/data-root.ts +++ b/src/components/account/fission/data-root.ts @@ -103,7 +103,7 @@ export async function update( }, proofs: await Promise.all(proofs.map( - async proof => ticketCID(proof).toString() + async proof => (await ticketCID(proof)).toString() )), }) ) diff --git a/src/components/account/fission/implementations/common.ts b/src/components/account/fission/implementations/common.ts index d1532ec1..75615634 100644 --- a/src/components/account/fission/implementations/common.ts +++ b/src/components/account/fission/implementations/common.ts @@ -5,10 +5,21 @@ import * as Identifier from "../../../identifier/implementation.js" import { AccountQuery } from "../../../../authority/query.js" import { CID } from "../../../../common/index.js" import { Agent, DNS, Manners } from "../../../../components.js" +import { FileSystemCarrier } from "../../../../fs/types.js" import { Inventory } from "../../../../inventory.js" +import { Names } from "../../../../repositories/names.js" import { Ticket } from "../../../../ticket/types.js" import { DataRoot, lookupUserDID } from "../index.js" +//////// +// 🏔️ // +//////// + +export const NAMES = { + fileSystem(identifierDID: string) { + return `ACCOUNT_FILE_SYSTEM_DID#${identifierDID}` + }, +} //////// // 🧩 // //////// @@ -20,14 +31,7 @@ export type Annex = { * This method can be used to load a local-only file system before an account is registered. * When you register an account, the file system will sync with the Fission server, making it available through Fission IPFS nodes. */ - volume: (username?: string) => Promise<{ - dataRoot?: CID - dataRootUpdater: ( - dataRoot: CID, - proofs: Ticket[] - ) => Promise<{ updated: true } | { updated: false; reason: string }> - did: string - }> + volume: (username?: string) => Promise } export type Dependencies = { @@ -37,31 +41,26 @@ export type Dependencies = { manners: Manners.Implementation } -/////////////// -// DATA ROOT // -/////////////// +///////////////// +// FILE SYSTEM // +///////////////// export async function volume( endpoints: Fission.Endpoints, dependencies: Dependencies, identifier: Identifier.Implementation, - tickets: Inventory, + inventory: Inventory, + names: Names, username?: string -): Promise<{ - dataRoot?: CID - dataRootUpdater: ( - dataRoot: CID, - proofs: Ticket[] - ) => Promise<{ updated: true } | { updated: false; reason: string }> - did: string -}> { - const accountProof = findAccountProofTicket(await identifier.did(), tickets) +): Promise { + const accountProof = findAccountProofTicket(await identifier.did(), inventory) const accountUsername = accountProof ? findUsernameFact(accountProof) : null if (username && username !== accountUsername) { - return otherVolume(endpoints, dependencies, identifier, tickets, username) + throw new Error("Loading a volume of another user is currently disabled") + // TODO: return otherVolume(endpoints, dependencies, identifier, inventory, username) } else { - return accountVolume(endpoints, dependencies, identifier, tickets) + return accountVolume(endpoints, dependencies, identifier, inventory, names) } } @@ -69,42 +68,53 @@ export async function accountVolume( endpoints: Fission.Endpoints, dependencies: Dependencies, identifier: Identifier.Implementation, - tickets: Inventory -): Promise<{ - dataRoot?: CID - dataRootUpdater: ( - dataRoot: CID, - proofs: Ticket[] - ) => Promise<{ updated: true } | { updated: false; reason: string }> - did: string -}> { + inventory: Inventory, + names: Names +): Promise { const dataRootUpdater = async (dataRoot: CID, proofs: Ticket[]) => { - const { suffices } = await hasSufficientAuthority(dependencies, identifier, tickets) + const { suffices } = await hasSufficientAuthority(dependencies, identifier, inventory) if (!suffices) return { updated: false, reason: "Not authenticated yet, lacking authority." } - return updateDataRoot(endpoints, dependencies, identifier, tickets, dataRoot, proofs) + return updateDataRoot(endpoints, dependencies, identifier, inventory, dataRoot, proofs) } - const { suffices } = await hasSufficientAuthority(dependencies, identifier, tickets) + const { suffices } = await hasSufficientAuthority(dependencies, identifier, inventory) const identifierDID = await identifier.did() if (!suffices) { - return { - dataRoot: undefined, - dataRootUpdater, - did: identifierDID, - } + const name = NAMES.fileSystem(identifierDID) + const did = names.subject(name) + + console.log(name, did) + + return did + ? { dataRootUpdater, id: { did } } + : { dataRootUpdater, id: { name } } + } + + // Find account-proof UCAN + const accountProof = findAccountProofTicket(identifierDID, inventory) + + if (!accountProof) { + throw new Error("Expected to find account proof") + } + + const name = NAMES.fileSystem(accountProof.audience) + const did = names.subject(name) + + if (!did) { + // futile, because a file system should not be loaded in this state. + return { dataRootUpdater, futile: true, id: { name } } } if (!dependencies.manners.program.online()) { return { dataRoot: undefined, dataRootUpdater, - did: await fileSystemDID(dependencies, identifier, tickets) || identifierDID, + id: { did }, } } - // Find account-proof UCAN - const accountProof = findAccountProofTicket(identifierDID, tickets) + // TODO: Keep username in `names` as well const username = accountProof ? findUsernameFact(accountProof) : null if (!username) { @@ -116,43 +126,39 @@ export async function accountVolume( return { dataRoot: await DataRoot.lookup(endpoints, dependencies, username).then(a => a || undefined), dataRootUpdater, - did: await fileSystemDID(dependencies, identifier, tickets) || identifierDID, + id: { did }, } } -export async function otherVolume( - endpoints: Fission.Endpoints, - dependencies: Dependencies, - identifier: Identifier.Implementation, - inventory: Inventory, - username: string -): Promise<{ - dataRoot?: CID - dataRootUpdater: ( - dataRoot: CID, - proofs: Ticket[] - ) => Promise<{ updated: true } | { updated: false; reason: string }> - did: string -}> { - if (!dependencies.manners.program.online()) { - throw new Error("Cannot load another user's volume while offline") - } - - const userDID = await lookupUserDID(endpoints, dependencies.dns, username) - if (!userDID) throw new Error("User not found") - - const dataRootUpdater = async (dataRoot: CID, proofs: Ticket[]) => { - // TODO: Add ability to update data root of another user - // For this we need the account capability to update the volume pointer. - throw new Error("Not implemented yet") - } - - return { - dataRoot: await DataRoot.lookup(endpoints, dependencies, username).then(a => a || undefined), - dataRootUpdater, - did: userDID, - } -} +// TODO: Disabled because it isn't using the correct DID +// +// export async function otherVolume( +// endpoints: Fission.Endpoints, +// dependencies: Dependencies, +// identifier: Identifier.Implementation, +// inventory: Inventory, +// username: string +// ): Promise { +// if (!dependencies.manners.program.online()) { +// throw new Error("Cannot load another user's volume while offline") +// } +// +// const userDID = await lookupUserDID(endpoints, dependencies.dns, username) +// if (!userDID) throw new Error("User not found") +// +// const dataRootUpdater = async (dataRoot: CID, proofs: Ticket[]) => { +// // TODO: Add ability to update data root of another user +// // For this we need the account capability to update the volume pointer. +// throw new Error("Not implemented yet") +// } +// +// return { +// dataRoot: await DataRoot.lookup(endpoints, dependencies, username).then(a => a || undefined), +// dataRootUpdater, +// // FIXME: This DID is not correct! +// did: userDID, +// } +// } export async function updateDataRoot( endpoints: Fission.Endpoints, @@ -273,16 +279,16 @@ export function findAccountProofTicket( */ export function findAccountTicket( audience: string, - tickets: Inventory + inventory: Inventory ): Ticket | null { const matcher = (ticket: Ticket) => !!findUsernameFact(ticket) // Grab the UCANs addressed to this audience (ideally current identifier), // then look for the username fact ucan in the delegation chains of those UCANs. - return tickets.lookupTicketByAudience(audience).reduce( + return inventory.lookupTicketByAudience(audience).reduce( (acc: Ticket | null, ticket) => { if (acc) return acc - const hasProof = !!tickets.descendUntilMatchingTicket(ticket, matcher, Ucan.ticketProofResolver) + const hasProof = !!inventory.descendUntilMatchingTicket(ticket, matcher, Ucan.ticketProofResolver) if (hasProof) return ticket return null }, diff --git a/src/components/account/fission/implementations/delegated.ts b/src/components/account/fission/implementations/delegated.ts index bb680577..cf85329d 100644 --- a/src/components/account/fission/implementations/delegated.ts +++ b/src/components/account/fission/implementations/delegated.ts @@ -1,6 +1,8 @@ import * as Fission from "../../../../common/fission.js" +import * as Identifier from "../../../identifier/implementation.js" import * as Common from "./common.js" +import { Names } from "../../../../repositories/names.js" import { Ticket } from "../../../../ticket/types.js" import { Implementation } from "../../implementation.js" import { Annex, Dependencies } from "./common.js" @@ -25,8 +27,9 @@ export async function canRegister( } export async function register( - formValues: Record, - identifierTicket: Ticket + identifier: Identifier.Implementation, + names: Names, + formValues: Record ): Promise< | { registered: true; tickets: Ticket[] } | { registered: false; reason: string } @@ -48,8 +51,8 @@ export function implementation( const endpoints = optionalEndpoints || Fission.PRODUCTION return { - annex: (identifier, ucanDictionary) => ({ - volume: (...args) => Common.volume(endpoints, dependencies, identifier, ucanDictionary, ...args), + annex: (identifier, inventory, names) => ({ + volume: (...args) => Common.volume(endpoints, dependencies, identifier, inventory, names, ...args), }), canRegister, diff --git a/src/components/account/fission/implementations/standard.ts b/src/components/account/fission/implementations/standard.ts index f7e8b3ac..b0c38955 100644 --- a/src/components/account/fission/implementations/standard.ts +++ b/src/components/account/fission/implementations/standard.ts @@ -1,8 +1,9 @@ import * as Fission from "../../../../common/fission.js" import * as Ucan from "../../../../ucan/ts-ucan/index.js" +import * as Identifier from "../../../identifier/implementation.js" import * as Common from "./common.js" -import { AccountQuery } from "../../../../authority/query.js" +import { Names } from "../../../../repositories/names.js" import { Ticket } from "../../../../ticket/types.js" import { Implementation } from "../../implementation.js" import { isUsernameAvailable, isUsernameValid } from "../index.js" @@ -41,7 +42,6 @@ export async function requestVerificationCode( const ucan = await Ucan.build({ audience: await Fission.did(endpoints, dependencies.dns), - // issuer: await Ucan.keyPair(dependencies.agent), FIXME: Should use agent issuer: { did: () => identifierDID, jwtAlg: await dependencies.identifier.ucanAlgorithm(), @@ -141,8 +141,9 @@ export async function canRegister( export async function register( endpoints: Fission.Endpoints, dependencies: Dependencies, - formValues: Record, - identifierTicket: Ticket + identifier: Identifier.Implementation, + names: Names, + formValues: Record ): Promise< | { registered: true; tickets: Ticket[] } | { registered: false; reason: string } @@ -156,9 +157,9 @@ export async function register( } if (formValues.accountType === "app") { - return registerAppAccount(endpoints, dependencies, formValues, identifierTicket) + return registerAppAccount(endpoints, dependencies, identifier, names, formValues) } else if (formValues.accountType === "verified") { - return registerVerifiedAccount(endpoints, dependencies, formValues, identifierTicket) + return registerVerifiedAccount(endpoints, dependencies, identifier, names, formValues) } else { throw new Error("Invalid account type") } @@ -167,8 +168,9 @@ export async function register( async function registerAppAccount( endpoints: Fission.Endpoints, dependencies: Dependencies, - formValues: Record, - identifierTicket: Ticket + identifier: Identifier.Implementation, + names: Names, + formValues: Record ): Promise< | { registered: true; tickets: Ticket[] } | { registered: false; reason: string } @@ -179,8 +181,9 @@ async function registerAppAccount( async function registerVerifiedAccount( endpoints: Fission.Endpoints, dependencies: Dependencies, - formValues: Record, - identifierTicket: Ticket + identifier: Identifier.Implementation, + names: Names, + formValues: Record ): Promise< | { registered: true; tickets: Ticket[] } | { registered: false; reason: string } @@ -191,13 +194,11 @@ async function registerVerifiedAccount( const token = Ucan.encode( await Ucan.build({ audience: await Fission.did(endpoints, dependencies.dns), - // issuer: await Ucan.keyPair(dependencies.agent), FIXME: Should use agent issuer: { did: () => identifierDID, jwtAlg: await dependencies.identifier.ucanAlgorithm(), sign: data => dependencies.identifier.sign(data), }, - // proofs: [Ucan.encode(identifierUcan)], proofs: [], facts: [{ code }], }) @@ -244,9 +245,9 @@ export function implementation( const endpoints = optionalEndpoints || Fission.PRODUCTION return { - annex: (identifier, ucanDictionary) => ({ + annex: (identifier, inventory, names) => ({ requestVerificationCode: (...args) => requestVerificationCode(endpoints, dependencies, ...args), - volume: (...args) => Common.volume(endpoints, dependencies, identifier, ucanDictionary, ...args), + volume: (...args) => Common.volume(endpoints, dependencies, identifier, inventory, names, ...args), }), canRegister: (...args) => canRegister(endpoints, dependencies, ...args), diff --git a/src/components/account/implementation.ts b/src/components/account/implementation.ts index ba3d1ccc..96710015 100644 --- a/src/components/account/implementation.ts +++ b/src/components/account/implementation.ts @@ -1,5 +1,6 @@ import { AccountQuery } from "../../authority/query.js" import { Inventory } from "../../inventory.js" +import { Names } from "../../repositories/names.js" import { Ticket } from "../../ticket/types.js" import * as Identifier from "../identifier/implementation.js" @@ -13,7 +14,7 @@ export type Implementation = { /** * Additional methods you want to be part of `program.account` */ - annex: (identifier: Identifier.Implementation, inventory: Inventory) => Annex + annex: (identifier: Identifier.Implementation, inventory: Inventory, names: Names) => Annex // CREATION @@ -27,7 +28,11 @@ export type Implementation = { /** * How to register an account with this account system. */ - register: (formValues: Record, identifierTicket: Ticket) => Promise< + register: ( + identifier: Identifier.Implementation, + names: Names, + formValues: Record + ) => Promise< { registered: true; tickets: Ticket[] } | { registered: false; reason: string } > @@ -36,7 +41,10 @@ export type Implementation = { /** * The DID associated with this account. */ - did(identifier: Identifier.Implementation, inventory: Inventory): Promise + did( + identifier: Identifier.Implementation, + inventory: Inventory + ): Promise /** * Check if we have everything we need (eg. capabilities) regarding the account. diff --git a/src/components/account/local.ts b/src/components/account/local.ts index c768bc1f..53693346 100644 --- a/src/components/account/local.ts +++ b/src/components/account/local.ts @@ -2,6 +2,7 @@ import * as Identifier from "../identifier/implementation.js" import { AccountQuery } from "../../authority/query.js" import { Inventory } from "../../inventory.js" +import { Names } from "../../repositories/names.js" import { Ticket } from "../../ticket/types.js" import { Implementation } from "./implementation.js" @@ -22,8 +23,9 @@ export async function canRegister(): Promise< } export async function register( - formValues: Record, - identifierTicket: Ticket + identifier: Identifier.Implementation, + names: Names, + formValues: Record ): Promise< { registered: true; tickets: Ticket[] } | { registered: false; reason: string } > { @@ -36,14 +38,14 @@ export async function register( export async function did( identifier: Identifier.Implementation, - tickets: Inventory + inventory: Inventory ): Promise { return identifier.did() } export async function hasSufficientAuthority( identifier: Identifier.Implementation, - tickets: Inventory + inventory: Inventory ): Promise< { suffices: true } | { suffices: false; reason: string } > { diff --git a/src/components/authority/browser-url.ts b/src/components/authority/browser-url.ts index ba0a965f..a5041a35 100644 --- a/src/components/authority/browser-url.ts +++ b/src/components/authority/browser-url.ts @@ -1,4 +1,3 @@ -import * as Identifier from "../identifier/implementation.js" import * as Provider from "./browser-url/provider.js" import * as Requestor from "./browser-url/requestor.js" import * as DefaultClerk from "./clerk/default.js" @@ -14,12 +13,10 @@ export { ProvideResponse, RequestResponse } from "./browser-url/common.js" export { ProvideParams } from "./browser-url/provider.js" export { RequestParams } from "./browser-url/requestor.js" -export function implementation( - identifier: Identifier.Implementation -): Implementation { +export function implementation(): Implementation { return { provide: Provider.provide, request: Requestor.request, - clerk: DefaultClerk.implementation(identifier), + clerk: DefaultClerk.implementation(), } } diff --git a/src/components/authority/browser-url/common.ts b/src/components/authority/browser-url/common.ts index c19a7553..ea5d15dd 100644 --- a/src/components/authority/browser-url/common.ts +++ b/src/components/authority/browser-url/common.ts @@ -7,6 +7,7 @@ import { base58btc } from "multiformats/bases/base58" import * as Uint8Arrays from "uint8arrays" import { isBlob, isObject, isString } from "../../../common/type-checks.js" +import { Names } from "../../../repositories/names.js" //////// // 🏔️️ // diff --git a/src/components/authority/browser-url/provider.ts b/src/components/authority/browser-url/provider.ts index 79511de0..96e4e312 100644 --- a/src/components/authority/browser-url/provider.ts +++ b/src/components/authority/browser-url/provider.ts @@ -19,6 +19,7 @@ import { Channel as ChannelType } from "../../../channel.js" import { isObject, isString } from "../../../common/type-checks.js" import { EventEmitter } from "../../../events/emitter.js" import { Inventory } from "../../../inventory.js" +import { Names } from "../../../repositories/names.js" import { Ticket } from "../../../ticket/types.js" import { CIPHER_TEXT_ENCODING, @@ -43,6 +44,7 @@ export type ProvideParams = { eventEmitter: EventEmitter inventory: Inventory queries: Query.Query[] + names: Names } export async function provide( @@ -126,6 +128,7 @@ class ProviderSession extends Sess challenge: Uint8Array dependencies: MessageHandlerParams["dependencies"] inventory: Inventory + names: Names constructor( params: MessageHandlerParams & { @@ -137,6 +140,7 @@ class ProviderSession extends Sess this.challenge = params.challenge this.dependencies = params.dependencies this.inventory = params.inventory + this.names = params.names } //////////////////////// @@ -202,6 +206,8 @@ class ProviderSession extends Sess const remoteIdentifierDID = payload.identifier + console.log(payload) + const queries = payload.queries.map(Query.fromJSON) const allContained = queries.every(child => { return queries.some(parent => Query.isContained({ parent, child })) @@ -249,6 +255,8 @@ class ProviderSession extends Sess sign: this.dependencies.identifier.sign, } + const resolvedNames: Record = {} + const fsQueries = queries.reduce( (acc, q) => q.query === "fileSystem" ? [...acc, q] : acc, [] as Query.FileSystemQuery[] @@ -282,54 +290,76 @@ class ProviderSession extends Sess }) const nested = await Promise.all(promises) - return nested.flat() + const collectedTickets = nested.flat() + + return { + query: q, + tickets: collectedTickets, + } }) const accountTicketsBundles = await Promise.all(accountTicketPromises) - const accountTickets = Tickets.collectUnique(accountTicketsBundles.flat()) + const accountTickets = accountTicketsBundles const fileSystemTicketPromises = fsQueries - .map(q => this.inventory.lookupFileSystemTicket(q.path, q.did)) - .reduce((acc, a) => a ? [...acc, a] : acc, [] as Ticket[]) - .map(async t => { + .map(query => { + const did = this.names.resolveId(query.id) + if (!did) return [] + + if ("name" in query.id) { + resolvedNames[query.id.name] = did + } + + const ticket = this.inventory.lookupFileSystemTicket(query.path, did) + if (!ticket) return [] + + return [{ ticket, query }] + }) + .flat() + .map(async item => { const ucan = await Ucan.build({ audience: remoteIdentifierDID, issuer: identifierKeyPair, - proofs: [(await Tickets.cid(t)).toString()], + proofs: [(await Tickets.cid(item.ticket)).toString()], }) const ticket = Ucan.toTicket(ucan) - - return this.inventory.bundleTickets( + const tickets = this.inventory.bundleTickets( ticket, Ucan.ticketProofResolver ) + + return { query: item.query, tickets } }) const fileSystemTicketBundles = await Promise.all(fileSystemTicketPromises) - const fileSystemTickets = Tickets.collectUnique(fileSystemTicketBundles.flat()) + const fileSystemTickets = fileSystemTicketBundles const accessKeys = fsQueries .filter(q => Path.isPartition("private", q.path)) - .map(q => this.inventory.findAccessKey(q.path, q.did)) - .reduce( - (acc, a) => a ? [...acc, a] : acc, - [] as AccessKeyWithContext[] - ) + .map(q => { + const did = this.names.resolveId(q.id) + if (!did) return [] + const key = this.inventory.findAccessKey(q.path, did) + if (!key) return [] + return [{ query: q, key }] + }) + .flat() this.sendMessage( "query", encryptJSONPayload(cipher, { - approved: queries.map(Query.toJSON), accountTickets, fileSystemTickets, accessKeys: accessKeys.map(a => { return { - did: a.did, - key: Uint8Arrays.toString(a.key, CIPHER_TEXT_ENCODING), - path: Path.toPosix(a.path), + did: a.key.did, + key: Uint8Arrays.toString(a.key.key, CIPHER_TEXT_ENCODING), + query: a.query, + path: Path.toPosix(a.key.path), } }), + resolvedNames, }) ) diff --git a/src/components/authority/browser-url/requestor.ts b/src/components/authority/browser-url/requestor.ts index eafdde75..380d5c14 100644 --- a/src/components/authority/browser-url/requestor.ts +++ b/src/components/authority/browser-url/requestor.ts @@ -239,43 +239,57 @@ class RequestorSession extends Session { !isObject(payload) || !Array.isArray(payload.accessKeys) || !Array.isArray(payload.accountTickets) - || !Array.isArray(payload.approved) || !Array.isArray(payload.fileSystemTickets) || !payload.accessKeys.every((p: unknown) => - isObject(p) && isString(p.did) && isString(p.key) && isString(p.path) + isObject(p) && isString(p.did) && isObject(p.query) && isString(p.key) && isString(p.path) ) - || !payload.accountTickets.every((p: unknown) => isObject(p)) - || !payload.approved.every((p: unknown) => isString(p) || isObject(p)) - || !payload.fileSystemTickets.every((p: unknown) => isObject(p)) + || !payload.accountTickets.every((p: unknown) => isObject(p) && isObject(p.query) && Array.isArray(p.tickets)) + || !payload.fileSystemTickets.every((p: unknown) => isObject(p) && isObject(p.query) && Array.isArray(p.tickets)) + || !isObject(payload.resolvedNames) + || !Object.values(payload.resolvedNames).every((s: unknown) => isString(s)) ) { return this.earlyExit(`Ignoring queries from ${msg.did}, improperly encoded query approvals.`, payload) } - const accountTickets = payload.accountTickets.reduce( - (acc, t) => isTicket(t) ? [...acc, t] : acc, - [] as Ticket[] - ) + let authorisedQueries: Query.Query[] = [] - const fileSystemTickets = payload.fileSystemTickets.reduce( - (acc, t) => isTicket(t) ? [...acc, t] : acc, - [] as Ticket[] - ) + const accountTickets = payload.accountTickets.map(i => { + const query = Query.fromJSON(i.query) + authorisedQueries.push(query) + const tickets: Ticket[] = i.tickets.reduce( + (acc: Ticket[], t: unknown) => isTicket(t) ? [...acc, t] : acc, + [] + ) + + return { query, tickets } + }) + + const fileSystemTickets = payload.fileSystemTickets.map(i => { + const query = Query.fromJSON(i.query) + authorisedQueries.push(query) + const tickets: Ticket[] = i.tickets.reduce( + (acc: Ticket[], t: unknown) => isTicket(t) ? [...acc, t] : acc, + [] + ) + + return { query, tickets } + }) const accessKeys = payload.accessKeys.map(a => { return { did: a.did, key: Uint8Arrays.fromString(a.key, CIPHER_TEXT_ENCODING), + query: Query.fromJSON(a.query), path: Path.fromPosix(a.path), } }) - const queries = payload.approved.map(q => Query.fromJSON(q)) - this.resolve({ accessKeys, accountTickets, fileSystemTickets, - authorisedQueries: queries, + authorisedQueries, + resolvedNames: payload.resolvedNames as Record, requestResponse: {}, }) diff --git a/src/components/authority/clerk/default.ts b/src/components/authority/clerk/default.ts index 6897c008..fffe5f63 100644 --- a/src/components/authority/clerk/default.ts +++ b/src/components/authority/clerk/default.ts @@ -1,5 +1,10 @@ +import { ed25519 } from "@noble/curves/ed25519" +import { tag } from "iso-base/varint" +import { base58btc } from "multiformats/bases/base58" + import * as AgentDID from "../../../agent/did.js" import * as Path from "../../../path/index.js" +import * as Tickets from "../../../ticket/index.js" import * as Ucan from "../../../ucan/ts-ucan/index.js" import * as Agent from "../../agent/implementation.js" import * as Identifier from "../../identifier/implementation.js" @@ -11,25 +16,28 @@ import { Clerk } from "../implementation.js" // CLERK // /////////// -export async function createFileSystemTicket( - identifier: Identifier.Implementation, +export async function createOriginFileSystemTicket( path: Path.DistinctivePath, audience: string ): Promise { - const identifierDID = await identifier.did() + const privateKey = ed25519.utils.randomPrivateKey() + const publicKey = ed25519.getPublicKey(privateKey) + + const did = `did:key:${base58btc.encode(tag(0xed, publicKey))}` + const ucan = await Ucan.build({ // from & to issuer: { - did: () => identifierDID, - jwtAlg: await identifier.ucanAlgorithm(), - sign: identifier.sign, + did: () => did, + jwtAlg: "EdDSA", + sign: async (data: Uint8Array) => ed25519.sign(data, privateKey), }, audience, // capabilities capabilities: [ { - with: { scheme: "wnfs", hierPart: `//${identifierDID}${Path.toPosix(path, { absolute: true })}` }, + with: { scheme: "wnfs", hierPart: `//${did}${Path.toPosix(path, { absolute: true })}` }, can: { namespace: "fs", segments: ["*"] }, }, ], @@ -40,7 +48,8 @@ export async function createFileSystemTicket( export async function identifierToAgentDelegation( identifier: Identifier.Implementation, - agent: Agent.Implementation + agent: Agent.Implementation, + proofs: Ticket[] ): Promise { const identifierDID = await identifier.did() const ucan = await Ucan.build({ @@ -50,14 +59,7 @@ export async function identifierToAgentDelegation( sign: identifier.sign, }, audience: await AgentDID.signing(agent), - capabilities: [ - // Powerbox concept: - // Every capability given to the identifier may be used by the agent. - { - with: { scheme: "ucan", hierPart: `${identifierDID}/*` }, - can: { namespace: "ucan", segments: ["*"] }, - }, - ], + proofs: await Promise.all(proofs.map(t => Tickets.cid(t).toString())), }) return Ucan.toTicket(ucan) @@ -81,13 +83,11 @@ export function matchFileSystemTicket( // 🛳️ // //////// -export function implementation( - identifier: Identifier.Implementation -): Clerk { +export function implementation(): Clerk { return { tickets: { fileSystem: { - create: (...args) => createFileSystemTicket(identifier, ...args), + origin: createOriginFileSystemTicket, matcher: matchFileSystemTicket, }, misc: { diff --git a/src/components/authority/implementation.ts b/src/components/authority/implementation.ts index 0dc66049..798ef131 100644 --- a/src/components/authority/implementation.ts +++ b/src/components/authority/implementation.ts @@ -20,9 +20,9 @@ export type Clerk = { tickets: { fileSystem: { /** - * Given a root path, create file system ticket. + * Given a root path, create the origin file system ticket. */ - create: (path: Path.DistinctivePath, audience: string) => Promise + origin: (path: Path.DistinctivePath, audience: string) => Promise /** * Given a root path, find a matching file system ticket. */ @@ -44,7 +44,8 @@ export type Clerk = { */ identifierToAgentDelegation: ( identifier: Identifier.Implementation, - agent: Agent.Implementation + agent: Agent.Implementation, + proofs: Ticket[] ) => Promise } } @@ -56,9 +57,10 @@ export type Clerk = { export type AuthorityArtefacts = { accessKeys: AccessKeyWithContext[] - accountTickets: Ticket[] + accountTickets: { query: Query; tickets: Ticket[] }[] authorisedQueries: Query[] - fileSystemTickets: Ticket[] + fileSystemTickets: { query: Query; tickets: Ticket[] }[] + resolvedNames: Record requestResponse: RequestResponse } diff --git a/src/components/storage/implementation.ts b/src/components/storage/implementation.ts index 578effbb..d9202e57 100644 --- a/src/components/storage/implementation.ts +++ b/src/components/storage/implementation.ts @@ -9,6 +9,7 @@ export type Implementation = { KEYS: { CABINET: string CID_LOG: string + NAMES: string } getItem: (key: string) => Promise diff --git a/src/components/storage/keys/default.ts b/src/components/storage/keys/default.ts index c7b3e001..58ce350a 100644 --- a/src/components/storage/keys/default.ts +++ b/src/components/storage/keys/default.ts @@ -3,4 +3,5 @@ import { Implementation } from "../implementation.js" export const KEYS: Implementation["KEYS"] = { CABINET: "cabinet", CID_LOG: "cid-log", + NAMES: "names", } diff --git a/src/compositions/fission.ts b/src/compositions/fission.ts index c7b936bf..98f27dce 100644 --- a/src/compositions/fission.ts +++ b/src/compositions/fission.ts @@ -159,7 +159,7 @@ export async function components( const agent = await Agent.implementation({ store: agentStore }) const identifier = await Identifier.implementation({ store: identifierStore }) const depot = await Depot.implementation(manners, storage, `${namespace}/blockstore`, endpoints) - const authority = Authority.implementation(identifier) + const authority = Authority.implementation() const account = (() => { switch (settings?.accountType || "standard") { diff --git a/src/compositions/local.ts b/src/compositions/local.ts index 25bf2f52..0a5ed607 100644 --- a/src/compositions/local.ts +++ b/src/compositions/local.ts @@ -62,7 +62,7 @@ export async function components( const identifier = await Identifier.implementation({ store: identifierStore }) const manners = Manners.implementation(config) const account = Account.implementation() - const authority = Authority.implementation(identifier) + const authority = Authority.implementation() // Fin return { diff --git a/src/fileSystem.ts b/src/fileSystem.ts index e1dec757..d3b1a2ad 100644 --- a/src/fileSystem.ts +++ b/src/fileSystem.ts @@ -6,11 +6,11 @@ import * as Path from "./path/index.js" import * as CIDLog from "./repositories/cid-log.js" import { Maybe } from "./common/index.js" -import { Authority, Storage } from "./components.js" +import { Authority, Identifier, Storage } from "./components.js" import { FileSystem } from "./fs/class.js" -import { Dependencies } from "./fs/types.js" +import { Dependencies, FileSystemCarrier } from "./fs/types.js" import { Inventory } from "./inventory.js" -import { Ticket } from "./ticket/types.js" +import { Names } from "./repositories/names.js" //////// // 🛠️ // @@ -21,22 +21,53 @@ import { Ticket } from "./ticket/types.js" */ export async function loadFileSystem(args: { cabinet: Cabinet - dataRoot?: CID - dataRootUpdater?: ( - dataRoot: CID, - proofs: Ticket[] - ) => Promise<{ updated: true } | { updated: false; reason: string }> + carrier: FileSystemCarrier dependencies: Dependencies & { authority: Authority.Implementation + identifier: Identifier.Implementation + storage: Storage.Implementation + } + names: Names +}): Promise { + if (args.carrier.futile) { + throw new Error( + "Cannot load a file system using a futile carrier. This usually means you're in the middle of an important process in which the file system cannot be loaded yet." + ) + } + + // DID is known, so we expect an existing file system. + if ("did" in args.carrier.id) { + return loadExisting({ + ...args, + did: args.carrier.id.did, + }) + } + + // DID is unknown, that means a new file system. + return createNew({ + ...args, + didName: args.carrier.id.name, + }) +} + +//////// +// ㊙️ // +//////// + +async function loadExisting(args: { + cabinet: Cabinet + carrier: FileSystemCarrier + dependencies: Dependencies & { + authority: Authority.Implementation + identifier: Identifier.Implementation storage: Storage.Implementation } did: string }): Promise { - const { cabinet, dataRootUpdater, dependencies, did } = args + const { cabinet, carrier, dependencies, did } = args const { authority, depot, manners, storage } = dependencies - let cid: Maybe = args.dataRoot || null - let fs: FileSystem + let cid: Maybe = "dataRoot" in carrier ? carrier.dataRoot || null : null // Create CIDLog, namespaced by identifier const cidLog = await CIDLog.create({ storage, did }) @@ -47,8 +78,11 @@ export async function loadFileSystem(args: { if (!cid) { // No DNS CID yet cid = cidLog.newest() - if (cid) manners.log("📓 Using local CID:", cid.toString()) - else manners.log("📓 Creating a new file system") + if (cid) { + manners.log("📓 Using local CID:", cid.toString()) + } else { + throw new Error(`Expected a file system to exists for the DID: ${did}`) + } } else if (logIdx === cidLog.length() - 1) { // DNS is up to date manners.log("📓 DNSLink is up to date:", cid.toString()) @@ -68,37 +102,71 @@ export async function loadFileSystem(args: { // that are only stored locally. } - // If a file system exists, load it and return it + // File system class instance const inventory = new Inventory(authority.clerk, cabinet) - const updateDataRoot = dataRootUpdater + const updateDataRoot = carrier.dataRootUpdater - if (cid) { - await manners.fileSystem.hooks.beforeLoadExisting(cid, depot) + await manners.fileSystem.hooks.beforeLoadExisting(cid, depot) - fs = await FileSystem.fromCID( - cid, - { cidLog, dependencies, did, inventory, updateDataRoot } - ) + const fs = await FileSystem.fromCID( + cid, + { cidLog, dependencies, did, inventory, updateDataRoot } + ) - // Mount private nodes - await Promise.all( - (cabinet.accessKeys[fs.did] || []).map(async a => { - return fs.mountPrivateNode({ - path: Path.removePartition(a.path), - capsuleKey: a.key, - }) + // Mount private nodes + await Promise.all( + (cabinet.accessKeys[fs.did] || []).map(async a => { + return fs.mountPrivateNode({ + path: Path.removePartition(a.path), + capsuleKey: a.key, }) - ) + }) + ) - await manners.fileSystem.hooks.afterLoadExisting(fs, depot) + await manners.fileSystem.hooks.afterLoadExisting(fs, depot) + + return fs +} - return fs +async function createNew(args: { + cabinet: Cabinet + carrier: FileSystemCarrier + dependencies: Dependencies & { + authority: Authority.Implementation + identifier: Identifier.Implementation + storage: Storage.Implementation } + didName: string + names: Names +}) { + const { cabinet, carrier, dependencies, didName, names } = args + const { authority, depot, manners, storage } = dependencies + + manners.log("📓 Creating a new file system") + + // Self delegate file system UCAN & add stuff to cabinet + const fileSystemTicket = await authority.clerk.tickets.fileSystem.origin( + Path.root(), + await dependencies.identifier.did() + ) + + await cabinet.addTicket("file_system", fileSystemTicket) + + // The file system DID is the issuer of the initial file system ticket + const did = fileSystemTicket.issuer + + await names.add({ name: didName, subject: did }) + + // Create CIDLog, namespaced by identifier + const cidLog = await CIDLog.create({ storage, did }) + + // Create new FileSystem instance + const inventory = new Inventory(authority.clerk, cabinet) + const updateDataRoot = carrier.dataRootUpdater - // Otherwise make a new one await manners.fileSystem.hooks.beforeLoadNew(depot) - fs = await FileSystem.empty({ + const fs = await FileSystem.empty({ cidLog, dependencies, did, @@ -108,14 +176,6 @@ export async function loadFileSystem(args: { const maybeMount = await manners.fileSystem.hooks.afterLoadNew(fs, depot) - // Self delegate file system UCAN & add stuff to cabinet - const fileSystemTicket = await authority.clerk.tickets.fileSystem.create( - Path.root(), - fs.did - ) - - await cabinet.addTicket("file_system", fileSystemTicket) - if (maybeMount) { await cabinet.addAccessKey({ did: fs.did, diff --git a/src/fs/class.ts b/src/fs/class.ts index 544f595d..83f6a39d 100644 --- a/src/fs/class.ts +++ b/src/fs/class.ts @@ -22,6 +22,7 @@ import { AnySupportedDataType, DataForType, DataRootChange, + DataRootUpdater, DataType, Dependencies, DirectoryItem, @@ -42,7 +43,7 @@ export type FileSystemOptions = { did: string inventory: Inventory settleTimeBeforePublish?: number - updateDataRoot?: (dataRoot: CID, proofs: Ticket[]) => Promise<{ updated: true } | { updated: false; reason: string }> + updateDataRoot?: DataRootUpdater } /** @group File System */ @@ -54,7 +55,7 @@ export class FileSystem { #inventory: Inventory #rootTree: RootTree.RootTree #settleTimeBeforePublish: number - #updateDataRoot?: (dataRoot: CID, proofs: Ticket[]) => Promise<{ updated: true } | { updated: false; reason: string }> + #updateDataRoot?: DataRootUpdater #privateNodes: MountedPrivateNodes = {} #rng: Rng.Rng diff --git a/src/fs/types.ts b/src/fs/types.ts index dd87d6a1..c4ad0fc5 100644 --- a/src/fs/types.ts +++ b/src/fs/types.ts @@ -5,6 +5,7 @@ import * as Path from "../path/index.js" import { CID } from "../common/cid.js" import { Partition, Partitioned } from "../path/index.js" +import { Ticket } from "../ticket/types.js" //////// // 🧩 // @@ -28,6 +29,12 @@ export type DataForType = D extends "bytes" ? U : D extends "utf8" ? string : never +/** @group File System */ +export type DataRootUpdater = ( + dataRoot: CID, + proofs: Ticket[] +) => Promise<{ updated: true } | { updated: false; reason: string }> + /** @internal */ export type Dependencies = { depot: Depot.Implementation @@ -46,6 +53,14 @@ export type DirectoryItemWithKind = DirectoryItem & { path: Path.Distinctive> } +/** @group File System */ +export type FileSystemCarrier = { + dataRoot?: CID + dataRootUpdater?: DataRootUpdater + futile?: boolean + id: { did: string } | { name: string } +} + /** @group File System */ export type MutationOptions = { skipPublish?: boolean diff --git a/src/index.ts b/src/index.ts index 4e6c357a..e51cfc8c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import * as AuthorityEvents from "./events/authority.js" import * as Events from "./events/program.js" import * as Path from "./path/index.js" import * as Cabinet from "./repositories/cabinet.js" +import * as Names from "./repositories/names.js" import { FileSystemQuery, Query } from "./authority/query.js" import { CID } from "./common/cid.js" @@ -33,8 +34,8 @@ import { ListenTo, listenTo } from "./events/listen.js" import { loadFileSystem } from "./fileSystem.js" import { FileSystem } from "./fs/class.js" import { addSampleData } from "./fs/data/sample.js" +import { DataRootUpdater, FileSystemCarrier } from "./fs/types.js" import { Inventory } from "./inventory.js" -import { Ticket } from "./ticket/types.js" //////////////// // RE-EXPORTS // @@ -185,14 +186,7 @@ export type FileSystemCategory = { * program.fileSystem.load({ dataRoot: cid, dataRootUpdater: updateFn, did: "did:some-identifier" }) * ``` */ - load: (params: { - dataRoot?: CID - dataRootUpdater?: ( - dataRoot: CID, - proofs: Ticket[] - ) => Promise<{ updated: true } | { updated: false; reason: string }> - did: string - }) => Promise + load: (params: FileSystemCarrier) => Promise } /** @@ -262,6 +256,8 @@ export async function program< // Create repositories const cabinet = await Cabinet.create({ storage: components.storage }) + const names = await Names.create({ storage: components.storage }) + const inventory = new Inventory(components.authority.clerk, cabinet) cabinet.events.on("collection:changed", async ({ collection }) => { @@ -278,7 +274,6 @@ export async function program< async has( query: Query | (Query | Query[])[] ): Promise<{ has: true } | { has: false; reason: string }> { - const audience = await identifier.did() const queries = (Array.isArray(query) ? query : [query]).flat() // Account access @@ -304,7 +299,9 @@ export async function program< const hasAccessToFsPaths = fsQueries.filter(q => Path.isPartition("private", q.path)).reduce( (acc, query) => { if (acc === false) return false - return cabinet.hasAccessKey(audience, query.path) + const did = names.resolveId(query.id) + if (!did) return false + return cabinet.hasAccessKey(did, query.path) }, true ) @@ -332,6 +329,7 @@ export async function program< eventEmitter: authorityEmitter, inventory, queries, + names, }) }, @@ -351,10 +349,19 @@ export async function program< }) if (response) { - await cabinet.addTickets("account", response.accountTickets) - await cabinet.addTickets("file_system", response.fileSystemTickets) + const accountTickets = response.accountTickets.map(i => i.tickets).flat() + const fileSystemTickets = response.fileSystemTickets.map(i => i.tickets).flat() + + await cabinet.addTickets("account", accountTickets) + await cabinet.addTickets("file_system", fileSystemTickets) await cabinet.addAccessKeys(response.accessKeys) + names.add( + Object.entries(response.resolvedNames).map(([k, v]) => { + return { name: k, subject: v } + }) + ) + await authorityEmitter.emit("request:authorised", { queries: response.authorisedQueries }) await authorityEmitter.emit("request:authorized", { queries: response.authorisedQueries }) @@ -370,19 +377,8 @@ export async function program< // Other categories const fileSystemCategory: FileSystemCategory = { addSampleData: (fs: FileSystem) => addSampleData(fs), - load: async (params: { - dataRoot?: CID - dataRootUpdater?: ( - dataRoot: CID, - proofs: Ticket[] - ) => Promise<{ updated: true } | { updated: false; reason: string }> - did: string - }) => { - const dataRoot = params.dataRoot - const dataRootUpdater = params.dataRootUpdater - const did = params.did - - return loadFileSystem({ cabinet, dataRoot, dataRootUpdater, dependencies: components, did }) + load: async (params: FileSystemCarrier) => { + return loadFileSystem({ cabinet, names, carrier: params, dependencies: components }) }, } @@ -412,10 +408,10 @@ export async function program< fileSystem: fileSystemCategory, account: { - register: Auth.register({ account, agent, authority, identifier, cabinet }), + register: Auth.register({ account, agent, authority, identifier, cabinet, names }), canRegister: account.canRegister, - ...components.account.annex(identifier, inventory), + ...components.account.annex(identifier, inventory, names), }, } diff --git a/src/repositories/cabinet.ts b/src/repositories/cabinet.ts index ea94208d..0327c8fb 100644 --- a/src/repositories/cabinet.ts +++ b/src/repositories/cabinet.ts @@ -4,7 +4,7 @@ import * as Storage from "../components/storage/implementation.js" import * as Path from "../path/index.js" import { AccessKeyWithContext } from "../accessKey.js" -import { CID, decodeCID, encodeCID } from "../common/cid.js" +import { decodeCID, encodeCID } from "../common/cid.js" import { isObject, isString } from "../common/type-checks.js" import Repository, { RepositoryOptions } from "../repository.js" import { cid as ticketCID } from "../ticket/index.js" diff --git a/src/repositories/names.ts b/src/repositories/names.ts new file mode 100644 index 00000000..7abb9e34 --- /dev/null +++ b/src/repositories/names.ts @@ -0,0 +1,68 @@ +import * as Storage from "../components/storage/implementation.js" +import Repository, { RepositoryOptions } from "../repository.js" + +//////// +// 🧩 // +//////// + +export type Item = { + name: string + subject: string // ie. the thing that is being named +} + +// name → subject +export type Collection = Record + +//////// +// 🛠️ // +//////// + +export function create({ storage }: { storage: Storage.Implementation }): Promise { + return Repo.create({ + storage, + storageName: storage.KEYS.NAMES, + }) +} + +/////////// +// CLASS // +/////////// + +export { Repo as Names } + +export class Repo extends Repository { + private constructor(options: RepositoryOptions) { + super(options) + } + + // IMPLEMENTATION + + emptyCollection() { + return {} + } + + mergeCollections(a: Collection, b: Collection): Collection { + return { + ...a, + ...b, + } + } + + async toCollection(item: Item): Promise { + return { [item.name]: item.subject } + } + + // 🛠️ + + resolveId(id: { did: string } | { name: string }): string | null { + if ("did" in id) { + return id.did + } else { + return this.subject(id.name) + } + } + + subject(name: string): string | null { + return this.collection[name] || null + } +} diff --git a/src/repository.ts b/src/repository.ts index b91efd1c..e9930bca 100644 --- a/src/repository.ts +++ b/src/repository.ts @@ -46,8 +46,8 @@ export default abstract class Repository { return repo } - async add(newItems: I[]): Promise { - const col = await newItems.reduce( + async add(newItems: I[] | I): Promise { + const col = await (Array.isArray(newItems) ? newItems : [newItems]).reduce( async (acc: Promise, item) => this.mergeCollections(await acc, await this.toCollection(item)), Promise.resolve(this.collection) ) diff --git a/src/ticket/index.ts b/src/ticket/index.ts index 00e6b0f9..15e5df07 100644 --- a/src/ticket/index.ts +++ b/src/ticket/index.ts @@ -18,22 +18,6 @@ export async function cid(ticket: Ticket): Promise { return CID.createV1(Raw.code, multihash) } -export function collectUnique(tickets: Ticket[]): Ticket[] { - return tickets.reduce((acc, ticket) => { - if (acc.tokens.includes(ticket.token)) { - return acc - } - - return { - tickets: [...acc.tickets, ticket], - tokens: [...acc.tokens, ticket.token], - } - }, { - tickets: [] as Ticket[], - tokens: [] as string[], - }).tickets -} - export function isTicket(ticket: unknown): ticket is Ticket { return isObject(ticket) && isString(ticket.issuer) && isString(ticket.audience) && isString(ticket.token) } diff --git a/src/ticket/types.ts b/src/ticket/types.ts index 6f30169c..4314c643 100644 --- a/src/ticket/types.ts +++ b/src/ticket/types.ts @@ -1,6 +1,6 @@ import { CID } from "../common/cid.js" -const CATEGORIES = ["account", "file_system", "misc"] as const +const CATEGORIES = ["account", "agent", "file_system"] as const /** * A ticket is a [UCAN](https://github.com/ucan-wg/spec) or UCAN-like token.