Web Scratch 2025-11-16T10:11:13+09:00 https://efcl.info/ azu YAPC::Fukuoka 2025で「読む技術・書く技術・伝える技術 - 15年続けて分かった持続可能なオープンソース開発」という発表をしました 2025-11-15T15:15:00+09:00 https://efcl.info/2025/11/15/yapc-fukuoka <![CDATA[

YAPC::Fukuoka 2025で「読む技術・書く技術・伝える技術 - 15年続けて分かった持続可能なオープンソース開発」というタイトルで発表をしました。

スライドは次のページで公開しています。

発表内容

この発表では、15年間のオープンソース活動から学んだ持続可能な開発のための3つのプロジェクトについて話しています。

読む技術 - JSer.info

JSer.infoは2011年から続けている週刊のJavaScript情報ブログで、これまでに750記事以上を公開しています。 「整理されたデータである『情報』を伝えること」をテーマに、14年間続けてきた情報収集システムやワークフローについて紹介しています。

関連記事:

書く技術 - textlint

textlintは、自然言語のLintツールで、2014年から開発を続けています。 現在では200以上のルールを持つエコシステムになっており、AI時代における文章品質の自動化についても触れています。

関連記事:

伝える技術 - JavaScript Primer

JavaScript Primerは、2016年から開発を続けているJavaScriptの入門書です。 「変化に対応できること」をテーマに、最初からLiving Standard戦略や長期的な運用を目的として設計されました。 100人以上のコントリビューターが参加しており、どのように継続的なメンテナンスとコミュニティの参加を実現しているかについて話しています。

関連記事:

持続可能性のループ

発表のテーマは「心理的負荷を技術的依存に転換する」という考え方です。

バーンアウトは期待と現実のギャップから生じます。心理的負荷はコントロールが難しく、バーンアウトのリスクを高めます。 一方で、技術的依存は自動化が可能で、継続することで改善が加速し、コントロールがしやすいです。

burnout

The End of Burnout by Jonathan Malesic - Paper - University of California Press

そもそも持続可能性を気にしているのは、それぞれのプロジェクトがアウトプット(作ったもの)ではなくアウトカム(実際の影響・信頼・教育)を目指してやっているからです。長期的な視点でプロジェクトを継続するにコントロールが難しい心理的負荷を意識的に減らす工夫について話しました。

スライド作成プロセス

今回のスライドは、Claude Desktopで「こんなテーマにしたい、過去の自分の公開したものとかを調べてまとめてみて」という形でDeep Researchした結果を元に構成しました。 その後、大まかなスライドで発表練習をして構造的な問題もしゃべりながら音声ファイルを文字起こしし、それを元にスライドを修正するというのを2回ぐらいやって構造を固めました。(若干テーマが持続的開発に変わったのはこの辺のイテレーションで変わった)

構造が固まったら、Claude Desktopのプロジェクトをファイルにdumpして、Claude Codeでスライドを書いていくというフローで作成しました。

この辺は、過去にPublicに書いてきたことをAIが読んである程度書いたものを人間が整理して伝えるということができて結構面白かったです。

おわりに

JSer.infoは趣味の延長として始まったプロジェクトですが、textlintやJavaScript Primerは最初から長期的な運用を見据えて設計しました。 それぞれのプロジェクトが9年や11年や14年と続けてこられたのは、持続可能性を意識した設計と、コミュニティの支援があったからです。

スライドのAppendixに大量の参考リンクとかがあるので、興味ある人はぜひ見てみてください。

オープンソースの活動は色々な形があるので、JSer.info、textlint、jsprimerに限らず、何かしらの形で関わってみると面白いかもしれません。

GitHub Sponsorsでの支援も大歓迎です!

YAPC::Fukuoka 2025 お疲れ様でした!

]]>
ni.zsh v1.8.0リリース: Socket Firewallによるパッケージインストール時のチェック機能を追加 2025-10-08T23:00:00+09:00 https://efcl.info/2025/10/08/ni.zsh-v1.8.0-socket-firewall <![CDATA[

npm/yarn/pnpm/bunを同じコマンドで扱えるni.zshのv1.8.0をリリースしました。

このバージョンでは、Socket Firewallを統合し、パッケージのインストールと実行時にサプライチェーン攻撃から保護する機能を追加しました。

ni.zshについては、次の記事を参照してください。

ni.zsh v1.8.0の変更点

主要な変更点は次の通りです。詳細はリリースノートを参照してください。

🛡️ Socket Firewallの統合

Socket Firewallを統合し、パッケージのインストール時にセキュリティスキャンを行えるようになりました。

Socket Firewallは、HTTPプロキシとしてパッケージマネージャのネットワーク通信をインターセプトし、悪意のあるパッケージがダウンロードされる前にブロックするツールです。

従来のni.zshでは、Socket.dev APIを使ってリスクスコアを取得し、インストール前に確認を求める仕組みでした。 Socket Firewallも基本的には同じで、自動的にインストール時にマルウェアならブロックし、潜在的なマルウェアは警告を表示します。

詳しくは次の記事を参照してください。

セットアップ方法

Socket Firewallの機能を使用するには、次の手順でセットアップします。

  1. Socket Firewallをグローバルにインストール
npm i -g sfw
  1. .zshrc に環境変数を追加
export NI_USE_SOCKET_FIREWALL=1

設定後、次のコマンドでSocket Firewallによる保護が有効になります。

保護対象のコマンド

次のコマンドでSocket Firewallによる保護が有効になります。

  • ni / ni add <pkg> - パッケージのインストール
  • ni exec <cmd> / ni dlx <pkg> - パッケージの実行

ni.zshでは、パッケージをインストールする可能性があるコマンドのみを選択的にSocket Firewallを通すように実装されています。これにより、alias npm="sfw npm"のような全コマンドをプロキシする方法と比べて、余計なSocket Firewallのログが出力されず、必要な時だけ保護機能が動作します。

# パッケージインストール時に保護
ni add express

# 直接実行する場合も保護
ni dlx create-vite

マルウェアパッケージがブロックされると、次のように表示されます。

=== Socket Firewall ===
- Blocked npm package: name: lodahs; version: 1.0.0

カスタムバイナリパスの指定

デフォルトではsfwコマンドを使用しますが、カスタムのSocket Firewallバイナリパスを指定できます。

export NI_SOCKET_FIREWALL_BIN=/path/to/custom/sfw

なぜSocket Firewallを統合したか

最近のnpmエコシステムでは、サプライチェーン攻撃が急増しています。

過去にはevent-stream攻撃のような稀な事例でしたが、2024年から2025年にかけて次のような有名パッケージの侵害が相次いで発生しています。

  • tinycolor - 40以上のパッケージが侵害
  • chalk - ブラウザで実行するタイプのマルウェア
  • nx - AI Agentを使ってローカルの認証情報を盗み取る攻撃

これらの攻撃では、メンテナのアカウントが乗っ取られたり、マルウェアが連鎖的にマルウェアとなるnpmパッケージを公開しています。

Socket Firewallによる多層防御

Socket Firewallは、HTTPプロキシとしてネットワーク通信をインターセプトし、既知のマルウェアパッケージを自動的にブロックします。

これはnpmパッケージ/GitHub Actionsを利用する側/公開する側でサプライチェーン攻撃を防ぐためにやることメモで書いていた多層防御の一環として位置づけられます。

  1. ロック: version pinning / lockfile固定
  2. 事前スキャン: Socket.dev等で既知リスク検出
  3. インストール: Lifecycle Scriptsの制御、minimumReleaseAge
  4. アップデート: Renovate/Dependabotのcooldown(7日)
  5. 検証・改ざん検出: 署名/checksum/lockfile整合性確認

Socket Firewallは、この多層防御の「事前スキャン」段階の保護をするイメージです。

ni.zshは複数のパッケージマネージャ(npm、yarn、pnpm、bun)をラップするため、どのパッケージマネージャを使用していてもSocket Firewallによる保護を受けられます。

次のようにそれぞれのパッケージマネージャをsfwでラップするエイリアスするのと大体似たような動作になります。

alias yarn="sfw yarn"
alias pnpm="sfw pnpm"
alias npm="sfw npm"
alias bun="sfw bun"
alias npx="sfw npx"

Socket.dev APIとの違い

ni.zshでは、以前からSocket.dev APIを使ったマルウェア検知機能を提供していました。

Socket.dev APIは、パッケージのリスクスコアを取得して、インストール前に確認を求める仕組みでした。

基本的な機能性は同じですが、Socket Firewallは次のような違いがあります。

機能 Socket.dev API Socket Firewall
検知方法 APIでリスクスコアを取得 HTTPプロキシでネットワーク通信をインターセプト
保護レベル インストール前に確認を求める 既知のマルウェアは自動的にブロック
セットアップ APIキーが必要 APIキー不要(無料)
対応パッケージマネージャ npm/yarn/pnpm/bun npm/yarn/pnpm/pip/uv/cargo

Socket Firewallは無料で使えて、APIキーも不要なため、よりシンプルに使えるようになりました。

なお、従来のSocket.dev API機能は削除しました。警告が出ても意識的にチェックしない場合が多く、ブロックするか通すかの明確な判断の方が実用的だと考えたためです。もしSocket Firewallが間違ってパッケージをブロックした場合は、直接npmyarnなどのコマンドを使用することで回避できます。

