Skip to content

Commit 978dfd2

Browse files
authored
Implement 0.8 spec (#52)
* Adapt to 0.8 spec * Readme feedback
1 parent 46f406d commit 978dfd2

32 files changed

Lines changed: 990 additions & 628 deletions

.eslintrc.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,20 @@ module.exports = {
1313
"plugin:@typescript-eslint/recommended",
1414
],
1515
rules: {
16-
"@typescript-eslint/member-delimiter-style": ["error", {
16+
"@typescript-eslint/member-delimiter-style": [ "error", {
1717
"multiline": {
1818
"delimiter": "none",
1919
"requireLast": false
2020
},
21-
}],
22-
"@typescript-eslint/no-use-before-define": ["off"],
23-
"@typescript-eslint/semi": ["error", "never"],
21+
} ],
22+
"@typescript-eslint/no-use-before-define": [ "off" ],
23+
"@typescript-eslint/semi": [ "error", "never" ],
2424
"@typescript-eslint/ban-ts-comment": 1,
25-
"@typescript-eslint/quotes": ["error", "double", {
25+
"@typescript-eslint/quotes": [ "error", "double", {
2626
allowTemplateLiterals: true
27-
}],
27+
} ],
2828
// If you want to *intentionally* run a promise without awaiting, prepend it with "void " instead of "await "
29-
"@typescript-eslint/no-floating-promises": ["error"],
29+
"@typescript-eslint/no-floating-promises": [ "error" ],
30+
"@typescript-eslint/no-inferrable-types": [ "off" ],
3031
}
3132
}

README.md

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ At a high level, UCANs (“User Controlled Authorization Network”) are an auth
99

1010
No all-powerful authorization server or server of any kind is required for UCANs. Instead, everything a user can do is captured directly in a key or token, which can be sent to anyone who knows how to interpret the UCAN format. Because UCANs are self-contained, they are easy to consume permissionlessly, and they work well offline and in distributed systems.
1111

12-
1312
UCANs work
1413
- Server -> Server
1514
- Client -> Server
@@ -20,14 +19,18 @@ UCANs work
2019
Read more in the whitepaper: https://whitepaper.fission.codes/access-control/ucan
2120

2221

22+
2323
## Structure
24-
### Header
24+
25+
### Header
26+
2527
`alg`, Algorithm, the type of signature.
2628

2729
`typ`, Type, the type of this data structure, JWT.
2830

2931
`uav`, UCAN version.
30-
### Payload
32+
33+
### Payload
3134

3235
`att`, Attenuation, a list of resources and capabilities that the ucan grants.
3336

@@ -44,41 +47,52 @@ Read more in the whitepaper: https://whitepaper.fission.codes/access-control/uca
4447
`prf`, Proof, an optional nested token with equal or greater privileges.
4548

4649
### Signature
50+
4751
A signature (using `alg`) of the base64 encoded header and payload concatenated together and delimited by `.`
4852

