@absolutejs/policy 0.1.0 → 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/README.md CHANGED
@@ -8,3 +8,11 @@ and simulation evaluates an unpersisted candidate without changing live policy.
8
8
  `createAuthzenAdapter()` speaks the OpenID AuthZEN evaluation API while custom
9
9
  adapters can target Cedar, OPA, cloud IAM, or an embedded rules engine. PostgreSQL
10
10
  enforces one active version per policy with a partial unique index.
11
+
12
+ Version 0.2 adds direct support for the OpenID AuthZEN AARP and COAZ Working
13
+ Group Drafts. AARP helpers extract requestable denials, preserve their binding,
14
+ submit idempotent access requests, persist portable task handles, and only
15
+ produce approval context for a fresh PDP evaluation. COAZ validates MCP
16
+ `coaz` / `x-coaz-mapping` declarations and constructs single or aligned bulk
17
+ SARC requests through an injected CEL evaluator. Any mapping, PDP, or decision
18
+ failure denies tool execution.
package/dist/aarp.d.ts ADDED
@@ -0,0 +1,77 @@
1
+ export type AuthzenEntity = Record<string, unknown>;
2
+ export type AarpRequestableDenial = {
3
+ endpoint: string;
4
+ evaluationId?: string;
5
+ evaluatedAt?: string;
6
+ reason?: string;
7
+ expiresAt: string;
8
+ bindingToken?: string;
9
+ template?: string;
10
+ display?: Record<string, unknown>;
11
+ formUrl?: string;
12
+ requestSchemaUrl?: string;
13
+ requestCatalogsUrl?: string;
14
+ };
15
+ export type AarpAccessRequest = {
16
+ subject: AuthzenEntity;
17
+ resource?: AuthzenEntity;
18
+ action?: AuthzenEntity;
19
+ items?: Array<{
20
+ resource: AuthzenEntity;
21
+ action: AuthzenEntity;
22
+ requested_access?: AuthzenEntity;
23
+ denial?: AuthzenEntity;
24
+ }>;
25
+ context?: AuthzenEntity;
26
+ requested_access?: AuthzenEntity;
27
+ denial: {
28
+ evaluation_id?: string;
29
+ evaluated_at?: string;
30
+ expires_at: string;
31
+ reason?: string;
32
+ binding_token?: string;
33
+ template?: string;
34
+ };
35
+ };
36
+ export type AarpTaskResponse = {
37
+ task: {
38
+ id: string;
39
+ status: "pending" | "approved" | "denied" | "expired" | "cancelled";
40
+ status_endpoint: string;
41
+ expires_at?: string;
42
+ progress?: Record<string, unknown>;
43
+ display?: Record<string, unknown>;
44
+ links?: Record<string, string>;
45
+ items?: unknown[];
46
+ };
47
+ result?: {
48
+ mode: string;
49
+ approval?: {
50
+ id: string;
51
+ state?: string;
52
+ approved_until?: string;
53
+ };
54
+ };
55
+ };
56
+ /** Extracts a requestable denial from an AuthZEN decision. It never converts the denial into permission. */
57
+ export declare const readAarpRequestableDenial: (decision: {
58
+ allowed: boolean;
59
+ metadata?: Record<string, unknown>;
60
+ }, pdpMetadata: {
61
+ access_request_endpoint?: string;
62
+ }, now?: number) => AarpRequestableDenial | undefined;
63
+ export declare const createAarpAccessRequest: (denial: AarpRequestableDenial, request: Omit<AarpAccessRequest, "denial">) => AarpAccessRequest;
64
+ export type AarpFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
65
+ export declare const createAarpClient: ({ fetch: request, trustedOrigins, headers, maxResponseBytes, }: {
66
+ fetch?: AarpFetch;
67
+ trustedOrigins: string[];
68
+ headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
69
+ maxResponseBytes?: number;
70
+ }) => {
71
+ submit: (denial: AarpRequestableDenial, body: AarpAccessRequest, idempotencyKey: string) => Promise<AarpTaskResponse>;
72
+ status: (task: AarpTaskResponse["task"]) => Promise<AarpTaskResponse>;
73
+ };
74
+ /** Approval is only input to a mandatory fresh AuthZEN evaluation. */
75
+ export declare const aarpReevaluationContext: (response: AarpTaskResponse) => {
76
+ approval: NonNullable<AarpTaskResponse["result"]>["approval"];
77
+ } | undefined;
package/dist/coaz.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ export type CoazMappingEntity = Record<string, string>;
2
+ export type CoazMapping = {
3
+ subject: CoazMappingEntity[];
4
+ resource: CoazMappingEntity[];
5
+ action?: CoazMappingEntity[];
6
+ context: CoazMappingEntity[];
7
+ };
8
+ export type CoazTool = {
9
+ name: string;
10
+ coaz?: boolean;
11
+ inputSchema: Record<string, unknown> & {
12
+ "x-coaz-mapping"?: CoazMapping;
13
+ };
14
+ };
15
+ export type CoazCelEvaluator = (expression: string, variables: {
16
+ params: {
17
+ name: string;
18
+ arguments: Record<string, unknown>;
19
+ };
20
+ token: Record<string, unknown>;
21
+ }) => unknown | Promise<unknown>;
22
+ export type AuthzenEvaluation = {
23
+ subject: Record<string, unknown>;
24
+ action: Record<string, unknown>;
25
+ resource: Record<string, unknown>;
26
+ context: Record<string, unknown>;
27
+ };
28
+ export type CoazRequest = {
29
+ endpoint: "evaluation";
30
+ body: AuthzenEvaluation;
31
+ } | {
32
+ endpoint: "evaluations";
33
+ body: Partial<AuthzenEvaluation> & {
34
+ evaluations: Array<Partial<AuthzenEvaluation>>;
35
+ };
36
+ };
37
+ export declare const validateCoazTool: (tool: CoazTool) => CoazMapping;
38
+ export declare const createCoazRequest: ({ tool, arguments: args, token, verifyToken, evaluate, }: {
39
+ tool: CoazTool;
40
+ arguments: Record<string, unknown>;
41
+ token: Record<string, unknown>;
42
+ /** Must verify signature, issuer, audience, and expiration before claims are mapped. */
43
+ verifyToken: (token: Record<string, unknown>) => boolean | Promise<boolean>;
44
+ evaluate: CoazCelEvaluator;
45
+ }) => Promise<CoazRequest>;
46
+ export declare const COAZ_ERROR_CODES: {
47
+ readonly mapping: -32602;
48
+ readonly denied: -32401;
49
+ readonly pdpUnavailable: -32603;
50
+ };
51
+ export declare const enforceCoazDecisions: (decisions: readonly {
52
+ decision: boolean;
53
+ }[]) => void;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export type PolicyStatus = "draft" | "active" | "retired";
2
+ export * from "./aarp";
3
+ export * from "./coaz";
2
4
  export type PolicyDocument = {
3
5
  createdAt: number;
4
6
  createdBy: string;
package/dist/index.js CHANGED
@@ -1,4 +1,194 @@
1
1
  // @bun
2
+ // src/aarp.ts
3
+ var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
4
+ var https = (value) => {
5
+ if (typeof value !== "string")
6
+ return;
7
+ try {
8
+ const url = new URL(value);
9
+ return url.protocol === "https:" ? url.href : undefined;
10
+ } catch {
11
+ return;
12
+ }
13
+ };
14
+ var readAarpRequestableDenial = (decision, pdpMetadata, now = Date.now()) => {
15
+ if (decision.allowed)
16
+ return;
17
+ const context = object(decision.metadata);
18
+ const hint = object(context?.access_request);
19
+ if (!hint || typeof hint.expires_at !== "string" || Date.parse(hint.expires_at) <= now)
20
+ return;
21
+ const endpoint = https(hint.endpoint) ?? https(pdpMetadata.access_request_endpoint);
22
+ const evaluationId = typeof context?.evaluation_id === "string" ? context.evaluation_id : undefined;
23
+ const bindingToken = typeof hint.binding_token === "string" ? hint.binding_token : undefined;
24
+ if (!endpoint || !evaluationId && !bindingToken)
25
+ return;
26
+ return {
27
+ endpoint,
28
+ expiresAt: hint.expires_at,
29
+ ...evaluationId ? { evaluationId } : {},
30
+ ...bindingToken ? { bindingToken } : {},
31
+ ...typeof context?.evaluated_at === "string" ? { evaluatedAt: context.evaluated_at } : {},
32
+ ...typeof context?.reason === "string" ? { reason: context.reason } : {},
33
+ ...typeof hint.template === "string" ? { template: hint.template } : {},
34
+ ...object(hint.display) ? { display: object(hint.display) } : {},
35
+ ...typeof hint.form_url === "string" ? { formUrl: hint.form_url } : {},
36
+ ...typeof hint.request_schema_url === "string" ? { requestSchemaUrl: hint.request_schema_url } : {},
37
+ ...typeof hint.request_catalogs_url === "string" ? { requestCatalogsUrl: hint.request_catalogs_url } : {}
38
+ };
39
+ };
40
+ var createAarpAccessRequest = (denial, request) => ({
41
+ ...structuredClone(request),
42
+ denial: {
43
+ expires_at: denial.expiresAt,
44
+ ...denial.evaluationId ? { evaluation_id: denial.evaluationId } : {},
45
+ ...denial.evaluatedAt ? { evaluated_at: denial.evaluatedAt } : {},
46
+ ...denial.reason ? { reason: denial.reason } : {},
47
+ ...denial.bindingToken ? { binding_token: denial.bindingToken } : {},
48
+ ...denial.template ? { template: denial.template } : {}
49
+ }
50
+ });
51
+ var createAarpClient = ({
52
+ fetch: request = fetch,
53
+ trustedOrigins,
54
+ headers,
55
+ maxResponseBytes = 65536
56
+ }) => {
57
+ const call = async (urlValue, init) => {
58
+ const url = new URL(urlValue);
59
+ if (url.protocol !== "https:" || !trustedOrigins.includes(url.origin))
60
+ throw new Error("Untrusted AARP endpoint");
61
+ const supplied = typeof headers === "function" ? await headers() : headers;
62
+ const response = await request(url, {
63
+ ...init,
64
+ redirect: "manual",
65
+ headers: {
66
+ accept: "application/json",
67
+ ...Object.fromEntries(new Headers(supplied)),
68
+ ...Object.fromEntries(new Headers(init.headers))
69
+ }
70
+ });
71
+ if (response.status >= 300 && response.status < 400)
72
+ throw new Error("AARP redirects are denied");
73
+ const bytes = new Uint8Array(await response.arrayBuffer());
74
+ if (bytes.byteLength > maxResponseBytes)
75
+ throw new Error("AARP response is too large");
76
+ if (!response.ok)
77
+ throw new Error(`AARP service failed: ${response.status}`);
78
+ const body = JSON.parse(new TextDecoder().decode(bytes));
79
+ if (!body.task || typeof body.task.id !== "string" || !https(body.task.status_endpoint))
80
+ throw new Error("Invalid AARP task response");
81
+ return body;
82
+ };
83
+ return {
84
+ submit: (denial, body, idempotencyKey) => call(denial.endpoint, {
85
+ method: "POST",
86
+ headers: {
87
+ "content-type": "application/json",
88
+ "idempotency-key": idempotencyKey
89
+ },
90
+ body: JSON.stringify(body)
91
+ }),
92
+ status: (task) => call(task.status_endpoint, { method: "GET" })
93
+ };
94
+ };
95
+ var aarpReevaluationContext = (response) => response.task.status === "approved" && response.result?.mode === "reevaluate" && response.result.approval ? { approval: structuredClone(response.result.approval) } : undefined;
96
+ // src/coaz.ts
97
+ var validateCoazTool = (tool) => {
98
+ if (tool.coaz !== true)
99
+ throw new Error("Tool is not declared COAZ compatible");
100
+ const mapping = tool.inputSchema["x-coaz-mapping"];
101
+ if (!mapping || !Array.isArray(mapping.subject) || !Array.isArray(mapping.resource) || !Array.isArray(mapping.context) || mapping.subject.length === 0 || mapping.resource.length === 0 || mapping.context.length === 0)
102
+ throw new Error("Malformed x-coaz-mapping");
103
+ const lengths = [
104
+ mapping.subject.length,
105
+ mapping.resource.length,
106
+ mapping.action?.length ?? 1,
107
+ mapping.context.length
108
+ ];
109
+ const maximum = Math.max(...lengths);
110
+ if (lengths.some((length) => length !== 1 && length !== maximum))
111
+ throw new Error("COAZ mapping arrays have mismatched element counts");
112
+ for (const entities of [
113
+ mapping.subject,
114
+ mapping.resource,
115
+ mapping.action ?? [],
116
+ mapping.context
117
+ ])
118
+ for (const entity of entities)
119
+ for (const expression of Object.values(entity))
120
+ if (typeof expression !== "string" || expression.length === 0)
121
+ throw new Error("COAZ mapping expressions must be non-empty strings");
122
+ return mapping;
123
+ };
124
+ var createCoazRequest = async ({
125
+ tool,
126
+ arguments: args,
127
+ token,
128
+ verifyToken,
129
+ evaluate
130
+ }) => {
131
+ if (!await verifyToken(token))
132
+ throw new Error("COAZ token verification failed");
133
+ const mapping = validateCoazTool(tool);
134
+ const variables = { params: { name: tool.name, arguments: args }, token };
135
+ const resolveEntity = async (entity) => Object.fromEntries(await Promise.all(Object.entries(entity).map(async ([key, expression]) => [
136
+ key,
137
+ await evaluate(expression, variables)
138
+ ])));
139
+ const resolved = {
140
+ subject: await Promise.all(mapping.subject.map(resolveEntity)),
141
+ resource: await Promise.all(mapping.resource.map(resolveEntity)),
142
+ action: await Promise.all((mapping.action ?? [{ name: `'${tool.name.replaceAll("'", "\\'")}'` }]).map(resolveEntity)),
143
+ context: await Promise.all(mapping.context.map(resolveEntity))
144
+ };
145
+ const maximum = Math.max(resolved.subject.length, resolved.resource.length, resolved.action.length, resolved.context.length);
146
+ const at = (values, index) => structuredClone(values.length === 1 ? values[0] : values[index]);
147
+ if (maximum === 1)
148
+ return {
149
+ endpoint: "evaluation",
150
+ body: {
151
+ subject: at(resolved.subject, 0),
152
+ resource: at(resolved.resource, 0),
153
+ action: at(resolved.action, 0),
154
+ context: at(resolved.context, 0)
155
+ }
156
+ };
157
+ const defaults = {};
158
+ if (resolved.subject.length === 1)
159
+ defaults.subject = at(resolved.subject, 0);
160
+ if (resolved.resource.length === 1)
161
+ defaults.resource = at(resolved.resource, 0);
162
+ if (resolved.action.length === 1)
163
+ defaults.action = at(resolved.action, 0);
164
+ if (resolved.context.length === 1)
165
+ defaults.context = at(resolved.context, 0);
166
+ return {
167
+ endpoint: "evaluations",
168
+ body: {
169
+ ...defaults,
170
+ evaluations: Array.from({ length: maximum }, (_, index) => ({
171
+ ...resolved.subject.length > 1 ? { subject: at(resolved.subject, index) } : {},
172
+ ...resolved.resource.length > 1 ? { resource: at(resolved.resource, index) } : {},
173
+ ...resolved.action.length > 1 ? { action: at(resolved.action, index) } : {},
174
+ ...resolved.context.length > 1 ? { context: at(resolved.context, index) } : {}
175
+ }))
176
+ }
177
+ };
178
+ };
179
+ var COAZ_ERROR_CODES = {
180
+ mapping: -32602,
181
+ denied: -32401,
182
+ pdpUnavailable: -32603
183
+ };
184
+ var enforceCoazDecisions = (decisions) => {
185
+ if (decisions.length === 0 || decisions.some((decision) => decision.decision !== true)) {
186
+ const error = new Error("AuthZEN denied the MCP tool invocation");
187
+ error.code = COAZ_ERROR_CODES.denied;
188
+ throw error;
189
+ }
190
+ };
191
+
2
192
  // src/index.ts
3
193
  var canonical = (value) => {
4
194
  if (value === null || typeof value !== "object")
@@ -171,9 +361,17 @@ var createPostgresPolicyStore = ({
171
361
  };
172
362
  };
173
363
  export {
364
+ validateCoazTool,
365
+ readAarpRequestableDenial,
174
366
  policyPostgresSchemaSql,
367
+ enforceCoazDecisions,
175
368
  createPostgresPolicyStore,
176
369
  createPolicyEngine,
177
370
  createMemoryPolicyStore,
178
- createAuthzenAdapter
371
+ createCoazRequest,
372
+ createAuthzenAdapter,
373
+ createAarpClient,
374
+ createAarpAccessRequest,
375
+ aarpReevaluationContext,
376
+ COAZ_ERROR_CODES
179
377
  };