まとめ

ni.zsh v1.8.0では、Socket Firewallを統合してパッケージのセキュリティ保護機能を追加しました。

Socket Firewallを有効にするには、sfwをインストールしてNI_USE_SOCKET_FIREWALL=1を設定するだけです。

フィードバックがありましたら、GitHubのIssueでお知らせください。

]]>
npm Trusted PublishingでOIDCを使ってトークンレスでCIからnpmパッケージを公開する 2025-09-07T11:00:00+09:00 https://efcl.info/2025/09/07/npm-oidc <![CDATA[

npm Trusted Publishingが2025年7月31日に一般公開されました。 これにより、OpenID Connect (OIDC)を使ってnpmトークンなしでCI/CDからnpmパッケージを公開できるようになりました。

この記事では、npm Trusted Publishingの仕組みや設定方法、実際のリリースフローについて紹介します。

npm Trusted Publishingとは

npm Trusted Publishingは、npmレジストリとCI/CD環境(GitHub ActionsやGitLab CI/CD)の間でOIDCベースの信頼関係を確立する仕組みです。これにより、npmトークンを使わずにパッケージを公開できます。

これまでのCIからnpmパッケージを公開する際には、長期間有効なnpmトークンをCI/CDの環境変数(Secrets)に保存する必要がありました。しかし、このアプローチにはいくつかのセキュリティリスクがあります。

  • npmトークンがCIのログや設定ファイルに誤って露出する可能性
  • トークンが侵害された場合、無効化するまで悪用される可能性
  • トークンの手動ローテーションが必要
  • 必要以上に広い権限を持つことが多い
    • granular access tokens で scope を制限できるが、monorepo では設定/維持が難しく org 単位になりがち

npm Trusted Publishingは、これらの問題を解決します。 短時間だけ有効で、かつ特定ワークフローに限定された署名付きトークンを使うため、流出しても再利用されにくい構造になっています。

対応CI/CD環境

現在、npm Trusted Publishingは次のCI/CD環境をサポートしています。

  • GitHub Actions(GitHub-hosted runners)
  • GitLab CI/CD(GitLab.com shared runners)

設定方法

npm Trusted Publishingの設定は大きく2つのステップで構成されます。

1. npmjs.comでTrusted Publisherを設定

まず、npmjs.comのパッケージ設定でTrusted Publisherを設定します。

  1. npmjs.comのパッケージ設定ページを開く
  2. “Trusted Publisher”セクションを見つける
  3. “Select your publisher”でCI/CDプロバイダーを選択

