Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
"packageManager": "[email protected]+sha512.c89847b0667ddab50396bbbd008a2a43cf3b581efd59cf5d9aa8923ea1fb4b8106c041d540d08acb095037594d73ebc51e1ec89ee40c88b30b8a66c0fae0ac1b",
"scripts": {},
"devDependencies": {
"@nx/next": "20.3.2",
"@nx/devkit": "20.3.2",
"@nx/js": "catalog:",
"@nx/next": "20.3.2",
"@types/npm-registry-fetch": "^8.0.7",
"depcheck": "catalog:",
"eslint": "catalog:",
Expand All @@ -32,7 +32,13 @@
"hd-scripts>@typescript-eslint/eslint-plugin": "catalog:",
"hd-scripts>@typescript-eslint/parser": "catalog:",
"eslint-config-standard-with-typescript>@typescript-eslint/eslint-plugin": "catalog:",
"eslint-config-standard-with-typescript>@typescript-eslint/parser": "catalog:"
"eslint-config-standard-with-typescript>@typescript-eslint/parser": "catalog:",
"ucn": "link:../../../../../Library/pnpm/global/5/node_modules/@storacha/ucn",
"@storacha/ucn": "link:"
}
},
"dependencies": {
"@storacha/ucn": "link:",
"ucn": "link:../../../../../Library/pnpm/global/5/node_modules/@storacha/ucn"
}
}
62 changes: 50 additions & 12 deletions packages/ucn/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,23 @@ requests to remote peer(s). If no remote peers are specified, then the Storacha
rendezvous peer is used.

```js
import { Name, Agent, Proof } from '@storacha/ucn'
import { Name, Agent, Proof, NoValueError } from '@storacha/ucn'

// See "Signing Key and Proof Management" below.
// see "Signing Key and Proof Management" below.
const agent = Agent.parse(privateKey)
const proof = Proof.parse(proofYouCan)

const name = Name.from(agent, proof)
const { value } = await Name.resolve(name)

console.log(value)
// e.g. /ipfs/bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui
const name = Name.parse(agent, nameArchive)

try {
const { value } = await Name.resolve(name)

console.log(value)
// e.g. /ipfs/bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui
} catch (err) {
if (err.code === NoValueError.code) {
console.log(`No value has been published for ${name}`)
}
throw err
}
```

### Update
Expand Down Expand Up @@ -103,6 +109,7 @@ const agent = await Agent.generate()
const name = await Name.create(agent)

// agent that should be granted access to update the name
// use `agent.did()` to obtain
const recipient = DID.parse('did:key:z6Mkve9LRa8nvXx6Gj2GXevZFN5zHb476FZLS7o1q7fJThFV')

const proof = await Name.grant(name, recipient, { readOnly: false })
Expand All @@ -111,12 +118,27 @@ console.log(await Proof.format(proof))
// e.g. mAYIEAL3bDhGiZXJvb3RzgGd2ZXJzaW9uAbcCAXESIPa/Vl+6QuagDVY...
```

Using grant:

```js
import { Agent, Name, Proof } from '@storacha/ucn'

// the private key that corresponds to `did:key:z6Mkve9LRa8nvXx6Gj2GXevZFN5zHb476FZLS7o1q7fJThFV`
const agent = Agent.parse(process.env.UCN_PRIVATE_KEY)
// the grant created by the other party
const proof = await Proof.parse('mAYIEAL3bDhGiZXJvb3RzgGd2ZXJzaW9uAbcCAXESIPa/Vl+6QuagDVY...')
const name = Name.from(agent, [proof])

// ready to use! e.g.
// `const current = await Name.resolve(name)`
```

### Signing Key and Proof Management

The **agent private key** is the key used to sign UCAN invocations to update the
name.

The **proof** is a UCAN delegation from the _name_ to the agent, authorizing it
The **proofs** are UCAN delegations from the _name_ to the agent, authorizing it
to read (`clock/head`) and/or mutate (`clock/advance`) the current value.

Both of these items MUST be saved if a revision needs to be created in the
Expand All @@ -125,12 +147,28 @@ future.
```js
import fs from 'node:fs'
await fs.promises.writeFile('agent.priv', agent.encode())
await fs.promises.writeFile('proof.ucan', await name.proof.archive())
await fs.promises.writeFile('name.car', await name.archive())

