{"files": {"main.ts": "import {run} from './core.ts';\nimport {POLICY} from './policy.ts';\nexport function evaluate(payload: any): any { return run(payload, POLICY); }\n", "core.ts": "import {asciiFold, buildResult, collectRoles} from './helpers.ts';\nexport function run(payload: any, policy: any): any {\n  const subject: any = payload.subject;\n  const resource: any = payload.resource;\n  if (subject.tenant !== resource.tenant) { return buildResult(false, 'tenant', [], policy); }\n  if (payload.now - payload.issued_at > policy.token_ttl) { return buildResult(false, 'token', [], policy); }\n  if (policy.suspended_denies && subject.suspended) { return buildResult(false, 'suspended', [], policy); }\n  if (policy.owner_bypass && subject.owner) { return buildResult(true, 'owner', [], policy); }\n  const roles: Set<string> = collectRoles(subject, policy, payload.role_parents);\n  const matched: number[] = [];\n  let anyAllow: boolean = false;\n  let anyDeny: boolean = false;\n  const grants: any[] = payload.grants;\n  for (let i: number = 0; i < grants.length; i++) {\n    const grant: any = grants[i];\n    if (!roles.has(grant.role)) { continue; }\n    let actionMatch: boolean = false;\n    if (grant.action === '*' && policy.wildcard_action) { actionMatch = true; } else if (policy.action_casefold) { actionMatch = asciiFold(grant.action) === asciiFold(payload.action); } else { actionMatch = grant.action === payload.action; }\n    if (!actionMatch) { continue; }\n    let scopeMatch: boolean = false;\n    if (policy.resource_scope === 'exact') { scopeMatch = grant.scope === resource.id; } else { scopeMatch = resource.id.startsWith(grant.scope); }\n    if (!scopeMatch) { continue; }\n    if (grant.expires !== null) {\n      if (policy.expiry_inclusive) { if (!(payload.now <= grant.expires)) { continue; } } else { if (!(payload.now < grant.expires)) { continue; } }\n    }\n    matched.push(i);\n    if (grant.allow) { anyAllow = true; } else { anyDeny = true; }\n  }\n  if (matched.length === 0) { return buildResult(!!policy.default_allow, 'default', [], policy); }\n  const allowed: boolean = policy.deny_wins ? (anyAllow && !anyDeny) : anyAllow;\n  return buildResult(allowed, 'grants', matched, policy);\n}\n", "policy.ts": "export const POLICY: any = {\"action_casefold\": false, \"audit_denials\": false, \"default_allow\": false, \"deny_wins\": false, \"expiry_inclusive\": false, \"group_union\": false, \"inherit_roles\": false, \"owner_bypass\": false, \"resource_scope\": \"prefix\", \"suspended_denies\": false, \"token_ttl\": 200, \"wildcard_action\": false};\n", "helpers.ts": "export function asciiFold(s: string): string { return s.replace(/[A-Z]/g, (c: string) => c.toLowerCase()); }\nexport function buildResult(allowed: boolean, reason: string, matched: number[], policy: any): any { return {allowed: allowed, reason: reason, matched: matched.slice(), audit: (!allowed && policy.audit_denials) ? [reason] : []}; }\nexport function collectRoles(subject: any, policy: any, roleParents: any): Set<string> {\n  const roles: Set<string> = new Set<string>();\n  const initial: string[] = subject.roles.slice();\n  if (policy.group_union) { for (const g of subject.groups) { initial.push(g); } }\n  for (const r of initial) { roles.add(r); }\n  if (policy.inherit_roles) {\n    const queue: string[] = Array.from(roles);\n    let index: number = 0;\n    while (index < queue.length) {\n      const current: string = queue[index];\n      index++;\n      const parents: any = roleParents ? roleParents[current] : undefined;\n      if (parents) { for (const p of parents) { if (!roles.has(p)) { roles.add(p); queue.push(p); } } }\n    }\n  }\n  return roles;\n}\n"}}