001/* 002 * Copyright 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet; 018 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.NotThreadSafe; 023import javax.annotation.concurrent.ThreadSafe; 024import java.time.Instant; 025import java.util.Objects; 026import java.util.Optional; 027 028import static java.util.Objects.requireNonNull; 029 030/** 031 * Immutable result of one MCP subscription authorization check. The carrier is 032 * safe for concurrent access; an application remains responsible for the 033 * thread safety of an opaque application-context object stored in an allowed 034 * result. {@link Allowed} and {@link Denied} are the exhaustive result domain. 035 * 036 * <p> 037 * This is a closed domain; exhaustive switches are supported, and any new alternative requires an explicit reviewed contract amendment. 038 * 039 * @author <a href="https://www.revetkn.com">Mark Allen</a> 040 */ 041@ThreadSafe 042public sealed interface McpSubscriptionAuthorization 043 permits McpSubscriptionAuthorization.Allowed, 044 McpSubscriptionAuthorization.Denied { 045 /** 046 * Returns the shared denied authorization result. 047 * 048 * @return shared denied result 049 */ 050 @NonNull 051 static Denied deniedInstance() { 052 return Denied.INSTANCE; 053 } 054 055 /** 056 * An allowed subscription authorization with an application-requested 057 * expiration and optional replacement application context. 058 * 059 * @author <a href="https://www.revetkn.com">Mark Allen</a> 060 */ 061 @ThreadSafe 062 public final class Allowed implements McpSubscriptionAuthorization { 063 @NonNull 064 private final Instant validUntil; 065 @Nullable 066 private final Object applicationContext; 067 068 /** 069 * Creates an allowed authorization without an application context. 070 * 071 * @param validUntil application-requested authorization expiration 072 * @return allowed authorization 073 * @throws NullPointerException if {@code validUntil} is null 074 */ 075 @NonNull 076 public static Allowed fromValidUntil(@NonNull Instant validUntil) { 077 return new Allowed(validUntil); 078 } 079 080 /** 081 * Vends a builder primed with an application-requested expiration. 082 * 083 * @param validUntil application-requested authorization expiration 084 * @return allowed-authorization builder 085 * @throws NullPointerException if {@code validUntil} is null 086 */ 087 @NonNull 088 public static Builder withValidUntil(@NonNull Instant validUntil) { 089 return new Builder(validUntil); 090 } 091 092 private Allowed(@NonNull Instant validUntil) { 093 this.validUntil = requireNonNull(validUntil); 094 this.applicationContext = null; 095 } 096 097 private Allowed(@NonNull Builder builder) { 098 this.validUntil = requireNonNull(builder).validUntil; 099 this.applicationContext = builder.applicationContext; 100 } 101 102 /** @return application-requested authorization expiration */ 103 @NonNull 104 public Instant getValidUntil() { 105 return this.validUntil; 106 } 107 108 /** 109 * Returns replacement application context for subsequent authorization and 110 * projection work. Absence clears any previous application context. 111 * 112 * @return replacement application context, when present 113 */ 114 @NonNull 115 public Optional<@NonNull Object> getApplicationContext() { 116 return Optional.ofNullable(this.applicationContext); 117 } 118 119 /** @return whether this result contains the same values */ 120 @Override 121 public boolean equals(@Nullable Object other) { 122 if (this == other) 123 return true; 124 if (!(other instanceof Allowed allowed)) 125 return false; 126 return this.validUntil.equals(allowed.validUntil) 127 && Objects.equals(this.applicationContext, 128 allowed.applicationContext); 129 } 130 131 /** @return value-based hash code */ 132 @Override 133 public int hashCode() { 134 return 31 * this.validUntil.hashCode() 135 + Objects.hashCode(this.applicationContext); 136 } 137 138 /** @return diagnostic rendering without application-context data */ 139 @Override 140 @NonNull 141 public String toString() { 142 return "Allowed{validUntil=" + this.validUntil 143 + ", applicationContext=<redacted>}"; 144 } 145 146 /** 147 * Builder for immutable allowed subscription authorizations. 148 * <p> 149 * This class is intended for use by a single thread. 150 * 151 * @author <a href="https://www.revetkn.com">Mark Allen</a> 152 */ 153 @NotThreadSafe 154 public static final class Builder { 155 @NonNull 156 private final Instant validUntil; 157 @Nullable 158 private Object applicationContext; 159 160 private Builder(@NonNull Instant validUntil) { 161 this.validUntil = requireNonNull(validUntil); 162 } 163 164 /** 165 * Replaces the application context. Passing null clears it rather than 166 * retaining a context from an earlier authorization. 167 * 168 * @param applicationContext replacement application context, or null to 169 * clear it 170 * @return this builder 171 */ 172 @NonNull 173 public Builder applicationContext( 174 @Nullable Object applicationContext) { 175 this.applicationContext = applicationContext; 176 return this; 177 } 178 179 /** @return immutable allowed subscription authorization */ 180 @NonNull 181 public Allowed build() { 182 return new Allowed(this); 183 } 184 } 185 } 186 187 /** 188 * A data-free denied subscription authorization. 189 * 190 * @author <a href="https://www.revetkn.com">Mark Allen</a> 191 */ 192 @ThreadSafe 193 public final class Denied implements McpSubscriptionAuthorization { 194 @NonNull 195 private static final Denied INSTANCE = new Denied(); 196 197 private Denied() { 198 } 199 200 /** @return whether the other value is also a denied authorization */ 201 @Override 202 public boolean equals(@Nullable Object other) { 203 return other instanceof Denied; 204 } 205 206 /** @return value-based hash code */ 207 @Override 208 public int hashCode() { 209 return 0; 210 } 211 212 /** @return safe diagnostic rendering */ 213 @Override 214 @NonNull 215 public String toString() { 216 return "Denied{}"; 217 } 218 } 219}