iso-ucan 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/store.js ADDED
@@ -0,0 +1,187 @@
1
+ import { parse as didParse } from 'iso-did'
2
+ import { KV } from 'iso-kv'
3
+ import merge from 'it-merge'
4
+
5
+ /**
6
+ * @import {Delegation} from './delegation.js'
7
+ * @import {Driver} from 'iso-kv'
8
+ * @import {CID} from 'multiformats'
9
+ */
10
+
11
+ const POWERLINE = '$pwrl'
12
+
13
+ /**
14
+ * Store
15
+ */
16
+ export class Store {
17
+ /** @type {Driver} */
18
+ driver
19
+
20
+ /** @type {KV} */
21
+ kv
22
+
23
+ /**
24
+ * @param {Driver} driver
25
+ */
26
+ constructor(driver) {
27
+ this.driver = driver
28
+
29
+ this.kv = new KV({
30
+ driver,
31
+ })
32
+ }
33
+
34
+ /**
35
+ * Set a delegation in the store by CID and index it by cmd and aud
36
+ *
37
+ * @param {Delegation} delegation
38
+ */
39
+ async #set(delegation) {
40
+ const options = {
41
+ expiration: delegation.envelope.payload.exp,
42
+ }
43
+ const cid = delegation.cid.toString()
44
+
45
+ // Index by CID
46
+ await this.kv.set([cid], delegation, options)
47
+
48
+ // Index by subject and audience
49
+ /** @type {string | null} */
50
+ let sub = delegation.sub ? delegation.sub : null
51
+ if (sub === null) {
52
+ sub = POWERLINE
53
+ }
54
+ await this.kv.set([sub, delegation.aud, cid], cid, options)
55
+ }
56
+
57
+ /**
58
+ * @param {Delegation[]} delegations
59
+ */
60
+ async add(delegations) {
61
+ for (const delegation of delegations) {
62
+ await this.#set(delegation)
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Get a delegation by cid
68
+ *
69
+ * @param {string} cid
70
+ */
71
+ get(cid) {
72
+ return /** @type {typeof this.kv.get<Delegation>} */ (this.kv.get)([cid])
73
+ }
74
+
75
+ /**
76
+ * Resolve a proof by CID
77
+ *
78
+ * @param {CID} cid
79
+ */
80
+ async resolveProof(cid) {
81
+ const delegation = await this.get(cid.toString())
82
+ if (!delegation) {
83
+ throw new Error(`Delegation not found: ${cid.toString()}`)
84
+ }
85
+ return delegation
86
+ }
87
+
88
+ /**
89
+ * List proofs by sub and aud
90
+ *
91
+ * @param {import('./types.js').StoreProofsOptions} options
92
+ */
93
+ async *proofs(options) {
94
+ /**@type {string | null} */
95
+ let sub = options.sub ? options.sub.toString() : null
96
+ /**@type {string | undefined} */
97
+ let aud = options.aud ? options.aud.toString() : undefined
98
+
99
+ if (!aud && !sub) {
100
+ throw new Error('No audience or subject provided')
101
+ }
102
+ if (!aud && sub) {
103
+ aud = sub
104
+ }
105
+ if (sub === null) {
106
+ sub = POWERLINE
107
+ }
108
+ if (!aud) {
109
+ throw new Error('No audience provided')
110
+ }
111
+
112
+ for await (const element of this.kv.list({
113
+ prefix: [sub, aud],
114
+ })) {
115
+ const value = await this.get(element.value)
116
+
117
+ if (value) {
118
+ yield value
119
+ }
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Resolve a single chain of proofs ending with a root (subject === issuer).
125
+ * Returns the first such chain found (depth-first).
126
+ *
127
+ * @param {import('./types.js').StoreProofsOptions} options
128
+ * @returns {Promise<Delegation[]>}
129
+ */
130
+ async chain({ aud, sub, cmd }) {
131
+ // console.log('🚀 ~ aud', aud, 'sub', sub, 'cmd', cmd)
132
+ const parents = parentCmds(cmd)
133
+
134
+ const sources = merge(
135
+ this.proofs({ sub, aud, cmd }),
136
+ this.proofs({ sub: null, aud, cmd })
137
+ )
138
+
139
+ for await (const proof of sources) {
140
+ if (!parents.includes(proof.cmd)) continue
141
+
142
+ // If root, return this proof as the end of the path
143
+ if (proof.sub === didParse(proof.iss).did) {
144
+ // console.log('🚀 ~ found root:', proof.iss, proof.sub, proof.cmd)
145
+ return [proof]
146
+ }
147
+
148
+ // Otherwise, go deeper and prepend this proof if a path is found
149
+ // console.log('🚀 ~ trying deeper:', proof.iss, proof.sub, proof.cmd)
150
+ const nextPath = await this.chain({
151
+ aud: didParse(proof.iss).did,
152
+ sub: proof.sub ? proof.sub : sub,
153
+ cmd: proof.cmd,
154
+ })
155
+ if (nextPath?.length) {
156
+ return [proof, ...nextPath]
157
+ }
158
+ // console.log('🚀 ~ no path found')
159
+ }
160
+
161
+ // No path found
162
+ return []
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Returns all parent commands for a given command string.
168
+ *
169
+ * @param {string} cmd
170
+ * @returns {string[]}
171
+ * @example
172
+ * ```ts twoslash
173
+ * import { parentCmds } from 'iso-ucan/store'
174
+ * parentCmds('/foo/bar/baz') // ["/", "/foo", "/foo/bar", "/foo/bar/baz"]
175
+ * ```
176
+ */
177
+ function parentCmds(cmd) {
178
+ if (!cmd || cmd === '/') return ['/']
179
+ const parts = cmd.split('/').filter(Boolean)
180
+ const result = ['/']
181
+ let current = ''
182
+ for (const part of parts) {
183
+ current += `/${part}`
184
+ result.push(current)
185
+ }
186
+ return result
187
+ }
@@ -0,0 +1,131 @@
1
+ import type { DID, DIDURL } from 'iso-did/types'
2
+ import type { ISigner } from 'iso-signatures/types'
3
+ import type { CID } from 'multiformats'
4
+ import type { Policy } from './policy'
5
+ import type { VarsigAlgorithm, VarsigEncoding } from './varsig'
6
+
7
+ /**
8
+ * Envelope types
9
+ */
10
+
11
+ export type PayloadSpec = 'dlg' | 'inv'
12
+ export type PayloadTag = `ucan/${PayloadSpec}@${string}`
13
+ export interface PayloadBase {
14
+ /**
15
+ * Issuer DID (sender)
16
+ */
17
+ iss: DIDURL
18
+ /**
19
+ * Audience DID (receiver)
20
+ */
21
+ aud?: DID
22
+ /**
23
+ * Principal that the chain is about (the [Subject])
24
+ */
25
+ sub?: DID | null
26
+ /**
27
+ * The Command to eventually invoke
28
+ * @see https://github.com/ucan-wg/spec#command
29
+ */
30
+ cmd: string
31
+ /**
32
+ * The nonce of the UCAN
33
+ * @see https://github.com/ucan-wg/spec#nonce
34
+ */
35
+ nonce: Uint8Array
36
+ /**
37
+ * Meta (asserted, signed data) — is not delegated authority
38
+ */
39
+ meta?: Record<string, unknown>
40
+ /**
41
+ * Expiration UTC Unix Timestamp in seconds (valid until)
42
+ */
43
+ exp: number | null
44
+ /**
45
+ * "Not before" UTC Unix Timestamp in seconds (valid from)
46
+ */
47
+ nbf?: number
48
+ }
49
+
50
+ /**
51
+ * @see https://github.com/ucan-wg/invocation#invocation-payload
52
+ */
53
+ export interface InvocationPayload extends PayloadBase {
54
+ sub: DID
55
+ /**
56
+ * Optional if equal to subject
57
+ */
58
+ aud?: DID
59
+
60
+ /**
61
+ * Any [Arguments] that MUST be present in the Invocation
62
+ */
63
+ args: Record<string, unknown>
64
+ /**
65
+ * Delegations that prove the chain of authority
66
+ */
67
+ prf: CID[]
68
+ /**
69
+ * Issued at time
70
+ */
71
+ iat?: number
72
+ /**
73
+ * An OPTIONAL CID of the Receipt that enqueued the Task
74
+ */
75
+ cause?: CID
76
+ }
77
+
78
+ export interface DelegationPayload<Args = unknown> extends PayloadBase {
79
+ aud: DID
80
+ /**
81
+ * Subject can be null here for Powerline
82
+ * @see https://github.com/ucan-wg/delegation/#powerline
83
+ */
84
+ sub: DID | null
85
+ pol: Policy<Args>
86
+ }
87
+
88
+ export type Payload<Args = unknown> =
89
+ | InvocationPayload
90
+ | DelegationPayload<Args>
91
+ export type SignaturePayload<Args = unknown> = {
92
+ h: Uint8Array
93
+ [payloadTag: PayloadTag]: Payload<Args>
94
+ }
95
+
96
+ export type Envelope<Args = unknown> = [Uint8Array, SignaturePayload<Args>]
97
+
98
+ /**
99
+ * @see https://github.com/ucan-wg/spec#envelope
100
+ */
101
+ export type EnvelopeEncodeOptions = {
102
+ signature: Uint8Array
103
+ signaturePayload: SignaturePayload
104
+ }
105
+
106
+ export interface EnvelopeSignOptions {
107
+ /**
108
+ * @default '1.0.0'
109
+ */
110
+ version?: string
111
+ /**
112
+ * @default 'DAG-CBOR'
113
+ */
114
+ enc?: VarsigEncoding
115
+ spec: PayloadSpec
116
+ payload: Payload
117
+ signer: ISigner
118
+ }
119
+
120
+ export interface EnvelopeDecodeOptions {
121
+ envelope: Uint8Array
122
+ }
123
+
124
+ export type DecodedEnvelope<Spec extends PayloadSpec> = {
125
+ alg: VarsigAlgorithm
126
+ enc: VarsigEncoding
127
+ signature: Uint8Array
128
+ payload: Spec extends 'dlg' ? DelegationPayload : InvocationPayload
129
+ spec: Spec
130
+ version: string
131
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Policy types
3
+ * @see https://github.com/ucan-wg/delegation/#policy
4
+ */
5
+
6
+ /**
7
+ * Type-safe selector utilities
8
+ */
9
+
10
+ /**
11
+ * Extracts all possible paths from a data structure as string literals
12
+ */
13
+ type PathsToStringProps<T> = T extends
14
+ | string
15
+ | number
16
+ | boolean
17
+ | null
18
+ | undefined
19
+ ? never
20
+ : T extends Array<infer U>
21
+ ?
22
+ | `[]`
23
+ | `[${number}]`
24
+ | `[${number}:${number}]`
25
+ | `[${number}:]`
26
+ | `[:${number}]`
27
+ | `[:]`
28
+ | PathsToStringProps<U>
29
+ : T extends object
30
+ ? {
31
+ [K in keyof T & (string | number)]: K extends string
32
+ ? `.${K}` | `.${K}?` | `.${K}${PathsToStringProps<T[K]>}`
33
+ : `[${K}]` | `[${K}]?` | `[${K}]${PathsToStringProps<T[K]>}`
34
+ }[keyof T & (string | number)]
35
+ : never
36
+
37
+ /**
38
+ * Flattens nested path strings into a union of all possible selectors
39
+ */
40
+ type FlattenPaths<T> = T extends string
41
+ ? T
42
+ : T extends object
43
+ ? {
44
+ [K in keyof T]: T[K] extends string
45
+ ? T[K]
46
+ : T[K] extends object
47
+ ? FlattenPaths<T[K]>
48
+ : never
49
+ }[keyof T]
50
+ : never
51
+
52
+ /**
53
+ * Type-safe selector that only allows valid paths for the given data structure
54
+ */
55
+ export type Selector<Args = unknown> = Args extends unknown
56
+ ? FlattenPaths<PathsToStringProps<Args>> | '.'
57
+ : string
58
+
59
+ /**
60
+ * Extracts the type of the value at a given selector path (minimal version)
61
+ */
62
+ type SelectorValue<T, S extends string> = S extends `.${infer K}`
63
+ ? K extends keyof T
64
+ ? T[K]
65
+ : never
66
+ : S extends `[${infer I}]`
67
+ ? T extends Array<infer U>
68
+ ? U
69
+ : never
70
+ : never
71
+
72
+ /** Connectives */
73
+ export type EqualityOp = '==' | '!='
74
+ export type Equality<Args = unknown> = [EqualityOp, Selector<Args>, unknown]
75
+ export type InequalityOp = '<' | '<=' | '>' | '>='
76
+ export type Inequality<Args = unknown> = [InequalityOp, Selector<Args>, number]
77
+
78
+ export type NegateOp = 'not'
79
+ export type Negate<Args = unknown> = [NegateOp, Statement<Args>]
80
+
81
+ export type ConnectiveOp = 'and' | 'or'
82
+ export type Connective<Args = unknown> = [ConnectiveOp, Statement<Args>[]]
83
+
84
+ export type QuantifierOp = 'all' | 'any'
85
+ export type Quantifier<
86
+ Args = unknown,
87
+ Sel extends Selector<Args> = Selector<Args>,
88
+ > = [QuantifierOp, Sel, Statement<SelectorValue<Args, Sel>>]
89
+ export type LikeOp = 'like'
90
+ export type Like<Args = unknown> = [LikeOp, Selector<Args>, string]
91
+
92
+ export type Statement<Args = unknown> =
93
+ | Equality<Args>
94
+ | Inequality<Args>
95
+ | Negate<Args>
96
+ | Connective<Args>
97
+ | Quantifier<Args>
98
+ | Like<Args>
99
+
100
+ export type Policy<Args = unknown> = Statement<Args>[]
@@ -0,0 +1,22 @@
1
+ import type { SignatureType } from 'iso-signatures/types'
2
+
3
+ /**
4
+ * Varsig types
5
+ */
6
+
7
+ /**
8
+ * Supported encoding types for data to be signed or verified
9
+ */
10
+ export type VarsigEncoding = 'RAW' | 'DAG-CBOR'
11
+
12
+ /**
13
+ * Supported signature types
14
+ */
15
+ export type VarsigAlgorithm = SignatureType
16
+
17
+ export interface VarsigOptions {
18
+ enc: VarsigEncoding
19
+ alg: VarsigAlgorithm
20
+ }
21
+
22
+ export type DecodeOutput = VarsigOptions
package/src/types.ts ADDED
@@ -0,0 +1,243 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec'
2
+ import type { Resolver as _DidResolver } from 'iso-did'
3
+ import type { DID, DIDURLObject, VerifiableDID } from 'iso-did/types'
4
+ import type { ISigner } from 'iso-signatures/types'
5
+ import type { Resolver } from 'iso-signatures/verifiers/resolver.js'
6
+ import type { CID } from 'multiformats'
7
+ import type { Capability } from './capability'
8
+ import type { Delegation } from './delegation'
9
+ import type { Invocation } from './invocation'
10
+ import type { Store } from './store'
11
+ import type { Policy } from './types/policy'
12
+
13
+ export type { StandardSchemaV1 } from '@standard-schema/spec'
14
+ export type { DID, DIDURL, DIDURLObject } from 'iso-did/types'
15
+ export * from './types/envelope'
16
+ export * from './types/policy'
17
+ export * from './types/varsig'
18
+
19
+ export type ResolveProof = (proof: CID) => Promise<Delegation>
20
+ export type IsRevoked = (cid: CID) => Promise<boolean>
21
+ export type VerifierResolver = Resolver
22
+ export type DidResolver = _DidResolver
23
+
24
+ export type CborPrimitive =
25
+ | string
26
+ | number
27
+ | boolean
28
+ | null
29
+ | bigint
30
+ | Uint8Array
31
+ | ArrayBuffer
32
+ export type CborArray = CborValue[] | readonly CborValue[]
33
+ export type CborObject =
34
+ | { [Key in string]: CborValue }
35
+ | { [Key in string]?: CborValue | undefined }
36
+
37
+ export type CborValue = CborPrimitive | CborArray | CborObject
38
+
39
+ /**
40
+ * Delegation
41
+ */
42
+
43
+ export interface DelegationOptions<Args = unknown> {
44
+ iss: ISigner
45
+ aud: DID
46
+ sub: DID | null
47
+ pol: Policy<Args>
48
+ /**
49
+ * Expiration time in seconds or null for no expiration
50
+ */
51
+ exp?: number | null
52
+ /**
53
+ * Time to live in seconds, expiration thats precedence over ttl
54
+ * @default 300
55
+ */
56
+ ttl?: number
57
+ /**
58
+ * Not before time in seconds
59
+ * Delegation is not valid before this time
60
+ */
61
+ nbf?: number
62
+ nonce?: Uint8Array
63
+ cmd: string
64
+ meta?: CborObject
65
+ }
66
+
67
+ export interface DelegationFromOptions {
68
+ bytes: Uint8Array
69
+ isRevoked?: IsRevoked
70
+ didResolver?: DidResolver
71
+ verifierResolver: VerifierResolver
72
+ }
73
+
74
+ export interface DelegationValidateOptions {
75
+ isRevoked?: IsRevoked
76
+ didResolver?: DidResolver
77
+ verifierResolver: VerifierResolver
78
+ }
79
+
80
+ export type CapabilityDelegateOptions<Args = unknown> = Omit<
81
+ DelegationOptions<Args>,
82
+ 'cmd'
83
+ >
84
+ /**
85
+ * Invocation
86
+ */
87
+
88
+ /**
89
+ * Invocation options
90
+ */
91
+ export interface InvocationOptions {
92
+ iss: ISigner
93
+ aud?: DID
94
+ sub: DID
95
+ cmd: string
96
+ args: CborObject
97
+ /**
98
+ * Proofs of the chain of authority
99
+ */
100
+ prf: Delegation[]
101
+ /**
102
+ * Expiration time in seconds or null for no expiration
103
+ */
104
+ exp?: number | null
105
+ /**
106
+ * Time to live in seconds, expiration thats precedence over ttl
107
+ * @default 300
108
+ */
109
+ ttl?: number
110
+ /**
111
+ * Issued at time in seconds
112
+ */
113
+ iat?: number
114
+ nbf?: number
115
+ nonce?: Uint8Array
116
+ cause?: CID
117
+ meta?: CborObject
118
+ }
119
+
120
+ export interface InvocationFromOptions {
121
+ bytes: Uint8Array
122
+ audience: VerifiableDID
123
+ didResolver?: DidResolver
124
+ verifierResolver: VerifierResolver
125
+ isRevoked?: IsRevoked
126
+ resolveProof: ResolveProof
127
+ }
128
+
129
+ export interface CapabilityInvokeOptions<Schema extends StandardSchemaV1>
130
+ extends Omit<InvocationOptions, 'cmd' | 'args' | 'prf'> {
131
+ args: StandardSchemaV1.InferOutput<Schema>
132
+ store: Store
133
+ }
134
+
135
+ /**
136
+ * Store types
137
+ */
138
+
139
+ /**
140
+ * Resolve proofs options
141
+ */
142
+ export interface StoreProofsOptions {
143
+ aud?: DID
144
+ sub: DID | null
145
+ cmd: string
146
+ }
147
+
148
+ /**
149
+ * Server types
150
+ */
151
+ export type Promisable<T> = T | PromiseLike<T>
152
+
153
+ export interface HandlerOptions<Schema extends StandardSchemaV1> {
154
+ args: StandardSchemaV1.InferOutput<Schema>
155
+ store: Store
156
+ }
157
+
158
+ export type Handler<Schema extends StandardSchemaV1, Output> = (
159
+ options: HandlerOptions<Schema>
160
+ ) => Promisable<Output>
161
+
162
+ export interface RouteOptions<
163
+ Cap extends Capability<StandardSchemaV1>,
164
+ Output,
165
+ > {
166
+ capability: Cap
167
+ handler: Handler<Cap['schema'], Output>
168
+ }
169
+
170
+ export type RouteOutput<Cap extends Capability<StandardSchemaV1>, Output> = {
171
+ cap: Cap
172
+ fn: (options: RouteHandlerOptions) => Promisable<Output>
173
+ }
174
+
175
+ /**
176
+ * Infer the protocol from a router
177
+ */
178
+ export type InferProtocol<
179
+ Routes extends Record<
180
+ string,
181
+ RouteOutput<Capability<StandardSchemaV1>, unknown>
182
+ >,
183
+ > = {
184
+ [K in keyof Routes]: Routes[K] extends RouteOutput<infer Cap, infer Output>
185
+ ? {
186
+ cmd: Cap['cmd'] extends K ? Cap['cmd'] : never
187
+ in: StandardSchemaV1.InferOutput<Cap['schema']>
188
+ out: Output
189
+ }
190
+ : never
191
+ }[keyof Routes]
192
+
193
+ export type Protocol<
194
+ Cap extends Capability<StandardSchemaV1> = Capability<StandardSchemaV1>,
195
+ > = {
196
+ cmd: Cap['cmd']
197
+ in: StandardSchemaV1.InferOutput<Cap['schema']>
198
+ out: unknown
199
+ }[]
200
+
201
+ /**
202
+ * Creates a router type that validates:
203
+ * 1. Router keys match capability commands
204
+ * 2. Router values are RouteOutput types that match the capability
205
+ */
206
+ export type Router<Caps extends readonly Capability<StandardSchemaV1>[]> = {
207
+ [K in Caps[number]['cmd']]: Caps extends readonly (infer Cap)[]
208
+ ? Cap extends Capability<StandardSchemaV1>
209
+ ? Cap['cmd'] extends K
210
+ ? RouteOutput<Cap, unknown>
211
+ : never
212
+ : never
213
+ : never
214
+ }
215
+
216
+ export interface RouteHandlerOptions {
217
+ request: Request
218
+ issuer: ISigner
219
+ invocation: Invocation
220
+ store: Store
221
+ }
222
+
223
+ /**
224
+ * Client type that provides type-safe request methods based on router inference
225
+ */
226
+ export type RouterClient<
227
+ Router extends InferProtocol<
228
+ Record<string, RouteOutput<Capability<StandardSchemaV1>, unknown>>
229
+ >,
230
+ > = {
231
+ request<Cmd extends Router['cmd']>(
232
+ cmd: Cmd,
233
+ args: Extract<Router, { cmd: Cmd }>['in']
234
+ ): Promise<Extract<Router, { cmd: Cmd }>['out']>
235
+ }
236
+
237
+ export type ClientOptions = {
238
+ url: string
239
+ issuer: ISigner
240
+ audience: DIDURLObject
241
+ store: Store
242
+ capabilities: Capability<StandardSchemaV1, string>[]
243
+ }