{"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 {asciiEq, expandRoles, grantEffectIsAllow} from './helpers.ts';\n\nexport function run(payload: any, policy: any): any {\n  const make = (allowed: boolean, reason: string, matched: number[]): any => ({allowed: allowed, reason: reason, matched: matched.slice(), audit: (!allowed && policy.audit_denials) ? [reason] : []});\n  const subject: any = (payload && payload.subject) ? payload.subject : {};\n  const resource: any = (payload && payload.resource) ? payload.resource : {};\n  const now: any = payload ? payload.now : 0;\n  const issuedAt: any = payload ? payload.issued_at : 0;\n  const requestedAction: any = payload ? payload.action : '';\n  if (subject.tenant !== resource.tenant) return make(false, 'tenant', []);\n  if (now - issuedAt > policy.token_ttl) return make(false, 'token', []);\n  if (policy.suspended_denies && subject.suspended) return make(false, 'suspended', []);\n  if (policy.owner_bypass && subject.owner) return make(true, 'owner', []);\n  let roles: Set<string> = new Set<string>();\n  const baseRoles: any[] = Array.isArray(subject.roles) ? subject.roles : [];\n  for (const r of baseRoles) roles.add(r);\n  if (policy.group_union) {\n    const groups: any[] = Array.isArray(subject.groups) ? subject.groups : [];\n    for (const g of groups) {\n      if (typeof g === 'string') roles.add(g);\n      else if (g && Array.isArray(g.roles)) { for (const r of g.roles) roles.add(r); }\n      else if (g && typeof g.role === 'string') roles.add(g.role);\n      else if (g && typeof g.name === 'string') roles.add(g.name);\n      else if (g && typeof g.id === 'string') roles.add(g.id);\n    }\n  }\n  if (policy.inherit_roles) roles = expandRoles(roles, (payload && payload.role_parents) ? payload.role_parents : {});\n  const grants: any[] = (payload && Array.isArray(payload.grants)) ? payload.grants : [];\n  const matched: number[] = [];\n  let hasAllow: boolean = false;\n  let hasDeny: boolean = false;\n  for (let i = 0; i < grants.length; i++) {\n    const grant: any = grants[i] || {};\n    const grantRoles: any[] = Array.isArray(grant.role) ? grant.role : (Array.isArray(grant.roles) ? grant.roles : [grant.role]);\n    let roleOk: boolean = false;\n    for (const gr of grantRoles) if (roles.has(gr)) roleOk = true;\n    if (!roleOk) continue;\n    const grantAction: any = grant.action;\n    let actionOk: boolean = false;\n    if (typeof grantAction === 'string' && typeof requestedAction === 'string') {\n      if (asciiEq(grantAction, requestedAction, !!policy.action_casefold)) actionOk = true;\n      else if (policy.wildcard_action && grantAction === '*') actionOk = true;\n    }\n    if (!actionOk) continue;\n    const scope: any = grant.scope !== undefined ? grant.scope : (grant.resource !== undefined ? grant.resource : (grant.resource_id !== undefined ? grant.resource_id : grant.scope_id));\n    let scopeOk: boolean = false;\n    if (scope === undefined || scope === null) scopeOk = true;\n    else if (policy.resource_scope === 'prefix') scopeOk = typeof resource.id === 'string' && typeof scope === 'string' && resource.id.startsWith(scope);\n    else scopeOk = scope === resource.id;\n    if (!scopeOk) continue;\n    const expires: any = grant.expires !== undefined ? grant.expires : (grant.expires_at !== undefined ? grant.expires_at : null);\n    let expiryOk: boolean = true;\n    if (expires !== null && expires !== undefined) expiryOk = policy.expiry_inclusive ? (now <= expires) : (now < expires);\n    if (!expiryOk) continue;\n    matched.push(i);\n    if (grantEffectIsAllow(grant)) hasAllow = true; else hasDeny = true;\n  }\n  if (matched.length === 0) return make(!!policy.default_allow, 'default', []);\n  const allowed: boolean = policy.deny_wins ? (hasAllow && !hasDeny) : hasAllow;\n  return make(allowed, 'grants', matched);\n}\n", "policy.ts": "export const POLICY: any = {\"deny_wins\": false, \"expiry_inclusive\": false, \"inherit_roles\": false, \"owner_bypass\": false, \"action_casefold\": false, \"group_union\": false, \"resource_scope\": \"prefix\", \"suspended_denies\": false, \"token_ttl\": 200, \"wildcard_action\": false, \"audit_denials\": false, \"default_allow\": false};\n", "helpers.ts": "export function asciiLower(s: string): string { let out: string = ''; for (let i = 0; i < s.length; i++) { const c: number = s.charCodeAt(i); out += (c >= 65 && c <= 90) ? String.fromCharCode(c + 32) : s[i]; } return out; }\nexport function asciiEq(a: string, b: string, casefold: boolean): boolean { return casefold ? asciiLower(a) === asciiLower(b) : a === b; }\nexport function expandRoles(initial: Set<string>, roleParents: any): Set<string> { const result: Set<string> = new Set<string>(initial); const queue: string[] = Array.from(initial); let idx: number = 0; while (idx < queue.length) { const role: string = queue[idx++]; const parents: any = roleParents ? roleParents[role] : undefined; if (Array.isArray(parents)) { for (const p of parents) { if (!result.has(p)) { result.add(p); queue.push(p); } } } } return result; }\nexport function grantEffectIsAllow(grant: any): boolean { if (!grant) return true; const raw: any = grant.effect !== undefined ? grant.effect : (grant.decision !== undefined ? grant.decision : (grant.type !== undefined ? grant.type : grant.kind)); if (typeof raw === 'string') { const v: string = asciiLower(raw); if (v === 'deny' || v === 'denied' || v === 'false') return false; return true; } if (typeof grant.allow === 'boolean') return grant.allow; if (typeof grant.deny === 'boolean') return !grant.deny; if (typeof grant.allowed === 'boolean') return grant.allowed; return true; }\n"}}