Sub-scope 04 (notification) platform/template + platform/security — manifest and probes revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5 generatedAt=2026-08-30T06:49:18+00:00 === OWNED FILES === 9346bef6c5dd48f99d3844f3e11eef3ae25e466e main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmCallbackPayloadProtection.java 144 71ca753a825184bcf3097323883f4d27919fa29c main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java 220 dba783d4abc8a1a3353de194c360656fe55ecdda main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmNotificationPayloadProtection.java 159 5a214c569bbd6aef67ec7e9368cd5de6f4baf7ae main/java/dev/caskeleton/adapter/outbound/notification/platform/security/CredentialGeneration.java 69 e7354819fdffe7830c2c69e3819b6b6006335657 main/java/dev/caskeleton/adapter/outbound/notification/platform/security/HmacProviderRequestIdHasher.java 53 f2cc413558aa0fd9cc4f23f796ab21eb46db0d26 main/java/dev/caskeleton/adapter/outbound/notification/platform/security/ProviderCredentialManager.java 228 ab92af842fdbc57398d5fa304d483ef5ae9dc3e7 main/java/dev/caskeleton/adapter/outbound/notification/platform/security/SettingsSecretMaterialProvider.java 49 f0cb06f7f5e16b65aea7e2ede4a5128c3224cddd main/java/dev/caskeleton/adapter/outbound/notification/platform/template/CanonicalNotificationRenderer.java 185 de60cca7689ab2744368b1ec01b6e1602d7529a6 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JacksonInboxContentCodec.java 78 2ee51493e045e65b15bac6e613d8eb5fd0effefc main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JacksonNotificationVariablesCodec.java 52 a3bac7bcbe260d9da1c700405c7e068cebefc603 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JacksonTemplateContentCodec.java 34 46b2a550a65f32b9189d66feee894acf963e7271 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JsonSchemaVariableValidator.java 63 df06a2611c45006150e5abab4c6f5dededb5c597 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/NotificationDigest.java 31 84940d4cf820ac09a0ee2efffb69d33f462ce72f main/java/dev/caskeleton/adapter/outbound/notification/platform/template/NotificationJsonMapper.java 25 5c9fda8dad8c514638f54b0242aa33adc228cd9e main/java/dev/caskeleton/adapter/outbound/notification/platform/template/NotificationTemplateEngine.java 42 50cc859f260939c56110cdc16ad0a0e0028478c8 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/PlaceholderTemplateEngine.java 68 15a9409b412feb1965b3b6e3980c5cc7a5b822e0 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/Sha256MessageDigestAdapter.java 12 24351d1a05410d0c1a6a828c22ff945a5094d2e0 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/TemplateSlotMode.java 29 aa4c6fa386f64e0cd26cfe2a510296e158a23618 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/TemplateSlotPolicy.java 129 b7e6a77d01988109f8a52ca18321f106dcbd2b8c main/java/dev/caskeleton/adapter/outbound/notification/platform/template/ThymeleafNotificationRenderer.java 50 ef52533c065b81d09e74461628e5dc7ee3b52761 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/ThymeleafStringTemplateEngine.java 162 ca195f7f8facaba4926929311a0519169a2cfce2 test/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtectorTest.java 89 57066ca6a3bd5cd4effe1ba9b7cbda14c88c4a96 test/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmNotificationPayloadProtectionTest.java 195 ed5627bb3158dc87ce83b3c10bc05289e1ba5ca7 test/java/dev/caskeleton/adapter/outbound/notification/platform/security/CallbackPayloadBoundTest.java 108 3bb01206fc668913558bfe3b14a1d93495f6c235 test/java/dev/caskeleton/adapter/outbound/notification/platform/security/CallbackPayloadRotationTest.java 99 520a7a954f32e01f17531ebf325e965987e95bb6 test/java/dev/caskeleton/adapter/outbound/notification/platform/security/CredentialDrainWindowTest.java 171 cc4491121483e729536258f302d7309cd1d287d4 test/java/dev/caskeleton/adapter/outbound/notification/platform/security/ProviderCredentialManagerConcurrencyTest.java 100 20aadd974a813ee2cbfe78600107d2b3fc94c515 test/java/dev/caskeleton/adapter/outbound/notification/platform/security/SecurityFixtures.java 117 3635d36869434d2cc757968cf908d309152f1407 test/java/dev/caskeleton/adapter/outbound/notification/platform/template/BothEnginesHonourSlotModeTest.java 100 6ba89f7fec2f42c6c5d4f3f7b19b4c3bfde5cfcb test/java/dev/caskeleton/adapter/outbound/notification/platform/template/CanonicalNotificationRendererTest.java 148 b75f5ab0227fa81adf6020f67fc6666acecf72ca test/java/dev/caskeleton/adapter/outbound/notification/platform/template/SlotAwareRenderingTest.java 127 de4160b12485e82ac23df8827de5dbd4306c4ecc test/java/dev/caskeleton/adapter/outbound/notification/platform/template/ThymeleafNotificationRendererTest.java 179 === 8.1 AccessContext: 감사 컨텍스트를 읽는 코드가 있는가 === $ grep -rn '\.auditRequired()' --include=*.java . | wc -l 0 exit=0 $ grep -rn '\.purposeCode()' --include=*.java . | wc -l 0 exit=0 $ grep -rn 'AccessContext' --include=*.java . | grep -v import | grep -v 'AttachmentAccessContext' ./application-core/src/main/java/dev/caskeleton/application/notification/platform/security/AccessContext.java:6:public record AccessContext(String purposeCode, String actorRef, boolean auditRequired) { ./application-core/src/main/java/dev/caskeleton/application/notification/platform/security/AccessContext.java:8: public AccessContext { ./application-core/src/main/java/dev/caskeleton/application/notification/platform/security/AccessContext.java:17: public static AccessContext dispatch(String providerProfileId) { ./application-core/src/main/java/dev/caskeleton/application/notification/platform/security/AccessContext.java:18: return new AccessContext("DISPATCH", providerProfileId, false); ./application-core/src/main/java/dev/caskeleton/application/notification/platform/security/ContactPointProtector.java:12: ContactPointValue reveal(ProtectedContactPoint protectedValue, AccessContext context); ./adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/SmtpProviderRuntimeAssemblerTest.java:122: dev.caskeleton.application.notification.platform.security.AccessContext context) { ./adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtectorTest.java:27: assertThat(protector.reveal(first, AccessContext.dispatch("ses-primary"))).isEqualTo(value); ./adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtectorTest.java:65: assertThatThrownBy(() -> protector.reveal(moved, AccessContext.dispatch("ses-primary"))) ./adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtectorTest.java:86: assertThatThrownBy(() -> protector.reveal(rotated, AccessContext.dispatch("ses-primary"))) ./adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java:86: public ContactPointValue reveal(ProtectedContactPoint protectedValue, AccessContext context) { ./adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesNotificationProviderAdapter.java:115: AccessContext.dispatch(submission.profile().profileId().value())); ./adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushNotificationProviderAdapter.java:90: AccessContext.dispatch(submission.profile().profileId().value())); ./adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/smtp/SmtpNotificationProviderAdapter.java:90: AccessContext.dispatch(submission.profile().profileId().value())); ./adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioSmsProviderAdapter.java:86: AccessContext.dispatch(submission.profile().profileId().value())); ./adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/fcm/FcmBatchCoordinator.java:70: AccessContext.dispatch(submission.profile().profileId().value())); ./adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/apns/ApnsNotificationProviderAdapter.java:84: AccessContext.dispatch(submission.profile().profileId().value())); exit=0 $ grep -n 'context' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java 86: public ContactPointValue reveal(ProtectedContactPoint protectedValue, AccessContext context) { 88: Objects.requireNonNull(context, "context"); exit=0 === 8.2 두 render 오버로드의 예외 처리 비교 === $ grep -n 'catch\|process(\|verifyRendered' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/ThymeleafStringTemplateEngine.java 80: String rendered = engineFor(mode).process(source, contextFor(source, variables)); 81: return TemplateSlotPolicy.verifyRendered(mode, rendered); 91: return engine.process(source, contextFor(source, variables)); 92: } catch (RuntimeException failure) { exit=0 $ grep -rn 'engine.render(' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/CanonicalNotificationRenderer.java 125: return engine.render(modeOf(slot), template.content().requireSlot(slot), variables); 133: .map(source -> engine.render(modeOf(slot), source, variables)); exit=0 $ grep -rn 'render(source\|render(String source' --include=*.java adapter/outbound/notification/src/main adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/ThymeleafStringTemplateEngine.java:85: public String render(String source, Map variables) { adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/PlaceholderTemplateEngine.java:28: public String render(String source, Map variables) { adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/NotificationTemplateEngine.java:21: String render(String source, Map variables); exit=0 === 8.3 감사 싱크는 존재하는가 === $ grep -rn 'NotificationAuditPort' --include=*.java adapter/outbound/notification/src/main app-bootstrap/src/main | head adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeRotator.java:5:import dev.caskeleton.application.notification.platform.observation.NotificationAuditPort; adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeRotator.java:26: private final NotificationAuditPort audit; adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeRotator.java:34: NotificationAuditPort audit, adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationAudit.java:5:import dev.caskeleton.application.notification.platform.observation.NotificationAuditPort; adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationAudit.java:23: implements NotificationAuditPort, NotificationSecurityAuditPort { app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformDispatchConfig.java:34:import dev.caskeleton.application.notification.platform.observation.NotificationAuditPort; app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformDispatchConfig.java:283: dev.caskeleton.application.notification.platform.observation.NotificationAuditPort audit, app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformDispatchConfig.java:427: NotificationAuditPort audit, app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformObservabilityConfig.java:14:import dev.caskeleton.application.notification.platform.observation.NotificationAuditPort; app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformObservabilityConfig.java:104: *

One bean. {@link LoggingNotificationAudit} implements both {@link NotificationAuditPort} and exit=0 === 8.4 키 분리 / 길이 검사 === $ grep -n 'REQUIRED_KEY_BITS\|requireDistinctKeys\|associatedData' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java 49: private static final int REQUIRED_KEY_BITS = 256; 68: requireDistinctKeys(encryption, lookup); 75: byte[] ciphertext = encrypt(encryption, nonce, associatedData(value.type()), plaintext); 94: associatedData(protectedValue.type()), 108: if (encryption.lengthBits() != REQUIRED_KEY_BITS) { 110: "contact encryption key must be " + REQUIRED_KEY_BITS + " bits"); 115: private static void requireDistinctKeys(SecretKeyMaterial encryption, SecretKeyMaterial lookup) { 122: private static byte[] associatedData(ContactPointType type) { 129: SecretKeyMaterial key, byte[] nonce, byte[] associatedData, byte[] plaintext) { 136: cipher.updateAAD(associatedData); 145: SecretKeyMaterial key, byte[] nonce, byte[] associatedData, byte[] ciphertext) { 152: cipher.updateAAD(associatedData); exit=0 $ grep -rn 'SecretPurpose\.' --include=*.java adapter/outbound/notification/src/main | sed 's/:.*SecretPurpose\./ -> /' | sort -u | head -20 adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> CALLBACK_FINGERPRINT_HMAC); adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> CALLBACK_SIGNING); adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> CONTACT_ENCRYPTION, adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> CONTACT_LOOKUP_HMAC, adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> PAYLOAD_ENCRYPTION, adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> PROVIDER_CREDENTIAL); adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> PROVIDER_REQUEST_LOOKUP_HMAC); adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> VAPID_SIGNING); adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioCallbackAdapter.java -> CALLBACK_SIGNING) { adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webhook/WebhookNotificationProviderAdapter.java -> CALLBACK_SIGNING) { adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/VapidKeyRegistry.java -> VAPID_SIGNING) { adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmCallbackPayloadProtection.java -> CALLBACK_FINGERPRINT_HMAC).material(), "HmacSHA256")); adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java -> CONTACT_ENCRYPTION); adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java -> CONTACT_LOOKUP_HMAC), value.type(), value.normalized()); adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java -> CONTACT_LOOKUP_HMAC); adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmNotificationPayloadProtection.java -> PAYLOAD_ENCRYPTION); adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/HmacProviderRequestIdHasher.java -> PROVIDER_REQUEST_LOOKUP_HMAC).material(), adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/ProviderCredentialManager.java -> PROVIDER_CREDENTIAL) { exit=0