iso-ucan 0.5.2 → 2.0.0

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.
@@ -41,10 +41,6 @@ export interface PayloadBase {
41
41
  * Expiration UTC Unix Timestamp in seconds (valid until)
42
42
  */
43
43
  exp: number | null
44
- /**
45
- * "Not before" UTC Unix Timestamp in seconds (valid from)
46
- */
47
- nbf?: number
48
44
  }
49
45
 
50
46
  /**
@@ -83,6 +79,10 @@ export interface DelegationPayload<Args = unknown> extends PayloadBase {
83
79
  */
84
80
  sub: DID | null
85
81
  pol: Policy<Args>
82
+ /**
83
+ * "Not before" UTC Unix Timestamp in seconds (valid from)
84
+ */
85
+ nbf?: number
86
86
  }
87
87
 
88
88
  export type Payload<Args = unknown> =
@@ -24,7 +24,6 @@ type PathsToStringProps<T> = T extends
24
24
  | `[${number}:${number}]`
25
25
  | `[${number}:]`
26
26
  | `[:${number}]`
27
- | `[:]`
28
27
  | PathsToStringProps<U>
29
28
  : T extends object
30
29
  ? {
@@ -73,7 +72,11 @@ type SelectorValue<T, S extends string> = S extends `.${infer K}`
73
72
  export type EqualityOp = '==' | '!='
74
73
  export type Equality<Args = unknown> = [EqualityOp, Selector<Args>, unknown]
75
74
  export type InequalityOp = '<' | '<=' | '>' | '>='
76
- export type Inequality<Args = unknown> = [InequalityOp, Selector<Args>, number]
75
+ export type Inequality<Args = unknown> = [
76
+ InequalityOp,
77
+ Selector<Args>,
78
+ number | bigint,
79
+ ]
77
80
 
78
81
  export type NegateOp = 'not'
79
82
  export type Negate<Args = unknown> = [NegateOp, Statement<Args>]
package/src/types.ts CHANGED
@@ -123,14 +123,22 @@ export interface InvocationOptions extends DelegationValidateOptions {
123
123
  * Issued at time in seconds
124
124
  */
125
125
  iat?: number
126
- nbf?: number
127
126
  nonce?: Uint8Array
128
127
  cause?: CID
128
+ /**
129
+ * Metadata, omitted from the payload when empty
130
+ */
129
131
  meta?: CborObject
130
132
  }
131
133
 
132
134
  export interface InvocationFromOptions extends DelegationValidateOptions {
133
135
  bytes: Uint8Array
136
+ /**
137
+ * The current time in seconds, used for the invocation's own `exp`, the time
138
+ * bounds of its proofs and `maxTtl`. Mostly used for testing.
139
+ * @default Math.floor(Date.now() / 1000)
140
+ */
141
+ now?: number
134
142
  audience?: VerifiableDID
135
143
  resolveProof: ResolveProof
136
144
  /**
package/src/utils.js CHANGED
@@ -6,6 +6,7 @@ import { sha256 } from 'multiformats/hashes/sha2'
6
6
  import { z } from 'zod/v4'
7
7
 
8
8
  import * as Envelope from './envelope.js'
9
+ import { parseSelector } from './policy.js'
9
10
  import * as varsig from './varsig.js'
10
11
 
11
12
  /**
@@ -51,6 +52,29 @@ export async function cid(envelope) {
51
52
  return CID.create(1, dagCbor.code, hash)
52
53
  }
53
54
 
55
+ /**
56
+ * Assert that `bytes` is the canonical DAG-CBOR encoding of `envelope`.
57
+ *
58
+ * {@link cid} hashes the re-encoded envelope, and the DAG-CBOR decoder accepts
59
+ * some non-canonical input (e.g. unsorted map keys or short floats). Without
60
+ * this check, different byte strings would share a CID.
61
+ *
62
+ * @param {import('./types.js').DecodedEnvelope<PayloadSpec>} envelope
63
+ * @param {Uint8Array} bytes
64
+ */
65
+ export function assertCanonical(envelope, bytes) {
66
+ const encoded = Envelope.encode({
67
+ signature: envelope.signature,
68
+ // @ts-expect-error
69
+ signaturePayload: signaturePayload(envelope),
70
+ })
71
+ if (!equals(encoded, bytes)) {
72
+ throw new TypeError(
73
+ 'UCAN envelope is not canonical DAG-CBOR, re-encoding does not match the received bytes'
74
+ )
75
+ }
76
+ }
77
+
54
78
  /**
55
79
  * Replay key of a UCAN: the CID of its signature payload, the exact bytes the
56
80
  * signature covers.
@@ -124,6 +148,21 @@ export function assertExpiration(exp, now = nowInSeconds()) {
124
148
  }
125
149
  }
126
150
 
151
+ /**
152
+ * Validate an optional UCAN timestamp (e.g. `nbf` or `iat`): when present it
153
+ * must be an integer number of seconds within ±(2^53 − 1).
154
+ *
155
+ * @param {unknown} value
156
+ * @param {string} name - Field name used in the error message
157
+ */
158
+ export function assertTimestamp(value, name) {
159
+ if (value !== undefined && !Number.isSafeInteger(value)) {
160
+ throw new TypeError(
161
+ `UCAN ${name} must be a safe integer. Received: ${value}`
162
+ )
163
+ }
164
+ }
165
+
127
166
  /**
128
167
  * Validate the not before time of a UCAN
129
168
  *
@@ -131,7 +170,7 @@ export function assertExpiration(exp, now = nowInSeconds()) {
131
170
  * @param {number} [now]
132
171
  */
133
172
  export function assertNotBefore(nbf, now = nowInSeconds()) {
134
- if (nbf && nbf > now) {
173
+ if (nbf !== undefined && nbf > now) {
135
174
  throw new Error('UCAN not valid yet')
136
175
  }
137
176
  }
@@ -488,17 +527,32 @@ export const cborValue =
488
527
  })
489
528
  export const cborObject = z.record(z.string(), cborValue)
490
529
 
491
- export const selector = z.union([
492
- z.literal('.'),
493
- z.templateLiteral([z.literal('.'), z.string()]),
494
- ])
530
+ /**
531
+ * Policy selector, validated with {@link parseSelector}.
532
+ *
533
+ * @see https://github.com/ucan-wg/delegation#selectors
534
+ */
535
+ export const selector = z.string().superRefine((value, ctx) => {
536
+ try {
537
+ parseSelector(value)
538
+ } catch (error) {
539
+ ctx.addIssue({
540
+ code: 'custom',
541
+ message: /** @type {Error} */ (error).message,
542
+ })
543
+ }
544
+ })
495
545
 
496
546
  /**
497
547
  * @typedef {z.infer<typeof selector>} Selector
498
548
  */
499
549
 
500
550
  /**
501
- * Statement
551
+ * Statement, as defined by the spec's IPLD schema. Every statement is a tuple
552
+ * of exact arity. Inequality values are integers or floats; large integers
553
+ * decode from dag-cbor as `bigint`.
554
+ *
555
+ * @see https://github.com/ucan-wg/delegation#policy
502
556
  */
503
557
  export const statement =
504
558
  /** @type {typeof z.lazy<z.ZodType<import('../src/types.js').Statement<unknown>>>} */ (
@@ -509,7 +563,7 @@ export const statement =
509
563
  z.tuple([
510
564
  z.union([z.literal('=='), z.literal('!=')]),
511
565
  selector,
512
- z.unknown(),
566
+ cborValue,
513
567
  ]), // Equality
514
568
  z.tuple([
515
569
  z.union([
@@ -519,14 +573,14 @@ export const statement =
519
573
  z.literal('>='),
520
574
  ]),
521
575
  selector,
522
- z.number(),
576
+ z.union([z.number(), z.bigint()]),
523
577
  ]), // Inequality
524
578
  z.tuple([z.literal('like'), selector, z.string()]), // Like
525
579
  z.tuple([z.literal('not'), statement]),
526
580
  z.tuple([z.literal('and'), z.array(statement)]),
527
581
  z.tuple([z.literal('or'), z.array(statement)]),
528
- z.tuple([z.literal('all'), z.string(), statement]),
529
- z.tuple([z.literal('any'), z.string(), statement]),
582
+ z.tuple([z.literal('all'), selector, statement]),
583
+ z.tuple([z.literal('any'), selector, statement]),
530
584
  ])
