@zanii/mandate-verify-core 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -114,6 +114,36 @@ without a full request.
114
114
 
115
115
  Supply root-to-nearest-parent entries as `{ mandate, statusList }` with each parent's signed status envelope and independently configured root trust. Every link binds the parent ID, authorized child issuer, validity, depth and exact logical status-list reference. Numeric status claims, per-item key overrides and chains embedded in the action request do not establish authority. Supported references are the logical list ID or its `urn:zanii:status:` encoding; arbitrary URI suffixes do not bind a list. Stable-DID parent/status keys can be resolved through server-owned `resolveMandatePublicKey` and `resolveStatusPublicKey` pin callbacks; request- or artifact-supplied callbacks are not trusted context.
116
116
 
117
+ ## Root passports
118
+
119
+ `verifyPassport` checks an agent's mandate all the way up to a root authority (for example a
120
+ free zone), offline. The root signs one passport mandate to the firm's issuer DID. The firm then
121
+ issues its own tree (company, role, release, instance) with ordinary delegation, so it never
122
+ needs the root to sign anything else.
123
+
124
+ ```js
125
+ import { verifyPassport } from '#verify-core';
126
+
127
+ const result = verifyPassport({
128
+ passport, // signed by rootDid, agent = the firm's issuer DID, delegation allowed
129
+ passportStatusList, // the root's signed status list
130
+ rootDid,
131
+ mandate, // the agent instance's mandate
132
+ statusList,
133
+ delegationChain, // the firm's chain, top first: [{ mandate, statusList }, ...]
134
+ request: { agentDid, action, resource },
135
+ });
136
+ ```
137
+
138
+ It denies when the passport is not from `rootDid` (`PASSPORT_ROOT_UNTRUSTED`), is not a root
139
+ (`PASSPORT_NOT_ROOT`), names a different issuer (`PASSPORT_ISSUER_MISMATCH`), does not allow
140
+ issuing (`PASSPORT_NOT_DELEGABLE`), or when the firm's top mandate exceeds it
141
+ (`PASSPORT_SCOPE_EXCEEDED`). Then it runs `verifyMandate` twice: the passport against the
142
+ request with the root as the only trusted issuer (reasons tagged `scope: 'passport'`), and the
143
+ firm's chain with the firm's issuer as the only trusted issuer. Revoking any level, from the
144
+ passport down to the instance, denies everything below it. Budgets on the passport cannot be
145
+ counted offline, so they give `review`, not `allow`.
146
+
117
147
  ## Offline trust bundles
118
148
 
119
149
  `createOfflineTrustBundle` in `#db` reads an initialized control store and exports
@@ -223,5 +253,11 @@ the connected deployment boundary.
223
253
  ## Test
224
254
 
225
255
  ```powershell
226
- node --test "tests/verify.test.js" "tests/delegation.test.js"
256
+ node --test "tests/verify.test.js" "tests/delegation.test.js" "tests/passport.test.js"
227
257
  ```
