@peac/policy-kit 0.10.9 → 0.10.10
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 +1 -1
- package/dist/index.cjs +1356 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +1270 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +19 -6
- package/dist/compiler.js +0 -304
- package/dist/compiler.js.map +0 -1
- package/dist/enforce.js +0 -309
- package/dist/enforce.js.map +0 -1
- package/dist/enforcement-profiles.js +0 -293
- package/dist/enforcement-profiles.js.map +0 -1
- package/dist/evaluate.js +0 -258
- package/dist/evaluate.js.map +0 -1
- package/dist/generated/profiles.js +0 -212
- package/dist/generated/profiles.js.map +0 -1
- package/dist/index.js +0 -120
- package/dist/index.js.map +0 -1
- package/dist/loader.js +0 -245
- package/dist/loader.js.map +0 -1
- package/dist/profiles.js +0 -368
- package/dist/profiles.js.map +0 -1
- package/dist/types.js +0 -348
- package/dist/types.js.map +0 -1
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1270 @@
|
|
|
1
|
+
import { z, ZodError } from 'zod';
|
|
2
|
+
import { SubjectTypeSchema, ControlDecisionSchema, ControlLicensingModeSchema, ControlPurposeSchema } from '@peac/schema';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as yaml from 'yaml';
|
|
6
|
+
|
|
7
|
+
// src/types.ts
|
|
8
|
+
var POLICY_VERSION = "peac-policy/0.1";
|
|
9
|
+
var SubjectMatcherSchema = z.object({
|
|
10
|
+
/** Match by subject type(s) - single type or array */
|
|
11
|
+
type: z.union([SubjectTypeSchema, z.array(SubjectTypeSchema)]).optional(),
|
|
12
|
+
/** Match by label(s) - subject must have ALL specified labels */
|
|
13
|
+
labels: z.array(z.string().min(1)).optional(),
|
|
14
|
+
/** Match by subject ID pattern (exact match or prefix with *) */
|
|
15
|
+
id: z.string().min(1).optional()
|
|
16
|
+
}).strict();
|
|
17
|
+
var PolicyRuleSchema = z.object({
|
|
18
|
+
/** Rule name (for debugging/auditing) */
|
|
19
|
+
name: z.string().min(1),
|
|
20
|
+
/** Subject matcher (omit for any subject) */
|
|
21
|
+
subject: SubjectMatcherSchema.optional(),
|
|
22
|
+
/** Purpose(s) this rule applies to - single purpose or array */
|
|
23
|
+
purpose: z.union([ControlPurposeSchema, z.array(ControlPurposeSchema)]).optional(),
|
|
24
|
+
/** Licensing mode(s) this rule applies to */
|
|
25
|
+
licensing_mode: z.union([ControlLicensingModeSchema, z.array(ControlLicensingModeSchema)]).optional(),
|
|
26
|
+
/** Decision if rule matches */
|
|
27
|
+
decision: ControlDecisionSchema,
|
|
28
|
+
/** Reason for decision (for audit trail) */
|
|
29
|
+
reason: z.string().optional()
|
|
30
|
+
}).strict();
|
|
31
|
+
var PolicyDefaultsSchema = z.object({
|
|
32
|
+
/** Default decision when no rule matches */
|
|
33
|
+
decision: ControlDecisionSchema,
|
|
34
|
+
/** Default reason for audit trail */
|
|
35
|
+
reason: z.string().optional()
|
|
36
|
+
}).strict();
|
|
37
|
+
var PolicyDocumentSchema = z.object({
|
|
38
|
+
/** Policy format version */
|
|
39
|
+
version: z.literal(POLICY_VERSION),
|
|
40
|
+
/** Policy name/description (optional) */
|
|
41
|
+
name: z.string().optional(),
|
|
42
|
+
/** Default decision (required) */
|
|
43
|
+
defaults: PolicyDefaultsSchema,
|
|
44
|
+
/** Rules evaluated in order (first match wins) */
|
|
45
|
+
rules: z.array(PolicyRuleSchema)
|
|
46
|
+
}).strict();
|
|
47
|
+
var RateLimitConfigSchema = z.object({
|
|
48
|
+
/** Maximum requests allowed in the window */
|
|
49
|
+
limit: z.number().int().positive(),
|
|
50
|
+
/** Window size in seconds (e.g., 3600 for 1 hour) */
|
|
51
|
+
window_seconds: z.number().int().positive(),
|
|
52
|
+
/** Optional burst allowance above the limit */
|
|
53
|
+
burst: z.number().int().nonnegative().optional(),
|
|
54
|
+
/** How to partition rate limits (default: per-agent) */
|
|
55
|
+
partition: z.union([z.enum(["agent", "ip", "account"]), z.string().min(1)]).optional()
|
|
56
|
+
}).strict();
|
|
57
|
+
function parseRateLimit(input) {
|
|
58
|
+
const match = input.match(/^(\d+)\/(second|minute|hour|day)$/i);
|
|
59
|
+
if (!match) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Invalid rate limit format: "${input}". Expected format: "100/hour", "1000/day", etc.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const limit = parseInt(match[1], 10);
|
|
65
|
+
const unit = match[2].toLowerCase();
|
|
66
|
+
const windowMap = {
|
|
67
|
+
second: 1,
|
|
68
|
+
minute: 60,
|
|
69
|
+
hour: 3600,
|
|
70
|
+
day: 86400
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
limit,
|
|
74
|
+
window_seconds: windowMap[unit]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function formatRateLimit(config) {
|
|
78
|
+
const { limit, window_seconds } = config;
|
|
79
|
+
if (window_seconds === 1) return `${limit}/second`;
|
|
80
|
+
if (window_seconds === 60) return `${limit}/minute`;
|
|
81
|
+
if (window_seconds === 3600) return `${limit}/hour`;
|
|
82
|
+
if (window_seconds === 86400) return `${limit}/day`;
|
|
83
|
+
return `${limit}/${window_seconds}s`;
|
|
84
|
+
}
|
|
85
|
+
var DecisionRequirementsSchema = z.object({
|
|
86
|
+
/** Require a valid PEAC receipt */
|
|
87
|
+
receipt: z.boolean().optional()
|
|
88
|
+
// Extensible: add more requirements as needed
|
|
89
|
+
// attestation?: boolean;
|
|
90
|
+
// kyc?: boolean;
|
|
91
|
+
}).strict();
|
|
92
|
+
var ProfileParameterSchema = z.object({
|
|
93
|
+
/** Human-readable description */
|
|
94
|
+
description: z.string().min(1),
|
|
95
|
+
/** Whether this parameter is required */
|
|
96
|
+
required: z.boolean().optional(),
|
|
97
|
+
/** Default value if not provided */
|
|
98
|
+
default: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
99
|
+
/** Example value for documentation */
|
|
100
|
+
example: z.string().optional(),
|
|
101
|
+
/** Validation type for the parameter */
|
|
102
|
+
validate: z.enum(["email", "url", "rate_limit"]).optional()
|
|
103
|
+
}).strict();
|
|
104
|
+
var ProfileDefinitionSchema = z.object({
|
|
105
|
+
/** Unique profile identifier (e.g., 'news-media') */
|
|
106
|
+
id: z.string().min(1).regex(/^[a-z][a-z0-9-]*$/),
|
|
107
|
+
/** Human-readable profile name */
|
|
108
|
+
name: z.string().min(1),
|
|
109
|
+
/** Multi-line description of the profile */
|
|
110
|
+
description: z.string().min(1),
|
|
111
|
+
/** Base policy document */
|
|
112
|
+
policy: PolicyDocumentSchema,
|
|
113
|
+
/** Configurable parameters */
|
|
114
|
+
parameters: z.record(z.string(), ProfileParameterSchema),
|
|
115
|
+
/** Default values for profile instances */
|
|
116
|
+
defaults: z.object({
|
|
117
|
+
/** Default requirements for 'review' decisions */
|
|
118
|
+
requirements: DecisionRequirementsSchema.optional(),
|
|
119
|
+
/** Default rate limit */
|
|
120
|
+
rate_limit: RateLimitConfigSchema.optional()
|
|
121
|
+
}).strict().optional()
|
|
122
|
+
}).strict();
|
|
123
|
+
var PolicyConstraintsSchema = z.object({
|
|
124
|
+
/** Rate limit configuration */
|
|
125
|
+
rate_limit: z.object({
|
|
126
|
+
/** Window size in seconds */
|
|
127
|
+
window_s: z.number().int().positive(),
|
|
128
|
+
/** Maximum requests allowed in the window */
|
|
129
|
+
max: z.number().int().positive(),
|
|
130
|
+
/** Retry-After header value in seconds (optional) */
|
|
131
|
+
retry_after_s: z.number().int().positive().optional()
|
|
132
|
+
}).strict().optional(),
|
|
133
|
+
/** Budget constraints */
|
|
134
|
+
budget: z.object({
|
|
135
|
+
/** Maximum tokens allowed */
|
|
136
|
+
max_tokens: z.number().int().positive().optional(),
|
|
137
|
+
/** Maximum requests allowed */
|
|
138
|
+
max_requests: z.number().int().positive().optional()
|
|
139
|
+
}).strict().optional()
|
|
140
|
+
}).strict();
|
|
141
|
+
var EnforcementProfileSchema = z.object({
|
|
142
|
+
/** Profile identifier */
|
|
143
|
+
id: z.enum(["strict", "balanced", "open"]),
|
|
144
|
+
/** Human-readable name */
|
|
145
|
+
name: z.string().min(1),
|
|
146
|
+
/** Description of when to use this profile */
|
|
147
|
+
description: z.string().min(1),
|
|
148
|
+
/** Decision for requests with no purpose declared (missing header) */
|
|
149
|
+
undeclared_decision: z.enum(["allow", "deny", "review"]),
|
|
150
|
+
/** Decision for requests with unknown purpose tokens */
|
|
151
|
+
unknown_decision: z.enum(["allow", "deny", "review"]),
|
|
152
|
+
/** Purpose reason to record when undeclared/unknown is processed */
|
|
153
|
+
purpose_reason: z.enum([
|
|
154
|
+
"allowed",
|
|
155
|
+
"constrained",
|
|
156
|
+
"denied",
|
|
157
|
+
"downgraded",
|
|
158
|
+
"undeclared_default",
|
|
159
|
+
"unknown_preserved"
|
|
160
|
+
]),
|
|
161
|
+
/** Default constraints to apply for 'review' decisions */
|
|
162
|
+
default_constraints: PolicyConstraintsSchema.optional(),
|
|
163
|
+
/** Whether receipts are required for allowed requests */
|
|
164
|
+
receipts: z.enum(["required", "optional", "omit"])
|
|
165
|
+
}).strict();
|
|
166
|
+
var PolicyLoadError = class extends Error {
|
|
167
|
+
constructor(message, cause) {
|
|
168
|
+
super(message);
|
|
169
|
+
this.cause = cause;
|
|
170
|
+
this.name = "PolicyLoadError";
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
var PolicyValidationError = class extends PolicyLoadError {
|
|
174
|
+
constructor(message, issues) {
|
|
175
|
+
super(message);
|
|
176
|
+
this.issues = issues;
|
|
177
|
+
this.name = "PolicyValidationError";
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
function parsePolicy(content, format) {
|
|
181
|
+
let parsed;
|
|
182
|
+
try {
|
|
183
|
+
if (format === "json") {
|
|
184
|
+
parsed = JSON.parse(content);
|
|
185
|
+
} else if (format === "yaml") {
|
|
186
|
+
parsed = yaml.parse(content);
|
|
187
|
+
} else {
|
|
188
|
+
try {
|
|
189
|
+
parsed = JSON.parse(content);
|
|
190
|
+
} catch {
|
|
191
|
+
parsed = yaml.parse(content);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
throw new PolicyLoadError(
|
|
196
|
+
`Failed to parse policy: ${err instanceof Error ? err.message : String(err)}`,
|
|
197
|
+
err instanceof Error ? err : void 0
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return validatePolicy(parsed);
|
|
201
|
+
}
|
|
202
|
+
function validatePolicy(obj) {
|
|
203
|
+
try {
|
|
204
|
+
return PolicyDocumentSchema.parse(obj);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
if (err instanceof ZodError) {
|
|
207
|
+
const issues = err.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
208
|
+
throw new PolicyValidationError(`Policy validation failed: ${issues}`, err.issues);
|
|
209
|
+
}
|
|
210
|
+
throw new PolicyLoadError(
|
|
211
|
+
`Policy validation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
212
|
+
err instanceof Error ? err : void 0
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function loadPolicy(filePath) {
|
|
217
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
218
|
+
let format;
|
|
219
|
+
if (ext === ".json") {
|
|
220
|
+
format = "json";
|
|
221
|
+
} else if (ext === ".yaml" || ext === ".yml") {
|
|
222
|
+
format = "yaml";
|
|
223
|
+
}
|
|
224
|
+
let content;
|
|
225
|
+
try {
|
|
226
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
227
|
+
} catch (err) {
|
|
228
|
+
throw new PolicyLoadError(
|
|
229
|
+
`Failed to read policy file: ${err instanceof Error ? err.message : String(err)}`,
|
|
230
|
+
err instanceof Error ? err : void 0
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return parsePolicy(content, format);
|
|
234
|
+
}
|
|
235
|
+
function policyFileExists(filePath) {
|
|
236
|
+
try {
|
|
237
|
+
fs.accessSync(filePath, fs.constants.R_OK);
|
|
238
|
+
return true;
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function createExamplePolicy() {
|
|
244
|
+
return {
|
|
245
|
+
version: POLICY_VERSION,
|
|
246
|
+
name: "Example Policy",
|
|
247
|
+
defaults: {
|
|
248
|
+
decision: "deny",
|
|
249
|
+
reason: "No matching rule found"
|
|
250
|
+
},
|
|
251
|
+
rules: [
|
|
252
|
+
{
|
|
253
|
+
name: "allow-subscribed-crawl",
|
|
254
|
+
subject: {
|
|
255
|
+
type: "human",
|
|
256
|
+
labels: ["subscribed"]
|
|
257
|
+
},
|
|
258
|
+
purpose: "crawl",
|
|
259
|
+
licensing_mode: "subscription",
|
|
260
|
+
decision: "allow",
|
|
261
|
+
reason: "Subscribed users can crawl"
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "allow-verified-agents-inference",
|
|
265
|
+
subject: {
|
|
266
|
+
type: "agent",
|
|
267
|
+
labels: ["verified"]
|
|
268
|
+
},
|
|
269
|
+
purpose: ["inference", "ai_input"],
|
|
270
|
+
licensing_mode: "pay_per_inference",
|
|
271
|
+
decision: "allow",
|
|
272
|
+
reason: "Verified agents can run inference with payment"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: "review-org-train",
|
|
276
|
+
subject: {
|
|
277
|
+
type: "org"
|
|
278
|
+
},
|
|
279
|
+
purpose: "train",
|
|
280
|
+
decision: "review",
|
|
281
|
+
reason: "Training requests from organizations require review"
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function serializePolicyYaml(policy) {
|
|
287
|
+
return yaml.stringify(policy, {
|
|
288
|
+
lineWidth: 100,
|
|
289
|
+
defaultKeyType: "PLAIN",
|
|
290
|
+
defaultStringType: "QUOTE_DOUBLE"
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
function serializePolicyJson(policy, pretty = true) {
|
|
294
|
+
return JSON.stringify(policy, null, pretty ? 2 : void 0);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/evaluate.ts
|
|
298
|
+
function matchesSingleOrArray(value, pattern) {
|
|
299
|
+
if (pattern === void 0) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
if (value === void 0) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
if (Array.isArray(pattern)) {
|
|
306
|
+
return pattern.includes(value);
|
|
307
|
+
}
|
|
308
|
+
return value === pattern;
|
|
309
|
+
}
|
|
310
|
+
function matchesIdPattern(id, pattern) {
|
|
311
|
+
if (pattern === void 0) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
if (id === void 0) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
if (pattern.endsWith("*")) {
|
|
318
|
+
const prefix = pattern.slice(0, -1);
|
|
319
|
+
return id.startsWith(prefix);
|
|
320
|
+
}
|
|
321
|
+
return id === pattern;
|
|
322
|
+
}
|
|
323
|
+
function hasAllLabels(subjectLabels, requiredLabels) {
|
|
324
|
+
if (requiredLabels === void 0 || requiredLabels.length === 0) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
if (subjectLabels === void 0 || subjectLabels.length === 0) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
return requiredLabels.every((label) => subjectLabels.includes(label));
|
|
331
|
+
}
|
|
332
|
+
function matchesSubject(subject, matcher) {
|
|
333
|
+
if (matcher === void 0) {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
if (!matchesSingleOrArray(subject?.type, matcher.type)) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
if (!hasAllLabels(subject?.labels, matcher.labels)) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
if (!matchesIdPattern(subject?.id, matcher.id)) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
function ruleMatches(rule, context) {
|
|
348
|
+
if (!matchesSubject(context.subject, rule.subject)) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
if (!matchesSingleOrArray(context.purpose, rule.purpose)) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
if (!matchesSingleOrArray(context.licensing_mode, rule.licensing_mode)) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
function evaluate(policy, context) {
|
|
360
|
+
for (const rule of policy.rules) {
|
|
361
|
+
if (ruleMatches(rule, context)) {
|
|
362
|
+
return {
|
|
363
|
+
decision: rule.decision,
|
|
364
|
+
matched_rule: rule.name,
|
|
365
|
+
reason: rule.reason,
|
|
366
|
+
is_default: false
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
decision: policy.defaults.decision,
|
|
372
|
+
reason: policy.defaults.reason,
|
|
373
|
+
is_default: true
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function explainMatches(policy, context) {
|
|
377
|
+
const matches = [];
|
|
378
|
+
for (const rule of policy.rules) {
|
|
379
|
+
if (ruleMatches(rule, context)) {
|
|
380
|
+
matches.push(rule.name);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (matches.length === 0) {
|
|
384
|
+
matches.push("[default]");
|
|
385
|
+
}
|
|
386
|
+
return matches;
|
|
387
|
+
}
|
|
388
|
+
function findEffectiveRule(policy, context) {
|
|
389
|
+
for (const rule of policy.rules) {
|
|
390
|
+
if (ruleMatches(rule, context)) {
|
|
391
|
+
return rule;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return void 0;
|
|
395
|
+
}
|
|
396
|
+
function isAllowed(policy, context) {
|
|
397
|
+
const result = evaluate(policy, context);
|
|
398
|
+
return result.decision === "allow";
|
|
399
|
+
}
|
|
400
|
+
function isDenied(policy, context) {
|
|
401
|
+
const result = evaluate(policy, context);
|
|
402
|
+
return result.decision === "deny";
|
|
403
|
+
}
|
|
404
|
+
function requiresReview(policy, context) {
|
|
405
|
+
const result = evaluate(policy, context);
|
|
406
|
+
return result.decision === "review";
|
|
407
|
+
}
|
|
408
|
+
function evaluateBatch(policy, contexts) {
|
|
409
|
+
return contexts.map((context) => evaluate(policy, context));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/compiler.ts
|
|
413
|
+
var PEAC_PROTOCOL_VERSION = "0.9";
|
|
414
|
+
function compilePeacTxt(policy, options = {}) {
|
|
415
|
+
const lines = [];
|
|
416
|
+
const { includeComments = true, peacVersion = PEAC_PROTOCOL_VERSION } = options;
|
|
417
|
+
if (includeComments) {
|
|
418
|
+
lines.push("# PEAC Policy Discovery File");
|
|
419
|
+
lines.push(`# Generated from: ${policy.name || "peac-policy.yaml"}`);
|
|
420
|
+
lines.push("#");
|
|
421
|
+
lines.push("# Serve at: /.well-known/peac.txt");
|
|
422
|
+
lines.push("# See: https://www.peacprotocol.org");
|
|
423
|
+
lines.push("");
|
|
424
|
+
}
|
|
425
|
+
lines.push(`version: ${peacVersion}`);
|
|
426
|
+
const usage = policy.defaults.decision === "allow" ? "open" : "conditional";
|
|
427
|
+
lines.push(`usage: ${usage}`);
|
|
428
|
+
lines.push("");
|
|
429
|
+
const purposes = extractPurposes(policy);
|
|
430
|
+
if (purposes.length > 0) {
|
|
431
|
+
lines.push(`purposes: [${purposes.join(", ")}]`);
|
|
432
|
+
}
|
|
433
|
+
if (options.attribution && options.attribution !== "none") {
|
|
434
|
+
lines.push(`attribution: ${options.attribution}`);
|
|
435
|
+
}
|
|
436
|
+
const receiptsDefault = usage === "conditional" ? "required" : "optional";
|
|
437
|
+
const receiptsValue = options.receipts ?? receiptsDefault;
|
|
438
|
+
if (receiptsValue !== "omit") {
|
|
439
|
+
lines.push(`receipts: ${receiptsValue}`);
|
|
440
|
+
}
|
|
441
|
+
if (options.rateLimit) {
|
|
442
|
+
lines.push(`rate_limit: ${options.rateLimit}`);
|
|
443
|
+
}
|
|
444
|
+
if (options.negotiateUrl) {
|
|
445
|
+
lines.push(`negotiate: ${options.negotiateUrl}`);
|
|
446
|
+
}
|
|
447
|
+
if (options.contact) {
|
|
448
|
+
lines.push(`contact: ${options.contact}`);
|
|
449
|
+
}
|
|
450
|
+
if (policy.rules.length > 0 && includeComments) {
|
|
451
|
+
lines.push("");
|
|
452
|
+
lines.push("# Policy rules (first-match-wins, author order preserved):");
|
|
453
|
+
lines.push(`# Source: ${policy.name || "peac-policy.yaml"} (${policy.rules.length} rules)`);
|
|
454
|
+
for (const rule of policy.rules) {
|
|
455
|
+
lines.push(`# ${rule.name}: ${rule.decision}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return lines.join("\n") + "\n";
|
|
459
|
+
}
|
|
460
|
+
function compileRobotsSnippet(policy, options = {}) {
|
|
461
|
+
const lines = [];
|
|
462
|
+
const { includeComments = true } = options;
|
|
463
|
+
const aiCrawlers = [
|
|
464
|
+
"Anthropic-AI",
|
|
465
|
+
"CCBot",
|
|
466
|
+
"ChatGPT-User",
|
|
467
|
+
"Claude-Web",
|
|
468
|
+
"Cohere-AI",
|
|
469
|
+
"GPTBot",
|
|
470
|
+
"Google-Extended",
|
|
471
|
+
"Meta-ExternalAgent",
|
|
472
|
+
"Meta-ExternalFetcher",
|
|
473
|
+
"PerplexityBot",
|
|
474
|
+
"anthropic-ai",
|
|
475
|
+
"cohere-ai"
|
|
476
|
+
];
|
|
477
|
+
if (includeComments) {
|
|
478
|
+
lines.push("# AI Crawler Directives");
|
|
479
|
+
lines.push(`# Generated from PEAC policy: ${policy.name || "peac-policy.yaml"}`);
|
|
480
|
+
lines.push("#");
|
|
481
|
+
lines.push("# SNIPPET - Review before adding to your robots.txt");
|
|
482
|
+
lines.push(`# Default policy: ${policy.defaults.decision}`);
|
|
483
|
+
lines.push("");
|
|
484
|
+
}
|
|
485
|
+
const isDefaultAllow = policy.defaults.decision === "allow";
|
|
486
|
+
for (const crawler of aiCrawlers) {
|
|
487
|
+
lines.push(`User-agent: ${crawler}`);
|
|
488
|
+
if (isDefaultAllow) {
|
|
489
|
+
lines.push("Allow: /");
|
|
490
|
+
} else {
|
|
491
|
+
lines.push("Disallow: /");
|
|
492
|
+
if (includeComments) {
|
|
493
|
+
lines.push("# Requires PEAC receipt for access");
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
lines.push("");
|
|
497
|
+
}
|
|
498
|
+
return lines.join("\n");
|
|
499
|
+
}
|
|
500
|
+
function compileAiprefTemplates(policy, options = {}) {
|
|
501
|
+
const templates = [];
|
|
502
|
+
const { peacVersion = PEAC_PROTOCOL_VERSION } = options;
|
|
503
|
+
const usage = policy.defaults.decision === "allow" ? "open" : "conditional";
|
|
504
|
+
templates.push({
|
|
505
|
+
header: "PEAC-Policy",
|
|
506
|
+
value: `version=${peacVersion}; usage=${usage}; rules=${policy.rules.length}`,
|
|
507
|
+
description: "Debug/compatibility header - see peac.txt for authoritative policy"
|
|
508
|
+
});
|
|
509
|
+
if (policy.defaults.decision === "deny") {
|
|
510
|
+
templates.push({
|
|
511
|
+
header: "X-Robots-Tag",
|
|
512
|
+
value: "noai, noimageai",
|
|
513
|
+
description: "Compatibility header: signal no AI training (default deny policy)"
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
templates.push({
|
|
517
|
+
header: "# Compatibility Note",
|
|
518
|
+
value: "See /.well-known/peac.txt for authoritative policy",
|
|
519
|
+
description: "These headers are for compatibility only. AIPREF-style X-AI-* headers are not generated to avoid contradictions with conditional rules."
|
|
520
|
+
});
|
|
521
|
+
return templates;
|
|
522
|
+
}
|
|
523
|
+
function renderPolicyMarkdown(policy, options = {}) {
|
|
524
|
+
const lines = [];
|
|
525
|
+
lines.push(`# ${policy.name || "AI Access Policy"}`);
|
|
526
|
+
lines.push("");
|
|
527
|
+
lines.push(`> Generated from PEAC policy (${policy.version})`);
|
|
528
|
+
lines.push("");
|
|
529
|
+
lines.push("## Summary");
|
|
530
|
+
lines.push("");
|
|
531
|
+
lines.push(`- **Default Decision:** ${policy.defaults.decision}`);
|
|
532
|
+
if (policy.defaults.reason) {
|
|
533
|
+
lines.push(`- **Default Reason:** ${policy.defaults.reason}`);
|
|
534
|
+
}
|
|
535
|
+
lines.push(`- **Total Rules:** ${policy.rules.length}`);
|
|
536
|
+
lines.push("");
|
|
537
|
+
if (options.contact) {
|
|
538
|
+
lines.push(`For questions about this policy, contact: ${options.contact}`);
|
|
539
|
+
lines.push("");
|
|
540
|
+
}
|
|
541
|
+
lines.push("## How This Policy Works");
|
|
542
|
+
lines.push("");
|
|
543
|
+
lines.push(
|
|
544
|
+
"This policy uses **first-match-wins** semantics (like firewall rules). When an AI agent requests access:"
|
|
545
|
+
);
|
|
546
|
+
lines.push("");
|
|
547
|
+
lines.push("1. Rules are evaluated in order");
|
|
548
|
+
lines.push("2. The first matching rule determines the decision");
|
|
549
|
+
lines.push("3. If no rule matches, the default decision applies");
|
|
550
|
+
lines.push("");
|
|
551
|
+
if (policy.rules.length > 0) {
|
|
552
|
+
lines.push("## Rules");
|
|
553
|
+
lines.push("");
|
|
554
|
+
lines.push("> Rules are evaluated in order. The first matching rule wins.");
|
|
555
|
+
lines.push("");
|
|
556
|
+
for (const rule of policy.rules) {
|
|
557
|
+
lines.push(`### ${rule.name}`);
|
|
558
|
+
lines.push("");
|
|
559
|
+
lines.push(`- **Decision:** ${rule.decision}`);
|
|
560
|
+
if (rule.reason) {
|
|
561
|
+
lines.push(`- **Reason:** ${rule.reason}`);
|
|
562
|
+
}
|
|
563
|
+
if (rule.subject) {
|
|
564
|
+
const subjectParts = [];
|
|
565
|
+
if (rule.subject.type) {
|
|
566
|
+
const types = Array.isArray(rule.subject.type) ? rule.subject.type.join(", ") : rule.subject.type;
|
|
567
|
+
subjectParts.push(`type: ${types}`);
|
|
568
|
+
}
|
|
569
|
+
if (rule.subject.labels) {
|
|
570
|
+
subjectParts.push(`labels: ${rule.subject.labels.join(", ")}`);
|
|
571
|
+
}
|
|
572
|
+
if (rule.subject.id) {
|
|
573
|
+
subjectParts.push(`id: ${rule.subject.id}`);
|
|
574
|
+
}
|
|
575
|
+
if (subjectParts.length > 0) {
|
|
576
|
+
lines.push(`- **Subject:** ${subjectParts.join("; ")}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (rule.purpose) {
|
|
580
|
+
const purposes = Array.isArray(rule.purpose) ? rule.purpose.join(", ") : rule.purpose;
|
|
581
|
+
lines.push(`- **Purpose:** ${purposes}`);
|
|
582
|
+
}
|
|
583
|
+
if (rule.licensing_mode) {
|
|
584
|
+
const modes = Array.isArray(rule.licensing_mode) ? rule.licensing_mode.join(", ") : rule.licensing_mode;
|
|
585
|
+
lines.push(`- **Licensing Mode:** ${modes}`);
|
|
586
|
+
}
|
|
587
|
+
lines.push("");
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
lines.push("---");
|
|
591
|
+
lines.push("");
|
|
592
|
+
lines.push(
|
|
593
|
+
"*This policy is enforced via the PEAC Protocol. See [peacprotocol.org](https://www.peacprotocol.org) for more information.*"
|
|
594
|
+
);
|
|
595
|
+
lines.push("");
|
|
596
|
+
return lines.join("\n");
|
|
597
|
+
}
|
|
598
|
+
function extractPurposes(policy) {
|
|
599
|
+
const purposes = /* @__PURE__ */ new Set();
|
|
600
|
+
for (const rule of policy.rules) {
|
|
601
|
+
if (rule.purpose) {
|
|
602
|
+
if (Array.isArray(rule.purpose)) {
|
|
603
|
+
for (const p of rule.purpose) {
|
|
604
|
+
purposes.add(p);
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
purposes.add(rule.purpose);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return Array.from(purposes).sort();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/generated/profiles.ts
|
|
615
|
+
var PROFILE_IDS = ["api-provider", "news-media", "open-source", "saas-docs"];
|
|
616
|
+
var PROFILES = {
|
|
617
|
+
"api-provider": {
|
|
618
|
+
defaults: {
|
|
619
|
+
rate_limit: {
|
|
620
|
+
limit: 200,
|
|
621
|
+
window_seconds: 3600
|
|
622
|
+
},
|
|
623
|
+
requirements: {
|
|
624
|
+
receipt: true
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
description: "Policy profile for API providers and developer platforms.\nAllows search indexing for API discoverability.\nBlocks training to protect API design and documentation IP.\nRequires receipts for inference to enable usage tracking.\n",
|
|
628
|
+
id: "api-provider",
|
|
629
|
+
name: "API Provider",
|
|
630
|
+
parameters: {
|
|
631
|
+
contact: {
|
|
632
|
+
description: "Developer relations or API support email",
|
|
633
|
+
example: "api-support@example.com",
|
|
634
|
+
required: true,
|
|
635
|
+
validate: "email"
|
|
636
|
+
},
|
|
637
|
+
negotiate_url: {
|
|
638
|
+
description: "URL for API access negotiation",
|
|
639
|
+
required: false,
|
|
640
|
+
validate: "url"
|
|
641
|
+
},
|
|
642
|
+
rate_limit: {
|
|
643
|
+
default: "200/hour",
|
|
644
|
+
description: "Rate limit for API documentation access",
|
|
645
|
+
validate: "rate_limit"
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
policy: {
|
|
649
|
+
defaults: {
|
|
650
|
+
decision: "deny",
|
|
651
|
+
reason: "Default deny - explicit permission required"
|
|
652
|
+
},
|
|
653
|
+
name: "API Provider Policy",
|
|
654
|
+
rules: [
|
|
655
|
+
{
|
|
656
|
+
decision: "allow",
|
|
657
|
+
name: "allow-discovery",
|
|
658
|
+
purpose: ["crawl", "index", "search", "ai_index"],
|
|
659
|
+
reason: "Allow API discovery and search indexing"
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
decision: "deny",
|
|
663
|
+
name: "block-training",
|
|
664
|
+
purpose: "train",
|
|
665
|
+
reason: "API documentation training requires license"
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
decision: "review",
|
|
669
|
+
name: "inference-with-receipt",
|
|
670
|
+
purpose: ["inference", "ai_input"],
|
|
671
|
+
reason: "Inference requires valid PEAC receipt for tracking"
|
|
672
|
+
}
|
|
673
|
+
],
|
|
674
|
+
version: "peac-policy/0.1"
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
"news-media": {
|
|
678
|
+
defaults: {
|
|
679
|
+
rate_limit: {
|
|
680
|
+
limit: 100,
|
|
681
|
+
window_seconds: 3600
|
|
682
|
+
},
|
|
683
|
+
requirements: {
|
|
684
|
+
receipt: true
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
description: "Policy profile for news and media publishers.\nAllows search engine indexing while blocking unauthorized AI training.\nInference access requires a valid PEAC receipt.\n",
|
|
688
|
+
id: "news-media",
|
|
689
|
+
name: "News Media Publisher",
|
|
690
|
+
parameters: {
|
|
691
|
+
contact: {
|
|
692
|
+
description: "Contact email for licensing inquiries",
|
|
693
|
+
example: "licensing@example.com",
|
|
694
|
+
required: true,
|
|
695
|
+
validate: "email"
|
|
696
|
+
},
|
|
697
|
+
negotiate_url: {
|
|
698
|
+
description: "URL for license negotiation",
|
|
699
|
+
required: false,
|
|
700
|
+
validate: "url"
|
|
701
|
+
},
|
|
702
|
+
rate_limit: {
|
|
703
|
+
default: "100/hour",
|
|
704
|
+
description: "Rate limit for allowed purposes",
|
|
705
|
+
validate: "rate_limit"
|
|
706
|
+
}
|
|
707
|
+
},
|
|
708
|
+
policy: {
|
|
709
|
+
defaults: {
|
|
710
|
+
decision: "deny",
|
|
711
|
+
reason: "Default deny - explicit permission required"
|
|
712
|
+
},
|
|
713
|
+
name: "News Media Policy",
|
|
714
|
+
rules: [
|
|
715
|
+
{
|
|
716
|
+
decision: "allow",
|
|
717
|
+
name: "allow-search-indexing",
|
|
718
|
+
purpose: ["crawl", "index", "search", "ai_index"],
|
|
719
|
+
reason: "Allow discovery and search engine indexing"
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
decision: "deny",
|
|
723
|
+
name: "block-training",
|
|
724
|
+
purpose: "train",
|
|
725
|
+
reason: "Training requires explicit licensing agreement"
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
decision: "review",
|
|
729
|
+
name: "inference-needs-receipt",
|
|
730
|
+
purpose: ["inference", "ai_input"],
|
|
731
|
+
reason: "Inference access requires valid PEAC receipt"
|
|
732
|
+
}
|
|
733
|
+
],
|
|
734
|
+
version: "peac-policy/0.1"
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
"open-source": {
|
|
738
|
+
defaults: {
|
|
739
|
+
rate_limit: {
|
|
740
|
+
limit: 1e3,
|
|
741
|
+
window_seconds: 3600
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
description: "Policy profile for open source projects.\nFully open access including AI training.\nEncourages broad AI ecosystem adoption.\nReceipts are optional for attribution tracking.\n",
|
|
745
|
+
id: "open-source",
|
|
746
|
+
name: "Open Source Project",
|
|
747
|
+
parameters: {
|
|
748
|
+
attribution: {
|
|
749
|
+
default: "optional",
|
|
750
|
+
description: "Attribution requirement",
|
|
751
|
+
example: "required"
|
|
752
|
+
},
|
|
753
|
+
contact: {
|
|
754
|
+
description: "Project contact or maintainer email",
|
|
755
|
+
example: "maintainer@example.com",
|
|
756
|
+
required: false,
|
|
757
|
+
validate: "email"
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
policy: {
|
|
761
|
+
defaults: {
|
|
762
|
+
decision: "allow",
|
|
763
|
+
reason: "Open source - all access permitted"
|
|
764
|
+
},
|
|
765
|
+
name: "Open Source Policy",
|
|
766
|
+
rules: [],
|
|
767
|
+
version: "peac-policy/0.1"
|
|
768
|
+
}
|
|
769
|
+
},
|
|
770
|
+
"saas-docs": {
|
|
771
|
+
defaults: {
|
|
772
|
+
rate_limit: {
|
|
773
|
+
limit: 500,
|
|
774
|
+
window_seconds: 3600
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
description: "Policy profile for SaaS documentation sites.\nOpen access for search and inference to improve discoverability.\nTraining blocked to protect proprietary documentation.\nReceipts are optional to encourage AI agent adoption.\n",
|
|
778
|
+
id: "saas-docs",
|
|
779
|
+
name: "SaaS Documentation",
|
|
780
|
+
parameters: {
|
|
781
|
+
contact: {
|
|
782
|
+
description: "Contact email for questions",
|
|
783
|
+
example: "docs@example.com",
|
|
784
|
+
required: false,
|
|
785
|
+
validate: "email"
|
|
786
|
+
},
|
|
787
|
+
rate_limit: {
|
|
788
|
+
default: "500/hour",
|
|
789
|
+
description: "Rate limit for access",
|
|
790
|
+
validate: "rate_limit"
|
|
791
|
+
}
|
|
792
|
+
},
|
|
793
|
+
policy: {
|
|
794
|
+
defaults: {
|
|
795
|
+
decision: "allow",
|
|
796
|
+
reason: "Open by default for documentation"
|
|
797
|
+
},
|
|
798
|
+
name: "SaaS Documentation Policy",
|
|
799
|
+
rules: [
|
|
800
|
+
{
|
|
801
|
+
decision: "deny",
|
|
802
|
+
name: "block-training",
|
|
803
|
+
purpose: "train",
|
|
804
|
+
reason: "Proprietary documentation - training requires license"
|
|
805
|
+
}
|
|
806
|
+
],
|
|
807
|
+
version: "peac-policy/0.1"
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// src/profiles.ts
|
|
813
|
+
var ProfileError = class extends Error {
|
|
814
|
+
constructor(message, code) {
|
|
815
|
+
super(message);
|
|
816
|
+
this.code = code;
|
|
817
|
+
this.name = "ProfileError";
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
function listProfiles() {
|
|
821
|
+
return [...PROFILE_IDS];
|
|
822
|
+
}
|
|
823
|
+
function hasProfile(id) {
|
|
824
|
+
return PROFILE_IDS.includes(id);
|
|
825
|
+
}
|
|
826
|
+
function loadProfile(id) {
|
|
827
|
+
const profile = PROFILES[id];
|
|
828
|
+
if (!profile) {
|
|
829
|
+
throw new ProfileError(`Profile not found: ${id}`, "PROFILE_NOT_FOUND");
|
|
830
|
+
}
|
|
831
|
+
return profile;
|
|
832
|
+
}
|
|
833
|
+
function getProfile(id) {
|
|
834
|
+
if (!hasProfile(id)) {
|
|
835
|
+
return void 0;
|
|
836
|
+
}
|
|
837
|
+
return PROFILES[id];
|
|
838
|
+
}
|
|
839
|
+
function validateProfileParams(profile, params) {
|
|
840
|
+
const def = typeof profile === "string" ? loadProfile(profile) : profile;
|
|
841
|
+
const errors = [];
|
|
842
|
+
const warnings = [];
|
|
843
|
+
const paramDefs = def.parameters || {};
|
|
844
|
+
const providedKeys = new Set(Object.keys(params));
|
|
845
|
+
for (const [key, paramDef] of Object.entries(paramDefs)) {
|
|
846
|
+
if (paramDef.required && !providedKeys.has(key)) {
|
|
847
|
+
errors.push({
|
|
848
|
+
parameter: key,
|
|
849
|
+
message: `Required parameter missing: ${key}`,
|
|
850
|
+
code: "MISSING_REQUIRED"
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
for (const [key, value] of Object.entries(params)) {
|
|
855
|
+
const paramDef = paramDefs[key];
|
|
856
|
+
if (!paramDef) {
|
|
857
|
+
errors.push({
|
|
858
|
+
parameter: key,
|
|
859
|
+
message: `Unknown parameter: ${key}`,
|
|
860
|
+
code: "UNKNOWN_PARAMETER"
|
|
861
|
+
});
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
if (value === void 0 || value === null) {
|
|
865
|
+
if (paramDef.required) {
|
|
866
|
+
errors.push({
|
|
867
|
+
parameter: key,
|
|
868
|
+
message: `Required parameter is null/undefined: ${key}`,
|
|
869
|
+
code: "MISSING_REQUIRED"
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
const strValue = String(value);
|
|
875
|
+
const validationError = validateParameterValue(key, strValue, paramDef);
|
|
876
|
+
if (validationError) {
|
|
877
|
+
errors.push(validationError);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
valid: errors.length === 0,
|
|
882
|
+
errors,
|
|
883
|
+
warnings
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function validateParameterValue(key, value, paramDef) {
|
|
887
|
+
if (!paramDef.validate) {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
switch (paramDef.validate) {
|
|
891
|
+
case "email": {
|
|
892
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
893
|
+
if (!emailRegex.test(value)) {
|
|
894
|
+
return {
|
|
895
|
+
parameter: key,
|
|
896
|
+
message: `Invalid email format: ${value}`,
|
|
897
|
+
code: "INVALID_FORMAT"
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
case "url": {
|
|
903
|
+
try {
|
|
904
|
+
new URL(value);
|
|
905
|
+
} catch {
|
|
906
|
+
return {
|
|
907
|
+
parameter: key,
|
|
908
|
+
message: `Invalid URL format: ${value}`,
|
|
909
|
+
code: "INVALID_FORMAT"
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
case "rate_limit": {
|
|
915
|
+
try {
|
|
916
|
+
parseRateLimit(value);
|
|
917
|
+
} catch {
|
|
918
|
+
const parsed = RateLimitConfigSchema.safeParse(value);
|
|
919
|
+
if (!parsed.success) {
|
|
920
|
+
return {
|
|
921
|
+
parameter: key,
|
|
922
|
+
message: `Invalid rate limit format: ${value}. Expected format: "100/hour" or RateLimitConfig object`,
|
|
923
|
+
code: "INVALID_FORMAT"
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
function customizeProfile(profile, params = {}) {
|
|
933
|
+
const def = typeof profile === "string" ? loadProfile(profile) : profile;
|
|
934
|
+
const validation = validateProfileParams(def, params);
|
|
935
|
+
if (!validation.valid) {
|
|
936
|
+
const errorMessages = validation.errors.map((e) => `${e.parameter}: ${e.message}`).join(", ");
|
|
937
|
+
throw new ProfileError(`Parameter validation failed: ${errorMessages}`, "VALIDATION_FAILED");
|
|
938
|
+
}
|
|
939
|
+
const appliedParams = {};
|
|
940
|
+
for (const [key, paramDef] of Object.entries(def.parameters || {})) {
|
|
941
|
+
if (key in params && params[key] !== void 0 && params[key] !== null) {
|
|
942
|
+
appliedParams[key] = params[key];
|
|
943
|
+
} else if (paramDef.default !== void 0) {
|
|
944
|
+
appliedParams[key] = paramDef.default;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const policy = JSON.parse(JSON.stringify(def.policy));
|
|
948
|
+
const appliedDefaults = {};
|
|
949
|
+
if (def.defaults?.requirements) {
|
|
950
|
+
appliedDefaults.requirements = { ...def.defaults.requirements };
|
|
951
|
+
}
|
|
952
|
+
if (def.defaults?.rate_limit) {
|
|
953
|
+
appliedDefaults.rate_limit = { ...def.defaults.rate_limit };
|
|
954
|
+
}
|
|
955
|
+
if (appliedParams.rate_limit) {
|
|
956
|
+
const rateLimitValue = appliedParams.rate_limit;
|
|
957
|
+
if (typeof rateLimitValue === "string") {
|
|
958
|
+
try {
|
|
959
|
+
appliedDefaults.rate_limit = parseRateLimit(rateLimitValue);
|
|
960
|
+
} catch {
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return {
|
|
965
|
+
policy,
|
|
966
|
+
appliedDefaults,
|
|
967
|
+
parameters: appliedParams
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function getAllProfiles() {
|
|
971
|
+
return PROFILE_IDS.map((id) => PROFILES[id]);
|
|
972
|
+
}
|
|
973
|
+
function getProfileSummary(profile) {
|
|
974
|
+
const def = typeof profile === "string" ? loadProfile(profile) : profile;
|
|
975
|
+
const requiredParams = [];
|
|
976
|
+
const optionalParams = [];
|
|
977
|
+
for (const [key, paramDef] of Object.entries(def.parameters || {})) {
|
|
978
|
+
if (paramDef.required) {
|
|
979
|
+
requiredParams.push(key);
|
|
980
|
+
} else {
|
|
981
|
+
optionalParams.push(key);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return {
|
|
985
|
+
id: def.id,
|
|
986
|
+
name: def.name,
|
|
987
|
+
description: def.description,
|
|
988
|
+
defaultDecision: def.policy.defaults.decision,
|
|
989
|
+
ruleCount: def.policy.rules?.length || 0,
|
|
990
|
+
requiresReceipt: def.defaults?.requirements?.receipt ?? false,
|
|
991
|
+
requiredParams: requiredParams.sort(),
|
|
992
|
+
optionalParams: optionalParams.sort()
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/enforce.ts
|
|
997
|
+
function enforceDecision(decision, context = {}) {
|
|
998
|
+
switch (decision) {
|
|
999
|
+
case "allow":
|
|
1000
|
+
return {
|
|
1001
|
+
allowed: true,
|
|
1002
|
+
statusCode: 200,
|
|
1003
|
+
reason: "Access allowed by policy",
|
|
1004
|
+
challenge: false,
|
|
1005
|
+
decision
|
|
1006
|
+
};
|
|
1007
|
+
case "deny":
|
|
1008
|
+
return {
|
|
1009
|
+
allowed: false,
|
|
1010
|
+
statusCode: 403,
|
|
1011
|
+
reason: "Access denied by policy",
|
|
1012
|
+
challenge: false,
|
|
1013
|
+
decision
|
|
1014
|
+
};
|
|
1015
|
+
case "review": {
|
|
1016
|
+
if (context.receiptVerified === true) {
|
|
1017
|
+
return {
|
|
1018
|
+
allowed: true,
|
|
1019
|
+
statusCode: 200,
|
|
1020
|
+
reason: "Access allowed - receipt verified",
|
|
1021
|
+
challenge: false,
|
|
1022
|
+
decision
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
return {
|
|
1026
|
+
allowed: false,
|
|
1027
|
+
statusCode: 402,
|
|
1028
|
+
reason: "Access requires verification - present valid receipt",
|
|
1029
|
+
challenge: true,
|
|
1030
|
+
decision
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
default: {
|
|
1034
|
+
const _exhaustive = decision;
|
|
1035
|
+
return {
|
|
1036
|
+
allowed: false,
|
|
1037
|
+
statusCode: 403,
|
|
1038
|
+
reason: `Unknown decision: ${_exhaustive}`,
|
|
1039
|
+
challenge: false,
|
|
1040
|
+
decision
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function requiresChallenge(result) {
|
|
1046
|
+
return result.challenge;
|
|
1047
|
+
}
|
|
1048
|
+
function getChallengeHeader(result) {
|
|
1049
|
+
if (!result.challenge) {
|
|
1050
|
+
return void 0;
|
|
1051
|
+
}
|
|
1052
|
+
return 'PEAC realm="receipt", error="receipt_required"';
|
|
1053
|
+
}
|
|
1054
|
+
function enforceForHttp(decision, context = {}) {
|
|
1055
|
+
const result = enforceDecision(decision, context);
|
|
1056
|
+
const headers = {};
|
|
1057
|
+
if (result.challenge) {
|
|
1058
|
+
const challengeHeader = getChallengeHeader(result);
|
|
1059
|
+
if (challengeHeader) {
|
|
1060
|
+
headers["WWW-Authenticate"] = challengeHeader;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return {
|
|
1064
|
+
status: result.statusCode,
|
|
1065
|
+
headers,
|
|
1066
|
+
allowed: result.allowed,
|
|
1067
|
+
reason: result.reason
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
function enforcePurposeDecision(decision, context) {
|
|
1071
|
+
if (context.explicitUndeclared) {
|
|
1072
|
+
return {
|
|
1073
|
+
allowed: false,
|
|
1074
|
+
statusCode: 400,
|
|
1075
|
+
reason: '"undeclared" is not a valid purpose token - it is internal-only',
|
|
1076
|
+
decision
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
if (!context.purposeValid) {
|
|
1080
|
+
const tokenList = context.invalidTokens?.join(", ") || "unknown";
|
|
1081
|
+
return {
|
|
1082
|
+
allowed: false,
|
|
1083
|
+
statusCode: 400,
|
|
1084
|
+
reason: `Invalid purpose token(s): ${tokenList}`,
|
|
1085
|
+
decision
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
switch (decision) {
|
|
1089
|
+
case "allow":
|
|
1090
|
+
return {
|
|
1091
|
+
allowed: true,
|
|
1092
|
+
statusCode: 200,
|
|
1093
|
+
reason: "Purpose allowed by policy",
|
|
1094
|
+
decision
|
|
1095
|
+
};
|
|
1096
|
+
case "deny":
|
|
1097
|
+
return {
|
|
1098
|
+
allowed: false,
|
|
1099
|
+
statusCode: 403,
|
|
1100
|
+
reason: "Purpose denied by policy",
|
|
1101
|
+
decision
|
|
1102
|
+
};
|
|
1103
|
+
case "review":
|
|
1104
|
+
return {
|
|
1105
|
+
allowed: false,
|
|
1106
|
+
statusCode: 403,
|
|
1107
|
+
reason: "Purpose requires review - treated as denied for purpose enforcement",
|
|
1108
|
+
decision
|
|
1109
|
+
};
|
|
1110
|
+
default: {
|
|
1111
|
+
const _exhaustive = decision;
|
|
1112
|
+
return {
|
|
1113
|
+
allowed: false,
|
|
1114
|
+
statusCode: 403,
|
|
1115
|
+
reason: `Unknown decision: ${_exhaustive}`,
|
|
1116
|
+
decision
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
function getPurposeDecisionStatusCode(decision, purposeValid) {
|
|
1122
|
+
if (!purposeValid) {
|
|
1123
|
+
return 400;
|
|
1124
|
+
}
|
|
1125
|
+
switch (decision) {
|
|
1126
|
+
case "allow":
|
|
1127
|
+
return 200;
|
|
1128
|
+
case "deny":
|
|
1129
|
+
case "review":
|
|
1130
|
+
return 403;
|
|
1131
|
+
default:
|
|
1132
|
+
return 403;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// src/enforcement-profiles.ts
|
|
1137
|
+
var STRICT_PROFILE = {
|
|
1138
|
+
id: "strict",
|
|
1139
|
+
name: "Strict",
|
|
1140
|
+
description: "Deny undeclared purposes. Use for regulated data, private APIs, or compliance-critical resources.",
|
|
1141
|
+
undeclared_decision: "deny",
|
|
1142
|
+
unknown_decision: "deny",
|
|
1143
|
+
purpose_reason: "denied",
|
|
1144
|
+
receipts: "required"
|
|
1145
|
+
};
|
|
1146
|
+
var BALANCED_PROFILE = {
|
|
1147
|
+
id: "balanced",
|
|
1148
|
+
name: "Balanced",
|
|
1149
|
+
description: "Review undeclared purposes with rate limits. Default for general web publishers.",
|
|
1150
|
+
undeclared_decision: "review",
|
|
1151
|
+
unknown_decision: "review",
|
|
1152
|
+
purpose_reason: "undeclared_default",
|
|
1153
|
+
default_constraints: {
|
|
1154
|
+
rate_limit: {
|
|
1155
|
+
window_s: 3600,
|
|
1156
|
+
// 1 hour
|
|
1157
|
+
max: 100,
|
|
1158
|
+
retry_after_s: 60
|
|
1159
|
+
}
|
|
1160
|
+
},
|
|
1161
|
+
receipts: "optional"
|
|
1162
|
+
};
|
|
1163
|
+
var OPEN_PROFILE = {
|
|
1164
|
+
id: "open",
|
|
1165
|
+
name: "Open",
|
|
1166
|
+
description: "Allow undeclared purposes with recording. Use for public content and research data.",
|
|
1167
|
+
undeclared_decision: "allow",
|
|
1168
|
+
unknown_decision: "allow",
|
|
1169
|
+
purpose_reason: "allowed",
|
|
1170
|
+
receipts: "optional"
|
|
1171
|
+
};
|
|
1172
|
+
var ENFORCEMENT_PROFILES = {
|
|
1173
|
+
strict: STRICT_PROFILE,
|
|
1174
|
+
balanced: BALANCED_PROFILE,
|
|
1175
|
+
open: OPEN_PROFILE
|
|
1176
|
+
};
|
|
1177
|
+
var DEFAULT_ENFORCEMENT_PROFILE = "balanced";
|
|
1178
|
+
var ENFORCEMENT_PROFILE_IDS = [
|
|
1179
|
+
"strict",
|
|
1180
|
+
"balanced",
|
|
1181
|
+
"open"
|
|
1182
|
+
];
|
|
1183
|
+
function getEnforcementProfile(id) {
|
|
1184
|
+
const profile = ENFORCEMENT_PROFILES[id];
|
|
1185
|
+
if (!profile) {
|
|
1186
|
+
throw new Error(`Invalid enforcement profile ID: ${id}`);
|
|
1187
|
+
}
|
|
1188
|
+
return profile;
|
|
1189
|
+
}
|
|
1190
|
+
function isEnforcementProfileId(id) {
|
|
1191
|
+
return ENFORCEMENT_PROFILE_IDS.includes(id);
|
|
1192
|
+
}
|
|
1193
|
+
function getDefaultEnforcementProfile() {
|
|
1194
|
+
return ENFORCEMENT_PROFILES[DEFAULT_ENFORCEMENT_PROFILE];
|
|
1195
|
+
}
|
|
1196
|
+
var CANONICAL_PURPOSES = /* @__PURE__ */ new Set(["train", "search", "user_action", "inference", "index"]);
|
|
1197
|
+
var LEGACY_PURPOSE_MAP = {
|
|
1198
|
+
crawl: "index",
|
|
1199
|
+
ai_input: "inference",
|
|
1200
|
+
ai_index: "index"
|
|
1201
|
+
};
|
|
1202
|
+
function isCanonicalPurpose(token) {
|
|
1203
|
+
return CANONICAL_PURPOSES.has(token);
|
|
1204
|
+
}
|
|
1205
|
+
function isLegacyPurpose(token) {
|
|
1206
|
+
return token in LEGACY_PURPOSE_MAP;
|
|
1207
|
+
}
|
|
1208
|
+
function evaluatePurpose(declaredPurposes, profileId = DEFAULT_ENFORCEMENT_PROFILE) {
|
|
1209
|
+
const profile = getEnforcementProfile(profileId);
|
|
1210
|
+
if (declaredPurposes.length === 0) {
|
|
1211
|
+
return {
|
|
1212
|
+
decision: profile.undeclared_decision,
|
|
1213
|
+
purpose_reason: "undeclared_default",
|
|
1214
|
+
constraints: profile.undeclared_decision === "review" ? profile.default_constraints : void 0,
|
|
1215
|
+
purpose_declared: false,
|
|
1216
|
+
has_unknown_tokens: false,
|
|
1217
|
+
unknown_tokens: [],
|
|
1218
|
+
profile_id: profileId
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
const canonicalTokens = [];
|
|
1222
|
+
const legacyTokens = [];
|
|
1223
|
+
const unknownTokens = [];
|
|
1224
|
+
for (const token of declaredPurposes) {
|
|
1225
|
+
if (isCanonicalPurpose(token)) {
|
|
1226
|
+
canonicalTokens.push(token);
|
|
1227
|
+
} else if (isLegacyPurpose(token)) {
|
|
1228
|
+
legacyTokens.push(token);
|
|
1229
|
+
} else {
|
|
1230
|
+
unknownTokens.push(token);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (unknownTokens.length > 0 && canonicalTokens.length === 0 && legacyTokens.length === 0) {
|
|
1234
|
+
return {
|
|
1235
|
+
decision: profile.unknown_decision,
|
|
1236
|
+
purpose_reason: "unknown_preserved",
|
|
1237
|
+
constraints: profile.unknown_decision === "review" ? profile.default_constraints : void 0,
|
|
1238
|
+
purpose_declared: true,
|
|
1239
|
+
has_unknown_tokens: true,
|
|
1240
|
+
unknown_tokens: unknownTokens,
|
|
1241
|
+
profile_id: profileId
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
const enforcedPurpose = canonicalTokens[0] ?? LEGACY_PURPOSE_MAP[legacyTokens[0]];
|
|
1245
|
+
return {
|
|
1246
|
+
decision: "allow",
|
|
1247
|
+
purpose_enforced: enforcedPurpose,
|
|
1248
|
+
purpose_reason: unknownTokens.length > 0 ? "unknown_preserved" : "allowed",
|
|
1249
|
+
purpose_declared: true,
|
|
1250
|
+
has_unknown_tokens: unknownTokens.length > 0,
|
|
1251
|
+
unknown_tokens: unknownTokens,
|
|
1252
|
+
profile_id: profileId
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
function getPurposeStatusCode(result) {
|
|
1256
|
+
switch (result.decision) {
|
|
1257
|
+
case "allow":
|
|
1258
|
+
return 200;
|
|
1259
|
+
case "review":
|
|
1260
|
+
case "deny":
|
|
1261
|
+
return 403;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
function getRetryAfter(constraints) {
|
|
1265
|
+
return constraints?.rate_limit?.retry_after_s;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
export { BALANCED_PROFILE, DEFAULT_ENFORCEMENT_PROFILE, DecisionRequirementsSchema, ENFORCEMENT_PROFILES, ENFORCEMENT_PROFILE_IDS, EnforcementProfileSchema, OPEN_PROFILE, PEAC_PROTOCOL_VERSION, POLICY_VERSION, PROFILES, PROFILE_IDS, PolicyConstraintsSchema, PolicyDefaultsSchema, PolicyDocumentSchema, PolicyLoadError, PolicyRuleSchema, PolicyValidationError, ProfileDefinitionSchema, ProfileError, ProfileParameterSchema, RateLimitConfigSchema, STRICT_PROFILE, SubjectMatcherSchema, compileAiprefTemplates, compilePeacTxt, compileRobotsSnippet, createExamplePolicy, customizeProfile, enforceDecision, enforceForHttp, enforcePurposeDecision, evaluate, evaluateBatch, evaluatePurpose, explainMatches, findEffectiveRule, formatRateLimit, getAllProfiles, getChallengeHeader, getDefaultEnforcementProfile, getEnforcementProfile, getProfile, getProfileSummary, getPurposeDecisionStatusCode, getPurposeStatusCode, getRetryAfter, hasProfile, isAllowed, isDenied, isEnforcementProfileId, listProfiles, loadPolicy, loadProfile, parsePolicy, parseRateLimit, policyFileExists, renderPolicyMarkdown, requiresChallenge, requiresReview, serializePolicyJson, serializePolicyYaml, validatePolicy, validateProfileParams };
|
|
1269
|
+
//# sourceMappingURL=index.mjs.map
|
|
1270
|
+
//# sourceMappingURL=index.mjs.map
|