GitHub Actionsの場合は次のような設定をします。

  • Organization or user (必須): GitHubのユーザー名または組織名
  • Repository (必須): リポジトリ名
  • Workflow filename (必須): ワークフローのファイル名(例: publish.yml
    • ファイル名のみを入力(フルパスではない)
    • .ymlまたは.yaml拡張子を含める必要がある
    • ワークフローファイルは.github/workflows/内に存在する必要がある
  • Environment name (オプション): GitHub environmentsを使用する場合

2. CI/CDワークフローの設定

GitHub Actionsの設定例

ワークフローにOIDC権限 id-token: write を追加します。例えば、次のようなワークフローを設定すると、タグをプッシュしたときにnpmパッケージを公開できます。

name: Publish Package
on:
  push:
    tags:
      - 'v*'

permissions:
  id-token: write  # OIDCに必要
  contents: read

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: 'https://registry.npmjs.org'
      
      # npm 11.5.1以降が必要
      - name: Update npm
        run: npm install -g npm@latest
        
      - run: npm ci
      - run: npm run build --if-present
      - run: npm test
      - run: npm publish

重要なのはid-token: write権限とnpm 11.5.1以降を使用することです。これにより、GitHub Actionsがnpmに対して一時的な署名付きトークンを発行できるようになります。

  • 追記 Node.js v24.8.0でnpm v11.6.0が同梱されるようになりました

:memo: pnpmのpnpm publishコマンドはnpm publishに処理を移譲しているため、pnpmを使う場合もnpm 11.5.1以降が必要です。

実際のリリースフロー

自分が実際に運用している単独パッケージとmonorepoパッケージでのリリースフローを紹介します。

単独パッケージのパターン

新規パッケージのOIDCの準備

新規パッケージでは、OIDCはパッケージが一度公開されていないと設定できません。 そこで、この問題を解決するためにsetup-npm-trusted-publishというツールを作成しました。

READMEのみを含む初期パッケージを一度公開し、OIDC設定をするための下準備を自動化するツールです。 次のコマンドでパッケージを公開できるので、作成したパッケージにTrusted Publisherを手動で行います。

npx setup-npm-trusted-publish <パッケージ名>

実際のパッケージ例。

初めての公開後に、実際にCIからのPublishができるようになります。

既存パッケージのOIDCの準備

すでに一度でも公開済みであれば、そのパッケージのnpmjs.com設定画面から直接Trusted Publisherを追加できます。初期公開用の空パッケージを別途用意する手順は不要です。

CIからのPublishの実装

実運用では次の2つのWorkflowに分けています。

  1. create-release-pr.yml: リリースPRを作るだけ(publishしない)
  2. release.yml: リリースPRがマージされたら publish して GitHub Release を作る。npm 側の Trusted Publisher にはこの release.yml だけを登録する。

「PRでリリースノートを確認/編集 → マージで公開」というシンプルな二段構成です。

npmjs.comのTrusted Publisher設定では release.yml のみを指定します。create-release-pr.yml は公開権限を持たないリリースPRを作るだけのワークフローです。

そのため、user/example-pkgリポジトリで @user/example-pkgのTrusted Publisher設定は次のようになります。

  • 設定場所: https://www.npmjs.com/package/@user/example-pkg/settings/access
  • Organization or userには user を設定
  • Repositoryには example-pkg を設定
  • Workflow filenameには release.yml を設定
  • Environment name (オプション): なし

1Passwordを使った設定の効率化

Trusted Publisherの設定はnpmjs.comでの手動の作業が必要です。 手動設定でのミスを防ぐため、1Passwordのautofill機能を活用しています。

  1. 1Passwordで先にTrusted Publisherの設定項目を作成(リポジトリの情報を元にopコマンドで生成しておく)
  2. npmの設定画面で1Passwordのautofillを使用して自動入力

Trusted Publisherの設定は項目が多く間違いやすく、設定ミスを減らすことが目的です。 手入力をなくすことで、設定ミスを減らせます。

1. create-release-pr.yml の流れ

create-release-pr.ymlは手動でリリースPRを作成するワークフローです。 実行時に、アップデートするバージョンの種類(patch / minor / major)を選択します。

次のようなステップで動作します。

  1. セットアップ
  2. 依存関係インストールなしでバージョンだけ上げる(npm version 1.2.3 --no-git-tag-version 相当)
  3. 直前のリリースタグを取得
    • ある: releases/generate-notes API で差分ベースのリリースノートを生成
    • ない: 初回はテンプレ最小文言
  4. release/vX.Y.Z ブランチを作成し package.json (+ lockfile) をコミット
  5. DraftなPRを作成(ラベル: Type: Release
  6. PR本文に自動生成ノートを詰め、編集可能な状態で人間レビュー

create-release-pr.ymlは次のようなことを考慮しています。

  • GITHUB_TOKEN 権限でPRを作るため、リポジトリ設定で「Actions can create and approve PRs」をONにしておく
  • バージョン更新時に install しないので高速・副作用少なめ
  • GitHub Release Notes API で自動的に前回のリリースからの差分を取得

create-release-pr.yml は次のような内容です。

name: Create Release PR

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Version type'
        required: true
        type: choice
        options:
          - patch
          - minor
          - major

jobs:
  create-release-pr:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - name: Checkout
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          persist-credentials: false
      
      - name: Configure Git
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
      
      
      - name: Setup Node.js
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
        with:
          node-version: 'lts/*'
      
      # No need to install dependencies - npm version works without them
      - name: Version bump
        id: version
        run: |
          npm version "$VERSION_TYPE" --no-git-tag-version
          VERSION=$(jq -r '.version' package.json)
          echo "version=$VERSION" >> $GITHUB_OUTPUT
        env:
          VERSION_TYPE: ${{ github.event.inputs.version }}
      
      - name: Get release notes
        id: release-notes
        run: |
          # Get the default branch
          DEFAULT_BRANCH=$(gh api "repos/$GITHUB_REPOSITORY" --jq '.default_branch')
          
          # Get the latest release tag using GitHub API
          # Use the exit code to determine if a release exists
          if LAST_TAG=$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq '.tag_name' 2>/dev/null); then
            echo "Previous release found: $LAST_TAG"
          else
            LAST_TAG=""
            echo "No previous releases found - this will be the first release"
          fi
          
          # Generate release notes - only include previous_tag_name if we have a valid previous tag
          echo "Generating release notes for tag: v$VERSION"
          if [ -n "$LAST_TAG" ]; then
            echo "Using previous tag: $LAST_TAG"
            RELEASE_NOTES=$(gh api \
              --method POST \
              -H "Accept: application/vnd.github+json" \
              "/repos/$GITHUB_REPOSITORY/releases/generate-notes" \
              -f "tag_name=v$VERSION" \
              -f "target_commitish=$DEFAULT_BRANCH" \
              -f "previous_tag_name=$LAST_TAG" \
              --jq '.body')
          else
            echo "Generating notes from all commits"
            RELEASE_NOTES=$(gh api \
              --method POST \
              -H "Accept: application/vnd.github+json" \
              "/repos/$GITHUB_REPOSITORY/releases/generate-notes" \
              -f "tag_name=v$VERSION" \
              -f "target_commitish=$DEFAULT_BRANCH" \
              --jq '.body')
          fi
          
          # Set release notes as environment variable
          echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
          echo "$RELEASE_NOTES" >> $GITHUB_ENV
          echo "EOF" >> $GITHUB_ENV
        env:
          GH_TOKEN: ${{ github.token }}
          VERSION: ${{ steps.version.outputs.version }}
          GITHUB_REPOSITORY: ${{ github.repository }}

      - name: Create Pull Request
        uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
        with:
          branch: release/v${{ steps.version.outputs.version }}
          delete-branch: true
          title: "Release v${{ steps.version.outputs.version }}"
          body: |
            ${{ env.RELEASE_NOTES }}
          commit-message: "chore: release v${{ steps.version.outputs.version }}"
          labels: |
            Type: Release
          assignees: ${{ github.actor }}
          draft: true
2. release.yml の流れ

release.ymlはリリースPRがマージされたら自動でnpm publishを実行するワークフローです。

release.ymlは次のようなステップで動作します。

  1. PRがマージされたか & Type: Releaseラベルがあるかを確認
  2. package.json の version からタグ名 vX.Y.Z を決定
  3. そのタグが既に存在したら(再実行 / リトライ)何もしないで終了 → 冪等性確保
  4. Nodeセットアップ + (必要なら)依存 install / build / test
  5. npm publish --provenance (npm 11.5.1+ が必要)
  6. GitHub Release 作成
  7. 成功/失敗を元の PR にコメント

release.yml は次のような内容です。

name: Release

on:
  pull_request:
    branches:
      - master
      - main
    types:
      - closed

jobs:
  release:
    if: |
      github.event.pull_request.merged == true &&
      contains(github.event.pull_request.labels.*.name, 'Type: Release')
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write  # OIDC
      pull-requests: write  # PR comment
    steps:
      - name: Checkout
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          persist-credentials: false
      
      - name: Get package info
        id: package
        run: |
          VERSION=$(jq -r '.version' package.json)
          PACKAGE_NAME=$(jq -r '.name' package.json)
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "name=$PACKAGE_NAME" >> $GITHUB_OUTPUT
      
      - name: Check if tag exists
        id: tag-check
        run: |
          if git rev-parse "v$VERSION" >/dev/null 2>&1; then
            echo "exists=true" >> $GITHUB_OUTPUT
          else
            echo "exists=false" >> $GITHUB_OUTPUT
          fi
        env:
          VERSION: ${{ steps.package.outputs.version }}
      
      
      - name: Setup Node.js
        if: steps.tag-check.outputs.exists == 'false'
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
        with:
          node-version: 'lts/*'
          registry-url: 'https://registry.npmjs.org'
      
      - name: Install latest npm
        if: steps.tag-check.outputs.exists == 'false'
        run: |
          echo "Current npm version: $(npm -v)"
          npm install -g npm@latest
          echo "Updated npm version: $(npm -v)"
      
      - name: Install dependencies
        if: steps.tag-check.outputs.exists == 'false'
        run: npm ci
      
      - name: Publish to npm with provenance
        if: steps.tag-check.outputs.exists == 'false'
        run: npm publish --provenance --access public
      
      - name: Create GitHub Release with tag
        id: create-release
        if: steps.tag-check.outputs.exists == 'false'
        run: |
          RELEASE_URL=$(gh release create "v$VERSION" \
            --title "v$VERSION" \
            --target "$SHA" \
            --notes "$PR_BODY")
          echo "url=$RELEASE_URL" >> $GITHUB_OUTPUT
        env:
          GH_TOKEN: ${{ github.token }}
          VERSION: ${{ steps.package.outputs.version }}
          SHA: ${{ github.sha }}
          PR_BODY: ${{ github.event.pull_request.body }}
      
      - name: Comment on PR - Success
        if: |
          always() && 
          github.event_name == 'pull_request' && 
          steps.tag-check.outputs.exists == 'false' &&
          success()
        run: |
          gh pr comment "$PR_NUMBER" \
            --body "✅ **Release v$VERSION completed successfully!**
          
          - 📦 npm package: https://www.npmjs.com/package/$PACKAGE_NAME/v/$VERSION
          - 🏷️ GitHub Release: $RELEASE_URL
          - 🔗 Workflow run: $SERVER_URL/$REPOSITORY/actions/runs/$RUN_ID"
        env:
          GH_TOKEN: ${{ github.token }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          VERSION: ${{ steps.package.outputs.version }}
          PACKAGE_NAME: ${{ steps.package.outputs.name }}
          RELEASE_URL: ${{ steps.create-release.outputs.url }}
          SERVER_URL: ${{ github.server_url }}
          REPOSITORY: ${{ github.repository }}
          RUN_ID: ${{ github.run_id }}
      
      - name: Comment on PR - Failure
        if: |
          always() && 
          github.event_name == 'pull_request' && 
          steps.tag-check.outputs.exists == 'false' &&
          failure()
        run: |
          gh pr comment "$PR_NUMBER" \
            --body "❌ **Release v$VERSION failed**
          
          Please check the workflow logs for details.
          🔗 Workflow run: $SERVER_URL/$REPOSITORY/actions/runs/$RUN_ID"
        env:
          GH_TOKEN: ${{ github.token }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          VERSION: ${{ steps.package.outputs.version }}
          SERVER_URL: ${{ github.server_url }}
          REPOSITORY: ${{ github.repository }}
          RUN_ID: ${{ github.run_id }}
リリースを2分割にしている理由

create-release-pr.ymlrelease.ymlを分けている理由は次のとおりです。

  • create-release-pr.ymlでバージョンを間違ってもやり直しやすい(リリースPRを閉じて再度作成できる)
  • リリースPRで、リリースノートを確認/編集できる
  • リリースPRで、Contributorにmentionが飛ぶのでどのリリースで変更が入るかがわかりやすい
  • create-release-prとreleaseで分けることで、release.ymlをリトライできる

基本的にはどちらもブラウザ上で完結するので、ブラウザだけでリリースができるようになっています。

リリース手順

実際のリリース手順は次のとおりです。

  1. GitHub ActionsでPRを作成できるようにする
  • リポジトリ設定で”Allow GitHub Actions to create and approve pull requests”を有効化する
  1. リポジトリのラベルを設定
  1. Create Release PRでリリースPRを作成
  • ワークフローが自動でリリースPRを作成する
  • create release pr
  1. リリースPRをレビューしてリリースノートを書く
  • PR Bodyがそのままリリースノートになる
  • デフォルトで前回PRからの差分CHANGELOGが含まれる
  • release pr
  1. リリースPRをマージ
  • マージ後にCIが自動でnpm publishを実行する
  1. 自動リリース完了
  • npmパッケージを公開する
  • PRにリリース結果をコメントする
  • release complete

実際のサンプルは次のURLです。

単体パッケージのパターンまとめ

OIDC連携の設定は結構面倒ですが、ここまでの設定は自動化したスクリプトを書いています。

setup-release-pr-with-oidc.tsでは次のようなことをやっています。

  1. パッケージマネージャ検出: package.jsonpackageManager フィールド、なければ lockfile を見て npm / pnpm / yarn を判定する
  2. ビルド有無の確認: package.jsonscripts.build の存在を確認する
  3. ワークフローディレクトリ生成: .github/workflows/ ディレクトリを作成する
  4. Release PR 作成用Workflow生成: create-release-pr.yml をテンプレートから生成する
  5. Publish 用Workflow生成: release.yml をテンプレートから生成し OIDC (id-token: write) と provenance 付き publish を含める
  6. GitHub Actionsバージョン更新: pinact run --update で pinned された action の最新版取得を試行する(失敗時は警告表示)
  7. 変更のコミット: git status で差分を確認し workflow ファイルをコミットする(既存ならスキップ)
  8. リポジトリ情報取得: git remote get-url origin から owner/name を抽出する
  9. 手動必要設定の案内: GitHub Actions の PR 作成権限設定を有効化するよう警告を表示する
  10. npm Trusted Publisher 設定ガイド表示: npm アクセス設定ページURLと入力すべき項目(owner / repo / workflow名)を整形して出力する
  11. 1Password 連携: 1Password CLI が利用可能なら Trusted Publisher 用の項目を検索し、なければフィールド付きで作成する
  12. 対話メニュー起動: Enter で npm 設定ページをブラウザで開く / S でサマリ表示 / C で初回リリースPR作成手順案内 / Q で終了する
  13. 初回リリース手順リマインド: NPM 設定完了後に gh workflow run create-release-pr を案内する

一部の設定(npm 側の Trusted Publisher 設定、GitHub Actions の権限変更、初回ワークフロー実行など)は手動作業です。しかし、基本的にそのままの流れ作業で、手入力はなしでできるようにしています。

monorepoのパターン

monorepoの場合はsecretlintで実装している方法を使用しています。

基本的な流れは単独パッケージと同じです。

monorepoでも1つのバージョンを使うFixed modeを使ってるので、コマンドがちょっと違うだけで流れは全く同じです。

コマンドの対応表

操作 単独パッケージ monorepo
バージョン更新 npm version {patch|minor|major} pnpm version {patch|minor|major} --no-git-tag-version
+ pnpm --recursive exec pnpm pkg set version=\"$(node -p \"JSON.parse(fs.readFileSync('package.json', 'utf8')).version\")\"
パッケージ公開 npm publish --provenance pnpm -r publish --no-git-checks --access public --provenance

📝 pnpm publishは内部的にnpm publishを呼び出すため、npm 11.5.1以上が必要です。lernaはまだOIDCに対応していません。(Issue #4219)

これに追加でmonorepo用のチェックjobを実行しています。

OIDC未連携のパッケージが残っている場合はコメントで通知します。

monorepoだと、後からパッケージが増えることがあるので、その度にOIDC設定をする必要があります。 そのため、未設定のパッケージを検出して通知するワークフローを追加しています。

それ以外は、単独のパッケージとmonorepoでは大きな違いはありません。

npm Trusted Publishing + OIDCの効果

npmトークンをSecretsに保存しなくてよくなる

Trusted Publisherを設定してOIDC連携をすると、トークンレスでnpmパッケージをCIから公開できます。 これによって、GitHub ActionsのSecretsにnpmトークンを保存する必要がなくなります。

npmトークンの漏洩リスクがなくなり、手動でやるトークンのローテーションも不要になります。

npmトークンによる公開を禁止できる

公開後はパッケージ設定で”Require two-factor authentication and disallow tokens”を有効化しています。 この設定を有効にすると公開経路を対話操作 or OIDCでの公開のみに限定できます。

これによりパケージの公開に関して次のような制約が課されます。

  • メンテナーはnpm publishする際に、もう一度MFAでの認証が必要
  • 自動化トークンや汎用アクセストークンでの公開が禁止される(発行済みのtokenでの公開はできなくなる = token漏洩対策)
  • ただし、OIDCでの公開は可能(寿命が短いtokenでは公開できる)

そのため、汎用のnpmトークンが漏洩してもパッケージレベルでの乗っ取りを防げるようになります。

Require two-factor authentication and disallow tokens With this option, a maintainer must have two-factor authentication enabled for their account, and they must publish interactively. Maintainers will be required to enter 2FA credentials when they perform the publish. Automation tokens and granular access tokens cannot be used to publish packages.

Requiring 2FA for package publishing and settings modification | npm Docs

これは、npmアカウントをフィッシングで乗っ取られた場合も同様で、“Require two-factor authentication and disallow tokens”を有効にしてるパッケージにマルウェアを含めたバージョンを公開するのを難しくできます。

npmでMFAを設定している場合に、TOTPのようなフィッシング耐性が低いMFAだと突破されることがあります。(フィッシングサイトでTOTPまで入力させることで、アカウントを乗っ取る) しかし、“Require two-factor authentication and disallow tokens”が有効な場合は、乗っ取られたnpmアカウントで新規のMFAを追加時、npm publish時に、再びMFAの認証が求められるようになります。 これによって、攻撃者は2回MFAを突破しないといけないため、攻撃の難易度が上がります。

MFAの管理については次の記事を書いています。

Provenance生成できる

Trusted Publishingを使用すると、npmは自動でprovenance attestationsを生成・公開します。 これにより、パッケージがどこでどのようにビルドされたかを示せます。 利用者は生成元とビルド経路を機械的に検証しやすくなります。

Provenanceは、次のサイトで確認できます。

運用面の違い

  • ブラウザだけでnpmパッケージの公開とリリースノート作成が可能
  • リリースフローの標準化でメンテナーのバスファクターを軽減
  • 手動でのnpmトークン管理が不要になる
  • Protection RulesetEnvironmentのProtection Rulesなどを使わないと、GitHubの権限を渡すことがnpmの権限を渡すことと同義になる

現在の制約事項

現時点での主な制約は次のとおりです。

  • セルフホストランナーは未対応(現状はクラウドホスト型のみ)
  • 1つのパッケージに設定できるTrusted Publisherは1つだけ
  • プライベートリポジトリからはprovenanceを生成できない
  • npm CLIはv11.5.1以上が必要

不満点

実際に運用してみて感じる不満点もあります。

  • npmの最新版がNode.jsにビルトインされていないため、npm i -g npmが必要
  • GITHUB_TOKENで作成したPRではCIが即時には走らない。そのため、merge-gatekeeper等の必須チェックがあるとリリースPRがProtect Rulesにかかることがある
    • GitHub App Tokenを使うと解決するけどそこまでやるのは面倒

これが難しくて、ベストな方法がよくわかっていません。

改善ポイント

この変更によって、GitHub Actionsからnpmへのパッケージ公開においては、長期間有効なnpmトークンを使うよりはOIDCを使用することで確実によくなっています。 GitHub Actionsのsecretsにnpm tokenがなくなります。そのため、OIDCのtokenが漏れても短時間で無効になるため、攻撃難易度はあがります。

一方で、ローカルからnpmへpublishするのに比べて、GitHub Actionsからnpmにpublishすると若干リスクは変わる部分がある。 具体的には、従来はGitHubアカウントの乗っ取りがあっても、npmにpublishされることはありませんでした。 これは、GitHubとnpmが別々のアカウントであり、npmトークンが漏れない限りはnpmにpublishされることはなかったためです。

OIDC化によって、GitHubアカウントの乗っ取りにより、GitHub Actionsのワークフローを改変されてnpmにpublishされるリスクがあります。

具体的には次の2つのリスクが残っています。

  1. ワークフローファイルの改変リスク: release.ymlを直接変更されると、改変後のフローで不正なパッケージを公開される可能性がある
    • Permission的には contents: writeworkflow: writeがあれば可能
  2. 正規フローを装った攻撃: 悪意あるコンテンツを含む変更をPull Requestで通すことで、npmにマルウェアを公開することがまだ可能である
    • Permission的には contents: writeとPRにラベルを付与できる権限と、PRをマージできる権限があれば可能

npmのTrusted PublisherのドキュメントにあるようにAdditional security measuresでこれらのリスクはもう少し軽減できる。

自分の場合は、次の2つの対策を入れているけど、もっと楽でもっと良い方法があれば教えて欲しいです。

Environment Protection Ruleの設定

GitHub ActionsのEnvironmentのProtection Rulesを設定することで、release.ymlが動くときにもう一段階の認証を挟んでいます。

具体的には次のような設定をしています。

  • npmというEnvironmentを作成
  • そのEnvironmentにRequired reviewers(オーナー)を設定
  • 必ず手動レビューが通らないと実際にはリリースできないようにチェックを設定
  • 注意: npm 側の OIDC 設定で Environment を npm に設定する必要がある
# .github/workflows/release.yml
jobs:
  publish:
    runs-on: ubuntu-latest
    environment: npm  # 作成したEnvironment名 = npmのODC設定にもEnvironment名を設定する必要がある
    steps:
      # ...

この設定をすると、Actionが走る時に手動でレビューを通るような形にできます。(Required reviewersを入れなければ自動で通る)

environment protection

ConfirmするとActionが実行される。

手動でOK

チーム内でデプロイは誰かにチェック必須にしたい場合などに、この機能が利用できます。

CODEOWNERSによるワークフロー保護

release.yml自体が変更されることを防ぐため、次の対策を実装している。

  1. CODEOWNERSファイルの設定: ワークフローファイルの変更にはオーナーのレビューが必須
# CODEOWNERS
.github/CODEOWNERS @your-username
.github/workflows/ @your-username
  1. Branch Protection Rules: メインブランチへの直接プッシュを禁止し、必ず Pull Request を経由するルールを設定する。併せて “Require review from Code Owners” を有効化する。

これらの対策により、攻撃者がプロテクションルールを回避するにはPull Requestを通して変更する必要があります。 また、Code Ownersのレビューが必須になるため、攻撃者はオーナーのアカウントを乗っ取るか、Code Ownersに入っている人を乗っ取る必要があります。

Pull Request は公開で行われるため攻撃は可視化され、攻撃者にとって実行しづらくなります。

将来的な改善点

Protection RulesetのRestrict file paths(Enterprise)を使用することで、ファイルパスに対するより厳格なプロテクションルールを設定できるようになります。 これを活用すると、release.ymlに対する変更ルールをさらに厳しく設定できます。ただし、パブリックリポジトリではまだこの機能が利用できない状況です。

また、Protection RulesetやEnvironment Protection RuleがAPIで削除できるようになっています。そのため、アカウントが持っているGitHub Tokenが漏洩すると、これらのルールを削除して攻撃することも可能になってしまう。GitHub Actionsの secrets.GITHUB_TOKEN の漏洩への対策にはなっているが、アカウントのGitHub Tokenの漏洩への対策にはなっていないという問題があります。

release.ymlに関する処理を改ざんするには、必ずMFAを使わないといけないみたいな設定方法があればいいのですが、その方法が現状だとわかっていないです。

対策まとめ

現在のOIDC化とEnvironment + Protection Ruleset(CODEOWNERS)の組み合わせにより、次の効果が得られています。

  • 攻撃者は攻撃に公開Pull Requestを経由する必要がある
  • 水面下でのワークフロー改変やGitHub Actionsのトークン(secrets.GITHUB_TOKEN)の悪用が難しくなった
  • ローカルのnpm tokenが漏洩しても、既存のnpmパッケージへのマルウェア版の上書きを防げるようになった
    • require two-factor authentication and disallow tokensでMFAが必須になるため
  • リリースプロセスの透明性が向上した

完全な防御は不可能ですが、攻撃コストは結構上がる状態が作られている形です。

Require two-factor authentication and disallow tokensにより、ローカルのnpmトークンが漏洩した場合の対策が可能になっています。パッケージレベルでnpmトークンのみでのpublishができなくなるためです。 そのため、影響が大きいパッケージは”Require two-factor authentication and disallow tokens”が有効になっている状態が良いです。

“Require two-factor authentication and disallow tokens” は OIDC 化とは別に設定できます。しかし monorepo では手動認証を都度挟むのは数と時間の都合で難しく、結果として未設定パッケージが多い状態でした。

まとめ

Trusted Publishingを導入することで公開フローの属人性を下げつつ、長期間有効なnpm tokenなしにnpmパッケージを安全に公開できるようになります。また、Provenanceも自動生成されるため、サプライチェーンセキュリティに対する信頼性も向上します。

また、リリースフローをCI/CDにまとめることで、メンテナンスの負担軽減とセキュリティ向上を両立できます。新規パッケージや既存パッケージ、monorepoにも対応可能なため、ぜひ導入を検討してみてください。

実はこのリリースフローは3世代目です。 今まではmonorepoでしかやってなかった(npm token管理が大変なのでやれなかった)のですが、ブラウザだけで、リリースノート書けるし、Contirbutorにどのリリースで入るかが通知できて、パッケージ公開できる仕組みになっています。

細かいはまりどころ

OIDCを設定して、422が出たときは大体設定がミスっています

  • npmjs.com の Trusted Publishの設定を確認してください
  • publishする package.jsonrepository.url が、実際にpublishするworkflowが動いてるリポジトリと一致しているかを確認してください

npm error 422 Unprocessable Entity - PUT https://registry.npmjs.org/@honkit%2fmarkup-it - Error verifying sigstore provenance bundle: Failed to validate repository information: package.json: “repository.url” is “git+https://github.com/<publish元と異なりリポジトリ>”, expected to match “https://github.com/honkit/honkit” from provenance

  • GH_AUTHなどの余計な環境変数で、npmが使う認証情報を邪魔していないかを確認してください
    • OIDCの場合は、自動でやるため、GH_AUTHなどを設定してはいけない

参考資料

]]>
JavaScript Primer v7.0.0リリース: ES2025対応とIterator Helpersの新章追加 2025-08-18T09:00:00+09:00 https://efcl.info/2025/08/18/jsprimer-v7 <![CDATA[

JavaScript Primer v7.0.0をリリースしました 🎉

JavaScript Primer v7.0.0では、ECMAScript 2025への対応と、新しく「イテレータとジェネレータ」の章を追加しています。 Iterator Helpersをはじめとした、ES2025の新機能を学ぶことができます。

NotebookLMもES2025対応版に更新しています。

JavaScript Primer Sponsors

JavaScript Primer(jsprimer)では、Open Collectiveを通じてプロジェクトの更新に関わる資金を募っています。

今回のv7リリースにおいては、次の方々にご支援いただきました!

Gold Sponsors

jsprimer sponsors

Supporters

jsprimer backers

ご支援ありがとうございます!

JavaScript Primerスポンサーについては、次のページを参照してください。

また、ご支援いただいた資金は、jsprimerにcontributionしてくれた方へ還元できるような仕組みを引き続き運用しています。 今回から同じ金額を他のCollective(オープンソースプロジェクト)に寄付する仕組みも追加しました。 Contributorが受け取る代わりに、指定されたCollectiveへ同じ金額を寄付できます。

JavaScript Primer v7.0.0の変更点

JavaScript Primer v7.0.0の変更点について紹介します。

リリースノートは次のページからも確認できます。

更新をメールで受け取りたい方は、次のフォームから登録できます。

JavaScript Primerの更新情報を購読

* indicates required

変更のサマリ

ES2025

その他

新章: イテレータとジェネレータ

概要

ES2025で追加されたIterator Helpersを含む、イテレータとジェネレータに関する新しい章を追加しました。 これは、v7での最も大きな追加です。

変更されたページ

内容

この章では次の内容をカバーしています。

イテレータの基礎

  • Symbol.iteratorを使ったIterableプロトコルの実装とIteratorプロトコルについて
  • for...of文での反復処理
  • ビルトインオブジェクトとしてのIterableオブジェクト(配列、文字列、Map、Setなど)

Iterator Helpers (ES2025新機能)

  • Iterator.from()でイテレータの作成
  • Iterator.prototypeのメソッド
    • .map() - 値の変換
    • .filter() - 値のフィルタリング
    • .take() - 指定数の要素を取得
    • .drop() - 指定数の要素をスキップ
    • .flatMap() - 平坦化しながら変換
    • .reduce() - reduce
    • .toArray() - 配列への変換
// Iterator.prototype.toArray の使用例
const iterator = Iterator.from([1, 2, 3]).map((x) => x * 2);
// Iteratorを配列に変換
const array = iterator.toArray();
console.log(array); // => [2, 4, 6]

RegExp.escapeの追加

概要

ES2025で追加されたRegExp.escapeメソッドは、文字列内の正規表現の特殊文字を自動的にエスケープします。 ユーザー入力を正規表現で安全に扱うための重要な機能です。

変更されたページ

変更内容

// RegExp.escape の例
const escaped = RegExp.escape("+");
console.log(escaped); // \+

Set Methodsの追加

概要

ES2025でSetに数学的な集合演算を行うメソッドが追加されました。 これにより、複数のSet間の演算が簡潔に書けるようになりました。

変更されたページ

追加されたメソッド

集合演算のイメージ

集合演算のイメージ

// Set メソッドの例
const setA = new Set([1, 2, 3, 4, 5]);
const setB = new Set([3, 4, 5, 6, 7]);

// 和集合 (A∪B): セットAとセットBのすべての要素を含む集合
const unionSet = setA.union(setB);
console.log([...unionSet]); // => [1, 2, 3, 4, 5, 6, 7]

// 積集合 (A∩B): セットAとセットBの両方に存在する要素のみの集合
const intersectionSet = setA.intersection(setB);
console.log([...intersectionSet]); // => [3, 4, 5]

// 差集合 (A-B): セットAからセットBに含まれる要素を除いた集合
const differenceSet = setA.difference(setB);
console.log([...differenceSet]); // => [1, 2]

// 対称差集合 (A△B): セットAとセットBのどちらか一方にのみ存在する要素の集合
const symmetricDifferenceSet = setA.symmetricDifference(setB);
console.log([...symmetricDifferenceSet]); // => [1, 2, 6, 7]

// 部分集合の判定
console.log(new Set([3, 4]).isSubsetOf(setA)); // => true

// 上位集合の判定
console.log(setA.isSupersetOf(new Set([3, 4]))); // => true

// 互いに素(共通要素がない)の判定
console.log(setA.isDisjointFrom(new Set([6, 7]))); // => false

Import Attributesの追加

概要

ES2025で標準化されたImport Attributesは、JSONファイルなどのJavaScript以外のファイルをインポートする際の属性を指定する構文です。

変更されたページ

変更内容

// 静的インポートの例
import jsonData from "./data.json" with { type: "json" };
console.log(jsonData.name); // => "John"
console.log(jsonData.age); // => 30
// Dynamic Import で属性を指定
const jsonData = await import("./data.json", { with: { type: "json" } });

その他の追加

Dynamic Import (ES2020)

動的にモジュールをインポートするimport()関数の説明を追加しました。

// async/await を使った Dynamic Import
async function loadModule() {
    try {
        const module = await import("./math-utils.js");
        console.log(module.add(1, 2)); // => 3
    } catch (error) {
        console.error(error);
    }
}

Array.prototype.flatMap (ES2019)

配列の変換と平坦化を同時に行うflatMapメソッドの説明を追加しました。

const sentences = ["Hello World", "Good Morning"];
const words = sentences.flatMap(sentence => sentence.split(" "));
console.log(words); // ["Hello", "World", "Good", "Morning"]

おわりに

jsprimerでは毎年更新していけるような仕組み作りの一環としてJavaScript Primer - Open Collectiveでの支援を募集しています。サイト上へのロゴの掲載やリリースノートへのロゴの掲載などの特典を含んでいます。

また、文章の修正やコード的なコミットはいつでも歓迎しています!

毎年1月頃には、次のECMAScriptのリリースに合わせた計画を立て始めています。

この時期になると次のリリースに向けてのIssueが立ち始めるので、興味がある人はリポジトリをWatchしてください!

個人的なGitHub Sponsorsも募集しています。こちらも合わせてご支援いただけると嬉しいです。

]]>
AI Agentのコマンド実行にTouch IDを使った「人間の確認」を挟むCLIツール confirm-pam を作った 2025-07-05T15:30:00+09:00 https://efcl.info/2025/07/05/confirm-pam <![CDATA[

