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 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 all 51 frozen vectors in `../../test-vectors/`.
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 # 51 frozen vectors + role unit tests
332
+ npx vitest run # v1/v2/v3 vectors + role unit tests
286
333
  npx tsc --noEmit # typecheck
287
334
  ```
@@ -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 ?? "0.4-draft";
9
- if (signal.ccp !== supported)
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;