diff --git a/package.json b/package.json index e6a6610fa..e1d42f905 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "test:dev": "vitest -w" }, "dependencies": { + "@ipld/car": "^4.1.5", "@ucanto/client": "^1.0.1", "@ucanto/core": "^1.0.1", "@ucanto/interface": "^1.0.0", diff --git a/src/encoding.js b/src/encoding.js new file mode 100644 index 000000000..c32ef9e35 --- /dev/null +++ b/src/encoding.js @@ -0,0 +1,110 @@ +/* eslint-disable unicorn/prefer-spread */ +import { CarReader } from '@ipld/car/reader' +import { CarWriter } from '@ipld/car/writer' +import { Delegation } from '@ucanto/core/delegation' +// eslint-disable-next-line no-unused-vars +import * as Types from '@ucanto/interface' +import * as u8 from 'uint8arrays' + +/** + * @param {AsyncIterable} iterable + */ +function collector(iterable) { + const chunks = [] + const cfn = (async () => { + for await (const chunk of iterable) { + chunks.push(chunk) + } + return u8.concat(chunks) + })() + return cfn +} + +/** + * @param {Types.Delegation[]} delegations + * @param {import('uint8arrays/to-string').SupportedEncodings} encoding + */ +export async function encodeDelegations(delegations, encoding = 'base64url') { + if (delegations.length === 0) { + return '' + } + + const roots = delegations.map((d) => d.root.cid) + + // @ts-ignore + const { writer, out } = CarWriter.create(roots) + const collection = collector(out) + + for (const delegation of delegations) { + for (const block of delegation.export()) { + // @ts-ignore + await writer.put(block) + } + } + await writer.close() + + const bytes = await collection + + return u8.toString(bytes, encoding) +} + +/** + * Encode one {@link Types.Delegation Delegation} into a string + * + * @param {Types.Delegation} delegation + * @param {import('uint8arrays/to-string').SupportedEncodings} [encoding] + */ +export function delegationToString(delegation, encoding) { + return encodeDelegations([delegation], encoding) +} + +/** + * Decode string into {@link Types.Delegation Delegation} + * + * @template {Types.Capabilities} [T=Types.Capabilities] + * @param {import('./types').EncodedDelegation} raw + * @param {import('uint8arrays/to-string').SupportedEncodings} [encoding] + */ +export async function decodeDelegations(raw, encoding = 'base64url') { + if (!raw) { + return [] + } + const bytes = u8.fromString(raw, encoding) + const reader = await CarReader.fromBytes(bytes) + const roots = await reader.getRoots() + + /** @type {Types.Delegation[]} */ + const delegations = [] + + for (const root of roots) { + const rootBlock = await reader.get(root) + + if (rootBlock) { + const blocks = new Map() + for (const block of reader._blocks) { + if (block.cid.toString() !== root.toString()) + blocks.set(block.cid.toString(), block) + } + + // @ts-ignore + delegations.push(new Delegation(rootBlock, blocks)) + } else { + throw new Error('Failed to find root from raw delegation.') + } + } + + return delegations +} + +/** + * Decode string into a {@link Types.Delegation Delegation} + * + * @template {Types.Capabilities} [T=Types.Capabilities] + * @param {import('./types').EncodedDelegation} raw + * @param {import('uint8arrays/to-string').SupportedEncodings} [encoding] + */ +export async function stringToDelegation(raw, encoding) { + const delegations = await decodeDelegations(raw, encoding) + + return /** @type {Types.Delegation} */ (delegations[0]) +} diff --git a/src/index.js b/src/index.js index 023e24992..5eb1bc17c 100644 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,7 @@ import { generateDelegation, importDelegation, } from './delegation.js' +import { delegationToString, stringToDelegation } from './encoding.js' import * as Settings from './settings.js' import { Access, Store } from './store/index.js' import { checkUrl, sleep } from './utils.js' @@ -87,15 +88,16 @@ class Client { * @returns {Promise} */ async agent() { - let secret = this.settings.get('agent_secret') || null + const settings = await this.settings + let secret = settings.get('agent_secret') || null let id = Settings.toPrincipal(secret) if (!id) { id = await SigningPrincipal.generate() } - if (!this.settings.has('agent_secret')) { - this.settings.set('agent_secret', SigningPrincipal.format(id)) + if (!settings.has('agent_secret')) { + settings.set('agent_secret', SigningPrincipal.format(id)) } return id @@ -106,11 +108,12 @@ class Client { * @returns {Promise} */ async account() { - let secret = this.settings.get('account_secret') || null + const settings = await this.settings + let secret = settings.get('account_secret') || null // For now, move old secret value to new account_secret. - if (!secret && this.settings.has('secret')) { - secret = this.settings.get('secret') + if (!secret && settings.has('secret')) { + secret = settings.get('secret') // this.settings.delete('secret') } let id = Settings.toPrincipal(secret) @@ -118,8 +121,8 @@ class Client { id = await SigningPrincipal.generate() } - if (!this.settings.has('account_secret')) { - this.settings.set('account_secret', SigningPrincipal.format(id)) + if (!settings.has('account_secret')) { + settings.set('account_secret', SigningPrincipal.format(id)) } return id @@ -129,39 +132,30 @@ class Client { * @returns {Promise} */ async currentDelegation() { - let did = this.settings.has('delegation') - ? this.settings.get('delegation') - : null + const settings = await this.settings + let account = settings.has('account') ? settings.get('account') : null - let delegations = this.settings.has('delegations') - ? this.settings.get('delegations') + let delegations = settings.has('delegations') + ? settings.get('delegations') : {} //Generate first delegation from account to agent. - if (!did) { - const issuer = await this.account() - const to = (await this.agent()).did() - const del = await generateDelegation({ to, issuer }, true) - - did = (await this.account()).did() - - delegations[did] = { ucan: del, alias: 'self' } - this.settings.set('delegations', delegations) - this.settings.set('delegation', issuer.did()) - } - - delegations = this.settings.has('delegations') - ? this.settings.get('delegations') - : {} + if (!account) { + const account = await this.account() + const agent = (await this.agent()).did() + const del = await generateDelegation({ to: agent, issuer: account }, true) + + delegations[account.did()] = { + ucan: await delegationToString(del), + alias: 'self', + } + settings.set('delegations', delegations) + settings.set('account', account.did()) - try { - const ucan = delegations[did]?.ucan - const del = Delegation.import([ucan?.root]) return del - } catch (err) { - console.log('err', err) - return null } + + return stringToDelegation(delegations[account].ucan) } /** @@ -194,9 +188,10 @@ class Client { * @param {string|undefined} email - The email address to register with. */ async register(email) { - const savedEmail = this.settings.get('email') + const settings = await this.settings + const savedEmail = settings.get('email') if (!savedEmail) { - this.settings.set('email', email) + settings.set('email', email) } else if (email !== savedEmail) { throw new Error( 'Trying to register a second email, this is not supported yet.' @@ -313,6 +308,7 @@ class Client { * @returns {Promise} */ async importDelegation(bytes, alias = '') { + const settings = await this.settings const imported = await importDelegation(bytes) const did = imported.issuer.did() @@ -324,12 +320,12 @@ class Client { ) } - let delegations = this.settings.has('delegations') - ? this.settings.get('delegations') + let delegations = settings.has('delegations') + ? settings.get('delegations') : {} delegations[did] = { ucan: imported, alias } - this.settings.set('delegations', delegations) + settings.set('delegations', delegations) return imported } diff --git a/src/settings.js b/src/settings.js index 5ac883961..2f35382af 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1,6 +1,8 @@ import { Delegation, UCAN } from '@ucanto/core' import { SigningPrincipal } from '@ucanto/principal' +import { delegationToString, stringToDelegation } from './encoding.js' + /** * @typedef SettingsObject * @property {string} [secret] @@ -35,9 +37,9 @@ export function toPrincipal(secret) { /** * @param {Map|SettingsObject} objectToParse - * @returns {Map} + * @returns {Promise>} */ -export function objectToMap(objectToParse) { +export async function objectToMap(objectToParse) { // TODO: CHANGE LATER, store check is only for CONF if (objectToParse instanceof Map) { /** @type Map */ @@ -79,7 +81,7 @@ export function objectToMap(objectToParse) { for (const [did, del] of Object.entries(objectToParse.delegations)) { // @ts-ignore delegations[did] = { - ucan: UCAN.parse(del?.ucan), + ucan: await stringToDelegation(del?.ucan), alias: del.alias, } } @@ -100,9 +102,9 @@ export function objectToMap(objectToParse) { * Takes a JSON string and builds a settings object from it. * * @param {Map|string|SettingsObject} settings - The settings string (typically from cli export-settings) - * @returns {Map} The settings object. + * @returns {Promise>} The settings object. */ -export function importSettings(settings) { +export async function importSettings(settings) { if (typeof settings == 'string') { try { return objectToMap(JSON.parse(settings)) @@ -117,9 +119,9 @@ export function importSettings(settings) { * Takes a settings map and builds a POJO out of it. * * @param {Map} settings - The settings object. - * @returns {SettingsObject} The settings object. + * @returns {Promise} The settings object. */ -export function exportSettings(settings) { +export async function exportSettings(settings) { /** @type SettingsObject */ const output = {} @@ -156,11 +158,9 @@ export function exportSettings(settings) { output.delegations = {} for (const [did, del] of Object.entries(settings.get('delegations'))) { - const imported = Delegation.import([del?.ucan?.root]) - output.delegations[did] = { // @ts-ignore - ucan: UCAN.format(imported), + ucan: del.ucan, alias: del.alias, } } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 000000000..ff8907ae1 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,4 @@ +import type { Capabilities, Phantom } from '@ucanto/interface' + +export type EncodedDelegation = string & + Phantom