macOS で Touch ID を使った「人間の確認」ができるシンプルな CLI ツール confirm-pam を作りました。

このツールを使うことで、AI Agent が任意のコマンドやスクリプトの実行する前に、Touch ID による生体認証を要求できます。 コマンドラインから実行される処理に対して、人間による明示的な確認ステップを追加する仕組みを提供します。

confirm-pam とは

confirm-pam は、macOS の Touch ID を使った生体認証による確認プロンプトを提供する CLI ツールです。

主な特徴

  • Touch ID 認証をサポート
  • 認証ダイアログに任意のメッセージを表示
  • 0(成功)、1(失敗)、2(エラー)の 3 つの終了コードで結果を判定
  • Rust で書かれていて、現時点だと macOS のみ対応

基本的な使い方は次のようになります。

image

# 基本的な認証プロンプト
confirm-pam "この操作を実行しますか?"

# 認証成功時の終了コードは0
echo $? # 0

# 認証失敗時やキャンセル時の終了コードは1
# Touch IDの認証を失敗またはキャンセルした場合
echo $? # 1

インストール方法

confirm-pam はcrates.ioで公開されているため、cargo でインストールできます。

cargo install confirm-pam

また、GitHub からソースコードをクローンしてビルドできます。

git clone https://github.com/azu/confirm-pam.git
cd confirm-pam
cargo build --release

