{"version":3,"file":"index.cjs","names":["iamBuildPermissionKey","iamSplitPermissionKey"],"sources":["../../../src/client/vanilla/index.ts"],"sourcesContent":["/**\n * Framework-agnostic client-side access control.\n *\n * Use when you don't use React/Vue, or for Web Components,\n * Svelte, Solid, Angular, or vanilla JS.\n *\n * Usage:\n *\n *   import { IamAccessClient } from \"duck-iam/client/vanilla\";\n *\n *   // Initialize from server-provided permissions\n *   const access = new IamAccessClient(permissionsFromServer);\n *\n *   // Check\n *   access.can(\"delete\", \"post\");                    // boolean\n *   access.can(\"manage\", \"user\", undefined, \"admin\"); // scoped check\n *   access.cannot(\"manage\", \"billing\");               // boolean\n *\n *   // With change listener (for reactive frameworks)\n *   access.subscribe((perms) => { rerender(); });\n *   access.update(newPermissions);\n *\n *   // Or fetch from server\n *   const access = await IamAccessClient.fromServer(\"/api/permissions\", {\n *     headers: { Authorization: \"Bearer ...\" },\n *   });\n */\n\nimport type { IamClient } from '../../core/types'\nimport { iamBuildPermissionKey, iamSplitPermissionKey } from '../../shared/keys'\n\n/** Callback invoked when permissions are updated via {@link IamAccessClient.update} or {@link IamAccessClient.merge}. */\ntype Listener<TAction extends string = string, TResource extends string = string, TScope extends string = string> = (\n  permissions: IamClient.PermissionMap<TAction, TResource, TScope>,\n) => void\n\n/**\n * Provides framework-agnostic client-side access control.\n *\n * Wraps a {@link IamClient.PermissionMap} (typically fetched from the server) and\n * exposes `.can()` / `.cannot()` checks. Supports reactive updates via\n * `.subscribe()`.\n *\n * @template TAction - Constrains valid action strings.\n * @template TResource - Constrains valid resource strings.\n * @template TScope - Constrains valid scope strings.\n * @example\n * ```ts\n * const access = new IamAccessClient(permissionsFromServer)\n * if (access.can('delete', 'post')) deleteIt()\n * const unsub = access.subscribe(() => rerender())\n * ```\n */\nexport class IamAccessClient<\n  TAction extends string = string,\n  TResource extends string = string,\n  TScope extends string = string,\n> {\n  private _permissions: IamClient.PermissionMap<TAction, TResource, TScope>\n  private _listeners = new Set<Listener<TAction, TResource, TScope>>()\n\n  /**\n   * Creates a new client wrapping the given permission map.\n   *\n   * @param permissions - Optional initial permission map (set later via `update`).\n   */\n  constructor(permissions?: IamClient.PermissionMap<TAction, TResource, TScope>) {\n    this._permissions = permissions ?? ({} as IamClient.PermissionMap<TAction, TResource, TScope>)\n  }\n\n  /**\n   * Fetches a permission map from `url` and returns a populated client.\n   *\n   * @template TA - Constrains valid action strings.\n   * @template TR - Constrains valid resource strings.\n   * @template TS - Constrains valid scope strings.\n   * @param url - Specifies the endpoint that returns a JSON permission map.\n   * @param init - Optional `fetch` init (auth headers, signal, etc.).\n   * @returns A populated {@link IamAccessClient}.\n   * @throws Error when the response status is non-2xx.\n   */\n  static async fromServer<TA extends string = string, TR extends string = string, TS extends string = string>(\n    url: string,\n    init?: RequestInit,\n  ): Promise<IamAccessClient<TA, TR, TS>> {\n    const res = await fetch(url, {\n      ...init,\n      headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}) },\n    })\n    if (!res.ok) throw new Error(`Failed to fetch permissions: ${res.status}`)\n    const perms: IamClient.PermissionMap<TA, TR, TS> = await res.json()\n    return new IamAccessClient<TA, TR, TS>(perms)\n  }\n\n  /**\n   * Returns a readonly view of the current permission map.\n   *\n   * @returns Readonly map of action/resource keys to boolean grants.\n   */\n  get permissions(): Readonly<IamClient.PermissionMap<TAction, TResource, TScope>> {\n    return this._permissions\n  }\n\n  /**\n   * Returns whether the action is granted on the resource.\n   *\n   * @param action - Specifies the action being checked.\n   * @param resource - Specifies the resource type.\n   * @param resourceId - Optional resource instance ID.\n   * @param scope - Optional scope binding the check.\n   * @returns `true` when the permission map grants the combination.\n   */\n  can(action: TAction, resource: TResource, resourceId?: string, scope?: TScope): boolean {\n    const key = iamBuildPermissionKey(action, resource, resourceId, scope)\n    return (this._permissions as Record<string, boolean>)[key] ?? false\n  }\n\n  /**\n   * Returns whether the action is denied on the resource.\n   *\n   * @param action - Specifies the action being checked.\n   * @param resource - Specifies the resource type.\n   * @param resourceId - Optional resource instance ID.\n   * @param scope - Optional scope binding the check.\n   * @returns `true` when the permission map does not grant the combination.\n   */\n  cannot(action: TAction, resource: TResource, resourceId?: string, scope?: TScope): boolean {\n    return !this.can(action, resource, resourceId, scope)\n  }\n\n  /**\n   * Replaces the current permission map and notifies subscribers.\n   *\n   * Listener errors are caught so one failing handler cannot block others.\n   *\n   * @param permissions - Provides the new permission map.\n   * @returns Nothing.\n   */\n  update(permissions: IamClient.PermissionMap<TAction, TResource, TScope>): void {\n    this._permissions = permissions\n    for (const fn of this._listeners) {\n      try {\n        fn(permissions)\n      } catch (err) {\n        // Surface the throw without aborting other listeners.\n        // eslint-disable-next-line no-console\n        console.error('[@gentleduck/iam:client] listener threw - continuing to notify others', err)\n      }\n    }\n  }\n\n  /**\n   * Shallow-merges the given map into the current permissions and notifies subscribers.\n   *\n   * @param permissions - Provides the partial permission patch.\n   * @returns Nothing.\n   */\n  merge(permissions: IamClient.PermissionMap<TAction, TResource, TScope>): void {\n    this.update({ ...this._permissions, ...permissions })\n  }\n\n  /**\n   * Registers a listener to run on every permission change.\n   *\n   * @param fn - Listener invoked with the new permission map.\n   * @returns An unsubscribe function.\n   */\n  subscribe(fn: Listener<TAction, TResource, TScope>): () => void {\n    this._listeners.add(fn)\n    return () => this._listeners.delete(fn)\n  }\n\n  /**\n   * Lists every action allowed against the given resource type.\n   *\n   * Handles all key formats produced by `iamBuildPermissionKey`:\n   * `action:resource`, `action:resource:resourceId`, `scope:action:resource`,\n   * and `scope:action:resource:resourceId`.\n   *\n   * @param resource - Specifies the resource type to filter by.\n   * @returns Deduplicated array of actions allowed on `resource`.\n   */\n  allowedActions(resource: TResource): TAction[] {\n    const actions: TAction[] = []\n    for (const [key, allowed] of Object.entries(this._permissions)) {\n      if (!allowed) continue\n      const action = extractAction(key, resource)\n      if (action) actions.push(action as TAction)\n    }\n    return [...new Set(actions)]\n  }\n\n  /**\n   * Returns whether at least one action is allowed on the resource.\n   *\n   * @param resource - Specifies the resource type to probe.\n   * @returns `true` when any granted key targets the resource.\n   */\n  hasAnyOn(resource: TResource): boolean {\n    return Object.entries(this._permissions).some(([key, allowed]) => {\n      if (!allowed) return false\n      return extractAction(key, resource) !== null\n    })\n  }\n}\n\n/**\n * Extract the action from a permission key for a given resource.\n *\n * Key formats (from iamBuildPermissionKey):\n *   \"action:resource\"\n *   \"action:resource:resourceId\"\n *   \"scope:action:resource\"\n *   \"scope:action:resource:resourceId\"\n *\n * Rather than guessing the format from part count (ambiguous for 3 parts),\n * we check if the resource appears at the expected position for each format.\n */\nfunction extractAction(key: string, resource: string): string | null {\n  // iamSplitPermissionKey honours `\\:` / `\\\\` escapes; naive split mis-tokenises.\n  const parts = iamSplitPermissionKey(key)\n\n  switch (parts.length) {\n    case 2:\n      // action:resource\n      if (parts[1] === resource) return parts[0] as string\n      return null\n    case 3:\n      // Could be action:resource:resourceId OR scope:action:resource.\n      // Check both: resource at index 1 (unscoped) or index 2 (scoped).\n      if (parts[1] === resource) return parts[0] as string\n      if (parts[2] === resource) return parts[1] as string\n      return null\n    case 4:\n      // scope:action:resource:resourceId\n      if (parts[2] === resource) return parts[1] as string\n      return null\n    default:\n      return null\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqDA,IAAa,kBAAb,MAAa,gBAIX;CACA,AAAQ;CACR,AAAQ,6BAAa,IAAI,IAA0C;;;;;;CAOnE,YAAY,aAAmE;EAC7E,KAAK,eAAe,eAAgB,CAAC;CACvC;;;;;;;;;;;;CAaA,aAAa,WACX,KACA,MACsC;EACtC,MAAM,MAAM,MAAM,MAAM,KAAK;GAC3B,GAAG;GACH,SAAS;IAAE,gBAAgB;IAAoB,GAAI,MAAM,WAAW,CAAC;GAAG;EAC1E,CAAC;EACD,IAAI,CAAC,IAAI,IAAI,MAAM,IAAI,MAAM,gCAAgC,IAAI,QAAQ;EAEzE,OAAO,IAAI,gBAA4B,MADkB,IAAI,KAAK,CACtB;CAC9C;;;;;;CAOA,IAAI,cAA6E;EAC/E,OAAO,KAAK;CACd;;;;;;;;;;CAWA,IAAI,QAAiB,UAAqB,YAAqB,OAAyB;EACtF,MAAM,MAAMA,mCAAsB,QAAQ,UAAU,YAAY,KAAK;EACrE,OAAQ,KAAK,aAAyC,QAAQ;CAChE;;;;;;;;;;CAWA,OAAO,QAAiB,UAAqB,YAAqB,OAAyB;EACzF,OAAO,CAAC,KAAK,IAAI,QAAQ,UAAU,YAAY,KAAK;CACtD;;;;;;;;;CAUA,OAAO,aAAwE;EAC7E,KAAK,eAAe;EACpB,KAAK,MAAM,MAAM,KAAK,YACpB,IAAI;GACF,GAAG,WAAW;EAChB,SAAS,KAAK;GAGZ,QAAQ,MAAM,yEAAyE,GAAG;EAC5F;CAEJ;;;;;;;CAQA,MAAM,aAAwE;EAC5E,KAAK,OAAO;GAAE,GAAG,KAAK;GAAc,GAAG;EAAY,CAAC;CACtD;;;;;;;CAQA,UAAU,IAAsD;EAC9D,KAAK,WAAW,IAAI,EAAE;EACtB,aAAa,KAAK,WAAW,OAAO,EAAE;CACxC;;;;;;;;;;;CAYA,eAAe,UAAgC;EAC7C,MAAM,UAAqB,CAAC;EAC5B,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,KAAK,YAAY,GAAG;GAC9D,IAAI,CAAC,SAAS;GACd,MAAM,SAAS,cAAc,KAAK,QAAQ;GAC1C,IAAI,QAAQ,QAAQ,KAAK,MAAiB;EAC5C;EACA,OAAO,CAAC,GAAG,IAAI,IAAI,OAAO,CAAC;CAC7B;;;;;;;CAQA,SAAS,UAA8B;EACrC,OAAO,OAAO,QAAQ,KAAK,YAAY,CAAC,CAAC,MAAM,CAAC,KAAK,aAAa;GAChE,IAAI,CAAC,SAAS,OAAO;GACrB,OAAO,cAAc,KAAK,QAAQ,MAAM;EAC1C,CAAC;CACH;AACF;;;;;;;;;;;;;AAcA,SAAS,cAAc,KAAa,UAAiC;CAEnE,MAAM,QAAQC,mCAAsB,GAAG;CAEvC,QAAQ,MAAM,QAAd;EACE,KAAK;GAEH,IAAI,MAAM,OAAO,UAAU,OAAO,MAAM;GACxC,OAAO;EACT,KAAK;GAGH,IAAI,MAAM,OAAO,UAAU,OAAO,MAAM;GACxC,IAAI,MAAM,OAAO,UAAU,OAAO,MAAM;GACxC,OAAO;EACT,KAAK;GAEH,IAAI,MAAM,OAAO,UAAU,OAAO,MAAM;GACxC,OAAO;EACT,SACE,OAAO;CACX;AACF"}