Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/design/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@

| クラス | 対象 | 担い手 |
|---|---|---|
| 機械的(構造的担保) | 正規テキスト(S9)・SVG(S12)・`meta.id`=dirname(S11)・生成物/ソース乖離・サイズ上限・必須フィールド・sha512 整合・ホスト関数リテラル限定 best-effort | CI(`scripts/check-registry-integrity.mjs`) |
| 機械的(構造的担保) | 正規テキスト(S9)・SVG(S12)・ID=dirname 全種別(S11)・storeId 形式 + 全 kind 横断一意 + テーマ内部 ID 一意(notedeck#913)・生成物/ソース乖離・サイズ上限・必須フィールド・sha512 整合・ホスト関数リテラル限定 best-effort | CI(`scripts/check-registry-integrity.mjs`) |
| 論理(人手必須) | ロジックボム・権限と説明の釣り合い・第2引数の宛先・config 駆動の引数 | 人間(CI が候補フラグを提示) |

- Tier は種別でなく**導出されるホスト関数**で決める。`write:*` / `Nd:http` /
Expand Down
15 changes: 13 additions & 2 deletions docs/registry-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,21 @@ public/registry/
frontmatter から自動生成されます。`api.json` などの生成物はコミットせず、ビルドで
生成します(レビュー対象とソースを一致させるため。[security.md](design/security.md) S1)。

> **ID はディレクトリ名と一致必須。** `meta.id` を書く場合はディレクトリ名と完全に
> 一致させてください(不一致は既存アイテム乗っ取りの温床として CI が reject します)。
> **ID はディレクトリ名と一致必須。** `meta.id`(スキルは frontmatter の `id`)は
> ディレクトリ名と完全に一致させてください(不一致は既存アイテム乗っ取りの温床として
> CI が reject します)。
> `createdAt`/`updatedAt` は `meta.json` に書かず、git 履歴から自動採取されます。

ディレクトリ名(storeId)は NoteDeck 側でローカル同一性の正準リンクになるため
(notedeck#913)、CI(`scripts/check-registry-integrity.mjs`)が次を機械検査します。

- 形式は `^[a-z0-9-]{1,48}$`(小文字英数とハイフンのみ、48 文字以内)。
Windows 予約デバイス名(`con` / `prn` / `aux` / `nul` / `com1`-`com9` /
`lpt1`-`lpt9`)は不可
- ID は種別をまたいでレジストリ全体で一意(同じ ID を plugins と skills で
使い回すことはできない)
- テーマの `theme.json5` 内部 `id` はテーマ間で一意(欠損は許容、重複は reject)

### エントリの URL フィールド

- `sourceUrl` — 生ソース(`plugin.is` / `theme.json5` / `widget.is` / `skill.md` /
Expand Down
81 changes: 79 additions & 2 deletions scripts/check-registry-integrity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'
import { resolve, join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import JSON5 from 'json5'

const REGISTRY_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'public', 'registry')

Expand All @@ -25,9 +26,20 @@ const KINDS = {

const MAX_SOURCE_BYTES = 500 * 1024 // S2: サイズ上限 500KB

// notedeck#913: storeId(ディレクトリ名)はローカル同一性の正準リンクになるため
// 形式を機械保証する。小文字英数とハイフンのみ、48 文字以内。
const STORE_ID_RE = /^[a-z0-9-]{1,48}$/
// Windows 予約デバイス名。ローカル展開時にファイル/ディレクトリ名として使えない。
const WINDOWS_RESERVED_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/

const errors = []
const warnings = []

// notedeck#913: レジストリ全体(全 kind 横断)の ID 重複検査用
const idOwners = new Map() // storeId → ['kind/id', ...]
// notedeck#913: テーマ内部 ID(theme.json5 の id)の一意性検査用
const themeInternalIds = new Map() // 内部 id → [storeId, ...]

// S9: 見た目とパーサ解釈を乖離させる不可視/制御文字。
// bidi 制御は無条件 reject(Trojan Source, CVE-2021-42574)。
const BIDI_CONTROLS = /[‪-‮⁦-⁩]/
Expand Down Expand Up @@ -73,12 +85,37 @@ function checkSvg(label, svg) {
}
}

// skill.md の YAML frontmatter から id を取り出す。
// build-registry.js の parseFrontmatter と同じく浅い frontmatter のみ想定。
function frontmatterId(text) {
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/)
if (!m) return null
for (const line of m[1].split(/\r?\n/)) {
const kv = line.match(/^id\s*:\s*(.*)$/)
if (!kv) continue
const v = kv[1].trim()
return /^".*"$/.test(v) || /^'.*'$/.test(v) ? v.slice(1, -1) : v
}
return null
}

for (const [kind, sourceName] of Object.entries(KINDS)) {
const kindDir = join(REGISTRY_DIR, kind)
for (const id of scanDirs(kindDir)) {
const itemDir = join(kindDir, id)
const label = `${kind}/${id}`

// notedeck#913: storeId(ディレクトリ名)の形式検査
if (!STORE_ID_RE.test(id)) {
errors.push(`[${label}] storeId(ディレクトリ名)が不正 — ^[a-z0-9-]{1,48}$ に一致しない`)
} else if (WINDOWS_RESERVED_RE.test(id)) {
errors.push(`[${label}] storeId が Windows 予約デバイス名(${id})`)
}

// notedeck#913: 全 kind 横断の ID 重複検査(収集。判定はループ後)
if (!idOwners.has(id)) idOwners.set(id, [])
idOwners.get(id).push(label)

// 主ソース
const sourcePath = join(itemDir, sourceName)
if (!existsSync(sourcePath)) {
Expand All @@ -92,9 +129,20 @@ for (const [kind, sourceName] of Object.entries(KINDS)) {
}
}

// meta.json(skills は持たない)
// meta.json(skills は持たず skill.md の frontmatter が同じ役割)
const metaPath = join(itemDir, 'meta.json')
if (kind !== 'skills') {
if (kind === 'skills') {
// S11 / notedeck#913: skills も frontmatter の id = ディレクトリ名を強制。
// ここを素通しすると skills だけアイテム乗っ取り経路が残る。
if (existsSync(sourcePath)) {
const fmId = frontmatterId(readFileSync(sourcePath, 'utf-8'))
if (fmId == null) {
errors.push(`[${label}] skill.md の frontmatter に id が無い`)
} else if (fmId !== id) {
errors.push(`[${label}] frontmatter の id (${fmId}) がディレクトリ名 (${id}) と不一致 — アイテム乗っ取りの温床`)
}
}
} else {
if (!existsSync(metaPath)) {
errors.push(`[${label}] meta.json が無い`)
} else {
Expand All @@ -118,6 +166,22 @@ for (const [kind, sourceName] of Object.entries(KINDS)) {
}
}

// notedeck#913: テーマ内部 ID(theme.json5 の id)の収集(判定はループ後)。
// id 欠損は現状データで許容されているため fail にせず、重複のみ弾く。
if (kind === 'themes' && existsSync(sourcePath)) {
let theme
try {
theme = JSON5.parse(readFileSync(sourcePath, 'utf-8'))
} catch {
errors.push(`[${label}] theme.json5 が JSON5 として解釈できない`)
theme = {}
}
if (theme.id != null) {
if (!themeInternalIds.has(theme.id)) themeInternalIds.set(theme.id, [])
themeInternalIds.get(theme.id).push(id)
Comment on lines +172 to +181

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Trace top-level theme parsing and subsequent property access.
rg -n -C 10 'JSON5\.parse|theme\.id|themeData' \
  scripts/check-registry-integrity.mjs scripts/build-registry.js

# Confirm that the JSON5 grammar accepts null as a value.
curl -fsSL https://spec.json5.org/ | rg -n -i -C 2 'null|JSON5Value'

Repository: notedeck-dev/misstore

Length of output: 23721


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the checker control flow and its command entry point.
sed -n '1,230p' scripts/check-registry-integrity.mjs
printf '\n--- package scripts ---\n'
rg -n -C 3 'check-registry-integrity|build-registry' package.json README.md .github 2>/dev/null || true

# Probe the relevant JavaScript property-access behavior without executing repository code.
node - <<'JS'
for (const theme of [null, [], {}, 'text', 0, false]) {
  try {
    console.log(JSON.stringify({ value: theme, id: theme.id, outcome: 'no throw' }))
  } catch (error) {
    console.log(JSON.stringify({ value: theme, outcome: error.name, message: error.message }))
  }
}
JS

Repository: notedeck-dev/misstore

Length of output: 8526


非 object の theme.json5 をエラーとして処理してください。

JSON5.parse はトップレベルの null、配列、プリミティブ値を受理します。null の場合、theme.idTypeError を発生させ、CI チェックが終了します。

トップレベル値が object でない場合は integrity error を追加し、theme.id を参照しないでください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-registry-integrity.mjs` around lines 172 - 181, Validate the
parsed result in the theme-loading flow before accessing theme.id: when
JSON5.parse returns null, an array, or any primitive rather than an object, add
an integrity error and skip ID processing. Keep the existing parse-error
handling and only execute the themeInternalIds logic for object values.

}
}

// icon.svg(任意)
const iconPath = join(itemDir, 'icon.svg')
if (existsSync(iconPath)) {
Expand All @@ -128,6 +192,19 @@ for (const [kind, sourceName] of Object.entries(KINDS)) {
}
}

// notedeck#913: レジストリ全体(全 kind 横断)で storeId は一意
for (const [id, owners] of idOwners) {
if (owners.length > 1) {
errors.push(`[registry] ID "${id}" が重複: ${owners.join(', ')} — storeId はレジストリ全体で一意`)
}
}
// notedeck#913: テーマ内部 ID の一意性
for (const [tid, dirs] of themeInternalIds) {
if (dirs.length > 1) {
errors.push(`[themes] theme.json5 の内部 ID "${tid}" が重複: ${dirs.join(', ')}`)
}
}

for (const w of warnings) console.warn(`WARN ${w}`)
for (const e of errors) console.error(`ERROR ${e}`)

Expand Down
Loading