@kybernesis/arp-scope-catalog 0.2.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/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/index.cjs +518 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +144 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.js +501 -0
- package/dist/index.js.map +1 -0
- package/generated/manifest.json +1542 -0
- package/generated/scopes.json +1536 -0
- package/package.json +49 -0
- package/scopes/calendar.availability.read.yaml +35 -0
- package/scopes/calendar.events.cancel.yaml +24 -0
- package/scopes/calendar.events.create.yaml +31 -0
- package/scopes/calendar.events.modify.yaml +24 -0
- package/scopes/calendar.events.propose.yaml +35 -0
- package/scopes/calendar.events.read.yaml +38 -0
- package/scopes/connection.extend.yaml +28 -0
- package/scopes/connection.rescope.request.yaml +21 -0
- package/scopes/contacts.attributes.read.yaml +25 -0
- package/scopes/contacts.introduce.yaml +21 -0
- package/scopes/contacts.search.yaml +26 -0
- package/scopes/contacts.share.yaml +30 -0
- package/scopes/credentials.present.request.yaml +29 -0
- package/scopes/credentials.proof.zk.request.yaml +31 -0
- package/scopes/delegation.forward.task.yaml +36 -0
- package/scopes/files.project.files.delete.yaml +31 -0
- package/scopes/files.project.files.list.yaml +22 -0
- package/scopes/files.project.files.read.yaml +35 -0
- package/scopes/files.project.files.summarize.yaml +30 -0
- package/scopes/files.project.files.write.yaml +34 -0
- package/scopes/files.project.metadata.read.yaml +21 -0
- package/scopes/files.projects.list.yaml +18 -0
- package/scopes/files.share.external.yaml +39 -0
- package/scopes/identity.card.read.yaml +18 -0
- package/scopes/identity.introduction.request.yaml +24 -0
- package/scopes/identity.principal.verify.yaml +19 -0
- package/scopes/knowledge.query.yaml +31 -0
- package/scopes/messaging.chat.send.yaml +27 -0
- package/scopes/messaging.email.draft.compose.yaml +23 -0
- package/scopes/messaging.email.send.reviewed.yaml +36 -0
- package/scopes/messaging.email.summary.yaml +26 -0
- package/scopes/messaging.email.thread.read.yaml +29 -0
- package/scopes/messaging.relay.to_principal.yaml +22 -0
- package/scopes/notes.read.yaml +25 -0
- package/scopes/notes.search.yaml +24 -0
- package/scopes/notes.write.yaml +32 -0
- package/scopes/payments.authorize.capped.yaml +37 -0
- package/scopes/payments.history.read.yaml +28 -0
- package/scopes/payments.quote.request.yaml +18 -0
- package/scopes/payments.refund.request.yaml +24 -0
- package/scopes/tasks.assign.yaml +27 -0
- package/scopes/tasks.create.yaml +31 -0
- package/scopes/tasks.list.yaml +21 -0
- package/scopes/tasks.read.yaml +22 -0
- package/scopes/tasks.status.update.yaml +22 -0
- package/scopes/tools.invoke.mutating.yaml +37 -0
- package/scopes/tools.invoke.read.yaml +28 -0
- package/scopes/work.projects.list.yaml +18 -0
- package/scopes/work.reports.summary.yaml +29 -0
- package/scopes/work.status.read.yaml +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kybernesis AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @kybernesis/arp-scope-catalog
|
|
2
|
+
|
|
3
|
+
The ARP scope catalog v1 — 50 scope templates authored as YAML — plus the Handlebars→Cedar compiler that turns a selection of scopes into a compiled policy set.
|
|
4
|
+
|
|
5
|
+
Humans never write Cedar. They pick scopes from this catalog, and a compiler produces the policy. This package is both the source of truth (the `scopes/*.yaml` files) and the tooling (the loader + compiler) for doing that.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @kybernesis/arp-scope-catalog
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Load the catalog + compile a single scope
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { loadScopesFromDirectory, compileScope } from '@kybernesis/arp-scope-catalog';
|
|
19
|
+
|
|
20
|
+
const catalog = loadScopesFromDirectory('./node_modules/@kybernesis/arp-scope-catalog/scopes');
|
|
21
|
+
|
|
22
|
+
const permitCalendar = compileScope({
|
|
23
|
+
scope: catalog.find((s) => s.id === 'calendar.availability.read')!,
|
|
24
|
+
audienceDid: 'did:web:ghost.agent',
|
|
25
|
+
params: { days_ahead: 14 },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
console.log(permitCalendar);
|
|
29
|
+
// permit (
|
|
30
|
+
// principal == Agent::"did:web:ghost.agent",
|
|
31
|
+
// action == Action::"check_availability",
|
|
32
|
+
// resource == Calendar::"primary"
|
|
33
|
+
// ) when { context.query_window_days <= 14 };
|
|
34
|
+
// forbid ( ... ) when { action != Action::"check_availability" };
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Compile a bundle of scopes
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { compileBundle, BUNDLES, findBundle } from '@kybernesis/arp-scope-catalog';
|
|
41
|
+
|
|
42
|
+
const bundle = findBundle('bundle.scheduling_assistant.v1')!;
|
|
43
|
+
|
|
44
|
+
const compiled = compileBundle({
|
|
45
|
+
scopeIds: bundle.scopes.map((s) => s.id),
|
|
46
|
+
paramsMap: {
|
|
47
|
+
'calendar.availability.read': { days_ahead: 14 },
|
|
48
|
+
'calendar.events.propose': { max_attendees: 10, max_duration_min: 60 },
|
|
49
|
+
'contacts.search': { attribute_allowlist: ['name', 'email'] },
|
|
50
|
+
},
|
|
51
|
+
audienceDid: 'did:web:ghost.agent',
|
|
52
|
+
catalog,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
console.log(compiled.policies); // string[] — one compiled policy per scope
|
|
56
|
+
console.log(compiled.obligations); // Obligation[] — aggregated post-allow requirements
|
|
57
|
+
console.log(compiled.expandedScopeIds); // string[] — including implied scopes
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The bundle compiler:
|
|
61
|
+
- Transitively expands `implies` relations so the consent UI doesn't have to ask for prerequisites twice.
|
|
62
|
+
- Detects `conflicts_with` pairs and throws before compilation.
|
|
63
|
+
- Inherits parameters along implication edges (e.g., a `project_id` on `files.project.files.read` propagates to its implied `files.project.files.list`).
|
|
64
|
+
- Concatenates `obligations_forced` across the expanded set.
|
|
65
|
+
|
|
66
|
+
## The 50 scopes
|
|
67
|
+
|
|
68
|
+
Authored as one YAML file per scope under `scopes/`. The `generated/manifest.json` file is the public manifest served at `/.well-known/scope-catalog.json` — see `ARP-scope-catalog-v1.md` for the full list.
|
|
69
|
+
|
|
70
|
+
Scope categories: identity, calendar, messaging, files, contacts, tasks, notes, payments, work, credentials, tools, delegation.
|
|
71
|
+
|
|
72
|
+
Risk tiers: low, medium, high, critical (see §2 of the catalog doc for default obligations by tier).
|
|
73
|
+
|
|
74
|
+
## Phase
|
|
75
|
+
|
|
76
|
+
Shipped as part of Phase 1. See [`docs/ARP-phase-0-roadmap.md`](../../docs/ARP-phase-0-roadmap.md).
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var yaml = require('yaml');
|
|
6
|
+
var arpSpec = require('@kybernesis/arp-spec');
|
|
7
|
+
var crypto = require('crypto');
|
|
8
|
+
var Handlebars = require('handlebars');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
var Handlebars__default = /*#__PURE__*/_interopDefault(Handlebars);
|
|
13
|
+
|
|
14
|
+
// src/loader.ts
|
|
15
|
+
var ScopeLoadError = class extends Error {
|
|
16
|
+
file;
|
|
17
|
+
issues;
|
|
18
|
+
constructor(message, opts) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "ScopeLoadError";
|
|
21
|
+
if (opts?.file !== void 0) this.file = opts.file;
|
|
22
|
+
if (opts?.issues !== void 0) this.issues = opts.issues;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
function loadScopeFile(filePath) {
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
29
|
+
} catch (e) {
|
|
30
|
+
throw new ScopeLoadError(`cannot read ${filePath}: ${e.message}`, {
|
|
31
|
+
file: filePath
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = yaml.parse(raw);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
throw new ScopeLoadError(`invalid YAML in ${filePath}: ${e.message}`, {
|
|
39
|
+
file: filePath
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const result = arpSpec.ScopeTemplateSchema.safeParse(parsed);
|
|
43
|
+
if (!result.success) {
|
|
44
|
+
throw new ScopeLoadError(`scope ${filePath} failed schema validation`, {
|
|
45
|
+
file: filePath,
|
|
46
|
+
issues: result.error.issues
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return result.data;
|
|
50
|
+
}
|
|
51
|
+
function loadScopesFromDirectory(scopesDir) {
|
|
52
|
+
const st = fs.statSync(scopesDir);
|
|
53
|
+
if (!st.isDirectory()) {
|
|
54
|
+
throw new ScopeLoadError(`${scopesDir} is not a directory`);
|
|
55
|
+
}
|
|
56
|
+
const files = fs.readdirSync(scopesDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).sort();
|
|
57
|
+
const seen = /* @__PURE__ */ new Set();
|
|
58
|
+
const scopes = [];
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
const full = path.resolve(scopesDir, file);
|
|
61
|
+
const scope = loadScopeFile(full);
|
|
62
|
+
const expected = `${scope.id}.yaml`;
|
|
63
|
+
if (file !== expected && !(file === `${scope.id}.yml`)) {
|
|
64
|
+
throw new ScopeLoadError(
|
|
65
|
+
`filename ${file} does not match scope id ${scope.id} (expected ${expected})`,
|
|
66
|
+
{ file: full }
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (seen.has(scope.id)) {
|
|
70
|
+
throw new ScopeLoadError(`duplicate scope id ${scope.id}`, { file: full });
|
|
71
|
+
}
|
|
72
|
+
seen.add(scope.id);
|
|
73
|
+
scopes.push(scope);
|
|
74
|
+
}
|
|
75
|
+
scopes.sort((a, b) => a.id.localeCompare(b.id));
|
|
76
|
+
return scopes;
|
|
77
|
+
}
|
|
78
|
+
function canonicalize(value) {
|
|
79
|
+
if (value === null) return "null";
|
|
80
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
81
|
+
throw new Error("canonicalize: non-finite numbers are not allowed");
|
|
82
|
+
}
|
|
83
|
+
if (typeof value !== "object") {
|
|
84
|
+
return JSON.stringify(value);
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
return `[${value.map(canonicalize).join(",")}]`;
|
|
88
|
+
}
|
|
89
|
+
const obj = value;
|
|
90
|
+
const keys = Object.keys(obj).sort();
|
|
91
|
+
const parts = keys.map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`);
|
|
92
|
+
return `{${parts.join(",")}}`;
|
|
93
|
+
}
|
|
94
|
+
function sha256Hex(input) {
|
|
95
|
+
return crypto.createHash("sha256").update(input, "utf8").digest("hex");
|
|
96
|
+
}
|
|
97
|
+
function buildCatalogManifest(scopes, options = {}) {
|
|
98
|
+
const sorted = [...scopes].sort((a, b) => a.id.localeCompare(b.id));
|
|
99
|
+
const canonical = canonicalize(sorted);
|
|
100
|
+
const checksum = `sha256:${sha256Hex(canonical)}`;
|
|
101
|
+
const manifest = {
|
|
102
|
+
version: options.version ?? "v1",
|
|
103
|
+
updated_at: options.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
104
|
+
scope_count: sorted.length,
|
|
105
|
+
checksum,
|
|
106
|
+
scopes: sorted
|
|
107
|
+
};
|
|
108
|
+
const parsed = arpSpec.ScopeCatalogManifestSchema.parse(manifest);
|
|
109
|
+
return parsed;
|
|
110
|
+
}
|
|
111
|
+
var ScopeCompileError = class extends Error {
|
|
112
|
+
scopeId;
|
|
113
|
+
parameter;
|
|
114
|
+
constructor(message, opts) {
|
|
115
|
+
super(message);
|
|
116
|
+
this.name = "ScopeCompileError";
|
|
117
|
+
if (opts?.scopeId !== void 0) this.scopeId = opts.scopeId;
|
|
118
|
+
if (opts?.parameter !== void 0) this.parameter = opts.parameter;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
function parseRangeValidation(validation) {
|
|
122
|
+
const match = /^(-?\d+(?:\.\d+)?)\.\.(-?\d+(?:\.\d+)?)$/.exec(validation);
|
|
123
|
+
if (!match) return null;
|
|
124
|
+
return { min: Number(match[1]), max: Number(match[2]) };
|
|
125
|
+
}
|
|
126
|
+
function coerceToNumber(value) {
|
|
127
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
128
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
129
|
+
const n = Number(value);
|
|
130
|
+
if (Number.isFinite(n)) return n;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
function validateParameter(scopeId, def, initial) {
|
|
135
|
+
let value = initial;
|
|
136
|
+
const present = value !== void 0 && value !== null;
|
|
137
|
+
if (!present) {
|
|
138
|
+
if (def.default !== void 0) {
|
|
139
|
+
value = def.default;
|
|
140
|
+
} else if (def.required) {
|
|
141
|
+
throw new ScopeCompileError(
|
|
142
|
+
`missing required parameter '${def.name}' for scope ${scopeId}`,
|
|
143
|
+
{ scopeId, parameter: def.name }
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
switch (def.type) {
|
|
150
|
+
case "Integer": {
|
|
151
|
+
const n = coerceToNumber(value);
|
|
152
|
+
if (n === null || !Number.isInteger(n)) {
|
|
153
|
+
throw new ScopeCompileError(
|
|
154
|
+
`parameter '${def.name}' must be an integer`,
|
|
155
|
+
{ scopeId, parameter: def.name }
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (typeof def.validation === "string") {
|
|
159
|
+
const range = parseRangeValidation(def.validation);
|
|
160
|
+
if (range && (n < range.min || n > range.max)) {
|
|
161
|
+
throw new ScopeCompileError(
|
|
162
|
+
`parameter '${def.name}'=${n} out of range ${def.validation}`,
|
|
163
|
+
{ scopeId, parameter: def.name }
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return n;
|
|
168
|
+
}
|
|
169
|
+
case "Decimal": {
|
|
170
|
+
const n = coerceToNumber(value);
|
|
171
|
+
if (n === null) {
|
|
172
|
+
throw new ScopeCompileError(
|
|
173
|
+
`parameter '${def.name}' must be a number`,
|
|
174
|
+
{ scopeId, parameter: def.name }
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (typeof def.validation === "string") {
|
|
178
|
+
const range = parseRangeValidation(def.validation);
|
|
179
|
+
if (range && (n < range.min || n > range.max)) {
|
|
180
|
+
throw new ScopeCompileError(
|
|
181
|
+
`parameter '${def.name}'=${n} out of range ${def.validation}`,
|
|
182
|
+
{ scopeId, parameter: def.name }
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return n;
|
|
187
|
+
}
|
|
188
|
+
case "Enum": {
|
|
189
|
+
if (Array.isArray(def.validation) && !def.validation.includes(String(value))) {
|
|
190
|
+
throw new ScopeCompileError(
|
|
191
|
+
`parameter '${def.name}'='${String(value)}' is not one of [${def.validation.join(", ")}]`,
|
|
192
|
+
{ scopeId, parameter: def.name }
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
case "AgentDID": {
|
|
198
|
+
if (typeof value !== "string" || !arpSpec.DID_URI_REGEX.test(value)) {
|
|
199
|
+
throw new ScopeCompileError(
|
|
200
|
+
`parameter '${def.name}' must be a valid DID URI`,
|
|
201
|
+
{ scopeId, parameter: def.name }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
case "AgentDIDList": {
|
|
207
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
208
|
+
throw new ScopeCompileError(
|
|
209
|
+
`parameter '${def.name}' must be a non-empty array of DID URIs`,
|
|
210
|
+
{ scopeId, parameter: def.name }
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
for (const entry of value) {
|
|
214
|
+
if (typeof entry !== "string" || !arpSpec.DID_URI_REGEX.test(entry)) {
|
|
215
|
+
throw new ScopeCompileError(
|
|
216
|
+
`parameter '${def.name}' contains an invalid DID URI: ${String(entry)}`,
|
|
217
|
+
{ scopeId, parameter: def.name }
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
case "ToolIDList":
|
|
224
|
+
case "AttributeList":
|
|
225
|
+
case "EmailList": {
|
|
226
|
+
if (!Array.isArray(value)) {
|
|
227
|
+
throw new ScopeCompileError(
|
|
228
|
+
`parameter '${def.name}' must be an array`,
|
|
229
|
+
{ scopeId, parameter: def.name }
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
case "ProjectID": {
|
|
235
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
236
|
+
throw new ScopeCompileError(
|
|
237
|
+
`parameter '${def.name}' must be a non-empty string`,
|
|
238
|
+
{ scopeId, parameter: def.name }
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
case "Duration":
|
|
244
|
+
case "Timezone":
|
|
245
|
+
case "IANATimezone": {
|
|
246
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
247
|
+
throw new ScopeCompileError(
|
|
248
|
+
`parameter '${def.name}' must be a non-empty string`,
|
|
249
|
+
{ scopeId, parameter: def.name }
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function buildHandlebarsContext(audienceDid, scope, validated) {
|
|
257
|
+
const ctx = {
|
|
258
|
+
audience_did: audienceDid,
|
|
259
|
+
...validated
|
|
260
|
+
};
|
|
261
|
+
for (const [key, value] of Object.entries(validated)) {
|
|
262
|
+
if (Array.isArray(value)) {
|
|
263
|
+
ctx[`${key}_json`] = JSON.stringify(value);
|
|
264
|
+
ctx[`${key}_display`] = value.map((v) => String(v)).join(", ");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (scope.id === "calendar.events.read") {
|
|
268
|
+
ctx.include_private_flag = (validated.include_private ?? scope.parameters.find((p) => p.name === "include_private")?.default) === "yes";
|
|
269
|
+
}
|
|
270
|
+
return ctx;
|
|
271
|
+
}
|
|
272
|
+
function compileScope({
|
|
273
|
+
scope,
|
|
274
|
+
params = {},
|
|
275
|
+
audienceDid
|
|
276
|
+
}) {
|
|
277
|
+
if (!arpSpec.DID_URI_REGEX.test(audienceDid)) {
|
|
278
|
+
throw new ScopeCompileError(`audienceDid '${audienceDid}' is not a valid DID URI`, {
|
|
279
|
+
scopeId: scope.id
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const validated = {};
|
|
283
|
+
for (const def of scope.parameters) {
|
|
284
|
+
validated[def.name] = validateParameter(scope.id, def, params[def.name]);
|
|
285
|
+
}
|
|
286
|
+
const ctx = buildHandlebarsContext(audienceDid, scope, validated);
|
|
287
|
+
const template = Handlebars__default.default.compile(scope.cedar_template, { noEscape: true });
|
|
288
|
+
const rendered = template(ctx);
|
|
289
|
+
return normalizeBareEntityTypes(rendered.trim());
|
|
290
|
+
}
|
|
291
|
+
function normalizeBareEntityTypes(cedar) {
|
|
292
|
+
return cedar.replace(
|
|
293
|
+
/(principal|resource)\s*==\s*([A-Z][A-Za-z0-9_]*)(?=\s*[,)\n])/g,
|
|
294
|
+
"$1 is $2"
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/bundle-compiler.ts
|
|
299
|
+
var BundleCompileError = class extends Error {
|
|
300
|
+
scopeId;
|
|
301
|
+
conflict;
|
|
302
|
+
constructor(message, opts) {
|
|
303
|
+
super(message);
|
|
304
|
+
this.name = "BundleCompileError";
|
|
305
|
+
if (opts?.scopeId !== void 0) this.scopeId = opts.scopeId;
|
|
306
|
+
if (opts?.conflict !== void 0) this.conflict = opts.conflict;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
function indexCatalog(catalog) {
|
|
310
|
+
const map = /* @__PURE__ */ new Map();
|
|
311
|
+
for (const scope of catalog) {
|
|
312
|
+
map.set(scope.id, scope);
|
|
313
|
+
}
|
|
314
|
+
return map;
|
|
315
|
+
}
|
|
316
|
+
function expandImplications(seed, catalog) {
|
|
317
|
+
const order = [];
|
|
318
|
+
const visited = /* @__PURE__ */ new Set();
|
|
319
|
+
const impliedBy = /* @__PURE__ */ new Map();
|
|
320
|
+
const queue = seed.map((id) => ({
|
|
321
|
+
id,
|
|
322
|
+
parent: null
|
|
323
|
+
}));
|
|
324
|
+
while (queue.length > 0) {
|
|
325
|
+
const { id, parent } = queue.shift();
|
|
326
|
+
if (visited.has(id)) continue;
|
|
327
|
+
visited.add(id);
|
|
328
|
+
const scope = catalog.get(id);
|
|
329
|
+
if (!scope) {
|
|
330
|
+
throw new BundleCompileError(`unknown scope id '${id}'`, { scopeId: id });
|
|
331
|
+
}
|
|
332
|
+
order.push(id);
|
|
333
|
+
if (parent !== null && !impliedBy.has(id)) impliedBy.set(id, parent);
|
|
334
|
+
for (const implied of scope.implies) {
|
|
335
|
+
if (!visited.has(implied)) {
|
|
336
|
+
queue.push({ id: implied, parent: id });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return { order, impliedBy };
|
|
341
|
+
}
|
|
342
|
+
function detectConflicts(expanded, catalog) {
|
|
343
|
+
const set = new Set(expanded);
|
|
344
|
+
for (const id of expanded) {
|
|
345
|
+
const scope = catalog.get(id);
|
|
346
|
+
if (!scope) continue;
|
|
347
|
+
for (const conflict of scope.conflicts_with) {
|
|
348
|
+
if (set.has(conflict)) {
|
|
349
|
+
throw new BundleCompileError(
|
|
350
|
+
`scope '${id}' conflicts with '${conflict}' \u2014 cannot coexist in the same bundle`,
|
|
351
|
+
{ conflict: [id, conflict] }
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function resolveParamsForScope(scope, paramsMap, impliedBy, idx) {
|
|
358
|
+
const own = { ...paramsMap[scope.id] ?? {} };
|
|
359
|
+
let parentId = impliedBy.get(scope.id);
|
|
360
|
+
while (parentId) {
|
|
361
|
+
const parentParams = paramsMap[parentId];
|
|
362
|
+
if (parentParams) {
|
|
363
|
+
for (const [k, v] of Object.entries(parentParams)) {
|
|
364
|
+
if (own[k] === void 0) own[k] = v;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const grandparentId = impliedBy.get(parentId);
|
|
368
|
+
parentId = grandparentId && grandparentId !== parentId ? grandparentId : void 0;
|
|
369
|
+
}
|
|
370
|
+
return own;
|
|
371
|
+
}
|
|
372
|
+
function compileBundle({
|
|
373
|
+
scopeIds,
|
|
374
|
+
paramsMap = {},
|
|
375
|
+
audienceDid,
|
|
376
|
+
catalog
|
|
377
|
+
}) {
|
|
378
|
+
const idx = indexCatalog(catalog);
|
|
379
|
+
const { order, impliedBy } = expandImplications(scopeIds, idx);
|
|
380
|
+
detectConflicts(order, idx);
|
|
381
|
+
const policies = [];
|
|
382
|
+
const obligations = [];
|
|
383
|
+
for (const id of order) {
|
|
384
|
+
const scope = idx.get(id);
|
|
385
|
+
if (!scope) continue;
|
|
386
|
+
const params = resolveParamsForScope(scope, paramsMap, impliedBy);
|
|
387
|
+
try {
|
|
388
|
+
policies.push(
|
|
389
|
+
compileScope({
|
|
390
|
+
scope,
|
|
391
|
+
audienceDid,
|
|
392
|
+
params
|
|
393
|
+
})
|
|
394
|
+
);
|
|
395
|
+
} catch (e) {
|
|
396
|
+
if (e instanceof ScopeCompileError) throw e;
|
|
397
|
+
throw new BundleCompileError(
|
|
398
|
+
`failed to compile scope '${id}': ${e.message}`,
|
|
399
|
+
{ scopeId: id }
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
for (const ob of scope.obligations_forced) {
|
|
403
|
+
obligations.push({ type: ob.type, params: ob.params });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return { policies, obligations, expandedScopeIds: order };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// src/bundles.ts
|
|
410
|
+
var BUNDLES = [
|
|
411
|
+
{
|
|
412
|
+
id: "bundle.project_collaboration.v1",
|
|
413
|
+
version: "1.0.0",
|
|
414
|
+
label: "Project collaboration",
|
|
415
|
+
description: "Collaborate on a project \u2014 read files, task status, and notes; no writes or external sharing.",
|
|
416
|
+
scopes: [
|
|
417
|
+
{ id: "files.projects.list" },
|
|
418
|
+
{ id: "files.project.metadata.read", params: { project_id: "<user-picks>" } },
|
|
419
|
+
{ id: "files.project.files.read", params: { project_id: "<user-picks>", max_size_mb: 25 } },
|
|
420
|
+
{ id: "files.project.files.summarize", params: { project_id: "<user-picks>", max_output_words: 2e3 } },
|
|
421
|
+
{ id: "tasks.list", params: { project_id: "<user-picks>" } },
|
|
422
|
+
{ id: "tasks.read", params: { project_id: "<user-picks>" } },
|
|
423
|
+
{ id: "tasks.status.update", params: { project_id: "<user-picks>" } },
|
|
424
|
+
{ id: "notes.search", params: { collection_id: "<user-picks>" } },
|
|
425
|
+
{ id: "notes.read", params: { collection_id: "<user-picks>" } }
|
|
426
|
+
]
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
id: "bundle.scheduling_assistant.v1",
|
|
430
|
+
version: "1.0.0",
|
|
431
|
+
label: "Scheduling assistant",
|
|
432
|
+
description: "Coordinate meetings on your calendar.",
|
|
433
|
+
scopes: [
|
|
434
|
+
{ id: "calendar.availability.read", params: { days_ahead: 14 } },
|
|
435
|
+
{
|
|
436
|
+
id: "calendar.events.propose",
|
|
437
|
+
params: { max_attendees: 10, max_duration_min: 60 }
|
|
438
|
+
},
|
|
439
|
+
{ id: "contacts.search", params: { attribute_allowlist: ["name", "email"] } },
|
|
440
|
+
{ id: "messaging.relay.to_principal" }
|
|
441
|
+
]
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
id: "bundle.research_agent.v1",
|
|
445
|
+
version: "1.0.0",
|
|
446
|
+
label: "Research agent",
|
|
447
|
+
description: "Pull research without writing.",
|
|
448
|
+
scopes: [
|
|
449
|
+
{ id: "files.projects.list" },
|
|
450
|
+
{ id: "files.project.files.read", params: { project_id: "<user-picks>", max_size_mb: 25 } },
|
|
451
|
+
{ id: "files.project.files.summarize", params: { project_id: "<user-picks>", max_output_words: 2e3 } },
|
|
452
|
+
{ id: "notes.search", params: { collection_id: "<user-picks>" } },
|
|
453
|
+
{ id: "notes.read", params: { collection_id: "<user-picks>" } },
|
|
454
|
+
{ id: "knowledge.query", params: { kb_id: "<user-picks>", max_tokens: 8e3 } },
|
|
455
|
+
{ id: "credentials.proof.zk.request", params: { attribute: "verified_human" } }
|
|
456
|
+
]
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
id: "bundle.procurement_agent.v1",
|
|
460
|
+
version: "1.0.0",
|
|
461
|
+
label: "Procurement agent",
|
|
462
|
+
description: "Buy things under tight caps.",
|
|
463
|
+
scopes: [
|
|
464
|
+
{ id: "payments.quote.request" },
|
|
465
|
+
{
|
|
466
|
+
id: "payments.authorize.capped",
|
|
467
|
+
params: { max_per_txn_usd: 25, max_per_30d_usd: 200 }
|
|
468
|
+
},
|
|
469
|
+
{ id: "payments.history.read", params: { days_back: 90 } },
|
|
470
|
+
{ id: "messaging.relay.to_principal" }
|
|
471
|
+
]
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
id: "bundle.executive_assistant.v1",
|
|
475
|
+
version: "1.0.0",
|
|
476
|
+
label: "Executive assistant",
|
|
477
|
+
description: "Broad assistant; step-up on anything external-facing.",
|
|
478
|
+
scopes: [
|
|
479
|
+
{ id: "calendar.availability.read", params: { days_ahead: 14 } },
|
|
480
|
+
{
|
|
481
|
+
id: "calendar.events.propose",
|
|
482
|
+
params: { max_attendees: 10, max_duration_min: 60 }
|
|
483
|
+
},
|
|
484
|
+
{ id: "calendar.events.modify" },
|
|
485
|
+
{ id: "messaging.email.summary" },
|
|
486
|
+
{ id: "messaging.email.draft.compose" },
|
|
487
|
+
{ id: "messaging.email.send.reviewed", params: { recipient_allowlist: [] } },
|
|
488
|
+
{ id: "contacts.search", params: { attribute_allowlist: ["name", "email"] } },
|
|
489
|
+
{ id: "tasks.list", params: { project_id: "<user-picks>" } },
|
|
490
|
+
{ id: "tasks.read", params: { project_id: "<user-picks>" } },
|
|
491
|
+
{ id: "tasks.create", params: { project_id: "<user-picks>", max_per_day: 50 } },
|
|
492
|
+
{ id: "tasks.status.update", params: { project_id: "<user-picks>" } },
|
|
493
|
+
{ id: "notes.search", params: { collection_id: "<user-picks>" } },
|
|
494
|
+
{ id: "notes.read", params: { collection_id: "<user-picks>" } },
|
|
495
|
+
{ id: "notes.write", params: { collection_id: "<user-picks>", max_per_day: 100 } },
|
|
496
|
+
{ id: "work.status.read" },
|
|
497
|
+
{ id: "work.reports.summary", params: { period: "week" } }
|
|
498
|
+
]
|
|
499
|
+
}
|
|
500
|
+
];
|
|
501
|
+
function findBundle(id) {
|
|
502
|
+
return BUNDLES.find((b) => b.id === id);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
exports.BUNDLES = BUNDLES;
|
|
506
|
+
exports.BundleCompileError = BundleCompileError;
|
|
507
|
+
exports.ScopeCompileError = ScopeCompileError;
|
|
508
|
+
exports.ScopeLoadError = ScopeLoadError;
|
|
509
|
+
exports.buildCatalogManifest = buildCatalogManifest;
|
|
510
|
+
exports.canonicalize = canonicalize;
|
|
511
|
+
exports.compileBundle = compileBundle;
|
|
512
|
+
exports.compileScope = compileScope;
|
|
513
|
+
exports.findBundle = findBundle;
|
|
514
|
+
exports.loadScopeFile = loadScopeFile;
|
|
515
|
+
exports.loadScopesFromDirectory = loadScopesFromDirectory;
|
|
516
|
+
exports.sha256Hex = sha256Hex;
|
|
517
|
+
//# sourceMappingURL=index.cjs.map
|
|
518
|
+
//# sourceMappingURL=index.cjs.map
|