必要な環境

  • macOS 10.12.2 以降
  • Touch ID 対応デバイス
  • Touch ID がシステム環境設定で有効化されていること

基本的な使い方

confirm-pam は非常にシンプルな API を提供しています。

confirm-pam [メッセージ]

メッセージを指定すると、Touch ID の認証ダイアログにそのメッセージが表示されます。

image

# 渡したメッセージが認証ダイアログに表示されます
confirm-pam "重要な変更をコミットしようとしています。続行しますか?"

終了コードによって認証結果を判定できます。例えば次のような値が返されます。

  • 0: 認証成功
  • 1: 認証失敗またはユーザーによるキャンセル
  • 2: システムエラー(Touch ID が無効化されている等)

実用例

confirm-pam は次のようなユースケースを想定して作成しています。

git commit --no-verifyの回避の確認

危険な git コマンドを実行する前に確認を挟む例です。--no-verify オプションのような、通常の安全チェックを回避するコマンドの実行前に認証を要求します。

# ~/.zshrc や ~/.bashrc に追加
git() {
  if [[ $@ == *'commit'* && $@ == *'--no-verify'* ]]; then
    if confirm-pam "git commit --no-verifyを実行します。続行しますか?"; then
      command git "$@"
    else
      echo "認証に失敗しました。操作をキャンセルします。人間による確認が必要です。"
      return 1
    fi
  else
    command git "$@"
  fi
}

