-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a502c14
commit 34e03d8
Showing
6 changed files
with
455 additions
and
222 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,145 +1,61 @@ | ||
import * as ucan from 'ucan-storage' | ||
import * as ucans from 'ucans' | ||
import { storageSemantics } from 'ucan-common' | ||
import { UcanChain } from './ucan-chain.js' | ||
|
||
export class Service { | ||
/** | ||
* @param {any} privateKey | ||
* @param {ucan.KeyPair} keypair | ||
*/ | ||
constructor(privateKey) { | ||
this.privateKey = privateKey | ||
constructor(keypair) { | ||
this.keypair = keypair | ||
} | ||
|
||
/** | ||
* @param {any} did | ||
*/ | ||
async ucan(did) {} | ||
|
||
/** | ||
* @param {string} encodedUcan | ||
* @param {import('ucan-storage/dist/src/types').ValidateOptions} options | ||
*/ | ||
static async verify(encodedUcan, options) { | ||
const token = await UcanChained.fromToken(encodedUcan, options) | ||
|
||
return token | ||
} | ||
|
||
/** | ||
* @param {ucans.Chained} ucan | ||
* @param {string} key | ||
*/ | ||
static caps(ucan) { | ||
return ucans.capabilities(ucan, storageSemantics) | ||
static async fromPrivateKey(key) { | ||
const kp = await ucan.KeyPair.fromExportedKey(key) | ||
return new Service(kp) | ||
} | ||
} | ||
|
||
export class UcanChained { | ||
/** | ||
* @param {string} encoded | ||
* @param {import('ucan-storage/dist/src/types').Ucan<UcanChained>} decoded | ||
*/ | ||
constructor(encoded, decoded) { | ||
this._encoded = encoded | ||
this._decoded = decoded | ||
static async create() { | ||
return new Service(await ucan.KeyPair.create()) | ||
} | ||
|
||
/** | ||
* @param {string} encodedUcan | ||
* @param {import('ucan-storage/dist/src/types').ValidateOptions} options | ||
* @returns {Promise<UcanChained>} | ||
*/ | ||
static async fromToken(encodedUcan, options) { | ||
const token = await ucan.validate(encodedUcan, options) | ||
|
||
// parse proofs recursively | ||
const proofs = await Promise.all( | ||
token.payload.prf.map((encodedPrf) => | ||
UcanChained.fromToken(encodedPrf, options) | ||
) | ||
) | ||
|
||
// check sender/receiver matchups. A parent ucan's audience must match the child ucan's issuer | ||
const incorrectProof = proofs.find( | ||
(proof) => proof.audience() !== token.payload.iss | ||
) | ||
if (incorrectProof) { | ||
throw new Error( | ||
`Invalid UCAN: Audience ${incorrectProof.audience()} doesn't match issuer ${ | ||
token.payload.iss | ||
}` | ||
) | ||
} | ||
|
||
const ucanTransformed = { | ||
...token, | ||
payload: { | ||
...token.payload, | ||
prf: proofs, | ||
}, | ||
} | ||
|
||
return new UcanChained(encodedUcan, ucanTransformed) | ||
} | ||
|
||
/** | ||
* A representation of delegated capabilities throughout all ucan chains | ||
* | ||
* @template A | ||
* @param {(ucan: import('ucan-storage/dist/src/types').Ucan, reducedProofs: () => Iterable<A>) => A} reduceLayer | ||
* @returns {A} | ||
*/ | ||
reduce(reduceLayer) { | ||
// eslint-disable-next-line unicorn/no-this-assignment | ||
const that = this | ||
async validate(encodedUcan, options) { | ||
const token = await UcanChain.fromToken(encodedUcan, options) | ||
|
||
function* reduceProofs() { | ||
for (const proof of that.proofs()) { | ||
// eslint-disable-next-line unicorn/no-array-reduce | ||
yield proof.reduce((accumulator, element) => | ||
reduceLayer(accumulator, element) | ||
) | ||
} | ||
if (token.audience() !== this.did()) { | ||
throw new Error('Invalid UCAN: Audience does not match this service.') | ||
} | ||
|
||
return reduceLayer(this.payload(), reduceProofs) | ||
} | ||
|
||
/** | ||
* @returns { UcanChained[]} `prf`: Further UCANs possibly providing proof or origin for some capabilities in this UCAN. | ||
*/ | ||
proofs() { | ||
return this._decoded.payload.prf | ||
return token | ||
} | ||
|
||
/** | ||
* | ||
* This is the identity this UCAN transfers rights to. | ||
* It could e.g. be the DID of a service you're posting this UCAN as a JWT to, | ||
* or it could be the DID of something that'll use this UCAN as a proof to | ||
* continue the UCAN chain as an issuer. | ||
* @param {ucans.Chained} ucan | ||
*/ | ||
audience() { | ||
return this._decoded.payload.aud | ||
static caps(ucan) { | ||
return ucans.capabilities(ucan, storageSemantics) | ||
} | ||
|
||
/** | ||
* The UCAN must be signed with the private key of the issuer to be valid. | ||
*/ | ||
issuer() { | ||
return this._decoded.payload.iss | ||
did() { | ||
return this.keypair.did() | ||
} | ||
|
||
/** | ||
* The payload the top level represented by this Chain element. | ||
* Its proofs are omitted. To access proofs, use `.proofs()` | ||
* @param {any} did | ||
*/ | ||
payload() { | ||
return { | ||
...this._decoded, | ||
payload: { | ||
...this._decoded.payload, | ||
prf: [], | ||
}, | ||
} | ||
ucan(did) { | ||
return ucan.Ucan({ | ||
issuer: this.keypair, | ||
audience: did, | ||
capabilities: [{ with: `storage://${did}`, can: 'upload/*' }], | ||
}) | ||
} | ||
} |
Oops, something went wrong.