ccp-core 0.4.0 → 0.5.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/README.md +49 -2
- package/dist/decision.d.ts +2 -1
- package/dist/decision.js +7 -2
- package/dist/grants.d.ts +6 -0
- package/dist/grants.js +3 -3
- package/dist/identity.d.ts +150 -0
- package/dist/identity.js +495 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/requester.js +1 -1
- package/dist/runtime.d.ts +157 -6
- package/dist/runtime.js +437 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The TypeScript implementation of the Capability Contacts Protocol (draft
|
|
4
4
|
v0.4), shaped by role: **Requester**, **Consumer**, **Runtime**, **Agent**.
|
|
5
|
-
It reproduces
|
|
5
|
+
It reproduces the frozen v1, v2, and v3 vectors in `../../test-vectors/`.
|
|
6
6
|
|
|
7
7
|
Runs in browsers and Node (no Node-only APIs in `src/`).
|
|
8
8
|
|
|
@@ -122,6 +122,53 @@ limits never exceed `suggested_limits`, confirmation is never relaxed,
|
|
|
122
122
|
capabilities come from the enabled intents, and placement/egress are
|
|
123
123
|
derived from them.
|
|
124
124
|
|
|
125
|
+
## Identity and admission (§25.8)
|
|
126
|
+
|
|
127
|
+
Publish a signed identity document, verify one for a known contact, and
|
|
128
|
+
let a reviewed policy answer matching requests without a click.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import {
|
|
132
|
+
buildAdmissionPolicy,
|
|
133
|
+
buildIdentityDocument,
|
|
134
|
+
verifyAdmissionPolicy,
|
|
135
|
+
verifyIdentityDocument,
|
|
136
|
+
} from "ccp-core";
|
|
137
|
+
|
|
138
|
+
const identity = buildIdentityDocument(
|
|
139
|
+
{ name: "Example Service", manifestDigest: manifest.digest,
|
|
140
|
+
issuedAt: "2026-10-22T00:00:00Z", expiresAt: "2027-10-22T00:00:00Z" },
|
|
141
|
+
seed
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const peer = verifyIdentityDocument(identity, {
|
|
145
|
+
expectedIdentity: knownContactKey,
|
|
146
|
+
manifestDigest: manifest.digest,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const policy = verifyAdmissionPolicy(
|
|
150
|
+
buildAdmissionPolicy(
|
|
151
|
+
{ version: 1, default: "confirm", rules, issuedAt, expiresAt },
|
|
152
|
+
hostSeed
|
|
153
|
+
),
|
|
154
|
+
{ expectedHost: hostKey }
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const { result, grant } = await runtime.admitRequest({
|
|
158
|
+
policy,
|
|
159
|
+
request: { requesterKey, contactId, intent, risk, now },
|
|
160
|
+
manifest,
|
|
161
|
+
});
|
|
162
|
+
// result.decision is allow | confirm | deny; grant is set on allow.
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Accepting an invitation creates a contact, never a grant. `default` must
|
|
166
|
+
be `confirm` or `deny`, and only `read`/`disclose` intents can be
|
|
167
|
+
admitted. Policy grants use a deterministic id, so a revocation is not
|
|
168
|
+
undone by the next matching request. See
|
|
169
|
+
[`docs/guides/identity.md`](../../docs/guides/identity.md) and
|
|
170
|
+
[`docs/consistency-audit.md`](../../docs/consistency-audit.md).
|
|
171
|
+
|
|
125
172
|
## Durable stores
|
|
126
173
|
|
|
127
174
|
`Runtime` accepts pluggable stores. The interfaces are `GrantStore`,
|
|
@@ -282,6 +329,6 @@ Runnable examples: `examples/provider-node/` (HTTP provider) and
|
|
|
282
329
|
## Tests
|
|
283
330
|
|
|
284
331
|
```sh
|
|
285
|
-
npx vitest run #
|
|
332
|
+
npx vitest run # v1/v2/v3 vectors + role unit tests
|
|
286
333
|
npx tsc --noEmit # typecheck
|
|
287
334
|
```
|
package/dist/decision.d.ts
CHANGED
|
@@ -7,8 +7,9 @@ export interface DecisionContext {
|
|
|
7
7
|
counters: Record<string, number>;
|
|
8
8
|
intentSpec: any | null;
|
|
9
9
|
paramsSchema: any | null;
|
|
10
|
-
supportedCcp?: string;
|
|
10
|
+
supportedCcp?: string | readonly string[];
|
|
11
11
|
}
|
|
12
|
+
export declare const DEFAULT_SUPPORTED_CCP: readonly ["0.5-draft", "0.4-draft"];
|
|
12
13
|
/**
|
|
13
14
|
* The pure §10.3 decision. Returns `authorized`,
|
|
14
15
|
* `confirmation_required`, or a rejection code.
|
package/dist/decision.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { isValid } from "./schema.js";
|
|
2
|
+
export const DEFAULT_SUPPORTED_CCP = ["0.5-draft", "0.4-draft"];
|
|
3
|
+
function isSupported(value, supported) {
|
|
4
|
+
const labels = typeof supported === "string" ? [supported] : supported;
|
|
5
|
+
return typeof value === "string" && labels.includes(value);
|
|
6
|
+
}
|
|
2
7
|
/**
|
|
3
8
|
* The pure §10.3 decision. Returns `authorized`,
|
|
4
9
|
* `confirmation_required`, or a rejection code.
|
|
5
10
|
*/
|
|
6
11
|
export function decide(ctx) {
|
|
7
12
|
const { grant, signal, signer, now, seen, counters } = ctx;
|
|
8
|
-
const supported = ctx.supportedCcp ??
|
|
9
|
-
if (signal.ccp
|
|
13
|
+
const supported = ctx.supportedCcp ?? DEFAULT_SUPPORTED_CCP;
|
|
14
|
+
if (!isSupported(signal.ccp, supported))
|
|
10
15
|
return "invalid_request";
|
|
11
16
|
if (grant === null || grant === undefined)
|
|
12
17
|
return "no_matching_grant";
|
package/dist/grants.d.ts
CHANGED
|
@@ -40,6 +40,12 @@ export interface CreateGrantOptions {
|
|
|
40
40
|
expiresAt: string;
|
|
41
41
|
state?: GrantState;
|
|
42
42
|
stateReason?: string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Rule id that admitted this grant under an admission policy (§25.8.3).
|
|
45
|
+
* Suppressing per-action confirmation requires this, because the policy is
|
|
46
|
+
* the consumer's explicit automation decision (§9.5, §26.2).
|
|
47
|
+
*/
|
|
48
|
+
admittedBy?: string;
|
|
43
49
|
}
|
|
44
50
|
/**
|
|
45
51
|
* Build a least-authority grant from an approved manifest and intent list.
|
package/dist/grants.js
CHANGED
|
@@ -49,7 +49,7 @@ export function createGrant(options) {
|
|
|
49
49
|
throw new CcpError("invalid_request", "expiresAt must be after createdAt");
|
|
50
50
|
}
|
|
51
51
|
const limits = resolveLimits(definitions, options.limits);
|
|
52
|
-
const confirmation = resolveConfirmation(intents, definitions, options.confirmation);
|
|
52
|
+
const confirmation = resolveConfirmation(intents, definitions, options.confirmation, options.admittedBy);
|
|
53
53
|
const capabilities = resolveCapabilities(definitions, options.capabilities);
|
|
54
54
|
const placement = resolvePlacement(definitions, options.placement);
|
|
55
55
|
const egress = resolveEgress(definitions, options.egress ?? {});
|
|
@@ -98,7 +98,7 @@ function resolveLimits(definitions, requested) {
|
|
|
98
98
|
}
|
|
99
99
|
return limits;
|
|
100
100
|
}
|
|
101
|
-
function resolveConfirmation(intents, definitions, requested) {
|
|
101
|
+
function resolveConfirmation(intents, definitions, requested, admittedBy) {
|
|
102
102
|
const overrides = requested ?? {};
|
|
103
103
|
for (const intent of Object.keys(overrides)) {
|
|
104
104
|
if (!intents.includes(intent)) {
|
|
@@ -119,7 +119,7 @@ function resolveConfirmation(intents, definitions, requested) {
|
|
|
119
119
|
if (!CONFIRMATION_POLICIES.has(override)) {
|
|
120
120
|
throw new CcpError("invalid_request", `unknown confirmation policy: ${override}`);
|
|
121
121
|
}
|
|
122
|
-
if (base === "per_action" && override === "none") {
|
|
122
|
+
if (base === "per_action" && override === "none" && admittedBy === undefined) {
|
|
123
123
|
throw new CcpError("invalid_request", `confirmation for ${intent} cannot be relaxed to none`);
|
|
124
124
|
}
|
|
125
125
|
confirmation[intent] = override;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/** Wire type of an identity document (§25.8.1). */
|
|
2
|
+
export declare const IDENTITY_TYPE = "ccp.identity.v1";
|
|
3
|
+
/** Wire type of an admission policy (§25.8.3). */
|
|
4
|
+
export declare const POLICY_TYPE = "ccp.admission-policy.v1";
|
|
5
|
+
/** Tolerated clock skew for validity windows (§25.8.1). */
|
|
6
|
+
export declare const IDENTITY_CLOCK_SKEW_SECONDS = 120;
|
|
7
|
+
export type RiskClass = "read" | "disclose" | "write" | "irreversible";
|
|
8
|
+
/**
|
|
9
|
+
* Identity-layer results (§25.8). These are intentionally separate from the
|
|
10
|
+
* §10.4 decision codes: they occur while establishing a contact, before any
|
|
11
|
+
* signal exists, and must never be returned as a signal outcome.
|
|
12
|
+
*/
|
|
13
|
+
export type IdentityErrorCode = "invalid_request" | "unauthenticated" | "identity_expired" | "identity_changed" | "manifest_mismatch" | "policy_mismatch" | "policy_expired";
|
|
14
|
+
export declare class IdentityError extends Error {
|
|
15
|
+
readonly code: IdentityErrorCode;
|
|
16
|
+
constructor(code: IdentityErrorCode, message?: string);
|
|
17
|
+
}
|
|
18
|
+
export interface Endpoint {
|
|
19
|
+
transport: string;
|
|
20
|
+
manifest?: string;
|
|
21
|
+
signal?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface IdentityInput {
|
|
24
|
+
name?: string;
|
|
25
|
+
roles?: string[];
|
|
26
|
+
endpoints?: Endpoint[];
|
|
27
|
+
manifestDigest?: string;
|
|
28
|
+
policyDigest?: string;
|
|
29
|
+
issuedAt: string;
|
|
30
|
+
expiresAt: string;
|
|
31
|
+
ccp?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface VerifiedIdentity {
|
|
34
|
+
document: string;
|
|
35
|
+
identity: string;
|
|
36
|
+
name?: string;
|
|
37
|
+
roles: string[];
|
|
38
|
+
endpoints: Endpoint[];
|
|
39
|
+
manifestDigest?: string;
|
|
40
|
+
policyDigest?: string;
|
|
41
|
+
issuedAt: string;
|
|
42
|
+
expiresAt: string;
|
|
43
|
+
}
|
|
44
|
+
export interface VerifyIdentityOptions {
|
|
45
|
+
now?: string;
|
|
46
|
+
expectedIdentity?: string;
|
|
47
|
+
manifestDigest?: string;
|
|
48
|
+
policyDigest?: string;
|
|
49
|
+
}
|
|
50
|
+
/** `sha256:<hex>` over the signing bytes of an identity document. */
|
|
51
|
+
export declare function identityDigest(document: string): string;
|
|
52
|
+
/** Build and sign an identity document; the identity is derived from `seed`. */
|
|
53
|
+
export declare function buildIdentityDocument(input: IdentityInput, seed: Uint8Array): string;
|
|
54
|
+
/**
|
|
55
|
+
* Parse and verify an identity document (§25.8.1).
|
|
56
|
+
*
|
|
57
|
+
* `expectedIdentity` is the key the caller already knows for this contact.
|
|
58
|
+
* A different key is `identity_changed`, not `unauthenticated`, because the
|
|
59
|
+
* consequence differs: affected grants must be suspended (§7.4).
|
|
60
|
+
*/
|
|
61
|
+
export declare function verifyIdentityDocument(document: string, options?: VerifyIdentityOptions): VerifiedIdentity;
|
|
62
|
+
export interface PolicyRule {
|
|
63
|
+
id: string;
|
|
64
|
+
intents: string[];
|
|
65
|
+
risk: "read" | "disclose";
|
|
66
|
+
contacts?: string[];
|
|
67
|
+
requesters?: string[];
|
|
68
|
+
destinations?: string[];
|
|
69
|
+
limits?: Record<string, number>;
|
|
70
|
+
confirmation: "none";
|
|
71
|
+
expires_at: string;
|
|
72
|
+
}
|
|
73
|
+
export interface AdmissionPolicyInput {
|
|
74
|
+
version: number;
|
|
75
|
+
default: "confirm" | "deny";
|
|
76
|
+
rules: PolicyRule[];
|
|
77
|
+
issuedAt: string;
|
|
78
|
+
expiresAt: string;
|
|
79
|
+
ccp?: string;
|
|
80
|
+
}
|
|
81
|
+
export interface VerifiedPolicy {
|
|
82
|
+
document: string;
|
|
83
|
+
host: string;
|
|
84
|
+
version: number;
|
|
85
|
+
default: "confirm" | "deny";
|
|
86
|
+
rules: PolicyRule[];
|
|
87
|
+
issuedAt: string;
|
|
88
|
+
expiresAt: string;
|
|
89
|
+
digest: string;
|
|
90
|
+
}
|
|
91
|
+
export interface VerifyPolicyOptions {
|
|
92
|
+
now?: string;
|
|
93
|
+
expectedHost?: string;
|
|
94
|
+
}
|
|
95
|
+
export interface AdmissionRequest {
|
|
96
|
+
requesterKey: string;
|
|
97
|
+
contactId: string;
|
|
98
|
+
intent: string;
|
|
99
|
+
risk: RiskClass;
|
|
100
|
+
destination?: string;
|
|
101
|
+
now: string;
|
|
102
|
+
}
|
|
103
|
+
export interface AdmissionResult {
|
|
104
|
+
decision: "allow" | "confirm" | "deny";
|
|
105
|
+
reason: string;
|
|
106
|
+
ruleId?: string;
|
|
107
|
+
}
|
|
108
|
+
export interface AdmissionOptions {
|
|
109
|
+
/**
|
|
110
|
+
* Ids of grants the consumer has revoked. A match whose deterministic id is
|
|
111
|
+
* in this set is refused with `grant_revoked`; the policy cannot resurrect
|
|
112
|
+
* it (§25.8.3).
|
|
113
|
+
*/
|
|
114
|
+
revoked?: ReadonlySet<string>;
|
|
115
|
+
}
|
|
116
|
+
/** The grant a matched rule may create. `allow` still needs its own record. */
|
|
117
|
+
export interface AdmittedGrant {
|
|
118
|
+
/** Deterministic id; reused on every admission of this rule. */
|
|
119
|
+
id: string;
|
|
120
|
+
intent: string;
|
|
121
|
+
limits: Record<string, number>;
|
|
122
|
+
expiresAt: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Deterministic grant id for a policy rule and requester (§25.8.3).
|
|
126
|
+
*
|
|
127
|
+
* Reusing the id is what makes revocation durable: the host stores one
|
|
128
|
+
* record, the consumer revokes it, and every later admission of the same
|
|
129
|
+
* rule resolves to that revoked record.
|
|
130
|
+
*/
|
|
131
|
+
export declare function admittedGrantId(policy: VerifiedPolicy, ruleId: string, request: AdmissionRequest): string;
|
|
132
|
+
/** `sha256:<hex>` over the signing bytes of an admission policy. */
|
|
133
|
+
export declare function policyDigest(document: string): string;
|
|
134
|
+
/** Build and sign an admission policy; `host` is derived from `seed`. */
|
|
135
|
+
export declare function buildAdmissionPolicy(input: AdmissionPolicyInput, seed: Uint8Array): string;
|
|
136
|
+
/** Parse and verify an admission policy (§25.8.3). */
|
|
137
|
+
export declare function verifyAdmissionPolicy(document: string, options?: VerifyPolicyOptions): VerifiedPolicy;
|
|
138
|
+
/**
|
|
139
|
+
* Evaluate a verified policy against one request (§25.8.3).
|
|
140
|
+
*
|
|
141
|
+
* `allow` is not an execution: use `admittedGrant` to create the record and
|
|
142
|
+
* then proceed through §10.3.
|
|
143
|
+
*/
|
|
144
|
+
export declare function admit(policy: VerifiedPolicy, request: AdmissionRequest, options?: AdmissionOptions): AdmissionResult;
|
|
145
|
+
/**
|
|
146
|
+
* The grant record a match may create: exactly the requested intent, the
|
|
147
|
+
* rule's limits, and the earlier of the rule and policy expiries.
|
|
148
|
+
* Returns `undefined` unless the decision was `allow`.
|
|
149
|
+
*/
|
|
150
|
+
export declare function admittedGrant(policy: VerifiedPolicy, request: AdmissionRequest, result: AdmissionResult): AdmittedGrant | undefined;
|