{"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 {actionMatches, collectRoles, expiryMatches, scopeMatches} from './helpers.ts';\nexport function run(payload: any, policy: any): any {\n  const result = (allowed: boolean, reason: string, matched: number[]): any => ({allowed: allowed, reason: reason, matched: matched, audit: (!allowed && policy.audit_denials) ? [reason] : []});\n  if (payload.subject.tenant !== payload.resource.tenant) { return result(false, 'tenant', []); }\n  if (payload.now - payload.issued_at > policy.token_ttl) { return result(false, 'token', []); }\n  if (policy.suspended_denies && payload.subject.suspended) { return result(false, 'suspended', []); }\n  if (policy.owner_bypass && payload.subject.owner) { return result(true, 'owner', []); }\n  const initial: string[] = [];\n  for (const r of payload.subject.roles) { initial.push(r); }\n  if (policy.group_union) { for (const g of payload.subject.groups) { initial.push(g); } }\n  const roles: Set<string> = collectRoles(initial, payload.role_parents, !!policy.inherit_roles);\n  const matched: number[] = [];\n  for (let i = 0; i < payload.grants.length; i++) {\n    const grant: any = payload.grants[i];\n    if (!roles.has(grant.role)) { continue; }\n    if (!actionMatches(grant.action, payload.action, !!policy.action_casefold, !!policy.wildcard_action)) { continue; }\n    if (!scopeMatches(grant.scope, payload.resource.id, policy.resource_scope)) { continue; }\n    if (!expiryMatches(payload.now, grant.expires, !!policy.expiry_inclusive)) { continue; }\n    matched.push(i);\n  }\n  if (matched.length === 0) { return result(!!policy.default_allow, 'default', []); }\n  let hasAllow: boolean = false;\n  let hasDeny: boolean = false;\n  for (const idx of matched) { if (payload.grants[idx].allow) { hasAllow = true; } else { hasDeny = true; } }\n  const allowed: boolean = policy.deny_wins ? (hasAllow && !hasDeny) : hasAllow;\n  return result(allowed, 'grants', matched);\n}\n", "policy.ts": "export const POLICY: any = {\"action_casefold\": true, \"audit_denials\": true, \"default_allow\": true, \"deny_wins\": true, \"expiry_inclusive\": true, \"group_union\": true, \"inherit_roles\": true, \"owner_bypass\": true, \"resource_scope\": \"exact\", \"suspended_denies\": true, \"token_ttl\": 100, \"wildcard_action\": true};\n", "helpers.ts": "export function asciiLower(s: string): string {\n  let out: string = '';\n  for (let i = 0; i < s.length; i++) {\n    const code: number = s.charCodeAt(i);\n    if (code >= 65 && code <= 90) { out += String.fromCharCode(code + 32); } else { out += s.charAt(i); }\n  }\n  return out;\n}\nexport function actionMatches(grantAction: string, requested: string, casefold: boolean, wildcard: boolean): boolean {\n  if (wildcard && grantAction === '*') { return true; }\n  if (casefold) { return asciiLower(grantAction) === asciiLower(requested); }\n  return grantAction === requested;\n}\nexport function scopeMatches(scope: string, resourceId: string, mode: string): boolean {\n  if (mode === 'prefix') { return resourceId.startsWith(scope); }\n  return scope === resourceId;\n}\nexport function expiryMatches(now: number, expires: any, inclusive: boolean): boolean {\n  if (expires === null || expires === undefined) { return true; }\n  return inclusive ? now <= expires : now < expires;\n}\nexport function collectRoles(initial: string[], roleParents: any, inherit: boolean): Set<string> {\n  const seen: Set<string> = new Set<string>();\n  const stack: string[] = [];\n  for (const r of initial) { if (!seen.has(r)) { seen.add(r); stack.push(r); } }\n  if (!inherit) { return seen; }\n  while (stack.length > 0) {\n    const cur: string = stack.pop() as string;\n    const parents: any = roleParents ? roleParents[cur] : undefined;\n    if (Array.isArray(parents)) { for (const p of parents) { if (!seen.has(p)) { seen.add(p); stack.push(p); } } }\n  }\n  return seen;\n}\n"}}