{"version":3,"sources":["../src/index.ts","../src/entity-configuration.ts"],"sourcesContent":["/** @oidfed/leaf — Leaf entity: EC serving and trust chain discovery. */\n\nexport type { DiscoveryResult } from \"@oidfed/core\";\nexport {\n\tLeaf,\n\ttype LeafConfig,\n} from \"./entity-configuration.js\";\n","/** Leaf entity class: Entity Configuration serving with caching and trust chain discovery. */\nimport {\n\tbuildEntityConfigurationPayload,\n\tDEFAULT_ENTITY_STATEMENT_TTL_SECONDS,\n\tdiscoverEntity as discoverEntityThroughTrustChain,\n\ttype EntityContext,\n\ttype EntityId,\n\ttype EntityRole,\n\ttype EntityStatementMetadata,\n\tentityId,\n\terrorResponse,\n\ttype FederationKeyProvider,\n\ttype FederationOptions,\n\tisValidEntityId,\n\tJwtTyp,\n\tjwtResponse,\n\tMediaType,\n\tnowSeconds,\n\trequireMethod,\n\tsignEntityStatement,\n\ttype TrustAnchorSet,\n\ttype TrustMarkRef,\n\tvalidateFederationKeySet,\n} from \"@oidfed/core\";\n\nexport interface LeafConfig {\n\tentityId: EntityId | string;\n\tauthorityHints: readonly (EntityId | string)[];\n\tmetadata: EntityStatementMetadata;\n\tkeyProvider: FederationKeyProvider;\n\troles?: EntityRole[];\n\toptions?: FederationOptions;\n\ttrustMarks?: TrustMarkRef[];\n\ttrustAnchorHints?: readonly (EntityId | string)[];\n\tentityConfigurationTtlSeconds?: number;\n\ttrustAnchors?: TrustAnchorSet;\n}\n\nfunction cloneMetadata(metadata: EntityStatementMetadata): Record<string, Record<string, unknown>> {\n\tconst cloned: Record<string, Record<string, unknown>> = {};\n\tfor (const [entityType, entityMetadata] of Object.entries(metadata)) {\n\t\tcloned[entityType] = { ...entityMetadata };\n\t}\n\treturn cloned;\n}\n\nexport class Leaf {\n\tstatic discoverEntity(\n\t\ttargetEntityId: EntityId | string,\n\t\ttrustAnchors: TrustAnchorSet,\n\t\toptions?: FederationOptions,\n\t) {\n\t\treturn discoverEntityThroughTrustChain(entityId(targetEntityId), trustAnchors, options);\n\t}\n\n\tpublic readonly entityId: EntityId;\n\tprivate readonly routes = new Map<string, (request: Request) => Promise<Response>>();\n\tprivate readonly config: LeafConfig;\n\tprivate readonly metadata: Record<string, Record<string, unknown>>;\n\n\tprivate cachedJwt: string | null = null;\n\tprivate cachedExp: number | null = null;\n\tprivate inflight: Promise<string> | null = null;\n\n\tconstructor(config: LeafConfig) {\n\t\tconst rawEntityId = (\n\t\t\tconfig.entityId.endsWith(\"/\") ? config.entityId.slice(0, -1) : config.entityId\n\t\t) as EntityId;\n\n\t\tif (!isValidEntityId(rawEntityId)) {\n\t\t\tthrow new Error(\"entityId MUST be a valid HTTPS URL without query or fragment\");\n\t\t}\n\t\tthis.entityId = rawEntityId;\n\t\tthis.config = config;\n\n\t\tif (!config.keyProvider) {\n\t\t\tthrow new Error(\"keyProvider MUST be provided\");\n\t\t}\n\n\t\tif (!config.authorityHints || config.authorityHints.length === 0) {\n\t\t\tthrow new Error(\"authorityHints MUST NOT be empty for leaf entities\");\n\t\t}\n\t\tfor (const hint of config.authorityHints) {\n\t\t\tif (!isValidEntityId(hint)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`authorityHint '${hint}' is not a valid Entity Identifier — MUST be HTTPS URL without query or fragment`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tif (config.trustAnchorHints !== undefined && config.trustAnchorHints.length === 0) {\n\t\t\tthrow new Error(\"trustAnchorHints MUST NOT be empty when provided\");\n\t\t}\n\t\tfor (const hint of config.trustAnchorHints ?? []) {\n\t\t\tif (!isValidEntityId(hint)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`trustAnchorHint '${hint}' is not a valid Entity Identifier — MUST be HTTPS URL without query or fragment`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Initialize roles & merge metadata/routes\n\t\tconst metadata = cloneMetadata(config.metadata);\n\t\tconst context: EntityContext = {\n\t\t\tentityId: this.entityId,\n\t\t\tkeyProvider: config.keyProvider,\n\t\t\toptions: config.options,\n\t\t\t...(config.trustAnchors ? { trustAnchors: config.trustAnchors } : {}),\n\t\t\tauthorityHints: config.authorityHints as readonly EntityId[],\n\t\t};\n\n\t\tif (config.roles) {\n\t\t\tfor (const role of config.roles) {\n\t\t\t\tif (role.initialize) {\n\t\t\t\t\trole.initialize(context);\n\t\t\t\t}\n\t\t\t\tmetadata[role.type] = {\n\t\t\t\t\t...(metadata[role.type] ?? {}),\n\t\t\t\t\t...role.metadata,\n\t\t\t\t};\n\t\t\t\tif (role.routes) {\n\t\t\t\t\tfor (const [path, handler] of role.routes.entries()) {\n\t\t\t\t\t\tconst resolvedPath = path.startsWith(\"http\") ? new URL(path).pathname : path;\n\t\t\t\t\t\tthis.routes.set(resolvedPath, handler);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthis.metadata = metadata;\n\n\t\tif (Object.keys(this.metadata).length === 0) {\n\t\t\tthrow new Error(\"metadata MUST contain at least one Entity Type Identifier\");\n\t\t}\n\n\t\tconst fedEntity = this.metadata.federation_entity;\n\t\tif (fedEntity) {\n\t\t\tif (\"federation_fetch_endpoint\" in fedEntity) {\n\t\t\t\tthrow new Error(\"Leaf entities MUST NOT publish federation_fetch_endpoint\");\n\t\t\t}\n\t\t\tif (\"federation_list_endpoint\" in fedEntity) {\n\t\t\t\tthrow new Error(\"Leaf entities MUST NOT publish federation_list_endpoint\");\n\t\t\t}\n\t\t}\n\n\t\tconst ttlSeconds = config.entityConfigurationTtlSeconds ?? DEFAULT_ENTITY_STATEMENT_TTL_SECONDS;\n\t\tif (ttlSeconds <= 0) {\n\t\t\tthrow new Error(\"entityConfigurationTtlSeconds must be positive\");\n\t\t}\n\t}\n\n\tprivate async buildEntityConfiguration(): Promise<string> {\n\t\tconst keySet = await this.config.keyProvider.getFederationKeySet();\n\t\tvalidateFederationKeySet(keySet);\n\t\tconst now = nowSeconds(this.config.options?.clock);\n\t\tconst ttlSeconds =\n\t\t\tthis.config.entityConfigurationTtlSeconds ?? DEFAULT_ENTITY_STATEMENT_TTL_SECONDS;\n\t\tconst payload = buildEntityConfigurationPayload({\n\t\t\tentityId: this.entityId,\n\t\t\tjwks: keySet.jwks,\n\t\t\tmetadata: this.metadata,\n\t\t\tauthorityHints: this.config.authorityHints,\n\t\t\t...(this.config.trustAnchorHints ? { trustAnchorHints: this.config.trustAnchorHints } : {}),\n\t\t\t...(this.config.trustMarks ? { trustMarks: this.config.trustMarks } : {}),\n\t\t\tissuedAt: now,\n\t\t\tttlSeconds,\n\t\t});\n\n\t\tconst jwt = await signEntityStatement(payload, keySet.signer, {\n\t\t\ttyp: JwtTyp.EntityStatement,\n\t\t});\n\n\t\tthis.cachedJwt = jwt;\n\t\tthis.cachedExp = payload.exp;\n\t\treturn jwt;\n\t}\n\n\tasync getEntityConfiguration(): Promise<string> {\n\t\tif (this.cachedJwt && !this.isEntityConfigurationExpired()) {\n\t\t\treturn this.cachedJwt;\n\t\t}\n\t\tif (this.inflight) return this.inflight;\n\t\tthis.inflight = this.buildEntityConfiguration().finally(() => {\n\t\t\tthis.inflight = null;\n\t\t});\n\t\treturn this.inflight;\n\t}\n\n\tisEntityConfigurationExpired(): boolean {\n\t\tif (this.cachedExp === null) return true;\n\t\tconst now = nowSeconds(this.config.options?.clock);\n\t\treturn now >= this.cachedExp;\n\t}\n\n\tasync refreshEntityConfiguration(): Promise<string> {\n\t\tthis.inflight = null;\n\t\treturn this.buildEntityConfiguration();\n\t}\n\n\tasync handleRequest(request: Request): Promise<Response> {\n\t\tconst url = new URL(request.url);\n\t\tconst pathname = url.pathname;\n\n\t\t// 1. Check role-specific routes first\n\t\tconst roleHandler = this.routes.get(pathname);\n\t\tif (roleHandler) {\n\t\t\treturn roleHandler(request);\n\t\t}\n\n\t\t// 2. Check standard well-known route\n\t\tconst basePath = new URL(this.entityId).pathname.replace(/\\/$/, \"\");\n\t\tconst wellKnownPath = `${basePath}/.well-known/openid-federation`;\n\t\tif (pathname === wellKnownPath) {\n\t\t\tconst methodError = requireMethod(request, \"GET\");\n\t\t\tif (methodError) return methodError;\n\n\t\t\ttry {\n\t\t\t\tconst jwt = await this.getEntityConfiguration();\n\t\t\t\treturn jwtResponse(jwt, MediaType.EntityStatement);\n\t\t\t} catch (error) {\n\t\t\t\tthis.config.options?.logger?.error(\"Failed to serve entity configuration\", { error });\n\t\t\t\treturn errorResponse(500, \"server_error\", \"An internal error occurred\");\n\t\t\t}\n\t\t}\n\n\t\treturn errorResponse(404, \"not_found\", \"Unknown endpoint\");\n\t}\n}\n"],"mappings":"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","names":["discoverEntityThroughTrustChain"]}