Skip to content
niryuu edited this page May 25, 2026 · 1 revision

keyring.ts — API キー / ステージ管理シングルトン

src/lib/keyring.ts (137 行)。script タグから API キーを抽出して保持するシングルトン。embed 全体で 1 つのインスタンスを共有する。

公開 API

class Keyring {
  get apiKey(): string;
  set apiKey(key: string);
  get stage(): string;
  set stage(stage: string);
  get isGeoloniaStyle(): boolean;
  set isGeoloniaStyle(value: boolean);
  public parse(doc: Document = document): void;
  public reset(): void;
  public isGeoloniaStyleCheck(style: string): boolean;
}

const keyring = new Keyring();
export { keyring };

インスタンス状態 (keyring.ts:1-4)

class Keyring {
  #apiKey: string;
  #stage: string;
  #isGeoloniaStyle: boolean;
}

ECMAScript private field (#) で内部保護。

getter / setter

apiKey (keyring.ts:9-14, 30-32)

get apiKey() {
  if (!this.#apiKey) {
    this.parse();
  }
  return this.#apiKey;
}
set apiKey(key: string) {
  this.#apiKey = key;
}

遅延 parse: getter 呼び出し時に #apiKey が未設定なら自動で parse() を実行。

stage (keyring.ts:36-47)

get stage() {
  if (!this.#stage) {
    this.parse();
  }
  return this.#stage;
}
set stage(stage: string) {
  this.#stage = stage;
}

同様に遅延 parse。

isGeoloniaStyle (keyring.ts:18-26)

get isGeoloniaStyle() {
  return this.#isGeoloniaStyle;
}
set isGeoloniaStyle(value: boolean) {
  this.#isGeoloniaStyle = value;
}

getter は parse を起動しない。明示的にセットされていない場合は undefined を返す。 セットは parse-atts.ts:31 で行う。

parse(doc: Document = document) (keyring.ts:54-86)

script タグから API キーを抽出する唯一の方法

フロー

const scripts: HTMLScriptElement[] | HTMLCollectionOf<HTMLScriptElement> =
  doc.currentScript
    ? [doc.currentScript as HTMLScriptElement]
    : doc.getElementsByTagName('script');
  • document.currentScript があればそれだけを対象 (= 現在実行中の script)
  • なければ全 <script> タグを走査
for (const script of scripts) {
  const url = new URL(
    script.src.startsWith('https://') ||
    script.src.startsWith('http://') ||
    script.src.startsWith('//')
      ? script.src
      : `https://${location.host}/${script.src}`,
  );
  const apiKey = url.searchParams.get('geolonia-api-key');

  if (apiKey) {
    this.#apiKey = apiKey;
    this.#stage = process.env.MAP_PLATFORM_STAGE || 'dev';
    break;
  }
}
  • script の srchttps://, http://, // で始まらなければ https://{host}/{src} の絶対 URL に展開
  • URL の ?geolonia-api-key= パラメータが API キー
  • 最初に見つかった script で break (複数 script が embed.js を読んでいても 1 つ目だけ採用)
if (!this.#stage) {
  this.#stage = 'dev';
}

if (this.#isGeoloniaStyle && !this.#apiKey) {
  throw new Error('Cannot load API key.');
}
  • API キーが見つからなくても stage は 'dev' で fallback
  • ただし #isGeoloniaStyle === true かつ #apiKey 未取得ならここで throw

: process.env.MAP_PLATFORM_STAGE は webpack DefinePlugin でビルド時に静的置換される (webpack.config.js:7)。npm scripts (package.json:18,21,22) は MAP_PLATFORM_STAGE=v1 を渡しているので、npm startnpm run build でビルドしたバンドルは 'v1' 固定。それ以外 (例えば直接 webpack コマンド) なら 'dev'

parse() の冪等性

#apiKey がすでにセットされていても、再度呼ぶと再パースして上書きする。getter 経由の遅延 parse は !#apiKey ガードがあるので 1 回だけ。

しかし render.ts:21parse-atts.ts:27毎回呼ばれるので、実際には毎マップ初期化で再パースが走る (script タグの内容が変わらなければ結果同じだが、冗長)。

reset() (keyring.ts:89-93)

public reset() {
  this.#apiKey = undefined;
  this.#stage = undefined;
  this.#isGeoloniaStyle = false;
}

テスト用。プロダクションコードからは呼ばない。keyring.test.ts で各テスト前に呼んでいる。

isGeoloniaStyleCheck(style: string): boolean (keyring.ts:102-131)

スタイル文字列が Geolonia 内部スタイル (= API キー必須) かどうかを判定。

public isGeoloniaStyleCheck(style: string): boolean {
  if (!style || style === '') {
    return true;  // デフォルトは Geolonia
  }

  if (style.startsWith('https://cdn.geolonia.com/style/') ||
      style.startsWith('https://api.geolonia.com/')) {
    return true;
  }

  if (style.match(/^https?:\/\//)) {
    return false;  // 外部 URL
  }

  if (style.endsWith('.json')) {
    try {
      const absoluteUrl = new URL(style, location.href).href;
      return absoluteUrl.includes('geolonia.com');
    } catch {
      return false;
    }
  }

  return true;  // 論理名扱い (例: 'geolonia/basic')
}

5-way 分岐

  1. 空文字列 → Geolonia (デフォルトスタイル想定)
  2. https://cdn.geolonia.com/style/ または https://api.geolonia.com/ で始まる → Geolonia
  3. http(s):// で始まる (上記 2 以外) → 外部
  4. .json で終わる (絶対 URL ではない、つまり相対パス) → resolve して geolonia.com を含むか確認
  5. 上記すべて該当しない → Geolonia 論理名 (例: 'geolonia/basic', 'foo/bar')

境界条件の罠

  • './styles/foo.json' → 相対パス。new URL('./styles/foo.json', location.href)location.host 由来の URL になる。location.hostgeolonia.com を含むなら Geolonia 判定、それ以外なら外部
  • '/style.json' → 同じく相対 (絶対パス)。location.host 依存
  • '' (空文字列) → Geolonia (1 で early return)
  • 'foo' (single token) → Geolonia (5 にフォールスルー)
  • 'foo/bar/baz' → Geolonia (URL とみなさない)
  • 'geolonia.com/foo' (スキームなし) → Geolonia (5 にフォールスルー、URL チェックはスキーム必須)

グローバル状態と寿命

シングルトン (keyring.ts:134):

const keyring = new Keyring(); // Singleton
export { keyring };
  • モジュールロード時に 1 つだけ生成される
  • import するすべての場所で同じインスタンスを共有
  • ページのライフタイム全期間で持続
  • ページリロードでリセット

Web Worker 等の別 realm では別インスタンスになる。embed コードは Web Worker で動作することを想定していないので問題ないが、書き直し時に Worker 対応するなら考慮要。

呼び出し関係

呼び出し元 用途
render.ts:21 renderGeoloniaMap 開始時に keyring.parse()
parse-atts.ts:27 parseAtts 内で keyring.parse(container) (container 引数は誤用、Module-Parse-Atts 参照)
parse-atts.ts:31 keyring.isGeoloniaStyle = keyring.isGeoloniaStyleCheck(style)
parse-atts.ts:58,59 keyring.apiKey, keyring.stage getter
util.ts:59 checkPermission 内で keyring.apiKey チェック
util.ts:219 getStyle 内で keyring.isGeoloniaStyleCheck(style) 呼び出し
render.ts:90 keyring.isGeoloniaStyle && !keyring.apiKey 警告判定

エラーケース

  1. API キー必須シーンで未取得:
    • parse() 内で #isGeoloniaStyle === true && !#apiKeythrow new Error('Cannot load API key.') (keyring.ts:84)
    • stack trace が parse 呼び出し元になるので、原因 (どの map / どの DOM 要素か) が特定しにくい
  2. isGeoloniaStyle が未設定で apiKey 必須:
    • parse() の throw を回避するため、parse-atts.ts:31isGeoloniaStyle セットが必須
    • parse-atts より先に keyring.apiKey を直接読むコードがあると意図しない state で parse される

怪しい点 (詳細は Suspicious-Logic)

  • isGeoloniaStyleCheck の 5-way 分岐の境界条件
  • parse-atts.ts:27keyring.parse(container) 引数誤用
  • throw 時の stack trace 不明瞭性
  • stage の build-time 固定

書き直し時の指針

  • maps-core (lib) 側に置く
  • ただし parse() が DOM を走査するので、lib として "pure" にするなら parse(scripts: HTMLScriptElement[]) のように引数で受ける形に
  • 自動 parse は wrapper 側で keyring.parse(document.getElementsByTagName('script')) を呼ぶ
  • MAP_PLATFORM_STAGE の build-time 注入はランタイム可変化を検討 (lib の汎用性のため)
  • isGeoloniaStyleCheck のロジックは utility 関数として独立化

Clone this wiki locally