{"version":3,"file":"index.cjs","names":["nodePath","validatePolicy","validateRole","parsePolicyRowShared","parseRoleRowShared"],"sources":["../../../src/adapters/file/index.ts"],"sourcesContent":["import * as nodePath from 'node:path'\nimport type { AccessControl, IamAdapter, IamPrimitives, IamRequest } from '../../core/types'\nimport {\n  parsePolicyRow as parsePolicyRowShared,\n  parseRoleRow as parseRoleRowShared,\n  validatePolicy,\n  validateRole,\n} from '../../core/validate'\n\nexport namespace IamFile {\n  /**\n   * Describes the minimal `node:fs/promises`-compatible surface used by {@link IamFileAdapter}.\n   *\n   * Tests inject an in-memory fake; production passes the real Node module.\n   */\n  export interface IFS {\n    /**\n     * Reads a file as UTF-8 text.\n     *\n     * @param path - Absolute path to read.\n     * @param encoding - Must be `'utf8'`.\n     * @returns The file contents as a string.\n     */\n    readFile(path: string, encoding: 'utf8'): Promise<string>\n    /**\n     * Writes a file as UTF-8 text.\n     *\n     * @param path - Absolute path to write.\n     * @param data - String contents to persist.\n     * @param encoding - Must be `'utf8'`.\n     * @returns Resolves once the write completes.\n     */\n    writeFile(path: string, data: string, encoding: 'utf8'): Promise<void>\n    /**\n     * Creates a directory. **Not recursive** - the immediate parent must\n     * already exist, so a typo in `init.path` cannot silently build a deep\n     * tree.\n     *\n     * @param path - Absolute directory to create.\n     * @returns Resolves once the directory exists.\n     */\n    mkdir(path: string, options?: { recursive?: boolean }): Promise<unknown>\n    /**\n     * Optional: resolve symlinks for a path. When present (e.g. the real\n     * `node:fs/promises` provides it) the adapter uses it during the\n     * `rootDir` containment check to reject symlinks that escape the root.\n     * Test fakes typically omit this; the symlink check is skipped when\n     * unavailable.\n     *\n     * @param path - Path to canonicalise.\n     * @returns The canonical path with all symlinks resolved.\n     */\n    realpath?(path: string): Promise<string>\n  }\n\n  /** Describes initialization options for {@link IamFileAdapter}. */\n  export interface IInit<TFS extends IFS = IFS> {\n    /**\n     * Specifies the **absolute** path of the JSON store file.\n     *\n     * Rejected at construction when:\n     * - the resolved path is not absolute,\n     * - the normalized path contains a `..` segment, or\n     * - {@link rootDir} is set and the path escapes it.\n     *\n     * The adapter creates the file on first write, but **does not** recursively\n     * create directories - the immediate parent must already exist, guarding\n     * against a typo in `path` accidentally building deep paths.\n     */\n    path: string\n    /**\n     * Optional containment root. When set, {@link path} must resolve to a\n     * location inside this directory (after symlink resolution if the\n     * filesystem driver exposes `realpath`). Strongly recommended whenever\n     * any part of `path` is derived from caller-controlled input.\n     *\n     * If omitted, the adapter logs a one-shot `console.warn` at construction\n     * and accepts any absolute path.\n     */\n    rootDir?: string\n    /**\n     * Provides the filesystem driver. Pass `await import('node:fs/promises')`\n     * in Node or Bun, or any object implementing {@link IFS} for tests.\n     */\n    fs: TFS\n    /**\n     * Invoked when a stored row fails JSON parse or shape validation. The\n     * malformed row is dropped from the loaded state; the rest are returned\n     * intact. Wire this to your alerting pipeline so corrupt rows do not\n     * silently vanish from authorization decisions.\n     */\n    onPolicyError?: (err: Error, ctx: { adapter: 'file'; rowId: string }) => void\n  }\n\n  /**\n   * Describes the on-disk JSON state shape held by {@link IamFileAdapter}.\n   *\n   * Exposed for typing the internal cache field; not part of the wire API.\n   *\n   * @template TAction - Constrains valid action strings.\n   * @template TResource - Constrains valid resource strings.\n   * @template TRole - Constrains valid role strings.\n   * @template TScope - Constrains valid scope strings.\n   */\n  export interface IState<\n    TAction extends string,\n    TResource extends string,\n    TRole extends string,\n    TScope extends string,\n  > {\n    policies: Record<string, AccessControl.IPolicy<TAction, TResource, TRole>>\n    roles: Record<string, AccessControl.IRole<TAction, TResource, TRole, TScope>>\n    assignments: Record<string, Array<{ role: TRole; scope?: TScope }>>\n    attributes: Record<string, IamPrimitives.Attributes>\n  }\n}\n\n/**\n * Persists the access store as a single JSON file; single-writer model (no external locking).\n *\n * @template TAction - Constrains valid action strings.\n * @template TResource - Constrains valid resource strings.\n * @template TRole - Constrains valid role strings.\n * @template TScope - Constrains valid scope strings.\n */\n/**\n * Process-wide latch for the missing-rootDir warning. The warning text is the\n * same regardless of which adapter triggered it, and the resolved path is\n * deliberately omitted so log scrapers cannot use it as a path-existence\n * oracle.\n */\nlet _ROOTDIR_WARNED_FIRED = false\n\nexport class IamFileAdapter<\n  TAction extends string = string,\n  TResource extends string = string,\n  TRole extends string = string,\n  TScope extends string = string,\n  TFS extends IamFile.IFS = IamFile.IFS,\n> implements IamAdapter.IAdapter<TAction, TResource, TRole, TScope>\n{\n  private readonly _path: string\n  private readonly _parentDir: string\n  private readonly _rootDir: string | null\n  private readonly _fs: TFS\n  private readonly _onPolicyError?: (err: Error, ctx: { adapter: 'file'; rowId: string }) => void\n  private _cache: IamFile.IState<TAction, TResource, TRole, TScope> | null = null\n  private _loadInFlight: Promise<IamFile.IState<TAction, TResource, TRole, TScope>> | null = null\n  // realpath is re-checked on every I/O so an attacker cannot swap the file\n  // for a symlink after first read and redirect subsequent writes.\n\n  /**\n   * Create the adapter; synchronously validates `init.path` for absoluteness, `..`, and `rootDir` containment.\n   *\n   * @param init - Provides the store path and filesystem driver.\n   */\n  constructor(init: IamFile.IInit<TFS>) {\n    // Reject raw `..`; path.resolve would silently collapse it.\n    if (init.path.split(/[\\\\/]+/).includes('..')) {\n      throw new Error(`[@gentleduck/iam:file] IamFileAdapter path contains a \"..\" segment: \"${init.path}\"`)\n    }\n    const resolved = nodePath.resolve(init.path)\n    if (!nodePath.isAbsolute(resolved)) {\n      throw new Error(`[@gentleduck/iam:file] IamFileAdapter path must resolve to an absolute path: \"${init.path}\"`)\n    }\n    // Refuse pre-resolve relative paths; path.resolve would silently join cwd.\n    if (!nodePath.isAbsolute(init.path)) {\n      throw new Error(`[@gentleduck/iam:file] IamFileAdapter path must be supplied as an absolute path: \"${init.path}\"`)\n    }\n\n    let rootDir: string | null = null\n    if (init.rootDir !== undefined) {\n      if (!nodePath.isAbsolute(init.rootDir)) {\n        throw new Error(`[@gentleduck/iam:file] IamFileAdapter rootDir must be absolute: \"${init.rootDir}\"`)\n      }\n      rootDir = nodePath.resolve(init.rootDir)\n      const rel = nodePath.relative(rootDir, resolved)\n      if (rel.startsWith('..') || nodePath.isAbsolute(rel)) {\n        throw new Error(`[@gentleduck/iam:file] IamFileAdapter path \"${resolved}\" escapes rootDir \"${rootDir}\"`)\n      }\n    } else if (!_ROOTDIR_WARNED_FIRED) {\n      // Once-per-process; do not echo the path (request-derived; log-oracle).\n      _ROOTDIR_WARNED_FIRED = true\n      // eslint-disable-next-line no-console\n      console.warn(\n        '[@gentleduck/iam:file] IamFileAdapter constructed without rootDir. ' +\n          'Any caller deriving the path from request data should set rootDir for defence in depth.',\n      )\n    }\n\n    this._path = resolved\n    this._parentDir = nodePath.dirname(resolved)\n    this._rootDir = rootDir\n    this._fs = init.fs\n    this._onPolicyError = init.onPolicyError\n  }\n\n  /**\n   * Resolves symlinks via `realpath` (when the FS driver exposes one) and\n   * re-checks containment under `_rootDir`. Runs on every read AND every\n   * write so an attacker cannot swap the file for a symlink after the first\n   * I/O and steer later writes elsewhere. Symlink check is skipped when\n   * `realpath` is unavailable (test fakes, browser bundles) - the\n   * constructor already enforced textual containment.\n   */\n  private async _assertWithinRoot(): Promise<void> {\n    if (!this._rootDir || !this._fs.realpath) return\n    // The store file itself may not exist yet (first run); fall back to the\n    // parent directory's realpath, which must exist by the time we read or\n    // write.\n    let canonical: string\n    try {\n      canonical = await this._fs.realpath(this._path)\n    } catch (err) {\n      // Non-ENOENT failures must propagate; hostile symlinks could bypass containment.\n      const code = err !== null && err !== undefined ? Reflect.get(Object(err), 'code') : undefined\n      if (code && code !== 'ENOENT') throw err\n      try {\n        const canonicalParent = await this._fs.realpath(this._parentDir)\n        canonical = nodePath.join(canonicalParent, nodePath.basename(this._path))\n      } catch (parentErr) {\n        const parentCode =\n          parentErr !== null && parentErr !== undefined ? Reflect.get(Object(parentErr), 'code') : undefined\n        if (parentCode && parentCode !== 'ENOENT') throw parentErr\n        // Parent doesn't exist either - the read path's ENOENT branch handles\n        // it; the write path will surface the missing-parent error explicitly.\n        return\n      }\n    }\n    const rel = nodePath.relative(this._rootDir, canonical)\n    if (rel.startsWith('..') || nodePath.isAbsolute(rel)) {\n      throw new Error(\n        `[@gentleduck/iam:file] IamFileAdapter realpath \"${canonical}\" escapes rootDir \"${this._rootDir}\" (symlink traversal)`,\n      )\n    }\n  }\n\n  private _reportPolicyError(err: Error, rowId: string): void {\n    if (this._onPolicyError) {\n      this._onPolicyError(err, { adapter: 'file', rowId })\n      return\n    }\n    // eslint-disable-next-line no-console\n    console.warn(`[@gentleduck/iam:file] dropped malformed row \"${rowId}\": ${err.message}`)\n  }\n\n  private async _loadState(): Promise<IamFile.IState<TAction, TResource, TRole, TScope>> {\n    if (this._cache) return this._cache\n    if (this._loadInFlight) return this._loadInFlight\n    // Clear in-flight on ANY throw (including _assertWithinRoot\n    // symlink-escape) - a stuck rejected promise would otherwise pin the\n    // adapter in a permanent failure state until process restart.\n    const pending = (async () => {\n      try {\n        await this._assertWithinRoot()\n        let raw: string\n        try {\n          raw = await this._fs.readFile(this._path, 'utf8')\n        } catch (err) {\n          // Only ENOENT is recoverable; anything else must surface.\n          const code = err !== null && err !== undefined ? Reflect.get(Object(err), 'code') : undefined\n          if (code !== 'ENOENT') {\n            throw new Error(\n              `[@gentleduck/iam:file] load failed (${code ?? 'unknown'}): ${err instanceof Error ? err.message : String(err)}`,\n            )\n          }\n          // Null-proto dicts so attacker-controlled ids (`__proto__`) cannot\n          // (a) read Object.prototype back, or (b) pollute the proto chain\n          // via setter assignment.\n          const empty: IamFile.IState<TAction, TResource, TRole, TScope> = {\n            policies: Object.create(null),\n            roles: Object.create(null),\n            assignments: Object.create(null),\n            attributes: Object.create(null),\n          }\n          this._cache = empty\n          return empty\n        }\n        let parsedRaw: unknown\n        try {\n          parsedRaw = JSON.parse(raw)\n        } catch (err) {\n          // Throw, never set _cache to {}; a later _flush would erase a recoverable file.\n          this._reportPolicyError(err instanceof Error ? err : new Error(String(err)), this._path)\n          throw new Error(\n            `[@gentleduck/iam:file] store at \"${this._path}\" is corrupt (JSON parse failed) - refusing to load; restore from backup before retrying`,\n          )\n        }\n\n        const parsed = isPlainObject(parsedRaw) ? parsedRaw : {}\n\n        // IamValidate each row; drop malformed entries instead of returning them.\n        // Null-proto so prototype-key reads/writes can't pollute the proto chain.\n        const policies: Record<string, AccessControl.IPolicy<TAction, TResource, TRole>> = Object.create(null)\n        const policiesRaw = isPlainObject(parsed.policies) ? parsed.policies : {}\n        for (const [rowId, p] of Object.entries(policiesRaw)) {\n          const policy = parsePolicyRow<TAction, TResource, TRole>(p)\n          if (policy !== null) {\n            policies[rowId] = policy\n          } else {\n            const issues = validatePolicy(p)\n              .issues.map((i) => i.message)\n              .join('; ')\n            this._reportPolicyError(new Error(`Invalid policy \"${rowId}\": ${issues}`), rowId)\n          }\n        }\n        const roles: Record<string, AccessControl.IRole<TAction, TResource, TRole, TScope>> = Object.create(null)\n        const rolesRaw = isPlainObject(parsed.roles) ? parsed.roles : {}\n        for (const [rowId, r] of Object.entries(rolesRaw)) {\n          const role = parseRoleRow<TAction, TResource, TRole, TScope>(r)\n          if (role !== null) {\n            roles[rowId] = role\n          } else {\n            const issues = validateRole(r)\n              .issues.map((i) => i.message)\n              .join('; ')\n            this._reportPolicyError(new Error(`Invalid role \"${rowId}\": ${issues}`), rowId)\n          }\n        }\n\n        const state: IamFile.IState<TAction, TResource, TRole, TScope> = {\n          policies,\n          roles,\n          assignments: parseFileAssignments<TRole, TScope>(parsed.assignments, (rowId, reason) =>\n            this._reportPolicyError(new Error(`assignments[${rowId}]: ${reason}`), rowId),\n          ),\n          attributes: parseFileAttributes(parsed.attributes, (rowId, reason) =>\n            this._reportPolicyError(new Error(`attributes[${rowId}]: ${reason}`), rowId),\n          ),\n        }\n        this._cache = state\n        return state\n      } finally {\n        // Always clear in-flight, even on throw.\n        this._loadInFlight = null\n      }\n    })()\n    this._loadInFlight = pending\n    // Catch-noop on the stored promise so an unawaited rejection elsewhere\n    // doesn't crash Node; the caller still sees the rejection via `pending`.\n    pending.catch(() => undefined)\n    return pending\n  }\n\n  private async _flush(): Promise<void> {\n    if (!this._cache) return\n    await this._assertWithinRoot()\n    // Non-recursive mkdir of the immediate parent only. If a grandparent is\n    // missing the caller's deployment is misconfigured - throwing here is\n    // safer than silently building a deep tree from a typo'd `init.path`.\n    try {\n      await this._fs.mkdir(this._parentDir)\n    } catch (err) {\n      // EEXIST is the happy path - directory already there. Anything else is\n      // a real problem and must surface to the caller.\n      const code = err !== null && err !== undefined ? Reflect.get(Object(err), 'code') : undefined\n      if (code !== 'EEXIST') {\n        throw new Error(\n          `[@gentleduck/iam:file] IamFileAdapter parent directory \"${this._parentDir}\" is not accessible (${code ?? 'unknown'}). ` +\n            'Create it explicitly; the adapter no longer does recursive mkdir.',\n        )\n      }\n    }\n    await this._fs.writeFile(this._path, JSON.stringify(this._cache, null, 2), 'utf8')\n  }\n\n  /**\n   * Lists every policy persisted on disk.\n   *\n   * @param _opts - Ignored read options accepted for interface compatibility.\n   * @returns All stored policies.\n   */\n  async listPolicies(_opts?: IamAdapter.IReadOptions): Promise<AccessControl.IPolicy<TAction, TResource, TRole>[]> {\n    const s = await this._loadState()\n    return Object.values(s.policies)\n  }\n\n  /**\n   * Fetches a single policy by ID.\n   *\n   * @param id - Identifies the policy to look up.\n   * @param _opts - Ignored read options accepted for interface compatibility.\n   * @returns The matching policy or `null` when absent.\n   */\n  async getPolicy(\n    id: string,\n    _opts?: IamAdapter.IReadOptions,\n  ): Promise<AccessControl.IPolicy<TAction, TResource, TRole> | null> {\n    const s = await this._loadState()\n    return s.policies[id] ?? null\n  }\n\n  /**\n   * Stores or overwrites a policy and flushes to disk.\n   *\n   * @param p - Provides the policy to persist.\n   * @returns Resolves once the file is rewritten.\n   */\n  async savePolicy(p: AccessControl.IPolicy<TAction, TResource, TRole>): Promise<void> {\n    const s = await this._loadState()\n    s.policies[p.id] = p\n    await this._flush()\n  }\n\n  /**\n   * Removes a policy by ID and flushes to disk.\n   *\n   * @param id - Identifies the policy to delete.\n   * @returns Resolves once the file is rewritten.\n   */\n  async deletePolicy(id: string): Promise<void> {\n    const s = await this._loadState()\n    delete s.policies[id]\n    await this._flush()\n  }\n\n  /**\n   * Lists every role persisted on disk.\n   *\n   * @param _opts - Ignored read options accepted for interface compatibility.\n   * @returns All stored roles.\n   */\n  async listRoles(_opts?: IamAdapter.IReadOptions): Promise<AccessControl.IRole<TAction, TResource, TRole, TScope>[]> {\n    const s = await this._loadState()\n    return Object.values(s.roles)\n  }\n\n  /**\n   * Fetches a single role by ID.\n   *\n   * @param id - Identifies the role to look up.\n   * @param _opts - Ignored read options accepted for interface compatibility.\n   * @returns The matching role or `null` when absent.\n   */\n  async getRole(\n    id: string,\n    _opts?: IamAdapter.IReadOptions,\n  ): Promise<AccessControl.IRole<TAction, TResource, TRole, TScope> | null> {\n    const s = await this._loadState()\n    return s.roles[id] ?? null\n  }\n\n  /**\n   * Stores or overwrites a role and flushes to disk.\n   *\n   * @param r - Provides the role to persist.\n   * @returns Resolves once the file is rewritten.\n   */\n  async saveRole(r: AccessControl.IRole<TAction, TResource, TRole, TScope>): Promise<void> {\n    const s = await this._loadState()\n    s.roles[r.id] = r\n    await this._flush()\n  }\n\n  /**\n   * Removes a role by ID and flushes to disk.\n   *\n   * @param id - Identifies the role to delete.\n   * @returns Resolves once the file is rewritten.\n   */\n  async deleteRole(id: string): Promise<void> {\n    const s = await this._loadState()\n    delete s.roles[id]\n    await this._flush()\n  }\n\n  /**\n   * Lists unscoped (global) roles assigned to a subject.\n   *\n   * @param id - Identifies the subject whose global roles are read.\n   * @param _opts - Ignored read options accepted for interface compatibility.\n   * @returns Deduplicated array of role IDs without scope.\n   */\n  async getSubjectRoles(id: string, _opts?: IamAdapter.IReadOptions): Promise<TRole[]> {\n    const s = await this._loadState()\n    const entries = s.assignments[id] ?? []\n    return [...new Set(entries.filter((e) => e.scope == null).map((e) => e.role))]\n  }\n\n  /**\n   * Lists scoped role assignments for a subject.\n   *\n   * @param id - Identifies the subject whose scoped roles are read.\n   * @param _opts - Ignored read options accepted for interface compatibility.\n   * @returns Array of `(role, scope)` pairs for scoped assignments only.\n   */\n  async getSubjectScopedRoles(\n    id: string,\n    _opts?: IamAdapter.IReadOptions,\n  ): Promise<IamRequest.IScopedRole<TRole, TScope>[]> {\n    const s = await this._loadState()\n    const hasScope = (e: { role: TRole; scope?: TScope }): e is { role: TRole; scope: TScope } => e.scope != null\n    return (s.assignments[id] ?? []).filter(hasScope).map((e) => ({ role: e.role, scope: e.scope }))\n  }\n\n  /**\n   * Grants a role to a subject, optionally restricted to a scope.\n   *\n   * Duplicate `(role, scope)` pairs are silently ignored.\n   *\n   * @param id - Identifies the subject receiving the role.\n   * @param roleId - Specifies the role being granted.\n   * @param scope - Optional scope binding the assignment.\n   * @returns Resolves once the file is rewritten.\n   */\n  async assignRole(id: string, roleId: TRole, scope?: TScope): Promise<void> {\n    const s = await this._loadState()\n    let entries = s.assignments[id]\n    if (!entries) {\n      entries = []\n      s.assignments[id] = entries\n    }\n    if (!entries.some((e) => e.role === roleId && e.scope === scope)) {\n      entries.push({ role: roleId, scope })\n    }\n    await this._flush()\n  }\n\n  /**\n   * Removes a role assignment from a subject.\n   *\n   * @param id - Identifies the subject losing the role.\n   * @param roleId - Specifies the role being revoked.\n   * @param scope - Optional scope to match; omit to revoke unscoped only.\n   * @returns Resolves once the file is rewritten.\n   */\n  async revokeRole(id: string, roleId: TRole, scope?: TScope): Promise<void> {\n    const s = await this._loadState()\n    const entries = s.assignments[id]\n    if (!entries) return\n    // scope-undefined removes ALL matching role assignments - matches the\n    // redis/drizzle/prisma contract.\n    s.assignments[id] =\n      scope === undefined\n        ? entries.filter((e) => e.role !== roleId)\n        : entries.filter((e) => !(e.role === roleId && e.scope === scope))\n    await this._flush()\n  }\n\n  /**\n   * Fetches the attribute bag stored for a subject.\n   *\n   * @param id - Identifies the subject whose attributes are read.\n   * @param _opts - Ignored read options accepted for interface compatibility.\n   * @returns The subject's attributes or `{}` when none are recorded.\n   */\n  async getSubjectAttributes(id: string, _opts?: IamAdapter.IReadOptions): Promise<IamPrimitives.Attributes> {\n    const s = await this._loadState()\n    return s.attributes[id] ?? {}\n  }\n\n  /**\n   * Shallow-merges new attributes into the subject's existing bag.\n   *\n   * @param id - Identifies the subject whose attributes are written.\n   * @param attrs - Provides the partial attribute patch to merge in.\n   * @returns Resolves once the file is rewritten.\n   */\n  async setSubjectAttributes(id: string, attrs: IamPrimitives.Attributes): Promise<void> {\n    const s = await this._loadState()\n    s.attributes[id] = Object.assign(Object.create(null), s.attributes[id] ?? {}, attrs)\n    await this._flush()\n  }\n}\n\n/**\n * Structural parser for the file adapter's `assignments` map. Malformed\n * rows are dropped and reported via the supplied error sink.\n */\nfunction parseFileAssignments<TRole extends string, TScope extends string>(\n  raw: unknown,\n  report: (rowId: string, reason: string) => void,\n): Record<string, Array<{ role: TRole; scope?: TScope }>> {\n  if (raw === undefined || raw === null) return Object.create(null)\n  if (typeof raw !== 'object' || Array.isArray(raw)) {\n    report('__root__', `expected object, got ${Array.isArray(raw) ? 'array' : typeof raw}`)\n    return Object.create(null)\n  }\n  const out: Record<string, Array<{ role: TRole; scope?: TScope }>> = Object.create(null)\n  for (const [rowId, rowVal] of Object.entries(raw)) {\n    if (!Array.isArray(rowVal)) {\n      report(rowId, `expected array of {role, scope?}, got ${rowVal === null ? 'null' : typeof rowVal}`)\n      continue\n    }\n    const entries: Array<{ role: TRole; scope?: TScope }> = []\n    let perEntryError: string | null = null\n    for (const entry of rowVal) {\n      if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {\n        perEntryError = `assignment entry not a plain object`\n        break\n      }\n      const role = Reflect.get(entry, 'role')\n      if (typeof role !== 'string' || role.length === 0) {\n        perEntryError = `assignment entry missing/non-string role`\n        break\n      }\n      const scope = Reflect.get(entry, 'scope')\n      if (scope !== undefined && (typeof scope !== 'string' || scope.length === 0)) {\n        perEntryError = `assignment entry scope must be a non-empty string when present`\n        break\n      }\n      const narrowed: { role: TRole; scope?: TScope } =\n        scope === undefined\n          ? { role: roleAs<TRole>(role) }\n          : { role: roleAs<TRole>(role), scope: scopeAs<TScope>(scope) }\n      entries.push(narrowed)\n    }\n    if (perEntryError !== null) {\n      report(rowId, perEntryError)\n      continue\n    }\n    out[rowId] = entries\n  }\n  return out\n}\n\n/**\n * Structural parser for the file adapter's `attributes` map. Malformed\n * rows are dropped and reported via the supplied error sink.\n */\nfunction parseFileAttributes(\n  raw: unknown,\n  report: (rowId: string, reason: string) => void,\n): Record<string, IamPrimitives.Attributes> {\n  if (raw === undefined || raw === null) return Object.create(null)\n  if (typeof raw !== 'object' || Array.isArray(raw)) {\n    report('__root__', `expected object, got ${Array.isArray(raw) ? 'array' : typeof raw}`)\n    return Object.create(null)\n  }\n  const out: Record<string, IamPrimitives.Attributes> = Object.create(null)\n  for (const [rowId, rowVal] of Object.entries(raw)) {\n    if (typeof rowVal !== 'object' || rowVal === null || Array.isArray(rowVal)) {\n      report(rowId, `expected attributes object, got ${rowVal === null ? 'null' : typeof rowVal}`)\n      continue\n    }\n    const attrs: IamPrimitives.Attributes = Object.create(null)\n    for (const [k, v] of Object.entries(rowVal)) {\n      attrs[k] = v as IamPrimitives.AttributeValue\n    }\n    out[rowId] = attrs\n  }\n  return out\n}\n\n/**\n * Narrowing helpers for the typed-string generics. We have already\n * runtime-validated that the value is a non-empty string; the role/scope\n * type parameters are TS-only constraints that the file adapter can't\n * verify (the legitimate string values are determined by the calling\n * app's `createIam`).\n */\nfunction roleAs<TRole extends string>(s: string): TRole {\n  return s as TRole\n}\nfunction scopeAs<TScope extends string>(s: string): TScope {\n  return s as TScope\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n  return typeof v === 'object' && v !== null && !Array.isArray(v)\n}\n\nconst parsePolicyRow = parsePolicyRowShared\nconst parseRoleRow = parseRoleRowShared\n"],"mappings":"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"}