壊さないもの¶
arkhe の設計は、その多くが拒否でできている。すべては 1 つの約束から出ている—— 一度配った名前が、別のものを指すようになることはない。
これらは方針文書ではなくコードで守っている。人が覚えていることに頼る規則は、 いつか破られる。たいていは深夜に、別の何かを直している最中に。
公開した ARK は削除しない¶
行を消すと解決が止まる。解決しなくなった識別子は、壊れた識別子である。Ark は
ORM の層で削除を拒む——しかも見るのは「一度でも公開したか」であって、
「今 公開中か」ではない。後者で見ると、先に公開を取り下げるだけで守りを抜けられる。
出口は 2 つあり、どちらも跡が残る。 公開の取り下げ(/api/unpublish)は
戻せる操作で、行は残る。削除と破棄は戻せない——理由と ARK の打ち直しを要求し、
その名前は二度と採られない台帳へ移る。どちらも呼ぶ側の届く範囲の内側だけである。
どちらを通っても NR は守られる。 外に出て消された名前は永久に 404 を返す
——別のものを指すようになることは無い。破れうるのは「解決し続ける」ほうで、
だから出口は狭く、記録が残り、削除命令や公開してはならなかったもののためにだけ
使う。整理のために使うものではない。
対象が本当に失われたときは tombstone にする。識別子と記述は残り、到達性だけが落ちる。
以後リゾルバは、転送ではなく記述を返す。これは FAIR A2——データが利用できなくなっても メタデータは参照できるべき——そのものである。
外に出す前なら、まだ引き返せる¶
NR が縛るのは、外へ出した名前である。 採番した瞬間から縛られるわけではない
——下書きの対象に先に番号を振るのは普通の運用で、その登録が取りやめになったとき、
誰も指さない番号が永久に残るほうが約束を守っていることになるわけではない。
そこで ARK は公開前として採れる。公開するまでは公開のリゾルバでは解決せず ——知らない名前と同じに答える——削除できる。
閉域のリゾルバは別である(ARKHE_RESOLVE_UNPUBLISHED=1)。閉じた網の中で採番した
ARK を、その網のリゾルバが解決できないなら配る意味が無い。解決するかどうかは、ARK の
状態だけでなく、どのリゾルバが答えているかで決まる——分散して運用する。
curl -X POST /api/mint -d '{"reserve": true}' # まだ解決しない
curl -X POST /api/delete -d '{"ark": "ark:99999/x9…"}' # 未公開ならこれだけ
curl -X POST /api/delete/bulk -d '{"data": ["ark:99999/x9…", "…"]}' # まとめて捨てる
curl -X POST /api/publish -d '{"ark": "ark:99999/x9…"}' # ここから解決を始める
curl -X POST /api/unpublish -d '{"ark": "…", "reason": "…", "confirm": "…"}'
curl -X POST /api/publish -d '{"ark": "ark:99999/x9…"}' # 出し直す
curl -X POST /api/delete -d '{"ark": "…", "reason": "…", "confirm": "…"}' # 出した名前
一度も外に出していないうちは、削除は 1 回の呼び出しで、何も要求しない——理由も 打ち直しも要らず、まとめて捨てるのも 1 回。予約でまとめて採るのは実際の使い方で、 番号は審査中の対象に、ときに何年も預けられる。捨てるのが採るより高くつくなら、 放置された番号が台帳に残るだけで、それは予約という仕組みが避けようとしていた当のもの である。重くしても何も守られない。
公開は戻せる。削除は戻せない。 公開を取り下げても行は残るので、出し直せる
——出してよいかの判断は対象を持っている側にあり、そこから RA に上げて戻って
くるまで待たせれば、その間は出たままになる。公開中の ARK への /api/delete は
今までどおり 409 で断る——先に取り下げるか、/api/purge で一手に行う。
「世に出た」かどうかは、採番する側からは観測できない。 そこで arkhe は初回の公開を その代理に使う——一度でも解決したなら、以後その名前は誰かが持っているものとして扱う。 この代用は片側にしか外れない。誰にも見られないまま公開された ARK に重い経路を要求 することはあり、その代償は理由 1 行と ARK の打ち直しである。逆——外に出た名前を私的な 予約として扱う——は取り返しがつかないが、この向きでは起こらない。
戻らないのは「一度出した」ことのほうである。 初めて公開した時刻は記録に残り、 決して消えない。これが決めるのは儀式の重さで、誰も見なかった予約の取り下げは 軽く、世に出た名前を引っ込めるには理由と打ち直しが要る。重さは名前の履歴に 従い、行う人の位には従わない——危ないのは「誰が消すか」ではなく 「何が消えるか」だからである。届く範囲はほかと同じに縛る——組織は自分の shoulder、NAAN 管理者は自 NAAN の内側で行う。
名前は解放しない。 取り下げた名前は別の台帳に移り、採番でも取り込みでも二度と
当たらない。だから閉域で引けていた ARK を取り下げても、その名前が別のものを指すことは
無い——残った参照は 404 になるだけである(NR が守るのはそこである)。予約した文字列は既に誰かの手に渡っていることが多く(それが予約の目的
そのものである)、別の対象に振り直せば、外からは NR 違反と見分けがつかない。
止めるのは転送であって、解決ではない¶
行き先が信用できなくなったとき——委譲先が落ちた、間違った URL を配ってしまった、 取り下げを求められた——急いで止めたいが、識別子を殺してはいけない。
404 は嘘である(その識別子は存在する)。503 は「今は答えられない」と言えるが、
永続識別子が壊れて見えることに変わりはない。arkhe は 200 と記述を返す
——公開した ARK は削除しないと同じ経路で、識別子は生きたまま転送だけが止まる。
arkhe hold add ark ark:99999/x9tn1qkq2g7 --days 3 --reason "行き先を確認中"
arkhe hold add shoulder 7 --days 1 --reason "委譲先のリゾルバが落ちている"
tombstone と混ぜない。あちらは「対象が失われた」という恒久の宣言で、こちらは 「対象は在るが、今は行き先を出せない」という期限つきの措置である。だから hold は 元の行き先を残し、期限が切れれば時計だけで元に戻る(戻し忘れが起きない)。
?info も ?? も止めない。永続性の宣言を引っ込めることになるからで、
止めている理由と期限はむしろそこに載せて公開する。
名前空間も削除しない¶
shoulder を消すと、乱数割当が既に使われた文字列を再び当てうる。消さずに
status=retired にする。既存の ARK は解決し続ける。
とくに delegated の shoulder は消せない——外部の minter が、この台帳の知らない名前を
その中に作っている可能性がある。
採番が更新に化けない¶
arkhe が出発点にした arklet で、最重大の欠陥がこれだった。 主キーの衝突が save() に
吸収されて UPDATE に化け、既存の ARK の向き先を黙って書き換えていた(本家では
その後直っている——ほかの実装との比較で確かめた)。arkhe は
採番を 1 か所の経路に閉じ、衝突は失敗させ、数えて、別の名前で採り直す。
衝突回数を握りつぶさず返しているのは、衝突率の上昇が名前空間の枯渇を知らせる唯一の 合図だから。
到達範囲はリクエストで広がらない¶
authority / manager_id / shoulder_id / allowed_scopes は登録の属性である。
リクエストは shoulder を名指すことはできるが、答えは常に「それが主体の範囲の内側か
どうか」だけ。
トークンは自分が運ぶ scope を狭めることはできる。増やすことはできない。
人と機械は別の型¶
machine の主体は鍵で認証し、外部ログインでは名乗れない。person の主体は身元を
外部が保証し、API キーを持てない。
前段のプロキシを正しく置けばなりすましは防げる。だが設定 1 つの誤りが「一括投入 バッチとして全件書き換え」に化けるのは、放置してよい鋭さではない。
記録は対象より長く残る¶
AuditEvent は他の表と外部キーで結んでいない。記録は対象が消えても残るべきもので、
参照整合性で縛ると逆の力が働く——「消せないから記録も消す」が楽になってしまう。
NAAN 以上に届く操作は全件記録する。届く範囲が広いほど、後から誰が何をしたかを 辿れる必要が高いから。
どこで守っているか¶
| 不変条件 | 実装 |
|---|---|
| 公開した ARK と shoulder の削除禁止 | db/models.py の before_delete |
| 取り下げた名前を二度と採らない | WithdrawnName(domain/minting.py が見る) |
| 保留は転送だけを止める | domain/resolution.py の hold_of / effective_hold |
retired から戻せない |
domain/admin_ops.py の遷移表 |
| 採番が更新に化けない | domain/minting.py の単一 INSERT 経路 |
| 到達範囲は登録の属性 | domain/authz.py(判断は 1 か所) |
| 人と機械の別 | auth/apikey.py と auth/login.py の subject_type 検査 |
いずれも、外すと落ちるテストが付いている。tests/test_models.py と
tests/test_authz.py を参照。