@aiquants/auth-directory-drizzle 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/guards.ts","../src/ledger-identity.ts","../src/link-store.ts","../src/membership-mode.ts","../src/promote.ts","../src/promote-repository.ts","../src/sql-limits.ts","../src/schema.ts","../src/sync.ts","../src/sync-messages.ts","../src/sync-ports.ts","../src/sync-queue.ts","../src/sync-repository.ts"],"sourcesContent":["/**\n * @module @aiquants/auth-directory-drizzle\n * @description Drizzle (mssql) adapter for `@aiquants/auth-directory-core`.\n * `@aiquants/auth-directory-core` 向けの drizzle (mssql) アダプタ。\n *\n * identity 側のテーブル定義と、その上で動く 4 つの面を提供する:\n * 1. **リンクストア** — 外部リンク・帰属証明・グループ許可・同期要求キューの読み書き\n * 2. **同期リポジトリ** — 同期 1 実行分の記憶域操作 (削除は認可側のロックアウトガード経由)\n * 3. **昇格** — 初回ログイン直後に台帳を実所属へ引き上げる (上流へは到達しない)\n * 4. **ガード** — 外部供給グループへ管理ロールが付かないようにする認可ストアのデコレータ\n */\nexport * from \"./db\"\nexport * from \"./guards\"\nexport * from \"./ledger-identity\"\nexport * from \"./link-store\"\nexport * from \"./membership-mode\"\nexport * from \"./promote\"\nexport * from \"./promote-repository\"\nexport * from \"./schema\"\nexport * from \"./sql-limits\"\nexport * from \"./sync\"\nexport * from \"./sync-messages\"\nexport * from \"./sync-ports\"\nexport * from \"./sync-queue\"\nexport * from \"./sync-repository\"\n","/**\n * @module guards\n * @description Keeps administrative roles off groups whose membership an upstream directory owns.\n * 所属の所有者が上流ディレクトリにあるグループへ、管理ロールが付かないようにする。\n *\n * **不変条件は状態の性質であって、遷移の性質ではない。**\n *\n * 最初の実装は `assignGroupRole` だけを差し替えていた。しかしそれは「外部グループへ管理ロールを\n * 割り当てる」という 1 つの遷移を塞いだにすぎず、**無害なロールを外部グループへ割り当ててから、\n * そのロールを管理ロールへ昇格させる**という順序が開いたままだった。不変条件を守るには、それを\n * 崩し得るすべての遷移を塞ぐ必要がある。\n *\n * 崩し得る遷移は 3 つある。\n *\n * | # | 遷移 | 塞ぐもの |\n * | --- | --- | --- |\n * | 1 | 外部グループが管理ロールを得る | `assignGroupRole` |\n * | 2 | 外部グループが持つロールが管理ロールになる | `createGrant` |\n * | 3 | 管理ロールを持つグループが外部連携される | `assertGroupMayBeLinked` |\n *\n * `updateGrant` は塞ぐ必要がない。`(role_id, resource_id, action)` が一意であるため、管理リソースの\n * 4 アクションを揃えるには 4 行の付与が要り、既に 4 行あるロールは既に管理ロールである。既存行の\n * アクションを差し替えても集合の要素数は増えないので、`updateGrant` は管理権を **作れない**。\n *\n * `setRoleActive` も塞ぐ必要がない。遷移 1 の判定はロールの有効・無効を見ないため、無効な管理\n * ロールも外部グループへは割り当てられない。したがって「無効な管理ロールを外部グループが保持した\n * まま有効化される」という状態には到達し得ない。\n *\n * **なぜ認可パッケージの中で判定しないのか。** 判定を `@aiquants/authz-drizzle` の管理ストアへ\n * 埋め込むと、認可パッケージがグループの「出所」という identity 側の概念を知ることになる。依存の\n * 向きは auth → authz の一方通行で保ちたいので、包む側 (identity 側) から課す。\n *\n * **なぜ禁止するのか。** 外部グループの所属は上流の管理者が変更できる。そこへ管理ロールを付けると、\n * 上流側の 1 操作がこの配備の管理者を増やしたり、逆に最後の管理者を奪ったりする。ブレークグラス\n * 経路は常にローカルに残しておく必要がある。\n */\nimport { AUTHZ_ACTIONS, type AuthzAction, type AuthzAdminStore, type AuthzGrant } from \"@aiquants/authz-core\"\n\n/** The link lookup this guard needs. 本ガードが必要とするリンク参照。 */\nexport type ExternalLinkLookup = {\n /**\n * The upstream key of the group, or `null` when its membership is locally managed.\n * そのグループの上流キー。ローカル管理なら `null`。\n *\n * 「外部かどうか」と「拒否メッセージに出す名前」を 1 回の照会で返す。判定のためだけに\n * 引いて、名前のためにもう一度引く形にすると、拒否のたびに往復が 2 倍になる。\n *\n * ⚠️ `runner` を渡された実装は、**必ずその上で** 引くこと。別接続で引くと、この読み取りの\n * 結果を前提に書き込む呼び出し側にとって前提が保たれない (読んだ後・書く前に連携が作られる)。\n */\n externalGroupKey(tenantId: string, groupId: number, runner?: unknown): Promise<string | null>\n}\n\n/** Configuration of the guard. ガードの構成。 */\nexport type ExternalGroupGuardOptions = {\n linkLookup: ExternalLinkLookup\n /** App key the administrative resource belongs to. 管理リソースが属するアプリキー。 */\n adminAppKey: string\n /** Resource key that grants administration. 管理を与えるリソースキー。 */\n adminResourceKey: string\n /**\n * Refusal message. 拒否メッセージ。\n *\n * @param roleKey Role that would have conferred administration. 管理権を与えることになるロール。\n * @param groupKey Group that would have held it. それを保持することになるグループ。\n */\n message: (roleKey: string, groupKey: string) => string\n}\n\n/** Default refusal message (Japanese, matching the admin UI's locale). 既定の拒否メッセージ。 */\nexport const defaultExternalGroupGuardMessage = (roleKey: string, groupKey: string): string =>\n `外部ディレクトリ連携グループ (${groupKey}) には管理ロール (${roleKey}) を持たせられません。上流側の所属変更がそのまま管理者の増減になるため、管理者経路はローカル管理のグループに限定しています。`\n\n/**\n * Collects the actions a role holds on the administrative resource.\n * あるロールが管理リソースに対して保持しているアクションを集める処理。\n */\nfunction adminActionsOf(grants: readonly AuthzGrant[], roleId: number, adminAppKey: string, adminResourceKey: string): Set<AuthzAction> {\n return new Set(grants.filter((g) => g.roleId === roleId && g.appKey === adminAppKey && g.resourceKey === adminResourceKey).map((g) => g.action))\n}\n\n/**\n * Reports whether an action set confers administration.\n * あるアクション集合が管理権を与えるかを判定する処理。\n *\n * 「管理者」の定義は認可コアの最後の管理者判定と同じ — 管理リソースの 4 原子アクションを\n * **すべて** 保持していること。定義が食い違うと、ここで通したロールをロックアウトガードが\n * 管理者として数える (あるいはその逆の) 不整合が生じる。\n */\nconst confersAdministration = (actions: ReadonlySet<AuthzAction>): boolean => AUTHZ_ACTIONS.every((a) => actions.has(a))\n\n/** Human-facing name of a role, falling back to its id. ロールの表示名 (無ければ ID)。 */\nconst roleLabel = (grants: readonly AuthzGrant[], roleId: number): string => grants.find((g) => g.roleId === roleId)?.roleKey ?? `#${roleId}`\n\n/**\n * Wraps an authorization admin store so administrative power cannot reach an external group.\n * 外部グループへ管理権が届かないよう認可管理ストアを包む処理。\n *\n * `assignGroupRole` と `createGrant` を差し替え、残りは透過的に委譲する。3 番目の方向\n * (連携の開始) は {@link assertGroupMayBeLinked} が担い、リンクを作る側が必ず通す。\n *\n * @param store Store to wrap. 包む対象のストア。\n * @param opts Link lookup and administrative resource identity. リンク参照と管理リソースの同定。\n * @returns A store with the same surface. 同一表面のストア。\n */\nexport function withExternalGroupGuards<R>(store: AuthzAdminStore<R>, opts: ExternalGroupGuardOptions): AuthzAdminStore<R> {\n return decorate(store as AuthzAdminStore, opts) as unknown as AuthzAdminStore<R>\n}\n\n/**\n * Applies the guards to one store, and to every store it hands out.\n * 1 つのストアと、そのストアが渡すすべてのストアへガードを適用する処理。\n *\n * ⚠️ **再帰であることが要点である。** `withTransaction` は「トランザクションに束ねたストア」を\n * 呼び出し側へ渡す。素通しにすると、包んだはずのストアから **無防備なストアを取り出せる** —\n * `guarded.withTransaction(tx => tx.assignGroupRole(外部グループ, 管理ロール))` の 1 行で、\n * デコレータの保証がまるごと消える。包む対象が「ストアを返すメソッド」を持つ以上、デコレータは\n * その返り値についても閉じていなければならない。\n *\n * @param target - Store to wrap. 包む対象のストア。\n * @param opts - Link lookup and administrative resource identity. リンク参照と管理リソースの同定。\n * @returns A guarded store whose `withTransaction` also yields guarded stores. 再帰的に保護されたストア。\n */\nfunction decorate(target: AuthzAdminStore, opts: ExternalGroupGuardOptions): AuthzAdminStore {\n const plain = target\n\n /**\n * Refuses when any externally-sourced group of the tenant holds one of the given roles.\n * 指定ロールのいずれかを外部供給グループが保持している場合に拒否する処理。\n *\n * @param tx - Store bound to the deciding transaction. 判定を行うトランザクションに束ねたストア。\n * @param runner - The transaction itself, for the link lookup. リンク参照のためのトランザクション。\n * @param tenantId - Tenant being guarded. 保護対象テナント。\n * @param roleIds - Roles whose holders must not be external. 保持者が外部であってはならないロール。\n * @param grants - Grants already read on the same transaction. 同一トランザクションで読んだ付与。\n * @returns Nothing. 戻り値なし。\n * @throws {Error} When an external group holds the role. 外部グループが保持している場合。\n */\n async function refuseIfHeldByExternalGroup(tx: AuthzAdminStore, runner: unknown, tenantId: string, roleIds: ReadonlySet<number>, grants: readonly AuthzGrant[]): Promise<void> {\n if (roleIds.size === 0) return\n const assignments = await tx.listGroupRoles(tenantId)\n // 同一グループへの重複問い合わせを避ける (グループ数ぶんのリンク参照で足りる)\n const seen = new Set<number>()\n for (const a of assignments) {\n if (!roleIds.has(a.roleId) || seen.has(a.groupId)) continue\n seen.add(a.groupId)\n const externalKey = await opts.linkLookup.externalGroupKey(tenantId, a.groupId, runner)\n if (externalKey !== null) throw new Error(opts.message(a.roleKey || roleLabel(grants, a.roleId), externalKey))\n }\n }\n\n return {\n ...target,\n\n withTransaction: (fn, options) => plain.withTransaction((tx, runner) => fn(decorate(tx, opts), runner), options),\n\n async assignGroupRole(tenantId: string, groupId: number, roleId: number, actor: string): Promise<void> {\n // 証拠の読取と書込を 1 つの直列化トランザクションに閉じる。分けると、検査を通った\n // 直後・書込の前に前提が動く。しかも並行する 2 つの操作が **どちらも単独では通る**\n // 検査を同時に通過でき、その結果だけが不変条件を破る\n await plain.withTransaction(async (tx, runner) => {\n const grants = await tx.listGrants(tenantId)\n // ロールの有効・無効は見ない。無効な管理ロールを外部グループが保持できてしまうと、\n // 有効化するだけで上流が管理者を握る状態が完成する\n if (confersAdministration(adminActionsOf(grants, roleId, opts.adminAppKey, opts.adminResourceKey))) {\n const externalKey = await opts.linkLookup.externalGroupKey(tenantId, groupId, runner)\n if (externalKey !== null) throw new Error(opts.message(roleLabel(grants, roleId), externalKey))\n }\n await tx.assignGroupRole(tenantId, groupId, roleId, actor)\n })\n },\n\n async createGrant(tenantId: string, input: Parameters<AuthzAdminStore[\"createGrant\"]>[1], actor: string): Promise<void> {\n await plain.withTransaction(async (tx, runner) => {\n const resources = await tx.listResources(tenantId)\n const target = resources.find((r) => r.id === input.resourceId)\n // 管理リソース以外への付与は、このロールを管理ロールにしない\n if (target?.appKey === opts.adminAppKey && target?.resourceKey === opts.adminResourceKey) {\n const grants = await tx.listGrants(tenantId)\n const projected = adminActionsOf(grants, input.roleId, opts.adminAppKey, opts.adminResourceKey)\n projected.add(input.action)\n // この付与でロールが管理ロールに「なる」場合のみ、保持者を調べる\n if (confersAdministration(projected)) await refuseIfHeldByExternalGroup(tx, runner, tenantId, new Set([input.roleId]), grants)\n }\n await tx.createGrant(tenantId, input, actor)\n })\n },\n }\n}\n\n/**\n * Refuses to link a group that already holds an administrative role.\n * 既に管理ロールを保持しているグループの外部連携を拒否する処理。\n *\n * 3 つ目の方向。これを通さずにリンクを作ると、ローカルで管理ロールを付けてから連携させる経路が\n * 残る。{@link withExternalGroupGuards} と違って呼び出し側が忘れ得るため、リンクを作る側の\n * 構成に必須ポートとして組み込むこと。\n *\n * @param store Authorization admin store used to read the grants. 付与を読む認可管理ストア。\n * @param tenantId Tenant the group belongs to. グループが属するテナント。\n * @param groupId Group about to be linked. 連携しようとしているグループ。\n * @param opts Administrative resource identity and refusal message. 管理リソースの同定と文言。\n * @returns Nothing; returns normally only when linking is allowed. 許可される場合のみ正常復帰。\n * @throws {Error} When the group holds the administrative role. 管理ロールを保持している場合。\n */\nexport async function assertGroupMayBeLinked(store: AuthzAdminStore, tenantId: string, groupId: number, opts: Pick<ExternalGroupGuardOptions, \"adminAppKey\" | \"adminResourceKey\" | \"message\">): Promise<void> {\n const assignments = (await store.listGroupRoles(tenantId)).filter((a) => a.groupId === groupId)\n if (assignments.length === 0) return\n const grants = await store.listGrants(tenantId)\n for (const a of assignments) {\n if (confersAdministration(adminActionsOf(grants, a.roleId, opts.adminAppKey, opts.adminResourceKey))) throw new Error(opts.message(a.roleKey || roleLabel(grants, a.roleId), a.groupKey))\n }\n}\n","/**\n * Resolves which member-ledger rows belong to one identity.\n * 台帳のどの行が 1 人の identity のものかを解決するモジュール。\n *\n * 台帳 (`TDGroupMemberEmail`) は「上流に居る人」を、ローカルユーザーへ解決できているかに\n * 関わらず保持する。ここはその台帳から本人の行だけを取り出す唯一の場所であり、帰属証明\n * (`listLedgerGroupIds`) と初回ログイン時の昇格 (`promoteLedgerForUser`) は同じ答えを見る。\n * 突合規則が 2 箇所にあると、片方だけが「本人」と認めた瞬間に権限が食い違う。\n */\nimport type { AuthzTables } from \"@aiquants/authz-drizzle\"\nimport { and, eq } from \"drizzle-orm\"\nimport type { DirectoryDb } from \"./db\"\nimport type { AuthDirectoryTables } from \"./schema\"\n\n/** One ledger row attributed to an identity. identity に帰属した台帳 1 行。 */\nexport type LedgerIdentityRow = {\n ledgerId: number\n groupId: number\n /** Inherited from the parent group — the ledger has no tenant column. 親グループ由来。 */\n tenantId: string\n}\n\n/** The two tables this resolution spans. この解決が跨る 2 表。 */\nexport type LedgerIdentityTables = {\n /** `TDGroupMemberEmail`. */\n ledger: AuthDirectoryTables[\"TDGroupMemberEmail\"]\n /** `TMGroup` — owns the tenant and the active flag. テナントと有効フラグの所有者。 */\n group: AuthzTables[\"TMGroup\"]\n}\n\ntype Row = { ledgerId: number; groupId: number; tenantId: string; externalUserId: string | null }\n\n/**\n * Finds every ledger row that belongs to the identity, within one tenant.\n * 1 テナント内で、その identity に属する台帳行をすべて探す処理。\n *\n * 上流 ID の一致は無条件に本人。アドレスの一致は、その行が **別の** 上流 ID に紐付いていない\n * ときだけ本人である。単純な `OR` にすると後者の条件が消え、退職者のアドレスを再利用した\n * 新入社員が前任者の所属をそのまま継承する。\n *\n * ⚠️ この優先順位は **行ごと** に適用する。「ID で当たったらアドレスは見ない」という打ち切りは\n * 誤りで、同じ人物の行が ID 付きと ID 無しに混在すると ID 無しの行が丸ごと落ちる。\n *\n * @param db - Connected database surface. 接続済みの DB 面。\n * @param tables - The ledger and its parent group table. 台帳と親グループ表。\n * @param args - Tenant scope and the identity to match. テナント範囲と突合対象の identity。\n * @returns Matching rows, deduplicated and ordered by ledger id. 重複排除・台帳 ID 昇順の一致行。\n */\nexport async function findLedgerRowsForIdentity(db: DirectoryDb, tables: LedgerIdentityTables, args: { tenantId: string; externalUserId: string | null; email: string }): Promise<LedgerIdentityRow[]> {\n const { ledger, group } = tables\n const { tenantId, externalUserId, email } = args\n if (externalUserId === null && email === \"\") return []\n\n const project = { ledgerId: ledger.id, groupId: ledger.groupId, tenantId: group.tenantId, externalUserId: ledger.externalUserId }\n /** Runs one identity probe. 1 つの identity 条件で台帳を引く処理。 */\n const probe = async (condition: unknown): Promise<Row[]> => {\n const rows = await db\n .select<Row>(project)\n .from(ledger)\n // 無効化されたグループは証拠にしない。凍結した台帳で帰属証明が通り続けると、\n // 同期が止まった状態のまま在籍者が増え続ける\n .innerJoin(group, and(eq(group.id, ledger.groupId), eq(group.tenantId, tenantId), eq(group.isActive, true)))\n .where(condition)\n return rows.map((r) => ({ ledgerId: Number(r.ledgerId), groupId: Number(r.groupId), tenantId: String(r.tenantId), externalUserId: r.externalUserId === null ? null : String(r.externalUserId) }))\n }\n\n /** Deduplicates by ledger id and drops the identity column. 台帳 ID で重複排除する処理。 */\n const finalize = (rows: Row[]): LedgerIdentityRow[] => {\n const byId = new Map<number, LedgerIdentityRow>()\n for (const r of rows) byId.set(r.ledgerId, { ledgerId: r.ledgerId, groupId: r.groupId, tenantId: r.tenantId })\n return [...byId.values()].sort((a, b) => a.ledgerId - b.ledgerId)\n }\n\n // 上流 ID の一致は無条件に本人である。アドレスの一致は、その行が **別の** 上流 ID に\n // 紐付いていないときだけ本人である\n const byId = externalUserId === null ? [] : await probe(eq(ledger.externalUserId, externalUserId))\n if (email === \"\") return finalize(byId)\n const byEmail = await probe(eq(ledger.email, email))\n const claimedByAnother = (row: Row) => externalUserId !== null && row.externalUserId !== null && row.externalUserId !== externalUserId\n // 優先順位は **行ごと** に適用する。「ID で 1 件でも当たったらアドレスは見ない」と実装すると、\n // 同じ人物の行が ID 付きと ID 無しに混在したとき (上流が ID を返さない経路・ID 採取前に\n // 書かれた行) に ID 無しの行が丸ごと落ち、identity を多く渡すほど所属が減る\n return finalize([...byId, ...byEmail.filter((r) => !claimedByAnother(r))])\n}\n","/**\n * @module link-store\n * @description Reads and writes the identity-side tables: external group links, the upstream member\n * ledger, tenant membership proof, group-based login allowlist, and the sync request queue.\n * identity 側テーブルの読み書き。外部グループリンク・上流メンバー台帳・テナント帰属証明・\n * グループ単位のログイン許可・同期要求キュー。\n *\n * **テナントの扱いは 2 種類ある。混同すると事故になる。**\n *\n * - **絞り込み条件**: リンク・帰属証明グループ。読み書きの双方で SQL 述語になる。\n * - **所有者**: グループ許可。管理 UI の編集スコープを決めるだけで、**ログイン判定では\n * 述語にしない** — 認証はテナントの確定より前に起きるため、判定の時点でテナントは存在しない。\n * このモジュールでは {@link createDirectoryLinkStore} の `isEmailAllowedByGroup` だけが\n * その例外であり、意図的に全テナントを対象とする。\n */\nimport { type AuthAllowlistEntry, type AuthDirectoryMembershipMode, type AuthDirectoryStatus, type AuthGroupExternalLink, normalizeAuthEmail } from \"@aiquants/auth-core\"\nimport { assertTenantMembershipPolicy, type TenantMembershipPolicy } from \"@aiquants/auth-directory-core\"\nimport { assertTenantId, assertTenantMatch } from \"@aiquants/authz-core\"\nimport type { AuthzTables } from \"@aiquants/authz-drizzle\"\nimport { and, asc, count, eq, isNull } from \"drizzle-orm\"\nimport type { DirectoryDb, EnsureConnection } from \"./db\"\nimport { findLedgerRowsForIdentity } from \"./ledger-identity\"\nimport { assertMembershipMode } from \"./membership-mode\"\nimport type { AuthDirectoryTables } from \"./schema\"\n\n/**\n * Proves a group may become externally sourced. グループを外部供給にしてよいことの証明。\n *\n * `assertGroupMayBeLinked` を呼ぶだけのポートである。自由関数のままだと呼び出し側が忘れられ、\n * 「ローカルで管理ロールを付けてから連携させる」経路が残る。必須の構成項目にすることで、\n * 忘れることを構造的に不可能にする。\n */\nexport type LinkableAssertion = {\n /**\n * Owns a transaction, proves the group may be linked on it, then runs `write` on the same runner.\n * トランザクションを所有し、その上で連携可能であることを証明してから、同じランナーで `write` を実行する処理。\n *\n * ⚠️ **証明だけを返す形にしてはならない。** 証明と連携の作成が別のトランザクションだと、\n * 証明を通った直後・リンクを作る前に、そのグループへ管理ロールを割り当てる操作が\n * 割り込める。割り当て側の検査は「まだリンクが無い」と見て通り、連携側の検査は\n * 「まだ管理ロールが無い」と見て通る — **どちらも単独では正しく、結果だけが不変条件を破る**。\n *\n * @param tenantId - Tenant that owns the group. グループを所有するテナント。\n * @param groupId - Group about to become externally sourced. 外部供給にしようとしているグループ。\n * @param write - The link creation, run on the proving transaction. 証明したトランザクション上で行う連携作成。\n * @returns Nothing. 戻り値なし。\n * @throws When the group holds an administrative role. 管理ロールを保持している場合。\n */\n assertLinkableAndWrite(tenantId: string, groupId: number, write: (runner: unknown) => Promise<void>): Promise<void>\n}\n\n/** Options for the link store. リンクストアの構成。 */\nexport type DirectoryLinkStoreOptions = {\n /** Required — see {@link LinkableAssertion}. 必須 (忘れられる経路を作らないため)。 */\n linkable: LinkableAssertion\n /** Establishes the connection before the first statement. 最初の文の前に接続を確立する処理。 */\n ensureConnection?: EnsureConnection\n /**\n * Seconds after which a successful sync counts as overdue. 同期が期限超過とみなされる秒数。\n *\n * 同期間隔の数倍を指定する。短すぎると常時警告が出て誰も読まなくなり、長すぎると停止に\n * 気付けない。\n */\n stalenessThresholdSec: number\n /** False when directory integration is not wired for this deployment. 未結線なら false。 */\n isEnabled: boolean\n /**\n * Upstream system this deployment synchronizes against, e.g. `\"google\"`. 同期先の上流システム。\n *\n * 新しい連携を作る面へそのまま渡る。UI に供給元を焼き込ませないための注入点であり、\n * 同期ランナーに設定する値と同一でなければならない — 食い違うと、作られたリンクは\n * 同期側で「別プロバイダ」として恒久的に拒否される。\n */\n provider: string\n /**\n * Milliseconds to reuse a loaded tenant-membership policy. 帰属方針を再利用するミリ秒。\n *\n * 既定は 0 (毎回引き直す)。帰属証明は **権限を 1 件も読む前に毎リクエスト走る** ため、\n * 証明グループを 1 つも宣言していない配備でも 3 表の結合が 1 往復増える。\n *\n * ⚠️ 既定を非 0 にしない。認可の判断材料を、頼まれてもいないのに黙って古くするライブラリは\n * 信用できない。有効化は配備側が「どれだけ古くてよいか」を決めて明示する。\n * 本ストア自身の書き込み (証明グループの増減・連携の作成解除) は自動で破棄するため、\n * 遅れが出るのは **記憶域を直接編集した場合だけ** である。\n */\n policyCacheTtlMs?: number\n}\n\n/** One external link row as the admin surface sees it. 管理面が見る外部リンク行。 */\nexport type DirectoryLinkRow = AuthGroupExternalLink & { linkId: number; groupId: number; tenantId: string }\n\n/** Input for creating a link. リンク作成の入力。 */\nexport type LinkGroupInput = {\n provider: string\n externalId: string\n externalKey: string\n membershipMode: AuthDirectoryMembershipMode\n}\n\n/**\n * Creates the identity-side data store.\n * identity 側のデータストアを生成する処理。\n *\n * @param dbUnknown Drizzle database instance. drizzle のデータベースインスタンス。\n * @param tables Identity-side tables. identity 側テーブル群。\n * @param authzTables Authorization tables (for the group master and the real membership).\n * 認可テーブル群 (グループマスタと実所属表のため)。\n * @param opts Store options. ストアの構成。\n * @returns The store. ストア。\n */\nexport function createDirectoryLinkStore(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: DirectoryLinkStoreOptions) {\n const db = dbUnknown as DirectoryDb\n const ready = opts.ensureConnection ?? (async () => {})\n const policyCacheTtlMs = opts.policyCacheTtlMs ?? 0\n if (!Number.isFinite(policyCacheTtlMs) || policyCacheTtlMs < 0) {\n throw new TypeError(`[@aiquants/auth-directory-drizzle] policyCacheTtlMs must be a non-negative number, received ${String(opts.policyCacheTtlMs)}`)\n }\n /** Per-tenant memo of the membership policy. テナント単位の帰属方針メモ。 */\n const policyCache = new Map<string, { policy: TenantMembershipPolicy; loadedAt: number }>()\n /** Drops memos this store's own writes could have invalidated. 自身の書込が無効化し得るメモを捨てる処理。 */\n const forgetPolicy = (tenantId: string) => policyCache.delete(tenantId)\n const { TDGroupExternalLink: link, TDGroupMemberEmail: ledger, TDTenantMembershipGroup: membership, TDAllowlistGroup: allowGroup, TDDirectorySyncRequest: queue } = tables\n const { TMGroup: group, TDGroupMember: groupMember } = authzTables\n\n /**\n * Refuses to make a break-glass proof group externally sourced.\n * break-glass の証明グループを外部供給にすることを拒否する処理。\n *\n * 不変条件を読み取り側 (`loadTenantMembershipPolicy`) だけで守ると、1 回の連携操作が\n * **全リクエストの帰属証明を落とす** 全面障害に化ける。遷移が起きるここで断る。\n *\n * @param runner - The transaction issuing the write. 書き込みを発行するトランザクション。\n * @param tenantId - Owning tenant. 所有テナント。\n * @param groupId - Group about to be linked. 連携しようとしているグループ。\n * @returns Nothing. 戻り値なし。\n * @throws {Error} When the group is a break-glass proof group. break-glass だった場合。\n */\n async function assertNotBreakGlass(runner: DirectoryDb, tenantId: string, groupId: number): Promise<void> {\n const rows = await runner\n .select<{ groupId: number }>({ groupId: membership.groupId })\n .from(membership)\n .where(and(eq(membership.tenantId, tenantId), eq(membership.groupId, groupId), eq(membership.isBreakGlass, true)))\n if (rows.length > 0) {\n throw new Error(`Group ${groupId} is a break-glass proof group; a break-glass proof path must be one the synchronization can never touch.`)\n }\n }\n\n /**\n * Proves the group belongs to the tenant before any row that references it is written.\n * そのグループを参照する行を書く前に、テナントへの帰属を証明する処理。\n *\n * リンク表・台帳・許可表はいずれも `TMGroup` を参照するが、テナントの正準値は親側にある。\n * ID を素通しで受けると、他テナントのグループへ紐付けを作れてしまう。\n *\n * @throws {Error} When the group does not exist in this tenant. 自テナントに存在しない場合。\n */\n async function assertGroupInTenant(runner: DirectoryDb, tenantId: string, groupId: number): Promise<void> {\n const [owned] = await runner\n .select<{ id: number }>({ id: group.id })\n .from(group)\n .where(and(eq(group.id, groupId), eq(group.tenantId, tenantId)))\n if (!owned) throw new Error(`Group not found in tenant: ${groupId}`)\n }\n\n /** Maps a raw link row plus its unresolved count onto the public shape. 公開形への変換。 */\n function toLinkRow(row: Record<string, unknown>, unresolvedCount: number): DirectoryLinkRow {\n return {\n linkId: Number(row.linkId),\n groupId: Number(row.groupId),\n tenantId: String(row.tenantId),\n provider: String(row.provider),\n externalId: String(row.externalId),\n externalKey: String(row.externalKey),\n membershipMode: assertMembershipMode(String(row.membershipMode)),\n isSyncEnabled: Boolean(row.isSyncEnabled),\n ...(row.syncedAt ? { syncedAt: new Date(row.syncedAt as string).toISOString() } : {}),\n ...(row.syncError ? { syncError: String(row.syncError) } : {}),\n unresolvedCount,\n }\n }\n\n /** Counts ledger rows that resolved to no local user, per group. グループ別の未解決件数。 */\n async function unresolvedCounts(runner: DirectoryDb, tenantId: string): Promise<Map<number, number>> {\n const rows = await runner\n .select<{ groupId: number; n: number }>({ groupId: ledger.groupId, n: count() })\n .from(ledger)\n .innerJoin(group, and(eq(group.id, ledger.groupId), eq(group.tenantId, tenantId)))\n .where(isNull(ledger.resolvedUserId))\n .groupBy(ledger.groupId)\n return new Map(rows.map((r) => [Number(r.groupId), Number(r.n)]))\n }\n\n return {\n /**\n * Lists every external link of the tenant. 当該テナントの外部リンクを列挙する処理。\n *\n * @param tenantId Tenant to narrow on. 絞り込むテナント。\n */\n async listLinks(tenantId: string): Promise<DirectoryLinkRow[]> {\n assertTenantId(tenantId, \"listLinks(tenantId)\")\n await ready()\n const rows = await db\n .select<Record<string, unknown>>({\n linkId: link.id,\n groupId: link.groupId,\n tenantId: link.tenantId,\n provider: link.provider,\n externalId: link.externalId,\n externalKey: link.externalKey,\n membershipMode: link.membershipMode,\n isSyncEnabled: link.isSyncEnabled,\n syncedAt: link.syncedAt,\n syncError: link.syncError,\n })\n .from(link)\n .where(eq(link.tenantId, tenantId))\n .orderBy(asc(link.groupId))\n const counts = await unresolvedCounts(db, tenantId)\n // テナントは行から射影した実値で再検証する。SQL の比較は照合順序に従うため、\n // 述語だけでは厳密一致の契約を満たさない\n return rows.map((r) => {\n assertTenantMatch(tenantId, String(r.tenantId), \"listLinks(row.tenantId)\")\n return toLinkRow(r, counts.get(Number(r.groupId)) ?? 0)\n })\n },\n\n /**\n * Returns the upstream key of the group, or `null` when it is locally managed.\n * そのグループの上流キーを返す処理。ローカル管理なら `null`。\n *\n * `withExternalGroupGuards` の判定材料であり、同時に拒否メッセージの表示名でもある。\n * 判定と名前を 1 回の照会で返すことで、拒否のたびの往復を 1 本に保つ。\n */\n async externalGroupKey(tenantId: string, groupId: number, runner?: unknown): Promise<string | null> {\n assertTenantId(tenantId, \"externalGroupKey(tenantId)\")\n await ready()\n // ⚠️ ランナーを受け取れることが要点である。呼び出し側が「この読み取りの結果を前提に\n // 書き込む」とき、読み取りだけ別接続で行うと、その前提は書込が着地するまでに崩れ得る。\n // 同じトランザクションで引けば、リンク行に範囲ロックが掛かり前提が保たれる\n const [row] = await ((runner as DirectoryDb | undefined) ?? db)\n .select<{ externalKey: string }>({ externalKey: link.externalKey })\n .from(link)\n .where(and(eq(link.groupId, groupId), eq(link.tenantId, tenantId)))\n return row ? String(row.externalKey) : null\n },\n\n /**\n * Links a local group to an upstream group. ローカルグループを上流グループへ結び付ける処理。\n *\n * 既に管理ロールを持つグループは {@link LinkableAssertion} が拒否する。\n *\n * @throws {Error} When the group belongs to another tenant. 他テナントのグループの場合。\n */\n async linkGroup(tenantId: string, groupId: number, input: LinkGroupInput, actor: string): Promise<void> {\n assertTenantId(tenantId, \"linkGroup(tenantId)\")\n const membershipMode = assertMembershipMode(input.membershipMode)\n const externalId = input.externalId.trim()\n const externalKey = normalizeAuthEmail(input.externalKey)\n if (externalId === \"\" || externalKey === \"\") throw new TypeError(\"[@aiquants/auth-directory-drizzle] externalId and externalKey must be non-empty\")\n await ready()\n // 管理ロールを保持したまま連携させると、上流側の所属変更がそのまま管理者の増減になる。\n // 証明とリンク作成は **同じトランザクション** で行う。分けると、証明を通った直後に\n // そのグループへ管理ロールを割り当てる操作が割り込め、双方の検査が個別に通ったまま\n // 「外部供給グループが管理ロールを持つ」状態が成立する\n await opts.linkable.assertLinkableAndWrite(tenantId, groupId, async (runner) => {\n const tx = runner as DirectoryDb\n await assertGroupInTenant(tx, tenantId, groupId)\n await assertNotBreakGlass(tx, tenantId, groupId)\n await tx.insert(link).output().values({ groupId, tenantId, provider: input.provider, externalId, externalKey, membershipMode, isSyncEnabled: true, vanishedObservations: 0, createdAt: new Date(), createdBy: actor })\n })\n // 方針の材料が変わった。自身の書込に限っては、遅れを一切許さない\n forgetPolicy(tenantId)\n },\n\n /**\n * Removes the link, leaving the projected membership in place.\n * リンクを解除する処理。射影済みの所属はそのまま残す。\n *\n * 所属まで消すと、連携を切っただけで全員がグループから外れる (= 大量剥奪) 。解除は\n * 「上流の言うことを聞くのをやめる」であって「今の所属を捨てる」ではない。台帳と、\n * 台帳を通してしか効かないグループ許可は、残しても意味を持たないため削除する。\n */\n async unlinkGroup(tenantId: string, groupId: number): Promise<void> {\n assertTenantId(tenantId, \"unlinkGroup(tenantId)\")\n await ready()\n await db.transaction(async (tx) => {\n await assertGroupInTenant(tx, tenantId, groupId)\n await tx.delete(ledger).where(eq(ledger.groupId, groupId))\n // グループ許可は台帳を通してしか効かない。台帳を消して許可行だけ残すと、\n // 昨日まで効いていた設定が無言の no-op になる\n await tx.delete(allowGroup).where(and(eq(allowGroup.groupId, groupId), eq(allowGroup.tenantId, tenantId)))\n await tx.delete(link).where(and(eq(link.groupId, groupId), eq(link.tenantId, tenantId)))\n })\n // 連携の有無は break-glass の成否を左右する (上流所有の break-glass は拒否される)\n forgetPolicy(tenantId)\n },\n\n /** Pauses or resumes synchronization for one group. 1 グループの同期を停止・再開する処理。 */\n async setSyncEnabled(tenantId: string, groupId: number, isSyncEnabled: boolean, actor: string): Promise<void> {\n assertTenantId(tenantId, \"setSyncEnabled(tenantId)\")\n await ready()\n await db\n .update(link)\n .set({ isSyncEnabled, updatedAt: new Date(), updatedBy: actor })\n .output()\n .where(and(eq(link.groupId, groupId), eq(link.tenantId, tenantId)))\n },\n\n /**\n * Queues a synchronization request. 同期要求をキューへ積む処理。\n *\n * web プロセスは上流へ到達できないため、要求を行として残し同期ジョブに拾わせる。\n * 誰がいつ何を要求し、結果がどうだったかがそのまま監査記録になる。\n *\n * @param groupId `null` requests every group of the tenant. `null` はテナント全件。\n */\n async requestSync(tenantId: string, groupId: number | null, requestedBy: string): Promise<void> {\n assertTenantId(tenantId, \"requestSync(tenantId)\")\n await ready()\n await db.transaction(async (tx) => {\n if (groupId !== null) await assertGroupInTenant(tx, tenantId, groupId)\n await tx.insert(queue).output().values({ tenantId, groupId, requestedBy, requestedAt: new Date(), createdAt: new Date(), createdBy: requestedBy })\n })\n },\n\n /**\n * Summarizes synchronization health for the admin header.\n * 管理ヘッダー向けに同期の健全性を要約する処理。\n *\n * 一度も成功していない場合も陳腐化として報告する。「不明」を健全側へ倒さない —\n * 判定できないことは、健全であることの証明ではない。\n */\n async getStatus(tenantId: string): Promise<AuthDirectoryStatus> {\n assertTenantId(tenantId, \"getStatus(tenantId)\")\n await ready()\n const rows = await db\n .select<{ groupId: number; isSyncEnabled: boolean; syncedAt: string | null; syncError: string | null }>({\n groupId: link.groupId,\n isSyncEnabled: link.isSyncEnabled,\n syncedAt: link.syncedAt,\n syncError: link.syncError,\n })\n .from(link)\n .where(eq(link.tenantId, tenantId))\n const [pending] = await db\n .select<{ n: number }>({ n: count() })\n .from(queue)\n .where(and(eq(queue.tenantId, tenantId), isNull(queue.finishedAt)))\n\n const enabled = rows.filter((r) => Boolean(r.isSyncEnabled))\n const stamps = enabled.map((r) => (r.syncedAt ? new Date(r.syncedAt).getTime() : null))\n const oldest = stamps.length > 0 && stamps.every((t): t is number => t !== null) ? Math.min(...stamps) : null\n // リンクがあるのに 1 本も有効でない状態を「健全」と報告してはならない。台帳は凍結した\n // まま権限を配り続けるのに、画面上は N 件連携済み・エラー 0・陳腐化なしに見える\n const allPaused = rows.length > 0 && enabled.length === 0\n const isStale = allPaused || (enabled.length > 0 && (oldest === null || Date.now() - oldest > opts.stalenessThresholdSec * 1000))\n return {\n isEnabled: opts.isEnabled,\n provider: opts.provider,\n linkedGroupCount: rows.length,\n ...(oldest !== null ? { oldestSyncedAt: new Date(oldest).toISOString() } : {}),\n isStale,\n pausedGroupCount: rows.length - enabled.length,\n stalenessThresholdSec: opts.stalenessThresholdSec,\n erroredGroupIds: rows\n .filter((r) => Boolean(r.syncError))\n .map((r) => Number(r.groupId))\n .sort((a, b) => a - b),\n pendingRequestCount: Number(pending?.n ?? 0),\n }\n },\n\n /**\n * Builds the tenant's membership policy, proving every break-glass group is local.\n * テナントの帰属方針を組み立てる処理。break-glass がローカル管理であることも証明する。\n *\n * 「証明を要求するか」を行数から推測しない。宣言が 1 行も無ければ `no-proof-required`\n * という **方針** を返し、宣言があれば `require-proof` を返す。区別を型の上に残すことで、\n * 「宣言し忘れ」や「障害で 0 件が返った」が「証明不要」に化ける経路を塞ぐ。\n *\n * @throws {Error} When a break-glass group is externally sourced. 上流所有だった場合。\n * @throws {TypeError} When the resulting policy is unsatisfiable. 成立しない方針の場合。\n */\n async loadTenantMembershipPolicy(tenantId: string): Promise<TenantMembershipPolicy> {\n assertTenantId(tenantId, \"loadTenantMembershipPolicy(tenantId)\")\n const memo = policyCacheTtlMs > 0 ? policyCache.get(tenantId) : undefined\n if (memo && Date.now() - memo.loadedAt < policyCacheTtlMs) return memo.policy\n await ready()\n const rows = await db\n .select<{ groupId: number; isBreakGlass: boolean; externalLinkId: number | null }>({ groupId: membership.groupId, isBreakGlass: membership.isBreakGlass, externalLinkId: link.id })\n .from(membership)\n .innerJoin(group, and(eq(group.id, membership.groupId), eq(group.tenantId, tenantId)))\n .leftJoin(link, eq(link.groupId, membership.groupId))\n .where(eq(membership.tenantId, tenantId))\n if (rows.length === 0) {\n const empty: TenantMembershipPolicy = { kind: \"no-proof-required\" }\n if (policyCacheTtlMs > 0) policyCache.set(tenantId, { policy: empty, loadedAt: Date.now() })\n return empty\n }\n\n const proofGroupIds = rows.map((r) => Number(r.groupId))\n const breakGlass = rows.filter((r) => Boolean(r.isBreakGlass))\n for (const r of breakGlass) {\n // 上流所有のグループを break-glass にしても復帰経路にならない。上流の事故が\n // すべての証明経路を同時に壊すため、そこにこそ触れない経路が要る\n if (r.externalLinkId !== null) throw new Error(`Break-glass group ${Number(r.groupId)} is externally sourced; a break-glass proof path must be one the synchronization can never touch.`)\n }\n const policy: TenantMembershipPolicy = { kind: \"require-proof\", proofGroupIds, breakGlassGroupIds: breakGlass.map((r) => Number(r.groupId)) }\n assertTenantMembershipPolicy(policy, `tenant ${tenantId}`)\n // 成立した方針だけを覚える。例外を覚えると、記憶域を直した後も TTL のあいだ\n // 落ち続ける (直したのに直らない、という最も追いにくい状態になる)\n if (policyCacheTtlMs > 0) policyCache.set(tenantId, { policy, loadedAt: Date.now() })\n return policy\n },\n\n /**\n * Declares a group as proof of tenant membership. グループをテナント帰属の証明に指定する処理。\n *\n * この表に 1 行でも入った瞬間、そのテナントの帰属証明は「証明必須」へ切り替わる。行が\n * 無い状態は「証明不要」であり、書き込み手段が無ければ機能そのものが到達不能になる。\n *\n * `isBreakGlass` は「同期が決して触れない復帰経路」を意味する。上流の事故はすべての\n * 連携グループを同時に壊すため、そこにこそ外部供給でない経路が要る。\n *\n * @param tenantId - Owning tenant. 所有テナント。\n * @param groupId - Group to declare. 指定するグループ。\n * @param isBreakGlass - True for the locally-managed recovery path. 復帰経路なら true。\n * @param actor - Who performed it. 実行者。\n * @returns Nothing. 戻り値なし。\n * @throws {Error} When the group is not in the tenant, or a break-glass group is linked.\n * 自テナントに無い場合、または break-glass に連携済みグループを指定した場合。\n */\n async setTenantMembershipGroup(tenantId: string, groupId: number, isBreakGlass: boolean, actor: string): Promise<void> {\n assertTenantId(tenantId, \"setTenantMembershipGroup(tenantId)\")\n await ready()\n await db.transaction(async (tx) => {\n await assertGroupInTenant(tx, tenantId, groupId)\n if (isBreakGlass) {\n // 上流所有のグループを break-glass にしても復帰経路にならない。読み取り側でも\n // 拒否しているが、そこで初めて落ちると 1 行の投入が全リクエストを止める\n const [linked] = await tx\n .select<{ id: number }>({ id: link.id })\n .from(link)\n .where(and(eq(link.tenantId, tenantId), eq(link.groupId, groupId)))\n if (linked) throw new Error(`Group ${groupId} is externally sourced; a break-glass proof path must be one the synchronization can never touch.`)\n }\n const existing = await tx\n .select<{ groupId: number }>({ groupId: membership.groupId })\n .from(membership)\n .where(and(eq(membership.tenantId, tenantId), eq(membership.groupId, groupId)))\n if (existing.length > 0) {\n await tx\n .update(membership)\n .set({ isBreakGlass, updatedAt: new Date(), updatedBy: actor })\n .output()\n .where(and(eq(membership.tenantId, tenantId), eq(membership.groupId, groupId)))\n forgetPolicy(tenantId)\n return\n }\n await tx.insert(membership).output().values({ tenantId, groupId, isBreakGlass, createdAt: new Date(), createdBy: actor })\n })\n // 方針の材料が変わった。自身の書込に限っては、遅れを一切許さない\n forgetPolicy(tenantId)\n },\n\n /**\n * Withdraws a group from the tenant membership proof.\n * グループをテナント帰属の証明から外す処理。\n *\n * 最後の break-glass 行を外すと、以降の証明は上流だけに依存する。上流の事故で全員が\n * 締め出される状態になるため、読み取り側の検証 (`assertTenantMembershipPolicy`) が\n * 成立しない方針を拒否する — ここで先に断る。\n *\n * @param tenantId - Owning tenant. 所有テナント。\n * @param groupId - Group to withdraw. 外すグループ。\n * @returns Nothing. 戻り値なし。\n * @throws {Error} When it is the last break-glass group of a non-empty policy.\n * 証明が残るのに最後の break-glass を外そうとした場合。\n */\n async removeTenantMembershipGroup(tenantId: string, groupId: number): Promise<void> {\n assertTenantId(tenantId, \"removeTenantMembershipGroup(tenantId)\")\n await ready()\n await db.transaction(async (tx) => {\n const rows = await tx.select<{ groupId: number; isBreakGlass: boolean }>({ groupId: membership.groupId, isBreakGlass: membership.isBreakGlass }).from(membership).where(eq(membership.tenantId, tenantId))\n const remaining = rows.filter((r) => Number(r.groupId) !== groupId)\n // 全部外すのは構わない (証明不要へ戻るだけ)。危険なのは、証明を残したまま\n // 復帰経路だけを失うことである\n if (remaining.length > 0 && !remaining.some((r) => Boolean(r.isBreakGlass))) {\n throw new Error(`Removing group ${groupId} would leave tenant ${tenantId} with a membership proof but no break-glass path.`)\n }\n await tx.delete(membership).where(and(eq(membership.tenantId, tenantId), eq(membership.groupId, groupId)))\n })\n forgetPolicy(tenantId)\n },\n\n /**\n * Reactivates a group that was deactivated after vanishing upstream.\n * 上流消滅で無効化されたグループを再有効化する処理。\n *\n * 同期は成功時に自動で再有効化するが、運用者が先に直したい場合の入口も要る。無効化を\n * 一方通行の扉にしないための、もう 1 本の出口である。\n */\n async setGroupActive(tenantId: string, groupId: number, isActive: boolean, actor: string): Promise<void> {\n assertTenantId(tenantId, \"setGroupActive(tenantId)\")\n await ready()\n await db\n .update(group)\n .set({ isActive, updatedAt: new Date(), updatedBy: actor })\n .output()\n .where(and(eq(group.id, groupId), eq(group.tenantId, tenantId)))\n },\n\n /**\n * Reports the groups the identity matches in the upstream ledger.\n * 上流台帳で identity が一致するグループを報告する処理。\n *\n * ⚠️ 実所属表ではなく **台帳** を見ること。初回ログインでユーザー行が生まれた直後、\n * まだ射影されていない瞬間に、正規の利用者が「非メンバー」として全拒否されるのを防ぐ。\n */\n async listLedgerGroupIds(tenantId: string, identity: { externalUserId: string | null; email: string | null | undefined }): Promise<number[]> {\n assertTenantId(tenantId, \"listLedgerGroupIds(tenantId)\")\n // 配備スイッチが off のとき、台帳は更新されない。止まった台帳を根拠に判断を続けると、\n // スイッチは「運用者の制御を減らすだけで、上流由来の権限は減らさない」ものになる。\n // 既に書かれた所属行 (`TDGroupMember`) はローカルの資産として残る\n if (!opts.isEnabled) return []\n await ready()\n const rows = await findLedgerRowsForIdentity(db, { ledger, group }, { tenantId, externalUserId: identity.externalUserId?.trim() || null, email: normalizeAuthEmail(identity.email) })\n return [...new Set(rows.map((r) => r.groupId))].sort((a, b) => a - b)\n },\n\n /**\n * Reports whether an address may sign in through a group allowlist entry.\n * グループ許可によってそのアドレスがサインインできるかを判定する処理。\n *\n * ⚠️ **テナント述語を持たない。意図的である。** 認証はテナントの確定より前に起きるため、\n * 判定の時点でテナントは存在しない。この配備が受け持つ全テナントの和集合で判定する。\n * ここに述語を足すと初回ログインが全滅する。\n *\n * 判定は実所属ではなく **台帳** を見る。初回ログインの時点では、そもそもユーザー行が\n * まだ存在しない。\n */\n async isEmailAllowedByGroup(email: string | null | undefined): Promise<boolean> {\n // 同上。停止中の台帳でサインインを許し続けると、切ったつもりの経路が生き残る\n if (!opts.isEnabled) return false\n const normalized = normalizeAuthEmail(email)\n if (normalized === \"\") return false\n await ready()\n const [row] = await db\n .select<{ id: number }>({ id: ledger.id })\n .from(ledger)\n .innerJoin(allowGroup, eq(allowGroup.groupId, ledger.groupId))\n // 無効化されたグループでは許可しない。is_active が同期にだけ効いてアクセスに\n // 効かないと、凍結した台帳でサインインが通り続ける (向きが逆になる)。\n // ⚠️ テナント述語は入れない。認証はテナントの確定より前に起きるためであり、\n // is_active はそれとは別の軸である\n .innerJoin(group, and(eq(group.id, ledger.groupId), eq(group.isActive, true)))\n .where(eq(ledger.email, normalized))\n return Boolean(row)\n },\n\n /**\n * Lists group-based allowlist entries of the tenant. テナントのグループ許可を列挙する処理。\n *\n * `auth-core` の判別共用体をそのまま返す。別形で返すと、`type` と `readOnly` を消費側が\n * 付け直すことになり、「未設定 = 編集可」という暗黙の既定が呼び出し地点ごとに復活する。\n */\n async listAllowlistGroups(tenantId: string): Promise<AuthAllowlistEntry[]> {\n assertTenantId(tenantId, \"listAllowlistGroups(tenantId)\")\n await ready()\n const rows = await db\n .select<{ id: number; groupId: number; groupKey: string; groupName: string; description: string | null }>({\n id: allowGroup.id,\n groupId: allowGroup.groupId,\n groupKey: group.groupKey,\n groupName: group.name,\n description: allowGroup.description,\n })\n .from(allowGroup)\n .innerJoin(group, and(eq(group.id, allowGroup.groupId), eq(group.tenantId, tenantId)))\n .where(eq(allowGroup.tenantId, tenantId))\n .orderBy(asc(group.name))\n return rows.map((r) => ({\n id: Number(r.id),\n type: \"group\" as const,\n groupId: Number(r.groupId),\n groupKey: String(r.groupKey),\n groupName: String(r.groupName),\n ...(r.description ? { description: String(r.description) } : {}),\n owner: { kind: \"tenant\", tenantId },\n // テナントの管理者が足した行なので編集できる。環境変数由来の行と違い不変ではない\n readOnly: false,\n }))\n },\n\n /** Adds a group-based allowlist entry. グループ許可を追加する処理。 */\n async addAllowlistGroup(tenantId: string, groupId: number, description: string | undefined, actor: string): Promise<void> {\n assertTenantId(tenantId, \"addAllowlistGroup(tenantId)\")\n await ready()\n await db.transaction(async (tx) => {\n await assertGroupInTenant(tx, tenantId, groupId)\n // 許可は台帳を通してしか効かない。台帳を持たないローカル管理グループを足しても\n // 誰にも何も与えず、しかも書込側も読取側も何も報告しない無言の no-op になる\n const [linked] = await tx\n .select<{ id: number }>({ id: link.id })\n .from(link)\n .where(and(eq(link.groupId, groupId), eq(link.tenantId, tenantId)))\n if (!linked) throw new Error(`Group ${groupId} is locally managed; only externally-sourced groups can gate sign-in, because the decision reads the upstream member ledger.`)\n await tx\n .insert(allowGroup)\n .output()\n .values({ tenantId, groupId, description: description ?? null, createdAt: new Date(), createdBy: actor })\n })\n },\n\n /** Removes a group-based allowlist entry. グループ許可を削除する処理。 */\n async removeAllowlistGroup(tenantId: string, id: number): Promise<void> {\n assertTenantId(tenantId, \"removeAllowlistGroup(tenantId)\")\n await ready()\n await db.delete(allowGroup).where(and(eq(allowGroup.id, id), eq(allowGroup.tenantId, tenantId)))\n },\n\n /**\n * Reports the number of real membership rows the tenant's linked groups hold.\n * テナントの連携グループが持つ実所属行の件数を報告する処理。\n *\n * 連携を切る前に「何人分の所属が残るのか」を運用者へ示すために使う。\n */\n async countProjectedMembers(tenantId: string, groupId: number): Promise<number> {\n assertTenantId(tenantId, \"countProjectedMembers(tenantId)\")\n await ready()\n const [row] = await db\n .select<{ n: number }>({ n: count() })\n .from(groupMember)\n .innerJoin(group, and(eq(group.id, groupMember.groupId), eq(group.tenantId, tenantId)))\n .where(eq(groupMember.groupId, groupId))\n return Number(row?.n ?? 0)\n },\n }\n}\n","/**\n * @module membership-mode\n * @description The single reader of the `membership_mode` column.\n * `membership_mode` 列を読む唯一の入口。\n *\n * 以前は読み手が 2 つあり、片方は未知の値を `TypeError` で拒否し、もう片方は黙って `\"direct\"` へ\n * 倒していた。同じ列に相反する 2 つの方針が同居し、しかも倒すほうが削除を決める側だった。\n * 入れ子展開のつもりのリンクが直接所属だけを返せば、入れ子側の全員が削除候補になる。\n */\nimport type { AuthDirectoryMembershipMode } from \"@aiquants/auth-core\"\n\n/** The two modes, in one place. 2 つのモードの唯一の定義位置。 */\nexport const MEMBERSHIP_MODES: readonly AuthDirectoryMembershipMode[] = [\"direct\", \"transitive\"]\n\n/**\n * Rejects a membership mode that is not one of the two known values.\n * 既知の 2 値でない所属展開モードを拒否する処理。\n *\n * 既定へ倒さないのは、入れ子展開が黙って有効になると権限が静かに広がり、逆に黙って無効になると\n * 入れ子側の全員が削除候補になるためである。どちらの向きにも倒さない。\n *\n * @param value Raw column value. 列の生値。\n * @returns The validated mode. 検証済みのモード。\n * @throws {TypeError} When the value is unknown. 未知の値の場合。\n */\nexport function assertMembershipMode(value: string): AuthDirectoryMembershipMode {\n if (!MEMBERSHIP_MODES.includes(value as AuthDirectoryMembershipMode)) {\n throw new TypeError(`[@aiquants/auth-directory-drizzle] membershipMode must be one of ${MEMBERSHIP_MODES.join(\" | \")}, received ${JSON.stringify(value)}`)\n }\n return value as AuthDirectoryMembershipMode\n}\n","/**\n * @module promote\n * @description Turns ledger rows into real memberships for a user that has just been provisioned.\n * プロビジョニング直後のユーザーについて、台帳の行を実所属へ昇格させる処理。\n *\n * **上流へは一切到達しない。** 昇格に必要なのは「このアドレス (または上流 ID) は、どのグループの\n * 台帳に載っているか」だけであり、それはすべてローカルにある。おかげで web プロセスは\n * ディレクトリの資格情報を持たないまま、初回ログインのその場でグループ権限を有効化できる。\n *\n * 上流の資格情報を持つのは同期ジョブだけ、という配置を壊さないために、本モジュールに\n * `DirectoryProvider` を持ち込んではならない。\n */\nimport { normalizeAuthEmail } from \"@aiquants/auth-core\"\nimport { assertTenantId, assertTenantMatch } from \"@aiquants/authz-core\"\n\n/** Storage operations promotion performs. 昇格が行う記憶域操作。 */\nexport type PromotionRepository = {\n /**\n * Finds ledger rows matching the identity, across every linked group of the tenant.\n * テナントの全連携グループを対象に、identity と一致する台帳行を探す処理。\n *\n * 突合順は上流 ID が第一キー、アドレスが第二キー。\n *\n * ⚠️ 各行が属するテナントも返すこと。`TDGroupMemberEmail` はテナント列を持たず親から継承する\n * ため、台帳だけで絞った実装は静かにテナントを跨ぐ。呼び出し側はこの値で再検証する。\n */\n findLedgerMatches(args: { tenantId: string; externalUserId: string | null; email: string }): Promise<Array<{ ledgerId: number; groupId: number; tenantId: string }>>\n\n /**\n * Records the resolution and inserts the missing membership rows, atomically.\n * 解決結果の記録と不足している所属行の挿入を、原子的に行う処理。\n *\n * @returns Group ids the user was actually added to. 実際に追加されたグループ ID。\n */\n attachUserToGroups(args: { tenantId: string; userId: number; ledgerIds: number[]; groupIds: number[] }): Promise<number[]>\n}\n\n/** What the promotion did. 昇格が行ったこと。 */\nexport type PromotionResult = {\n /** Groups the user gained membership of. 所属を得たグループ。 */\n addedGroupIds: number[]\n /** Ledger rows the user was matched against. 突合した台帳行。 */\n matchedLedgerIds: number[]\n}\n\n/**\n * Promotes ledger rows to real memberships for one just-provisioned user.\n * プロビジョニング直後の 1 ユーザーについて、台帳の行を実所属へ昇格させる処理。\n *\n * 追加しか行わないため、最後の管理者ロックアウト判定は不要である (所属が増えて管理者が\n * 減ることはない)。それでも記憶域側では 1 トランザクションで書くこと — 台帳の解決記録だけが\n * 残って所属が入らないと、次回の同期まで「解決済みなのに権限が無い」状態が続く。\n *\n * @param repository Storage operations. 記憶域操作。\n * @param args Tenant, the resolved user, and the identity to match on. テナント・ユーザー・identity。\n * @returns What was promoted. 昇格した内容。\n * @throws {AuthzTenantError} When the tenant id is blank, or a matched row belongs to another\n * tenant. テナント ID が空、または一致行が別テナントに属する場合。\n * @throws {TypeError} When the user id is not a positive integer. ユーザー ID が正整数でない場合。\n */\nexport async function promoteLedgerForUser(repository: PromotionRepository, args: { tenantId: string; userId: number; externalUserId: string | null; email: string | null | undefined }): Promise<PromotionResult> {\n const tenantId = assertTenantId(args.tenantId, \"promoteLedgerForUser(tenantId)\")\n if (!Number.isInteger(args.userId) || args.userId <= 0) {\n throw new TypeError(`[@aiquants/auth-directory-drizzle] userId must be a positive integer, received ${String(args.userId)}`)\n }\n\n const email = normalizeAuthEmail(args.email)\n const externalUserId = args.externalUserId?.trim() || null\n // 突合材料がどちらも無ければ、探すこと自体が全件走査になる。何もしないのが正しい\n if (externalUserId === null && email === \"\") return { addedGroupIds: [], matchedLedgerIds: [] }\n\n const matches = await repository.findLedgerMatches({ tenantId, externalUserId, email })\n if (matches.length === 0) return { addedGroupIds: [], matchedLedgerIds: [] }\n // 境界をポートの docstring に委ねない。所属の追加はロールへ繋がるため、1 行の取り違えが\n // そのままテナントを跨いだ権限付与になる\n for (const match of matches) assertTenantMatch(tenantId, match.tenantId, \"promoteLedgerForUser(findLedgerMatches row)\")\n\n const ledgerIds = [...new Set(matches.map((m) => m.ledgerId))].sort((a, b) => a - b)\n const groupIds = [...new Set(matches.map((m) => m.groupId))].sort((a, b) => a - b)\n const addedGroupIds = await repository.attachUserToGroups({ tenantId, userId: args.userId, ledgerIds, groupIds })\n return { addedGroupIds: [...addedGroupIds].sort((a, b) => a - b), matchedLedgerIds: ledgerIds }\n}\n","/**\n * Drizzle-backed repository for first-sign-in membership promotion.\n * 初回ログイン時の所属昇格を担う drizzle 実装のリポジトリ。\n *\n * 上流へは一切到達しない。台帳は同期ジョブが既に書き終えているので、web プロセスは\n * ディレクトリの資格情報を持たないまま、サインアップしたその場でグループ権限を有効化できる。\n * 資格情報を持つのは同期ジョブだけ、という配置はこの分離に依存している。\n */\nimport { assertTenantId } from \"@aiquants/authz-core\"\nimport type { AuthzTables } from \"@aiquants/authz-drizzle\"\nimport { and, eq, inArray } from \"drizzle-orm\"\nimport type { DirectoryDb, EnsureConnection } from \"./db\"\nimport { findLedgerRowsForIdentity } from \"./ledger-identity\"\nimport type { PromotionRepository } from \"./promote\"\nimport type { AuthDirectoryTables } from \"./schema\"\nimport { chunk, chunkByParameters, MAX_PARAMETERS } from \"./sql-limits\"\n\n/** Options for {@link createPromotionRepository}. 昇格リポジトリの生成オプション。 */\nexport type PromotionRepositoryOptions = {\n /** Establishes the connection before the first statement. 最初の文の前に接続を確立する処理。 */\n ensureConnection?: EnsureConnection\n /** Written to the audit columns of inserted membership rows. 挿入行の監査列に書く実行者。 */\n actor: string\n /** Injected clock — tests must not depend on wall time. 注入時計 (実時刻に依存させない)。 */\n now?: () => Date\n}\n\n/** Columns the membership INSERT binds per row. 所属 INSERT が 1 行あたり束ねる列数。 */\nconst MEMBERSHIP_INSERT_PARAMETERS = 4\n/** Parameters the marking statement binds outside its two `IN` lists. 印付け文の `IN` 以外の枠。 */\nconst RESERVED_FOR_MARKING = 100\n\n/**\n * Builds the promotion repository over the directory ledger and the authz membership table.\n * ディレクトリ台帳と認可の所属表に対して、昇格リポジトリを構築する処理。\n *\n * @param dbUnknown - Drizzle database instance. drizzle のデータベースインスタンス。\n * @param tables - Directory tables (owns the ledger). ディレクトリ表 (台帳の所有者)。\n * @param authzTables - Authorization tables (owns groups and membership). 認可表 (グループと所属の所有者)。\n * @param opts - Connection, actor and clock. 接続・実行者・時計。\n * @returns A repository consumable by `promoteLedgerForUser`. `promoteLedgerForUser` が使うリポジトリ。\n */\nexport function createPromotionRepository(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: PromotionRepositoryOptions): PromotionRepository {\n const db = dbUnknown as DirectoryDb\n const ready = opts.ensureConnection ?? (async () => {})\n const now = opts.now ?? (() => new Date())\n const ledger = tables.TDGroupMemberEmail\n const { TMGroup: group, TDGroupMember: groupMember } = authzTables\n\n return {\n async findLedgerMatches(args) {\n assertTenantId(args.tenantId, \"findLedgerMatches(tenantId)\")\n await ready()\n return findLedgerRowsForIdentity(db, { ledger, group }, args)\n },\n\n async attachUserToGroups({ tenantId, userId, ledgerIds, groupIds }) {\n assertTenantId(tenantId, \"attachUserToGroups(tenantId)\")\n await ready()\n if (ledgerIds.length === 0) return []\n\n return db.transaction(async (tx) => {\n // 自テナントのグループであることを、書き込みと同じトランザクションで証明する。\n // 呼び出し側で確認済みでも、その間に連携が解除された行へ所属を書けてしまう\n const owned = new Set<number>()\n for (const part of chunk(groupIds)) {\n const rows = await tx\n .select<{ id: number }>({ id: group.id })\n .from(group)\n .where(and(inArray(group.id, part), eq(group.tenantId, tenantId), eq(group.isActive, true)))\n for (const r of rows) owned.add(Number(r.id))\n }\n const targets = groupIds.filter((id) => owned.has(id))\n if (targets.length === 0) return []\n\n const existing = new Set<number>()\n for (const part of chunk(targets)) {\n const rows = await tx\n .select<{ groupId: number }>({ groupId: groupMember.groupId })\n .from(groupMember)\n .where(and(eq(groupMember.userId, userId), inArray(groupMember.groupId, part)))\n for (const r of rows) existing.add(Number(r.groupId))\n }\n const missing = targets.filter((id) => !existing.has(id))\n\n // 台帳 → 所属 の順で書く。同期側 (sync-repository) も同じ順で X ロックを取るため、\n // 逆順にすると初回ログインと同期が正面衝突してデッドロック (1205) になり、\n // 敗者が同期ならそのグループの反映が丸ごと巻き戻る。\n // 印を先に付けても取り残しは起きない — 同一トランザクションなので、挿入が\n // 落ちれば印も一緒に消える\n //\n // この 1 文は `IN` を 2 つ描画する。台帳 ID 側だけを分割すると、グループ ID 側は\n // 毎回そのままの長さで乗るため、所属グループの多い人物で上限を越える\n const ledgerChunkSize = Math.max(1, MAX_PARAMETERS - RESERVED_FOR_MARKING - targets.length)\n for (const part of chunk(ledgerIds, ledgerChunkSize)) {\n await tx\n .update(ledger)\n .set({ resolvedUserId: userId })\n .output()\n .where(and(inArray(ledger.id, part), inArray(ledger.groupId, targets)))\n }\n if (missing.length > 0) {\n const createdAt = now()\n // 1 行 4 列。行数で割ると列の多い表で静かに上限を越える\n for (const part of chunkByParameters(missing, MEMBERSHIP_INSERT_PARAMETERS)) {\n await tx\n .insert(groupMember)\n .output()\n .values(part.map((groupId) => ({ groupId, userId, createdAt, createdBy: opts.actor })))\n }\n }\n\n return missing\n })\n },\n }\n}\n","/**\n * SQL Server parameter-limit helpers shared by every batched statement.\n * 一括文が共有する SQL Server のパラメータ上限まわりのヘルパー。\n *\n * ⚠️ 上限は **行数ではなくパラメータ数** である。1 行あたり何個の値を束ねるかは文ごとに違うので、\n * 行数だけで分割すると、列の多い `VALUES` が静かに上限を越える。しかも越えるのは件数が閾値を\n * 跨いだ日だけで、それまでは同じコードが問題なく動いて見える。\n */\n\n/**\n * Hard limit of parameters per statement. 1 文あたりのパラメータ数の上限。\n *\n * SQL Server の RPC 1 回あたりのパラメータ上限は 2100。数個は文自身の値 (`SET` 句・スカラー\n * 述語) が使うため、分割の基準にはここから余裕を引いた値を用いる。\n */\nexport const MAX_PARAMETERS = 2100\n\n/** Parameters reserved for the statement's own scalar values. 文自身のスカラー値のための予約枠。 */\nconst RESERVED_PARAMETERS = 100\n\n/**\n * Maximum ids per `IN` list of single-column predicates. 単列述語の `IN` 句 1 回あたりの最大 ID 数。\n *\n * 1 行 1 パラメータの用途 (`inArray(col, ids)`) 専用の既定値である。複数列を束ねる `VALUES` や、\n * 1 文に `IN` を 2 つ描画する `UPDATE` にそのまま使ってはならない — {@link chunkByParameters} を使う。\n */\nexport const MAX_IN_LIST = 500\n\n/**\n * Splits a list into chunks that fit one statement.\n * 1 文に収まる大きさへリストを分割する処理。\n *\n * @param items - Items to split. 分割対象。\n * @param size - Maximum chunk size. 1 チャンクの最大数。\n * @returns Chunks in the original order. 元の順序を保ったチャンク列。\n * @throws {TypeError} When the size is not a positive integer. 正の整数でない場合。\n */\nexport function chunk<T>(items: readonly T[], size = MAX_IN_LIST): T[][] {\n if (!Number.isInteger(size) || size <= 0) throw new TypeError(`[@aiquants/auth-directory-drizzle] chunk size must be a positive integer, received ${String(size)}`)\n const out: T[][] = []\n for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))\n return out\n}\n\n/**\n * Splits a list so each chunk fits the parameter budget of one statement.\n * 各チャンクが 1 文のパラメータ枠に収まるようリストを分割する処理。\n *\n * `VALUES` は 1 行につき列数ぶんのパラメータを束ねる。7 列の台帳行なら 500 行で 3500 個になり、\n * 上限 2100 を越えて **実行される前に** 拒否される。しかもその失敗は再試行しても同じ場所で起き、\n * そのグループは永久に台帳を持てない。\n *\n * @param items - Items to split. 分割対象。\n * @param parametersPerItem - Parameters each item binds. 1 件が束ねるパラメータ数。\n * @param reservedParameters - Parameters the statement itself binds. 文自身が束ねる数。\n * @returns Chunks in the original order. 元の順序を保ったチャンク列。\n * @throws {TypeError} When a single item cannot fit the budget. 1 件すら収まらない場合。\n */\nexport function chunkByParameters<T>(items: readonly T[], parametersPerItem: number, reservedParameters = RESERVED_PARAMETERS): T[][] {\n if (!Number.isInteger(parametersPerItem) || parametersPerItem <= 0) {\n throw new TypeError(`[@aiquants/auth-directory-drizzle] parametersPerItem must be a positive integer, received ${String(parametersPerItem)}`)\n }\n if (!Number.isInteger(reservedParameters) || reservedParameters < 0) {\n throw new TypeError(`[@aiquants/auth-directory-drizzle] reservedParameters must be a non-negative integer, received ${String(reservedParameters)}`)\n }\n const budget = MAX_PARAMETERS - reservedParameters\n const size = Math.floor(budget / parametersPerItem)\n // 1 件すら収まらないなら、分割では解決しない。黙って 1 件ずつ発行すると、実行時に必ず\n // 落ちる文をひたすら送り続けることになる\n if (size < 1) throw new TypeError(`[@aiquants/auth-directory-drizzle] a single item binds ${parametersPerItem} parameters, which exceeds the ${budget} available in one statement`)\n return chunk(items, size)\n}\n","/**\n * @module schema\n * @description Drizzle (mssql) table definitions for the identity side of access control —\n * external group provenance, the upstream member ledger, tenant membership proof, the login\n * allowlist, and the synchronization queue.\n * アクセス制御の identity 面 (外部グループの出所・上流メンバー台帳・テナント帰属証明・\n * ログイン許可リスト・同期要求キュー) の drizzle (mssql) テーブル定義。\n *\n * **なぜ認可スキーマ (`@aiquants/authz-drizzle`) と別パッケージなのか**\n *\n * これらの表を読むのは認証・ユーザー管理の経路だけで、認可のクエリは 1 本も触らない。責務が\n * 違うことに加えて、実務上の理由がある: `@aiquants/authz-drizzle` は複数の配備が共有する公開\n * パッケージであり、そこへ列や表を足すと **全配備に DDL 移行を強制する** (drizzle は INSERT の\n * 列リストにスキーマ定義の全列を並べるため、実表に無い列を宣言した瞬間に SQL Server 207 で\n * 必ず失敗する)。外部ディレクトリ連携を使わない配備にまで移行を強いないため、別パッケージの\n * 別表として足す形にしてある。\n *\n * **テナント列の方針**\n *\n * 認可スキーマの原則 (リンク表は参照先からテナントを継承し、二重管理を作らない) を踏襲する。\n * 例外は 2 つあり、いずれも理由を各表の docstring に記した。物理 `DEFAULT` は **付けない** —\n * 既定値は「既存行のある表に NOT NULL 列を後付けする」ためのものであり、新規表では\n * 「テナントを渡し忘れた書き込みが黙って既定テナントへ入る」経路を作るだけである。\n */\nimport { AUTHZ_TENANT_COLUMN } from \"@aiquants/authz-drizzle\"\nimport { type AnyMsSqlColumn, bigint, bit, datetime2, foreignKey, int, mssqlSchema, nvarchar, primaryKey, uniqueIndex } from \"drizzle-orm/mssql-core\"\n\n/** Minimal shape of a table this schema points a foreign key at. 外部キーの参照先に要る最小形。 */\nexport type ReferencedTable = { id: AnyMsSqlColumn }\n\n/**\n * Physical name of the provenance discriminator carried by an external link row.\n * 外部リンク行が持つ供給元識別子の物理名。\n */\nexport const AUTH_DIRECTORY_PROVIDER_COLUMN = \"provider\"\n\n/**\n * Table names that carry {@link AUTHZ_TENANT_COLUMN}.\n * テナント列を持つ表の名前一覧。\n *\n * `TDGroupMemberEmail` が含まれないのは、親グループからテナントを継承するためである。\n */\nexport const AUTH_DIRECTORY_TENANT_SCOPED_TABLES = [\"TDGroupExternalLink\", \"TDTenantMembershipGroup\", \"TDAllowlistGroup\", \"TDDirectorySyncRequest\", \"TMAllowlist\"] as const\n\n/**\n * Builds the identity-side tables under an arbitrary schema name.\n * 任意のスキーマ名の下に identity 側テーブル群を構築する処理。\n *\n * @param schemaName Schema the tables live in. テーブルを配置するスキーマ名。\n * @param opts Injected tables the foreign keys point at. 外部キーの参照先として注入する表。\n * @returns Every identity-side table, keyed by table name. 表名をキーにしたテーブル群。\n */\nexport function defineAuthDirectorySchema<S extends string>(schemaName: S, opts: { groupTable: ReferencedTable; userTable: ReferencedTable }) {\n const s = mssqlSchema(schemaName)\n\n /** Audit columns for a table that is inserted, updated and deleted. 更新経路を持つ表の監査列。 */\n const audit = () => ({\n createdAt: datetime2(\"created_at\"),\n createdBy: nvarchar(\"created_by\", { length: 50 }),\n updatedAt: datetime2(\"updated_at\"),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }),\n })\n\n /**\n * Audit columns for an insert-and-delete-only table (no `updated_*` pair).\n * 追記・削除のみで更新されない表用の監査列。\n *\n * drizzle は INSERT の列リストに **スキーマ上の全列** を並べるため、実表に存在しない列を\n * 宣言すると `Invalid column name` (SQL Server 207) で必ず失敗する。宣言は実表に合わせる。\n */\n const insertOnlyAudit = () => ({\n createdAt: datetime2(\"created_at\"),\n createdBy: nvarchar(\"created_by\", { length: 50 }),\n })\n\n /**\n * Provenance of a group whose membership an upstream directory owns.\n * 所属の所有者が上流ディレクトリにあるグループの出所。\n *\n * ⚠️ **`tenant_id` はリンク表としては非正規化である。** 参照先の `TMGroup` が既にテナントを\n * 持つため原則からは外れるが、`UNIQUE (tenant_id, provider, external_id)` =「同一の上流\n * グループはテナントごとに 1 回だけリンクできる」を SQL Server の制約として表現するには\n * 列が要る (表を跨いだ一意性は宣言できない)。乖離を防ぐため、書込時に親の `tenant_id` と\n * 一致することを必ず証明すること。\n */\n const TDGroupExternalLink = s.table(\n \"TDGroupExternalLink\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n groupId: bigint(\"group_id\", { mode: \"number\" }).notNull(),\n tenantId: nvarchar(AUTHZ_TENANT_COLUMN, { length: 64 }).notNull(),\n provider: nvarchar(AUTH_DIRECTORY_PROVIDER_COLUMN, { length: 32 }).notNull(),\n // 上流の安定 ID。アドレスは改名され得るため正準にしない\n externalId: nvarchar(\"external_id\", { length: 256 }).notNull(),\n externalKey: nvarchar(\"external_key\", { length: 320 }).notNull(),\n membershipMode: nvarchar(\"membership_mode\", { length: 16 }).notNull(),\n isSyncEnabled: bit(\"is_sync_enabled\").notNull(),\n syncedAt: datetime2(\"synced_at\"),\n syncError: nvarchar(\"sync_error\", { length: 1000 }),\n // 上流でグループが見つからない状態が何回連続したか。1 度の 404 で無効化すると、\n // 伝播待ちや権限変更の一瞬で生きているグループが落ちる\n vanishedObservations: int(\"vanished_observations\").notNull(),\n ...audit(),\n },\n (t) => [\n primaryKey({ name: `${schemaName}_TDGroupExternalLink_pk`, columns: [t.id] }),\n // 1 グループ 1 リンク。2 本張れると、どちらの上流が所属を所有するのか決まらない\n uniqueIndex(`${schemaName}_TDGroupExternalLink_group_uq`).on(t.groupId),\n uniqueIndex(`${schemaName}_TDGroupExternalLink_external_uq`).on(t.tenantId, t.provider, t.externalId),\n foreignKey({ name: `${schemaName}_TDGroupExternalLink_group_id_fk`, columns: [t.groupId], foreignColumns: [opts.groupTable.id] }),\n ],\n )\n\n /**\n * Every member observed upstream, including people with no local user row.\n * 上流で観測された全メンバー。ローカルユーザー行を持たない人物も含む。\n *\n * 未ログインの人物のユーザー行は意図的に作らないため、実所属表には入れられない。それでも\n * ログイン許可の判定とテナント帰属証明は **初回ログインの時点で** 成立させる必要があるので、\n * アドレスを台帳として保持する。\n *\n * `resolved_user_id` に外部キーを張らないのは、解決できない行こそがこの表の存在理由だから\n * である。整合はプロビジョニング直後の昇格処理が担保する。\n */\n const TDGroupMemberEmail = s.table(\n \"TDGroupMemberEmail\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n groupId: bigint(\"group_id\", { mode: \"number\" }).notNull(),\n // 照合形 (auth-core の normalizeAuthEmail を通した値) で保存する\n email: nvarchar(\"email\", { length: 320 }).notNull(),\n externalUserId: nvarchar(\"external_user_id\", { length: 128 }),\n resolvedUserId: bigint(\"resolved_user_id\", { mode: \"number\" }),\n observedAt: datetime2(\"observed_at\").notNull(),\n ...audit(),\n },\n (t) => [\n primaryKey({ name: `${schemaName}_TDGroupMemberEmail_pk`, columns: [t.id] }),\n uniqueIndex(`${schemaName}_TDGroupMemberEmail_group_email_uq`).on(t.groupId, t.email),\n foreignKey({ name: `${schemaName}_TDGroupMemberEmail_group_id_fk`, columns: [t.groupId], foreignColumns: [opts.groupTable.id] }),\n ],\n )\n\n /**\n * Groups a tenant accepts as proof that a caller belongs to it.\n * あるテナントが「この呼び出し元は自分に属する」と認める証明用グループ。\n *\n * `is_break_glass` の行は **ローカル管理のグループでなければならない**。同期は上流の言う\n * ことに従って所属を消すため、証明経路のすべてが上流由来だと、上流側の事故がそのまま\n * 「誰もテナントに入れない」状態を生む。修復する権限を持つ人も締め出されるため、同期が\n * 決して触れない経路を最低 1 本残す。\n */\n const TDTenantMembershipGroup = s.table(\n \"TDTenantMembershipGroup\",\n {\n tenantId: nvarchar(AUTHZ_TENANT_COLUMN, { length: 64 }).notNull(),\n groupId: bigint(\"group_id\", { mode: \"number\" }).notNull(),\n isBreakGlass: bit(\"is_break_glass\").notNull(),\n ...insertOnlyAudit(),\n },\n (t) => [primaryKey({ name: `${schemaName}_TDTenantMembershipGroup_pk`, columns: [t.tenantId, t.groupId] }), foreignKey({ name: `${schemaName}_TDTenantMembershipGroup_group_id_fk`, columns: [t.groupId], foreignColumns: [opts.groupTable.id] })],\n )\n\n /**\n * Groups whose members may sign in. メンバーのサインインを許可するグループ。\n *\n * ⚠️ `tenant_id` は **所有者** であって絞り込み条件ではない。認証はテナントの確定より前に\n * 起きるため、許可判定の時点でテナントは存在しない。判定はこの配備が受け持つ全テナントの\n * 和集合で行うこと。読み取りにテナント述語を足すと初回ログインが全滅する。\n */\n const TDAllowlistGroup = s.table(\n \"TDAllowlistGroup\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n tenantId: nvarchar(AUTHZ_TENANT_COLUMN, { length: 64 }).notNull(),\n groupId: bigint(\"group_id\", { mode: \"number\" }).notNull(),\n description: nvarchar(\"description\", { length: 512 }),\n ...insertOnlyAudit(),\n },\n (t) => [\n primaryKey({ name: `${schemaName}_TDAllowlistGroup_pk`, columns: [t.id] }),\n uniqueIndex(`${schemaName}_TDAllowlistGroup_tenant_group_uq`).on(t.tenantId, t.groupId),\n foreignKey({ name: `${schemaName}_TDAllowlistGroup_group_id_fk`, columns: [t.groupId], foreignColumns: [opts.groupTable.id] }),\n ],\n )\n\n /**\n * Queue of synchronization requests raised from the admin UI.\n * 管理 UI から起票された同期要求のキュー。\n *\n * 上流へ到達できるのは同期ジョブだけで、web プロセスは資格情報を持たない。要求を行として\n * 積む形にすることで、サービス間の到達性も認証も不要になり、**誰がいつ何を要求し、結果が\n * どうだったか** がそのまま監査記録として残る。\n *\n * `group_id` が `NULL` はテナント全件を意味する。テナントを跨げないので `tenant_id` は\n * 自前で持つ (継承元が無い)。\n */\n const TDDirectorySyncRequest = s.table(\n \"TDDirectorySyncRequest\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n tenantId: nvarchar(AUTHZ_TENANT_COLUMN, { length: 64 }).notNull(),\n groupId: bigint(\"group_id\", { mode: \"number\" }),\n requestedBy: nvarchar(\"requested_by\", { length: 320 }).notNull(),\n requestedAt: datetime2(\"requested_at\").notNull(),\n pickedAt: datetime2(\"picked_at\"),\n finishedAt: datetime2(\"finished_at\"),\n result: nvarchar(\"result\", { length: 32 }),\n error: nvarchar(\"error\", { length: 2000 }),\n ...insertOnlyAudit(),\n },\n (t) => [primaryKey({ name: `${schemaName}_TDDirectorySyncRequest_pk`, columns: [t.id] })],\n )\n\n /**\n * Login allowlist patterns (addresses and bare hosts).\n * ログイン許可リストのパターン (アドレスと裸ホスト)。\n *\n * `@aiquants/authz-drizzle` から移設した表である。認可のクエリは 1 本もこの表に触らず、\n * 読むのは認証とユーザー管理の経路だけであるため、責務としては identity 側に属する。\n * 移設に合わせて `tenant_id` を足したが、意味は {@link TDAllowlistGroup} と同じく\n * **所有者** であり、ログイン判定の絞り込み条件ではない。\n */\n const TMAllowlist = s.table(\n \"TMAllowlist\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n tenantId: nvarchar(AUTHZ_TENANT_COLUMN, { length: 64 }).notNull(),\n pattern: nvarchar(\"pattern\", { length: 256 }).notNull(),\n type: nvarchar(\"type\", { length: 32 }).notNull(),\n description: nvarchar(\"description\", { length: 512 }),\n ...insertOnlyAudit(),\n },\n (t) => [primaryKey({ name: `${schemaName}_TMAllowlist_pk`, columns: [t.id] }), uniqueIndex(`${schemaName}_TMAllowlist_pattern_uq`).on(t.tenantId, t.pattern)],\n )\n\n return { TDGroupExternalLink, TDGroupMemberEmail, TDTenantMembershipGroup, TDAllowlistGroup, TDDirectorySyncRequest, TMAllowlist }\n}\n\n/** Explicit alias for the tables `defineAuthDirectorySchema` returns (avoids TS2742). 明示型。 */\nexport type AuthDirectoryTables = {\n TDGroupExternalLink: ReturnType<typeof defineAuthDirectorySchema>[\"TDGroupExternalLink\"]\n TDGroupMemberEmail: ReturnType<typeof defineAuthDirectorySchema>[\"TDGroupMemberEmail\"]\n TDTenantMembershipGroup: ReturnType<typeof defineAuthDirectorySchema>[\"TDTenantMembershipGroup\"]\n TDAllowlistGroup: ReturnType<typeof defineAuthDirectorySchema>[\"TDAllowlistGroup\"]\n TDDirectorySyncRequest: ReturnType<typeof defineAuthDirectorySchema>[\"TDDirectorySyncRequest\"]\n TMAllowlist: ReturnType<typeof defineAuthDirectorySchema>[\"TMAllowlist\"]\n}\n","/**\n * @module sync\n * @description Orchestration of one synchronization run: read upstream, reconcile, apply — and\n * stop at the first sign that \"upstream said nothing\" might mean \"we could not read upstream\".\n * 1 回の同期実行のオーケストレーション。上流を読み、突合し、適用する。そして「上流が何も\n * 言わなかった」が「上流を読めなかった」かもしれない兆候が出た時点で止める。\n *\n * 記憶域は {@link DirectorySyncRepository} 越しにしか触らないため、手順そのものは\n * データベースを起動せずに全分岐を検証できる。\n */\nimport { assertBlastRadiusPolicy, type BlastRadiusPolicy, type DirectoryGroup, type DirectoryProvider, reconcile } from \"@aiquants/auth-directory-core\"\nimport { defaultSyncMessages, type SyncMessages } from \"./sync-messages\"\nimport { type ApplyReport, type DirectorySyncRepository, type SyncLink, type SyncLoggerPort, type SyncOutcome, UNHEALTHY_SYNC_OUTCOMES } from \"./sync-ports\"\n\n/** Configuration of a synchronization runner. 同期ランナーの構成。 */\nexport type DirectorySyncConfig = {\n /**\n * Which upstream this runner speaks for. このランナーが担当する上流。\n *\n * リンク行の `provider` と照合する。照合しないと、別 provider のリンクを受け取った\n * ランナーがそれを「上流に存在しない」と判定し、他 provider のグループを無効化してしまう。\n */\n provider: string\n directory: DirectoryProvider\n repository: DirectorySyncRepository\n /** Per-axis removal limits. See `auth-directory-core`. 軸ごとの削除上限。 */\n policy: BlastRadiusPolicy\n /**\n * Consecutive absences required before a group is deactivated. 無効化に要する連続不在回数。\n *\n * 1 度の 404 で無効化すると、伝播待ちや権限変更の一瞬で生きているグループが落ちる。\n */\n vanishedThreshold: number\n /**\n * Supplies the current time. 現在時刻を供給する処理。\n *\n * 注入するのは決定性のためだけではない。1 回の実行の中で「観測時刻」と「同期時刻」が\n * 別々に進むと、どの行がどの実行で書かれたのかを後から辿れなくなる。\n */\n now: () => Date\n logger: SyncLoggerPort\n /** Operator-facing text. 運用者向けの文言。 */\n messages?: SyncMessages\n}\n\n/** Result of one run over a set of links. リンク集合 1 巡分の結果。 */\nexport type DirectorySyncRunResult = {\n outcomes: SyncOutcome[]\n /**\n * Links that did not end in a healthy, converged state. 健全に収束しなかったリンク数。\n *\n * 消滅の観測と無効化も含める。グループが恒久的に同期対象から外れることは、実行の中で\n * 最も重い結末であり、それを「0 unhealthy」と報告しては監視の意味がない。\n */\n unhealthyCount: number\n}\n\n/**\n * Creates a synchronization runner. 同期ランナーを生成する処理。\n *\n * @param config Injected provider, repository, limits, clock and logger. 注入する構成。\n * @returns The runner. ランナー。\n * @throws {TypeError} When the policy or the threshold is invalid. 方針・しきい値が不正な場合。\n */\nexport function createDirectorySync(config: DirectorySyncConfig) {\n const { directory, repository, policy, now, logger } = config\n const messages = config.messages ?? defaultSyncMessages\n // 結線時点で落とす。最初の同期まで設定不備に気付けないと、その間ずっと陳腐化が進む\n assertBlastRadiusPolicy(policy)\n if (!Number.isInteger(config.vanishedThreshold) || config.vanishedThreshold < 1) {\n throw new TypeError(`[@aiquants/auth-directory-drizzle] vanishedThreshold must be a positive integer, received ${String(config.vanishedThreshold)}`)\n }\n if (typeof config.provider !== \"string\" || config.provider.trim() === \"\") {\n throw new TypeError(\"[@aiquants/auth-directory-drizzle] provider must be a non-empty string so links of other providers can be told apart\")\n }\n\n /**\n * Records a failure without letting the recording itself end the run.\n * 記録そのものが実行を終わらせないようにして失敗を記録する処理。\n *\n * `recordFailure` は失敗した書込と同じデータベースを触る。その書込が落ちた原因がデータベース\n * 側にあるなら、記録も落ちる可能性が高い。素通しにすると、1 本目のリンクの失敗が残り全部の\n * リンクを未実行のまま実行ごと終わらせる。\n */\n async function noteFailure(link: SyncLink, reason: string): Promise<void> {\n try {\n await repository.recordFailure(link, reason)\n } catch (error) {\n logger.error(`[auth-directory] could not record the failure for ${link.externalKey}:`, error)\n }\n }\n\n /** Reads the upstream group, or explains why it could not. 上流グループを読む処理。 */\n async function readUpstream(link: SyncLink): Promise<{ kind: \"ok\"; group: DirectoryGroup; members: Awaited<ReturnType<DirectoryProvider[\"listGroupMembers\"]>> } | { kind: \"missing\" } | { kind: \"failed\"; error: Error }> {\n try {\n const group = await directory.getGroup(link.externalId)\n if (group === null) return { kind: \"missing\" }\n return { kind: \"ok\", group, members: await directory.listGroupMembers(group, link.membershipMode) }\n } catch (error) {\n return { kind: \"failed\", error: error instanceof Error ? error : new Error(String(error)) }\n }\n }\n\n /** Handles a group the upstream says does not exist. 上流が存在しないと答えたグループの処理。 */\n async function handleMissing(link: SyncLink): Promise<SyncOutcome> {\n const observations = await repository.recordVanishedObservation(link, messages.groupMissing(link.externalKey, link.vanishedObservations + 1, config.vanishedThreshold))\n if (observations < config.vanishedThreshold) {\n logger.warn(`[auth-directory] ${link.externalKey} missing upstream (${observations}/${config.vanishedThreshold})`)\n return { kind: \"missing\", link, observations }\n }\n // 無効化はするが削除はしない。削除するとロール割当 (運用者の付与意思) まで失われる。\n // 上流が復旧すれば次の実行が同じリンクを読み、成功時に再有効化する\n await repository.markGroupInactive(link)\n await noteFailure(link, messages.groupVanished(link.externalKey, observations))\n logger.warn(`[auth-directory] ${link.externalKey} deactivated after ${observations} consecutive absences`)\n return { kind: \"vanished\", link }\n }\n\n /**\n * Synchronizes one group. 1 グループを同期する処理。\n *\n * 上流の読み取りが失敗した場合、**削除は一切行わない**。読めなかったことと「全員が抜けた」\n * ことは、返ってきたデータの上では区別がつかない。区別できない以上、破壊しない側へ倒す。\n *\n * @param link The link to synchronize. 対象リンク。\n * @returns What happened. 何が起きたか。\n */\n async function syncOne(link: SyncLink): Promise<SyncOutcome> {\n if (link.provider !== config.provider) {\n // 他 provider のリンクを読むと、こちらの上流に存在しないという理由で無効化してしまう\n await noteFailure(link, messages.foreignProvider(link.provider, config.provider))\n logger.warn(`[auth-directory] skipping ${link.externalKey}: belongs to provider ${link.provider}`)\n return { kind: \"foreign-provider\", link }\n }\n\n const upstream = await readUpstream(link)\n if (upstream.kind === \"missing\") return handleMissing(link)\n if (upstream.kind === \"failed\") {\n await noteFailure(link, messages.upstreamUnreadable(upstream.error.message))\n logger.error(`[auth-directory] upstream read failed for ${link.externalKey}:`, upstream.error)\n return { kind: \"upstream-failed\", link, error: upstream.error }\n }\n\n try {\n const [snapshot, resolved] = await Promise.all([repository.loadGroupSnapshot(link), repository.resolveMembers(upstream.members)])\n // 突合が件数を減らしていたら、その差分はそのまま削除対象になる。上流の完全性だけを\n // 守っても、突合で落ちた人物は同じように消える\n if (resolved.length !== upstream.members.length) {\n const reason = messages.resolverIncomplete(upstream.members.length, resolved.length)\n await noteFailure(link, reason)\n logger.error(`[auth-directory] ${reason}`)\n return { kind: \"local-failed\", link, error: new Error(reason) }\n }\n\n const plan = reconcile(snapshot, resolved, policy)\n if (plan.aborted) {\n await noteFailure(link, messages.planRefused(plan.aborted))\n logger.warn(`[auth-directory] plan refused for ${link.externalKey}: ${plan.aborted.reason}`)\n return { kind: \"aborted\", link, plan }\n }\n\n const hasWork = plan.ledgerUpserts.length > 0 || plan.ledgerDeletes.length > 0 || plan.projectionAdds.length > 0 || plan.projectionRemoves.length > 0\n const stamp = now()\n let report: ApplyReport = { refusals: [] }\n if (hasWork) report = await repository.applyPlan(link, plan, stamp)\n // 変化が無くても成功時刻は進め、無効化されていたグループはここで再有効化する。\n // 進めないと、正常に動いている同期が陳腐化として警告され続け、本物の停止が埋もれる\n await repository.recordSuccess(link, upstream.group, stamp)\n\n if (report.refusals.length > 0) {\n // 拒否は失敗ではない (残りは適用済み) が、収束していないので必ず記録する\n await noteFailure(link, messages.removalsRefused(report.refusals))\n logger.warn(`[auth-directory] ${report.refusals.length} removal(s) refused for ${link.externalKey}`)\n }\n if (plan.unresolvedEmails.length > 0) {\n logger.info(`[auth-directory] ${link.externalKey}: ${plan.unresolvedEmails.length} upstream member(s) have no local user yet`)\n }\n return hasWork ? { kind: \"applied\", link, plan } : { kind: \"unchanged\", link }\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error))\n await noteFailure(link, messages.localWriteFailed(err.message))\n logger.error(`[auth-directory] local write failed for ${link.externalKey}:`, err)\n return { kind: \"local-failed\", link, error: err }\n }\n }\n\n return {\n syncOne,\n\n /**\n * Synchronizes every eligible link in scope. 対象範囲の全リンクを同期する処理。\n *\n * リンクは **逐次** 処理する。並列化すると上流のレート制限を一気に消費し、最初に\n * 弾かれたグループだけが失敗して部分的に古い状態が残る。1 グループの失敗は他へ\n * 波及させず、実行自体は最後まで続ける。\n *\n * @param scope Restrict to one tenant, or one group within it. 対象範囲。\n * @returns Every outcome, and how many ended unhealthy. 全結果と不健全件数。\n */\n async run(scope: { tenantId?: string; groupId?: number } = {}): Promise<DirectorySyncRunResult> {\n const links = await repository.listSyncableLinks(scope)\n const outcomes: SyncOutcome[] = []\n for (const link of links) {\n try {\n outcomes.push(await syncOne(link))\n } catch (error) {\n // syncOne は自前で捕捉するが、記録経路まで壊れた場合の最後の網。1 本の\n // リンクのために残り全部を未実行にしない\n const err = error instanceof Error ? error : new Error(String(error))\n logger.error(`[auth-directory] ${link.externalKey} ended abnormally:`, err)\n outcomes.push({ kind: \"local-failed\", link, error: err })\n }\n }\n const unhealthyCount = outcomes.filter((o) => UNHEALTHY_SYNC_OUTCOMES.includes(o.kind)).length\n logger.info(`[auth-directory] run finished: ${outcomes.length} link(s), ${unhealthyCount} unhealthy`)\n return { outcomes, unhealthyCount }\n },\n }\n}\n","/**\n * @module sync-messages\n * @description Operator-facing text the synchronization records against a link.\n * 同期がリンクへ記録する、運用者向けの文言。\n *\n * 文言を注入可能にしてあるのは、`sync_error` が管理 UI にそのまま出るためである。ホスト非依存を\n * 求められるパッケージへ日本語を焼き込むと、日本語以外の配備がデータベースに日本語を持つことになる。\n * 既定は日本語 (このリポジトリの管理 UI に合わせる)。\n */\nimport type { SyncAbort } from \"@aiquants/auth-directory-core\"\n\n/** The messages one synchronization can produce. 同期が生成し得る文言。 */\nexport type SyncMessages = {\n /** The upstream could not be read at all. 上流をまったく読めなかった。 */\n upstreamUnreadable: (reason: string) => string\n /** The local write failed. ローカルへの書込が失敗した。 */\n localWriteFailed: (reason: string) => string\n /** The resolver returned fewer members than the upstream listed. 突合が件数を減らした。 */\n resolverIncomplete: (upstream: number, resolved: number) => string\n /** The group is missing upstream but not yet deactivated. 消滅の観測 (未確定)。 */\n groupMissing: (externalKey: string, observations: number, threshold: number) => string\n /** The group has been deactivated after repeated absence. 消滅確定・無効化。 */\n groupVanished: (externalKey: string, observations: number) => string\n /** The reconciler refused the plan. 突合が plan を拒否した。 */\n planRefused: (abort: SyncAbort) => string\n /** The lockout guard refused some removals. ロックアウトガードが一部の削除を拒否した。 */\n removalsRefused: (refusals: readonly { userId: number; refusal?: string }[]) => string\n /** The link belongs to a different provider. 他 provider のリンクだった。 */\n foreignProvider: (linkProvider: string, runnerProvider: string) => string\n}\n\n/** Renders at most `limit` items, saying how many were left out. 上限付きの列挙 (省略数を明示)。 */\nconst sample = (items: readonly string[], limit = 20): string => (items.length <= limit ? items.join(\", \") : `${items.slice(0, limit).join(\", \")} ほか ${items.length - limit} 件`)\n\n/** Default Japanese messages. 既定の日本語文言。 */\nexport const defaultSyncMessages: SyncMessages = {\n upstreamUnreadable: (reason) => `上流ディレクトリの読み取りに失敗しました: ${reason}`,\n localWriteFailed: (reason) => `ローカルへの書き込みに失敗しました: ${reason}`,\n resolverIncomplete: (upstream, resolved) =>\n `上流が ${upstream} 件のメンバーを返しましたが、突合結果は ${resolved} 件でした。件数が減る突合は、解決できなかった人物を「上流から消えた」として削除してしまうため中断しました。解決できないメンバーは省略せず resolvedUserId: null で返す必要があります。`,\n groupMissing: (externalKey, observations, threshold) => `上流でグループが見つかりません (${externalKey})。連続 ${observations} 回目です。${threshold} 回に達した時点でグループを無効化します。`,\n groupVanished: (externalKey, observations) => `上流でグループが ${observations} 回連続で見つかりませんでした (${externalKey})。グループを無効化しました。上流が復旧すれば次回の同期で自動的に再有効化されます。`,\n planRefused: (abort) => {\n if (abort.reason === \"invalid-upstream-data\") return `同期を中断しました: 上流のメンバー一覧に扱えないアドレスが含まれています (${sample(abort.offendingEmails)})。`\n const axis = abort.axis === \"membership\" ? \"権限を失う人数\" : \"サインインできなくなる人数\"\n const withheld = abort.axis === \"membership\" ? abort.withheldProjectionRemoves.map(String) : abort.withheldLedgerDeletes\n return [\n `同期を中断しました: ${axis}が ${abort.population} 人中 ${abort.removalCount} 人になります`,\n `(上限: ${abort.limits.maxRemovalCount} 件 / 比率 ${abort.limits.maxRemovalRatio})。`,\n `差し止めた対象: ${sample(withheld)}。`,\n \"上流側の設定を確認してください。意図した変更であれば、この軸の上限だけを一時的に引き上げて再実行します。\",\n ].join(\" \")\n },\n removalsRefused: (refusals) => `${refusals.length} 件の所属解除が最後の管理者ガードにより拒否されました (ユーザー ${sample(refusals.map((r) => String(r.userId)))})。残りの変更は適用済みです。`,\n foreignProvider: (linkProvider, runnerProvider) => `このリンクの供給元は ${linkProvider} ですが、実行中の同期は ${runnerProvider} 用です。対応する同期ジョブから実行してください。`,\n}\n","/**\n * @module sync-ports\n * @description The narrow storage surface the synchronization orchestration depends on.\n * 同期オーケストレーションが依存する、狭い記憶域の面。\n *\n * オーケストレーションを drizzle から切り離すために置いている。同期の手順そのもの\n * (何を順に行い、どこで打ち切るか) は、データベースを起動せずに全分岐を検証できるべきである —\n * 検証しにくい手順は検証されず、検証されない手順は「読めなかったら消さない」を静かに破る。\n */\nimport type { AuthDirectoryMembershipMode, DirectoryGroup, DirectoryMember, GroupSyncPlan, LocalGroupSnapshot, ResolvedDirectoryMember } from \"@aiquants/auth-directory-core\"\n\n/**\n * One external group link, as the synchronization needs to see it.\n * 同期が見る必要のある外部グループリンク 1 件。\n */\nexport type SyncLink = {\n linkId: number\n groupId: number\n groupKey: string\n tenantId: string\n /** Which upstream owns this group. どの上流がこのグループを所有するか。 */\n provider: string\n externalId: string\n externalKey: string\n membershipMode: AuthDirectoryMembershipMode\n /** False when the group has been deactivated. グループが無効化されている場合 false。 */\n isGroupActive: boolean\n /**\n * How many consecutive runs have found the group missing upstream.\n * 上流でグループが見つからない状態が何回連続したか。\n *\n * 1 度の 404 で無効化すると、伝播待ちや権限変更の一瞬で生きているグループが落ちる。\n */\n vanishedObservations: number\n}\n\n/** Why one group's synchronization ended the way it did. 1 グループの同期がどう終わったか。 */\nexport type SyncOutcome =\n | { kind: \"applied\"; link: SyncLink; plan: GroupSyncPlan }\n /** Nothing changed. Not a failure — an idempotent run. 変化なし。失敗ではなく冪等な実行。 */\n | { kind: \"unchanged\"; link: SyncLink }\n /** The plan was refused by the reconciler's own guards. 突合自身のガードが plan を拒否した。 */\n | { kind: \"aborted\"; link: SyncLink; plan: GroupSyncPlan }\n /** The group is missing upstream, but not yet often enough to act on. 消滅の観測 (未確定)。 */\n | { kind: \"missing\"; link: SyncLink; observations: number }\n /** The group has been missing long enough that it was deactivated. 消滅確定・無効化した。 */\n | { kind: \"vanished\"; link: SyncLink }\n /** Reading upstream failed. 上流の読み取りが失敗した。 */\n | { kind: \"upstream-failed\"; link: SyncLink; error: Error }\n /** Writing locally failed. ローカルへの書込が失敗した。 */\n | { kind: \"local-failed\"; link: SyncLink; error: Error }\n /** The link belongs to another provider than the one running. 他 provider のリンクだった。 */\n | { kind: \"foreign-provider\"; link: SyncLink }\n\n/** Outcomes that mean the group is not in a healthy, converged state. 健全でない結末。 */\nexport const UNHEALTHY_SYNC_OUTCOMES: readonly SyncOutcome[\"kind\"][] = [\"aborted\", \"missing\", \"vanished\", \"upstream-failed\", \"local-failed\", \"foreign-provider\"]\n\n/** What happened to one guarded removal. ガード付き削除 1 件の結末。 */\nexport type RemovalOutcome = { userId: number; applied: boolean; refusal?: string }\n\n/** What `applyPlan` actually managed to do. `applyPlan` が実際に行えたこと。 */\nexport type ApplyReport = {\n /** Removals the administrator lockout guard refused. ロックアウトガードが拒否した削除。 */\n refusals: RemovalOutcome[]\n}\n\n/**\n * Storage operations the synchronization performs.\n * 同期が行う記憶域操作。\n */\nexport type DirectorySyncRepository = {\n /**\n * Lists links that are eligible to run. 実行対象のリンクを列挙する処理。\n *\n * ⚠️ 無効化されたグループのリンクも返すこと。返さないと、上流が復旧しても二度と同期されず、\n * 再有効化の機会が永久に失われる。\n *\n * @param scope Restrict to one tenant, or one group within it. テナント、または特定グループへ限定。\n */\n listSyncableLinks(scope: { tenantId?: string; groupId?: number }): Promise<SyncLink[]>\n\n /** Reads the local ledger and the real membership for one group. 台帳と実所属を読む処理。 */\n loadGroupSnapshot(link: SyncLink): Promise<LocalGroupSnapshot>\n\n /**\n * Resolves upstream members against the local user table.\n * 上流メンバーをローカルユーザー表へ突合する処理。\n *\n * 突合順は上流 ID が第一キー、アドレスが第二キー。再利用されたアドレスが他人の所属を\n * 継承する事故を防ぐため、この順序は実装側の裁量ではなく契約である。\n *\n * ⚠️ **入力と同じ件数を返すこと。** 解決できないメンバーは省略せず `resolvedUserId: null` で\n * 返す。省略すると、その人物は「上流から消えた」と解釈されて削除対象になる。\n */\n resolveMembers(members: readonly DirectoryMember[]): Promise<ResolvedDirectoryMember[]>\n\n /**\n * Applies one group's plan, taking every membership removal through the lockout guard.\n * 1 グループ分の plan を適用する処理。実所属の削除は必ず最後の管理者ガードを通す。\n *\n * ⚠️ ロックアウトガードが 1 件の削除を拒否しても、**残りの削除を続けること**。1 件で打ち切ると、\n * 拒否された行がグループ全体の収束を永久に止める (次回以降も同じ行で止まる)。拒否は\n * {@link ApplyReport.refusals} で報告する。\n *\n * @returns What was refused. 拒否された内容。\n * @throws When the write itself failed. 書込そのものが失敗した場合。\n */\n applyPlan(link: SyncLink, plan: GroupSyncPlan, observedAt: Date): Promise<ApplyReport>\n\n /** Records a successful run and reactivates the group if it was deactivated. 成功の記録。 */\n recordSuccess(link: SyncLink, group: DirectoryGroup, syncedAt: Date): Promise<void>\n\n /** Records a failure reason against the link, leaving membership untouched. 失敗を記録する処理。 */\n recordFailure(link: SyncLink, reason: string): Promise<void>\n\n /**\n * Records one observation that the group is missing upstream. 上流での消滅を 1 回観測した記録。\n *\n * @returns How many consecutive observations there now are. 連続観測回数。\n */\n recordVanishedObservation(link: SyncLink, reason: string): Promise<number>\n\n /** Marks a group inactive after it has been missing long enough. 消滅確定後の無効化。 */\n markGroupInactive(link: SyncLink): Promise<void>\n}\n\n/** Minimal logging surface. 最小のログ面。 */\nexport type SyncLoggerPort = {\n info(message: string, ...args: unknown[]): void\n warn(message: string, ...args: unknown[]): void\n error(message: string, ...args: unknown[]): void\n}\n","/**\n * Consumer side of the \"sync now\" request queue.\n * 「今すぐ同期」要求キューの消費側。\n *\n * 管理 UI は上流へ到達できないため、要求を `TDDirectorySyncRequest` の行として残す\n * (`createDirectoryLinkStore.requestSync`)。それを拾って結果を書き戻すのは、資格情報を持つ\n * 同期ジョブだけである。生産側と消費側を別モジュールに分けているのは、web プロセスが\n * 消費側の関数を import できてしまう状態そのものを避けるためではなく、**どちらの責務が\n * どのプロセスに属するかを読んで分かる形にするため**である。\n */\nimport { assertTenantId } from \"@aiquants/authz-core\"\nimport { and, asc, eq, isNull } from \"drizzle-orm\"\nimport type { DirectoryDb, EnsureConnection } from \"./db\"\nimport type { AuthDirectoryTables } from \"./schema\"\n\n/** One claimed request. 取得済みの要求 1 件。 */\nexport type ClaimedSyncRequest = {\n id: number\n tenantId: string\n /** `null` means every group of the tenant. `null` はテナント全件を意味する。 */\n groupId: number | null\n requestedBy: string\n}\n\n/** How a claimed request ended. 取得した要求の終わり方。 */\nexport type SyncRequestResult = { result: \"succeeded\" | \"failed\"; error?: string | null }\n\n/** Options for {@link createSyncRequestQueue}. キュー消費側の生成オプション。 */\nexport type SyncRequestQueueOptions = {\n ensureConnection?: EnsureConnection\n /** Injected clock — the run's timestamps must all come from one source. 注入時計。 */\n now: () => Date\n /**\n * Longest an error message may be, matching the column. 列に合わせたエラー文の上限長。\n *\n * 超過分は切り捨てる。切り捨てないと、記録そのものが失敗して「何が起きたか分からない\n * まま要求が取得済みで固まる」という最悪の状態になる。\n */\n maxErrorLength?: number\n}\n\n/** `TDDirectorySyncRequest.error` の列長。 */\nconst ERROR_COLUMN_LENGTH = 2000\n\n/**\n * Creates the consumer for the synchronization request queue.\n * 同期要求キューの消費側を生成する処理。\n *\n * @param dbUnknown - Drizzle database instance. drizzle のデータベースインスタンス。\n * @param tables - Directory tables (owns the queue). ディレクトリ表 (キューの所有者)。\n * @param opts - Connection, clock and truncation bound. 接続・時計・切り詰め上限。\n * @returns The claim / finish pair. 取得と完了記録の 2 操作。\n */\nexport function createSyncRequestQueue(dbUnknown: unknown, tables: AuthDirectoryTables, opts: SyncRequestQueueOptions) {\n const db = dbUnknown as DirectoryDb\n const ready = opts.ensureConnection ?? (async () => {})\n const queue = tables.TDDirectorySyncRequest\n const maxErrorLength = opts.maxErrorLength ?? ERROR_COLUMN_LENGTH\n if (!Number.isInteger(maxErrorLength) || maxErrorLength < 1 || maxErrorLength > ERROR_COLUMN_LENGTH) {\n throw new TypeError(`[@aiquants/auth-directory-drizzle] maxErrorLength must be an integer within 1..${ERROR_COLUMN_LENGTH}, received ${String(opts.maxErrorLength)}`)\n }\n\n return {\n /**\n * Claims the oldest unpicked request, marking it picked in the same statement.\n * 最も古い未取得の要求を、同じ文で取得済みにしながら取り出す処理。\n *\n * 取得と印付けを分けると、2 つのジョブが同じ要求を拾って同じグループを二重に同期する。\n * `UPDATE ... OUTPUT` は 1 文なので、行を掴んだのが誰かは常に 1 つに決まる。\n *\n * @returns The claimed request, or `null` when the queue is empty. 取得結果 (空なら `null`)。\n */\n async claim(): Promise<ClaimedSyncRequest | null> {\n await ready()\n const pending = await db.select<{ id: number }>({ id: queue.id }).from(queue).where(isNull(queue.pickedAt)).orderBy(asc(queue.id))\n\n for (const candidate of pending) {\n // 取得は compare-and-swap で行う。`picked_at is null` を更新条件に残すことで、\n // 同じ行を狙った 2 つ目のジョブは 0 行しか返らず、静かに次の候補へ進む。\n // 先に select してから無条件 update すると、両方が同じグループを二重に同期する\n const rows = await db\n .update(queue)\n .set({ pickedAt: opts.now() })\n .output()\n .where(and(eq(queue.id, Number(candidate.id)), isNull(queue.pickedAt)))\n const row = (rows as Array<Record<string, unknown>> | undefined)?.[0]\n if (!row) continue\n const tenantId = String(row.tenant_id ?? row.tenantId ?? \"\")\n const rawGroupId = row.group_id ?? row.groupId ?? null\n return {\n id: Number(row.id),\n tenantId: assertTenantId(tenantId, \"claim(row.tenantId)\"),\n groupId: rawGroupId === null ? null : Number(rawGroupId),\n requestedBy: String(row.requested_by ?? row.requestedBy ?? \"\"),\n }\n }\n return null\n },\n\n /**\n * Records how a claimed request ended. 取得済みの要求がどう終わったかを記録する処理。\n *\n * @param id - The claimed request id. 取得した要求の ID。\n * @param outcome - Result and optional error text. 結果と (あれば) エラー文。\n * @returns Nothing. 戻り値なし。\n */\n async finish(id: number, outcome: SyncRequestResult): Promise<void> {\n await ready()\n const error = outcome.error ? outcome.error.slice(0, maxErrorLength) : null\n await db.update(queue).set({ finishedAt: opts.now(), result: outcome.result, error }).output().where(eq(queue.id, id))\n },\n\n /**\n * Lists requests claimed long ago but never finished. 取得後に完了しなかった要求の一覧。\n *\n * ジョブが落ちると、行は取得済みのまま完了しない。放置すると要求した人はいつまでも\n * 結果を見られないので、運用が拾えるよう明示的に数えられる形にしておく。\n *\n * @param olderThan - Cutoff for `picked_at`. `picked_at` の締切時刻。\n * @returns The stranded request ids. 取り残された要求の ID。\n */\n async listStranded(olderThan: Date): Promise<number[]> {\n await ready()\n const rows = await db.select<{ id: number; pickedAt: string | null }>({ id: queue.id, pickedAt: queue.pickedAt }).from(queue).where(isNull(queue.finishedAt)).orderBy(asc(queue.id))\n return rows.filter((r) => r.pickedAt !== null && new Date(r.pickedAt) < olderThan).map((r) => Number(r.id))\n },\n }\n}\n","/**\n * @module sync-repository\n * @description Drizzle implementation of the storage operations one synchronization run performs.\n * 1 回の同期実行が行う記憶域操作の drizzle 実装。\n *\n * **削除は必ず認可側のロックアウトガードを通す。** 所属はグループ経由でロールに繋がるため、\n * 上流の 1 人の離脱がそのテナントの最後の管理者を奪いうる。判定と書込を分離すると、同時実行で\n * 双方が判定を通過して管理者を失うため、`guardedWrite` の直列化トランザクションの内側で書く。\n *\n * **1 件の拒否でグループ全体を止めない。** ガードが 1 人の削除を拒否したときに例外で打ち切ると、\n * その時点までの書込が commit 済みのまま残り、次回以降も同じ行で同じ位置で止まる。拒否された\n * 1 行がグループ全体の収束を永久に妨げるため、拒否は集約して報告し、残りの削除は続行する。\n */\nimport { normalizeAuthEmail } from \"@aiquants/auth-core\"\nimport type { DirectoryGroup, DirectoryMember, GroupSyncPlan, LocalGroupSnapshot, ResolvedDirectoryMember } from \"@aiquants/auth-directory-core\"\nimport { assertTenantId, assertTenantMatch } from \"@aiquants/authz-core\"\nimport type { AuthzTables, createAdminLockoutGuard } from \"@aiquants/authz-drizzle\"\nimport { and, eq, inArray, isNull, notInArray, or } from \"drizzle-orm\"\nimport type { DirectoryDb, EnsureConnection } from \"./db\"\nimport { assertMembershipMode } from \"./membership-mode\"\nimport type { AuthDirectoryTables } from \"./schema\"\nimport { chunk, chunkByParameters } from \"./sql-limits\"\n\n/** Columns the ledger INSERT binds per row. 台帳 INSERT が 1 行あたり束ねる列数。 */\nconst LEDGER_INSERT_PARAMETERS = 7\n/** Columns the membership INSERT binds per row. 所属 INSERT が 1 行あたり束ねる列数。 */\nconst MEMBERSHIP_INSERT_PARAMETERS = 4\n\nimport type { ApplyReport, DirectorySyncRepository, RemovalOutcome, SyncLink } from \"./sync-ports\"\n\n/**\n * Resolves upstream members against the host's identity tables.\n * 上流メンバーをホストの identity 表へ突合するポート。\n *\n * ホストごとに identity の持ち方が違う (このリポジトリでは `TDOpenid` → `TDUsers`) ため、\n * パッケージ側では表を名指しできない。突合順が **上流 ID 第一・アドレス第二** であることは\n * 実装側の裁量ではなく契約である — 再利用されたアドレスが他人の所属を継承する事故を防ぐ。\n *\n * ⚠️ 入力と同じ件数を返すこと。解決できないメンバーを省略すると、その人物は削除対象になる。\n */\nexport type MemberResolverPort = {\n resolve(members: readonly DirectoryMember[]): Promise<ResolvedDirectoryMember[]>\n}\n\n/** Options for the synchronization repository. 同期リポジトリの構成。 */\nexport type SyncRepositoryOptions = {\n ensureConnection?: EnsureConnection\n memberResolver: MemberResolverPort\n /** Lockout guard from `@aiquants/authz-drizzle`. 認可側のロックアウトガード。 */\n lockoutGuard: ReturnType<typeof createAdminLockoutGuard>\n /**\n * Actor recorded on rows the synchronization writes. 同期が書く行に記録する実行者。\n *\n * 空にすると、同期が作った行と手作業で入った行を監査上区別できなくなる。しかも更新系では\n * 最後に触れた人間の名前が残ったまま更新時刻だけ進み、監査が同期の変更を人間の仕業として\n * 記録することになる。\n */\n actor: string\n}\n\n/**\n * Creates the drizzle-backed synchronization repository.\n * drizzle を用いた同期リポジトリを生成する処理。\n *\n * @param dbUnknown Drizzle database instance. drizzle のデータベースインスタンス。\n * @param tables Identity-side tables. identity 側テーブル群。\n * @param authzTables Authorization tables (group master and real membership). 認可テーブル群。\n * @param opts Injected resolver, lockout guard and audit actor. 注入する構成。\n * @returns The repository. リポジトリ。\n */\nexport function createDirectorySyncRepository(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: SyncRepositoryOptions): DirectorySyncRepository {\n const db = dbUnknown as DirectoryDb\n const ready = opts.ensureConnection ?? (async () => {})\n const { TDGroupExternalLink: link, TDGroupMemberEmail: ledger } = tables\n const { TMGroup: group, TDGroupMember: groupMember } = authzTables\n const actor = opts.actor\n\n return {\n async listSyncableLinks(scope) {\n await ready()\n const conditions = [eq(link.isSyncEnabled, true)]\n if (scope.tenantId !== undefined) conditions.push(eq(link.tenantId, assertTenantId(scope.tenantId, \"listSyncableLinks(tenantId)\")))\n if (scope.groupId !== undefined) conditions.push(eq(link.groupId, scope.groupId))\n const rows = await db\n .select<Record<string, unknown>>({\n linkId: link.id,\n groupId: link.groupId,\n linkTenantId: link.tenantId,\n groupTenantId: group.tenantId,\n groupKey: group.groupKey,\n isGroupActive: group.isActive,\n provider: link.provider,\n externalId: link.externalId,\n externalKey: link.externalKey,\n membershipMode: link.membershipMode,\n vanishedObservations: link.vanishedObservations,\n })\n .from(link)\n // 無効化されたグループも返す。除外すると上流が復旧しても二度と読まれず、\n // 再有効化の機会が永久に失われる (無効化が一方通行の扉になる)\n .innerJoin(group, eq(group.id, link.groupId))\n .where(and(...conditions))\n return rows.map((r): SyncLink => {\n // リンク行のテナントは親からの非正規化コピーである。乖離した行を素通しすると、\n // 名乗っていないテナントのグループを書き換えることになる\n assertTenantMatch(String(r.groupTenantId), String(r.linkTenantId), \"listSyncableLinks(link.tenantId vs TMGroup.tenant_id)\")\n return {\n linkId: Number(r.linkId),\n groupId: Number(r.groupId),\n groupKey: String(r.groupKey),\n tenantId: String(r.groupTenantId),\n provider: String(r.provider),\n externalId: String(r.externalId),\n externalKey: String(r.externalKey),\n // 既定へ倒さない。未知の値を direct と読むと、入れ子展開のつもりのリンクが\n // 直接所属だけを返し、入れ子側の全員が削除候補になる\n membershipMode: assertMembershipMode(String(r.membershipMode)),\n isGroupActive: Boolean(r.isGroupActive),\n vanishedObservations: Number(r.vanishedObservations ?? 0),\n }\n })\n },\n\n async loadGroupSnapshot(syncLink): Promise<LocalGroupSnapshot> {\n await ready()\n const [ledgerRows, memberRows] = await Promise.all([\n db\n .select<{ email: string; externalUserId: string | null; resolvedUserId: number | null }>({\n email: ledger.email,\n externalUserId: ledger.externalUserId,\n resolvedUserId: ledger.resolvedUserId,\n })\n .from(ledger)\n .where(eq(ledger.groupId, syncLink.groupId)),\n db.select<{ userId: number }>({ userId: groupMember.userId }).from(groupMember).where(eq(groupMember.groupId, syncLink.groupId)),\n ])\n return {\n ledger: ledgerRows.map((r) => ({\n email: String(r.email),\n ...(r.externalUserId ? { externalUserId: String(r.externalUserId) } : {}),\n resolvedUserId: r.resolvedUserId === null ? null : Number(r.resolvedUserId),\n })),\n // 実測値を渡す。台帳から導出すると、手作業で入った行や連携前の残骸を取り違える\n projectedUserIds: memberRows.map((r) => Number(r.userId)),\n }\n },\n\n resolveMembers(members) {\n return opts.memberResolver.resolve(members)\n },\n\n async applyPlan(syncLink: SyncLink, plan: GroupSyncPlan, observedAt: Date): Promise<ApplyReport> {\n assertTenantId(syncLink.tenantId, \"applyPlan(link.tenantId)\")\n await ready()\n\n // --- 追加系 ---------------------------------------------------------------\n // 所属を増やすだけなのでロックアウト判定を必要としない。1 トランザクションで書き切り、\n // 台帳だけ進んで所属が入らない中途半端な状態を残さない\n if (plan.ledgerUpserts.length > 0 || plan.projectionAdds.length > 0) {\n await db.transaction(async (tx) => {\n // 存在確認を 1 件ずつ発行すると、3,000 人のグループで 3,000 往復になる。\n // アドレスをまとめて引き、その結果で挿入と更新に振り分ける\n const upserts = plan.ledgerUpserts.map((m) => ({ ...m, email: normalizeAuthEmail(m.email) }))\n const existing = new Map<string, number>()\n for (const emails of chunk(upserts.map((m) => m.email))) {\n const rows = await tx\n .select<{ id: number; email: string }>({ id: ledger.id, email: ledger.email })\n .from(ledger)\n .where(and(eq(ledger.groupId, syncLink.groupId), inArray(ledger.email, emails)))\n for (const r of rows) existing.set(String(r.email), Number(r.id))\n }\n\n const inserts = upserts.filter((m) => !existing.has(m.email))\n // 1 行 7 列。行数で割ると 500 行 = 3500 パラメータになり、上限 2100 を越えて\n // 実行される前に拒否される (しかも再試行しても同じ場所で落ちる)\n for (const batch of chunkByParameters(inserts, LEDGER_INSERT_PARAMETERS)) {\n await tx\n .insert(ledger)\n .output()\n .values(batch.map((m) => ({ groupId: syncLink.groupId, email: m.email, externalUserId: m.externalUserId ?? null, resolvedUserId: m.resolvedUserId, observedAt, createdAt: observedAt, createdBy: actor })))\n }\n for (const m of upserts) {\n const id = existing.get(m.email)\n if (id === undefined) continue\n await tx\n .update(ledger)\n .set({ externalUserId: m.externalUserId ?? null, resolvedUserId: m.resolvedUserId, observedAt, updatedAt: observedAt, updatedBy: actor })\n .output()\n .where(eq(ledger.id, id))\n }\n\n // 所属 INSERT は 1 行 4 列。同じ batch を `IN` 述語にも使うため、狭いほうに合わせる\n for (const batch of chunkByParameters(plan.projectionAdds, MEMBERSHIP_INSERT_PARAMETERS)) {\n // 既に居る行を弾いてから挿入する。初回ログインの昇格と競合したとき、\n // 一意制約違反でグループ全体の同期を落とさないため\n const present = new Set(\n (\n await tx\n .select<{ userId: number }>({ userId: groupMember.userId })\n .from(groupMember)\n .where(and(eq(groupMember.groupId, syncLink.groupId), inArray(groupMember.userId, batch)))\n ).map((r) => Number(r.userId)),\n )\n const missing = batch.filter((id) => !present.has(id))\n if (missing.length > 0) {\n await tx\n .insert(groupMember)\n .output()\n .values(missing.map((userId) => ({ groupId: syncLink.groupId, userId, createdAt: observedAt, createdBy: actor })))\n }\n }\n })\n }\n\n // --- 削除系 ---------------------------------------------------------------\n // 1 件ずつロックアウトガードを通す。`AdminRemoval` は記述子を 1 つしか取らず、\n // 一括版を認可コアへ足すと境界が壊れる。件数は突合側のブレーカが抑えている\n const refusals: RemovalOutcome[] = []\n for (const userId of plan.projectionRemoves) {\n try {\n await opts.lockoutGuard.guardedWrite(syncLink.tenantId, { removeGroupMembership: { groupId: syncLink.groupId, userId } }, \"remove-group-member\", (run) =>\n (run as unknown as DirectoryDb).delete(groupMember).where(and(eq(groupMember.groupId, syncLink.groupId), eq(groupMember.userId, userId))),\n )\n } catch (error) {\n // ガードの拒否で打ち切らない。打ち切ると、拒否された 1 行が以後の実行でも\n // 同じ位置で止まり、グループ全体が永久に収束しなくなる\n refusals.push({ userId, applied: false, refusal: error instanceof Error ? error.message : String(error) })\n }\n }\n\n // 台帳の削除は所属の削除より後に行う。逆順にすると、途中で失敗したときに\n // 「ログインできないのに所属だけ残る」状態になる。\n // ⚠️ 拒否された人物の台帳行は残す。所属を保ったままサインインだけ奪うと、権限は\n // 持っているのに入れないという、どちらの側から見ても説明のつかない状態になる\n const refusedUserIds = refusals.map((r) => r.userId)\n if (plan.ledgerDeletes.length > 0) {\n await db.transaction(async (tx) => {\n for (const batch of chunk(plan.ledgerDeletes)) {\n const keepRefused = refusedUserIds.length > 0 ? or(isNull(ledger.resolvedUserId), notInArray(ledger.resolvedUserId, refusedUserIds)) : undefined\n await tx.delete(ledger).where(and(eq(ledger.groupId, syncLink.groupId), inArray(ledger.email, batch), ...(keepRefused ? [keepRefused] : [])))\n }\n })\n }\n\n return { refusals }\n },\n\n async recordSuccess(syncLink, upstreamGroup: DirectoryGroup, syncedAt) {\n await ready()\n await db.transaction(async (tx) => {\n await tx\n .update(link)\n // `external_id` も書き戻す。管理 UI は運用者が知っている値 (アドレス) でしか\n // 連携を作れないが、アドレスは上流で改名され得る。最初の成功時に安定 ID へ\n // 置き換えておかないと、改名された瞬間にグループが「消滅した」と判定され、\n // その全員がサインインも帰属証明も失う。自己修復の経路はここ 1 本だけである\n .set({ syncedAt, syncError: null, vanishedObservations: 0, externalId: upstreamGroup.externalId, externalKey: normalizeAuthEmail(upstreamGroup.externalKey), updatedAt: syncedAt, updatedBy: actor })\n .output()\n .where(eq(link.id, syncLink.linkId))\n // 上流が戻っていれば再有効化する。無効化を一方通行にしないための唯一の経路\n if (!syncLink.isGroupActive) {\n await tx\n .update(group)\n .set({ isActive: true, updatedAt: syncedAt, updatedBy: actor })\n .output()\n .where(and(eq(group.id, syncLink.groupId), eq(group.tenantId, syncLink.tenantId)))\n }\n })\n },\n\n async recordFailure(syncLink, reason) {\n await ready()\n // `syncedAt` は進めない。失敗を成功時刻で覆い隠すと、陳腐化の警告が出なくなる\n await db\n .update(link)\n .set({ syncError: reason.slice(0, 1000), updatedAt: new Date(), updatedBy: actor })\n .output()\n .where(eq(link.id, syncLink.linkId))\n },\n\n async recordVanishedObservation(syncLink, reason) {\n await ready()\n const next = syncLink.vanishedObservations + 1\n await db\n .update(link)\n .set({ vanishedObservations: next, syncError: reason.slice(0, 1000), updatedAt: new Date(), updatedBy: actor })\n .output()\n .where(eq(link.id, syncLink.linkId))\n return next\n },\n\n async markGroupInactive(syncLink) {\n await ready()\n // 削除しない。削除するとロール割当 (運用者の付与意思) まで失われる\n await db\n .update(group)\n .set({ isActive: false, updatedAt: new Date(), updatedBy: actor })\n .output()\n .where(and(eq(group.id, syncLink.groupId), eq(group.tenantId, syncLink.tenantId)))\n },\n }\n}\n\n/** Exported so hosts can size their own batches to the same statement budget. 同じ分割幅を共有するため。 */\n"],"mappings":"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","names":["index_exports","__export","AUTH_DIRECTORY_PROVIDER_COLUMN","AUTH_DIRECTORY_TENANT_SCOPED_TABLES","MAX_IN_LIST","MAX_PARAMETERS","MEMBERSHIP_MODES","UNHEALTHY_SYNC_OUTCOMES","assertGroupMayBeLinked","assertMembershipMode","chunk","chunkByParameters","createDirectoryLinkStore","createDirectorySync","createDirectorySyncRepository","createPromotionRepository","createSyncRequestQueue","defaultExternalGroupGuardMessage","defaultSyncMessages","defineAuthDirectorySchema","findLedgerRowsForIdentity","promoteLedgerForUser","withExternalGroupGuards","__toCommonJS","import_authz_core","defaultExternalGroupGuardMessage","roleKey","groupKey","adminActionsOf","grants","roleId","adminAppKey","adminResourceKey","g","confersAdministration","actions","a","roleLabel","withExternalGroupGuards","store","opts","decorate","target","plain","refuseIfHeldByExternalGroup","tx","runner","tenantId","roleIds","assignments","seen","externalKey","fn","options","groupId","actor","input","r","projected","assertGroupMayBeLinked","import_drizzle_orm","findLedgerRowsForIdentity","db","tables","args","ledger","group","tenantId","externalUserId","email","project","probe","condition","r","finalize","rows","byId","a","b","byEmail","claimedByAnother","row","import_auth_core","import_auth_directory_core","import_authz_core","import_drizzle_orm","MEMBERSHIP_MODES","assertMembershipMode","value","createDirectoryLinkStore","dbUnknown","tables","authzTables","opts","db","ready","policyCacheTtlMs","policyCache","forgetPolicy","tenantId","link","ledger","membership","allowGroup","queue","group","groupMember","assertNotBreakGlass","runner","groupId","assertGroupInTenant","owned","toLinkRow","row","unresolvedCount","assertMembershipMode","unresolvedCounts","rows","r","counts","input","actor","membershipMode","externalId","externalKey","tx","isSyncEnabled","requestedBy","pending","enabled","stamps","oldest","t","isStale","a","b","memo","empty","proofGroupIds","breakGlass","policy","isBreakGlass","linked","remaining","isActive","identity","findLedgerRowsForIdentity","email","normalized","description","id","import_auth_core","import_authz_core","promoteLedgerForUser","repository","args","tenantId","email","externalUserId","matches","match","ledgerIds","m","a","groupIds","import_authz_core","import_drizzle_orm","MAX_PARAMETERS","MAX_IN_LIST","chunk","items","size","out","i","chunkByParameters","parametersPerItem","reservedParameters","budget","MEMBERSHIP_INSERT_PARAMETERS","RESERVED_FOR_MARKING","createPromotionRepository","dbUnknown","tables","authzTables","opts","db","ready","now","ledger","group","groupMember","args","findLedgerRowsForIdentity","tenantId","userId","ledgerIds","groupIds","tx","owned","part","chunk","rows","r","targets","id","existing","missing","ledgerChunkSize","createdAt","chunkByParameters","groupId","import_authz_drizzle","import_mssql_core","AUTH_DIRECTORY_PROVIDER_COLUMN","AUTH_DIRECTORY_TENANT_SCOPED_TABLES","defineAuthDirectorySchema","schemaName","opts","s","audit","insertOnlyAudit","TDGroupExternalLink","t","TDGroupMemberEmail","TDTenantMembershipGroup","TDAllowlistGroup","TDDirectorySyncRequest","TMAllowlist","import_auth_directory_core","sample","items","limit","defaultSyncMessages","reason","upstream","resolved","externalKey","observations","threshold","abort","axis","withheld","refusals","r","linkProvider","runnerProvider","UNHEALTHY_SYNC_OUTCOMES","createDirectorySync","config","directory","repository","policy","now","logger","messages","defaultSyncMessages","noteFailure","link","reason","error","readUpstream","group","handleMissing","observations","syncOne","upstream","snapshot","resolved","plan","hasWork","stamp","report","err","scope","links","outcomes","unhealthyCount","o","UNHEALTHY_SYNC_OUTCOMES","import_authz_core","import_drizzle_orm","ERROR_COLUMN_LENGTH","createSyncRequestQueue","dbUnknown","tables","opts","db","ready","queue","maxErrorLength","pending","candidate","row","tenantId","rawGroupId","id","outcome","error","olderThan","r","import_auth_core","import_authz_core","import_drizzle_orm","LEDGER_INSERT_PARAMETERS","MEMBERSHIP_INSERT_PARAMETERS","createDirectorySyncRepository","dbUnknown","tables","authzTables","opts","db","ready","link","ledger","group","groupMember","actor","scope","conditions","r","assertMembershipMode","syncLink","ledgerRows","memberRows","members","plan","observedAt","tx","upserts","m","existing","emails","chunk","rows","inserts","batch","chunkByParameters","id","present","missing","userId","refusals","run","error","refusedUserIds","keepRefused","upstreamGroup","syncedAt","reason","next"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/catalog-store.ts","../src/db.ts","../src/sql-limits.ts","../src/guards.ts","../src/ledger-identity.ts","../src/link-store.ts","../src/membership-mode.ts","../src/promote.ts","../src/promote-repository.ts","../src/schema.ts","../src/sync.ts","../src/sync-messages.ts","../src/sync-ports.ts","../src/sync-queue.ts","../src/sync-repository.ts"],"sourcesContent":["/**\n * @module @aiquants/auth-directory-drizzle\n * @description Drizzle (mssql) adapter for `@aiquants/auth-directory-core`.\n * `@aiquants/auth-directory-core` 向けの drizzle (mssql) アダプタ。\n *\n * identity 側のテーブル定義と、その上で動く 4 つの面を提供する:\n * 1. **リンクストア** — 外部リンク・帰属証明・グループ許可・同期要求キューの読み書き\n * 2. **同期リポジトリ** — 同期 1 実行分の記憶域操作 (削除は認可側のロックアウトガード経由)\n * 3. **昇格** — 初回ログイン直後に台帳を実所属へ引き上げる (上流へは到達しない)\n * 4. **ガード** — 外部供給グループへ管理ロールが付かないようにする認可ストアのデコレータ\n */\nexport * from \"./catalog-store\"\nexport * from \"./db\"\nexport * from \"./guards\"\nexport * from \"./ledger-identity\"\nexport * from \"./link-store\"\nexport * from \"./membership-mode\"\nexport * from \"./promote\"\nexport * from \"./promote-repository\"\nexport * from \"./schema\"\nexport * from \"./sql-limits\"\nexport * from \"./sync\"\nexport * from \"./sync-messages\"\nexport * from \"./sync-ports\"\nexport * from \"./sync-queue\"\nexport * from \"./sync-repository\"\n","/**\n * @module catalog-store\n * @description The upstream group list, written by the sync job and read by the web.\n * 同期ジョブが書き、web が読む、上流のグループ一覧。\n *\n * ⚠️ **この表と、このモジュールが在る理由は境界である。** ドメイン全体委任の資格情報は組織の\n * ディレクトリを丸ごと読めるため、外部からの要求を終端する web プロセスには置かない。上流へ\n * 届くのは同期ジョブだけとし、web は「ジョブが書いた行」を読む。管理面の選択欄は利便のための\n * 読み取りであり、その利便のために資格情報の置き場所を動かしてよい理由にはならない。\n *\n * ここには生産側 (`replace`) と消費側 (`read`) の両方が居る。プロセスを分ける仕掛けは\n * import の可否ではなく **資格情報を持つかどうか** であり、`replace` を呼べても上流を\n * 読めないプロセスは書くべき内容を持てない。\n */\nimport type { AuthDirectoryCatalogGroup, AuthDirectoryCatalogSnapshot } from \"@aiquants/auth-core\"\nimport { assertTenantId } from \"@aiquants/authz-core\"\nimport { and, asc, eq } from \"drizzle-orm\"\nimport { type DirectoryDb, type EnsureConnection, toInstant } from \"./db\"\nimport type { AuthDirectoryTables } from \"./schema\"\nimport { chunkByParameters } from \"./sql-limits\"\n\n/**\n * One upstream group, as observed. 観測した上流グループ 1 件。\n *\n * ⚠️ 別名ではなく `@aiquants/auth-core` の型そのものである。層ごとに「同じ意味の別の型」を宣言すると、\n * 構造的部分型のせいでどの境界も通ってしまい、上流へ足した欄が途中で黙って捨てられる。\n */\nexport type DirectoryCatalogEntry = AuthDirectoryCatalogGroup\n\n/** What the web reads. web が読む内容 ({@link AuthDirectoryCatalogSnapshot} の項目名違いではなく同一契約)。 */\nexport type DirectoryCatalogSnapshot = AuthDirectoryCatalogSnapshot\n\n/** Options a reader needs. 読み手が必要とするオプション。 */\nexport type DirectoryCatalogReaderOptions = {\n ensureConnection?: EnsureConnection\n}\n\n/**\n * Options a writer needs. 書き手が必要とするオプション。\n *\n * ⚠️ 読み手にこれを要求してはならない。読むだけの呼び出し側に時計と監査主体を持たせると、\n * その場で `replace` を呼ぶ経路が「既に手元にあるもの」になり、上流を読めないプロセスが\n * 観測時刻を詐称できてしまう。だから生成関数を 2 つに分けている。\n */\nexport type DirectoryCatalogWriterOptions = DirectoryCatalogReaderOptions & {\n /** Injected clock — one observation must carry one timestamp. 注入時計。 */\n now: () => Date\n /** Who the audit columns record. 監査列に残す主体。 */\n actor: string\n}\n\n/** `TDDirectoryCatalog` の 1 行が束ねるパラメータ数 (INSERT の列リストと一致させる)。 */\nconst PARAMETERS_PER_ROW = 10\n\n/**\n * 列長。超過は切り詰める — 1 件のせいで一覧全体が書けなくなる方が損である。\n *\n * ⚠️ テナントと供給元も含める。これらは設定由来なので普通は短いが、切らなければ SQL 8152 で\n * **置き換えごと** 失敗し、その配備の一覧は二度と更新されない (入力は決定的なので毎周期同じ)。\n */\nconst LIMITS = { tenantId: 64, provider: 32, externalId: 256, externalKey: 320, name: 256, description: 1000, actor: 50 } as const\n\n/** Largest value the `member_count` column (`int`) holds. `member_count` 列 (`int`) の上限。 */\nconst MAX_MEMBER_COUNT = 2_147_483_647\n\n/**\n * Reduces an upstream member count to something the column accepts. 上流の人数を列が受け取れる形へ落とす処理。\n *\n * ⚠️ 文字列や桁数は上流の答え次第であり、こちらでは決められない。`int` に収まらない値、\n * 小数、負数、`NaN` を素通しすると `INSERT` が算術エラーで落ちる — `replace` は 1 テナント分を\n * **1 つのトランザクション** で書くので、1 件の異常値が **その配備の一覧更新を丸ごと失わせる**。\n * しかも入力は決定的なので、次の周期以降も同じ場所で永久に落ち続ける。人数は選択欄の装飾に\n * すぎない。**値を捨てて一覧を守る** のが正しい。\n *\n * @param value - What the upstream answered. 上流が答えた値。\n * @returns A storable count, or `null`. 保存できる人数、または `null`。\n */\nfunction toStorableMemberCount(value: number | undefined): number | null {\n if (value === undefined || !Number.isFinite(value)) return null\n const whole = Math.trunc(value)\n if (whole < 0) return null\n return whole > MAX_MEMBER_COUNT ? MAX_MEMBER_COUNT : whole\n}\n\n/**\n * Refuses a blank or over-long scope key. 空、または列に収まらない範囲キーを拒否する処理。\n *\n * ⚠️ **範囲キーは切り詰めてはならない。** 名前や説明の切り詰めは見た目が縮むだけだが、テナントや\n * 供給元を切り詰めると **行の所属先そのものが変わる** — 先頭 64 文字を共有する 2 つのテナントが\n * 1 つに潰れる。ここは fail-fast が正しく、書けないなら書かないのが正しい。\n *\n * @param value The scope key. 範囲キー。\n * @param limit The column length. 列長。\n * @param name The field name, for the message. 文言に載せる欄名。\n * @returns The trimmed key. 前後の空白を落としたキー。\n * @throws {TypeError} When it is blank or too long. 空白のみ、または列長を越える場合。\n */\nfunction assertScopeKey(value: string, limit: number, name: string): string {\n const trimmed = value.trim()\n if (trimmed === \"\") throw new TypeError(`[@aiquants/auth-directory-drizzle] ${name} must not be blank`)\n if (trimmed.length > limit) throw new TypeError(`[@aiquants/auth-directory-drizzle] ${name} exceeds its column length (${limit}); truncating it would change which rows it owns`)\n return trimmed\n}\n\n/**\n * Creates the write side of the catalog. 候補一覧の **書き手** を生成する処理。\n *\n * ⚠️ これを呼べるのは上流の資格情報を持つプロセスだけであるべきである。読むだけの側は\n * {@link createDirectoryCatalogReader} を使う — 分けているのは整理のためではなく、「読むだけの\n * 呼び出し側の手元に `replace` が在る」状態そのものを作らないためである。\n *\n * @param dbUnknown - Drizzle database instance. drizzle のデータベースインスタンス。\n * @param tables - Directory tables (owns the catalog). ディレクトリ表 (候補一覧の所有者)。\n * @param opts - Connection, clock and actor. 接続・時計・監査主体。\n * @returns The `replace` operation. 置き換え操作。\n */\nexport function createDirectoryCatalogWriter(dbUnknown: unknown, tables: AuthDirectoryTables, opts: DirectoryCatalogWriterOptions) {\n const db = dbUnknown as DirectoryDb\n const ready = opts.ensureConnection ?? (async () => {})\n const catalog = tables.TDDirectoryCatalog\n const observation = tables.TMDirectoryCatalogObservation\n\n /**\n * 上流が同じ ID を 2 度返しても、一意制約に当てて全体を落とさない。後勝ちで畳む。\n *\n * ⚠️ **切り詰めた後の値で畳む。** 先に畳むと、列長を越えた先だけが違う 2 件が別物として残り、\n * 切り詰めた瞬間に同一キーになって一意制約に当たる。しかも入力は決定的なので、その配備の\n * 一覧は次の周期以降も永久に更新できない\n */\n const dedupe = (rows: readonly CatalogRow[]): CatalogRow[] => {\n const byId = new Map<string, CatalogRow>()\n for (const row of rows) byId.set(row.externalId, row)\n return [...byId.values()]\n }\n\n return {\n /**\n * Replaces this tenant's catalog with what the upstream currently answers.\n * このテナントの候補一覧を、上流の現在の答えで置き換える処理。\n *\n * ⚠️ 差分更新ではなく **全置換** である。消えたグループを残すと、管理面はそれを選ばせ続け、\n * 選んだ運用者は 10 分後に「上流に無い」と言われる。1 つのトランザクションで消してから\n * 書くことで、途中の状態を web に読ませない。\n *\n * @param tenantId - Owner tenant. 所有テナント。\n * @param provider - Upstream provider key. 上流プロバイダのキー。\n * @param entries - The upstream's current answer. 上流の現在の答え。\n * @returns How many rows were written. 書き込んだ行数。\n */\n async replace(tenantId: string, provider: string, entries: readonly DirectoryCatalogEntry[]): Promise<number> {\n await ready()\n const tenant = assertScopeKey(assertTenantId(tenantId, \"replace(tenantId)\"), LIMITS.tenantId, \"tenantId\")\n const upstream = assertScopeKey(provider, LIMITS.provider, \"provider\")\n // ⚠️ 実行者も列に収まることを確かめる。名前や説明と違って切り詰められない (誰の仕業かが\n // 変わる) し、収まらないまま書くと `INSERT` が SQL 8152 で落ちて **置き換えごと**\n // 巻き戻る — 設定由来の決定的な入力なので、その配備の一覧は次の周期以降も永久に\n // 更新されない。配備名で実行者を区別しようとすると 50 文字は意外に近い\n const writer = assertScopeKey(opts.actor, LIMITS.actor, \"actor\")\n const observedAt = opts.now()\n const rows = dedupe(\n entries.map((entry) => ({\n tenantId: tenant,\n provider: upstream,\n externalId: entry.externalId.slice(0, LIMITS.externalId),\n externalKey: entry.externalKey.slice(0, LIMITS.externalKey),\n name: entry.name.slice(0, LIMITS.name),\n description: entry.description === undefined ? null : entry.description.slice(0, LIMITS.description),\n memberCount: toStorableMemberCount(entry.memberCount),\n observedAt,\n createdAt: observedAt,\n createdBy: writer,\n })),\n )\n\n await db.transaction(async (tx) => {\n await tx.delete(catalog).where(and(eq(catalog.tenantId, tenant), eq(catalog.provider, upstream)))\n // ⚠️ 上限は行数ではなくパラメータ数である。10 列 × 500 行 = 5000 で、上限 2100 を\n // 越えた文は **実行される前に** 拒否される。しかも越えるのは件数が閾値を跨いだ日だけ\n for (const batch of chunkByParameters(rows, PARAMETERS_PER_ROW)) await tx.insert(catalog).values(batch)\n // ⚠️ 観測の事実は **行数から独立して** 記録する。同じトランザクションで書くのは、\n // 「一覧は新しいのに観測記録は古い」という読み方をどの瞬間にも許さないため\n await tx.delete(observation).where(and(eq(observation.tenantId, tenant), eq(observation.provider, upstream)))\n await tx.insert(observation).values([{ tenantId: tenant, provider: upstream, observedAt, entryCount: rows.length, createdAt: observedAt, createdBy: writer, updatedAt: observedAt, updatedBy: writer }])\n })\n return rows.length\n },\n }\n}\n\n/** 書込み 1 行分の形 (列長を切り詰めた後)。 */\ntype CatalogRow = {\n tenantId: string\n provider: string\n externalId: string\n externalKey: string\n name: string\n description: string | null\n memberCount: number | null\n observedAt: Date\n createdAt: Date\n createdBy: string\n}\n\n/**\n * Creates the read side of the catalog. 候補一覧の **読み手** を生成する処理。\n *\n * 時計も監査主体も要らない。要らないものを求めないことが、この関数がここに在る理由である。\n *\n * @param dbUnknown - Drizzle database instance. drizzle のデータベースインスタンス。\n * @param tables - Directory tables (owns the catalog). ディレクトリ表 (候補一覧の所有者)。\n * @param opts - Connection handling. 接続の扱い。\n * @returns The `read` operation. 読み取り操作。\n */\nexport function createDirectoryCatalogReader(dbUnknown: unknown, tables: AuthDirectoryTables, opts: DirectoryCatalogReaderOptions = {}) {\n const db = dbUnknown as DirectoryDb\n const ready = opts.ensureConnection ?? (async () => {})\n const catalog = tables.TDDirectoryCatalog\n const observation = tables.TMDirectoryCatalogObservation\n\n return {\n /**\n * Reads this tenant's catalog. このテナントの候補一覧を読む処理。\n *\n * ⚠️ **観測の有無は行数から導かない。** 上流を正しく読めて答えが 0 件だった配備 (借用先の\n * 権限が狭い、対象 OU が空) と、同期が一度も走っていない配備は、候補一覧の行数ではどちらも\n * 0 である。前者を「同期が動いていない」と診断すると、運用は健全な部品を調べに行く。\n * `observedAt` は観測記録そのものから取り、`null` は **一度も観測していない** だけを意味する。\n *\n * @param tenantId - Owner tenant. 所有テナント。\n * @param provider - Upstream provider key. 上流プロバイダのキー。\n * @returns The groups and the observation time. グループと観測時刻。\n */\n async read(tenantId: string, provider: string): Promise<DirectoryCatalogSnapshot> {\n await ready()\n const tenant = assertScopeKey(assertTenantId(tenantId, \"read(tenantId)\"), LIMITS.tenantId, \"tenantId\")\n const upstream = assertScopeKey(provider, LIMITS.provider, \"provider\")\n // ⚠️ 一覧と観測記録を **1 文で** 読む。2 文に分けると、置き換えの commit を跨いだ\n // 組み合わせ (古い一覧 + 新しい観測記録) が観測でき、下の整合検査が健全な記憶域を\n // 「壊れている」と判定して選択欄を消す。置き換えは毎周期走るので、窓は狭くない。\n //\n // ⚠️ **トランザクションで囲んでも解決しない。** 2 文を並行に投げると node-mssql は\n // `EREQINPROG` で断る (1 トランザクション = 1 接続 = 同時に 1 要求)。順番に投げれば\n // 通るが、READ COMMITTED では文ごとに別のスナップショットを見るため、跨ぐ問題は\n // そのまま残る。1 文にすれば、隔離レベルに依らず組み合わせが原子的に決まる。\n //\n // ⚠️ 起点は **観測記録の側** である。一覧を起点にすると「上流を読めて 0 件だった」\n // 配備で 1 行も返らず、未観測と区別できなくなる — その区別こそがこの表の存在理由である\n const rows = await db\n .select<{ observedAt: string | Date; entryCount: number; externalId: string | null; externalKey: string | null; name: string | null; description: string | null; memberCount: number | null }>({\n observedAt: observation.observedAt,\n entryCount: observation.entryCount,\n externalId: catalog.externalId,\n externalKey: catalog.externalKey,\n name: catalog.name,\n description: catalog.description,\n memberCount: catalog.memberCount,\n })\n .from(observation)\n .leftJoin(catalog, and(eq(catalog.tenantId, observation.tenantId), eq(catalog.provider, observation.provider)))\n .where(and(eq(observation.tenantId, tenant), eq(observation.provider, upstream)))\n .orderBy(asc(catalog.name))\n\n const groups = rows\n .filter((row) => row.externalId !== null)\n .map((row) => ({\n externalId: String(row.externalId),\n externalKey: String(row.externalKey),\n name: String(row.name),\n ...(row.description === null ? {} : { description: row.description }),\n ...(row.memberCount === null ? {} : { memberCount: Number(row.memberCount) }),\n }))\n const mark = rows[0]\n if (mark === undefined) return { groups, observedAt: null }\n // ⚠️ 観測記録は行数から独立して持つが、**食い違ってはならない**。食い違うのは片方だけが\n // 消えた (手作業の掃除、部分復元、供給元キーの改名) ときであり、そのときの一覧は上流の\n // 写しではない。数え直せば判るのに黙って「観測済み」と答えると、運用者は消えたはずの\n // グループを自信をもって選ぶ\n if (Number(mark.entryCount) !== groups.length) {\n throw new Error(`[@aiquants/auth-directory-drizzle] catalog is inconsistent for (${tenant}, ${upstream}): the observation records ${mark.entryCount} entries but ${groups.length} rows are present`)\n }\n const observedAt = toInstant(mark.observedAt)\n // ⚠️ 読めない時刻を `null` (= 未観測) に丸めない。壊れた値を「無かったこと」として返すと、\n // 記憶域の破損が運用上は「まだ一度も同期していない」に化ける — 調べる先が変わってしまう。\n // 整合検査と同じく、信用できない記憶域は使わせない側へ倒す\n if (!Number.isFinite(observedAt.getTime())) {\n throw new Error(`[@aiquants/auth-directory-drizzle] catalog observation for (${tenant}, ${upstream}) has an unreadable observed_at`)\n }\n return { groups, observedAt }\n },\n }\n}\n","/**\n * @module db\n * @description Minimal query-builder shapes this adapter uses.\n * 本アダプタが使う最小の問い合わせビルダー形状。\n *\n * drizzle の具体型ではなく構造的な型で受けるのは、ホストが用意した `NodeMsSqlDatabase` の型\n * パラメータ (スキーマ・ロガー等) が配備ごとに違い、それを署名へ引き回すと利用側が\n * `as any` を書く羽目になるためである。実体は実行時にこれらを満たす。\n */\n\ntype Rows<T> = PromiseLike<T[]>\n\nexport interface SelectChain<T> extends Rows<T> {\n from(t: unknown): SelectChain<T>\n innerJoin(t: unknown, on: unknown): SelectChain<T>\n leftJoin(t: unknown, on: unknown): SelectChain<T>\n where(cond: unknown): SelectChain<T>\n orderBy(...cols: unknown[]): SelectChain<T>\n groupBy(...cols: unknown[]): SelectChain<T>\n}\n\nexport interface InsertChain {\n values(v: unknown): PromiseLike<unknown>\n output(): InsertChain\n}\n\nexport interface UpdateChain {\n set(v: unknown): UpdateChain\n output(): UpdateChain\n where(cond: unknown): PromiseLike<unknown>\n}\n\nexport interface DeleteChain {\n where(cond: unknown): PromiseLike<unknown>\n}\n\n/** The database surface the directory adapter needs. ディレクトリアダプタが必要とする DB 面。 */\nexport interface DirectoryDb {\n select<T = Record<string, unknown>>(fields?: unknown): SelectChain<T>\n insert(t: unknown): InsertChain\n update(t: unknown): UpdateChain\n delete(t: unknown): DeleteChain\n transaction<T>(fn: (tx: DirectoryDb) => Promise<T>, config?: unknown): Promise<T>\n}\n\n/** Ensures a connection exists before the first statement. 最初の文の前に接続を確立する処理。 */\nexport type EnsureConnection = () => Promise<void>\n\n/**\n * Reads a driver timestamp as an instant, treating an offset-less string as UTC.\n * ドライバの時刻を瞬間として読む処理 (オフセットの無い文字列は UTC として解釈)。\n *\n * ⚠️ オフセットを持たない ISO 文字列 (`2026-09-09 03:04:05.000`) は仕様上 **実行環境の時間帯**\n * で解釈される。ドライバは通常 `Date` を返すのでこの経路は普段通らないが、通った日に JST なら\n * 9 時間ずれる — 鮮度は「常に古い」と報告され、締切の比較は 9 時間ぶん甘くなる。読む場所ごとに\n * 対処すると必ずどこかが漏れるので、1 つの関数に閉じる。\n *\n * @param value - What the driver returned. ドライバが返した値。\n * @returns The instant. その瞬間。\n */\nexport function toInstant(value: string | Date): Date {\n if (value instanceof Date) return value\n return new Date(/(?:Z|[+-]\\d{2}:?\\d{2})$/.test(value) ? value : `${value.replace(\" \", \"T\")}Z`)\n}\n","/**\n * SQL Server parameter-limit helpers shared by every batched statement.\n * 一括文が共有する SQL Server のパラメータ上限まわりのヘルパー。\n *\n * ⚠️ 上限は **行数ではなくパラメータ数** である。1 行あたり何個の値を束ねるかは文ごとに違うので、\n * 行数だけで分割すると、列の多い `VALUES` が静かに上限を越える。しかも越えるのは件数が閾値を\n * 跨いだ日だけで、それまでは同じコードが問題なく動いて見える。\n */\n\n/**\n * Hard limit of parameters per statement. 1 文あたりのパラメータ数の上限。\n *\n * SQL Server の RPC 1 回あたりのパラメータ上限は 2100。数個は文自身の値 (`SET` 句・スカラー\n * 述語) が使うため、分割の基準にはここから余裕を引いた値を用いる。\n */\nexport const MAX_PARAMETERS = 2100\n\n/** Parameters reserved for the statement's own scalar values. 文自身のスカラー値のための予約枠。 */\nexport const RESERVED_PARAMETERS = 100\n\n/**\n * Maximum ids per `IN` list of single-column predicates. 単列述語の `IN` 句 1 回あたりの最大 ID 数。\n *\n * 1 行 1 パラメータの用途 (`inArray(col, ids)`) 専用の既定値である。複数列を束ねる `VALUES` や、\n * 1 文に `IN` を 2 つ描画する `UPDATE` にそのまま使ってはならない — {@link chunkByParameters} を使う。\n */\nexport const MAX_IN_LIST = 500\n\n/**\n * Splits a list into chunks that fit one statement.\n * 1 文に収まる大きさへリストを分割する処理。\n *\n * @param items - Items to split. 分割対象。\n * @param size - Maximum chunk size. 1 チャンクの最大数。\n * @returns Chunks in the original order. 元の順序を保ったチャンク列。\n * @throws {TypeError} When the size is not a positive integer. 正の整数でない場合。\n */\nexport function chunk<T>(items: readonly T[], size = MAX_IN_LIST): T[][] {\n if (!Number.isInteger(size) || size <= 0) throw new TypeError(`[@aiquants/auth-directory-drizzle] chunk size must be a positive integer, received ${String(size)}`)\n const out: T[][] = []\n for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))\n return out\n}\n\n/**\n * Splits a list so each chunk fits the parameter budget of one statement.\n * 各チャンクが 1 文のパラメータ枠に収まるようリストを分割する処理。\n *\n * `VALUES` は 1 行につき列数ぶんのパラメータを束ねる。7 列の台帳行なら 500 行で 3500 個になり、\n * 上限 2100 を越えて **実行される前に** 拒否される。しかもその失敗は再試行しても同じ場所で起き、\n * そのグループは永久に台帳を持てない。\n *\n * @param items - Items to split. 分割対象。\n * @param parametersPerItem - Parameters each item binds. 1 件が束ねるパラメータ数。\n * @param reservedParameters - Parameters the statement itself binds. 文自身が束ねる数。\n * @returns Chunks in the original order. 元の順序を保ったチャンク列。\n * @throws {TypeError} When a single item cannot fit the budget. 1 件すら収まらない場合。\n */\nexport function chunkByParameters<T>(items: readonly T[], parametersPerItem: number, reservedParameters = RESERVED_PARAMETERS): T[][] {\n if (!Number.isInteger(parametersPerItem) || parametersPerItem <= 0) {\n throw new TypeError(`[@aiquants/auth-directory-drizzle] parametersPerItem must be a positive integer, received ${String(parametersPerItem)}`)\n }\n if (!Number.isInteger(reservedParameters) || reservedParameters < 0) {\n throw new TypeError(`[@aiquants/auth-directory-drizzle] reservedParameters must be a non-negative integer, received ${String(reservedParameters)}`)\n }\n const budget = MAX_PARAMETERS - reservedParameters\n const size = Math.floor(budget / parametersPerItem)\n // 1 件すら収まらないなら、分割では解決しない。黙って 1 件ずつ発行すると、実行時に必ず\n // 落ちる文をひたすら送り続けることになる\n if (size < 1) throw new TypeError(`[@aiquants/auth-directory-drizzle] a single item binds ${parametersPerItem} parameters, which exceeds the ${budget} available in one statement`)\n return chunk(items, size)\n}\n","/**\n * @module guards\n * @description Keeps administrative roles off groups whose membership an upstream directory owns.\n * 所属の所有者が上流ディレクトリにあるグループへ、管理ロールが付かないようにする。\n *\n * **不変条件は状態の性質であって、遷移の性質ではない。**\n *\n * 最初の実装は `assignGroupRole` だけを差し替えていた。しかしそれは「外部グループへ管理ロールを\n * 割り当てる」という 1 つの遷移を塞いだにすぎず、**無害なロールを外部グループへ割り当ててから、\n * そのロールを管理ロールへ昇格させる**という順序が開いたままだった。不変条件を守るには、それを\n * 崩し得るすべての遷移を塞ぐ必要がある。\n *\n * 崩し得る遷移は 3 つある。\n *\n * | # | 遷移 | 塞ぐもの |\n * | --- | --- | --- |\n * | 1 | 外部グループが管理ロールを得る | `assignGroupRole` |\n * | 2 | 外部グループが持つロールが管理ロールになる | `createGrant` |\n * | 3 | 管理ロールを持つグループが外部連携される | `assertGroupMayBeLinked` |\n * | 4 | 外部グループが持つロールの付与が差し替わって管理ロールになる | `updateGrant` |\n *\n * ⚠️ `updateGrant` を「塞ぐ必要がない」と読んではならない。行数だけを見る論法 —\n * 「`(role_id, resource_id, action)` は一意だから、アクションを差し替えても集合は増えない」 —\n * は **ロール 1 つの中でしか成り立たない**。判定はこのファイルのどの遷移でも\n * **グループの全ロールの合併** で行う。ロール 1 が `{read, create}`、ロール 2 が\n * `{read, update}` を持つ外部グループでは合併は 3 アクションで、どちらの割当も正当に通る。\n * そこでロール 2 の `read` を `delete` へ差し替えると、ロール 2 の行数は 2 のままなのに\n * 合併は 4 アクションに達し、上流のディレクトリ管理者がこの配備の管理者を決められるようになる。\n *\n * `setRoleActive` も塞ぐ必要がない。遷移 1 の判定はロールの有効・無効を見ないため、無効な管理\n * ロールも外部グループへは割り当てられない。したがって「無効な管理ロールを外部グループが保持した\n * まま有効化される」という状態には到達し得ない。\n *\n * **なぜ認可パッケージの中で判定しないのか。** 判定を `@aiquants/authz-drizzle` の管理ストアへ\n * 埋め込むと、認可パッケージがグループの「出所」という identity 側の概念を知ることになる。依存の\n * 向きは auth → authz の一方通行で保ちたいので、包む側 (identity 側) から課す。\n *\n * **なぜ禁止するのか。** 外部グループの所属は上流の管理者が変更できる。そこへ管理ロールを付けると、\n * 上流側の 1 操作がこの配備の管理者を増やしたり、逆に最後の管理者を奪ったりする。ブレークグラス\n * 経路は常にローカルに残しておく必要がある。\n */\nimport { AUTHZ_ACTIONS, type AuthzAction, type AuthzAdminStore, type AuthzGrant } from \"@aiquants/authz-core\"\n\n/** The link lookup this guard needs. 本ガードが必要とするリンク参照。 */\nexport type ExternalLinkLookup = {\n /**\n * The upstream key of the group, or `null` when its membership is locally managed.\n * そのグループの上流キー。ローカル管理なら `null`。\n *\n * 「外部かどうか」と「拒否メッセージに出す名前」を 1 回の照会で返す。判定のためだけに\n * 引いて、名前のためにもう一度引く形にすると、拒否のたびに往復が 2 倍になる。\n *\n * ⚠️ `runner` を渡された実装は、**必ずその上で** 引くこと。別接続で引くと、この読み取りの\n * 結果を前提に書き込む呼び出し側にとって前提が保たれない (読んだ後・書く前に連携が作られる)。\n */\n externalGroupKey(tenantId: string, groupId: number, runner?: unknown): Promise<string | null>\n}\n\n/** Configuration of the guard. ガードの構成。 */\nexport type ExternalGroupGuardOptions = {\n linkLookup: ExternalLinkLookup\n /** App key the administrative resource belongs to. 管理リソースが属するアプリキー。 */\n adminAppKey: string\n /** Resource key that grants administration. 管理を与えるリソースキー。 */\n adminResourceKey: string\n /**\n * Refusal message. 拒否メッセージ。\n *\n * @param roleKey Role that would have conferred administration. 管理権を与えることになるロール。\n * @param groupKey Group that would have held it. それを保持することになるグループ。\n */\n message: (roleKey: string, groupKey: string) => string\n}\n\n/** Default refusal message (Japanese, matching the admin UI's locale). 既定の拒否メッセージ。 */\nexport const defaultExternalGroupGuardMessage = (roleKey: string, groupKey: string): string =>\n `外部ディレクトリ連携グループ (${groupKey}) には管理ロール (${roleKey}) を持たせられません。上流側の所属変更がそのまま管理者の増減になるため、管理者経路はローカル管理のグループに限定しています。`\n\n/**\n * Collects the actions a set of roles holds on the administrative resource, as a union.\n * ロール集合が管理リソースに対して保持しているアクションを **合併して** 集める処理。\n *\n * ⚠️ **評価単位はロールではなくグループである。** 1 ロールずつ判定すると、`{read, create}` と\n * `{update, delete}` のように **単独ではどちらも管理ロールでない 2 つ** を同じグループへ割り当てる\n * だけで、合併では 4 アクション揃った状態を作れてしまう。実効権限は合併で決まるため、判定も\n * 合併で行わなければ、ロックアウトガード側の定義 (利用者ごとに全経路を合併してから 4 つ揃うかを\n * 見る) と食い違う。\n *\n * @param grants All grants in the tenant. テナント内の全付与。\n * @param roleIds Roles whose actions are unioned. 合併対象のロール。\n * @param adminAppKey Administrative resource's app key. 管理リソースのアプリキー。\n * @param adminResourceKey Administrative resource's key. 管理リソースのリソースキー。\n * @returns The union of actions on the administrative resource. 管理リソースに対するアクションの合併。\n */\nfunction adminActionsOf(grants: readonly AuthzGrant[], roleIds: ReadonlySet<number>, adminAppKey: string, adminResourceKey: string): Set<AuthzAction> {\n return new Set(grants.filter((g) => roleIds.has(g.roleId) && g.appKey === adminAppKey && g.resourceKey === adminResourceKey).map((g) => g.action))\n}\n\n/**\n * Lists the roles one group holds. あるグループが保持しているロールを列挙する処理。\n *\n * @param assignments All group-role assignments in the tenant. テナント内の全グループ割当。\n * @param groupId The group in question. 対象のグループ。\n * @returns The role ids that group holds. そのグループが保持するロール ID。\n */\nfunction rolesOfGroup(assignments: readonly { groupId: number; roleId: number }[], groupId: number): Set<number> {\n return new Set(assignments.filter((a) => a.groupId === groupId).map((a) => a.roleId))\n}\n\n/**\n * Reports whether an action set confers administration.\n * あるアクション集合が管理権を与えるかを判定する処理。\n *\n * 「管理者」の定義は認可コアの最後の管理者判定と同じ — 管理リソースの 4 原子アクションを\n * **すべて** 保持していること。定義が食い違うと、ここで通した状態をロックアウトガードが\n * 管理者として数える (あるいはその逆の) 不整合が生じる。\n *\n * ⚠️ 渡すアクション集合は **グループが保持する全ロールの合併** でなければならない。ロック\n * アウトガードは利用者ごとに全経路を合併してから 4 つ揃うかを見る。1 ロールずつ判定すると、\n * 補い合う 2 ロールの組が両方の検査を素通りする。\n */\nconst confersAdministration = (actions: ReadonlySet<AuthzAction>): boolean => AUTHZ_ACTIONS.every((a) => actions.has(a))\n\n/** Human-facing name of a role, falling back to its id. ロールの表示名 (無ければ ID)。 */\nconst roleLabel = (grants: readonly AuthzGrant[], roleId: number): string => grants.find((g) => g.roleId === roleId)?.roleKey ?? `#${roleId}`\n\n/**\n * Names the roles responsible for conferring administration on a group.\n * グループに管理権を与えている当のロールを名指しする処理。\n *\n * 単独で管理権を与えるロールが在ればそれを名指しする (最も分かりやすい)。無ければ、管理リソースへ\n * 何かを持ち寄っているロールを連ねる — 「合併すると管理権になる」ことを、無関係なロール 1 つを\n * 名指しして誤らせないためである。\n *\n * @param grants All grants in the tenant. テナント内の全付与。\n * @param roleIds Roles the group holds. グループが保持するロール。\n * @param adminAppKey Administrative resource's app key. 管理リソースのアプリキー。\n * @param adminResourceKey Administrative resource's key. 管理リソースのリソースキー。\n * @returns A label naming the culprit role, or the contributing roles joined. 原因のロール名 (複数なら連結)。\n */\nfunction culpritLabel(grants: readonly AuthzGrant[], roleIds: ReadonlySet<number>, adminAppKey: string, adminResourceKey: string): string {\n const alone = [...roleIds].find((id) => confersAdministration(adminActionsOf(grants, new Set([id]), adminAppKey, adminResourceKey)))\n if (alone !== undefined) return roleLabel(grants, alone)\n const contributing = [...roleIds].filter((id) => adminActionsOf(grants, new Set([id]), adminAppKey, adminResourceKey).size > 0)\n return contributing.map((id) => roleLabel(grants, id)).join(\" + \")\n}\n\n/**\n * Wraps an authorization admin store so administrative power cannot reach an external group.\n * 外部グループへ管理権が届かないよう認可管理ストアを包む処理。\n *\n * `assignGroupRole` と `createGrant` を差し替え、残りは透過的に委譲する。3 番目の方向\n * (連携の開始) は {@link assertGroupMayBeLinked} が担い、リンクを作る側が必ず通す。\n *\n * @param store Store to wrap. 包む対象のストア。\n * @param opts Link lookup and administrative resource identity. リンク参照と管理リソースの同定。\n * @returns A store with the same surface. 同一表面のストア。\n */\nexport function withExternalGroupGuards<R>(store: AuthzAdminStore<R>, opts: ExternalGroupGuardOptions): AuthzAdminStore<R> {\n return decorate(store as AuthzAdminStore, opts) as unknown as AuthzAdminStore<R>\n}\n\n/**\n * Applies the guards to one store, and to every store it hands out.\n * 1 つのストアと、そのストアが渡すすべてのストアへガードを適用する処理。\n *\n * ⚠️ **再帰であることが要点である。** `withTransaction` は「トランザクションに束ねたストア」を\n * 呼び出し側へ渡す。素通しにすると、包んだはずのストアから **無防備なストアを取り出せる** —\n * `guarded.withTransaction(tx => tx.assignGroupRole(外部グループ, 管理ロール))` の 1 行で、\n * デコレータの保証がまるごと消える。包む対象が「ストアを返すメソッド」を持つ以上、デコレータは\n * その返り値についても閉じていなければならない。\n *\n * @param target - Store to wrap. 包む対象のストア。\n * @param opts - Link lookup and administrative resource identity. リンク参照と管理リソースの同定。\n * @returns A guarded store whose `withTransaction` also yields guarded stores. 再帰的に保護されたストア。\n */\nfunction decorate(target: AuthzAdminStore, opts: ExternalGroupGuardOptions): AuthzAdminStore {\n const plain = target\n\n return {\n ...target,\n\n withTransaction: (fn, options) => plain.withTransaction((tx, runner) => fn(decorate(tx, opts), runner), options),\n\n async assignGroupRole(tenantId: string, groupId: number, roleId: number, actor: string): Promise<void> {\n // 証拠の読取と書込を 1 つの直列化トランザクションに閉じる。分けると、検査を通った\n // 直後・書込の前に前提が動く。しかも並行する 2 つの操作が **どちらも単独では通る**\n // 検査を同時に通過でき、その結果だけが不変条件を破る\n await plain.withTransaction(async (tx, runner) => {\n const grants = await tx.listGrants(tenantId)\n // 判定は「この割当の後にこのグループが持つロールの合併」で行う。追加するロール\n // 単独で見ると、補い合う 2 ロールを順に割り当てるだけで 4 アクション揃う\n const after = rolesOfGroup(await tx.listGroupRoles(tenantId), groupId)\n after.add(roleId)\n // ロールの有効・無効は見ない。無効な管理ロールを外部グループが保持できてしまうと、\n // 有効化するだけで上流が管理者を握る状態が完成する\n if (confersAdministration(adminActionsOf(grants, after, opts.adminAppKey, opts.adminResourceKey))) {\n const externalKey = await opts.linkLookup.externalGroupKey(tenantId, groupId, runner)\n if (externalKey !== null) throw new Error(opts.message(culpritLabel(grants, after, opts.adminAppKey, opts.adminResourceKey), externalKey))\n }\n await tx.assignGroupRole(tenantId, groupId, roleId, actor)\n })\n },\n\n async updateGrant(tenantId: string, id: number, input: Parameters<AuthzAdminStore[\"updateGrant\"]>[2], actor: string): Promise<void> {\n // アクションを差し替えないなら、合併は動かない (範囲の変更は権限の種類を増やさない)\n const nextAction = input.action\n if (nextAction === undefined) return plain.updateGrant(tenantId, id, input, actor)\n await plain.withTransaction(async (tx, runner) => {\n const grants = await tx.listGrants(tenantId)\n const target = grants.find((g) => g.id === id)\n if (target === undefined) return tx.updateGrant(tenantId, id, input, actor)\n const resources = await tx.listResources(tenantId)\n const resource = resources.find((r) => r.id === target.resourceId)\n // 管理リソース以外への付与は、このロールを管理ロールにしない\n if (resource?.appKey === opts.adminAppKey && resource?.resourceKey === opts.adminResourceKey) {\n const assignments = await tx.listGroupRoles(tenantId)\n for (const groupId of new Set(assignments.filter((a) => a.roleId === target.roleId).map((a) => a.groupId))) {\n const roleIds = rolesOfGroup(assignments, groupId)\n // 差し替え **後** の合併で判定する。差し替えた行の旧アクションは消えるが、\n // 同じアクションを別の行が持っていれば残る — 行ごとに引き算してはならない\n const projected = adminActionsOf(\n grants.filter((g) => g.id !== id),\n roleIds,\n opts.adminAppKey,\n opts.adminResourceKey,\n )\n if (roleIds.has(target.roleId)) projected.add(nextAction)\n if (!confersAdministration(projected)) continue\n const externalKey = await opts.linkLookup.externalGroupKey(tenantId, groupId, runner)\n if (externalKey !== null) throw new Error(opts.message(culpritLabel(grants, roleIds, opts.adminAppKey, opts.adminResourceKey) || roleLabel(grants, target.roleId), externalKey))\n }\n }\n await tx.updateGrant(tenantId, id, input, actor)\n })\n },\n\n async createGrant(tenantId: string, input: Parameters<AuthzAdminStore[\"createGrant\"]>[1], actor: string): Promise<void> {\n await plain.withTransaction(async (tx, runner) => {\n const resources = await tx.listResources(tenantId)\n const target = resources.find((r) => r.id === input.resourceId)\n // 管理リソース以外への付与は、このロールを管理ロールにしない\n if (target?.appKey === opts.adminAppKey && target?.resourceKey === opts.adminResourceKey) {\n const grants = await tx.listGrants(tenantId)\n const assignments = await tx.listGroupRoles(tenantId)\n // このロールを持つグループごとに、**そのグループの全ロールの合併** が管理権へ\n // 達するかを見る。ロール単独で見ると、補い合う 2 ロールを 1 つのグループへ\n // 集めた状態が検査を素通りする\n for (const groupId of new Set(assignments.filter((a) => a.roleId === input.roleId).map((a) => a.groupId))) {\n const projected = adminActionsOf(grants, rolesOfGroup(assignments, groupId), opts.adminAppKey, opts.adminResourceKey)\n projected.add(input.action)\n if (!confersAdministration(projected)) continue\n const externalKey = await opts.linkLookup.externalGroupKey(tenantId, groupId, runner)\n if (externalKey !== null) throw new Error(opts.message(culpritLabel(grants, rolesOfGroup(assignments, groupId), opts.adminAppKey, opts.adminResourceKey) || roleLabel(grants, input.roleId), externalKey))\n }\n }\n await tx.createGrant(tenantId, input, actor)\n })\n },\n }\n}\n\n/**\n * Refuses to link a group that already holds an administrative role.\n * 既に管理ロールを保持しているグループの外部連携を拒否する処理。\n *\n * 3 つ目の方向。これを通さずにリンクを作ると、ローカルで管理ロールを付けてから連携させる経路が\n * 残る。{@link withExternalGroupGuards} と違って呼び出し側が忘れ得るため、リンクを作る側の\n * 構成に必須ポートとして組み込むこと。\n *\n * @param store Authorization admin store used to read the grants. 付与を読む認可管理ストア。\n * @param tenantId Tenant the group belongs to. グループが属するテナント。\n * @param groupId Group about to be linked. 連携しようとしているグループ。\n * @param opts Administrative resource identity and refusal message. 管理リソースの同定と文言。\n * @returns Nothing; returns normally only when linking is allowed. 許可される場合のみ正常復帰。\n * @throws {Error} When the group holds the administrative role. 管理ロールを保持している場合。\n */\nexport async function assertGroupMayBeLinked(store: AuthzAdminStore, tenantId: string, groupId: number, opts: Pick<ExternalGroupGuardOptions, \"adminAppKey\" | \"adminResourceKey\" | \"message\">): Promise<void> {\n const assignments = (await store.listGroupRoles(tenantId)).filter((a) => a.groupId === groupId)\n if (assignments.length === 0) return\n const grants = await store.listGrants(tenantId)\n // 判定は保持ロールの合併で行う。1 ロールずつ見ると、補い合う 2 ロールを持つグループが通る\n const roleIds = new Set(assignments.map((a) => a.roleId))\n if (!confersAdministration(adminActionsOf(grants, roleIds, opts.adminAppKey, opts.adminResourceKey))) return\n const first = assignments[0]\n if (first) throw new Error(opts.message(culpritLabel(grants, roleIds, opts.adminAppKey, opts.adminResourceKey), first.groupKey))\n}\n","/**\n * Resolves which member-ledger rows belong to one identity.\n * 台帳のどの行が 1 人の identity のものかを解決するモジュール。\n *\n * 台帳 (`TDGroupMemberEmail`) は「上流に居る人」を、ローカルユーザーへ解決できているかに\n * 関わらず保持する。ここはその台帳から本人の行だけを取り出す唯一の場所であり、帰属証明\n * (`listLedgerGroupIds`) と初回ログイン時の昇格 (`promoteLedgerForUser`) は同じ答えを見る。\n * 突合規則が 2 箇所にあると、片方だけが「本人」と認めた瞬間に権限が食い違う。\n */\nimport type { AuthzTables } from \"@aiquants/authz-drizzle\"\nimport { and, eq } from \"drizzle-orm\"\nimport type { DirectoryDb } from \"./db\"\nimport type { AuthDirectoryTables } from \"./schema\"\n\n/** One ledger row attributed to an identity. identity に帰属した台帳 1 行。 */\nexport type LedgerIdentityRow = {\n ledgerId: number\n groupId: number\n /** Inherited from the parent group — the ledger has no tenant column. 親グループ由来。 */\n tenantId: string\n}\n\n/** The two tables this resolution spans. この解決が跨る 2 表。 */\nexport type LedgerIdentityTables = {\n /** `TDGroupMemberEmail`. */\n ledger: AuthDirectoryTables[\"TDGroupMemberEmail\"]\n /** `TMGroup` — owns the tenant and the active flag. テナントと有効フラグの所有者。 */\n group: AuthzTables[\"TMGroup\"]\n}\n\ntype Row = { ledgerId: number; groupId: number; tenantId: string; externalUserId: string | null }\n\n/**\n * Finds every ledger row that belongs to the identity, within one tenant.\n * 1 テナント内で、その identity に属する台帳行をすべて探す処理。\n *\n * 上流 ID の一致は無条件に本人。アドレスの一致は、その行が **別の** 上流 ID に紐付いていない\n * ときだけ本人である。単純な `OR` にすると後者の条件が消え、退職者のアドレスを再利用した\n * 新入社員が前任者の所属をそのまま継承する。\n *\n * ⚠️ この優先順位は **行ごと** に適用する。「ID で当たったらアドレスは見ない」という打ち切りは\n * 誤りで、同じ人物の行が ID 付きと ID 無しに混在すると ID 無しの行が丸ごと落ちる。\n *\n * @param db - Connected database surface. 接続済みの DB 面。\n * @param tables - The ledger and its parent group table. 台帳と親グループ表。\n * @param args - Tenant scope and the identity to match. テナント範囲と突合対象の identity。\n * @returns Matching rows, deduplicated and ordered by ledger id. 重複排除・台帳 ID 昇順の一致行。\n */\nexport async function findLedgerRowsForIdentity(db: DirectoryDb, tables: LedgerIdentityTables, args: { tenantId: string; externalUserId: string | null; email: string }): Promise<LedgerIdentityRow[]> {\n const { ledger, group } = tables\n const { tenantId, externalUserId, email } = args\n if (externalUserId === null && email === \"\") return []\n\n const project = { ledgerId: ledger.id, groupId: ledger.groupId, tenantId: group.tenantId, externalUserId: ledger.externalUserId }\n /** Runs one identity probe. 1 つの identity 条件で台帳を引く処理。 */\n const probe = async (condition: unknown): Promise<Row[]> => {\n const rows = await db\n .select<Row>(project)\n .from(ledger)\n // 無効化されたグループは証拠にしない。凍結した台帳で帰属証明が通り続けると、\n // 同期が止まった状態のまま在籍者が増え続ける\n .innerJoin(group, and(eq(group.id, ledger.groupId), eq(group.tenantId, tenantId), eq(group.isActive, true)))\n .where(condition)\n return rows.map((r) => ({ ledgerId: Number(r.ledgerId), groupId: Number(r.groupId), tenantId: String(r.tenantId), externalUserId: r.externalUserId === null ? null : String(r.externalUserId) }))\n }\n\n /** Deduplicates by ledger id and drops the identity column. 台帳 ID で重複排除する処理。 */\n const finalize = (rows: Row[]): LedgerIdentityRow[] => {\n const byId = new Map<number, LedgerIdentityRow>()\n for (const r of rows) byId.set(r.ledgerId, { ledgerId: r.ledgerId, groupId: r.groupId, tenantId: r.tenantId })\n return [...byId.values()].sort((a, b) => a.ledgerId - b.ledgerId)\n }\n\n // 上流 ID の一致は無条件に本人である。アドレスの一致は、その行が **別の** 上流 ID に\n // 紐付いていないときだけ本人である\n const byId = externalUserId === null ? [] : await probe(eq(ledger.externalUserId, externalUserId))\n if (email === \"\") return finalize(byId)\n const byEmail = await probe(eq(ledger.email, email))\n // ⚠️ 上流 ID を **持たない** 署名者にも同じ検査を効かせる。`externalUserId !== null` を条件に\n // 加えると、ID を出せない署名者に対してだけ検査が丸ごと消え、**別の上流 ID が名乗りを上げて\n // いる行** までアドレス一致で拾ってしまう。証拠が少ない側が多くの所属を得る形になる。\n // 行に付いた上流 ID は「この行はその人のものだ」という主張であり、それを否定できないなら\n // 使わない側へ倒す\n const claimedByAnother = (row: Row) => row.externalUserId !== null && row.externalUserId !== externalUserId\n // 優先順位は **行ごと** に適用する。「ID で 1 件でも当たったらアドレスは見ない」と実装すると、\n // 同じ人物の行が ID 付きと ID 無しに混在したとき (上流が ID を返さない経路・ID 採取前に\n // 書かれた行) に ID 無しの行が丸ごと落ち、identity を多く渡すほど所属が減る\n return finalize([...byId, ...byEmail.filter((r) => !claimedByAnother(r))])\n}\n","/**\n * @module link-store\n * @description Reads and writes the identity-side tables: external group links, the upstream member\n * ledger, tenant membership proof, group-based login allowlist, and the sync request queue.\n * identity 側テーブルの読み書き。外部グループリンク・上流メンバー台帳・テナント帰属証明・\n * グループ単位のログイン許可・同期要求キュー。\n *\n * **テナントの扱いは 2 種類ある。混同すると事故になる。**\n *\n * - **絞り込み条件**: リンク・帰属証明グループ。読み書きの双方で SQL 述語になる。\n * - **所有者**: グループ許可。管理 UI の編集スコープを決めるだけで、**ログイン判定では\n * 述語にしない** — 認証はテナントの確定より前に起きるため、判定の時点でテナントは存在しない。\n * このモジュールでは {@link createDirectoryLinkStore} の `isEmailAllowedByGroup` だけが\n * その例外であり、意図的に全テナントを対象とする。\n */\nimport { type AuthAllowlistEntry, type AuthDirectoryMembershipMode, type AuthDirectoryStatus, type AuthGroupExternalLink, normalizeAuthEmail } from \"@aiquants/auth-core\"\nimport { assertTenantMembershipPolicy, type TenantMembershipPolicy } from \"@aiquants/auth-directory-core\"\nimport { assertTenantId, assertTenantMatch } from \"@aiquants/authz-core\"\nimport type { AuthzTables } from \"@aiquants/authz-drizzle\"\nimport { and, asc, count, eq, isNull } from \"drizzle-orm\"\nimport { type DirectoryDb, type EnsureConnection, toInstant } from \"./db\"\nimport { findLedgerRowsForIdentity } from \"./ledger-identity\"\nimport { assertMembershipMode } from \"./membership-mode\"\nimport type { AuthDirectoryTables } from \"./schema\"\n\n/**\n * Proves a group may become externally sourced. グループを外部供給にしてよいことの証明。\n *\n * `assertGroupMayBeLinked` を呼ぶだけのポートである。自由関数のままだと呼び出し側が忘れられ、\n * 「ローカルで管理ロールを付けてから連携させる」経路が残る。必須の構成項目にすることで、\n * 忘れることを構造的に不可能にする。\n */\nexport type LinkableAssertion = {\n /**\n * Owns a transaction, proves the group may be linked on it, then runs `write` on the same runner.\n * トランザクションを所有し、その上で連携可能であることを証明してから、同じランナーで `write` を実行する処理。\n *\n * ⚠️ **証明だけを返す形にしてはならない。** 証明と連携の作成が別のトランザクションだと、\n * 証明を通った直後・リンクを作る前に、そのグループへ管理ロールを割り当てる操作が\n * 割り込める。割り当て側の検査は「まだリンクが無い」と見て通り、連携側の検査は\n * 「まだ管理ロールが無い」と見て通る — **どちらも単独では正しく、結果だけが不変条件を破る**。\n *\n * @param tenantId - Tenant that owns the group. グループを所有するテナント。\n * @param groupId - Group about to become externally sourced. 外部供給にしようとしているグループ。\n * @param write - The link creation, run on the proving transaction. 証明したトランザクション上で行う連携作成。\n * @returns Nothing. 戻り値なし。\n * @throws When the group holds an administrative role. 管理ロールを保持している場合。\n */\n assertLinkableAndWrite(tenantId: string, groupId: number, write: (runner: unknown) => Promise<void>): Promise<void>\n}\n\n/** Options for the link store. リンクストアの構成。 */\nexport type DirectoryLinkStoreOptions = {\n /** Required — see {@link LinkableAssertion}. 必須 (忘れられる経路を作らないため)。 */\n linkable: LinkableAssertion\n /** Establishes the connection before the first statement. 最初の文の前に接続を確立する処理。 */\n ensureConnection?: EnsureConnection\n /**\n * Seconds after which a successful sync counts as overdue. 同期が期限超過とみなされる秒数。\n *\n * 同期間隔の数倍を指定する。短すぎると常時警告が出て誰も読まなくなり、長すぎると停止に\n * 気付けない。\n */\n stalenessThresholdSec: number\n /** False when directory integration is not wired for this deployment. 未結線なら false。 */\n isEnabled: boolean\n /**\n * Upstream system this deployment synchronizes against, e.g. `\"google\"`. 同期先の上流システム。\n *\n * 新しい連携を作る面へそのまま渡る。UI に供給元を焼き込ませないための注入点であり、\n * 同期ランナーに設定する値と同一でなければならない — 食い違うと、作られたリンクは\n * 同期側で「別プロバイダ」として恒久的に拒否される。\n */\n provider: string\n /**\n * Milliseconds to reuse a loaded tenant-membership policy. 帰属方針を再利用するミリ秒。\n *\n * 既定は 0 (毎回引き直す)。帰属証明は **権限を 1 件も読む前に毎リクエスト走る** ため、\n * 証明グループを 1 つも宣言していない配備でも 3 表の結合が 1 往復増える。\n *\n * ⚠️ 既定を非 0 にしない。認可の判断材料を、頼まれてもいないのに黙って古くするライブラリは\n * 信用できない。有効化は配備側が「どれだけ古くてよいか」を決めて明示する。\n * 本ストア自身の書き込み (証明グループの増減・連携の作成解除) は自動で破棄するため、\n * 遅れが出るのは **記憶域を直接編集した場合だけ** である。\n */\n policyCacheTtlMs?: number\n}\n\n/** One external link row as the admin surface sees it. 管理面が見る外部リンク行。 */\nexport type DirectoryLinkRow = AuthGroupExternalLink & { linkId: number; groupId: number; tenantId: string }\n\n/** Input for creating a link. リンク作成の入力。 */\nexport type LinkGroupInput = {\n provider: string\n externalId: string\n externalKey: string\n membershipMode: AuthDirectoryMembershipMode\n}\n\n/**\n * What `requestSync` did. `requestSync` が行ったこと。\n *\n * ⚠️ `void` にしてはならない。同じ範囲の要求が既に待っているとき、積み増さないのは正しいが、\n * それを成功と区別せずに返すと、押しても反応しない画面ができる — しかも詰まった 1 行が以後の\n * 押下すべてを飲み込む。呼び出し側が運用者へ言えるだけの情報を返す。\n */\nexport type SyncRequestOutcome = \"queued\" | \"already-pending\"\n\n/**\n * Whether a thrown value is a unique-key violation. 投げられた値が一意制約違反かどうか。\n *\n * SQL Server は 2601 (一意索引) と 2627 (一意制約) を使い分ける。ドライバの型に依存しないよう、\n * 数値の `number` プロパティだけで判定する。\n *\n * @param error - The thrown value. 投げられた値。\n * @returns True when it is a unique-key violation. 一意制約違反なら true。\n */\nfunction isUniqueViolation(error: unknown): boolean {\n const code = typeof error === \"object\" && error !== null && \"number\" in error ? (error as { number: unknown }).number : undefined\n return code === 2601 || code === 2627\n}\n\n/**\n * Creates the identity-side data store.\n * identity 側のデータストアを生成する処理。\n *\n * @param dbUnknown Drizzle database instance. drizzle のデータベースインスタンス。\n * @param tables Identity-side tables. identity 側テーブル群。\n * @param authzTables Authorization tables (for the group master and the real membership).\n * 認可テーブル群 (グループマスタと実所属表のため)。\n * @param opts Store options. ストアの構成。\n * @returns The store. ストア。\n */\nexport function createDirectoryLinkStore(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: DirectoryLinkStoreOptions) {\n const db = dbUnknown as DirectoryDb\n const ready = opts.ensureConnection ?? (async () => {})\n const policyCacheTtlMs = opts.policyCacheTtlMs ?? 0\n if (!Number.isFinite(policyCacheTtlMs) || policyCacheTtlMs < 0) {\n throw new TypeError(`[@aiquants/auth-directory-drizzle] policyCacheTtlMs must be a non-negative number, received ${String(opts.policyCacheTtlMs)}`)\n }\n /** Per-tenant memo of the membership policy. テナント単位の帰属方針メモ。 */\n const policyCache = new Map<string, { policy: TenantMembershipPolicy; loadedAt: number }>()\n /** Drops memos this store's own writes could have invalidated. 自身の書込が無効化し得るメモを捨てる処理。 */\n const forgetPolicy = (tenantId: string) => policyCache.delete(tenantId)\n const { TDGroupExternalLink: link, TDGroupMemberEmail: ledger, TDTenantMembershipGroup: membership, TDAllowlistGroup: allowGroup, TDDirectorySyncRequest: queue } = tables\n const { TMGroup: group, TDGroupMember: groupMember } = authzTables\n\n /**\n * Refuses to make a break-glass proof group externally sourced.\n * break-glass の証明グループを外部供給にすることを拒否する処理。\n *\n * 不変条件を読み取り側 (`loadTenantMembershipPolicy`) だけで守ると、1 回の連携操作が\n * **全リクエストの帰属証明を落とす** 全面障害に化ける。遷移が起きるここで断る。\n *\n * @param runner - The transaction issuing the write. 書き込みを発行するトランザクション。\n * @param tenantId - Owning tenant. 所有テナント。\n * @param groupId - Group about to be linked. 連携しようとしているグループ。\n * @returns Nothing. 戻り値なし。\n * @throws {Error} When the group is a break-glass proof group. break-glass だった場合。\n */\n async function assertNotBreakGlass(runner: DirectoryDb, tenantId: string, groupId: number): Promise<void> {\n const rows = await runner\n .select<{ groupId: number }>({ groupId: membership.groupId })\n .from(membership)\n .where(and(eq(membership.tenantId, tenantId), eq(membership.groupId, groupId), eq(membership.isBreakGlass, true)))\n if (rows.length > 0) {\n throw new Error(`Group ${groupId} is a break-glass proof group; a break-glass proof path must be one the synchronization can never touch.`)\n }\n }\n\n /**\n * Proves the group belongs to the tenant before any row that references it is written.\n * そのグループを参照する行を書く前に、テナントへの帰属を証明する処理。\n *\n * リンク表・台帳・許可表はいずれも `TMGroup` を参照するが、テナントの正準値は親側にある。\n * ID を素通しで受けると、他テナントのグループへ紐付けを作れてしまう。\n *\n * @throws {Error} When the group does not exist in this tenant. 自テナントに存在しない場合。\n */\n async function assertGroupInTenant(runner: DirectoryDb, tenantId: string, groupId: number): Promise<void> {\n const [owned] = await runner\n .select<{ id: number }>({ id: group.id })\n .from(group)\n .where(and(eq(group.id, groupId), eq(group.tenantId, tenantId)))\n if (!owned) throw new Error(`Group not found in tenant: ${groupId}`)\n }\n\n /** Maps a raw link row plus its unresolved count onto the public shape. 公開形への変換。 */\n function toLinkRow(row: Record<string, unknown>, unresolvedCount: number): DirectoryLinkRow {\n return {\n linkId: Number(row.linkId),\n groupId: Number(row.groupId),\n tenantId: String(row.tenantId),\n provider: String(row.provider),\n externalId: String(row.externalId),\n externalKey: String(row.externalKey),\n membershipMode: assertMembershipMode(String(row.membershipMode)),\n isSyncEnabled: Boolean(row.isSyncEnabled),\n ...(row.syncedAt ? { syncedAt: toInstant(row.syncedAt as string).toISOString() } : {}),\n ...(row.syncError ? { syncError: String(row.syncError) } : {}),\n unresolvedCount,\n }\n }\n\n /** Counts ledger rows that resolved to no local user, per group. グループ別の未解決件数。 */\n async function unresolvedCounts(runner: DirectoryDb, tenantId: string): Promise<Map<number, number>> {\n const rows = await runner\n .select<{ groupId: number; n: number }>({ groupId: ledger.groupId, n: count() })\n .from(ledger)\n .innerJoin(group, and(eq(group.id, ledger.groupId), eq(group.tenantId, tenantId)))\n .where(isNull(ledger.resolvedUserId))\n .groupBy(ledger.groupId)\n return new Map(rows.map((r) => [Number(r.groupId), Number(r.n)]))\n }\n\n return {\n /**\n * Lists every external link of the tenant. 当該テナントの外部リンクを列挙する処理。\n *\n * @param tenantId Tenant to narrow on. 絞り込むテナント。\n */\n async listLinks(tenantId: string): Promise<DirectoryLinkRow[]> {\n assertTenantId(tenantId, \"listLinks(tenantId)\")\n await ready()\n const rows = await db\n .select<Record<string, unknown>>({\n linkId: link.id,\n groupId: link.groupId,\n tenantId: link.tenantId,\n provider: link.provider,\n externalId: link.externalId,\n externalKey: link.externalKey,\n membershipMode: link.membershipMode,\n isSyncEnabled: link.isSyncEnabled,\n syncedAt: link.syncedAt,\n syncError: link.syncError,\n })\n .from(link)\n .where(eq(link.tenantId, tenantId))\n .orderBy(asc(link.groupId))\n const counts = await unresolvedCounts(db, tenantId)\n // テナントは行から射影した実値で再検証する。SQL の比較は照合順序に従うため、\n // 述語だけでは厳密一致の契約を満たさない\n return rows.map((r) => {\n assertTenantMatch(tenantId, String(r.tenantId), \"listLinks(row.tenantId)\")\n return toLinkRow(r, counts.get(Number(r.groupId)) ?? 0)\n })\n },\n\n /**\n * Returns the upstream key of the group, or `null` when it is locally managed.\n * そのグループの上流キーを返す処理。ローカル管理なら `null`。\n *\n * `withExternalGroupGuards` の判定材料であり、同時に拒否メッセージの表示名でもある。\n * 判定と名前を 1 回の照会で返すことで、拒否のたびの往復を 1 本に保つ。\n */\n async externalGroupKey(tenantId: string, groupId: number, runner?: unknown): Promise<string | null> {\n assertTenantId(tenantId, \"externalGroupKey(tenantId)\")\n await ready()\n // ⚠️ ランナーを受け取れることが要点である。呼び出し側が「この読み取りの結果を前提に\n // 書き込む」とき、読み取りだけ別接続で行うと、その前提は書込が着地するまでに崩れ得る。\n // 同じトランザクションで引けば、リンク行に範囲ロックが掛かり前提が保たれる\n const [row] = await ((runner as DirectoryDb | undefined) ?? db)\n .select<{ externalKey: string }>({ externalKey: link.externalKey })\n .from(link)\n .where(and(eq(link.groupId, groupId), eq(link.tenantId, tenantId)))\n return row ? String(row.externalKey) : null\n },\n\n /**\n * Links a local group to an upstream group. ローカルグループを上流グループへ結び付ける処理。\n *\n * 既に管理ロールを持つグループは {@link LinkableAssertion} が拒否する。\n *\n * @throws {Error} When the group belongs to another tenant. 他テナントのグループの場合。\n */\n async linkGroup(tenantId: string, groupId: number, input: LinkGroupInput, actor: string): Promise<void> {\n assertTenantId(tenantId, \"linkGroup(tenantId)\")\n const membershipMode = assertMembershipMode(input.membershipMode)\n const externalId = input.externalId.trim()\n const externalKey = normalizeAuthEmail(input.externalKey)\n if (externalId === \"\" || externalKey === \"\") throw new TypeError(\"[@aiquants/auth-directory-drizzle] externalId and externalKey must be non-empty\")\n await ready()\n // 管理ロールを保持したまま連携させると、上流側の所属変更がそのまま管理者の増減になる。\n // 証明とリンク作成は **同じトランザクション** で行う。分けると、証明を通った直後に\n // そのグループへ管理ロールを割り当てる操作が割り込め、双方の検査が個別に通ったまま\n // 「外部供給グループが管理ロールを持つ」状態が成立する\n await opts.linkable.assertLinkableAndWrite(tenantId, groupId, async (runner) => {\n const tx = runner as DirectoryDb\n await assertGroupInTenant(tx, tenantId, groupId)\n await assertNotBreakGlass(tx, tenantId, groupId)\n await tx.insert(link).output().values({ groupId, tenantId, provider: input.provider, externalId, externalKey, membershipMode, isSyncEnabled: true, vanishedObservations: 0, createdAt: new Date(), createdBy: actor })\n })\n // 方針の材料が変わった。自身の書込に限っては、遅れを一切許さない\n forgetPolicy(tenantId)\n },\n\n /**\n * Removes the link, leaving the projected membership in place.\n * リンクを解除する処理。射影済みの所属はそのまま残す。\n *\n * 所属まで消すと、連携を切っただけで全員がグループから外れる (= 大量剥奪) 。解除は\n * 「上流の言うことを聞くのをやめる」であって「今の所属を捨てる」ではない。台帳と、\n * 台帳を通してしか効かないグループ許可は、残しても意味を持たないため削除する。\n */\n async unlinkGroup(tenantId: string, groupId: number): Promise<void> {\n assertTenantId(tenantId, \"unlinkGroup(tenantId)\")\n await ready()\n await db.transaction(async (tx) => {\n await assertGroupInTenant(tx, tenantId, groupId)\n await tx.delete(ledger).where(eq(ledger.groupId, groupId))\n // グループ許可は台帳を通してしか効かない。台帳を消して許可行だけ残すと、\n // 昨日まで効いていた設定が無言の no-op になる\n await tx.delete(allowGroup).where(and(eq(allowGroup.groupId, groupId), eq(allowGroup.tenantId, tenantId)))\n await tx.delete(link).where(and(eq(link.groupId, groupId), eq(link.tenantId, tenantId)))\n })\n // 連携の有無は break-glass の成否を左右する (上流所有の break-glass は拒否される)\n forgetPolicy(tenantId)\n },\n\n /** Pauses or resumes synchronization for one group. 1 グループの同期を停止・再開する処理。 */\n async setSyncEnabled(tenantId: string, groupId: number, isSyncEnabled: boolean, actor: string): Promise<void> {\n assertTenantId(tenantId, \"setSyncEnabled(tenantId)\")\n await ready()\n await db\n .update(link)\n .set({ isSyncEnabled, updatedAt: new Date(), updatedBy: actor })\n .output()\n .where(and(eq(link.groupId, groupId), eq(link.tenantId, tenantId)))\n },\n\n /**\n * Queues a synchronization request. 同期要求をキューへ積む処理。\n *\n * web プロセスは上流へ到達できないため、要求を行として残し同期ジョブに拾わせる。\n * 誰がいつ何を要求し、結果がどうだったかがそのまま監査記録になる。\n *\n * @param groupId `null` requests every group of the tenant. `null` はテナント全件。\n */\n async requestSync(tenantId: string, groupId: number | null, requestedBy: string): Promise<SyncRequestOutcome> {\n assertTenantId(tenantId, \"requestSync(tenantId)\")\n await ready()\n return db.transaction<SyncRequestOutcome>(async (tx) => {\n if (groupId !== null) await assertGroupInTenant(tx, tenantId, groupId)\n // ⚠️ 同じ範囲の未処理要求が既に在るなら、積み増さない。ボタンは連打でき、押した数だけ\n // 上流への全件走査が並ぶ。`update` 権限しか持たない主体でも、要求を並べるだけで\n // 上流の割当を使い切れてしまう — そうなると **所属の同期まで巻き添えで止まる**。\n //\n // ⚠️ ただし抑止には期限を置く。期限なしにすると、担当ジョブが居ないテナントで\n // 1 行が詰まった瞬間から、以後の「今すぐ同期」が永久に黙って捨てられる — 押しても\n // 何も起きない画面になり、運用者には理由が見えない\n const pending = await tx\n .select<{ id: number }>({ id: queue.id })\n .from(queue)\n .where(and(eq(queue.tenantId, tenantId), groupId === null ? isNull(queue.groupId) : eq(queue.groupId, groupId), isNull(queue.pickedAt)))\n // ⚠️ **黙って捨てない。** 積み増さないこと自体は正しい (押した数だけ上流への全件\n // 走査が並び、`update` 権限だけの主体が割当を使い切れる) が、`{ ok: true }` を\n // 返して何も起きないと、押しても反応しない画面になり、しかも詰まった 1 行が\n // 以後の押下すべてを飲み込む。何が起きたかを返し、呼び出し側に言わせる\n if (pending.length > 0) return \"already-pending\"\n // 上の読み取りは **速い経路** にすぎない。同時に届いた要求はどれも「未処理は無い」と\n // 読むので、抑止の本体は `UQ_*_pending` (未処理行に対する一意索引) である\n try {\n await tx.insert(queue).output().values({ tenantId, groupId, requestedBy, requestedAt: new Date(), createdAt: new Date(), createdBy: requestedBy })\n } catch (error) {\n if (!isUniqueViolation(error)) throw error\n return \"already-pending\"\n }\n return \"queued\"\n })\n },\n\n /**\n * Summarizes synchronization health for the admin header.\n * 管理ヘッダー向けに同期の健全性を要約する処理。\n *\n * 一度も成功していない場合も陳腐化として報告する。「不明」を健全側へ倒さない —\n * 判定できないことは、健全であることの証明ではない。\n */\n async getStatus(tenantId: string): Promise<AuthDirectoryStatus> {\n assertTenantId(tenantId, \"getStatus(tenantId)\")\n await ready()\n const rows = await db\n .select<{ groupId: number; isSyncEnabled: boolean; syncedAt: string | null; syncError: string | null }>({\n groupId: link.groupId,\n isSyncEnabled: link.isSyncEnabled,\n syncedAt: link.syncedAt,\n syncError: link.syncError,\n })\n .from(link)\n .where(eq(link.tenantId, tenantId))\n const [pending] = await db\n .select<{ n: number }>({ n: count() })\n .from(queue)\n .where(and(eq(queue.tenantId, tenantId), isNull(queue.finishedAt)))\n\n const enabled = rows.filter((r) => Boolean(r.isSyncEnabled))\n const stamps = enabled.map((r) => (r.syncedAt ? toInstant(r.syncedAt).getTime() : null))\n const oldest = stamps.length > 0 && stamps.every((t): t is number => t !== null) ? Math.min(...stamps) : null\n // リンクがあるのに 1 本も有効でない状態を「健全」と報告してはならない。台帳は凍結した\n // まま権限を配り続けるのに、画面上は N 件連携済み・エラー 0・陳腐化なしに見える\n const allPaused = rows.length > 0 && enabled.length === 0\n const isStale = allPaused || (enabled.length > 0 && (oldest === null || Date.now() - oldest > opts.stalenessThresholdSec * 1000))\n return {\n isEnabled: opts.isEnabled,\n provider: opts.provider,\n linkedGroupCount: rows.length,\n ...(oldest !== null ? { oldestSyncedAt: new Date(oldest).toISOString() } : {}),\n isStale,\n pausedGroupCount: rows.length - enabled.length,\n stalenessThresholdSec: opts.stalenessThresholdSec,\n erroredGroupIds: rows\n .filter((r) => Boolean(r.syncError))\n .map((r) => Number(r.groupId))\n .sort((a, b) => a - b),\n pendingRequestCount: Number(pending?.n ?? 0),\n }\n },\n\n /**\n * Builds the tenant's membership policy, proving every break-glass group is local.\n * テナントの帰属方針を組み立てる処理。break-glass がローカル管理であることも証明する。\n *\n * 「証明を要求するか」を行数から推測しない。宣言が 1 行も無ければ `no-proof-required`\n * という **方針** を返し、宣言があれば `require-proof` を返す。区別を型の上に残すことで、\n * 「宣言し忘れ」や「障害で 0 件が返った」が「証明不要」に化ける経路を塞ぐ。\n *\n * @throws {Error} When a break-glass group is externally sourced. 上流所有だった場合。\n * @throws {TypeError} When the resulting policy is unsatisfiable. 成立しない方針の場合。\n */\n async loadTenantMembershipPolicy(tenantId: string): Promise<TenantMembershipPolicy> {\n assertTenantId(tenantId, \"loadTenantMembershipPolicy(tenantId)\")\n const memo = policyCacheTtlMs > 0 ? policyCache.get(tenantId) : undefined\n if (memo && Date.now() - memo.loadedAt < policyCacheTtlMs) return memo.policy\n await ready()\n const rows = await db\n .select<{ groupId: number; isBreakGlass: boolean; externalLinkId: number | null }>({ groupId: membership.groupId, isBreakGlass: membership.isBreakGlass, externalLinkId: link.id })\n .from(membership)\n .innerJoin(group, and(eq(group.id, membership.groupId), eq(group.tenantId, tenantId)))\n // ⚠️ 結合にもテナントを載せる。書き込み側は既に束ねており、読み側だけが片手落ちだった。\n // 乖離した行が 1 つ結合されると、break-glass が「上流所有」と誤判定されて例外になり、\n // その例外はリクエストごとの帰属証明の中で起きる — 全リクエストが落ち、直すための\n // 管理面も開かない\n .leftJoin(link, and(eq(link.groupId, membership.groupId), eq(link.tenantId, tenantId)))\n .where(eq(membership.tenantId, tenantId))\n if (rows.length === 0) {\n const empty: TenantMembershipPolicy = { kind: \"no-proof-required\" }\n if (policyCacheTtlMs > 0) policyCache.set(tenantId, { policy: empty, loadedAt: Date.now() })\n return empty\n }\n\n const proofGroupIds = rows.map((r) => Number(r.groupId))\n const breakGlass = rows.filter((r) => Boolean(r.isBreakGlass))\n for (const r of breakGlass) {\n // 上流所有のグループを break-glass にしても復帰経路にならない。上流の事故が\n // すべての証明経路を同時に壊すため、そこにこそ触れない経路が要る\n if (r.externalLinkId !== null) throw new Error(`Break-glass group ${Number(r.groupId)} is externally sourced; a break-glass proof path must be one the synchronization can never touch.`)\n }\n const policy: TenantMembershipPolicy = { kind: \"require-proof\", proofGroupIds, breakGlassGroupIds: breakGlass.map((r) => Number(r.groupId)) }\n assertTenantMembershipPolicy(policy, `tenant ${tenantId}`)\n // 成立した方針だけを覚える。例外を覚えると、記憶域を直した後も TTL のあいだ\n // 落ち続ける (直したのに直らない、という最も追いにくい状態になる)\n if (policyCacheTtlMs > 0) policyCache.set(tenantId, { policy, loadedAt: Date.now() })\n return policy\n },\n\n /**\n * Declares a group as proof of tenant membership. グループをテナント帰属の証明に指定する処理。\n *\n * この表に 1 行でも入った瞬間、そのテナントの帰属証明は「証明必須」へ切り替わる。行が\n * 無い状態は「証明不要」であり、書き込み手段が無ければ機能そのものが到達不能になる。\n *\n * `isBreakGlass` は「同期が決して触れない復帰経路」を意味する。上流の事故はすべての\n * 連携グループを同時に壊すため、そこにこそ外部供給でない経路が要る。\n *\n * @param tenantId - Owning tenant. 所有テナント。\n * @param groupId - Group to declare. 指定するグループ。\n * @param isBreakGlass - True for the locally-managed recovery path. 復帰経路なら true。\n * @param actor - Who performed it. 実行者。\n * @returns Nothing. 戻り値なし。\n * @throws {Error} When the group is not in the tenant, when a break-glass group is linked,\n * or when the result would be a proof with no break-glass path.\n * 自テナントに無い場合、break-glass に連携済みグループを指定した場合、または結果が\n * 「証明はあるが復帰経路が無い」方針になる場合。\n */\n async setTenantMembershipGroup(tenantId: string, groupId: number, isBreakGlass: boolean, actor: string): Promise<void> {\n assertTenantId(tenantId, \"setTenantMembershipGroup(tenantId)\")\n await ready()\n await db.transaction(async (tx) => {\n await assertGroupInTenant(tx, tenantId, groupId)\n if (isBreakGlass) {\n // 上流所有のグループを break-glass にしても復帰経路にならない。読み取り側でも\n // 拒否しているが、そこで初めて落ちると 1 行の投入が全リクエストを止める\n const [linked] = await tx\n .select<{ id: number }>({ id: link.id })\n .from(link)\n .where(and(eq(link.tenantId, tenantId), eq(link.groupId, groupId)))\n if (linked) throw new Error(`Group ${groupId} is externally sourced; a break-glass proof path must be one the synchronization can never touch.`)\n }\n const existing = await tx\n .select<{ groupId: number }>({ groupId: membership.groupId })\n .from(membership)\n .where(and(eq(membership.tenantId, tenantId), eq(membership.groupId, groupId)))\n // ⚠️ 書いた結果が **成立しない方針** にならないことを、同じトランザクションで\n // 確かめる。「証明は要るが break-glass が 1 つも無い」状態は読み取り側\n // (`assertTenantMembershipPolicy`) が拒否するので、以後この配備の **全リクエスト**\n // が落ちる — 直すための管理面も開けない。削除側にはこの検査が在り、設定側には\n // 無かった: 最初の 1 行を break-glass 以外で入れる/最後の break-glass を降ろす、\n // のどちらも同じ袋小路を作る\n if (!isBreakGlass) {\n const rows = await tx.select<{ groupId: number; isBreakGlass: boolean }>({ groupId: membership.groupId, isBreakGlass: membership.isBreakGlass }).from(membership).where(eq(membership.tenantId, tenantId))\n const others = rows.filter((r) => Number(r.groupId) !== groupId)\n if (!others.some((r) => Boolean(r.isBreakGlass))) {\n throw new Error(`Declaring group ${groupId} as ordinary proof would leave tenant ${tenantId} with a membership proof but no break-glass path; declare a locally-managed break-glass group first.`)\n }\n }\n if (existing.length > 0) {\n await tx\n .update(membership)\n .set({ isBreakGlass, updatedAt: new Date(), updatedBy: actor })\n .output()\n .where(and(eq(membership.tenantId, tenantId), eq(membership.groupId, groupId)))\n return\n }\n await tx.insert(membership).output().values({ tenantId, groupId, isBreakGlass, createdAt: new Date(), createdBy: actor })\n })\n // 方針の材料が変わった。自身の書込に限っては、遅れを一切許さない\n forgetPolicy(tenantId)\n },\n\n /**\n * Withdraws a group from the tenant membership proof.\n * グループをテナント帰属の証明から外す処理。\n *\n * 最後の break-glass 行を外すと、以降の証明は上流だけに依存する。上流の事故で全員が\n * 締め出される状態になるため、読み取り側の検証 (`assertTenantMembershipPolicy`) が\n * 成立しない方針を拒否する — ここで先に断る。\n *\n * @param tenantId - Owning tenant. 所有テナント。\n * @param groupId - Group to withdraw. 外すグループ。\n * @returns Nothing. 戻り値なし。\n * @throws {Error} When it is the last break-glass group of a non-empty policy.\n * 証明が残るのに最後の break-glass を外そうとした場合。\n */\n async removeTenantMembershipGroup(tenantId: string, groupId: number): Promise<void> {\n assertTenantId(tenantId, \"removeTenantMembershipGroup(tenantId)\")\n await ready()\n await db.transaction(async (tx) => {\n const rows = await tx.select<{ groupId: number; isBreakGlass: boolean }>({ groupId: membership.groupId, isBreakGlass: membership.isBreakGlass }).from(membership).where(eq(membership.tenantId, tenantId))\n const remaining = rows.filter((r) => Number(r.groupId) !== groupId)\n // 全部外すのは構わない (証明不要へ戻るだけ)。危険なのは、証明を残したまま\n // 復帰経路だけを失うことである\n if (remaining.length > 0 && !remaining.some((r) => Boolean(r.isBreakGlass))) {\n throw new Error(`Removing group ${groupId} would leave tenant ${tenantId} with a membership proof but no break-glass path.`)\n }\n await tx.delete(membership).where(and(eq(membership.tenantId, tenantId), eq(membership.groupId, groupId)))\n })\n forgetPolicy(tenantId)\n },\n\n /**\n * Reactivates a group that was deactivated after vanishing upstream.\n * 上流消滅で無効化されたグループを再有効化する処理。\n *\n * 同期は成功時に自動で再有効化するが、運用者が先に直したい場合の入口も要る。無効化を\n * 一方通行の扉にしないための、もう 1 本の出口である。\n */\n async setGroupActive(tenantId: string, groupId: number, isActive: boolean, actor: string): Promise<void> {\n assertTenantId(tenantId, \"setGroupActive(tenantId)\")\n await ready()\n await db\n .update(group)\n .set({ isActive, updatedAt: new Date(), updatedBy: actor })\n .output()\n .where(and(eq(group.id, groupId), eq(group.tenantId, tenantId)))\n },\n\n /**\n * Reports the groups the identity matches in the upstream ledger.\n * 上流台帳で identity が一致するグループを報告する処理。\n *\n * ⚠️ 実所属表ではなく **台帳** を見ること。初回ログインでユーザー行が生まれた直後、\n * まだ射影されていない瞬間に、正規の利用者が「非メンバー」として全拒否されるのを防ぐ。\n */\n async listLedgerGroupIds(tenantId: string, identity: { externalUserId: string | null; email: string | null | undefined }): Promise<number[]> {\n assertTenantId(tenantId, \"listLedgerGroupIds(tenantId)\")\n // 配備スイッチが off のとき、台帳は更新されない。止まった台帳を根拠に判断を続けると、\n // スイッチは「運用者の制御を減らすだけで、上流由来の権限は減らさない」ものになる。\n // 既に書かれた所属行 (`TDGroupMember`) はローカルの資産として残る\n if (!opts.isEnabled) return []\n await ready()\n const rows = await findLedgerRowsForIdentity(db, { ledger, group }, { tenantId, externalUserId: identity.externalUserId?.trim() || null, email: normalizeAuthEmail(identity.email) })\n return [...new Set(rows.map((r) => r.groupId))].sort((a, b) => a - b)\n },\n\n /**\n * Reports whether an address may sign in through a group allowlist entry.\n * グループ許可によってそのアドレスがサインインできるかを判定する処理。\n *\n * ⚠️ **テナント述語を持たない。意図的である。** 認証はテナントの確定より前に起きるため、\n * 判定の時点でテナントは存在しない。この配備が受け持つ全テナントの和集合で判定する。\n * ここに述語を足すと初回ログインが全滅する。\n *\n * 判定は実所属ではなく **台帳** を見る。初回ログインの時点では、そもそもユーザー行が\n * まだ存在しない。\n */\n async isEmailAllowedByGroup(email: string | null | undefined): Promise<boolean> {\n // 同上。停止中の台帳でサインインを許し続けると、切ったつもりの経路が生き残る\n if (!opts.isEnabled) return false\n const normalized = normalizeAuthEmail(email)\n if (normalized === \"\") return false\n await ready()\n const [row] = await db\n .select<{ id: number }>({ id: ledger.id })\n .from(ledger)\n .innerJoin(allowGroup, eq(allowGroup.groupId, ledger.groupId))\n // 無効化されたグループでは許可しない。is_active が同期にだけ効いてアクセスに\n // 効かないと、凍結した台帳でサインインが通り続ける (向きが逆になる)。\n // ⚠️ テナント述語は入れない。認証はテナントの確定より前に起きるためであり、\n // is_active はそれとは別の軸である\n .innerJoin(group, and(eq(group.id, ledger.groupId), eq(group.isActive, true)))\n .where(eq(ledger.email, normalized))\n return Boolean(row)\n },\n\n /**\n * Lists group-based allowlist entries of the tenant. テナントのグループ許可を列挙する処理。\n *\n * `auth-core` の判別共用体をそのまま返す。別形で返すと、`type` と `readOnly` を消費側が\n * 付け直すことになり、「未設定 = 編集可」という暗黙の既定が呼び出し地点ごとに復活する。\n */\n async listAllowlistGroups(tenantId: string): Promise<AuthAllowlistEntry[]> {\n assertTenantId(tenantId, \"listAllowlistGroups(tenantId)\")\n await ready()\n const rows = await db\n .select<{ id: number; groupId: number; groupKey: string; groupName: string; description: string | null }>({\n id: allowGroup.id,\n groupId: allowGroup.groupId,\n groupKey: group.groupKey,\n groupName: group.name,\n description: allowGroup.description,\n })\n .from(allowGroup)\n .innerJoin(group, and(eq(group.id, allowGroup.groupId), eq(group.tenantId, tenantId)))\n .where(eq(allowGroup.tenantId, tenantId))\n .orderBy(asc(group.name))\n return rows.map((r) => ({\n id: Number(r.id),\n type: \"group\" as const,\n groupId: Number(r.groupId),\n groupKey: String(r.groupKey),\n groupName: String(r.groupName),\n ...(r.description ? { description: String(r.description) } : {}),\n owner: { kind: \"tenant\", tenantId },\n // テナントの管理者が足した行なので編集できる。環境変数由来の行と違い不変ではない\n readOnly: false,\n }))\n },\n\n /** Adds a group-based allowlist entry. グループ許可を追加する処理。 */\n async addAllowlistGroup(tenantId: string, groupId: number, description: string | undefined, actor: string): Promise<void> {\n assertTenantId(tenantId, \"addAllowlistGroup(tenantId)\")\n await ready()\n await db.transaction(async (tx) => {\n await assertGroupInTenant(tx, tenantId, groupId)\n // 許可は台帳を通してしか効かない。台帳を持たないローカル管理グループを足しても\n // 誰にも何も与えず、しかも書込側も読取側も何も報告しない無言の no-op になる\n const [linked] = await tx\n .select<{ id: number }>({ id: link.id })\n .from(link)\n .where(and(eq(link.groupId, groupId), eq(link.tenantId, tenantId)))\n if (!linked) throw new Error(`Group ${groupId} is locally managed; only externally-sourced groups can gate sign-in, because the decision reads the upstream member ledger.`)\n await tx\n .insert(allowGroup)\n .output()\n .values({ tenantId, groupId, description: description ?? null, createdAt: new Date(), createdBy: actor })\n })\n },\n\n /** Removes a group-based allowlist entry. グループ許可を削除する処理。 */\n async removeAllowlistGroup(tenantId: string, id: number): Promise<void> {\n assertTenantId(tenantId, \"removeAllowlistGroup(tenantId)\")\n await ready()\n await db.delete(allowGroup).where(and(eq(allowGroup.id, id), eq(allowGroup.tenantId, tenantId)))\n },\n\n /**\n * Reports the number of real membership rows the tenant's linked groups hold.\n * テナントの連携グループが持つ実所属行の件数を報告する処理。\n *\n * 連携を切る前に「何人分の所属が残るのか」を運用者へ示すために使う。\n */\n async countProjectedMembers(tenantId: string, groupId: number): Promise<number> {\n assertTenantId(tenantId, \"countProjectedMembers(tenantId)\")\n await ready()\n const [row] = await db\n .select<{ n: number }>({ n: count() })\n .from(groupMember)\n .innerJoin(group, and(eq(group.id, groupMember.groupId), eq(group.tenantId, tenantId)))\n .where(eq(groupMember.groupId, groupId))\n return Number(row?.n ?? 0)\n },\n }\n}\n","/**\n * @module membership-mode\n * @description The single reader of the `membership_mode` column.\n * `membership_mode` 列を読む唯一の入口。\n *\n * 以前は読み手が 2 つあり、片方は未知の値を `TypeError` で拒否し、もう片方は黙って `\"direct\"` へ\n * 倒していた。同じ列に相反する 2 つの方針が同居し、しかも倒すほうが削除を決める側だった。\n * 入れ子展開のつもりのリンクが直接所属だけを返せば、入れ子側の全員が削除候補になる。\n */\nimport type { AuthDirectoryMembershipMode } from \"@aiquants/auth-core\"\n\n/** The two modes, in one place. 2 つのモードの唯一の定義位置。 */\nexport const MEMBERSHIP_MODES: readonly AuthDirectoryMembershipMode[] = [\"direct\", \"transitive\"]\n\n/**\n * Rejects a membership mode that is not one of the two known values.\n * 既知の 2 値でない所属展開モードを拒否する処理。\n *\n * 既定へ倒さないのは、入れ子展開が黙って有効になると権限が静かに広がり、逆に黙って無効になると\n * 入れ子側の全員が削除候補になるためである。どちらの向きにも倒さない。\n *\n * @param value Raw column value. 列の生値。\n * @returns The validated mode. 検証済みのモード。\n * @throws {TypeError} When the value is unknown. 未知の値の場合。\n */\nexport function assertMembershipMode(value: string): AuthDirectoryMembershipMode {\n if (!MEMBERSHIP_MODES.includes(value as AuthDirectoryMembershipMode)) {\n throw new TypeError(`[@aiquants/auth-directory-drizzle] membershipMode must be one of ${MEMBERSHIP_MODES.join(\" | \")}, received ${JSON.stringify(value)}`)\n }\n return value as AuthDirectoryMembershipMode\n}\n","/**\n * @module promote\n * @description Turns ledger rows into real memberships for a user that has just been provisioned.\n * プロビジョニング直後のユーザーについて、台帳の行を実所属へ昇格させる処理。\n *\n * **上流へは一切到達しない。** 昇格に必要なのは「このアドレス (または上流 ID) は、どのグループの\n * 台帳に載っているか」だけであり、それはすべてローカルにある。おかげで web プロセスは\n * ディレクトリの資格情報を持たないまま、初回ログインのその場でグループ権限を有効化できる。\n *\n * 上流の資格情報を持つのは同期ジョブだけ、という配置を壊さないために、本モジュールに\n * `DirectoryProvider` を持ち込んではならない。\n */\nimport { normalizeAuthEmail } from \"@aiquants/auth-core\"\nimport { assertTenantId, assertTenantMatch } from \"@aiquants/authz-core\"\n\n/** Storage operations promotion performs. 昇格が行う記憶域操作。 */\nexport type PromotionRepository = {\n /**\n * Finds ledger rows matching the identity, across every linked group of the tenant.\n * テナントの全連携グループを対象に、identity と一致する台帳行を探す処理。\n *\n * 突合順は上流 ID が第一キー、アドレスが第二キー。\n *\n * ⚠️ 各行が属するテナントも返すこと。`TDGroupMemberEmail` はテナント列を持たず親から継承する\n * ため、台帳だけで絞った実装は静かにテナントを跨ぐ。呼び出し側はこの値で再検証する。\n */\n findLedgerMatches(args: { tenantId: string; externalUserId: string | null; email: string }): Promise<Array<{ ledgerId: number; groupId: number; tenantId: string }>>\n\n /**\n * Records the resolution and inserts the missing membership rows, atomically.\n * 解決結果の記録と不足している所属行の挿入を、原子的に行う処理。\n *\n * @returns Group ids the user was actually added to. 実際に追加されたグループ ID。\n */\n attachUserToGroups(args: { tenantId: string; userId: number; ledgerIds: number[]; groupIds: number[] }): Promise<number[]>\n}\n\n/** What the promotion did. 昇格が行ったこと。 */\nexport type PromotionResult = {\n /** Groups the user gained membership of. 所属を得たグループ。 */\n addedGroupIds: number[]\n /** Ledger rows the user was matched against. 突合した台帳行。 */\n matchedLedgerIds: number[]\n}\n\n/**\n * Promotes ledger rows to real memberships for one just-provisioned user.\n * プロビジョニング直後の 1 ユーザーについて、台帳の行を実所属へ昇格させる処理。\n *\n * 追加しか行わないため、最後の管理者ロックアウト判定は不要である (所属が増えて管理者が\n * 減ることはない)。それでも記憶域側では 1 トランザクションで書くこと — 台帳の解決記録だけが\n * 残って所属が入らないと、次回の同期まで「解決済みなのに権限が無い」状態が続く。\n *\n * @param repository Storage operations. 記憶域操作。\n * @param args Tenant, the resolved user, and the identity to match on. テナント・ユーザー・identity。\n * @returns What was promoted. 昇格した内容。\n * @throws {AuthzTenantError} When the tenant id is blank, or a matched row belongs to another\n * tenant. テナント ID が空、または一致行が別テナントに属する場合。\n * @throws {TypeError} When the user id is not a positive integer. ユーザー ID が正整数でない場合。\n */\nexport async function promoteLedgerForUser(repository: PromotionRepository, args: { tenantId: string; userId: number; externalUserId: string | null; email: string | null | undefined }): Promise<PromotionResult> {\n const tenantId = assertTenantId(args.tenantId, \"promoteLedgerForUser(tenantId)\")\n if (!Number.isInteger(args.userId) || args.userId <= 0) {\n throw new TypeError(`[@aiquants/auth-directory-drizzle] userId must be a positive integer, received ${String(args.userId)}`)\n }\n\n const email = normalizeAuthEmail(args.email)\n const externalUserId = args.externalUserId?.trim() || null\n // 突合材料がどちらも無ければ、探すこと自体が全件走査になる。何もしないのが正しい\n if (externalUserId === null && email === \"\") return { addedGroupIds: [], matchedLedgerIds: [] }\n\n const matches = await repository.findLedgerMatches({ tenantId, externalUserId, email })\n if (matches.length === 0) return { addedGroupIds: [], matchedLedgerIds: [] }\n // 境界をポートの docstring に委ねない。所属の追加はロールへ繋がるため、1 行の取り違えが\n // そのままテナントを跨いだ権限付与になる\n for (const match of matches) assertTenantMatch(tenantId, match.tenantId, \"promoteLedgerForUser(findLedgerMatches row)\")\n\n const ledgerIds = [...new Set(matches.map((m) => m.ledgerId))].sort((a, b) => a - b)\n const groupIds = [...new Set(matches.map((m) => m.groupId))].sort((a, b) => a - b)\n const addedGroupIds = await repository.attachUserToGroups({ tenantId, userId: args.userId, ledgerIds, groupIds })\n return { addedGroupIds: [...addedGroupIds].sort((a, b) => a - b), matchedLedgerIds: ledgerIds }\n}\n","/**\n * Drizzle-backed repository for first-sign-in membership promotion.\n * 初回ログイン時の所属昇格を担う drizzle 実装のリポジトリ。\n *\n * 上流へは一切到達しない。台帳は同期ジョブが既に書き終えているので、web プロセスは\n * ディレクトリの資格情報を持たないまま、サインアップしたその場でグループ権限を有効化できる。\n * 資格情報を持つのは同期ジョブだけ、という配置はこの分離に依存している。\n */\nimport { assertTenantId } from \"@aiquants/authz-core\"\nimport type { AuthzTables } from \"@aiquants/authz-drizzle\"\nimport { and, eq, inArray } from \"drizzle-orm\"\nimport type { DirectoryDb, EnsureConnection } from \"./db\"\nimport { findLedgerRowsForIdentity } from \"./ledger-identity\"\nimport type { PromotionRepository } from \"./promote\"\nimport type { AuthDirectoryTables } from \"./schema\"\nimport { chunk, chunkByParameters, MAX_PARAMETERS } from \"./sql-limits\"\n\n/** Options for {@link createPromotionRepository}. 昇格リポジトリの生成オプション。 */\nexport type PromotionRepositoryOptions = {\n /** Establishes the connection before the first statement. 最初の文の前に接続を確立する処理。 */\n ensureConnection?: EnsureConnection\n /** Written to the audit columns of inserted membership rows. 挿入行の監査列に書く実行者。 */\n actor: string\n /** Injected clock — tests must not depend on wall time. 注入時計 (実時刻に依存させない)。 */\n now?: () => Date\n}\n\n/** Columns the membership INSERT binds per row. 所属 INSERT が 1 行あたり束ねる列数。 */\nconst MEMBERSHIP_INSERT_PARAMETERS = 4\n/** Parameters the marking statement binds outside its two `IN` lists. 印付け文の `IN` 以外の枠。 */\nconst RESERVED_FOR_MARKING = 100\n\n/**\n * Builds the promotion repository over the directory ledger and the authz membership table.\n * ディレクトリ台帳と認可の所属表に対して、昇格リポジトリを構築する処理。\n *\n * @param dbUnknown - Drizzle database instance. drizzle のデータベースインスタンス。\n * @param tables - Directory tables (owns the ledger). ディレクトリ表 (台帳の所有者)。\n * @param authzTables - Authorization tables (owns groups and membership). 認可表 (グループと所属の所有者)。\n * @param opts - Connection, actor and clock. 接続・実行者・時計。\n * @returns A repository consumable by `promoteLedgerForUser`. `promoteLedgerForUser` が使うリポジトリ。\n */\nexport function createPromotionRepository(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: PromotionRepositoryOptions): PromotionRepository {\n const db = dbUnknown as DirectoryDb\n const ready = opts.ensureConnection ?? (async () => {})\n const now = opts.now ?? (() => new Date())\n const ledger = tables.TDGroupMemberEmail\n const { TMGroup: group, TDGroupMember: groupMember } = authzTables\n\n return {\n async findLedgerMatches(args) {\n assertTenantId(args.tenantId, \"findLedgerMatches(tenantId)\")\n await ready()\n return findLedgerRowsForIdentity(db, { ledger, group }, args)\n },\n\n async attachUserToGroups({ tenantId, userId, ledgerIds, groupIds }) {\n assertTenantId(tenantId, \"attachUserToGroups(tenantId)\")\n await ready()\n if (ledgerIds.length === 0) return []\n\n return db.transaction(async (tx) => {\n // 自テナントのグループであることを、書き込みと同じトランザクションで証明する。\n // 呼び出し側で確認済みでも、その間に連携が解除された行へ所属を書けてしまう\n const owned = new Set<number>()\n for (const part of chunk(groupIds)) {\n const rows = await tx\n .select<{ id: number }>({ id: group.id })\n .from(group)\n .where(and(inArray(group.id, part), eq(group.tenantId, tenantId), eq(group.isActive, true)))\n for (const r of rows) owned.add(Number(r.id))\n }\n const targets = groupIds.filter((id) => owned.has(id))\n if (targets.length === 0) return []\n\n const existing = new Set<number>()\n for (const part of chunk(targets)) {\n const rows = await tx\n .select<{ groupId: number }>({ groupId: groupMember.groupId })\n .from(groupMember)\n .where(and(eq(groupMember.userId, userId), inArray(groupMember.groupId, part)))\n for (const r of rows) existing.add(Number(r.groupId))\n }\n const missing = targets.filter((id) => !existing.has(id))\n\n // 台帳 → 所属 の順で書く。同期側 (sync-repository) も同じ順で X ロックを取るため、\n // 逆順にすると初回ログインと同期が正面衝突してデッドロック (1205) になり、\n // 敗者が同期ならそのグループの反映が丸ごと巻き戻る。\n // 印を先に付けても取り残しは起きない — 同一トランザクションなので、挿入が\n // 落ちれば印も一緒に消える\n //\n // この 1 文は `IN` を 2 つ描画する。台帳 ID 側だけを分割すると、グループ ID 側は\n // 毎回そのままの長さで乗るため、所属グループの多い人物で上限を越える\n const ledgerChunkSize = Math.max(1, MAX_PARAMETERS - RESERVED_FOR_MARKING - targets.length)\n for (const part of chunk(ledgerIds, ledgerChunkSize)) {\n await tx\n .update(ledger)\n .set({ resolvedUserId: userId })\n .output()\n .where(and(inArray(ledger.id, part), inArray(ledger.groupId, targets)))\n }\n if (missing.length > 0) {\n const createdAt = now()\n // 1 行 4 列。行数で割ると列の多い表で静かに上限を越える\n for (const part of chunkByParameters(missing, MEMBERSHIP_INSERT_PARAMETERS)) {\n await tx\n .insert(groupMember)\n .output()\n .values(part.map((groupId) => ({ groupId, userId, createdAt, createdBy: opts.actor })))\n }\n }\n\n return missing\n })\n },\n }\n}\n","/**\n * @module schema\n * @description Drizzle (mssql) table definitions for the identity side of access control —\n * external group provenance, the upstream member ledger, tenant membership proof, the login\n * allowlist, and the synchronization queue.\n * アクセス制御の identity 面 (外部グループの出所・上流メンバー台帳・テナント帰属証明・\n * ログイン許可リスト・同期要求キュー) の drizzle (mssql) テーブル定義。\n *\n * **なぜ認可スキーマ (`@aiquants/authz-drizzle`) と別パッケージなのか**\n *\n * これらの表を読むのは認証・ユーザー管理の経路だけで、認可のクエリは 1 本も触らない。責務が\n * 違うことに加えて、実務上の理由がある: `@aiquants/authz-drizzle` は複数の配備が共有する公開\n * パッケージであり、そこへ列や表を足すと **全配備に DDL 移行を強制する** (drizzle は INSERT の\n * 列リストにスキーマ定義の全列を並べるため、実表に無い列を宣言した瞬間に SQL Server 207 で\n * 必ず失敗する)。外部ディレクトリ連携を使わない配備にまで移行を強いないため、別パッケージの\n * 別表として足す形にしてある。\n *\n * **テナント列の方針**\n *\n * 認可スキーマの原則 (リンク表は参照先からテナントを継承し、二重管理を作らない) を踏襲する。\n * 例外は 2 つあり、いずれも理由を各表の docstring に記した。物理 `DEFAULT` は **付けない** —\n * 既定値は「既存行のある表に NOT NULL 列を後付けする」ためのものであり、新規表では\n * 「テナントを渡し忘れた書き込みが黙って既定テナントへ入る」経路を作るだけである。\n */\nimport { AUTHZ_TENANT_COLUMN } from \"@aiquants/authz-drizzle\"\nimport { type AnyMsSqlColumn, bigint, bit, datetime2, foreignKey, int, mssqlSchema, nvarchar, primaryKey, uniqueIndex } from \"drizzle-orm/mssql-core\"\n\n/** Minimal shape of a table this schema points a foreign key at. 外部キーの参照先に要る最小形。 */\nexport type ReferencedTable = { id: AnyMsSqlColumn }\n\n/**\n * Physical name of the provenance discriminator carried by an external link row.\n * 外部リンク行が持つ供給元識別子の物理名。\n */\nexport const AUTH_DIRECTORY_PROVIDER_COLUMN = \"provider\"\n\n/**\n * Table names that carry {@link AUTHZ_TENANT_COLUMN}.\n * テナント列を持つ表の名前一覧。\n *\n * `TDGroupMemberEmail` が含まれないのは、親グループからテナントを継承するためである。\n */\nexport const AUTH_DIRECTORY_TENANT_SCOPED_TABLES = [\"TDGroupExternalLink\", \"TDTenantMembershipGroup\", \"TDAllowlistGroup\", \"TDDirectorySyncRequest\", \"TDDirectoryCatalog\", \"TMDirectoryCatalogObservation\", \"TMAllowlist\"] as const\n\n/**\n * Builds the identity-side tables under an arbitrary schema name.\n * 任意のスキーマ名の下に identity 側テーブル群を構築する処理。\n *\n * @param schemaName Schema the tables live in. テーブルを配置するスキーマ名。\n * @param opts Injected tables the foreign keys point at. 外部キーの参照先として注入する表。\n * @returns Every identity-side table, keyed by table name. 表名をキーにしたテーブル群。\n */\nexport function defineAuthDirectorySchema<S extends string>(schemaName: S, opts: { groupTable: ReferencedTable; userTable: ReferencedTable }) {\n const s = mssqlSchema(schemaName)\n\n /** Audit columns for a table that is inserted, updated and deleted. 更新経路を持つ表の監査列。 */\n const audit = () => ({\n createdAt: datetime2(\"created_at\"),\n createdBy: nvarchar(\"created_by\", { length: 50 }),\n updatedAt: datetime2(\"updated_at\"),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }),\n })\n\n /**\n * Audit columns for an insert-and-delete-only table (no `updated_*` pair).\n * 追記・削除のみで更新されない表用の監査列。\n *\n * drizzle は INSERT の列リストに **スキーマ上の全列** を並べるため、実表に存在しない列を\n * 宣言すると `Invalid column name` (SQL Server 207) で必ず失敗する。宣言は実表に合わせる。\n */\n const insertOnlyAudit = () => ({\n createdAt: datetime2(\"created_at\"),\n createdBy: nvarchar(\"created_by\", { length: 50 }),\n })\n\n /**\n * Provenance of a group whose membership an upstream directory owns.\n * 所属の所有者が上流ディレクトリにあるグループの出所。\n *\n * ⚠️ **`tenant_id` はリンク表としては非正規化である。** 参照先の `TMGroup` が既にテナントを\n * 持つため原則からは外れるが、`UNIQUE (tenant_id, provider, external_id)` =「同一の上流\n * グループはテナントごとに 1 回だけリンクできる」を SQL Server の制約として表現するには\n * 列が要る (表を跨いだ一意性は宣言できない)。乖離を防ぐため、書込時に親の `tenant_id` と\n * 一致することを必ず証明すること。\n */\n const TDGroupExternalLink = s.table(\n \"TDGroupExternalLink\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n groupId: bigint(\"group_id\", { mode: \"number\" }).notNull(),\n tenantId: nvarchar(AUTHZ_TENANT_COLUMN, { length: 64 }).notNull(),\n provider: nvarchar(AUTH_DIRECTORY_PROVIDER_COLUMN, { length: 32 }).notNull(),\n // 上流の安定 ID。アドレスは改名され得るため正準にしない\n externalId: nvarchar(\"external_id\", { length: 256 }).notNull(),\n externalKey: nvarchar(\"external_key\", { length: 320 }).notNull(),\n membershipMode: nvarchar(\"membership_mode\", { length: 16 }).notNull(),\n isSyncEnabled: bit(\"is_sync_enabled\").notNull(),\n syncedAt: datetime2(\"synced_at\"),\n syncError: nvarchar(\"sync_error\", { length: 1000 }),\n // 上流でグループが見つからない状態が何回連続したか。1 度の 404 で無効化すると、\n // 伝播待ちや権限変更の一瞬で生きているグループが落ちる\n vanishedObservations: int(\"vanished_observations\").notNull(),\n ...audit(),\n },\n (t) => [\n primaryKey({ name: `${schemaName}_TDGroupExternalLink_pk`, columns: [t.id] }),\n // 1 グループ 1 リンク。2 本張れると、どちらの上流が所属を所有するのか決まらない\n uniqueIndex(`${schemaName}_TDGroupExternalLink_group_uq`).on(t.groupId),\n uniqueIndex(`${schemaName}_TDGroupExternalLink_external_uq`).on(t.tenantId, t.provider, t.externalId),\n foreignKey({ name: `${schemaName}_TDGroupExternalLink_group_id_fk`, columns: [t.groupId], foreignColumns: [opts.groupTable.id] }),\n ],\n )\n\n /**\n * Every member observed upstream, including people with no local user row.\n * 上流で観測された全メンバー。ローカルユーザー行を持たない人物も含む。\n *\n * 未ログインの人物のユーザー行は意図的に作らないため、実所属表には入れられない。それでも\n * ログイン許可の判定とテナント帰属証明は **初回ログインの時点で** 成立させる必要があるので、\n * アドレスを台帳として保持する。\n *\n * `resolved_user_id` に外部キーを張らないのは、解決できない行こそがこの表の存在理由だから\n * である。整合はプロビジョニング直後の昇格処理が担保する。\n */\n const TDGroupMemberEmail = s.table(\n \"TDGroupMemberEmail\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n groupId: bigint(\"group_id\", { mode: \"number\" }).notNull(),\n // 照合形 (auth-core の normalizeAuthEmail を通した値) で保存する\n email: nvarchar(\"email\", { length: 320 }).notNull(),\n externalUserId: nvarchar(\"external_user_id\", { length: 128 }),\n resolvedUserId: bigint(\"resolved_user_id\", { mode: \"number\" }),\n observedAt: datetime2(\"observed_at\").notNull(),\n ...audit(),\n },\n (t) => [\n primaryKey({ name: `${schemaName}_TDGroupMemberEmail_pk`, columns: [t.id] }),\n uniqueIndex(`${schemaName}_TDGroupMemberEmail_group_email_uq`).on(t.groupId, t.email),\n foreignKey({ name: `${schemaName}_TDGroupMemberEmail_group_id_fk`, columns: [t.groupId], foreignColumns: [opts.groupTable.id] }),\n ],\n )\n\n /**\n * Groups a tenant accepts as proof that a caller belongs to it.\n * あるテナントが「この呼び出し元は自分に属する」と認める証明用グループ。\n *\n * `is_break_glass` の行は **ローカル管理のグループでなければならない**。同期は上流の言う\n * ことに従って所属を消すため、証明経路のすべてが上流由来だと、上流側の事故がそのまま\n * 「誰もテナントに入れない」状態を生む。修復する権限を持つ人も締め出されるため、同期が\n * 決して触れない経路を最低 1 本残す。\n */\n const TDTenantMembershipGroup = s.table(\n \"TDTenantMembershipGroup\",\n {\n tenantId: nvarchar(AUTHZ_TENANT_COLUMN, { length: 64 }).notNull(),\n groupId: bigint(\"group_id\", { mode: \"number\" }).notNull(),\n isBreakGlass: bit(\"is_break_glass\").notNull(),\n ...insertOnlyAudit(),\n },\n (t) => [primaryKey({ name: `${schemaName}_TDTenantMembershipGroup_pk`, columns: [t.tenantId, t.groupId] }), foreignKey({ name: `${schemaName}_TDTenantMembershipGroup_group_id_fk`, columns: [t.groupId], foreignColumns: [opts.groupTable.id] })],\n )\n\n /**\n * Groups whose members may sign in. メンバーのサインインを許可するグループ。\n *\n * ⚠️ `tenant_id` は **所有者** であって絞り込み条件ではない。認証はテナントの確定より前に\n * 起きるため、許可判定の時点でテナントは存在しない。判定はこの配備が受け持つ全テナントの\n * 和集合で行うこと。読み取りにテナント述語を足すと初回ログインが全滅する。\n */\n const TDAllowlistGroup = s.table(\n \"TDAllowlistGroup\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n tenantId: nvarchar(AUTHZ_TENANT_COLUMN, { length: 64 }).notNull(),\n groupId: bigint(\"group_id\", { mode: \"number\" }).notNull(),\n description: nvarchar(\"description\", { length: 512 }),\n ...insertOnlyAudit(),\n },\n (t) => [\n primaryKey({ name: `${schemaName}_TDAllowlistGroup_pk`, columns: [t.id] }),\n uniqueIndex(`${schemaName}_TDAllowlistGroup_tenant_group_uq`).on(t.tenantId, t.groupId),\n foreignKey({ name: `${schemaName}_TDAllowlistGroup_group_id_fk`, columns: [t.groupId], foreignColumns: [opts.groupTable.id] }),\n ],\n )\n\n /**\n * Queue of synchronization requests raised from the admin UI.\n * 管理 UI から起票された同期要求のキュー。\n *\n * 上流へ到達できるのは同期ジョブだけで、web プロセスは資格情報を持たない。要求を行として\n * 積む形にすることで、サービス間の到達性も認証も不要になり、**誰がいつ何を要求し、結果が\n * どうだったか** がそのまま監査記録として残る。\n *\n * `group_id` が `NULL` はテナント全件を意味する。テナントを跨げないので `tenant_id` は\n * 自前で持つ (継承元が無い)。\n */\n const TDDirectorySyncRequest = s.table(\n \"TDDirectorySyncRequest\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n tenantId: nvarchar(AUTHZ_TENANT_COLUMN, { length: 64 }).notNull(),\n groupId: bigint(\"group_id\", { mode: \"number\" }),\n requestedBy: nvarchar(\"requested_by\", { length: 320 }).notNull(),\n requestedAt: datetime2(\"requested_at\").notNull(),\n pickedAt: datetime2(\"picked_at\"),\n finishedAt: datetime2(\"finished_at\"),\n result: nvarchar(\"result\", { length: 32 }),\n error: nvarchar(\"error\", { length: 2000 }),\n ...insertOnlyAudit(),\n },\n // ⚠️ 未処理行に対する一意索引は drizzle では宣言しない (フィルタ付き一意索引を表現できない)。\n // 実体は `apps/quants-mssql` の `UQ_authz_TDDirectorySyncRequest_pending` にあり、\n // アプリ側の重複抑止はその **速い経路** にすぎない — 同時に届いた要求はどれも\n // 「未処理は無い」と読むため、制約が無いと抑止そのものが効かない\n (t) => [primaryKey({ name: `${schemaName}_TDDirectorySyncRequest_pk`, columns: [t.id] })],\n )\n\n /**\n * Login allowlist patterns (addresses and bare hosts).\n * ログイン許可リストのパターン (アドレスと裸ホスト)。\n *\n * `@aiquants/authz-drizzle` から移設した表である。認可のクエリは 1 本もこの表に触らず、\n * 読むのは認証とユーザー管理の経路だけであるため、責務としては identity 側に属する。\n * 移設に合わせて `tenant_id` を足したが、意味は {@link TDAllowlistGroup} と同じく\n * **所有者** であり、ログイン判定の絞り込み条件ではない。\n */\n const TMAllowlist = s.table(\n \"TMAllowlist\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n tenantId: nvarchar(AUTHZ_TENANT_COLUMN, { length: 64 }).notNull(),\n pattern: nvarchar(\"pattern\", { length: 256 }).notNull(),\n type: nvarchar(\"type\", { length: 32 }).notNull(),\n description: nvarchar(\"description\", { length: 512 }),\n ...insertOnlyAudit(),\n },\n (t) => [primaryKey({ name: `${schemaName}_TMAllowlist_pk`, columns: [t.id] }), uniqueIndex(`${schemaName}_TMAllowlist_pattern_uq`).on(t.tenantId, t.pattern)],\n )\n\n /**\n * The upstream's group list, as the sync job last observed it.\n * 同期ジョブが最後に観測した、上流のグループ一覧。\n *\n * ⚠️ **この表が在る理由は、web プロセスに上流の資格情報を持たせないことである。** ドメイン全体\n * 委任の鍵は組織のディレクトリを丸ごと読めるため、外部からの要求を終端するプロセスには置かない。\n * 上流へ届くのは同期ジョブだけとし、web は「ジョブが書いた行」を読む。管理面の選択欄は\n * 利便のための読み取りであり、その利便のために境界を崩す価値は無い。\n *\n * 内容は上流の現在の答えの **写し** である。差分更新ではなく毎回まるごと置き換える\n * (足りない行を残すと、上流で消えたグループを選ばせ続けることになる)。\n */\n const TDDirectoryCatalog = s.table(\n \"TDDirectoryCatalog\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n tenantId: nvarchar(AUTHZ_TENANT_COLUMN, { length: 64 }).notNull(),\n provider: nvarchar(AUTH_DIRECTORY_PROVIDER_COLUMN, { length: 32 }).notNull(),\n // リンク表と同じく、安定 ID が正準である。アドレスは改名され得る\n externalId: nvarchar(\"external_id\", { length: 256 }).notNull(),\n externalKey: nvarchar(\"external_key\", { length: 320 }).notNull(),\n name: nvarchar(\"name\", { length: 256 }).notNull(),\n description: nvarchar(\"description\", { length: 1000 }),\n memberCount: int(\"member_count\"),\n // 観測時刻。1 回の置き換えでは全行が同じ値になる (`TMDirectoryCatalogObservation` と一致)。\n // 行ごとに持つのは、1 件の由来を単独で追えるようにするためである\n observedAt: datetime2(\"observed_at\").notNull(),\n ...insertOnlyAudit(),\n },\n (t) => [primaryKey({ name: `${schemaName}_TDDirectoryCatalog_pk`, columns: [t.id] }), uniqueIndex(`${schemaName}_TDDirectoryCatalog_external_uq`).on(t.tenantId, t.provider, t.externalId)],\n )\n\n /**\n * One row per upstream observation target, recording *that* it was observed.\n * 観測対象ごとに 1 行。**観測が起きたこと自体** を記録する。\n *\n * ⚠️ **候補一覧の行数から観測の有無は導けない。** 上流を正しく読めて答えが 0 件だった配備\n * (借用先の権限が狭い、対象 OU が空) と、同期が一度も走っていない配備は、`TDDirectoryCatalog`\n * の行数ではどちらも 0 である。前者を「同期が動いていない」と診断すると、運用は健全な部品を\n * 調べに行く。観測の事実は行数から独立した場所に持つ。\n */\n const TMDirectoryCatalogObservation = s.table(\n \"TMDirectoryCatalogObservation\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n tenantId: nvarchar(AUTHZ_TENANT_COLUMN, { length: 64 }).notNull(),\n provider: nvarchar(AUTH_DIRECTORY_PROVIDER_COLUMN, { length: 32 }).notNull(),\n observedAt: datetime2(\"observed_at\").notNull(),\n /** 観測時の件数。0 は「読めて空だった」ことの記録であり、未観測ではない。 */\n entryCount: int(\"entry_count\").notNull(),\n ...audit(),\n },\n (t) => [primaryKey({ name: `${schemaName}_TMDirectoryCatalogObservation_pk`, columns: [t.id] }), uniqueIndex(`${schemaName}_TMDirectoryCatalogObservation_target_uq`).on(t.tenantId, t.provider)],\n )\n\n return { TDGroupExternalLink, TDGroupMemberEmail, TDTenantMembershipGroup, TDAllowlistGroup, TDDirectorySyncRequest, TDDirectoryCatalog, TMDirectoryCatalogObservation, TMAllowlist }\n}\n\n/** Explicit alias for the tables `defineAuthDirectorySchema` returns (avoids TS2742). 明示型。 */\nexport type AuthDirectoryTables = {\n TDGroupExternalLink: ReturnType<typeof defineAuthDirectorySchema>[\"TDGroupExternalLink\"]\n TDGroupMemberEmail: ReturnType<typeof defineAuthDirectorySchema>[\"TDGroupMemberEmail\"]\n TDTenantMembershipGroup: ReturnType<typeof defineAuthDirectorySchema>[\"TDTenantMembershipGroup\"]\n TDAllowlistGroup: ReturnType<typeof defineAuthDirectorySchema>[\"TDAllowlistGroup\"]\n TDDirectorySyncRequest: ReturnType<typeof defineAuthDirectorySchema>[\"TDDirectorySyncRequest\"]\n TDDirectoryCatalog: ReturnType<typeof defineAuthDirectorySchema>[\"TDDirectoryCatalog\"]\n TMDirectoryCatalogObservation: ReturnType<typeof defineAuthDirectorySchema>[\"TMDirectoryCatalogObservation\"]\n TMAllowlist: ReturnType<typeof defineAuthDirectorySchema>[\"TMAllowlist\"]\n}\n","/**\n * @module sync\n * @description Orchestration of one synchronization run: read upstream, reconcile, apply — and\n * stop at the first sign that \"upstream said nothing\" might mean \"we could not read upstream\".\n * 1 回の同期実行のオーケストレーション。上流を読み、突合し、適用する。そして「上流が何も\n * 言わなかった」が「上流を読めなかった」かもしれない兆候が出た時点で止める。\n *\n * 記憶域は {@link DirectorySyncRepository} 越しにしか触らないため、手順そのものは\n * データベースを起動せずに全分岐を検証できる。\n */\nimport { assertBlastRadiusPolicy, type BlastRadiusPolicy, type DirectoryGroup, type DirectoryProvider, reconcile } from \"@aiquants/auth-directory-core\"\nimport { classifySyncFailure, defaultSyncMessages, type SyncMessages } from \"./sync-messages\"\nimport { type ApplyReport, type DirectorySyncRepository, NEWLY_UNHEALTHY_SYNC_OUTCOMES, type SyncLink, type SyncLoggerPort, type SyncOutcome, type SyncScope, UNHEALTHY_SYNC_OUTCOMES } from \"./sync-ports\"\n\n/** Configuration of a synchronization runner. 同期ランナーの構成。 */\nexport type DirectorySyncConfig = {\n /**\n * Which upstream this runner speaks for. このランナーが担当する上流。\n *\n * リンク行の `provider` と照合する。照合しないと、別 provider のリンクを受け取った\n * ランナーがそれを「上流に存在しない」と判定し、他 provider のグループを無効化してしまう。\n */\n provider: string\n directory: DirectoryProvider\n repository: DirectorySyncRepository\n /** Per-axis removal limits. See `auth-directory-core`. 軸ごとの削除上限。 */\n policy: BlastRadiusPolicy\n /**\n * Consecutive absences required before a group is deactivated. 無効化に要する連続不在回数。\n *\n * 1 度の 404 で無効化すると、伝播待ちや権限変更の一瞬で生きているグループが落ちる。\n */\n vanishedThreshold: number\n /**\n * Supplies the current time. 現在時刻を供給する処理。\n *\n * 注入するのは決定性のためだけではない。1 回の実行の中で「観測時刻」と「同期時刻」が\n * 別々に進むと、どの行がどの実行で書かれたのかを後から辿れなくなる。\n */\n now: () => Date\n logger: SyncLoggerPort\n /** Operator-facing text. 運用者向けの文言。 */\n messages?: SyncMessages\n}\n\n/** Result of one run over a set of links. リンク集合 1 巡分の結果。 */\nexport type DirectorySyncRunResult = {\n outcomes: SyncOutcome[]\n /**\n * Links that did not end in a healthy, converged state. 健全に収束しなかったリンク数。\n *\n * 消滅の観測と無効化も含める。グループが恒久的に同期対象から外れることは、実行の中で\n * 最も重い結末であり、それを「0 unhealthy」と報告しては監視の意味がない。\n */\n unhealthyCount: number\n /**\n * Links that became unhealthy on this run. この実行で新たに不健全になったリンク数。\n *\n * ⚠️ 通知はこちらで判断する。`unhealthyCount` は状態の量なので、消滅済みのグループが 1 つ\n * 残っているだけで毎周期 1 以上になり、同じ 1 件を周期ぶん通知し続ける — 本当に新しい\n * 消滅がその中に埋もれる。数えるのは状態、知らせるのは変化である。\n */\n newlyUnhealthyCount: number\n}\n\n/**\n * Creates a synchronization runner. 同期ランナーを生成する処理。\n *\n * @param config Injected provider, repository, limits, clock and logger. 注入する構成。\n * @returns The runner. ランナー。\n * @throws {TypeError} When the policy or the threshold is invalid. 方針・しきい値が不正な場合。\n */\nexport function createDirectorySync(config: DirectorySyncConfig) {\n const { directory, repository, policy, now, logger } = config\n const messages = config.messages ?? defaultSyncMessages\n // 結線時点で落とす。最初の同期まで設定不備に気付けないと、その間ずっと陳腐化が進む\n assertBlastRadiusPolicy(policy)\n if (!Number.isInteger(config.vanishedThreshold) || config.vanishedThreshold < 1) {\n throw new TypeError(`[@aiquants/auth-directory-drizzle] vanishedThreshold must be a positive integer, received ${String(config.vanishedThreshold)}`)\n }\n if (typeof config.provider !== \"string\" || config.provider.trim() === \"\") {\n throw new TypeError(\"[@aiquants/auth-directory-drizzle] provider must be a non-empty string so links of other providers can be told apart\")\n }\n\n /**\n * Records a failure without letting the recording itself end the run.\n * 記録そのものが実行を終わらせないようにして失敗を記録する処理。\n *\n * `recordFailure` は失敗した書込と同じデータベースを触る。その書込が落ちた原因がデータベース\n * 側にあるなら、記録も落ちる可能性が高い。素通しにすると、1 本目のリンクの失敗が残り全部の\n * リンクを未実行のまま実行ごと終わらせる。\n */\n async function noteFailure(link: SyncLink, reason: string): Promise<void> {\n try {\n await repository.recordFailure(link, reason)\n } catch (error) {\n logger.error(`[auth-directory] could not record the failure for ${link.externalKey}:`, error)\n }\n }\n\n /** Reads the upstream group, or explains why it could not. 上流グループを読む処理。 */\n async function readUpstream(link: SyncLink): Promise<{ kind: \"ok\"; group: DirectoryGroup; members: Awaited<ReturnType<DirectoryProvider[\"listGroupMembers\"]>> } | { kind: \"missing\" } | { kind: \"failed\"; error: Error }> {\n try {\n const group = await directory.getGroup(link.externalId)\n if (group === null) return { kind: \"missing\" }\n return { kind: \"ok\", group, members: await directory.listGroupMembers(group, link.membershipMode) }\n } catch (error) {\n return { kind: \"failed\", error: error instanceof Error ? error : new Error(String(error)) }\n }\n }\n\n /** Handles a group the upstream says does not exist. 上流が存在しないと答えたグループの処理。 */\n async function handleMissing(link: SyncLink): Promise<SyncOutcome> {\n // ⚠️ 既に無効化まで至った消滅は、掃引のたびに **同じ事実を書き直さない**。書き直すと\n // 観測回数は際限なく増え、`updated_at` は「最後に人が触った時刻」ではなく「最後に掃引が\n // 回った時刻」になり、運用通知は同じ 1 件の消滅を周期ぶん繰り返す — 本当に新しい消滅が\n // その中に埋もれる。上流が戻れば成功経路が再有効化するので、見張りを緩めたことにはならない。\n // 運用者が手で有効へ戻した場合 (`isGroupActive`) はここを通し、正しく無効化し直す\n if (link.vanishedObservations >= config.vanishedThreshold && !link.isGroupActive) {\n logger.warn(`[auth-directory] ${link.externalKey} still missing upstream (already deactivated after ${link.vanishedObservations} absences)`)\n return { kind: \"already-vanished\", link }\n }\n const observations = await repository.recordVanishedObservation(link, messages.groupMissing(link.externalKey, link.vanishedObservations + 1, config.vanishedThreshold))\n if (observations < config.vanishedThreshold) {\n logger.warn(`[auth-directory] ${link.externalKey} missing upstream (${observations}/${config.vanishedThreshold})`)\n return { kind: \"missing\", link, observations }\n }\n // 無効化はするが削除はしない。削除するとロール割当 (運用者の付与意思) まで失われる。\n // 上流が復旧すれば次の実行が同じリンクを読み、成功時に再有効化する\n await repository.markGroupInactive(link)\n await noteFailure(link, messages.groupVanished(link.externalKey, observations))\n logger.warn(`[auth-directory] ${link.externalKey} deactivated after ${observations} consecutive absences`)\n return { kind: \"vanished\", link }\n }\n\n /**\n * Synchronizes one group. 1 グループを同期する処理。\n *\n * 上流の読み取りが失敗した場合、**削除は一切行わない**。読めなかったことと「全員が抜けた」\n * ことは、返ってきたデータの上では区別がつかない。区別できない以上、破壊しない側へ倒す。\n *\n * @param link The link to synchronize. 対象リンク。\n * @returns What happened. 何が起きたか。\n */\n async function syncOne(link: SyncLink): Promise<SyncOutcome> {\n if (link.provider !== config.provider) {\n // 他 provider のリンクを読むと、こちらの上流に存在しないという理由で無効化してしまう\n await noteFailure(link, messages.foreignProvider(link.provider, config.provider))\n logger.warn(`[auth-directory] skipping ${link.externalKey}: belongs to provider ${link.provider}`)\n return { kind: \"foreign-provider\", link }\n }\n\n const upstream = await readUpstream(link)\n if (upstream.kind === \"missing\") return handleMissing(link)\n if (upstream.kind === \"failed\") {\n // ⚠️ 例外文は載せない。`sync_error` は管理 UI の全タブに出るため、閲覧権限しか持たない\n // 主体に上流の生の応答本文を読ませることになる。原文は直後の logger へ\n await noteFailure(link, messages.upstreamUnreadable(classifySyncFailure(upstream.error)))\n logger.error(`[auth-directory] upstream read failed for ${link.externalKey}:`, upstream.error)\n return { kind: \"upstream-failed\", link, error: upstream.error }\n }\n\n try {\n // ⚠️ 所属を触る前に上流の同一性を確定させる。手入力しかできない配備では、暫定の\n // 安定 ID (アドレス) で同じ上流を 2 回リンクできてしまう。ここで一意制約に当たれば、\n // **何も投影しないまま** 落ちる。後に回すと、二重の所属が先に commit される\n await repository.resolveExternalIdentity(link, upstream.group)\n const [snapshot, resolved] = await Promise.all([repository.loadGroupSnapshot(link), repository.resolveMembers(upstream.members)])\n // 突合が件数を減らしていたら、その差分はそのまま削除対象になる。上流の完全性だけを\n // 守っても、突合で落ちた人物は同じように消える\n if (resolved.length !== upstream.members.length) {\n const reason = messages.resolverIncomplete(upstream.members.length, resolved.length)\n await noteFailure(link, reason)\n logger.error(`[auth-directory] ${reason}`)\n return { kind: \"local-failed\", link, error: new Error(reason) }\n }\n\n const plan = reconcile(snapshot, resolved, policy)\n if (plan.aborted) {\n await noteFailure(link, messages.planRefused(plan.aborted))\n // ⚠️ 差し止めた対象は **ログにだけ** 出す。`sync_error` は管理 UI の全タブに出るため、\n // 並べるとグループの実際の名簿を閲覧権限だけの主体に読ませることになる。運用者が\n // 対象を知る必要は確かにあるので、失わずにここへ残す\n const withheld = plan.aborted.reason === \"invalid-upstream-data\" ? plan.aborted.offendingEmails : plan.aborted.axis === \"membership\" ? plan.aborted.withheldProjectionRemoves.map(String) : plan.aborted.withheldLedgerDeletes\n logger.warn(`[auth-directory] plan refused for ${link.externalKey}: ${plan.aborted.reason}; withheld: ${withheld.join(\", \")}`)\n return { kind: \"aborted\", link, plan }\n }\n\n const hasWork = plan.ledgerUpserts.length > 0 || plan.ledgerDeletes.length > 0 || plan.projectionAdds.length > 0 || plan.projectionRemoves.length > 0\n const stamp = now()\n let report: ApplyReport = { refusals: [], failures: [] }\n if (hasWork) report = await repository.applyPlan(link, plan, stamp)\n // 変化が無くても成功時刻は進め、無効化されていたグループはここで再有効化する。\n // 進めないと、正常に動いている同期が陳腐化として警告され続け、本物の停止が埋もれる\n await repository.recordSuccess(link, upstream.group, stamp)\n\n if (report.refusals.length > 0) {\n // 拒否は失敗ではない (残りは適用済み) が、収束していないので必ず記録する\n await noteFailure(link, messages.removalsRefused(report.refusals))\n logger.warn(`[auth-directory] ${report.refusals.length} removal(s) refused for ${link.externalKey}: users ${report.refusals.map((r) => r.userId).join(\", \")}`)\n }\n if (report.failures.length > 0) {\n // 書込の失敗は待てば直り得る。拒否と混ぜず、別の文言で記録する\n await noteFailure(link, messages.removalWritesFailed(report.failures))\n logger.warn(`[auth-directory] ${report.failures.length} removal(s) could not be written for ${link.externalKey}: users ${report.failures.map((f) => f.userId).join(\", \")}`)\n }\n if (plan.unresolvedEmails.length > 0) {\n logger.info(`[auth-directory] ${link.externalKey}: ${plan.unresolvedEmails.length} upstream member(s) have no local user yet`)\n }\n // ⚠️ 収束しなかった実行を「適用済み」で終わらせない。拒否も書込失敗も残ったまま\n // `synced_at` だけ進むと、鮮度は健全・通知は無音・要求キューの結果は成功になり、\n // 上流が外した人物がグループに残っていることを示す信号がどこにも出なくなる\n if (report.refusals.length > 0 || report.failures.length > 0) return { kind: \"not-converged\", link, plan, refusals: report.refusals, failures: report.failures }\n return hasWork ? { kind: \"applied\", link, plan } : { kind: \"unchanged\", link }\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error))\n await noteFailure(link, messages.localWriteFailed(\"localWriteError\"))\n logger.error(`[auth-directory] local write failed for ${link.externalKey}:`, err)\n return { kind: \"local-failed\", link, error: err }\n }\n }\n\n return {\n syncOne,\n\n /**\n * Synchronizes every eligible link in scope. 対象範囲の全リンクを同期する処理。\n *\n * リンクは **逐次** 処理する。並列化すると上流のレート制限を一気に消費し、最初に\n * 弾かれたグループだけが失敗して部分的に古い状態が残る。1 グループの失敗は他へ\n * 波及させず、実行自体は最後まで続ける。\n *\n * @param scope Which tenant (and optionally which group) to cover. 対象テナントと範囲。\n * @returns Every outcome, and how many ended unhealthy. 全結果と不健全件数。\n */\n async run(scope: SyncScope): Promise<DirectorySyncRunResult> {\n const links = await repository.listSyncableLinks(scope)\n const outcomes: SyncOutcome[] = []\n for (const link of links) {\n try {\n outcomes.push(await syncOne(link))\n } catch (error) {\n // syncOne は自前で捕捉するが、記録経路まで壊れた場合の最後の網。1 本の\n // リンクのために残り全部を未実行にしない\n const err = error instanceof Error ? error : new Error(String(error))\n logger.error(`[auth-directory] ${link.externalKey} ended abnormally:`, err)\n outcomes.push({ kind: \"local-failed\", link, error: err })\n }\n }\n const unhealthyCount = outcomes.filter((o) => UNHEALTHY_SYNC_OUTCOMES.includes(o.kind)).length\n const newlyUnhealthyCount = outcomes.filter((o) => NEWLY_UNHEALTHY_SYNC_OUTCOMES.includes(o.kind)).length\n logger.info(`[auth-directory] run finished: ${outcomes.length} link(s), ${unhealthyCount} unhealthy (${newlyUnhealthyCount} new)`)\n return { outcomes, unhealthyCount, newlyUnhealthyCount }\n },\n }\n}\n","/**\n * @module sync-messages\n * @description Operator-facing text the synchronization records against a link.\n * 同期がリンクへ記録する、運用者向けの文言。\n *\n * 文言を注入可能にしてあるのは、`sync_error` が管理 UI にそのまま出るためである。ホスト非依存を\n * 求められるパッケージへ日本語を焼き込むと、日本語以外の配備がデータベースに日本語を持つことになる。\n * 既定は日本語 (このリポジトリの管理 UI に合わせる)。\n */\nimport type { SyncAbort } from \"@aiquants/auth-directory-core\"\n\n/**\n * Why a read or a write failed, classified for display. 読み書きが失敗した理由 (表示用の分類)。\n *\n * ⚠️ **例外文をそのまま `sync_error` へ入れてはならない。** この列は管理 UI のバナーとして\n * **全タブに** 出るため、閲覧権限しか持たない主体が上流の生の応答本文 (サービスアカウントの\n * アドレス、内部 URL、資格情報の断片) や DB の接続文字列・スキーマ名を読めてしまう。しかも\n * それは、候補一覧の **グループ名すら** 見せないと決めた相手である。原文はログへ、画面へは分類を。\n */\nexport type SyncFailureCause = \"denied\" | \"rateLimited\" | \"upstreamError\" | \"timeout\" | \"localWriteError\" | \"unknown\"\n\n/**\n * Classifies a thrown value without quoting it. 投げられた値を、引用せずに分類する処理。\n *\n * 上流アダプタの型に依存しないよう、`status` という数値プロパティの有無だけで判定する。\n * 判別できないものは `unknown` へ倒す (推測した分類を出すより、判らないと言う方が正しい)。\n *\n * @param error - The thrown value. 投げられた値。\n * @returns The display classification. 表示用の分類。\n */\nexport function classifySyncFailure(error: unknown): SyncFailureCause {\n if (error instanceof Error && (error.name === \"AbortError\" || error.name === \"TimeoutError\")) return \"timeout\"\n const status = typeof error === \"object\" && error !== null && \"status\" in error ? (error as { status: unknown }).status : undefined\n if (typeof status !== \"number\") return \"unknown\"\n if (status === 401 || status === 403) return \"denied\"\n if (status === 429 || status === 408) return \"rateLimited\"\n if (status >= 500) return \"upstreamError\"\n return \"unknown\"\n}\n\n/** The messages one synchronization can produce. 同期が生成し得る文言。 */\nexport type SyncMessages = {\n /** The upstream could not be read at all. 上流をまったく読めなかった。 */\n upstreamUnreadable: (cause: SyncFailureCause) => string\n /** The local write failed. ローカルへの書込が失敗した。 */\n localWriteFailed: (cause: SyncFailureCause) => string\n /** The resolver returned fewer members than the upstream listed. 突合が件数を減らした。 */\n resolverIncomplete: (upstream: number, resolved: number) => string\n /** The group is missing upstream but not yet deactivated. 消滅の観測 (未確定)。 */\n groupMissing: (externalKey: string, observations: number, threshold: number) => string\n /** The group has been deactivated after repeated absence. 消滅確定・無効化。 */\n groupVanished: (externalKey: string, observations: number) => string\n /**\n * Removals whose write failed for a transient reason. 一時的な理由で書けなかった削除。\n *\n * ⚠️ 拒否とは別の文言にする。「最後の管理者ガードが止めた」は人手を呼ぶ答えであり、\n * デッドロックや接続断に対して同じ文言を出すと、待てば直る事象に人を動かすことになる。\n */\n removalWritesFailed: (failures: readonly { userId: number; cause: SyncFailureCause }[]) => string\n /** The reconciler refused the plan. 突合が plan を拒否した。 */\n planRefused: (abort: SyncAbort) => string\n /** The lockout guard refused some removals. ロックアウトガードが一部の削除を拒否した。 */\n removalsRefused: (refusals: readonly { userId: number; refusal?: string }[]) => string\n /** The link belongs to a different provider. 他 provider のリンクだった。 */\n foreignProvider: (linkProvider: string, runnerProvider: string) => string\n}\n\n/**\n * Counts items instead of naming them. 列挙する代わりに数える処理。\n *\n * ⚠️ **`sync_error` に人の名前やアドレスを並べてはならない。** この列は管理 UI のバナーとして\n * 全タブに出るため、閲覧権限しか持たない主体がグループの実際の名簿を読めてしまう — 候補一覧の\n * グループ名すら見せないと決めた相手に、それより機微な情報を渡すことになる。しかも 1000 字で\n * 切られるため、末尾のアドレスは必ず途中で切れ、運用者に必要な部分から先に失われる。\n * 対象の一覧は `logger.warn` へ出し、画面には件数を出す。\n */\nconst countOf = (items: readonly string[]): string => `${items.length} 件`\n\n/** Default Japanese messages. 既定の日本語文言。 */\n/** 分類ごとの説明。原因の **種類** だけを述べ、上流や DB の文言は引用しない。 */\nconst causeText: Record<SyncFailureCause, string> = {\n denied: \"資格情報が拒否されました\",\n rateLimited: \"呼び出しが制限されています\",\n upstreamError: \"相手側が内部エラーを返しました\",\n timeout: \"応答がありませんでした\",\n localWriteError: \"書き込みが拒否されました\",\n unknown: \"詳細はサーバーのログに出ています\",\n}\n\nexport const defaultSyncMessages: SyncMessages = {\n upstreamUnreadable: (cause) => `上流ディレクトリの読み取りに失敗しました (${causeText[cause]})。`,\n localWriteFailed: (cause) => `ローカルへの書き込みに失敗しました (${causeText[cause]})。`,\n resolverIncomplete: (upstream, resolved) =>\n `上流が ${upstream} 件のメンバーを返しましたが、突合結果は ${resolved} 件でした。件数が減る突合は、解決できなかった人物を「上流から消えた」として削除してしまうため中断しました。解決できないメンバーは省略せず resolvedUserId: null で返す必要があります。`,\n groupMissing: (externalKey, observations, threshold) => `上流でグループが見つかりません (${externalKey})。連続 ${observations} 回目です。${threshold} 回に達した時点でグループを無効化します。`,\n groupVanished: (externalKey, observations) => `上流でグループが ${observations} 回連続で見つかりませんでした (${externalKey})。グループを無効化しました。上流が復旧すれば次回の同期で自動的に再有効化されます。`,\n planRefused: (abort) => {\n if (abort.reason === \"invalid-upstream-data\") return `同期を中断しました: 上流のメンバー一覧に扱えないアドレスが ${countOf(abort.offendingEmails)} 含まれています。対象はサーバーのログに出ています。`\n const axis = abort.axis === \"membership\" ? \"権限を失う人数\" : \"サインインできなくなる人数\"\n const withheld = abort.axis === \"membership\" ? abort.withheldProjectionRemoves.map(String) : abort.withheldLedgerDeletes\n return [\n `同期を中断しました: ${axis}が ${abort.population} 人中 ${abort.removalCount} 人になります`,\n `(上限: ${abort.limits.maxRemovalCount} 件 / 比率 ${abort.limits.maxRemovalRatio})。`,\n `差し止めた対象: ${countOf(withheld)} (一覧はサーバーのログに出ています)。`,\n \"上流側の設定を確認してください。意図した変更であれば、この軸の上限だけを一時的に引き上げて再実行します。\",\n ].join(\" \")\n },\n // ⚠️ この 1 つだけが対象を名指しする。差し止められたのは **ローカルのユーザー ID** であり、\n // 同じ管理面の利用者タブが ID も名前もアドレスも一覧している — 隠しても守るものが無く、\n // 隠すと運用者が誰の解除が止まったのか分からなくなる。上流由来の値を並べる他の文言とは\n // 出所が違う\n removalWritesFailed: (failures) => `${failures.length} 件の所属解除が書き込めませんでした (${[...new Set(failures.map((f) => causeText[f.cause]))].join(\" / \")})。次回の実行で再試行します。`,\n removalsRefused: (refusals) => `${refusals.length} 件の所属解除が最後の管理者ガードにより拒否されました (ユーザー ${refusals.map((r) => r.userId).join(\", \")})。残りの変更は適用済みです。`,\n foreignProvider: (linkProvider, runnerProvider) => `このリンクの供給元は ${linkProvider} ですが、実行中の同期は ${runnerProvider} 用です。対応する同期ジョブから実行してください。`,\n}\n","/**\n * @module sync-ports\n * @description The narrow storage surface the synchronization orchestration depends on.\n * 同期オーケストレーションが依存する、狭い記憶域の面。\n *\n * オーケストレーションを drizzle から切り離すために置いている。同期の手順そのもの\n * (何を順に行い、どこで打ち切るか) は、データベースを起動せずに全分岐を検証できるべきである —\n * 検証しにくい手順は検証されず、検証されない手順は「読めなかったら消さない」を静かに破る。\n */\n\nimport type { AuthDirectoryMembershipMode, DirectoryGroup, DirectoryMember, GroupSyncPlan, LocalGroupSnapshot, ResolvedDirectoryMember } from \"@aiquants/auth-directory-core\"\nimport type { SyncFailureCause } from \"./sync-messages\"\n\n/**\n * One external group link, as the synchronization needs to see it.\n * 同期が見る必要のある外部グループリンク 1 件。\n */\nexport type SyncLink = {\n linkId: number\n groupId: number\n groupKey: string\n tenantId: string\n /** Which upstream owns this group. どの上流がこのグループを所有するか。 */\n provider: string\n externalId: string\n externalKey: string\n membershipMode: AuthDirectoryMembershipMode\n /** False when the group has been deactivated. グループが無効化されている場合 false。 */\n isGroupActive: boolean\n /**\n * How many consecutive runs have found the group missing upstream.\n * 上流でグループが見つからない状態が何回連続したか。\n *\n * 1 度の 404 で無効化すると、伝播待ちや権限変更の一瞬で生きているグループが落ちる。\n */\n vanishedObservations: number\n}\n\n/**\n * Which links a run covers. 1 回の実行が対象にするリンクの範囲。\n *\n * ⚠️ `tenantId` は **必須** である。1 つのジョブが持つ上流の資格情報は 1 つのテナントに対応する。\n * 範囲を絞らないと、別テナントのリンクを読んだうえで **自分の資格情報で得た結果をその別テナントへ\n * 書き込む**。上流が 404 を返せば (借用先の外にあるグループでは通常そうなる) 消滅と判定して\n * 相手のグループを無効化し、外部 ID が偶然一致すれば相手のグループへ自分の所属を射影する。\n * 要求した側の権限は一切関係しないため、既定値 1 つで越境が成立する形にしてはならない。\n * 同じ理由で `createSyncRequestQueue` も構築時にテナントを固定する。\n */\nexport type SyncScope = {\n tenantId: string\n /** Restrict further to one group within the tenant. テナント内の 1 グループへさらに絞る。 */\n groupId?: number\n}\n\n/** Why one group's synchronization ended the way it did. 1 グループの同期がどう終わったか。 */\nexport type SyncOutcome =\n | { kind: \"applied\"; link: SyncLink; plan: GroupSyncPlan }\n /** Nothing changed. Not a failure — an idempotent run. 変化なし。失敗ではなく冪等な実行。 */\n | { kind: \"unchanged\"; link: SyncLink }\n /** The plan was refused by the reconciler's own guards. 突合自身のガードが plan を拒否した。 */\n | { kind: \"aborted\"; link: SyncLink; plan: GroupSyncPlan }\n /** The group is missing upstream, but not yet often enough to act on. 消滅の観測 (未確定)。 */\n | { kind: \"missing\"; link: SyncLink; observations: number }\n /** The group has been missing long enough that it was deactivated. 消滅確定・無効化した。 */\n | { kind: \"vanished\"; link: SyncLink }\n /**\n * Still missing, and already deactivated on an earlier run. 消滅済みで、以前の実行で無効化済み。\n *\n * ⚠️ `vanished` と分ける理由は、**運用通知が状態ではなく変化に反応するべき** だからである。\n * 同じ 1 件の消滅を掃引のたびに通知すると、本当に新しい消滅がその中に埋もれる。この結末は\n * 「今回の掃引では何も変わっていない」ことの表明であり、健全ではないが新規でもない。\n */\n | { kind: \"already-vanished\"; link: SyncLink }\n /** Reading upstream failed. 上流の読み取りが失敗した。 */\n | { kind: \"upstream-failed\"; link: SyncLink; error: Error }\n /** Writing locally failed. ローカルへの書込が失敗した。 */\n | { kind: \"local-failed\"; link: SyncLink; error: Error }\n /** The link belongs to another provider than the one running. 他 provider のリンクだった。 */\n | { kind: \"foreign-provider\"; link: SyncLink }\n /**\n * The plan was applied only in part. plan の一部しか適用できなかった。\n *\n * ⚠️ `applied` と分ける。拒否や書込失敗が残ったまま「適用済み」と答えると、上流が外した人物が\n * グループに残っているのに、鮮度は健全・通知は無音・要求キューの結果は成功になる — 収束して\n * いないことを示す信号がどこにも出ない。\n */\n | { kind: \"not-converged\"; link: SyncLink; plan: GroupSyncPlan; refusals: RemovalOutcome[]; failures: RemovalFailure[] }\n\n/**\n * Outcomes that mean something changed for the worse on this run. この実行で新たに悪化した結末。\n *\n * ⚠️ **状態ではなく変化を数える。** `already-vanished` は不健全な状態ではあるが、以前の実行で\n * 既に記録・通知済みであり、今回の掃引では何も変わっていない。ここに入れると同じ 1 件が周期ぶん\n * 通知され、本当に新しい消滅が埋もれる。「今どれだけ壊れているか」を数えたい呼び出し側は\n * {@link UNHEALTHY_SYNC_OUTCOMES} を使う。\n */\nexport const NEWLY_UNHEALTHY_SYNC_OUTCOMES: readonly SyncOutcome[\"kind\"][] = [\"aborted\", \"missing\", \"vanished\", \"upstream-failed\", \"local-failed\", \"foreign-provider\", \"not-converged\"]\n\n/** Outcomes that mean the group is not in a healthy, converged state. 健全でない結末。 */\nexport const UNHEALTHY_SYNC_OUTCOMES: readonly SyncOutcome[\"kind\"][] = [...NEWLY_UNHEALTHY_SYNC_OUTCOMES, \"already-vanished\"]\n\n/** What happened to one guarded removal. ガード付き削除 1 件の結末。 */\nexport type RemovalOutcome = { userId: number; applied: boolean; refusal?: string }\n\n/**\n * One removal the write itself could not complete. 書き込み自体が完了しなかった削除 1 件。\n *\n * ⚠️ **拒否とは別に数える。** 「最後の管理者を失う」はガードの判断であり、運用者への答えは\n * 「別の管理者を立てよ」である。一方でデッドロックの犠牲や接続断は待てば直るものであり、\n * 答えは「再試行せよ」である。両方を同じ袋に入れると、待てば直る事象に対して人手を呼び、\n * 人手が要る事象を放置することになる。\n */\nexport type RemovalFailure = { userId: number; cause: SyncFailureCause }\n\n/** What `applyPlan` actually managed to do. `applyPlan` が実際に行えたこと。 */\nexport type ApplyReport = {\n /** Removals the administrator lockout guard refused. ロックアウトガードが拒否した削除。 */\n refusals: RemovalOutcome[]\n /** Removals whose write failed for a transient reason. 一時的な理由で書けなかった削除。 */\n failures: RemovalFailure[]\n}\n\n/**\n * Storage operations the synchronization performs.\n * 同期が行う記憶域操作。\n */\nexport type DirectorySyncRepository = {\n /**\n * Lists links that are eligible to run. 実行対象のリンクを列挙する処理。\n *\n * ⚠️ 無効化されたグループのリンクも返すこと。返さないと、上流が復旧しても二度と同期されず、\n * 再有効化の機会が永久に失われる。\n *\n * @param scope Restrict to one tenant, or one group within it. テナント、または特定グループへ限定。\n */\n listSyncableLinks(scope: SyncScope): Promise<SyncLink[]>\n\n /** Reads the local ledger and the real membership for one group. 台帳と実所属を読む処理。 */\n loadGroupSnapshot(link: SyncLink): Promise<LocalGroupSnapshot>\n\n /**\n * Resolves upstream members against the local user table.\n * 上流メンバーをローカルユーザー表へ突合する処理。\n *\n * 突合順は上流 ID が第一キー、アドレスが第二キー。再利用されたアドレスが他人の所属を\n * 継承する事故を防ぐため、この順序は実装側の裁量ではなく契約である。\n *\n * ⚠️ **入力と同じ件数を返すこと。** 解決できないメンバーは省略せず `resolvedUserId: null` で\n * 返す。省略すると、その人物は「上流から消えた」と解釈されて削除対象になる。\n */\n resolveMembers(members: readonly DirectoryMember[]): Promise<ResolvedDirectoryMember[]>\n\n /**\n * Applies one group's plan, taking every membership removal through the lockout guard.\n * 1 グループ分の plan を適用する処理。実所属の削除は必ず最後の管理者ガードを通す。\n *\n * ⚠️ ロックアウトガードが 1 件の削除を拒否しても、**残りの削除を続けること**。1 件で打ち切ると、\n * 拒否された行がグループ全体の収束を永久に止める (次回以降も同じ行で止まる)。拒否は\n * {@link ApplyReport.refusals} で報告する。\n *\n * @returns What was refused. 拒否された内容。\n * @throws When the write itself failed. 書込そのものが失敗した場合。\n */\n applyPlan(link: SyncLink, plan: GroupSyncPlan, observedAt: Date): Promise<ApplyReport>\n\n /** Records a successful run and reactivates the group if it was deactivated. 成功の記録。 */\n /**\n * Persists the upstream's stable identity before any membership is applied.\n * 所属を適用する **前** に、上流の安定 ID を確定させる処理。\n *\n * ⚠️ **順序がこの操作の意味である。** 管理面は、候補一覧が引けない配備ではアドレスを暫定の\n * 安定 ID として保存する。同じ上流グループを別の綴り (アドレスと別名、あるいは本当の ID) で\n * 2 回リンクできてしまい、`UNIQUE (tenant, provider, external_id)` も文字列が違うので通る。\n * 確定を所属の適用より前に置くことで、重複したリンクは **何も投影しないまま** 一意制約に\n * 当たって落ちる。後に置くと、二重の所属が先に commit され、両方のロールが配られてから\n * 書き戻しだけが失敗し続ける。\n *\n * @param link - The link being synchronized. 同期中のリンク。\n * @param upstreamGroup - What the upstream answered. 上流が答えた内容。\n * @returns Nothing. 戻り値なし。\n * @throws When another link already owns this upstream group. 同じ上流を別のリンクが持つ場合。\n */\n resolveExternalIdentity(link: SyncLink, upstreamGroup: DirectoryGroup): Promise<void>\n recordSuccess(link: SyncLink, group: DirectoryGroup, syncedAt: Date): Promise<void>\n\n /** Records a failure reason against the link, leaving membership untouched. 失敗を記録する処理。 */\n recordFailure(link: SyncLink, reason: string): Promise<void>\n\n /**\n * Records one observation that the group is missing upstream. 上流での消滅を 1 回観測した記録。\n *\n * @returns How many consecutive observations there now are. 連続観測回数。\n */\n recordVanishedObservation(link: SyncLink, reason: string): Promise<number>\n\n /** Marks a group inactive after it has been missing long enough. 消滅確定後の無効化。 */\n markGroupInactive(link: SyncLink): Promise<void>\n}\n\n/** Minimal logging surface. 最小のログ面。 */\nexport type SyncLoggerPort = {\n info(message: string, ...args: unknown[]): void\n warn(message: string, ...args: unknown[]): void\n error(message: string, ...args: unknown[]): void\n}\n","/**\n * Consumer side of the \"sync now\" request queue.\n * 「今すぐ同期」要求キューの消費側。\n *\n * 管理 UI は上流へ到達できないため、要求を `TDDirectorySyncRequest` の行として残す\n * (`createDirectoryLinkStore.requestSync`)。それを拾って結果を書き戻すのは、資格情報を持つ\n * 同期ジョブだけである。生産側と消費側を別モジュールに分けているのは、web プロセスが\n * 消費側の関数を import できてしまう状態そのものを避けるためではなく、**どちらの責務が\n * どのプロセスに属するかを読んで分かる形にするため**である。\n */\nimport { assertTenantId } from \"@aiquants/authz-core\"\nimport { and, asc, eq, isNull } from \"drizzle-orm\"\nimport { type DirectoryDb, type EnsureConnection, toInstant } from \"./db\"\nimport type { AuthDirectoryTables } from \"./schema\"\n\n/** One claimed request. 取得済みの要求 1 件。 */\nexport type ClaimedSyncRequest = {\n id: number\n tenantId: string\n /** `null` means every group of the tenant. `null` はテナント全件を意味する。 */\n groupId: number | null\n requestedBy: string\n}\n\n/**\n * Requests that are not progressing. 進んでいない要求。\n *\n * ⚠️ 2 つを分けて数える。`stranded` は「掴まれたが終わっていない」— ジョブが落ちた跡である。\n * `unclaimed` は「誰にも掴まれていない」— **そのテナントを担当するジョブが居ない** 跡であり、\n * 掴まれた行だけを見ていると丸ごと見落とす。重複抑止が在るため、後者は 1 行で以後の要求すべてを\n * 黙って捨てる。\n */\nexport type StrandedRequests = { stranded: number[]; unclaimed: number[] }\n\n/**\n * Recorded when a request is closed because the process that claimed it never finished it.\n * 掴んだプロセスが終わりを書かなかったために閉じた要求へ記録される理由。\n */\nexport const STRANDED_REASON = \"The job that claimed this request ended without recording an outcome; whether the synchronization ran is unknown.\"\n\n/** How a claimed request ended. 取得した要求の終わり方。 */\nexport type SyncRequestResult = { result: \"succeeded\" | \"failed\"; error?: string | null }\n\n/** Options for {@link createSyncRequestQueue}. キュー消費側の生成オプション。 */\nexport type SyncRequestQueueOptions = {\n ensureConnection?: EnsureConnection\n /**\n * The only tenant this consumer may claim for. この消費側が取得してよい唯一のテナント。\n *\n * ⚠️ **必須である。** 1 つのジョブが持つ上流の資格情報は 1 つのテナントに対応する。範囲を\n * 絞らないと、別テナントの要求を掴んだジョブが **自分の資格情報で読んだ結果を、その別テナントへ\n * 書き込む**。要求を出した側の権限は一切関係しないため、これは要求 1 つで越境が起きる形になる。\n *\n * ⚠️ その代償として、**担当ジョブの居ないテナントの要求は誰にも観測されない**。`listStranded`\n * もこのテナントに絞られる以上、消費側が存在しないテナントを数える主体はどこにも居ない。\n * 見えるのは web 側だけである — `getStatus().pendingRequestCount` が管理面のバナーに出て、\n * `requestSync` は 2 度目以降を `already-pending` として返す。配備時は、リンクを持つ\n * すべてのテナントにジョブを 1 つずつ立てること。\n */\n tenantId: string\n /** Injected clock — the run's timestamps must all come from one source. 注入時計。 */\n now: () => Date\n /**\n * Reports a request that could not be closed. 閉じられなかった要求を報告する処理。\n *\n * ⚠️ {@link createSyncRequestQueue.abandonStranded} は 1 件の失敗で残りを諦めない。\n * 諦めない以上、失敗そのものが黙って消える経路になってはならない — 記録の宛先は\n * 呼び出し側が持っているので、ここから渡す。省略時は何もしない。\n */\n onCloseFailed?: (id: number, error: unknown) => void\n /**\n * Longest an error message may be, matching the column. 列に合わせたエラー文の上限長。\n *\n * 超過分は切り捨てる。切り捨てないと、記録そのものが失敗して「何が起きたか分からない\n * まま要求が取得済みで固まる」という最悪の状態になる。\n */\n maxErrorLength?: number\n}\n\n/** `TDDirectorySyncRequest.error` の列長。 */\nconst ERROR_COLUMN_LENGTH = 2000\n\n/**\n * Creates the consumer for the synchronization request queue.\n * 同期要求キューの消費側を生成する処理。\n *\n * @param dbUnknown - Drizzle database instance. drizzle のデータベースインスタンス。\n * @param tables - Directory tables (owns the queue). ディレクトリ表 (キューの所有者)。\n * @param opts - Connection, clock and truncation bound. 接続・時計・切り詰め上限。\n * @returns The claim / finish pair. 取得と完了記録の 2 操作。\n */\nexport function createSyncRequestQueue(dbUnknown: unknown, tables: AuthDirectoryTables, opts: SyncRequestQueueOptions) {\n const db = dbUnknown as DirectoryDb\n const ready = opts.ensureConnection ?? (async () => {})\n const queue = tables.TDDirectorySyncRequest\n const tenantId = assertTenantId(opts.tenantId, \"createSyncRequestQueue(tenantId)\")\n const maxErrorLength = opts.maxErrorLength ?? ERROR_COLUMN_LENGTH\n if (!Number.isInteger(maxErrorLength) || maxErrorLength < 1 || maxErrorLength > ERROR_COLUMN_LENGTH) {\n throw new TypeError(`[@aiquants/auth-directory-drizzle] maxErrorLength must be an integer within 1..${ERROR_COLUMN_LENGTH}, received ${String(opts.maxErrorLength)}`)\n }\n\n const queueApi = {\n /**\n * Claims the oldest unpicked request, marking it picked in the same statement.\n * 最も古い未取得の要求を、同じ文で取得済みにしながら取り出す処理。\n *\n * 取得と印付けを分けると、2 つのジョブが同じ要求を拾って同じグループを二重に同期する。\n * `UPDATE ... OUTPUT` は 1 文なので、行を掴んだのが誰かは常に 1 つに決まる。\n *\n * @returns The claimed request, or `null` when the queue is empty. 取得結果 (空なら `null`)。\n */\n async claim(): Promise<ClaimedSyncRequest | null> {\n await ready()\n const pending = await db\n .select<{ id: number }>({ id: queue.id })\n .from(queue)\n .where(and(eq(queue.tenantId, tenantId), isNull(queue.pickedAt)))\n .orderBy(asc(queue.id))\n\n for (const candidate of pending) {\n // 取得は compare-and-swap で行う。`picked_at is null` を更新条件に残すことで、\n // 同じ行を狙った 2 つ目のジョブは 0 行しか返らず、静かに次の候補へ進む。\n // 先に select してから無条件 update すると、両方が同じグループを二重に同期する\n const rows = await db\n .update(queue)\n .set({ pickedAt: opts.now() })\n .output()\n // テナント述語は更新条件にも残す。select と update の間に行が別テナントへ\n // 変わることは無いが、範囲を 1 か所でしか守らない形にしない\n .where(and(eq(queue.tenantId, tenantId), eq(queue.id, Number(candidate.id)), isNull(queue.pickedAt)))\n const row = (rows as Array<Record<string, unknown>> | undefined)?.[0]\n if (!row) continue\n // ⚠️ 正準の綴りだけを読む。別綴りを `??` で連ねると、`groupId` が読めなかったときに\n // **最も広い値** (`null` = テナント全件) へ静かに落ちる — 1 グループの要求が\n // テナント全体の走査に化ける。`requestedBy` を空文字へ落とすのも同じで、\n // 実行者不明の監査行が黙って生まれる。読めない形は落ちて知らせる\n if (row.requestedBy === undefined || row.requestedBy === null) {\n throw new TypeError(`[@aiquants/auth-directory-drizzle] the driver returned a row without requestedBy for request ${String(row.id)}`)\n }\n const rawGroupId = row.groupId ?? null\n return {\n id: Number(row.id),\n // ⚠️ 行から読み直さず、**この消費側に設定されたテナント** を返す。行の値を\n // 信じると、述語を 1 つ緩めただけで越境した書き込みが成立してしまう\n tenantId,\n groupId: rawGroupId === null ? null : Number(rawGroupId),\n requestedBy: String(row.requestedBy),\n }\n }\n return null\n },\n\n /**\n * Records how a claimed request ended. 取得済みの要求がどう終わったかを記録する処理。\n *\n * @param id - The claimed request id. 取得した要求の ID。\n * @param outcome - Result and optional error text. 結果と (あれば) エラー文。\n * @returns Nothing. 戻り値なし。\n */\n async finish(id: number, outcome: SyncRequestResult): Promise<void> {\n await ready()\n const error = outcome.error ? outcome.error.slice(0, maxErrorLength) : null\n // ⚠️ 完了の記録も自分のテナントに限る。この 1 本だけ範囲を持たないと、ID を別経路\n // (運用の手作業、別インスタンスの `listStranded`) から得た消費側が、他テナントの\n // 要求行へ結果を書き込み、そのテナントのジョブが処理していない要求を「完了」にする\n // ⚠️ 終わりは 1 度だけ書ける。述語から未完了条件を外すと、既に記録された結末を後から\n // 上書きできてしまう — `abandonStranded` は一覧を読んでから 1 件ずつ閉じるので、その\n // 間に本来の消費側が「成功」を書いた行を「失敗・走ったかどうか不明」で塗り潰す\n await db\n .update(queue)\n .set({ finishedAt: opts.now(), result: outcome.result, error })\n .output()\n .where(and(eq(queue.tenantId, tenantId), eq(queue.id, id), isNull(queue.finishedAt)))\n },\n\n /**\n * Lists requests that are not progressing. 進んでいない要求の一覧。\n *\n * ジョブが落ちると、行は取得済みのまま完了しない。放置すると要求した人はいつまでも\n * 結果を見られないので、運用が拾えるよう明示的に数えられる形にしておく。\n *\n * ⚠️ **掴まれていない古い行も数える。** 取得済みだけを見ると、そのテナントを担当する\n * ジョブが居ない場合が丸ごと盲点になる — その行は永久に処理されないのに、重複抑止の\n * せいで以後の「今すぐ同期」まで黙って捨てられる。\n *\n * @param olderThan - Cutoff for both timestamps. 両方の時刻に対する締切時刻。\n * @returns Stranded and unclaimed request ids. 取り残された要求と、掴まれていない要求の ID。\n */\n async listStranded(olderThan: Date): Promise<StrandedRequests> {\n await ready()\n const rows = await db\n .select<{ id: number; pickedAt: string | Date | null; requestedAt: string | Date }>({ id: queue.id, pickedAt: queue.pickedAt, requestedAt: queue.requestedAt })\n .from(queue)\n .where(and(eq(queue.tenantId, tenantId), isNull(queue.finishedAt)))\n .orderBy(asc(queue.id))\n\n // ⚠️ 読めない時刻は「古い」側へ倒す。`NaN < date` は false なので、素直に比較すると\n // 読めない行がどちらの区分にも入らず、報告も解放もされないまま残る — しかも未取得なら\n // 重複抑止に居座って、以後の要求まで黙って捨てる。見えない方へ倒してはならない\n const before = (value: string | Date) => {\n const at = toInstant(value)\n return Number.isNaN(at.getTime()) || at < olderThan\n }\n return {\n stranded: rows.filter((r) => r.pickedAt !== null && before(r.pickedAt)).map((r) => Number(r.id)),\n unclaimed: rows.filter((r) => r.pickedAt === null && before(r.requestedAt)).map((r) => Number(r.id)),\n }\n },\n\n /**\n * Closes stranded requests as failed, so the queue can move again.\n * 取り残された要求を失敗として閉じ、キューを再び動かす処理。\n *\n * ⚠️ **掴んだ行を未処理へ戻さない。** 戻すと `UQ_..._pending` (未処理行の一意制約) に\n * 衝突し得る — 同じ範囲の新しい要求が既に並んでいれば、戻す操作そのものが失敗する。\n * 失敗として閉じれば、監査記録は残り、要求した人は結果 (失敗) を見られ、次の「今すぐ\n * 同期」は重複抑止に掛からない。\n *\n * ⚠️ **走ったかどうかは分からない。** 記録する理由も、成否ではなく「掴んだプロセスが\n * 終わりを書かなかった」という観測事実だけにする。推測を記録に混ぜない。\n *\n * ⚠️ 掴まれていない行 (`unclaimed`) には触れない。あれはジョブが落ちた跡ではなく\n * **担当ジョブが居ない** 跡であり、閉じると原因が消えて再発し続ける。\n *\n * @param olderThan - Requests picked before this are considered abandoned.\n * これより前に掴まれた要求を放棄と見なす締切時刻。\n * @returns The ids closed. 閉じた要求の ID。\n */\n async abandonStranded(olderThan: Date): Promise<number[]> {\n const { stranded } = await queueApi.listStranded(olderThan)\n const closed: number[] = []\n for (const id of stranded) {\n // ⚠️ 1 件の失敗で残りを諦めない。デッドロックの犠牲や接続断は 1 行だけに起きる。\n // まとめて中断すると、閉じられたはずの行が次の周期まで詰まったままになる\n try {\n await queueApi.finish(id, { result: \"failed\", error: STRANDED_REASON })\n closed.push(id)\n } catch (error) {\n opts.onCloseFailed?.(id, error)\n }\n }\n return closed\n },\n }\n return queueApi\n}\n","/**\n * @module sync-repository\n * @description Drizzle implementation of the storage operations one synchronization run performs.\n * 1 回の同期実行が行う記憶域操作の drizzle 実装。\n *\n * **削除は必ず認可側のロックアウトガードを通す。** 所属はグループ経由でロールに繋がるため、\n * 上流の 1 人の離脱がそのテナントの最後の管理者を奪いうる。判定と書込を分離すると、同時実行で\n * 双方が判定を通過して管理者を失うため、`guardedWrite` の直列化トランザクションの内側で書く。\n *\n * **1 件の拒否でグループ全体を止めない。** ガードが 1 人の削除を拒否したときに例外で打ち切ると、\n * その時点までの書込が commit 済みのまま残り、次回以降も同じ行で同じ位置で止まる。拒否された\n * 1 行がグループ全体の収束を永久に妨げるため、拒否は集約して報告し、残りの削除は続行する。\n */\nimport { normalizeAuthEmail } from \"@aiquants/auth-core\"\nimport type { DirectoryGroup, DirectoryMember, GroupSyncPlan, LocalGroupSnapshot, ResolvedDirectoryMember } from \"@aiquants/auth-directory-core\"\nimport { assertTenantId, assertTenantMatch } from \"@aiquants/authz-core\"\nimport type { AuthzTables, createAdminLockoutGuard } from \"@aiquants/authz-drizzle\"\nimport { and, eq, inArray, isNull, notInArray, or } from \"drizzle-orm\"\nimport type { DirectoryDb, EnsureConnection } from \"./db\"\nimport { assertMembershipMode } from \"./membership-mode\"\nimport type { AuthDirectoryTables } from \"./schema\"\nimport { chunk, chunkByParameters, MAX_IN_LIST, MAX_PARAMETERS, RESERVED_PARAMETERS } from \"./sql-limits\"\n\n/** Columns the ledger INSERT binds per row. 台帳 INSERT が 1 行あたり束ねる列数。 */\nconst LEDGER_INSERT_PARAMETERS = 7\n/** Columns the membership INSERT binds per row. 所属 INSERT が 1 行あたり束ねる列数。 */\nconst MEMBERSHIP_INSERT_PARAMETERS = 4\n\nimport { classifySyncFailure } from \"./sync-messages\"\nimport type { ApplyReport, DirectorySyncRepository, RemovalFailure, RemovalOutcome, SyncLink } from \"./sync-ports\"\n\n/**\n * Resolves upstream members against the host's identity tables.\n * 上流メンバーをホストの identity 表へ突合するポート。\n *\n * ホストごとに identity の持ち方が違う (このリポジトリでは `TDOpenid` → `TDUsers`) ため、\n * パッケージ側では表を名指しできない。突合順が **上流 ID 第一・アドレス第二** であることは\n * 実装側の裁量ではなく契約である — 再利用されたアドレスが他人の所属を継承する事故を防ぐ。\n *\n * ⚠️ 入力と同じ件数を返すこと。解決できないメンバーを省略すると、その人物は削除対象になる。\n */\nexport type MemberResolverPort = {\n resolve(members: readonly DirectoryMember[]): Promise<ResolvedDirectoryMember[]>\n}\n\n/** Options for the synchronization repository. 同期リポジトリの構成。 */\nexport type SyncRepositoryOptions = {\n ensureConnection?: EnsureConnection\n /**\n * Wall-clock supplier. 実行の時刻を供給する処理。\n *\n * ⚠️ 走査側 (`createDirectorySync`) は時計を注入されており、「1 回の実行の中で観測時刻と\n * 同期時刻が別々に進むと、どの行がどの実行で書かれたのか後から辿れなくなる」という理由が\n * 書いてある。記憶域側だけが `new Date()` を直に読むと、**同じ実行の中で 2 つの時計が進む** —\n * 失敗の記録と成功の記録が別の時刻を持ち、その理由は誰にも判らない。省略時は実時計。\n */\n now?: () => Date\n memberResolver: MemberResolverPort\n /** Lockout guard from `@aiquants/authz-drizzle`. 認可側のロックアウトガード。 */\n lockoutGuard: ReturnType<typeof createAdminLockoutGuard>\n /**\n * Actor recorded on rows the synchronization writes. 同期が書く行に記録する実行者。\n *\n * 空にすると、同期が作った行と手作業で入った行を監査上区別できなくなる。しかも更新系では\n * 最後に触れた人間の名前が残ったまま更新時刻だけ進み、監査が同期の変更を人間の仕業として\n * 記録することになる。\n */\n actor: string\n}\n\n/**\n * Tells a guard's decision apart from a driver failure. ガードの判断とドライバの失敗を見分ける処理。\n *\n * ⚠️ 判定できないものは **失敗側** へ倒す。拒否として記録すると人手を呼ぶ文言が出るため、\n * 確信が持てないなら「待てば直るかもしれない」側に置くほうが害が小さい。ドライバの例外は\n * 数値のエラー番号 (`number` / `code`) を持つので、それを手掛かりにする。\n *\n * @param error - What the guarded write threw. ガード付き書込が投げた値。\n * @returns True when the guard itself refused. ガード自身が断った場合に true。\n */\nfunction isRefusal(error: unknown): error is Error {\n if (!(error instanceof Error)) return false\n const driver = error as { number?: unknown; code?: unknown }\n return driver.number === undefined && driver.code === undefined && error.name === \"Error\"\n}\n\n/**\n * Creates the drizzle-backed synchronization repository.\n * drizzle を用いた同期リポジトリを生成する処理。\n *\n * @param dbUnknown Drizzle database instance. drizzle のデータベースインスタンス。\n * @param tables Identity-side tables. identity 側テーブル群。\n * @param authzTables Authorization tables (group master and real membership). 認可テーブル群。\n * @param opts Injected resolver, lockout guard and audit actor. 注入する構成。\n * @returns The repository. リポジトリ。\n */\nexport function createDirectorySyncRepository(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: SyncRepositoryOptions): DirectorySyncRepository {\n const db = dbUnknown as DirectoryDb\n const ready = opts.ensureConnection ?? (async () => {})\n const now = opts.now ?? (() => new Date())\n const { TDGroupExternalLink: link, TDGroupMemberEmail: ledger } = tables\n const { TMGroup: group, TDGroupMember: groupMember } = authzTables\n const actor = opts.actor\n\n return {\n async listSyncableLinks(scope) {\n await ready()\n const tenantId = assertTenantId(scope.tenantId, \"listSyncableLinks(tenantId)\")\n const conditions = [eq(link.isSyncEnabled, true), eq(link.tenantId, tenantId)]\n if (scope.groupId !== undefined) conditions.push(eq(link.groupId, scope.groupId))\n const rows = await db\n .select<Record<string, unknown>>({\n linkId: link.id,\n groupId: link.groupId,\n linkTenantId: link.tenantId,\n groupTenantId: group.tenantId,\n groupKey: group.groupKey,\n isGroupActive: group.isActive,\n provider: link.provider,\n externalId: link.externalId,\n externalKey: link.externalKey,\n membershipMode: link.membershipMode,\n vanishedObservations: link.vanishedObservations,\n })\n .from(link)\n // 無効化されたグループも返す。除外すると上流が復旧しても二度と読まれず、\n // 再有効化の機会が永久に失われる (無効化が一方通行の扉になる)\n // ⚠️ 結合にもテナントを載せる。述語だけで絞ると、リンク行の非正規化コピーが\n // 乖離した瞬間に他テナントのグループ行が結合され得る。範囲を 1 か所でしか\n // 守らない形にしない (下の `assertTenantMatch` は乖離の検出であって、\n // 乖離した行を結合させない仕組みではない)\n .innerJoin(group, and(eq(group.id, link.groupId), eq(group.tenantId, tenantId)))\n .where(and(...conditions))\n return rows.map((r): SyncLink => {\n // リンク行のテナントは親からの非正規化コピーである。乖離した行を素通しすると、\n // 名乗っていないテナントのグループを書き換えることになる\n assertTenantMatch(String(r.groupTenantId), String(r.linkTenantId), \"listSyncableLinks(link.tenantId vs TMGroup.tenant_id)\")\n return {\n linkId: Number(r.linkId),\n groupId: Number(r.groupId),\n groupKey: String(r.groupKey),\n tenantId: String(r.groupTenantId),\n provider: String(r.provider),\n externalId: String(r.externalId),\n externalKey: String(r.externalKey),\n // 既定へ倒さない。未知の値を direct と読むと、入れ子展開のつもりのリンクが\n // 直接所属だけを返し、入れ子側の全員が削除候補になる\n membershipMode: assertMembershipMode(String(r.membershipMode)),\n isGroupActive: Boolean(r.isGroupActive),\n vanishedObservations: Number(r.vanishedObservations ?? 0),\n }\n })\n },\n\n async loadGroupSnapshot(syncLink): Promise<LocalGroupSnapshot> {\n await ready()\n const [ledgerRows, memberRows] = await Promise.all([\n db\n .select<{ email: string; externalUserId: string | null; resolvedUserId: number | null }>({\n email: ledger.email,\n externalUserId: ledger.externalUserId,\n resolvedUserId: ledger.resolvedUserId,\n })\n .from(ledger)\n .where(eq(ledger.groupId, syncLink.groupId)),\n db.select<{ userId: number }>({ userId: groupMember.userId }).from(groupMember).where(eq(groupMember.groupId, syncLink.groupId)),\n ])\n return {\n ledger: ledgerRows.map((r) => ({\n email: String(r.email),\n ...(r.externalUserId ? { externalUserId: String(r.externalUserId) } : {}),\n resolvedUserId: r.resolvedUserId === null ? null : Number(r.resolvedUserId),\n })),\n // 実測値を渡す。台帳から導出すると、手作業で入った行や連携前の残骸を取り違える\n projectedUserIds: memberRows.map((r) => Number(r.userId)),\n }\n },\n\n resolveMembers(members) {\n return opts.memberResolver.resolve(members)\n },\n\n async applyPlan(syncLink: SyncLink, plan: GroupSyncPlan, observedAt: Date): Promise<ApplyReport> {\n assertTenantId(syncLink.tenantId, \"applyPlan(link.tenantId)\")\n await ready()\n\n // --- 追加系 ---------------------------------------------------------------\n // 所属を増やすだけなのでロックアウト判定を必要としない。1 トランザクションで書き切り、\n // 台帳だけ進んで所属が入らない中途半端な状態を残さない\n if (plan.ledgerUpserts.length > 0 || plan.projectionAdds.length > 0) {\n await db.transaction(async (tx) => {\n // 存在確認を 1 件ずつ発行すると、3,000 人のグループで 3,000 往復になる。\n // アドレスをまとめて引き、その結果で挿入と更新に振り分ける\n const upserts = plan.ledgerUpserts.map((m) => ({ ...m, email: normalizeAuthEmail(m.email) }))\n const existing = new Map<string, number>()\n for (const emails of chunk(upserts.map((m) => m.email))) {\n const rows = await tx\n .select<{ id: number; email: string }>({ id: ledger.id, email: ledger.email })\n .from(ledger)\n .where(and(eq(ledger.groupId, syncLink.groupId), inArray(ledger.email, emails)))\n for (const r of rows) existing.set(String(r.email), Number(r.id))\n }\n\n const inserts = upserts.filter((m) => !existing.has(m.email))\n // 1 行 7 列。行数で割ると 500 行 = 3500 パラメータになり、上限 2100 を越えて\n // 実行される前に拒否される (しかも再試行しても同じ場所で落ちる)\n for (const batch of chunkByParameters(inserts, LEDGER_INSERT_PARAMETERS)) {\n await tx\n .insert(ledger)\n .output()\n .values(batch.map((m) => ({ groupId: syncLink.groupId, email: m.email, externalUserId: m.externalUserId ?? null, resolvedUserId: m.resolvedUserId, observedAt, createdAt: observedAt, createdBy: actor })))\n }\n for (const m of upserts) {\n const id = existing.get(m.email)\n if (id === undefined) continue\n await tx\n .update(ledger)\n .set({ externalUserId: m.externalUserId ?? null, resolvedUserId: m.resolvedUserId, observedAt, updatedAt: observedAt, updatedBy: actor })\n .output()\n .where(eq(ledger.id, id))\n }\n\n // 所属 INSERT は 1 行 4 列。同じ batch を `IN` 述語にも使うため、狭いほうに合わせる\n for (const batch of chunkByParameters(plan.projectionAdds, MEMBERSHIP_INSERT_PARAMETERS)) {\n // 既に居る行を弾いてから挿入する。初回ログインの昇格と競合したとき、\n // 一意制約違反でグループ全体の同期を落とさないため\n const present = new Set(\n (\n await tx\n .select<{ userId: number }>({ userId: groupMember.userId })\n .from(groupMember)\n .where(and(eq(groupMember.groupId, syncLink.groupId), inArray(groupMember.userId, batch)))\n ).map((r) => Number(r.userId)),\n )\n const missing = batch.filter((id) => !present.has(id))\n if (missing.length > 0) {\n await tx\n .insert(groupMember)\n .output()\n .values(missing.map((userId) => ({ groupId: syncLink.groupId, userId, createdAt: observedAt, createdBy: actor })))\n }\n }\n })\n }\n\n // --- 削除系 ---------------------------------------------------------------\n // 1 件ずつロックアウトガードを通す。`AdminRemoval` は記述子を 1 つしか取らず、\n // 一括版を認可コアへ足すと境界が壊れる。件数は突合側のブレーカが抑えている\n const refusals: RemovalOutcome[] = []\n const failures: RemovalFailure[] = []\n for (const userId of plan.projectionRemoves) {\n try {\n await opts.lockoutGuard.guardedWrite(syncLink.tenantId, { removeGroupMembership: { groupId: syncLink.groupId, userId } }, \"remove-group-member\", (run) =>\n (run as unknown as DirectoryDb).delete(groupMember).where(and(eq(groupMember.groupId, syncLink.groupId), eq(groupMember.userId, userId))),\n )\n } catch (error) {\n // ガードの拒否で打ち切らない。打ち切ると、拒否された 1 行が以後の実行でも\n // 同じ位置で止まり、グループ全体が永久に収束しなくなる。\n // ⚠️ 例外をすべて「拒否」にしない。ガードは SERIALIZABLE で走るため、デッドロックの\n // 犠牲 (1205)・ロック待ちの時限切れ・接続断もここへ落ちてくる。それらを拒否として\n // 記録すると、運用者は「最後の管理者ガードが止めた」と読み、待てば直る事象に\n // 対して人を動かす — 逆に、人手が要る事象は待てば直ると読まれて放置される\n if (isRefusal(error)) refusals.push({ userId, applied: false, refusal: error.message })\n else failures.push({ userId, cause: classifySyncFailure(error) })\n }\n }\n\n // 台帳の削除は所属の削除より後に行う。逆順にすると、途中で失敗したときに\n // 「ログインできないのに所属だけ残る」状態になる。\n // ⚠️ 拒否された人物の台帳行は残す。所属を保ったままサインインだけ奪うと、権限は\n // 持っているのに入れないという、どちらの側から見ても説明のつかない状態になる\n const refusedUserIds = refusals.map((r) => r.userId)\n if (plan.ledgerDeletes.length > 0) {\n // ⚠️ この 1 文は `IN` を 2 つ描画する (アドレスと、残す人物の ID)。アドレス側だけを\n // 固定長で分割すると、拒否が多い実行では ID 側が毎回そのままの長さで乗り、上限を\n // 越えた文が **実行前に** 拒否される。しかも入力は決定的なので、そのグループの\n // 台帳は次の周期以降も同じ場所で失敗し続ける。枠から逆算する\n const emailChunkSize = Math.max(1, MAX_PARAMETERS - RESERVED_PARAMETERS - 1 - refusedUserIds.length)\n await db.transaction(async (tx) => {\n for (const batch of chunk(plan.ledgerDeletes, Math.min(MAX_IN_LIST, emailChunkSize))) {\n const keepRefused = refusedUserIds.length > 0 ? or(isNull(ledger.resolvedUserId), notInArray(ledger.resolvedUserId, refusedUserIds)) : undefined\n await tx.delete(ledger).where(and(eq(ledger.groupId, syncLink.groupId), inArray(ledger.email, batch), ...(keepRefused ? [keepRefused] : [])))\n }\n })\n }\n\n return { refusals, failures }\n },\n\n // ⚠️ リンク表への書き込みは ID だけで絞らない。同じファイルの `TMGroup` 側は既にテナントを\n // 束ねており、ここだけが片手落ちだった。`syncOne` は公開されているため、呼び出し側は\n // `listSyncableLinks` を通さずに組み立てた `SyncLink` を渡せる — その場合、リンクとテナントの\n // 対応は「読み取り時に JS で 1 度確かめた」だけで、書き込みの述語には現れない。とりわけ\n // `resolveExternalIdentity` は `external_id` を書き換えるので、他テナントのリンクを\n // 選んだ上流グループへ向け直せてしまう\n async resolveExternalIdentity(syncLink, upstreamGroup: DirectoryGroup) {\n await ready()\n // 既に上流の答えと一致しているなら書かない。毎周期 UPDATE を打つ意味は無い\n if (syncLink.externalId === upstreamGroup.externalId && syncLink.externalKey === normalizeAuthEmail(upstreamGroup.externalKey)) return\n // ⚠️ ここで一意制約に当たったら、同じ上流グループを別のリンクが既に持っている。\n // 例外はそのまま上へ返す — 所属を適用する前なので、二重の投影は起きていない\n await db\n .update(link)\n .set({ externalId: upstreamGroup.externalId, externalKey: normalizeAuthEmail(upstreamGroup.externalKey) })\n .output()\n .where(and(eq(link.id, syncLink.linkId), eq(link.tenantId, syncLink.tenantId)))\n },\n\n async recordSuccess(syncLink, upstreamGroup: DirectoryGroup, syncedAt) {\n await ready()\n await db.transaction(async (tx) => {\n await tx\n .update(link)\n // `external_id` も書き戻す。管理 UI は運用者が知っている値 (アドレス) でしか\n // 連携を作れないが、アドレスは上流で改名され得る。最初の成功時に安定 ID へ\n // 置き換えておかないと、改名された瞬間にグループが「消滅した」と判定され、\n // その全員がサインインも帰属証明も失う。自己修復の経路はここ 1 本だけである\n .set({ syncedAt, syncError: null, vanishedObservations: 0, externalId: upstreamGroup.externalId, externalKey: normalizeAuthEmail(upstreamGroup.externalKey), updatedAt: syncedAt, updatedBy: actor })\n .output()\n .where(and(eq(link.id, syncLink.linkId), eq(link.tenantId, syncLink.tenantId)))\n // 上流が戻っていれば再有効化する。無効化を一方通行にしないための唯一の経路\n if (!syncLink.isGroupActive) {\n await tx\n .update(group)\n .set({ isActive: true, updatedAt: syncedAt, updatedBy: actor })\n .output()\n .where(and(eq(group.id, syncLink.groupId), eq(group.tenantId, syncLink.tenantId)))\n }\n })\n },\n\n async recordFailure(syncLink, reason) {\n await ready()\n // `syncedAt` は進めない。失敗を成功時刻で覆い隠すと、陳腐化の警告が出なくなる\n await db\n .update(link)\n .set({ syncError: reason.slice(0, 1000), updatedAt: now(), updatedBy: actor })\n .output()\n .where(and(eq(link.id, syncLink.linkId), eq(link.tenantId, syncLink.tenantId)))\n },\n\n async recordVanishedObservation(syncLink, reason) {\n await ready()\n const next = syncLink.vanishedObservations + 1\n await db\n .update(link)\n .set({ vanishedObservations: next, syncError: reason.slice(0, 1000), updatedAt: now(), updatedBy: actor })\n .output()\n .where(and(eq(link.id, syncLink.linkId), eq(link.tenantId, syncLink.tenantId)))\n return next\n },\n\n async markGroupInactive(syncLink) {\n await ready()\n // 削除しない。削除するとロール割当 (運用者の付与意思) まで失われる。\n // ⚠️ 既に無効な行は対象から外す。外さないと、消えたままのグループを掃引のたびに\n // 書き直し、`updated_at` と `updated_by` が「最後に人が触った時刻」ではなく\n // 「最後に掃引が回った時刻」になる — 監査記録として読めなくなる。\n // 述語で絞るので、運用者が手で有効へ戻した場合は次の掃引が正しく戻す\n await db\n .update(group)\n .set({ isActive: false, updatedAt: now(), updatedBy: actor })\n .output()\n .where(and(eq(group.id, syncLink.groupId), eq(group.tenantId, syncLink.tenantId), eq(group.isActive, true)))\n },\n }\n}\n"],"mappings":"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","names":["index_exports","__export","AUTH_DIRECTORY_PROVIDER_COLUMN","AUTH_DIRECTORY_TENANT_SCOPED_TABLES","MAX_IN_LIST","MAX_PARAMETERS","MEMBERSHIP_MODES","NEWLY_UNHEALTHY_SYNC_OUTCOMES","RESERVED_PARAMETERS","STRANDED_REASON","UNHEALTHY_SYNC_OUTCOMES","assertGroupMayBeLinked","assertMembershipMode","chunk","chunkByParameters","classifySyncFailure","createDirectoryCatalogReader","createDirectoryCatalogWriter","createDirectoryLinkStore","createDirectorySync","createDirectorySyncRepository","createPromotionRepository","createSyncRequestQueue","defaultExternalGroupGuardMessage","defaultSyncMessages","defineAuthDirectorySchema","findLedgerRowsForIdentity","promoteLedgerForUser","toInstant","withExternalGroupGuards","__toCommonJS","import_authz_core","import_drizzle_orm","toInstant","value","MAX_PARAMETERS","RESERVED_PARAMETERS","MAX_IN_LIST","chunk","items","size","out","i","chunkByParameters","parametersPerItem","reservedParameters","budget","PARAMETERS_PER_ROW","LIMITS","MAX_MEMBER_COUNT","toStorableMemberCount","value","whole","assertScopeKey","limit","name","trimmed","createDirectoryCatalogWriter","dbUnknown","tables","opts","db","ready","catalog","observation","dedupe","rows","byId","row","tenantId","provider","entries","tenant","upstream","writer","observedAt","entry","tx","batch","chunkByParameters","createDirectoryCatalogReader","groups","mark","toInstant","import_authz_core","defaultExternalGroupGuardMessage","roleKey","groupKey","adminActionsOf","grants","roleIds","adminAppKey","adminResourceKey","g","rolesOfGroup","assignments","groupId","a","confersAdministration","actions","roleLabel","roleId","culpritLabel","alone","id","withExternalGroupGuards","store","opts","decorate","target","plain","fn","options","tx","runner","tenantId","actor","after","externalKey","input","nextAction","resource","r","projected","assertGroupMayBeLinked","first","import_drizzle_orm","findLedgerRowsForIdentity","db","tables","args","ledger","group","tenantId","externalUserId","email","project","probe","condition","r","finalize","rows","byId","a","b","byEmail","claimedByAnother","row","import_auth_core","import_auth_directory_core","import_authz_core","import_drizzle_orm","MEMBERSHIP_MODES","assertMembershipMode","value","isUniqueViolation","error","code","createDirectoryLinkStore","dbUnknown","tables","authzTables","opts","db","ready","policyCacheTtlMs","policyCache","forgetPolicy","tenantId","link","ledger","membership","allowGroup","queue","group","groupMember","assertNotBreakGlass","runner","groupId","assertGroupInTenant","owned","toLinkRow","row","unresolvedCount","assertMembershipMode","toInstant","unresolvedCounts","rows","r","counts","input","actor","membershipMode","externalId","externalKey","tx","isSyncEnabled","requestedBy","pending","enabled","stamps","oldest","t","isStale","a","b","memo","empty","proofGroupIds","breakGlass","policy","isBreakGlass","linked","existing","remaining","isActive","identity","findLedgerRowsForIdentity","email","normalized","description","id","import_auth_core","import_authz_core","promoteLedgerForUser","repository","args","tenantId","email","externalUserId","matches","match","ledgerIds","m","b","groupIds","import_authz_core","import_drizzle_orm","MEMBERSHIP_INSERT_PARAMETERS","RESERVED_FOR_MARKING","createPromotionRepository","dbUnknown","tables","authzTables","opts","db","ready","now","ledger","group","groupMember","args","findLedgerRowsForIdentity","tenantId","userId","ledgerIds","groupIds","tx","owned","part","chunk","rows","r","targets","id","existing","missing","ledgerChunkSize","createdAt","chunkByParameters","groupId","import_authz_drizzle","import_mssql_core","AUTH_DIRECTORY_PROVIDER_COLUMN","AUTH_DIRECTORY_TENANT_SCOPED_TABLES","defineAuthDirectorySchema","schemaName","opts","s","audit","insertOnlyAudit","TDGroupExternalLink","t","TDGroupMemberEmail","TDTenantMembershipGroup","TDAllowlistGroup","TDDirectorySyncRequest","TMAllowlist","TDDirectoryCatalog","TMDirectoryCatalogObservation","import_auth_directory_core","classifySyncFailure","error","status","countOf","items","causeText","defaultSyncMessages","cause","upstream","resolved","externalKey","observations","threshold","abort","axis","withheld","failures","f","refusals","r","linkProvider","runnerProvider","NEWLY_UNHEALTHY_SYNC_OUTCOMES","UNHEALTHY_SYNC_OUTCOMES","createDirectorySync","config","directory","repository","policy","now","logger","messages","defaultSyncMessages","noteFailure","link","reason","error","readUpstream","group","handleMissing","observations","syncOne","upstream","classifySyncFailure","snapshot","resolved","plan","withheld","hasWork","stamp","report","r","f","err","scope","links","outcomes","unhealthyCount","o","UNHEALTHY_SYNC_OUTCOMES","newlyUnhealthyCount","NEWLY_UNHEALTHY_SYNC_OUTCOMES","import_authz_core","import_drizzle_orm","STRANDED_REASON","ERROR_COLUMN_LENGTH","createSyncRequestQueue","dbUnknown","tables","opts","db","ready","queue","tenantId","maxErrorLength","queueApi","pending","candidate","row","rawGroupId","id","outcome","error","olderThan","rows","before","value","at","toInstant","r","stranded","closed","import_auth_core","import_authz_core","import_drizzle_orm","LEDGER_INSERT_PARAMETERS","MEMBERSHIP_INSERT_PARAMETERS","isRefusal","error","driver","createDirectorySyncRepository","dbUnknown","tables","authzTables","opts","db","ready","now","link","ledger","group","groupMember","actor","scope","tenantId","conditions","r","assertMembershipMode","syncLink","ledgerRows","memberRows","members","plan","observedAt","tx","upserts","m","existing","emails","chunk","rows","inserts","batch","chunkByParameters","id","present","missing","userId","refusals","failures","run","classifySyncFailure","refusedUserIds","emailChunkSize","keepRefused","upstreamGroup","syncedAt","reason","next"]}