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<String, Object> variables) {
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/PlaceholderTemplateEngine.java:28:  public String render(String source, Map<String, Object> variables) {
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/NotificationTemplateEngine.java:21:  String render(String source, Map<String, Object> 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:   * <p>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