// or

import { Agent, Name } from '@storacha/ucn'
console.log(Agent.format(agent)) // base64 encoded string
console.log(await Proof.format(name.proof)) // base64 encoded string
console.log(await Name.format(name)) // base64 encoded string
```

Restoring exising credentials:

```js
import fs from 'node:fs'
import { Agent, Name } from '@storacha/ucn'

const agent = Agent.decode(await fs.promises.readFile('agent.priv'))
const name = await Name.extract(agent, await fs.promises.readFile('name.car'))

// or

const agent = Agent.parse(process.env.UCN_PRIVATE_KEY)
const name = await Name.parse(agent, process.env.UCN_NAME_ARCHIVE)
```

### Revision Persistence
Expand Down
1 change: 1 addition & 0 deletions packages/ucn/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
}
},
"files": [
"src",
"dist",
"!dist/**/*.js.map"
],
Expand Down
51 changes: 33 additions & 18 deletions packages/ucn/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import {
ConnectionView,
Delegation,
DID,
Proof,
Principal,
Signer,
UCANLink,
} from '@ucanto/interface'
import {
EventLink as ClockEventLink,
Expand All @@ -20,18 +22,19 @@ export type {
ConnectionView,
Delegation,
DID,
Proof,
Principal,
Service,
Signer,
UCANLink,
}

export type ClockConnection = ConnectionView<Service<RawValue>>
export type ClockConnection = ConnectionView<Service<Value>>

/**
* Name is a merkle clock backed, UCAN authorized, mutable reference to a
* resource.
* A merkle clock backed, UCAN authorized, mutable reference to a resource.
*/
export interface Name extends Principal {
export interface NameView extends Principal {
/**
* The agent that signs request to read/update the mutable reference.
*/
Expand All @@ -41,12 +44,24 @@ export interface Name extends Principal {
* the agent must be delegated the `clock/head` capability. For write
* access the agent must be delegated the `clock/advance` capability.
*/
proof: Delegation
proofs: Proof[]
/**
* Create a delegation allowing the passed receipient to read and/or mutate
* the current value of the name.
*/
grant: (receipent: DID, options?: GrantOptions) => Promise<Delegation>
/**
* Export the name as IPLD blocks.
*
* Note: this does NOT include signer information (the private key).
*/
export: () => AsyncIterable<Block>
/**
* Encode the name as a CAR file.
*
* Note: this does NOT include signer information (the private key).
*/
archive: () => Promise<Uint8Array>
}

export interface GrantOptions {
Expand All @@ -66,54 +81,54 @@ export interface GrantOptions {
*
* e.g. /ipfs/bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui
*/
export type RawValue = string
export type Value = string

/**
* A link to a name mutation event.
*/
export type EventLink = ClockEventLink<RawValue>
export type EventLink = ClockEventLink<Value>

/**
* A name mutation event.
*/
export type EventView = ClockEventView<RawValue>
export type EventView = ClockEventView<Value>

/**
* A name mutation event block.
*/
export type EventBlock = Block<ClockEventView<RawValue>>
export type EventBlock = Block<ClockEventView<Value>>

/**
* A name mutation event block with value.
*/
export type EventBlockView = ClockEventBlockView<RawValue>
export type EventBlockView = ClockEventBlockView<Value>

/**
* Value is the result of resolving the value of one or more revisions.
* The result of resolving the value of one or more revisions.
*/
export interface Value {
export interface ValueView {
/**
* The name the resolved value is associated with.
*/
name: Name
name: NameView
/**
* The resolved value.
*/
value: RawValue
value: Value
/**
* Revision(s) this resolution was calculated from.
*/
revision: Revision[]
revision: RevisionView[]
}

/**
* Revision is a representation of a past, current or future value for a name.
* A representation of a past, current or future value for a name.
*/
export interface Revision {
export interface RevisionView {
/**
* The value associated with this revision.
*/
value: RawValue
value: Value
/**
* The mutation event that backs this revision.
*/
Expand Down
9 changes: 8 additions & 1 deletion packages/ucn/src/bin/api.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
export type { DID, Delegation, Capability } from '@ucanto/interface'
export type { Name, EventLink, Value, Revision } from '../api.js'
export type {
NameView,
EventLink,
ValueView,
Proof,
RevisionView,
Signer,
} from '../api.js'
7 changes: 3 additions & 4 deletions packages/ucn/src/bin/cmd/grant.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as DID from '@ipld/dag-ucan/did'
import { getAgent, getNames } from '../lib.js'
import * as Name from '../../name.js'
import * as Proof from '../../proof.js'

/**
Expand All @@ -9,14 +8,14 @@ import * as Proof from '../../proof.js'
* @param {{ 'read-only'?: boolean }} [options]
*/
export const handler = async (id, recipient, options) => {
const [agent, names] = await Promise.all([getAgent(), getNames()])
const agent = await getAgent()
const names = await getNames(agent)
const nameID = DID.parse(id).did()
if (!names[nameID]) {
console.error(`unknown name: ${nameID}`)
process.exit(1)
}
const name = Name.from(agent, names[nameID])
const delegation = await name.grant(DID.parse(recipient).did(), {
const delegation = await names[nameID].grant(DID.parse(recipient).did(), {
readOnly: options?.['read-only'],
})
console.log(await Proof.format(delegation))
Expand Down
4 changes: 2 additions & 2 deletions packages/ucn/src/bin/cmd/import.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ export const handler = async (b64proof) => {
process.exit(1)
}

const name = Name.from(agent, proof)
const name = Name.from(agent, [proof])
await addName(name)

console.log(`${isReadOnly(proof) ? 'r-' : 'rw'}\t${name.did()}`)
console.log(`${isReadOnly([proof]) ? 'r-' : 'rw'}\t${name.did()}`)
}
6 changes: 3 additions & 3 deletions packages/ucn/src/bin/cmd/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { getAgent, getNames, isReadOnly } from '../lib.js'

/** @param {{ l?: boolean }} [options] */
export const handler = async (options) => {
const [agent, names] = await Promise.all([getAgent(), getNames()])
const agent = await getAgent()
const names = await getNames(agent)
if (options?.l)
console.log(`total ${Object.keys(names).length.toLocaleString()}`)
for (const [k, v] of Object.entries(names)) {
if (v.audience.did() !== agent.did()) continue
if (options?.l) {
console.log(`${isReadOnly(v) ? 'r-' : 'rw'}\t${k}`)
console.log(`${isReadOnly(v.proofs) ? 'r-' : 'rw'}\t${k}`)
} else {
console.log(k)
}
Expand Down
11 changes: 9 additions & 2 deletions packages/ucn/src/bin/cmd/remove.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import * as DID from '@ipld/dag-ucan/did'
import { removeName } from '../lib.js'
import { getAgent, getNames, removeName } from '../lib.js'

/** @param {string} id */
export const handler = async (id) => {
await removeName(DID.parse(id).did())
const agent = await getAgent()
const names = await getNames(agent)
const nameID = DID.parse(id).did()
if (!names[nameID]) {
console.error(`unknown name: ${nameID}`)
process.exit(1)
}
await removeName(names[nameID])
}
5 changes: 3 additions & 2 deletions packages/ucn/src/bin/cmd/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import * as Name from '../../name.js'
* @param {{ local?: boolean }} [options]
*/
export const handler = async (id, options) => {
const [agent, names] = await Promise.all([getAgent(), getNames()])
const agent = await getAgent()
const names = await getNames(agent)
const nameID = DID.parse(id).did()
if (!names[nameID]) {
console.error(`unknown name: ${nameID}`)
process.exit(1)
}

const name = Name.from(agent, names[nameID])
const name = names[nameID]
const base = await getValue(name)

let current
Expand Down
Loading
Loading