258
+
259
+ ## Releases
260
+
261
+ - **0.3.0**: `verifyPassport` checks an agent's mandate offline through the firm's own tree up to a
262
+ root authority (see "Root passports"). Additive; every 0.2.0 export is unchanged.
263
+ - **0.2.0**: first public release.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zanii/mandate-verify-core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "exports": "./src/index.js",
6
6
  "files": [
package/src/index.js CHANGED
@@ -26,6 +26,7 @@ import { validityPolicyReason } from './policy.js';
26
26
 
27
27
  export { scopeSubsetReasons, inheritParentRestrictions } from './delegation.js';
28
28
  export { verifyOfflineTrustBundle } from './offline.js';
29
+ export { verifyPassport } from './passport.js';
29
30
  export {
30
31
  evaluateConditions,
31
32
  verifyMandateAsync,
@@ -0,0 +1,102 @@
1
+ import { scopeSubsetReasons } from './delegation.js';
2
+ import { verifyMandate } from './index.js';
3
+
4
+ /**
5
+ * Verify an agent's passport chain up to a root authority (for example a free zone), offline.
6
+ *
7
+ * The root signs one passport mandate whose agent is the firm's issuer DID: that binds the
8
+ * firm's key under the root. The firm then issues its own tree (company, role, release,
9
+ * instance) with ordinary narrowing delegation, rooted in its issuer DID. This checks both:
10
+ *
11
+ * - the passport: signed by `rootDid`, a root itself, delegable, not revoked or expired in the
12
+ * root's signed status list, and covering the requested action and resource;
13
+ * - the firm's chain and leaf mandate, with the firm's issuer as the only trusted root;
14
+ * - that the firm's top mandate stays within the passport (actions, resources, expiry, limits).
15
+ *
16
+ * Budgets on the passport cannot be checked offline (no usage counts), so they surface as
17
+ * `review`, never as `allow`.
18
+ */
19
+ export function verifyPassport({
20
+ passport,
21
+ passportStatusList,
22
+ rootDid,
23
+ mandate,
24
+ statusList,
25
+ delegationChain = [],
26
+ request,
27
+ pins = {},
28
+ now = new Date(),
29
+ ...rest
30
+ } = {}) {
31
+ const evaluatedAt = new Date(now).toISOString();
32
+ const deny = (code, message) => ({
33
+ verdict: 'deny',
34
+ mandateId: mandate?.id,
35
+ agentDid: request?.agentDid,
36
+ evaluatedAt,
37
+ reasons: [{ code, severity: 'hard', message }],
38
+ checks: [{ code, ok: false, severity: 'hard' }],
39
+ });
40
+ const firmTop = delegationChain[0]?.mandate ?? mandate;
41
+ const firmDid = firmTop?.issuer;
42
+ const subject = passport?.credentialSubject;
43
+
44
+ if (typeof rootDid !== 'string' || !rootDid)
45
+ return deny('PASSPORT_ROOT_MISSING', 'A root DID is required');
46
+ if (!subject || passport.issuer !== rootDid) {
47
+ return deny('PASSPORT_ROOT_UNTRUSTED', 'Passport is not issued by the configured root');
48
+ }
49
+ if (passport.parentMandateId)
50
+ return deny('PASSPORT_NOT_ROOT', 'A passport must be a root mandate');
51
+ if (typeof firmDid !== 'string' || subject.agent?.did !== firmDid) {
52
+ return deny('PASSPORT_ISSUER_MISMATCH', 'Passport does not name the issuer of this chain');
53
+ }
54
+ if (subject.delegation?.allowed !== true) {
55
+ return deny('PASSPORT_NOT_DELEGABLE', 'Passport does not allow its holder to issue');
56
+ }
57
+ const scope = scopeSubsetReasons(firmTop, passport);
58
+ if (scope.length > 0) return deny('PASSPORT_SCOPE_EXCEEDED', scope.join('; '));
59
+
60
+ const authority = verifyMandate({
61
+ mandate: passport,
62
+ statusList: passportStatusList,
63
+ request: { ...request, agentDid: firmDid },
64
+ pins: {
65
+ trustedIssuers: [rootDid],
66
+ requireStatus: true,
67
+ requireApproval: false,
68
+ requireCertification: false,
69
+ requireEvidence: false,
70
+ ...(pins.clockSkewSeconds === undefined ? {} : { clockSkewSeconds: pins.clockSkewSeconds }),
71
+ ...(pins.maxStatusAgeSeconds === undefined
72
+ ? {}
73
+ : { maxStatusAgeSeconds: pins.maxStatusAgeSeconds }),
74
+ },
75
+ now,
76
+ });
77
+ const chain = verifyMandate({
78
+ ...rest,
79
+ mandate,
80
+ statusList,
81
+ delegationChain,
82
+ request,
83
+ pins: { ...pins, trustedIssuers: [firmDid] },
84
+ now,
85
+ });
86
+
87
+ const verdicts = [authority.verdict, chain.verdict];
88
+ const verdict = ['error', 'deny', 'review'].find((v) => verdicts.includes(v)) ?? 'allow';
89
+ return {
90
+ ...chain,
91
+ verdict,
92
+ reasons: [
93
+ ...authority.reasons.map((reason) => ({ ...reason, scope: 'passport' })),
94
+ ...chain.reasons,
95
+ ],
96
+ checks: [
97
+ ...authority.checks.map((check) => ({ ...check, scope: 'passport' })),
98
+ ...chain.checks,
99
+ ],
100
+ passport: { id: passport.id, root: rootDid, holder: firmDid, verdict: authority.verdict },
101
+ };
102
+ }