{"version":3,"sources":["/Users/shyun/comcom/ain-enterprise/ain-adk/dist/cjs/chunk-LKJONCFI.cjs","../../src/middlewares/authz.middleware.ts"],"names":["allowed"],"mappings":"AAAA;AACE;AACF,wDAA6B;AAC7B;AACA;ACHA,oDAA4B;AAK5B,SAAS,WAAA,CAAY,IAAA,EAAsB;AAC1C,EAAA,MAAM,QAAA,EAAU,IAAA,CACd,KAAA,CAAM,GAAG,CAAA,CACT,GAAA;AAAA,IAAI,CAAC,GAAA,EAAA,GACL,GAAA,CAAI,UAAA,CAAW,GAAG,EAAA,EACf,QAAA,EACA,GAAA,CAAI,OAAA,CAAQ,qBAAA,EAAuB,MAAM;AAAA,EAC7C,CAAA,CACC,IAAA,CAAK,GAAG,CAAA;AACV,EAAA,OAAO,IAAI,MAAA,CAAO,CAAA,CAAA,EAAI,OAAO,CAAA,GAAA,CAAK,CAAA;AACnC;AAMO,SAAS,qBAAA,CACf,QAAA,EACA,MAAA,EACiB;AACjB,EAAA,MAAM,SAAA,EAAuB,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,EAAA,GAAA,CAAO;AAAA,IAC/C,GAAG,CAAA;AAAA,IACH,GAAA,EAAK,WAAA,CAAY,CAAA,CAAE,IAAI;AAAA,EACxB,CAAA,CAAE,CAAA;AAEF,EAAA,OAAO,MAAA,CAAO,GAAA,EAAc,GAAA,EAAe,IAAA,EAAA,GAAuB;AAEjE,IAAA,MAAM,SAAA,EAAW,CAAA,EAAA;AACH,IAAA;AAEH,MAAA;AAEX,IAAA;AACY,IAAA;AAKc,IAAA;AACtB,IAAA;AACO,MAAA;AACH,QAAA;AACK,QAAA;AACP,QAAA;AACQ,UAAA;AACD,QAAA;AACC,UAAA;AACZ,QAAA;AAEY,QAAA;AACb,MAAA;AAEU,MAAA;AAGH,QAAA;AACF,QAAA;AACO,UAAA;AACX,QAAA;AACI,QAAA;AACGA,UAAAA;AACL,YAAA;AACM,YAAA;AACA,YAAA;AACN,YAAA;AACD,UAAA;AACIA,UAAAA;AACL,QAAA;AAOU,QAAA;AACH,UAAA;AACF,UAAA;AACG,YAAA;AACL,cAAA;AACM,cAAA;AACA,cAAA;AACN,cAAA;AACD,YAAA;AACK,YAAA;AACE,cAAA;AACP,YAAA;AACD,UAAA;AACD,QAAA;AACY,QAAA;AACb,MAAA;AAGqC,MAAA;AAC3B,MAAA;AACI,QAAA;AACA,QAAA;AACA,UAAA;AACb,QAAA;AACQ,QAAA;AACT,MAAA;AACgB,MAAA;AACf,QAAA;AACM,QAAA;AACA,QAAA;AACN,QAAA;AACD,MAAA;AACc,MAAA;AACH,QAAA;AACX,MAAA;AACW,MAAA;AACC,MAAA;AACD,IAAA;AACE,MAAA;AACd,IAAA;AACD,EAAA;AACD;ADhCsB;AACA;AACA;AACA","file":"/Users/shyun/comcom/ain-enterprise/ain-adk/dist/cjs/chunk-LKJONCFI.cjs","sourcesContent":[null,"import type { NextFunction, Request, RequestHandler, Response } from \"express\";\nimport { StatusCodes } from \"http-status-codes\";\nimport { AinHttpError } from \"@/types/agent\";\nimport type { PermissionResolver, RouteRequirement } from \"@/types/authz\";\n\n/** Convert an express path (\"/api/document/:id\") to a matcher regex. */\nfunction pathToRegex(path: string): RegExp {\n\tconst escaped = path\n\t\t.split(\"/\")\n\t\t.map((seg) =>\n\t\t\tseg.startsWith(\":\")\n\t\t\t\t? \"[^/]+\"\n\t\t\t\t: seg.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"),\n\t\t)\n\t\t.join(\"/\");\n\treturn new RegExp(`^${escaped}/?$`);\n}\n\ninterface Compiled extends RouteRequirement {\n\t_re: RegExp;\n}\n\nexport function createAuthzMiddleware(\n\tresolver: PermissionResolver,\n\troutes: RouteRequirement[],\n): RequestHandler {\n\tconst compiled: Compiled[] = routes.map((r) => ({\n\t\t...r,\n\t\t_re: pathToRegex(r.path),\n\t}));\n\n\treturn async (req: Request, res: Response, next: NextFunction) => {\n\t\t// Full path including the mount prefix (req.path is mount-relative).\n\t\tconst fullPath = `${req.baseUrl}${req.path}`;\n\t\tconst match = compiled.find(\n\t\t\t(r) =>\n\t\t\t\tr.method.toUpperCase() === req.method.toUpperCase() &&\n\t\t\t\tr._re.test(fullPath),\n\t\t);\n\t\tif (!match) return next();\n\n\t\t// Authorization is keyed on the human principal (email/UPN) when the\n\t\t// auth layer provides it, falling back to userId. Document ownership\n\t\t// stays keyed on userId (res.locals.userId) — handled by the controller.\n\t\tconst principal: string = res.locals.email ?? res.locals.userId ?? \"\";\n\t\ttry {\n\t\t\tif (match.mode === \"list\") {\n\t\t\t\tconst filters = await resolver.listFilter(principal, match.resource);\n\t\t\t\tres.locals.authzChecked = true;\n\t\t\t\tif (filters === null) {\n\t\t\t\t\tres.locals.authzListAll = true; // unrestricted (admin)\n\t\t\t\t} else if (filters.length > 0) {\n\t\t\t\t\tres.locals.authzFilters = filters; // own ∪ these filters\n\t\t\t\t}\n\t\t\t\t// [] → leave both unset → handler returns the caller's own records\n\t\t\t\treturn next();\n\t\t\t}\n\n\t\t\tif (match.mode === \"byId\") {\n\t\t\t\t// Additive: a matching role grants cross-user access (authzChecked).\n\t\t\t\t// Otherwise we defer to the handler's own owner check — no hard deny.\n\t\t\t\tconst loaded = match.loadAttrs ? await match.loadAttrs(req) : null;\n\t\t\t\tif (loaded === null) {\n\t\t\t\t\tthrow new AinHttpError(StatusCodes.NOT_FOUND, \"Not found\");\n\t\t\t\t}\n\t\t\t\tif (loaded !== \"skip\") {\n\t\t\t\t\tconst allowed = await resolver.can(\n\t\t\t\t\t\tprincipal,\n\t\t\t\t\t\tmatch.resource,\n\t\t\t\t\t\tmatch.action,\n\t\t\t\t\t\tloaded,\n\t\t\t\t\t);\n\t\t\t\t\tif (allowed) res.locals.authzChecked = true;\n\t\t\t\t}\n\t\t\t\t// Target-state gate: on a write that also carries new attributes\n\t\t\t\t// (e.g. an update relabeling the record), require write access to the\n\t\t\t\t// *target* state too. Without this, a caller could create a personal\n\t\t\t\t// (ungoverned) record and then relabel it into a governed\n\t\t\t\t// category/scope they lack a role for — a create-then-relabel bypass.\n\t\t\t\t// bodyAttrs returns \"skip\" when the target isn't governed.\n\t\t\t\tif (match.action === \"write\" && match.bodyAttrs) {\n\t\t\t\t\tconst target = match.bodyAttrs(req);\n\t\t\t\t\tif (target !== \"skip\") {\n\t\t\t\t\t\tconst okTarget = await resolver.can(\n\t\t\t\t\t\t\tprincipal,\n\t\t\t\t\t\t\tmatch.resource,\n\t\t\t\t\t\t\tmatch.action,\n\t\t\t\t\t\t\ttarget,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (!okTarget) {\n\t\t\t\t\t\t\tthrow new AinHttpError(StatusCodes.FORBIDDEN, \"Forbidden\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn next();\n\t\t\t}\n\n\t\t\t// fromBody / gate: hard gate (403 on deny).\n\t\t\tlet attrs: Record<string, string> = {};\n\t\t\tif (match.mode === \"fromBody\") {\n\t\t\t\tconst body = match.bodyAttrs ? match.bodyAttrs(req) : {};\n\t\t\t\tif (body === \"skip\") {\n\t\t\t\t\treturn next(); // not a governed resource → handler's own checks\n\t\t\t\t}\n\t\t\t\tattrs = body;\n\t\t\t}\n\t\t\tconst allowed = await resolver.can(\n\t\t\t\tprincipal,\n\t\t\t\tmatch.resource,\n\t\t\t\tmatch.action,\n\t\t\t\tattrs,\n\t\t\t);\n\t\t\tif (!allowed) {\n\t\t\t\tthrow new AinHttpError(StatusCodes.FORBIDDEN, \"Forbidden\");\n\t\t\t}\n\t\t\tres.locals.authzChecked = true;\n\t\t\treturn next();\n\t\t} catch (e) {\n\t\t\treturn next(e);\n\t\t}\n\t};\n}\n"]}