49-
## Build params
50-
Use `ucan.build` to help in formatting and signing a ucan. It takes the following parameters
53+
54+
55+
## Build
56+
57+
`ucan.build` can be used to help in formatting and signing a UCAN. It takes the following parameters:
5158
```ts
52-
export type BuildParams = {
53-
// to/from
54-
audience: string
59+
type BuildParams = {
60+
// from/to
5561
issuer: Keypair
62+
audience: string
5663

5764
// capabilities
58-
capabilities: Array<Capability>
65+
capabilities?: Array<Capability>
5966

6067
// time bounds
6168
lifetimeInSeconds?: number // expiration overrides lifetimeInSeconds
6269
expiration?: number
6370
notBefore?: number
6471

65-
// proof / other info
72+
// proofs / other info
6673
facts?: Array<Fact>
67-
proof?: string
68-
69-
// in the weeds
70-
ucanVersion?: string
74+
proofs?: Array<string>
75+
addNonce?: boolean
7176
}
7277
```
78+
7379
### Capabilities
74-
`capabilities` is an array of resources and permission level formatted as:
80+
81+
`capabilities` is an array of resource pointers and abilities:
7582
```ts
7683
{
77-
$TYPE: $IDENTIFIER,
78-
"cap": $CAPABILITY
84+
// `with` is a resource pointer in the form of a URI, which has a `scheme` and `hierPart`.
85+
// → "mailto:boris@fission.codes"
86+
with: { scheme: "mailto", hierPart: "boris@fission.codes" },
87+
88+
// `can` is an ability, which always has a namespace and optional segments.
89+
// → "msg/SEND"
90+
can: { namespace: "msg", segments: [ "SEND" ] }
7991
}
8092
```
8193
94+
95+
8296
## Installation
8397
8498
### NPM:
@@ -95,25 +109,25 @@ yarn add ucans
95109
96110
## Example
97111
```ts
98-
import * as ucan from 'ucans'
112+
import * as ucan from "ucans"
99113

100114
// in-memory keypair
101115
const keypair = await ucan.EdKeypair.create()
102116
const u = await ucan.build({
103-
audience: "did:key:zabcde...", //recipient DID
104-
issuer: keypair, //signing key
117+
audience: "did:key:zabcde...", // recipient DID
118+
issuer: keypair, // signing key
105119
capabilities: [ // permissions for ucan
106120
{
107-
"wnfs": "boris.fission.name/public/photos/",
108-
"cap": "OVERWRITE"
121+
with: { scheme: "wnfs", hierPart: "//boris.fission.name/public/photos/" },
122+
can: { namespace: "wnfs", segments: [ "OVERWRITE" ] }
109123
},
110124
{
111-
"wnfs": "boris.fission.name/private/4tZA6S61BSXygmJGGW885odfQwpnR2UgmCaS5CfCuWtEKQdtkRnvKVdZ4q6wBXYTjhewomJWPL2ui3hJqaSodFnKyWiPZWLwzp1h7wLtaVBQqSW4ZFgyYaJScVkBs32BThn6BZBJTmayeoA9hm8XrhTX4CGX5CVCwqvEUvHTSzAwdaR",
112-
"cap": "APPEND"
125+
with: { scheme: "wnfs", hierPart: "//boris.fission.name/private/4tZA6S61BSXygmJGGW885odfQwpnR2UgmCaS5CfCuWtEKQdtkRnvKVdZ4q6wBXYTjhewomJWPL2ui3hJqaSodFnKyWiPZWLwzp1h7wLtaVBQqSW4ZFgyYaJScVkBs32BThn6BZBJTmayeoA9hm8XrhTX4CGX5CVCwqvEUvHTSzAwdaR" },
126+
can: { namespace: "wnfs", segments: [ "APPEND" ] }
113127
},
114128
{
115-
"email": "boris@fission.codes",
116-
"cap": "SEND"
129+
with: { scheme: "mailto", hierPart: "boris@fission.codes" },
130+
can: { namespace: "wnfs", segments: [ "SEND" ] }
117131
}
118132
]
119133
})
@@ -124,6 +138,8 @@ const payload = await ucan.buildPayload(...)
124138
const u = await ucan.sign(payload, keyType, signingFn)
125139
```
126140

141+
142+
127143
## Sponsors
128144

129145
Sponsors that contribute developer time or resources to this implementation of UCANs:
@@ -133,4 +149,5 @@ Sponsors that contribute developer time or resources to this implementation of U
133149

134150

