Skip to content
Merged
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
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
"./vcdm": {
"types": "./dist/VCDM.d.ts",
"default": "./dist/VCDM.js"
},
"./zcap": {
"types": "./dist/ZCap.d.ts",
"default": "./dist/ZCap.js"
}
},
"devDependencies": {
Expand Down
98 changes: 98 additions & 0 deletions src/ZCap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*!
* Authorization Capabilities (zCap) interfaces.
* @see https://w3c-ccg.github.io/zcap-spec/
*/

/**
* A proof attached to a delegated zCap.
*/
export interface ICapabilityDelegationProof {
/** The cryptographic suite type (e.g. `'Ed25519Signature2020'`). */
type: string
/** ISO 8601 date-time the proof was created. */
created: string
/** Verification method URI used to sign. */
verificationMethod: string
/** Always `'capabilityDelegation'`. */
proofPurpose: 'capabilityDelegation'
/**
* Ordered capability chain (root → parent). All entries are string IDs
* except the last delegated zCap, which is embedded as an object.
*/
capabilityChain: Array<string | IDelegatedZcap>
/** The encoded proof value. */
proofValue: string
}

/**
* A root authorization capability (zCap). Root zCaps are unsigned, have no
* `expires` field and no delegation proof. Their `id` follows the convention
* `urn:zcap:root:${encodeURIComponent(invocationTarget)}`.
*/
export interface IRootZcap {
/** The zCap JSON-LD context URL. */
'@context': string
/** Capability ID (`urn:zcap:root:<encodedTarget>`). */
id: string
/** The DID(s) authorized to invoke. */
controller: string | string[]
/** Resource URI this capability grants access to (absolute URI). */
invocationTarget: string
}

/**
* A delegated authorization capability (zCap). Delegated capabilities narrow
* a parent capability and must carry exactly one `capabilityDelegation` proof.
*/
export interface IDelegatedZcap {
/** JSON-LD context array; first entry MUST be the zCap context URL. */
'@context': string[]
/** Capability ID (absolute URI). */
id: string
/** Parent capability ID (absolute URI). */
parentCapability: string
/** The DID(s) authorized to invoke. */
controller: string | string[]
/** Resource URI this capability grants access to (absolute URI). */
invocationTarget: string
/**
* The action(s) the controller may perform; if absent, no actions are
* allowed (except for the root zCap).
*/
allowedAction?: string | string[]
/** ISO 8601 date-time when this capability expires. */
expires: string
/** The capability delegation proof(s). */
proof: ICapabilityDelegationProof | ICapabilityDelegationProof[]
}

/**
* A zCap is either a root or a delegated authorization capability.
*
* Use this discriminated union when the call site knows it has a real,
* well-formed zCap and can narrow via `'parentCapability' in cap`.
*/
export type IZcap = IRootZcap | IDelegatedZcap

/**
* A permissive zCap shape that conflates root and delegated kinds and admits
* arbitrary additional properties (e.g. the opaque object returned from
* `jsigs.sign()`).
*
* Prefer the strict `IZcap` union when possible. Use `IZcapLike` only when:
* - the call site receives a capability whose kind is not yet known and
* cannot reasonably discriminate, or
* - the call site needs to accept the open-ended shape produced by
* linked-data signature libraries that may attach extra properties.
*/
export interface IZcapLike {
'@context': string | string[]
id: string
invocationTarget: string
controller?: string | string[]
parentCapability?: string
allowedAction?: string | string[]
expires?: string
proof?: ICapabilityDelegationProof | ICapabilityDelegationProof[]
[key: string]: unknown
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './DID.js'
export * from './KeyPair.js'
export * from './VCDM.js'
export * from './OBv3.js'
export * from './ZCap.js'
74 changes: 74 additions & 0 deletions test/ZCap.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { describe, test } from 'node:test'
import assert from 'node:assert'
import {
ICapabilityDelegationProof,
IDelegatedZcap,
IRootZcap,
IZcap,
IZcapLike
} from '../src'

await describe('RootZcap', async () => {
await test('exists', async () => {
const root: IRootZcap = {
'@context': 'https://w3id.org/zcap/v1',
id: 'urn:zcap:root:https%3A%2F%2Fexample.com%2Fresource',
controller: 'did:example:alice',
invocationTarget: 'https://example.com/resource'
}

assert.ok(root)
})
})

await describe('DelegatedZcap', async () => {
await test('exists', async () => {
const proof: ICapabilityDelegationProof = {
type: 'Ed25519Signature2020',
created: '2026-01-01T00:00:00Z',
verificationMethod: 'did:example:alice#key-1',
proofPurpose: 'capabilityDelegation',
capabilityChain: [
'urn:zcap:root:https%3A%2F%2Fexample.com%2Fresource'
],
proofValue: 'z3...'
}

const delegated: IDelegatedZcap = {
'@context': ['https://w3id.org/zcap/v1'],
id: 'urn:uuid:00000000-0000-0000-0000-000000000001',
parentCapability: 'urn:zcap:root:https%3A%2F%2Fexample.com%2Fresource',
controller: 'did:example:bob',
invocationTarget: 'https://example.com/resource',
allowedAction: ['GET'],
expires: '2026-12-31T23:59:59Z',
proof
}

assert.ok(delegated)
})
})

await describe('IZcap union', async () => {
await test('accepts root and delegated', async () => {
const root: IZcap = {
'@context': 'https://w3id.org/zcap/v1',
id: 'urn:zcap:root:https%3A%2F%2Fexample.com%2Fr',
controller: 'did:example:alice',
invocationTarget: 'https://example.com/r'
}
assert.ok(!('parentCapability' in root))
})
})

await describe('IZcapLike', async () => {
await test('admits extra properties', async () => {
const loose: IZcapLike = {
'@context': 'https://w3id.org/zcap/v1',
id: 'urn:zcap:root:https%3A%2F%2Fexample.com%2Fr',
invocationTarget: 'https://example.com/r',
arbitraryExtra: 'allowed'
}
assert.ok(loose)
})
})
Loading