531
585
  })
532
586
 
package/src/varsig.js CHANGED
@@ -95,7 +95,6 @@ function getPreset(bytes, presets) {
95
95
  throw new TypeError(`Unsupported preset ${bytes}`)
96
96
  }
97
97
 
98
- /**
99
98
  /**
100
99
  * Decode varsig header
101
100
  *
@@ -119,39 +118,51 @@ export function decode(buf) {
119
118
  }
120
119
  const [code] = varint.decode(buf, 2)
121
120
 
121
+ /** @type {import('./types.js').DecodeOutput} */
122
+ let output
122
123
  switch (SIG_PREFIX[code]) {
123
124
  case 'RSA': {
124
- return {
125
+ output = {
125
126
  alg: getPreset(bytes.readBytes(5), ['RS256']),
126
127
  enc: getEncoding(bytes.readBytes(1)),
127
128
  }
129
+ break
128
130
  }
129
131
  case 'EdDSA': {
130
- return {
132
+ output = {
131
133
  alg: getPreset(bytes.readBytes(5), ['Ed25519']),
132
134
  enc: getEncoding(bytes.readBytes(1)),
133
135
  }
136
+ break
134
137
  }
135
138
  case 'ECDSA': {
136
139
  const isEIP191 = varint.decode(buf, 2 + 5)[0] === EIP191_ENCODING
137
- if (isEIP191) {
138
- return {
139
- alg: getPreset(bytes.readBytes(8), ['EIP191']),
140
- enc: getEncoding(bytes.readBytes(1)),
141
- }
142
- }
143
-
144
- return {
145
- alg: getPreset(bytes.readBytes(5), [
146
- 'ES256',
147
- 'ES256K',
148
- 'ES384',
149
- 'ES512',
150
- ]),
151
- enc: getEncoding(bytes.readBytes(1)),
152
- }
140
+ output = isEIP191
141
+ ? {
142
+ alg: getPreset(bytes.readBytes(8), ['EIP191']),
143
+ enc: getEncoding(bytes.readBytes(1)),
144
+ }
145
+ : {
146
+ alg: getPreset(bytes.readBytes(5), [
147
+ 'ES256',
148
+ 'ES256K',
149
+ 'ES384',
150
+ 'ES512',
151
+ ]),
152
+ enc: getEncoding(bytes.readBytes(1)),
153
+ }
154
+ break
153
155
  }
154
156
  default:
155
157
  throw new TypeError(`Unsupported algorithm ${code}`)
156
158
  }
159
+
160
+ const trailing = buf.length - bytes.offset
161
+ if (trailing !== 0) {
162
+ throw new TypeError(
163
+ `Invalid varsig header ${trailing} trailing byte(s) after the payload encoding`
164
+ )
165
+ }
166
+
167
+ return output
157
168
  }