データモデル¶
Naan → Manager → Shoulder → Ark の 1 本で全 NAAN を扱う。個別 NAAN を持つ組織でも
shoulder を必ず使う——使わないと NAAN ごとにモデルが分岐し、first-digit 規約が NAAN に
よって成立したりしなかったりする。
erDiagram
NAAN ||--o{ MANAGER : "名前空間を委譲する"
NAAN ||--o{ SHOULDER : "配下に持つ"
NAAN ||--o{ ARK : "権威を持つ"
NAAN ||--o{ CLIENT : ""
MANAGER ||--o{ SHOULDER : "預かる"
MANAGER |o--|| SHOULDER : "既定の採番先"
MANAGER |o--o{ MANAGER : "統廃合で承継する"
MANAGER ||--o{ CLIENT : ""
SHOULDER ||--o{ ARK : "この名前空間で採番された"
SHOULDER |o--o{ CLIENT : "固定する(任意)"
CLIENT ||--o{ CREDENTIAL : "持つ"
ARK ||--o{ MINT_RECEIPT : "控え"
ARK ||--o{ ARK_CHANGE : "行き先が変わった記録"
NAAN {
string naan PK "N2: 文字列。099999 と 99999 は別の NAAN"
string name
bool is_authoritative "D3: 未知名を 404 と言えるか"
string redirect "権威を持たないときの委譲先"
string na_policy "永続性宣言(NP | NR, OP, CC | 2026 | URL)"
string minter "採番を外に委ねている場合の案内先"
}
MANAGER {
int id PK
string naan FK
string name "内部専用。公開しない"
int default_shoulder_id FK "shoulder 省略時の採番先"
string commitment_level "NLM の permanence ratings"
int quota_per_day "R3: null は無制限"
bool active
int succeeded_by_id FK "承継先。識別子は壊さない"
}
SHOULDER {
int id PK
string shoulder "例 /x9"
string naan FK
int manager_id FK "null は組織未割当"
string redirect "N2T: 解決の委譲($id / ${blade} / 303)"
string minter "N2T: 採番の委譲先"
string status "active / reserved / delegated / retired"
string note
}
ARK {
string ark PK "naan/name。**削除しない**"
string naan FK
int shoulder_id FK
string assigned_name
string url "空なら記述を返す(D6)"
string commitment "この対象への約束"
string metadata
string who "ERC"
string what_title "ERC: title 列"
string when "ERC"
string created_by "R2: 監査証跡"
string updated_by
}
CLIENT {
int id PK
string client_id UK "外部に見せる識別子。OIDC の azp と突き合わせる"
string naan FK
int manager_id FK
string subject_type "machine / person"
string authority "system / naan / manager"
int shoulder_id FK "1 つに固定する(任意)"
string allowed_scopes "ark:mint ark:update ..."
bool active "無効化でトークンが即座に効かなくなる"
date expires_at "authority=naan では必須"
}
CREDENTIAL {
int id PK
int client_pk FK
string kind "api_key / client_secret / password"
string prefix "照合を O(1) にする前置き。秘密ではない"
string hashed "Argon2。**平文は保存しない**"
bool active "失効させても行は消さない"
int failed_attempts "総当たり対策"
date locked_until
}
MINT_RECEIPT {
int id PK
string client_id "主体ごとに独立"
string request_id "F4: 冪等鍵"
string ark FK
}
ARK_CHANGE {
int id PK
string ark FK
date at
string action "update / tombstone"
string before_url "**復元したいのはこれ**"
string after_url
string by
string ip
}
AUDIT_EVENT {
int id PK
date at
string client_id
string authority
string action "mint / update / succeed / depart ..."
string target
json detail
}
UNKNOWN_SUBJECT {
int id PK
string subject "認可サーバ側の識別子(azp / client_id / sub)"
string issuer
date first_seen
date last_seen
int seen
string ip
}
UNKNOWN_SUBJECT は、認可サーバのトークンは正しいのに台帳に登録の無かった主体
である。client_id の綴りが 1 文字違うだけで 401 になるが、弾いた時点で arkhe は
正しい文字列を手に持っている——azp はもう署名検証を通っている。捨てずに残せば、
運用者は打ち直さずに登録できる。登録が済めば一覧から自動的に消える(照合は
問い合わせのたびに行うので、消す操作が要らない)。
他の表と外部キーで結ばない。どの組織のものかは分からないからで、推測もしない ——だから見えるのは NAAN 以上に届く主体だけにしてある。
ARK_CHANGE は ARK の行き先が変わった記録で、監査ログとは別に持つ。監査は
NAAN 単位以上の操作しか残さないが、採番も付け替えも組織が行うので、監査だけ
では肝心の変更が落ちる。NR を宣言する体系で「この識別子は変わらない」と言う
なら、変えたのは何でいつ誰が変えたのかを示せなければならない——さもないと、
約束を検証する手段が利用者の側に無い。
AUDIT_EVENT は他の表と外部キーで結ばない。記録は対象が消えても残るべきもので、
参照整合性で縛ると「消せないから記録も消す」という逆の力が働く。
図に描けないこと¶
ER 図は形しか示さない。arkhe の設計の中身は制約のほうにある。
| ARK は削除できない | 行を消すと解決が止まる=識別子が壊れる。before_delete で拒否する。対象が失われたら tombstone にするか url を空にして記述を返す |
| shoulder も削除できない | 乱数割当が同じ文字列を再び当てうる=NR 違反の芽。status=retired にする |
| retired からは戻せない | 引退した名前空間の再開は、その間に外部が同じ名前を使った可能性を否定できない |
| 採番は UPDATE に化けない | 主キー衝突は必ず失敗させる。arklet で最重大の欠陥がこれだった |
| 到達範囲は登録属性 | authority / manager_id / shoulder_id / allowed_scopes はクライアント登録の属性で、リクエストやトークン要求では広がらない |
| 人と機械を分ける | subject_type=machine は外部ログインで名乗れず、person は API キーを持てない |
| 循環参照 | manager.default_shoulder_id ⇄ shoulder.manager_id。PostgreSQL は CREATE TABLE の時点で参照先を要求するので、use_alter で後付けにしてある |
容量について¶
子リソースは採番しない。 ark:/99999/x9abc/page/3 のような深い参照は suffix
passthrough が賄うので、1 レコード 1 採番で足りる。ここが容量設計でいちばん効く。