Claude Code のような AI Agent はgit commit --no-verifyで pre-commit Hooks を回避してきます。 Touch ID などの人間の認証を挟むことで、Hook を無視したコミットを防げます。

claude-code

あと、人間がズルして --no-verify をつけてコミットするときに、確認の意味としてダイアログを出す方法としても使えます。

スクリプト内での使用

本番環境へのデプロイなど、影響範囲が大きい操作の確認ダイアログを表示する例です。

#!/bin/bash

# 危険な操作の前に確認
if confirm-pam "本番環境へのデプロイを開始します。続行しますか?"; then
  echo "デプロイを開始します..."
  # デプロイ処理
else
  echo "デプロイがキャンセルされました"
  exit 1
fi

yを入力しないと進めないようにする方法もありますが AI Agent は echo "y" | コマンド のような pipe で回避するので、人間の確認を挟みたい時に利用できます。

技術的な仕組み

confirm-pam は、将来的なクロスプラットフォーム対応を見据えたレイヤー化アーキテクチャを採用しています。

アーキテクチャ設計

main.rs (CLI エントリポイント)
    ↓
auth/mod.rs (認証抽象化レイヤー)
    ↓
platform/mod.rs (プラットフォーム固有実装)
    ↓
各OS実装 (macos/linux/windows)

macOS 実装

macOS では FFI(Foreign Function Interface)を使用して、Rust から Swift コードを呼び出しています。

実装の詳細は次の通りです。

  • Swift 実装: src/platform/macos/auth_helper.swift で LocalAuthentication フレームワークを使用
  • ビルドシステム: build.rsswiftc を使って Swift コードをコンパイル
  • 認証処理: Touch ID による生体認証を同期的に処理

LocalAuthentication フレームワーク

Swift 実装では、Apple の LocalAuthentication フレームワークを使用しています。

// 概念的な実装例
import LocalAuthentication

let context = LAContext()
let policy = LAPolicy.deviceOwnerAuthenticationWithBiometrics
context.evaluatePolicy(policy, localizedReason: message) { success, error in
    // 認証結果の処理
}

これにより、システム標準の認証ダイアログが表示され、Touch ID で認証します。

抽象化レイヤー

BiometricAuthenticator トレイトによって各プラットフォームの実装を抽象化しています。

主要な API は次の通りです。

  • authenticate(message: &str) -> Result<bool>: 認証し結果を返す
  • is_available() -> Result<bool>: 認証機能の利用可能性を確認

コンパイル時の条件分岐で各 OS 実装を選択し、単一のバイナリとして動作します。

今後のロードマップ

現在は macOS の Touch ID のみサポートしていますが、他のプラットフォームへの対応も予定しています。 しかし、自分は macOS しか使ってないので、他のプラットフォームの実装を追加したい人は PR を待ってます。

まとめ

confirm-pam は、macOS で Touch ID を使った生体認証確認を簡単に追加できる CLI ツールです。 AI が回避しにくい、人間による確認を挟むためのツールとして使ってみてください。

Note: 認証をたくさん出すと無意識的に OK してしまうので、この辺をもっと工夫する必要が出てくるかもしれません。

]]>
textlint v15.0.0をリリースしました。非推奨APIの削除とNode.js 20+サポート/MCPサーバの改善 2025-06-25T09:00:00+09:00 https://efcl.info/2025/06/25/textlint-v15 <![CDATA[

textlint v15.0.0をリリースしました!

textlint v15は、v12.3.0から非推奨としてマークされていた古いAPIをすべて削除するメジャーリリースです。

textlint v15.0.0の変更点

主要な変更点は次の通りです。