135151
## UCAN Toucan
152+
136153
![](https://ipfs.runfission.com/ipfs/QmcyAwK7AjvLXbGuL4cqG5nufEKJquFmFGo2SDsaAe939Z)

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@
3333
"dependencies": {
3434
"@stablelib/ed25519": "^1.0.2",
3535
"one-webcrypto": "^1.0.1",
36+
"semver": "^7.3.5",
3637
"uint8arrays": "^3.0.0"
3738
},
3839
"devDependencies": {
3940
"@types/jest": "^27.0.1",
4041
"@types/node": "^16.9.1",
42+
"@types/semver": "^7.3.9",
4143
"@typescript-eslint/eslint-plugin": "^5.5.0",
4244
"@typescript-eslint/parser": "^5.5.0",
4345
"eslint": "^8.3.0",

src/attenuation.ts

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
// https://whitepaper.fission.codes/access-control/ucan/jwt-authentication#attenuation
2-
import { Capability, Ucan } from "./types"
1+
// https://github.com/ucan-wg/spec/blob/dd4ac83f893cef109f5a26b07970b2484f23aabf/README.md#325-attenuation-scope
2+
import { Capability } from "./capability"
33
import { Chained } from "./chained"
4+
import { Ucan } from "./types"
45
import * as util from "./util"
56

67

8+
// TYPES
9+
10+
711
export interface CapabilitySemantics<A> {
812
/**
913
* Try to parse a capability into a representation used for
@@ -14,6 +18,7 @@ export interface CapabilitySemantics<A> {
1418
* return `null`.
1519
*/
1620
tryParsing(cap: Capability): A | null
21+
1722
/**
1823
* This figures out whether a given `childCap` can be delegated from `parentCap`.
1924
* There are three possible results with three return types respectively:
@@ -22,62 +27,57 @@ export interface CapabilitySemantics<A> {
2227
* - `CapabilityEscalation<A>`: It's clear that `childCap` is meant to be delegated from `parentCap`, but there's a rights escalation.
2328
*/
2429
tryDelegating(parentCap: A, childCap: A): A | null | CapabilityEscalation<A>
25-
// TODO builders
2630
}
2731

32+
export type CapabilityResult<A>
33+
= CapabilityWithInfo<A>
34+
| CapabilityEscalation<A>
2835

2936
export interface CapabilityInfo {
3037
originator: string // DID
3138
expiresAt: number
3239
notBefore?: number
3340
}
3441

35-
3642
export interface CapabilityWithInfo<A> {
3743
info: CapabilityInfo
3844
capability: A
3945
}
4046

41-
42-
export type CapabilityResult<A>
43-
= CapabilityWithInfo<A>
44-
| CapabilityEscalation<A>
45-
46-
4747
export interface CapabilityEscalation<A> {
4848
escalation: string // reason
4949
capability: A // the capability that escalated rights
5050
}
5151

52+
53+
54+
// TYPE CHECKING
55+
56+
5257
export function isCapabilityEscalation<A>(obj: unknown): obj is CapabilityEscalation<A> {
5358
return util.isRecord(obj)
5459
&& util.hasProp(obj, "escalation") && typeof obj.escalation === "string"
5560
&& util.hasProp(obj, "capability")
5661
}
5762

58-
export function hasCapability<Cap>(semantics: CapabilitySemantics<Cap>, capability: CapabilityWithInfo<Cap>, ucan: Chained): CapabilityWithInfo<Cap> | false {
59-
for (const cap of capabilities(ucan, semantics)) {
60-
if (isCapabilityEscalation(cap)) {
61-
continue
62-
}
6363

64-
const delegatedCapability = semantics.tryDelegating(cap.capability, capability.capability)
6564

66-
if (isCapabilityEscalation(delegatedCapability)) {
67-
continue
68-
}
65+
// PARSING
6966

70-
if (delegatedCapability != null) {
71-
return {
72-
info: delegateCapabilityInfo(capability.info, cap.info),
73-
capability: delegatedCapability,
74-
}
75-
}
76-
}
7767

78-
return false
68+
function parseCapabilityInfo(ucan: Ucan<never>): CapabilityInfo {
69+
return {
70+
originator: ucan.payload.iss,
71+
expiresAt: ucan.payload.exp,
72+
...(ucan.payload.nbf != null ? { notBefore: ucan.payload.nbf } : {}),
73+
}
7974
}
8075

76+
77+
78+
// FUNCTIONS
79+
80+
8181
export function canDelegate<A>(semantics: CapabilitySemantics<A>, capability: A, ucan: Chained): boolean {
8282
for (const cap of capabilities(ucan, semantics)) {
8383
if (isCapabilityEscalation(cap)) {
@@ -98,7 +98,6 @@ export function canDelegate<A>(semantics: CapabilitySemantics<A>, capability: A,
9898
return false
9999
}
100100

101-
102101
export function capabilities<A>(
103102
ucan: Chained,
104103
capability: CapabilitySemantics<A>,
@@ -171,10 +170,29 @@ function delegateCapabilityInfo(childInfo: CapabilityInfo, parentInfo: Capabilit
171170
}
172171
}
173172

174-
function parseCapabilityInfo(ucan: Ucan<never>): CapabilityInfo {
175-
return {
176-
originator: ucan.payload.iss,
177-
expiresAt: ucan.payload.exp,
178-
...(ucan.payload.nbf != null ? { notBefore: ucan.payload.nbf } : {}),
173+
export function hasCapability<Cap>(
174+
semantics: CapabilitySemantics<Cap>,
175+
capability: CapabilityWithInfo<Cap>,
176+
ucan: Chained
177+
): CapabilityWithInfo<Cap> | false {
178+
for (const cap of capabilities(ucan, semantics)) {
179+
if (isCapabilityEscalation(cap)) {
180+
continue
181+
}
182+
183+
const delegatedCapability = semantics.tryDelegating(cap.capability, capability.capability)
184+
185+
if (isCapabilityEscalation(delegatedCapability)) {
186+
continue
187+
}
188+
189+
if (delegatedCapability != null) {
190+
return {
191+
info: delegateCapabilityInfo(capability.info, cap.info),
192+
capability: delegatedCapability,
193+
}
194+
}
179195
}
196+
197+
return false
180198
}

src/builder.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as token from "./token"
22
import * as util from "./util"
3-
import { Capability, Keypair, Fact, UcanPayload, isKeypair, isCapability } from "./types"
3+
import { Keypair, Fact, UcanPayload, isKeypair } from "./types"
4+
import { Capability, isCapability } from "./capability"
45
import { CapabilityInfo, CapabilitySemantics, canDelegate } from "./attenuation"
56
import { Chained } from "./chained"
67
import { Store } from "./store"
@@ -158,7 +159,7 @@ export class Builder<State extends Partial<BuildableState>> {
158159
}
159160
return new Builder(this.state, {
160161
...this.defaultable,
161-
facts: [...this.defaultable.facts, fact, ...facts]
162+
facts: [ ...this.defaultable.facts, fact, ...facts ]
162163
})
163164
}
164165

@@ -180,7 +181,7 @@ export class Builder<State extends Partial<BuildableState>> {
180181
}
181182
return new Builder(this.state, {
182183
...this.defaultable,
183-
capabilities: [...this.defaultable.capabilities, capability, ...capabilities]
184+
capabilities: [ ...this.defaultable.capabilities, capability, ...capabilities ]
184185
})
185186
}
186187

@@ -231,9 +232,9 @@ export class Builder<State extends Partial<BuildableState>> {
231232
}
232233
return new Builder(this.state, {
233234
...this.defaultable,
234-
capabilities: [...this.defaultable.capabilities, requiredCapability],
235+
capabilities: [ ...this.defaultable.capabilities, requiredCapability ],
235236
proofs: this.defaultable.proofs.find(proof => proof.encoded() === storeOrProof.encoded()) == null
236-
? [...this.defaultable.proofs, storeOrProof]
237+
? [ ...this.defaultable.proofs, storeOrProof ]
237238
: this.defaultable.proofs
238239
})
239240
} else {
@@ -243,9 +244,9 @@ export class Builder<State extends Partial<BuildableState>> {
243244
if (result.success) {
244245
return new Builder(this.state, {
245246
...this.defaultable,
246-
capabilities: [...this.defaultable.capabilities, requiredCapability],
247+
capabilities: [ ...this.defaultable.capabilities, requiredCapability ],
247248
proofs: this.defaultable.proofs.find(proof => proof.encoded() === result.ucan.encoded()) == null
248-
? [...this.defaultable.proofs, result.ucan]
249+
? [ ...this.defaultable.proofs, result.ucan ]
249250
: this.defaultable.proofs
250251
})
251252
} else {

0 commit comments

Comments
 (0)