harn-stdlib 0.8.121

Embedded Harn standard library source catalog
Documentation
/**
 * std/oauth/token_exchange — RFC 8693 constants, provider capability rows,
 * and actor-claim helpers.
 *
 * Capability rows are data: callers can replace or extend the shipped rows
 * with provider `token_exchange` overrides or by passing overlay rows to
 * `token_exchange_catalog(...)`.
 */
import { token_exchange_capability_rows } from "std/oauth/token_exchange_catalog"

let __GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"

let __TOKEN_TYPE_PREFIX = "urn:ietf:params:oauth:token-type:"

type OAuthTokenExchangeCapability = {
  id: string,
  label?: string,
  supported: bool,
  token_url?: string,
  subject_token_types: list<string>,
  actor_token_types: list<string>,
  requested_token_types: list<string>,
  issued_token_types: list<string>,
  delegation: bool,
  impersonation: bool,
  provider_metadata_fields: list<string>,
  tracking?: dict,
  notes: list<string>,
}

fn __normalize_token_type(name) {
  let raw = trim(to_string(name ?? ""))
  if raw == "" {
    throw "std/oauth/token_exchange: token type is required"
  }
  if starts_with(raw, "urn:") {
    return raw
  }
  let key = lowercase(raw)
  if key == "access" {
    return __TOKEN_TYPE_PREFIX + "access_token"
  }
  if key == "id-jag" || key == "id_jag" {
    return __TOKEN_TYPE_PREFIX + "id-jag"
  }
  if key == "txn" || key == "txn_token" {
    return __TOKEN_TYPE_PREFIX + "txn_token"
  }
  if key == "self_signed" || key == "unsigned_json" {
    return __TOKEN_TYPE_PREFIX + key
  }
  if key == "access_token"
    || key == "refresh_token"
    || key == "id_token"
    || key == "jwt"
    || key == "saml1"
    || key == "saml2" {
    return __TOKEN_TYPE_PREFIX + key
  }
  throw "std/oauth/token_exchange: unsupported token type `" + raw + "`"
}

fn __normalize_token_types(values) {
  var out = []
  for value in values ?? [] {
    out = out + [__normalize_token_type(value)]
  }
  return out
}

fn __default_rows() {
  return token_exchange_capability_rows()
}

fn __normalize_row(raw) {
  if type_of(raw) != "dict" {
    throw "std/oauth/token_exchange: capability row must be a dict"
  }
  let id = trim(to_string(raw?.id ?? raw?.provider ?? raw?.authorization_server ?? ""))
  if id == "" {
    throw "std/oauth/token_exchange: capability row id is required"
  }
  return {
    id: id,
    label: raw?.label,
    supported: raw?.supported ?? true,
    token_url: raw?.token_url,
    subject_token_types: __normalize_token_types(raw?.subject_token_types ?? []),
    actor_token_types: __normalize_token_types(raw?.actor_token_types ?? []),
    requested_token_types: __normalize_token_types(raw?.requested_token_types ?? []),
    issued_token_types: __normalize_token_types(raw?.issued_token_types ?? raw?.requested_token_types ?? []),
    delegation: raw?.delegation ?? true,
    impersonation: raw?.impersonation ?? true,
    provider_metadata_fields: raw?.provider_metadata_fields ?? [],
    tracking: raw?.tracking,
    notes: raw?.notes ?? [],
  }
}

fn __catalog_from_rows(rows) {
  var out = {}
  for raw in rows ?? [] {
    let row = __normalize_row(raw)
    out = out + {[row.id]: row}
  }
  return out
}

fn __overlay_rows(overlays) {
  if overlays == nil {
    return []
  }
  if type_of(overlays) == "list" {
    return overlays
  }
  if type_of(overlays) == "dict" {
    var rows = []
    for id in overlays.keys() {
      let value = overlays[id]
      if type_of(value) != "dict" {
        throw "std/oauth/token_exchange: overlay `" + to_string(id) + "` must be a dict"
      }
      rows = rows + [value + {id: value?.id ?? id}]
    }
    return rows
  }
  throw "std/oauth/token_exchange: overlays must be a list or dict"
}

fn __provider_id(provider_or_id) {
  if type_of(provider_or_id) == "dict" {
    return trim(to_string(provider_or_id?.id ?? ""))
  }
  return trim(to_string(provider_or_id ?? ""))
}

/**
 * token_exchange_grant_type returns the RFC 8693 extension grant URI.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 */
pub fn token_exchange_grant_type() -> string {
  return __GRANT_TYPE
}

/**
 * token_type normalizes RFC 8693 token-type aliases to their URI values.
 *
 * @effects: []
 * @errors: [invalid_argument]
 * @api_stability: experimental
 * @example: token_type("access_token")
 */
pub fn token_type(name: string) -> string {
  return __normalize_token_type(name)
}

/**
 * token_exchange_catalog returns shipped capability rows plus overlays keyed by id.
 *
 * @effects: []
 * @errors: [invalid_argument]
 * @api_stability: experimental
 */
pub fn token_exchange_catalog(overlays = nil) -> dict {
  return __catalog_from_rows(__default_rows() + __overlay_rows(overlays))
}

/**
 * token_exchange_capability returns the effective row for a provider id or record.
 *
 * A provider record's `token_exchange` field wins over the shipped catalog so
 * package and project data can opt in without changing stdlib source.
 *
 * @effects: []
 * @errors: [invalid_argument]
 * @api_stability: experimental
 */
pub fn token_exchange_capability(provider_or_id, overlays = nil) -> OAuthTokenExchangeCapability? {
  if type_of(provider_or_id) == "dict" && provider_or_id?.token_exchange != nil {
    return __normalize_row(provider_or_id.token_exchange + {id: provider_or_id?.id})
  }
  let id = __provider_id(provider_or_id)
  if id == "" {
    return nil
  }
  return token_exchange_catalog(overlays)[id]
}

fn __actor_dict(actor) {
  if type_of(actor) == "dict" {
    let sub = trim(to_string(actor?.sub ?? ""))
    if sub == "" {
      throw "std/oauth/token_exchange: actor.sub is required"
    }
    return actor
  }
  return {sub: trim(to_string(actor ?? ""))}
}

/**
 * actor_claim builds the RFC 8693 nested `act` claim.
 *
 * Actors are ordered current-to-prior: `actor_claim([current, prior])`
 * returns `{sub: current, act: {sub: prior}}`.
 *
 * @effects: []
 * @errors: [invalid_argument]
 * @api_stability: experimental
 */
pub fn actor_claim(actors: list) -> dict {
  if len(actors) == 0 {
    throw "std/oauth/token_exchange: at least one actor is required"
  }
  var out = nil
  var index = len(actors) - 1
  while index >= 0 {
    var actor = __actor_dict(actors[index])
    if trim(to_string(actor.sub ?? "")) == "" {
      throw "std/oauth/token_exchange: actor.sub is required"
    }
    if out != nil {
      actor = actor + {act: out}
    }
    out = actor
    index = index - 1
  }
  return out ?? {}
}

/**
 * delegated_claims adds a nested RFC 8693 `act` claim to subject claims.
 *
 * @effects: []
 * @errors: [invalid_argument]
 * @api_stability: experimental
 */
pub fn delegated_claims(subject_claims: dict, actors: list) -> dict {
  if type_of(subject_claims) != "dict" {
    throw "std/oauth/token_exchange: subject_claims must be a dict"
  }
  return subject_claims + {act: actor_claim(actors)}
}