Breaking Changes

  • Node.js 20+のサポート(Node.js 16,18はサポート終了
  • TextLintEngineを削除 → createLinter()
  • TextFixEngineを削除 → createLinter()
  • TextLintCoreを削除 → createLinter()または@textlint/kernel
  • textlint(シングルトンインスタンス)を削除 → createLinter()
  • createFormatterを削除 → loadFormatter()

Node.js 20以上が必要

textlint v15では、Node.js 20.0.0以上が必要になりました。 Nodejs 18はEOL(End of Life)となっているため、Node.js 20以上を使用する必要があります。

非推奨APIの削除

textlintパッケージをNode.jsモジュールとして利用する場合のみ影響を受ける変更です。

textlint v15では、非推奨となっていた次のAPIが完全に削除されました。

移行ガイドは次のページに用意しています。

TextLintEnginecreateLinter()

変更前(v14以前):

const { TextLintEngine } = require("textlint");

const engine = new TextLintEngine({
    configFile: ".textlintrc.json"
});
const results = await engine.executeOnFiles(["*.md"]);
const output = engine.formatResults(results);
console.log(output);

変更後(v15以降):

import { createLinter, loadTextlintrc, loadLinterFormatter } from "textlint";

const descriptor = await loadTextlintrc({
    configFilePath: ".textlintrc.json"
});
const linter = createLinter({ descriptor });
const results = await linter.lintFiles(["*.md"]);
const formatter = await loadLinterFormatter({ formatterName: "stylish" });
const output = formatter.format(results);
console.log(output);

TextFixEnginecreateLinter()

変更前(v14以前):

const { TextFixEngine } = require("textlint");

const engine = new TextFixEngine();
const results = await engine.executeOnFiles(["*.md"]);

変更後(v15以降):

import { createLinter, loadTextlintrc } from "textlint";

const descriptor = await loadTextlintrc();
const linter = createLinter({ descriptor });
const results = await linter.fixFiles(["*.md"]);

TextLintCorecreateLinter()

変更前(v14以前):

const { TextLintCore } = require("textlint");

const textlint = new TextLintCore();
textlint.setupRules({
    "rule-name": require("./my-rule")
});
const result = await textlint.lintText("Hello world", "test.md");

変更後(v15以降):

import { createLinter } from "textlint";
import { TextlintKernelDescriptor } from "@textlint/kernel";
import { moduleInterop } from "@textlint/module-interop";

const descriptor = new TextlintKernelDescriptor({
    rules: [
        {
            ruleId: "rule-name",
            rule: moduleInterop((await import("./my-rule")).default)
        }
    ]
});
const linter = createLinter({ descriptor });
const result = await linter.lintText("Hello world", "test.md");

その他の改善

Exit Statusの改善

textlint v15では、ESLintの動作に合わせてExit Statusが改善されました。

  • Fatalエラー: Exit Status 2を返す(従来は1)
  • Lintエラー: Exit Status 1を返す(変更なし)
  • 成功: Exit Status 0を返す(変更なし)
# v15+ behavior
textlint nonexistent-file.md     # Exit Status: 2 (file search error)
textlint file-with-errors.md     # Exit Status: 1 (lint errors)
textlint clean-file.md           # Exit Status: 0 (success)

絶対パスのファイルも.textlintignoreのパターンを尊重

textlint v15では、絶対パスのファイルが.textlintignoreパターンを正しく尊重しない問題が修正されました。

以前は、.textlintignoreで記載されたパターンがマッチしていても、絶対パスとして渡された場合は無視されていませんでした。 v15では、絶対パスのファイルも.textlintignoreのパターンを正しく尊重するようになりました。

# Before v15 (Bug)
textlint /absolute/path/to/ignored-file.md  # Lintされてしまっていた

# v15+ (Fixed)
textlint /absolute/path/to/ignored-file.md  # Lintの対象外となった

詳しくは、GitHub Issue #1412を参照してください。

Model Context Protocol (MCP)の統合強化

textlint v14.8.0から、textlint --mcpでtextlintをMCPサーバーとして起動できます。

textlint v15では、MCPのサポートを改善しています。 2025-06-18のKey Changes - Model Context Protocolに基づいて、次の変更が行われました。

  • Structured Content: 出力結果の構造を事前に定義することで、AIツールが結果をより正確に解釈できるようになりました
  • Output Schema: 出力結果のJSON Schemaを提供することで、AIツールが結果を正確に解釈できるようにしました
  • isError: trueの追加: エラーが発生した場合、isError: trueを設定することで、AIツールでエラー状態を認識できるようにしました

詳しくは、次のGitHub Issueを参照してください。

まとめ

textlint v15では、非推奨となっているものを削除したり、動作の一貫性を改善するような変更をしています。

またtextlintの内部的にも、pnpmVitestへ移行し、 CIの合計時間を21m 5s → 7m 20sに短縮しています。(Windowsでnpmのインストールが遅かったのがボトルネック)

他にもMerge Gatekeeperを入れてAuto Mergeをできるようにしたり、Netlifyのキャッシュがやたら不安定なのでDeploy PR Preview actionを使ってPRのプレビューを作成するなどの改善をしています。

この辺を整理したので、textlint自体の開発体験もだいぶ良くなっています。

まだやるべきことはたくさんあるので、興味あるひとは是非Contributorとして参加してください!

実験的なものとしてtextlint-rule-preset-ai-writingとか書きながら考えていましたが、Linterの役割はちょっと広がってきているのかなと思いました。 Biome v2で追加されたAssistもそうですが、ErrorやWarningではなくSuggestionに近い部分も増えてきているように感じます。Suggestionに関してはLinterの役割なのかは微妙なところです。しかし、この辺りの機能はLinterの延長として実装されることも多くなっています。

特に自然言語はauto fixが難しいので、Suggestionのような形でHintを提供できる仕組みがあると、人間とAIにとっても使える感じのツールになるんじゃないかなーと思っています。

textlintでは、Contributionを歓迎しています。

また、GitHub Sponsorsでのサポートも歓迎しています。

関連リンク

]]>
Secretlint v10.0.0リリース: デフォルトでシークレットをマスク表示するように変更、Node.js 20+のサポート 2025-06-15T09:00:00+09:00 https://efcl.info/2025/06/15/secretlint-v10 <![CDATA[

機密情報を検出するSecretlintのv10.0.0をリリースしました!

このバージョンでは、デフォルトでLintの結果のシークレットをマスクして表示するように変更されました。

Secretlint v10.0.0の変更点

主要な変更点は次の通りです。詳細はリリースノートを参照してください。

🛡️ シークレットのマスク表示がデフォルトに

最も大きな変更は、見つけたシークレットをデフォルトでマスクして表示するようになったことです。

変更前:

✖ found credential: github_token
  GITHUB_TOKEN=ghp_1234567890abcdef1234567890abcdef12345678

変更後(v10.0.0):

✖ found credential: github_token
  GITHUB_TOKEN=*******************************************

実際のシークレット値を確認したい場合

デバッグなどで実際のシークレット値を確認したい場合は、--no-maskSecrets オプションが使用できます。

# デフォルト(マスク表示)
$ secretlint "**/*"

~/secretlint/secretlint/examples/cli/credential
  1:0  error  [AWSSecretAccessKey] found AWS Secret Access Key: ****************************************  @secretlint/secretlint-rule-preset-recommend > @secretlint/secretlint-rule-aws

✖ 1 problems (1 errors, 0 warnings)

# 実際のシークレット値を表示
$ secretlint --no-maskSecrets "**/*"

~/secretlint/secretlint/examples/cli/credential
  1:0  error  [AWSSecretAccessKey] found AWS Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYSECRETSKEY  @secretlint/secretlint-rule-preset-recommend > @secretlint/secretlint-rule-aws

✖ 1 problems (1 errors, 0 warnings)

🚨 Node.js 20+サポートへの変更

Node.js 18のサポートを終了し、Node.js 20以上が必要になりました。

移行ガイド

Node.jsバージョンの更新

Secretlint v10.0.0を使用するには、Node.js 20.0.0以上が必要です。

# Node.jsバージョンの確認
node --version  # v20.0.0以上である必要があります

# 必要に応じてNode.jsをアップデート

CI/CDでの設定更新

GitHub ActionsなどのCI/CDでSecretlintを使用している場合は、Node.jsバージョンを更新してください。

- uses: actions/setup-node@v4
  with:
    node-version: 22

マスク表示への対応

新しいデフォルトのマスク表示では、次の情報は引き続き表示されます。

  • ファイルパスと行番号
  • ルール名
  • エラーメッセージ

実際のシークレット値のみがマスクされるため、デバッグに必要な情報は保持されています。

実際の値を確認する必要がある場合は、--no-maskSecretsオプションを使用してください。

なぜデフォルトでマスクするように変更したか

最近の開発環境では、次のような状況が増えています。

  • AIツール: GitHub Copilot、Cursor、Claude Codeなどのツールがターミナル出力を解析する
    • シークレットの場所は分かるので本質的には変わらないが、標準出力を読み取るツールが増えている
  • クラウドCI/CD: CI/CDに外部サービスを利用するのが一般的になり、意図せずに長期間シークレットがログに残る
  • 画面共有: デモや会議でターミナルを共有する機会が増え、意図せずシークレットが表示される

見つかったシークレットはローテーションする必要があるため、本質的な問題は変わりません。 しかし、意図しない形でシークレットが外部に漏れるケースが増えているため、デフォルトの動作を変更しました。

Secretlint v10.0.0では、この問題を解決するためにデフォルトでマスク表示を行い、デフォルトの設定を安全側に変更しました。

まとめ

Secretlint v10.0.0では、デフォルトで結果をマスク表示するように変更しました。 ツールの使われ方が変化しているため、デフォルトの設定を安全側に変更しました。

フィードバックがありましたら、GitHubのIssueでお知らせください。

]]>
textlint v14.8.0をリリースしました - MCP(Model Context Protocol)をサポート 2025-06-10T09:00:00+09:00 https://efcl.info/2025/06/10/textlint-mcp <![CDATA[

textlint v14.8.0をリリースしました!

今回のリリースでは、実験的な機能としてMCP(Model Context Protocol)サポートを追加しました。 MCPは、AI言語モデルが外部のツールやサービスと統一されたインターフェースを通じて連携できるプロトコルです。

MCP(Model Context Protocol)サポート

textlint v14.8.0では、新しい--mcpフラグを追加しました。このフラグにより、textlintをMCPサーバーとして起動し、AI搭載のコードエディタ(VS Code with Copilot Chat、Cursor、Windsurf)から直接textlintを利用できるようになります。

MCPとは

Model Context Protocol(MCP)は、AI言語モデルが外部のツールやサービスと統一されたインターフェースを通じて連携するためのプロトコルです。これにより、AIアシスタントがtextlintの機能を直接利用して、文章のチェックや修正を行えるようになります。

前提条件

MCPサーバーを使用するには、次の条件を満たす必要があります。

  • textlint v14.8.0以降
  • MCPサポートを持つAI搭載のコードエディタ(VS Code with Copilot Chat、Cursor、Windsurf)
  • 設定済みのtextlintプロジェクト - textlintにはデフォルトルールがないため、MCPサーバーを使用する前に既存のtextlint設定が必要です

基本的な使い方

まず、プロジェクトでtextlintを設定する必要があります。 textlintにはデフォルトルールがないため、MCPサーバーを使用する前にプロジェクトで使うルールを設定する必要があります。

# textlintとルールをインストール
npm install --save-dev textlint textlint-rule-preset-ja-technical-writing

npx textlint --initを実行して、textlintの設定ファイルを作成します。これにより、基本的な設定が自動生成されます。

npx textlint --init

作成された.textlintrc.jsonファイルに必要な設定などがあれば、手動で編集します。 次の設定はtextlint --initで生成されたものです。

{
    "rules": {
        "preset-ja-technical-writing": true
    }
}

設定が動くかは、textlintコマンドを実行して確認できます。

npx textlint README.md

これでtextlintの設定は確認できたので、あとはエディタにMCPサーバの設定を追加します。

エディタ別の設定方法

VS Code with GitHub Copilot

VS CodeでtextlintのMCPサーバーを設定するには、プロジェクトに.vscode/mcp.jsonファイルを作成します:

