Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions sdk-core/api/sdk-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,58 @@ public final class org/dexpace/sdk/core/http/auth/AuthChallengeParser {
public static final fun parse (Ljava/lang/String;)Ljava/util/List;
}

public final class org/dexpace/sdk/core/http/auth/AuthDescriptor {
public static final field Companion Lorg/dexpace/sdk/core/http/auth/AuthDescriptor$Companion;
public synthetic fun <init> (Ljava/util/List;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun allowsAnonymous ()Z
public fun equals (Ljava/lang/Object;)Z
public final fun getRequirements ()Ljava/util/List;
public final fun getSchemes ()Ljava/util/List;
public fun hashCode ()I
public final fun newBuilder ()Lorg/dexpace/sdk/core/http/auth/AuthDescriptor$Builder;
public static final fun of (Ljava/util/List;)Lorg/dexpace/sdk/core/http/auth/AuthDescriptor;
public static final fun ofSchemes (Ljava/util/List;)Lorg/dexpace/sdk/core/http/auth/AuthDescriptor;
public fun toString ()Ljava/lang/String;
}

public final class org/dexpace/sdk/core/http/auth/AuthDescriptor$Builder : org/dexpace/sdk/core/generics/Builder {
public fun <init> ()V
public fun <init> (Lorg/dexpace/sdk/core/http/auth/AuthDescriptor;)V
public final fun addRequirement (Lorg/dexpace/sdk/core/http/auth/AuthRequirement;)Lorg/dexpace/sdk/core/http/auth/AuthDescriptor$Builder;
public final fun addScheme (Lorg/dexpace/sdk/core/http/auth/AuthScheme;)Lorg/dexpace/sdk/core/http/auth/AuthDescriptor$Builder;
public synthetic fun build ()Ljava/lang/Object;
public fun build ()Lorg/dexpace/sdk/core/http/auth/AuthDescriptor;
public final fun requirements (Ljava/util/List;)Lorg/dexpace/sdk/core/http/auth/AuthDescriptor$Builder;
}

public final class org/dexpace/sdk/core/http/auth/AuthDescriptor$Companion {
public final fun of (Ljava/util/List;)Lorg/dexpace/sdk/core/http/auth/AuthDescriptor;
public final fun ofSchemes (Ljava/util/List;)Lorg/dexpace/sdk/core/http/auth/AuthDescriptor;
}

public final class org/dexpace/sdk/core/http/auth/AuthDescriptorResolver {
public static final field Companion Lorg/dexpace/sdk/core/http/auth/AuthDescriptorResolver$Companion;
public static final field INSTANCE Lorg/dexpace/sdk/core/http/auth/AuthDescriptorResolver;
public fun <init> ()V
public final fun resolve (Ljava/util/Set;)Lorg/dexpace/sdk/core/http/auth/AuthResolution;
public final fun resolve (Ljava/util/Set;Lorg/dexpace/sdk/core/http/auth/AuthDescriptor;)Lorg/dexpace/sdk/core/http/auth/AuthResolution;
public final fun resolve (Ljava/util/Set;Lorg/dexpace/sdk/core/http/auth/AuthDescriptor;Lorg/dexpace/sdk/core/http/auth/AuthDescriptor;)Lorg/dexpace/sdk/core/http/auth/AuthResolution;
public final fun resolve (Ljava/util/Set;Lorg/dexpace/sdk/core/http/auth/AuthDescriptor;Lorg/dexpace/sdk/core/http/auth/AuthDescriptor;Lorg/dexpace/sdk/core/http/auth/AuthDescriptor;)Lorg/dexpace/sdk/core/http/auth/AuthResolution;
public static synthetic fun resolve$default (Lorg/dexpace/sdk/core/http/auth/AuthDescriptorResolver;Ljava/util/Set;Lorg/dexpace/sdk/core/http/auth/AuthDescriptor;Lorg/dexpace/sdk/core/http/auth/AuthDescriptor;Lorg/dexpace/sdk/core/http/auth/AuthDescriptor;ILjava/lang/Object;)Lorg/dexpace/sdk/core/http/auth/AuthResolution;
}

public final class org/dexpace/sdk/core/http/auth/AuthDescriptorResolver$Companion {
}

public final class org/dexpace/sdk/core/http/auth/AuthDescriptorTier : java/lang/Enum {
public static final field CLIENT Lorg/dexpace/sdk/core/http/auth/AuthDescriptorTier;
public static final field OPERATION Lorg/dexpace/sdk/core/http/auth/AuthDescriptorTier;
public static final field PER_CALL Lorg/dexpace/sdk/core/http/auth/AuthDescriptorTier;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lorg/dexpace/sdk/core/http/auth/AuthDescriptorTier;
public static fun values ()[Lorg/dexpace/sdk/core/http/auth/AuthDescriptorTier;
}

public final class org/dexpace/sdk/core/http/auth/AuthMetadata {
public fun <init> (Ljava/util/List;)V
public fun <init> (Ljava/util/List;Ljava/util/List;)V
Expand All @@ -75,6 +127,57 @@ public final class org/dexpace/sdk/core/http/auth/AuthMetadata {
public final fun getSchemes ()Ljava/util/List;
}

public final class org/dexpace/sdk/core/http/auth/AuthRequirement {
public static final field Companion Lorg/dexpace/sdk/core/http/auth/AuthRequirement$Companion;
public fun <init> (Lorg/dexpace/sdk/core/http/auth/AuthScheme;)V
public fun <init> (Lorg/dexpace/sdk/core/http/auth/AuthScheme;Ljava/util/List;)V
public fun <init> (Lorg/dexpace/sdk/core/http/auth/AuthScheme;Ljava/util/List;Ljava/util/Map;)V
public synthetic fun <init> (Lorg/dexpace/sdk/core/http/auth/AuthScheme;Ljava/util/List;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z
public final fun getOauthParams ()Ljava/util/Map;
public final fun getOauthScopes ()Ljava/util/List;
public final fun getScheme ()Lorg/dexpace/sdk/core/http/auth/AuthScheme;
public fun hashCode ()I
public final fun newBuilder ()Lorg/dexpace/sdk/core/http/auth/AuthRequirement$Builder;
public static final fun of (Lorg/dexpace/sdk/core/http/auth/AuthScheme;)Lorg/dexpace/sdk/core/http/auth/AuthRequirement;
public fun toString ()Ljava/lang/String;
}

public final class org/dexpace/sdk/core/http/auth/AuthRequirement$Builder : org/dexpace/sdk/core/generics/Builder {
public fun <init> ()V
public fun <init> (Lorg/dexpace/sdk/core/http/auth/AuthRequirement;)V
public synthetic fun build ()Ljava/lang/Object;
public fun build ()Lorg/dexpace/sdk/core/http/auth/AuthRequirement;
public final fun oauthParams (Ljava/util/Map;)Lorg/dexpace/sdk/core/http/auth/AuthRequirement$Builder;
public final fun oauthScopes (Ljava/util/List;)Lorg/dexpace/sdk/core/http/auth/AuthRequirement$Builder;
public final fun scheme (Lorg/dexpace/sdk/core/http/auth/AuthScheme;)Lorg/dexpace/sdk/core/http/auth/AuthRequirement$Builder;
}

public final class org/dexpace/sdk/core/http/auth/AuthRequirement$Companion {
public final fun of (Lorg/dexpace/sdk/core/http/auth/AuthScheme;)Lorg/dexpace/sdk/core/http/auth/AuthRequirement;
}

public final class org/dexpace/sdk/core/http/auth/AuthResolution {
public fun <init> (Lorg/dexpace/sdk/core/http/auth/AuthRequirement;Lorg/dexpace/sdk/core/http/auth/AuthDescriptorTier;)V
public final fun component1 ()Lorg/dexpace/sdk/core/http/auth/AuthRequirement;
public final fun component2 ()Lorg/dexpace/sdk/core/http/auth/AuthDescriptorTier;
public final fun copy (Lorg/dexpace/sdk/core/http/auth/AuthRequirement;Lorg/dexpace/sdk/core/http/auth/AuthDescriptorTier;)Lorg/dexpace/sdk/core/http/auth/AuthResolution;
public static synthetic fun copy$default (Lorg/dexpace/sdk/core/http/auth/AuthResolution;Lorg/dexpace/sdk/core/http/auth/AuthRequirement;Lorg/dexpace/sdk/core/http/auth/AuthDescriptorTier;ILjava/lang/Object;)Lorg/dexpace/sdk/core/http/auth/AuthResolution;
public fun equals (Ljava/lang/Object;)Z
public final fun getRequirement ()Lorg/dexpace/sdk/core/http/auth/AuthRequirement;
public final fun getScheme ()Lorg/dexpace/sdk/core/http/auth/AuthScheme;
public final fun getTier ()Lorg/dexpace/sdk/core/http/auth/AuthDescriptorTier;
public fun hashCode ()I
public final fun isAnonymous ()Z
public fun toString ()Ljava/lang/String;
}

public final class org/dexpace/sdk/core/http/auth/AuthResolutionException : java/lang/RuntimeException {
public fun <init> (Ljava/util/List;Ljava/util/Set;)V
public final fun getAvailableSchemes ()Ljava/util/Set;
public final fun getRequiredSchemes ()Ljava/util/List;
}

public final class org/dexpace/sdk/core/http/auth/AuthScheme : java/lang/Enum {
public static final field API_KEY Lorg/dexpace/sdk/core/http/auth/AuthScheme;
public static final field BASIC Lorg/dexpace/sdk/core/http/auth/AuthScheme;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright (c) 2026 dexpace and Omar Aljarrah
*
* Licensed under the MIT License. See LICENSE in the project root.
* SPDX-License-Identifier: MIT
*/

package org.dexpace.sdk.core.http.auth

import java.util.Collections
import org.dexpace.sdk.core.generics.Builder as GenericBuilder

/**
* Per-operation auth descriptor: the ordered set of [AuthRequirement]s an operation accepts,
* in preference order. The first requirement whose scheme can be satisfied by the available
* credentials wins (see [AuthDescriptorResolver]).
*
* This is the runtime primitive a code generator would later emit one instance of per
* operation, but it is fully hand-constructable today. A two-boolean "needs-auth /
* needs-key" model does not generalise to operations that accept several alternative
* schemes with different OAuth parameters; an ordered [requirements] list does.
*
* The descriptor is deliberately **scheme-agnostic**: it records *which* schemes are
* acceptable and in what order, never how a scheme is stamped onto the wire. Mapping a
* resolved requirement to a concrete credential and auth step is the resolver's and the
* caller's job; per-cloud / OAuth specifics stay in adapters.
*
* A descriptor that lists [AuthScheme.NO_AUTH] anywhere in [requirements] declares that the
* operation may be invoked anonymously. [allowsAnonymous] reports this; the resolver treats
* it as an always-satisfiable terminal alternative.
*
* Immutable: [requirements] is copied on the way in and exposed as an unmodifiable view.
*
* @param requirements the accepted auth alternatives, in preference order. Must not be empty.
* @throws IllegalArgumentException if [requirements] is empty.
*/
public class AuthDescriptor private constructor(
requirements: List<AuthRequirement>,
) {
/** The accepted auth alternatives, in preference order; an unmodifiable defensive copy. */
public val requirements: List<AuthRequirement> =
Collections.unmodifiableList(requirements.toList())

/** The schemes this operation accepts, in preference order. Convenience over [requirements]. */
public val schemes: List<AuthScheme>
get() = requirements.map { it.scheme }

init {
require(this.requirements.isNotEmpty()) { "requirements must not be empty" }
}

/** True if any requirement is [AuthScheme.NO_AUTH] — the operation may run anonymously. */
public fun allowsAnonymous(): Boolean = requirements.any { it.scheme == AuthScheme.NO_AUTH }

/** A [Builder] pre-filled with this descriptor's requirements. */
public fun newBuilder(): Builder = Builder(this)

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AuthDescriptor) return false
return requirements == other.requirements
}

override fun hashCode(): Int = requirements.hashCode()

override fun toString(): String = "AuthDescriptor(requirements=$requirements)"

/**
* Mutable builder for [AuthDescriptor]. At least one requirement must be added before
* [build]; requirements are kept in the order they are added.
*/
public class Builder : GenericBuilder<AuthDescriptor> {
private val requirements: MutableList<AuthRequirement> = mutableListOf()

public constructor()

/** Pre-fills the builder with an existing [descriptor]'s requirements. */
public constructor(descriptor: AuthDescriptor) {
requirements.addAll(descriptor.requirements)
}

/** Appends a single [requirement] to the preference order. */
public fun addRequirement(requirement: AuthRequirement): Builder =
apply {
requirements.add(requirement)
}

/** Appends a single [scheme] (with no OAuth parameters) to the preference order. */
public fun addScheme(scheme: AuthScheme): Builder =
apply {
requirements.add(AuthRequirement.of(scheme))
}

/** Replaces all requirements with [requirements], preserving order. */
public fun requirements(requirements: List<AuthRequirement>): Builder =
apply {
this.requirements.clear()
this.requirements.addAll(requirements)
}

override fun build(): AuthDescriptor = AuthDescriptor(requirements)
}

public companion object {
/**
* Builds a descriptor from [requirements] in the given preference order.
*
* @throws IllegalArgumentException if [requirements] is empty.
*/
@JvmStatic
public fun of(requirements: List<AuthRequirement>): AuthDescriptor = AuthDescriptor(requirements)

/**
* Builds a descriptor from bare [schemes] (no OAuth parameters), in preference order.
*
* @throws IllegalArgumentException if [schemes] is empty.
*/
@JvmStatic
public fun ofSchemes(schemes: List<AuthScheme>): AuthDescriptor =
AuthDescriptor(schemes.map { AuthRequirement.of(it) })
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2026 dexpace and Omar Aljarrah
*
* Licensed under the MIT License. See LICENSE in the project root.
* SPDX-License-Identifier: MIT
*/

package org.dexpace.sdk.core.http.auth

/**
* Deterministic resolver for the per-operation auth descriptor ladder.
*
* Two independent precedence orders are applied, in this order:
*
* 1. **Tier precedence** — across the three descriptor tiers, the most specific *present*
* descriptor wins outright: per-call override > operation default > client default
* (see [AuthDescriptorTier]). A lower tier is consulted only when every higher tier is
* absent (`null`). The descriptor chosen this way is the only one resolved against —
* a per-call descriptor that cannot be satisfied does **not** fall through to the
* operation descriptor; it fails, because the caller asked for that override explicitly.
*
* 2. **Requirement precedence** — within the chosen descriptor, [AuthDescriptor.requirements]
* are tried in declared order and the first one whose scheme is *satisfiable* is returned.
* [AuthScheme.NO_AUTH] is always satisfiable (anonymous access); any other scheme is
* satisfiable iff it is in the supplied `availableSchemes` set.
*
* The resolver is **scheme-agnostic**: it never inspects a concrete [Credential] or knows how
* a scheme is stamped onto the wire. The caller supplies the set of schemes it can satisfy
* (typically derived from the credentials configured on the client), and the resolver returns
* the [AuthRequirement] to apply — mapping that requirement to a credential and an auth step
* stays with the caller / adapter. Per-cloud and OAuth specifics never reach core.
*
* Stateless and therefore safe for concurrent use; the single shared [INSTANCE] is the
* intended entry point.
*/
public class AuthDescriptorResolver {
/**
* Resolves the descriptor ladder against [availableSchemes].
*
* At least one of [perCall], [operation], or [client] must be non-null. The most specific
* present descriptor is selected by tier precedence, then its requirements are matched in
* declared order.
*
* @param availableSchemes the schemes the caller can satisfy (e.g. from configured
* credentials). [AuthScheme.NO_AUTH] need not be present — it is always satisfiable.
* @param perCall the highest-precedence descriptor attached to this single call, or null.
* @param operation the descriptor declared by the operation, or null.
* @param client the client-wide default descriptor, or null.
* @return the resolved [AuthResolution] — the requirement to apply and the tier it came from.
* @throws IllegalArgumentException if all three descriptors are null.
* @throws AuthResolutionException if the selected descriptor lists no satisfiable scheme.
*/
@JvmOverloads
public fun resolve(
availableSchemes: Set<AuthScheme>,
perCall: AuthDescriptor? = null,
operation: AuthDescriptor? = null,
client: AuthDescriptor? = null,
): AuthResolution {
val (descriptor, tier) =
selectDescriptor(perCall, operation, client)
?: throw IllegalArgumentException(
"at least one of perCall, operation, or client descriptor must be supplied",
)

val match =
descriptor.requirements.firstOrNull { isSatisfiable(it.scheme, availableSchemes) }
?: throw AuthResolutionException(descriptor.schemes, availableSchemes)

return AuthResolution(match, tier)
}

private fun selectDescriptor(
perCall: AuthDescriptor?,
operation: AuthDescriptor?,
client: AuthDescriptor?,
): Pair<AuthDescriptor, AuthDescriptorTier>? =
when {
perCall != null -> perCall to AuthDescriptorTier.PER_CALL
operation != null -> operation to AuthDescriptorTier.OPERATION
client != null -> client to AuthDescriptorTier.CLIENT
else -> null
}

private fun isSatisfiable(
scheme: AuthScheme,
availableSchemes: Set<AuthScheme>,
): Boolean = scheme == AuthScheme.NO_AUTH || scheme in availableSchemes

public companion object {
/** Shared stateless resolver instance. */
@JvmField
public val INSTANCE: AuthDescriptorResolver = AuthDescriptorResolver()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2026 dexpace and Omar Aljarrah
*
* Licensed under the MIT License. See LICENSE in the project root.
* SPDX-License-Identifier: MIT
*/

package org.dexpace.sdk.core.http.auth

/**
* The precedence tier an [AuthDescriptor] occupies in the resolution ladder. Listed from
* highest precedence to lowest; [AuthDescriptorResolver] consults a present descriptor in
* this exact order and resolves against the first one that is supplied.
*/
public enum class AuthDescriptorTier {
/** A descriptor attached to a single call, overriding everything below it. */
PER_CALL,

/** The descriptor declared by the operation being invoked. */
OPERATION,

/** The client-wide default descriptor, used when nothing more specific is supplied. */
CLIENT,
}
Loading
Loading