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.ThreadSafe; 023import java.time.Instant; 024import java.util.Collections; 025import java.util.EnumSet; 026import java.util.Objects; 027import java.util.Set; 028 029import static java.util.Objects.requireNonNull; 030 031/** 032 * Immutable HTTP admission decision for session-enabled {@code 2025-06-18} 033 * and {@code 2025-11-25} GET delivery or DELETE cleanup. Soklet's 034 * {@code 2026-07-28} implementation does not use this decision. 035 * <p> 036 * Every acceptance supplies an explicit expiry. Soklet caps GET leases using 037 * its configured authorization, subscription and session lifetimes, and 038 * verifies expiry again before delivery or DELETE retirement. Application 039 * values reachable through the identity retain their own thread-safety and 040 * lifecycle contracts. 041 * 042 * <p> 043 * This is a closed domain; exhaustive switches are supported, and any new alternative requires an explicit reviewed contract amendment. 044 * 045 * @author <a href="https://www.revetkn.com">Mark Allen</a> 046 */ 047@ThreadSafe 048public sealed interface McpSessionTransportAdmissionDecision 049 permits McpSessionTransportAdmissionDecision.Accepted, 050 McpSessionTransportAdmissionDecision.Rejected { 051 /** 052 * Creates an accepted decision. The selected notification families must be 053 * a subset of the context's offered families; DELETE requires an empty set. 054 * The expiry must still be in the future when Soklet applies the decision. 055 * These operation-dependent conditions are checked by the runtime. 056 * 057 * @param admissionIdentity freshly admitted identity 058 * @param validUntil application authorization expiry 059 * @param notificationTypes selected notification families, possibly empty 060 * @return immutable accepted decision 061 * @throws NullPointerException if an argument or notification type is null 062 */ 063 @NonNull 064 static Accepted accepted(@NonNull McpAdmissionIdentity admissionIdentity, 065 @NonNull Instant validUntil, 066 @NonNull Set<@NonNull McpSubscriptionNotificationType> notificationTypes) { 067 return new Accepted(admissionIdentity, validUntil, notificationTypes); 068 } 069 070 /** 071 * Creates an HTTP rejection using the existing rejection's safe status and 072 * response headers. GET and DELETE have no JSON-RPC request ID, so the 073 * rejection's JSON-RPC error is not rendered on these paths. Header safety 074 * and framework-owned status rules remain checked before transport. 075 * 076 * @param admissionRejection HTTP status and application response headers 077 * @return immutable rejected decision 078 * @throws NullPointerException if the rejection is null 079 */ 080 @NonNull 081 static Rejected rejected(@NonNull McpAdmissionRejection admissionRejection) { 082 return new Rejected(admissionRejection); 083 } 084 085 /** 086 * Accepted HTTP identity, explicit expiry and immutable family selection. 087 * 088 * @author <a href="https://www.revetkn.com">Mark Allen</a> 089 */ 090 @ThreadSafe 091 public final class Accepted implements McpSessionTransportAdmissionDecision { 092 @NonNull private final McpAdmissionIdentity identity; 093 @NonNull private final Instant validUntil; 094 @NonNull private final Set<@NonNull McpSubscriptionNotificationType> notificationTypes; 095 096 private Accepted(@NonNull McpAdmissionIdentity admissionIdentity, 097 @NonNull Instant validUntil, 098 @NonNull Set<@NonNull McpSubscriptionNotificationType> notificationTypes) { 099 this.identity = requireNonNull(admissionIdentity); 100 this.validUntil = requireNonNull(validUntil); 101 EnumSet<McpSubscriptionNotificationType> copiedTypes = EnumSet.noneOf(McpSubscriptionNotificationType.class); 102 for (McpSubscriptionNotificationType notificationType : requireNonNull(notificationTypes)) 103 copiedTypes.add(requireNonNull(notificationType)); 104 this.notificationTypes = Collections.unmodifiableSet(copiedTypes); 105 } 106 107 /** @return freshly admitted identity */ 108 @NonNull public McpAdmissionIdentity getIdentity() { return this.identity; } 109 110 /** @return application authorization expiry */ 111 @NonNull public Instant getValidUntil() { return this.validUntil; } 112 113 /** @return immutable selected families in enum declaration order */ 114 @NonNull public Set<@NonNull McpSubscriptionNotificationType> getNotificationTypes() { 115 return this.notificationTypes; 116 } 117 118 /** @return whether every admission property is structurally equal */ 119 @Override 120 public boolean equals(@Nullable Object other) { 121 return this == other || other instanceof Accepted accepted 122 && this.identity.equals(accepted.identity) 123 && this.validUntil.equals(accepted.validUntil) 124 && this.notificationTypes.equals(accepted.notificationTypes); 125 } 126 127 /** @return value-based hash code */ 128 @Override public int hashCode() { return Objects.hash(this.identity, this.validUntil, this.notificationTypes); } 129 130 /** @return redacted diagnostic rendering */ 131 @Override @NonNull public String toString() { 132 return "Accepted{identity=<redacted>, validUntil=<redacted>, notificationTypeCount=" + this.notificationTypes.size() + "}"; 133 } 134 } 135 136 /** 137 * Rejected HTTP decision retaining the rejection carrier. Soklet validates 138 * its HTTP headers before applying it to the transport. 139 * 140 * @author <a href="https://www.revetkn.com">Mark Allen</a> 141 */ 142 @ThreadSafe 143 public final class Rejected implements McpSessionTransportAdmissionDecision { 144 @NonNull private final McpAdmissionRejection rejection; 145 146 private Rejected(@NonNull McpAdmissionRejection admissionRejection) { 147 this.rejection = requireNonNull(admissionRejection); 148 } 149 150 /** @return HTTP status and response-header rejection carrier */ 151 @NonNull public McpAdmissionRejection getRejection() { return this.rejection; } 152 153 /** @return whether this decision carries the same rejection */ 154 @Override 155 public boolean equals(@Nullable Object other) { 156 return this == other || other instanceof Rejected rejected && this.rejection.equals(rejected.rejection); 157 } 158 159 /** @return value-based hash code */ 160 @Override public int hashCode() { return this.rejection.hashCode(); } 161 162 /** @return redacted diagnostic rendering */ 163 @Override @NonNull public String toString() { return "Rejected{rejection=<redacted>}"; } 164 } 165}