{
    "servers": {
        "textlint": {
            "type": "stdio",
            "command": "npx",
            "args": ["textlint", "--mcp"]
        }
    }
}

また、コマンドパレットを使用して設定することも可能です:

  1. Ctrl+Shift+P (Windows/Linux) または Cmd+Shift+P (macOS) を押す
  2. MCP: Add Server を選択
  3. ドロップダウンから Command (stdio) を選択
  4. コマンドとして npx textlint --mcp を入力
  5. サーバーIDとして textlint を入力
  6. Workspace Settings を選択して .vscode/mcp.json に設定を作成

Cursor

プロジェクトディレクトリに .cursor/mcp.json ファイルを作成します:

{
    "mcpServers": {
        "textlint": {
            "command": "npx",
            "args": ["textlint", "--mcp"],
            "env": {}
        }
    }
}

グローバル設定の場合は、ホームディレクトリに~/.cursor/mcp.jsonを作成し、同じ設定を記述します。

Windsurf

  1. Windsurf → Settings → Advanced Settings に移動
  2. Cascadeセクションまでスクロールし、「Add Server」をクリック
  3. 「Add custom server +」を選択
  4. ~/.codeium/windsurf/mcp_config.jsonに次の設定を追加:
{
    "mcpServers": {
        "textlint": {
            "command": "npx",
            "args": ["textlint", "--mcp"],
            "env": {}
        }
    }
}
  1. 更新ボタンを押してMCPサーバーリストを更新

利用可能なツール

textlint MCPサーバーでは、textlintを使った文章のチェックや修正するツールを提供しています。

使用例

AIアシスタントで使用できるプロンプトの例:

現在のファイルをtextlint MCPを使って、見つかったテキストの問題を説明してください

src/README.mdのすべてのtextlintの問題を修正してください

このMarkdownテキストの文章の問題をチェックして、改善案を提案してください

docs/ディレクトリのすべてのファイルにtextlintの自動修正を適用してください

次のような感じで、textlintのlint結果をみながら修正までやってくれます。

利用例

サンプルリポジトリ

次のリポジトリは、VSCodeでtextlint MCPサーバーを設定したサンプルです。

git clone https://github.com/azu/textlint-mcp-example.git
cd textlint-mcp-example
npm ci
code .

トラブルシューティング

textlint MCPサーバーで問題が発生した場合:

  1. MCPサーバーの状態を確認: エディタのMCPサーバーリスト/ステータス機能を使用
  2. textlintのインストールを確認: textlintとルールが正しくインストールされていることを確認
  3. 設定を確認: .textlintrcファイルが有効であることを確認
  4. ログを確認: MCPサーバーログでエラーメッセージを確認
  5. 手動でテスト: ターミナルからnpx textlint --mcpを実行してサーバーが起動するかテスト

よくある問題:

  • textlint設定ファイルが存在しない
  • textlintルールやプラグインがインストールされていない
  • MCP設定のファイルパスが間違っている
  • バージョンの互換性(textlint v14.8.0以降が必要)

まとめ

textlint v14.8.0では、実験的な機能としてMCP(Model Context Protocol)サポートを追加しました。 これにより、AI搭載のコードエディタから直接textlintの機能を利用できるようになり、エディタ上で文章チェックと修正を行えるようになります。

この機能は実験的なものですが、AI Agentとtextlintの連携が簡単になります。 ぜひお試しいただき、フィードバックをお寄せください。

textlint --mcpは、@chick-pさんが実装してくれました。

textlintでは、Contributionを歓迎しています。

また、GitHub Sponsorsでのサポートも歓迎しています。

参考リンク

]]>
JavaScript PrimerのNotebookLMが利用可能になりました! 2025-06-07T18:06:00+09:00 https://efcl.info/2025/06/07/jsprimer-notebooklm <![CDATA[

JavaScript PrimerNotebookLMが利用可能になりました。 NotebookLMは、Googleが提供するAIを活用したノートブック環境で、登録したソースに対して質問をしたり、情報を検索したりすることができます。

次のリンクから、JavaScript PrimerのNotebookLMにアクセスできます。

JavaScript Primer on NotebookLM

JavaScript PrimerのコンテンツがNotebookLMに入ってることで、読者は書籍の内容についてNotebookLMに直接質問したり、Podcastとして音声で聞いたりすることができるようになります。

NotebookLMを活用することで、JavaScript Primerを読み進める中で生じた疑問点をその場で解消したり、特定の構文について「このコードはどういう意味ですか?」といった質問をしてjsprimerのコンテンツをベースにして回答を得たりできます。これは、従来の書籍では難しかった、検索しにくい構文に関する疑問も解決しやすいという利点があります。

たとえば、NotebookLMでは次のような質問をしたりできます。あと、Podcastにするのがなんだかんだ便利です。

  • =>はどういう意味?」
  • 「JavaScriptのクロージャーとは何ですか?」
  • 「Promiseとasync/awaitの違いは何ですか?」
  • 「オブジェクトの章の内容を要約してください」
  • 「配列の操作方法について書かれている章はどこですか?」

NotebookLM上のJavaScript Primerへは、以下のリンクからアクセスできます。

JavaScript Primerとは?

JavaScript Primerは、ECMAScript 2015以降をベースにしたJavaScriptの入門書として、常に最新の情報を提供すべく継続的なメンテナンスを行っている書籍です。 この継続的なメンテナンスの取り組みについては、TSKaigi 2025で「技術書をソフトウェア開発する」というタイトルで発表しました。

jsprimerでは、いつでもContributorsやSponsorsを募集しています!

まとめ

NotebookLMをPublicに公開する機能ができたので、JavaScript PrimerのコンテンツをまとめたNotebookLMを公開しました。

NotebookLMのソースの更新は、メジャーアップデートごとに行うつもりです。 URLベースならもっと簡単に更新できる方法があるのかもしれないので、もし知っている方がいれば教えてください。

]]>
TSKaigi 2025で「技術書をソフトウェア開発する」という発表をしました 2025-05-24T21:33:00+09:00 https://efcl.info/2025/05/24/tskaigi-2025-jsprimer <![CDATA[

TSKaigi 2025で「技術書をソフトウェア開発する」というタイトルで発表をしました。 スライドは次のページで公開しています。

基本的にはJavaScript PrimerというJavaScriptの入門書の話になっています。 どうやって作っていて、どうやって10年近くも更新を続けているのか、オープンソースとしての継続性などについて話しています。

関連:

JavaScript/TypeScript以外での通じる話が多いので、技術書や何かを伝える文章を書く人にとっても参考になるかなと思います。 今だと、LLMとかに投げる文章でも同様のことが言える場面も多いとは思います。

jsprimerの目的の一つに「変化に対応できるようにする」というものがあります。 これのためにjsprimerという書籍を更新しているので、jsprimerにContributeや金銭的に支援してくれる人や企業はいつでも募集しています。

p65: オープンソースに関わってもらうこと

スライド中にもありますが、現在のソフトウェア開発でオープンソースと関わらずに完結することはかなり難しいです。 そのため、入門書としての目的としてオープンソースに関わってもらうことを入れています。これはjsprimerをオープンソースで開発している理由の一つです。

オープンソースとの関わり方は色々な形があるので、jsprimerに限らずに何かしらの形で関わってみると面白いんじゃないかと思います。

GitHub Sponnsorなどでの支援も大歓迎です。

今回の発表のベースやProposalを書くときは結構LLMを使っていて、インタビュー形式でLLMに質問を考えてもらったものに回答して、そこから主張を抽出して作ってます。

スライドもLLMでアウトラインを見ながら、中身を書いていくということをやってた気がします。 昔から、アウトラインと中身を行き来して視点を変えながら書くというのをよくやっていた気がします。 アウトライナー実践入門でいうところのシェイクというやり方。

この辺は文章でもそうですが、プログラミングでも関数やモジュールという単位でアウトラインのように遠めに見ながら進めるタイミングと中身を見ながら進めるフェーズがあります。 そういった点では、文章とコードで大きな違いはないのかなと思う部分はありました。

文章やコードを読む上でも、既知から未知の方が読みやすいのは一緒で、コードの方がちょっと違うのは参照元へのジャンプ機能が充実しているという点かなと思います。 Ask the speakerでもあった気がしますが、文章を読んでる時の人間のコンテキストはかなり小さいので、jsprimerを書くときは一度に読むべきコンテキストを絞るイメージで書いています(文章ではジャンプが難しいという前提があります)。

簡単に言えば、別のページの参照を減らす(読んでるところを見てれば全部わかる)、サンプルコードを小さく保つ、パラグラフライティングで結論から書くといった話があります。 ユースケースのようなアプリケーションの章は、サンプルコードが断片的になりやすいので、それぞれのページの最後にコピペで動く完全な状態を提示することで、そこで戻って来れるような仕組みも入れています。 技術書はif文が書きにくいコード、みたいな感じで書いていくのが感覚と近いのかもしれないですね。

コードにおいては複雑度を測るのに循環的複雑度(Cyclomatic complexity)というメトリクスがありますが、 日本語だと建石評価式(自分がそう呼んでるだけ)みたいなものがあって@textstat/textstat-rule-tateish-levelとして実装したことがあります。

この辺の複雑度を低く保つことが読みやすい文章に繋がるのは同じかなと思いました。

TSKaigiお疲れ様でした!

]]>