Files
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

15 lines
6.5 KiB
JSON

{
"assetKey": "a10-f006-requireidentifier-branch",
"kind": "terminal",
"command": "set -e\nD=$(mktemp -d); trap 'rm -rf \"$D\"' EXIT\nJAR=/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/cache-redis/build/libs/cache-redis-0.0.1+21234e38cdb9.jar\njava -version 2>&1 | head -1\ncat > \"$D/IdentifierBranchProbe.java\" <<'JAVA'\nimport dev.caskeleton.adapter.outbound.cache.redis.sdk.api.key.RedisKeyRules;\nimport java.util.Locale;\nimport java.util.Random;\nimport java.util.regex.Pattern;\n\npublic final class IdentifierBranchProbe {\n\n private static final Pattern IDENTIFIER =\n Pattern.compile(\"^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$\");\n private static final Pattern JSON_WEB_TOKEN =\n Pattern.compile(\"^[A-Za-z0-9_-]{8,}\\\\.[A-Za-z0-9_-]{8,}\\\\.[A-Za-z0-9_-]{8,}$\");\n private static final Pattern INTERNATIONAL_PHONE = Pattern.compile(\"^\\\\+\\\\d[\\\\d.~-]{7,}$\");\n\n private static final String[] GUARDS = {\"문자클래스\", \"메일\", \"JWT\", \"전화\", \"접두\"};\n\n /** Which guards a value satisfies, in source order. */\n private static boolean[] guards(String v) {\n String low = v.toLowerCase(Locale.ROOT);\n return new boolean[] {\n !IDENTIFIER.matcher(v).matches(),\n v.indexOf('@') >= 0,\n JSON_WEB_TOKEN.matcher(v).matches(),\n INTERNATIONAL_PHONE.matcher(v).matches(),\n low.startsWith(\"bearer\") || low.startsWith(\"eyj\"),\n };\n }\n\n /** The message the first satisfied guard would produce. */\n private static String predict(String v) {\n boolean[] hit = guards(v);\n if (hit[0]) {\n return \"identifier must be 1..128 characters of [A-Za-z0-9._~-] and must not contain a\"\n + \" key separator\";\n }\n if (hit[1]) {\n return \"identifier must not contain a mail address\";\n }\n if (hit[2]) {\n return \"identifier must not contain a JSON web token\";\n }\n if (hit[3]) {\n return \"identifier must not contain a phone number\";\n }\n if (hit[4]) {\n return \"identifier must not contain authentication material\";\n }\n return \"통과\";\n }\n\n private static String actual(String v) {\n try {\n RedisKeyRules.requireIdentifier(v);\n return \"통과\";\n } catch (IllegalArgumentException e) {\n return e.getMessage();\n }\n }\n\n private static void run(String label, String v) {\n boolean[] hit = guards(v);\n StringBuilder marks = new StringBuilder();\n for (int i = 0; i < GUARDS.length; i++) {\n marks.append(hit[i] ? GUARDS[i] : \" \".repeat(GUARDS[i].length())).append(' ');\n }\n System.out.printf(\"%-16s %-4d %s| %s%n\", label, v.length(), marks, actual(v));\n }\n\n public static void main(String[] args) {\n String jwtHead = \"eyJhbGciOiJIUzI1NiJ9\";\n String longJwt = jwtHead + \".\" + \"a\".repeat(120) + \".\" + \"b\".repeat(96);\n\n System.out.println(\"입력 길이 걸리는 검사 전부 | requireIdentifier 가 낸 메시지\");\n System.out.println(\"-\".repeat(122));\n run(\"test: 메일\", \"person@example.com\");\n run(\"test: 전화\", \"+821012345678\");\n run(\"test: eyj\", jwtHead);\n run(\"test: bearer\", \"bearer-abcdefabcdef\");\n run(\"test: JWT\", jwtHead + \".eyJzdWIiOiIxIn0.c2lnbmF0dXJlLXZhbHVl\");\n run(\"test: 129자\", \"a\".repeat(129));\n System.out.println();\n System.out.println(\"JWT 분기만 걸리는 값이 있는가\");\n run(\"합성 JWT 26자\", \"abcdefgh.abcdefgh.abcdefgh\");\n run(\"_로 시작\", \"_bcdefgh.abcdefgh.abcdefgh\");\n run(\"실제 크기 JWT\", longJwt);\n System.out.println();\n System.out.println(\"메일과 전화 분기만 걸리는 값이 있는가\");\n System.out.println(\" 메일 분기는 '@' 를 요구한다 -> IDENTIFIER 문자 클래스에 '@' 없음\");\n System.out.println(\" 전화 분기는 '+' 로 시작한다 -> IDENTIFIER 첫 문자는 [A-Za-z0-9]\");\n System.out.println();\n System.out.println(\"옮겨 적은 정규식이 실제와 같은지 무작위 입력으로 대조한다.\");\n selfCheck(200_000);\n }\n\n private static void selfCheck(int rounds) {\n String alphabet = \"abcdefghjrzABZ09._~-@+:{} eyj.\";\n Random random = new Random(20260902L);\n int agreed = 0;\n int[] firstGuard = new int[GUARDS.length + 1];\n for (int i = 0; i < rounds; i++) {\n int length = 1 + random.nextInt(200);\n StringBuilder b = new StringBuilder(length);\n for (int j = 0; j < length; j++) {\n b.append(alphabet.charAt(random.nextInt(alphabet.length())));\n }\n // Half the draws start from a token shape so the later guards are reachable.\n String value = (i % 2 == 0) ? b.toString() : seeded(random, b.toString());\n if (actual(value).equals(predict(value))) {\n agreed++;\n }\n boolean[] hit = guards(value);\n int landed = GUARDS.length;\n for (int g = 0; g < GUARDS.length; g++) {\n if (hit[g]) {\n landed = g;\n break;\n }\n }\n firstGuard[landed]++;\n }\n System.out.printf(\" %d / %d 일치%n\", agreed, rounds);\n for (int g = 0; g < GUARDS.length; g++) {\n System.out.printf(\" %-10s 에서 갈린 입력 %d%n\", GUARDS[g], firstGuard[g]);\n }\n System.out.printf(\" %-10s %d%n\", \"통과\", firstGuard[GUARDS.length]);\n }\n\n /** A value shaped like something the later guards could match. */\n private static String seeded(Random random, String tail) {\n String core = tail.replaceAll(\"[^A-Za-z0-9_-]\", \"a\");\n return switch (random.nextInt(4)) {\n case 0 -> \"eyj\" + core;\n case 1 -> \"bearer\" + core;\n case 2 -> \"+8210\" + core.replaceAll(\"\\\\D\", \"1\");\n default -> {\n String seg = (core + \"abcdefgh\").substring(0, 8);\n yield seg + \".\" + seg + \".\" + seg;\n }\n };\n }\n}\nJAVA\njavac -encoding UTF-8 -cp \"$JAR\" -d \"$D\" \"$D/IdentifierBranchProbe.java\"\njava -Dstdout.encoding=UTF-8 -cp \"$JAR:$D\" IdentifierBranchProbe\n",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T12:55:51+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a10-f006-requireidentifier-branch.txt",
"svg": "evidence/rendered/a10-f006-requireidentifier-branch.svg",
"rawSha256": "6e0ca6756b46beb4d977b5f42cb98b840c675ddd63bf6cdb0fa05c683501d301",
"lines": 27,
"redaction": "none — 밀폐 탐침 실행 출력, 자격증명 없음"
}