@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.d.mts CHANGED
@@ -1,8 +1,8 @@
1
- import { AuthzAdminStore } from '@aiquants/authz-core';
2
- import { AuthzTables, createAdminLockoutGuard } from '@aiquants/authz-drizzle';
1
+ import { AuthDirectoryCatalogGroup, AuthDirectoryCatalogSnapshot, AuthGroupExternalLink, AuthDirectoryMembershipMode, AuthDirectoryStatus, AuthAllowlistEntry } from '@aiquants/auth-core';
3
2
  import * as drizzle_orm_mssql_core from 'drizzle-orm/mssql-core';
4
3
  import { AnyMsSqlColumn } from 'drizzle-orm/mssql-core';
5
- import { AuthGroupExternalLink, AuthDirectoryMembershipMode, AuthDirectoryStatus, AuthAllowlistEntry } from '@aiquants/auth-core';
4
+ import { AuthzAdminStore } from '@aiquants/authz-core';
5
+ import { AuthzTables, createAdminLockoutGuard } from '@aiquants/authz-drizzle';
6
6
  import { TenantMembershipPolicy, SyncAbort, AuthDirectoryMembershipMode as AuthDirectoryMembershipMode$1, LocalGroupSnapshot, DirectoryMember, ResolvedDirectoryMember, GroupSyncPlan, DirectoryGroup, DirectoryProvider, BlastRadiusPolicy } from '@aiquants/auth-directory-core';
7
7
 
8
8
  /**
@@ -45,103 +45,19 @@ interface DirectoryDb {
45
45
  }
46
46
  /** Ensures a connection exists before the first statement. 最初の文の前に接続を確立する処理。 */
47
47
  type EnsureConnection = () => Promise<void>;
48
-
49
- /**
50
- * @module guards
51
- * @description Keeps administrative roles off groups whose membership an upstream directory owns.
52
- * 所属の所有者が上流ディレクトリにあるグループへ、管理ロールが付かないようにする。
53
- *
54
- * **不変条件は状態の性質であって、遷移の性質ではない。**
55
- *
56
- * 最初の実装は `assignGroupRole` だけを差し替えていた。しかしそれは「外部グループへ管理ロールを
57
- * 割り当てる」という 1 つの遷移を塞いだにすぎず、**無害なロールを外部グループへ割り当ててから、
58
- * そのロールを管理ロールへ昇格させる**という順序が開いたままだった。不変条件を守るには、それを
59
- * 崩し得るすべての遷移を塞ぐ必要がある。
60
- *
61
- * 崩し得る遷移は 3 つある。
62
- *
63
- * | # | 遷移 | 塞ぐもの |
64
- * | --- | --- | --- |
65
- * | 1 | 外部グループが管理ロールを得る | `assignGroupRole` |
66
- * | 2 | 外部グループが持つロールが管理ロールになる | `createGrant` |
67
- * | 3 | 管理ロールを持つグループが外部連携される | `assertGroupMayBeLinked` |
68
- *
69
- * `updateGrant` は塞ぐ必要がない。`(role_id, resource_id, action)` が一意であるため、管理リソースの
70
- * 4 アクションを揃えるには 4 行の付与が要り、既に 4 行あるロールは既に管理ロールである。既存行の
71
- * アクションを差し替えても集合の要素数は増えないので、`updateGrant` は管理権を **作れない**。
72
- *
73
- * `setRoleActive` も塞ぐ必要がない。遷移 1 の判定はロールの有効・無効を見ないため、無効な管理
74
- * ロールも外部グループへは割り当てられない。したがって「無効な管理ロールを外部グループが保持した
75
- * まま有効化される」という状態には到達し得ない。
76
- *
77
- * **なぜ認可パッケージの中で判定しないのか。** 判定を `@aiquants/authz-drizzle` の管理ストアへ
78
- * 埋め込むと、認可パッケージがグループの「出所」という identity 側の概念を知ることになる。依存の
79
- * 向きは auth → authz の一方通行で保ちたいので、包む側 (identity 側) から課す。
80
- *
81
- * **なぜ禁止するのか。** 外部グループの所属は上流の管理者が変更できる。そこへ管理ロールを付けると、
82
- * 上流側の 1 操作がこの配備の管理者を増やしたり、逆に最後の管理者を奪ったりする。ブレークグラス
83
- * 経路は常にローカルに残しておく必要がある。
84
- */
85
-
86
- /** The link lookup this guard needs. 本ガードが必要とするリンク参照。 */
87
- type ExternalLinkLookup = {
88
- /**
89
- * The upstream key of the group, or `null` when its membership is locally managed.
90
- * そのグループの上流キー。ローカル管理なら `null`。
91
- *
92
- * 「外部かどうか」と「拒否メッセージに出す名前」を 1 回の照会で返す。判定のためだけに
93
- * 引いて、名前のためにもう一度引く形にすると、拒否のたびに往復が 2 倍になる。
94
- *
95
- * ⚠️ `runner` を渡された実装は、**必ずその上で** 引くこと。別接続で引くと、この読み取りの
96
- * 結果を前提に書き込む呼び出し側にとって前提が保たれない (読んだ後・書く前に連携が作られる)。
97
- */
98
- externalGroupKey(tenantId: string, groupId: number, runner?: unknown): Promise<string | null>;
99
- };
100
- /** Configuration of the guard. ガードの構成。 */
101
- type ExternalGroupGuardOptions = {
102
- linkLookup: ExternalLinkLookup;
103
- /** App key the administrative resource belongs to. 管理リソースが属するアプリキー。 */
104
- adminAppKey: string;
105
- /** Resource key that grants administration. 管理を与えるリソースキー。 */
106
- adminResourceKey: string;
107
- /**
108
- * Refusal message. 拒否メッセージ。
109
- *
110
- * @param roleKey Role that would have conferred administration. 管理権を与えることになるロール。
111
- * @param groupKey Group that would have held it. それを保持することになるグループ。
112
- */
113
- message: (roleKey: string, groupKey: string) => string;
114
- };
115
- /** Default refusal message (Japanese, matching the admin UI's locale). 既定の拒否メッセージ。 */
116
- declare const defaultExternalGroupGuardMessage: (roleKey: string, groupKey: string) => string;
117
- /**
118
- * Wraps an authorization admin store so administrative power cannot reach an external group.
119
- * 外部グループへ管理権が届かないよう認可管理ストアを包む処理。
120
- *
121
- * `assignGroupRole` と `createGrant` を差し替え、残りは透過的に委譲する。3 番目の方向
122
- * (連携の開始) は {@link assertGroupMayBeLinked} が担い、リンクを作る側が必ず通す。
123
- *
124
- * @param store Store to wrap. 包む対象のストア。
125
- * @param opts Link lookup and administrative resource identity. リンク参照と管理リソースの同定。
126
- * @returns A store with the same surface. 同一表面のストア。
127
- */
128
- declare function withExternalGroupGuards<R>(store: AuthzAdminStore<R>, opts: ExternalGroupGuardOptions): AuthzAdminStore<R>;
129
48
  /**
130
- * Refuses to link a group that already holds an administrative role.
131
- * 既に管理ロールを保持しているグループの外部連携を拒否する処理。
49
+ * Reads a driver timestamp as an instant, treating an offset-less string as UTC.
50
+ * ドライバの時刻を瞬間として読む処理 (オフセットの無い文字列は UTC として解釈)。
132
51
  *
133
- * 3 つ目の方向。これを通さずにリンクを作ると、ローカルで管理ロールを付けてから連携させる経路が
134
- * 残る。{@link withExternalGroupGuards} と違って呼び出し側が忘れ得るため、リンクを作る側の
135
- * 構成に必須ポートとして組み込むこと。
52
+ * ⚠️ オフセットを持たない ISO 文字列 (`2026-09-09 03:04:05.000`) は仕様上 **実行環境の時間帯**
53
+ * で解釈される。ドライバは通常 `Date` を返すのでこの経路は普段通らないが、通った日に JST なら
54
+ * 9 時間ずれる — 鮮度は「常に古い」と報告され、締切の比較は 9 時間ぶん甘くなる。読む場所ごとに
55
+ * 対処すると必ずどこかが漏れるので、1 つの関数に閉じる。
136
56
  *
137
- * @param store Authorization admin store used to read the grants. 付与を読む認可管理ストア。
138
- * @param tenantId Tenant the group belongs to. グループが属するテナント。
139
- * @param groupId Group about to be linked. 連携しようとしているグループ。
140
- * @param opts Administrative resource identity and refusal message. 管理リソースの同定と文言。
141
- * @returns Nothing; returns normally only when linking is allowed. 許可される場合のみ正常復帰。
142
- * @throws {Error} When the group holds the administrative role. 管理ロールを保持している場合。
57
+ * @param value - What the driver returned. ドライバが返した値。
58
+ * @returns The instant. その瞬間。
143
59
  */
144
- declare function assertGroupMayBeLinked(store: AuthzAdminStore, tenantId: string, groupId: number, opts: Pick<ExternalGroupGuardOptions, "adminAppKey" | "adminResourceKey" | "message">): Promise<void>;
60
+ declare function toInstant(value: string | Date): Date;
145
61
 
146
62
  /** Minimal shape of a table this schema points a foreign key at. 外部キーの参照先に要る最小形。 */
147
63
  type ReferencedTable = {
@@ -158,7 +74,7 @@ declare const AUTH_DIRECTORY_PROVIDER_COLUMN = "provider";
158
74
  *
159
75
  * `TDGroupMemberEmail` が含まれないのは、親グループからテナントを継承するためである。
160
76
  */
161
- declare const AUTH_DIRECTORY_TENANT_SCOPED_TABLES: readonly ["TDGroupExternalLink", "TDTenantMembershipGroup", "TDAllowlistGroup", "TDDirectorySyncRequest", "TMAllowlist"];
77
+ declare const AUTH_DIRECTORY_TENANT_SCOPED_TABLES: readonly ["TDGroupExternalLink", "TDTenantMembershipGroup", "TDAllowlistGroup", "TDDirectorySyncRequest", "TDDirectoryCatalog", "TMDirectoryCatalogObservation", "TMAllowlist"];
162
78
  /**
163
79
  * Builds the identity-side tables under an arbitrary schema name.
164
80
  * 任意のスキーマ名の下に identity 側テーブル群を構築する処理。
@@ -958,13 +874,13 @@ declare function defineAuthDirectorySchema<S extends string>(schemaName: S, opts
958
874
  };
959
875
  dialect: "mssql";
960
876
  }>;
961
- TMAllowlist: drizzle_orm_mssql_core.MsSqlTableWithColumns<{
962
- name: "TMAllowlist";
877
+ TDDirectoryCatalog: drizzle_orm_mssql_core.MsSqlTableWithColumns<{
878
+ name: "TDDirectoryCatalog";
963
879
  schema: S;
964
880
  columns: {
965
881
  createdAt: drizzle_orm_mssql_core.MsSqlColumn<{
966
882
  name: string;
967
- tableName: "TMAllowlist";
883
+ tableName: "TDDirectoryCatalog";
968
884
  dataType: "object date";
969
885
  data: Date;
970
886
  driverParam: string | Date;
@@ -980,7 +896,7 @@ declare function defineAuthDirectorySchema<S extends string>(schemaName: S, opts
980
896
  }, {}>;
981
897
  createdBy: drizzle_orm_mssql_core.MsSqlColumn<{
982
898
  name: string;
983
- tableName: "TMAllowlist";
899
+ tableName: "TDDirectoryCatalog";
984
900
  dataType: "string";
985
901
  data: string;
986
902
  driverParam: string | number;
@@ -996,7 +912,7 @@ declare function defineAuthDirectorySchema<S extends string>(schemaName: S, opts
996
912
  }, {}>;
997
913
  id: drizzle_orm_mssql_core.MsSqlColumn<{
998
914
  name: string;
999
- tableName: "TMAllowlist";
915
+ tableName: "TDDirectoryCatalog";
1000
916
  dataType: "number int53";
1001
917
  data: number;
1002
918
  driverParam: string;
@@ -1012,7 +928,7 @@ declare function defineAuthDirectorySchema<S extends string>(schemaName: S, opts
1012
928
  }, {}>;
1013
929
  tenantId: drizzle_orm_mssql_core.MsSqlColumn<{
1014
930
  name: string;
1015
- tableName: "TMAllowlist";
931
+ tableName: "TDDirectoryCatalog";
1016
932
  dataType: "string";
1017
933
  data: string;
1018
934
  driverParam: string | number;
@@ -1026,9 +942,9 @@ declare function defineAuthDirectorySchema<S extends string>(schemaName: S, opts
1026
942
  identity: undefined;
1027
943
  generated: undefined;
1028
944
  }, {}>;
1029
- pattern: drizzle_orm_mssql_core.MsSqlColumn<{
945
+ provider: drizzle_orm_mssql_core.MsSqlColumn<{
1030
946
  name: string;
1031
- tableName: "TMAllowlist";
947
+ tableName: "TDDirectoryCatalog";
1032
948
  dataType: "string";
1033
949
  data: string;
1034
950
  driverParam: string | number;
@@ -1042,9 +958,41 @@ declare function defineAuthDirectorySchema<S extends string>(schemaName: S, opts
1042
958
  identity: undefined;
1043
959
  generated: undefined;
1044
960
  }, {}>;
1045
- type: drizzle_orm_mssql_core.MsSqlColumn<{
961
+ externalId: drizzle_orm_mssql_core.MsSqlColumn<{
1046
962
  name: string;
1047
- tableName: "TMAllowlist";
963
+ tableName: "TDDirectoryCatalog";
964
+ dataType: "string";
965
+ data: string;
966
+ driverParam: string | number;
967
+ notNull: true;
968
+ hasDefault: false;
969
+ isPrimaryKey: false;
970
+ isAutoincrement: false;
971
+ hasRuntimeDefault: false;
972
+ enumValues: [string, ...string[]];
973
+ baseColumn: never;
974
+ identity: undefined;
975
+ generated: undefined;
976
+ }, {}>;
977
+ externalKey: drizzle_orm_mssql_core.MsSqlColumn<{
978
+ name: string;
979
+ tableName: "TDDirectoryCatalog";
980
+ dataType: "string";
981
+ data: string;
982
+ driverParam: string | number;
983
+ notNull: true;
984
+ hasDefault: false;
985
+ isPrimaryKey: false;
986
+ isAutoincrement: false;
987
+ hasRuntimeDefault: false;
988
+ enumValues: [string, ...string[]];
989
+ baseColumn: never;
990
+ identity: undefined;
991
+ generated: undefined;
992
+ }, {}>;
993
+ name: drizzle_orm_mssql_core.MsSqlColumn<{
994
+ name: string;
995
+ tableName: "TDDirectoryCatalog";
1048
996
  dataType: "string";
1049
997
  data: string;
1050
998
  driverParam: string | number;
@@ -1060,7 +1008,7 @@ declare function defineAuthDirectorySchema<S extends string>(schemaName: S, opts
1060
1008
  }, {}>;
1061
1009
  description: drizzle_orm_mssql_core.MsSqlColumn<{
1062
1010
  name: string;
1063
- tableName: "TMAllowlist";
1011
+ tableName: "TDDirectoryCatalog";
1064
1012
  dataType: "string";
1065
1013
  data: string;
1066
1014
  driverParam: string | number;
@@ -1074,100 +1022,601 @@ declare function defineAuthDirectorySchema<S extends string>(schemaName: S, opts
1074
1022
  identity: undefined;
1075
1023
  generated: undefined;
1076
1024
  }, {}>;
1025
+ memberCount: drizzle_orm_mssql_core.MsSqlColumn<{
1026
+ name: string;
1027
+ tableName: "TDDirectoryCatalog";
1028
+ dataType: "number int32";
1029
+ data: number;
1030
+ driverParam: number;
1031
+ notNull: false;
1032
+ hasDefault: false;
1033
+ isPrimaryKey: false;
1034
+ isAutoincrement: false;
1035
+ hasRuntimeDefault: false;
1036
+ enumValues: undefined;
1037
+ baseColumn: never;
1038
+ identity: undefined;
1039
+ generated: undefined;
1040
+ }, {}>;
1041
+ observedAt: drizzle_orm_mssql_core.MsSqlColumn<{
1042
+ name: string;
1043
+ tableName: "TDDirectoryCatalog";
1044
+ dataType: "object date";
1045
+ data: Date;
1046
+ driverParam: string | Date;
1047
+ notNull: true;
1048
+ hasDefault: false;
1049
+ isPrimaryKey: false;
1050
+ isAutoincrement: false;
1051
+ hasRuntimeDefault: false;
1052
+ enumValues: undefined;
1053
+ baseColumn: never;
1054
+ identity: undefined;
1055
+ generated: undefined;
1056
+ }, {}>;
1077
1057
  };
1078
1058
  dialect: "mssql";
1079
1059
  }>;
1080
- };
1081
- /** Explicit alias for the tables `defineAuthDirectorySchema` returns (avoids TS2742). 明示型。 */
1082
- type AuthDirectoryTables = {
1083
- TDGroupExternalLink: ReturnType<typeof defineAuthDirectorySchema>["TDGroupExternalLink"];
1084
- TDGroupMemberEmail: ReturnType<typeof defineAuthDirectorySchema>["TDGroupMemberEmail"];
1085
- TDTenantMembershipGroup: ReturnType<typeof defineAuthDirectorySchema>["TDTenantMembershipGroup"];
1086
- TDAllowlistGroup: ReturnType<typeof defineAuthDirectorySchema>["TDAllowlistGroup"];
1087
- TDDirectorySyncRequest: ReturnType<typeof defineAuthDirectorySchema>["TDDirectorySyncRequest"];
1088
- TMAllowlist: ReturnType<typeof defineAuthDirectorySchema>["TMAllowlist"];
1089
- };
1090
-
1091
- /**
1092
- * Resolves which member-ledger rows belong to one identity.
1093
- * 台帳のどの行が 1 人の identity のものかを解決するモジュール。
1094
- *
1095
- * 台帳 (`TDGroupMemberEmail`) は「上流に居る人」を、ローカルユーザーへ解決できているかに
1096
- * 関わらず保持する。ここはその台帳から本人の行だけを取り出す唯一の場所であり、帰属証明
1097
- * (`listLedgerGroupIds`) と初回ログイン時の昇格 (`promoteLedgerForUser`) は同じ答えを見る。
1098
- * 突合規則が 2 箇所にあると、片方だけが「本人」と認めた瞬間に権限が食い違う。
1099
- */
1100
-
1101
- /** One ledger row attributed to an identity. identity に帰属した台帳 1 行。 */
1102
- type LedgerIdentityRow = {
1103
- ledgerId: number;
1104
- groupId: number;
1105
- /** Inherited from the parent group — the ledger has no tenant column. 親グループ由来。 */
1106
- tenantId: string;
1107
- };
1108
- /** The two tables this resolution spans. この解決が跨る 2 表。 */
1109
- type LedgerIdentityTables = {
1110
- /** `TDGroupMemberEmail`. */
1111
- ledger: AuthDirectoryTables["TDGroupMemberEmail"];
1112
- /** `TMGroup` — owns the tenant and the active flag. テナントと有効フラグの所有者。 */
1113
- group: AuthzTables["TMGroup"];
1114
- };
1115
- /**
1116
- * Finds every ledger row that belongs to the identity, within one tenant.
1117
- * 1 テナント内で、その identity に属する台帳行をすべて探す処理。
1118
- *
1119
- * 上流 ID の一致は無条件に本人。アドレスの一致は、その行が **別の** 上流 ID に紐付いていない
1120
- * ときだけ本人である。単純な `OR` にすると後者の条件が消え、退職者のアドレスを再利用した
1121
- * 新入社員が前任者の所属をそのまま継承する。
1122
- *
1123
- * ⚠️ この優先順位は **行ごと** に適用する。「ID で当たったらアドレスは見ない」という打ち切りは
1124
- * 誤りで、同じ人物の行が ID 付きと ID 無しに混在すると ID 無しの行が丸ごと落ちる。
1125
- *
1126
- * @param db - Connected database surface. 接続済みの DB 面。
1127
- * @param tables - The ledger and its parent group table. 台帳と親グループ表。
1128
- * @param args - Tenant scope and the identity to match. テナント範囲と突合対象の identity。
1129
- * @returns Matching rows, deduplicated and ordered by ledger id. 重複排除・台帳 ID 昇順の一致行。
1130
- */
1131
- declare function findLedgerRowsForIdentity(db: DirectoryDb, tables: LedgerIdentityTables, args: {
1132
- tenantId: string;
1133
- externalUserId: string | null;
1134
- email: string;
1135
- }): Promise<LedgerIdentityRow[]>;
1136
-
1137
- /**
1138
- * @module link-store
1139
- * @description Reads and writes the identity-side tables: external group links, the upstream member
1140
- * ledger, tenant membership proof, group-based login allowlist, and the sync request queue.
1141
- * identity 側テーブルの読み書き。外部グループリンク・上流メンバー台帳・テナント帰属証明・
1142
- * グループ単位のログイン許可・同期要求キュー。
1143
- *
1144
- * **テナントの扱いは 2 種類ある。混同すると事故になる。**
1145
- *
1146
- * - **絞り込み条件**: リンク・帰属証明グループ。読み書きの双方で SQL 述語になる。
1147
- * - **所有者**: グループ許可。管理 UI の編集スコープを決めるだけで、**ログイン判定では
1148
- * 述語にしない** — 認証はテナントの確定より前に起きるため、判定の時点でテナントは存在しない。
1149
- * このモジュールでは {@link createDirectoryLinkStore} の `isEmailAllowedByGroup` だけが
1150
- * その例外であり、意図的に全テナントを対象とする。
1151
- */
1152
-
1153
- /**
1154
- * Proves a group may become externally sourced. グループを外部供給にしてよいことの証明。
1155
- *
1156
- * `assertGroupMayBeLinked` を呼ぶだけのポートである。自由関数のままだと呼び出し側が忘れられ、
1157
- * 「ローカルで管理ロールを付けてから連携させる」経路が残る。必須の構成項目にすることで、
1158
- * 忘れることを構造的に不可能にする。
1159
- */
1160
- type LinkableAssertion = {
1161
- /**
1162
- * Owns a transaction, proves the group may be linked on it, then runs `write` on the same runner.
1163
- * トランザクションを所有し、その上で連携可能であることを証明してから、同じランナーで `write` を実行する処理。
1164
- *
1165
- * ⚠️ **証明だけを返す形にしてはならない。** 証明と連携の作成が別のトランザクションだと、
1166
- * 証明を通った直後・リンクを作る前に、そのグループへ管理ロールを割り当てる操作が
1167
- * 割り込める。割り当て側の検査は「まだリンクが無い」と見て通り、連携側の検査は
1168
- * 「まだ管理ロールが無い」と見て通る — **どちらも単独では正しく、結果だけが不変条件を破る**。
1169
- *
1170
- * @param tenantId - Tenant that owns the group. グループを所有するテナント。
1060
+ TMDirectoryCatalogObservation: drizzle_orm_mssql_core.MsSqlTableWithColumns<{
1061
+ name: "TMDirectoryCatalogObservation";
1062
+ schema: S;
1063
+ columns: {
1064
+ createdAt: drizzle_orm_mssql_core.MsSqlColumn<{
1065
+ name: string;
1066
+ tableName: "TMDirectoryCatalogObservation";
1067
+ dataType: "object date";
1068
+ data: Date;
1069
+ driverParam: string | Date;
1070
+ notNull: false;
1071
+ hasDefault: false;
1072
+ isPrimaryKey: false;
1073
+ isAutoincrement: false;
1074
+ hasRuntimeDefault: false;
1075
+ enumValues: undefined;
1076
+ baseColumn: never;
1077
+ identity: undefined;
1078
+ generated: undefined;
1079
+ }, {}>;
1080
+ createdBy: drizzle_orm_mssql_core.MsSqlColumn<{
1081
+ name: string;
1082
+ tableName: "TMDirectoryCatalogObservation";
1083
+ dataType: "string";
1084
+ data: string;
1085
+ driverParam: string | number;
1086
+ notNull: false;
1087
+ hasDefault: false;
1088
+ isPrimaryKey: false;
1089
+ isAutoincrement: false;
1090
+ hasRuntimeDefault: false;
1091
+ enumValues: [string, ...string[]];
1092
+ baseColumn: never;
1093
+ identity: undefined;
1094
+ generated: undefined;
1095
+ }, {}>;
1096
+ updatedAt: drizzle_orm_mssql_core.MsSqlColumn<{
1097
+ name: string;
1098
+ tableName: "TMDirectoryCatalogObservation";
1099
+ dataType: "object date";
1100
+ data: Date;
1101
+ driverParam: string | Date;
1102
+ notNull: false;
1103
+ hasDefault: false;
1104
+ isPrimaryKey: false;
1105
+ isAutoincrement: false;
1106
+ hasRuntimeDefault: false;
1107
+ enumValues: undefined;
1108
+ baseColumn: never;
1109
+ identity: undefined;
1110
+ generated: undefined;
1111
+ }, {}>;
1112
+ updatedBy: drizzle_orm_mssql_core.MsSqlColumn<{
1113
+ name: string;
1114
+ tableName: "TMDirectoryCatalogObservation";
1115
+ dataType: "string";
1116
+ data: string;
1117
+ driverParam: string | number;
1118
+ notNull: false;
1119
+ hasDefault: false;
1120
+ isPrimaryKey: false;
1121
+ isAutoincrement: false;
1122
+ hasRuntimeDefault: false;
1123
+ enumValues: [string, ...string[]];
1124
+ baseColumn: never;
1125
+ identity: undefined;
1126
+ generated: undefined;
1127
+ }, {}>;
1128
+ id: drizzle_orm_mssql_core.MsSqlColumn<{
1129
+ name: string;
1130
+ tableName: "TMDirectoryCatalogObservation";
1131
+ dataType: "number int53";
1132
+ data: number;
1133
+ driverParam: string;
1134
+ notNull: true;
1135
+ hasDefault: true;
1136
+ isPrimaryKey: false;
1137
+ isAutoincrement: false;
1138
+ hasRuntimeDefault: false;
1139
+ enumValues: undefined;
1140
+ baseColumn: never;
1141
+ identity: undefined;
1142
+ generated: {};
1143
+ }, {}>;
1144
+ tenantId: drizzle_orm_mssql_core.MsSqlColumn<{
1145
+ name: string;
1146
+ tableName: "TMDirectoryCatalogObservation";
1147
+ dataType: "string";
1148
+ data: string;
1149
+ driverParam: string | number;
1150
+ notNull: true;
1151
+ hasDefault: false;
1152
+ isPrimaryKey: false;
1153
+ isAutoincrement: false;
1154
+ hasRuntimeDefault: false;
1155
+ enumValues: [string, ...string[]];
1156
+ baseColumn: never;
1157
+ identity: undefined;
1158
+ generated: undefined;
1159
+ }, {}>;
1160
+ provider: drizzle_orm_mssql_core.MsSqlColumn<{
1161
+ name: string;
1162
+ tableName: "TMDirectoryCatalogObservation";
1163
+ dataType: "string";
1164
+ data: string;
1165
+ driverParam: string | number;
1166
+ notNull: true;
1167
+ hasDefault: false;
1168
+ isPrimaryKey: false;
1169
+ isAutoincrement: false;
1170
+ hasRuntimeDefault: false;
1171
+ enumValues: [string, ...string[]];
1172
+ baseColumn: never;
1173
+ identity: undefined;
1174
+ generated: undefined;
1175
+ }, {}>;
1176
+ observedAt: drizzle_orm_mssql_core.MsSqlColumn<{
1177
+ name: string;
1178
+ tableName: "TMDirectoryCatalogObservation";
1179
+ dataType: "object date";
1180
+ data: Date;
1181
+ driverParam: string | Date;
1182
+ notNull: true;
1183
+ hasDefault: false;
1184
+ isPrimaryKey: false;
1185
+ isAutoincrement: false;
1186
+ hasRuntimeDefault: false;
1187
+ enumValues: undefined;
1188
+ baseColumn: never;
1189
+ identity: undefined;
1190
+ generated: undefined;
1191
+ }, {}>;
1192
+ entryCount: drizzle_orm_mssql_core.MsSqlColumn<{
1193
+ name: string;
1194
+ tableName: "TMDirectoryCatalogObservation";
1195
+ dataType: "number int32";
1196
+ data: number;
1197
+ driverParam: number;
1198
+ notNull: true;
1199
+ hasDefault: false;
1200
+ isPrimaryKey: false;
1201
+ isAutoincrement: false;
1202
+ hasRuntimeDefault: false;
1203
+ enumValues: undefined;
1204
+ baseColumn: never;
1205
+ identity: undefined;
1206
+ generated: undefined;
1207
+ }, {}>;
1208
+ };
1209
+ dialect: "mssql";
1210
+ }>;
1211
+ TMAllowlist: drizzle_orm_mssql_core.MsSqlTableWithColumns<{
1212
+ name: "TMAllowlist";
1213
+ schema: S;
1214
+ columns: {
1215
+ createdAt: drizzle_orm_mssql_core.MsSqlColumn<{
1216
+ name: string;
1217
+ tableName: "TMAllowlist";
1218
+ dataType: "object date";
1219
+ data: Date;
1220
+ driverParam: string | Date;
1221
+ notNull: false;
1222
+ hasDefault: false;
1223
+ isPrimaryKey: false;
1224
+ isAutoincrement: false;
1225
+ hasRuntimeDefault: false;
1226
+ enumValues: undefined;
1227
+ baseColumn: never;
1228
+ identity: undefined;
1229
+ generated: undefined;
1230
+ }, {}>;
1231
+ createdBy: drizzle_orm_mssql_core.MsSqlColumn<{
1232
+ name: string;
1233
+ tableName: "TMAllowlist";
1234
+ dataType: "string";
1235
+ data: string;
1236
+ driverParam: string | number;
1237
+ notNull: false;
1238
+ hasDefault: false;
1239
+ isPrimaryKey: false;
1240
+ isAutoincrement: false;
1241
+ hasRuntimeDefault: false;
1242
+ enumValues: [string, ...string[]];
1243
+ baseColumn: never;
1244
+ identity: undefined;
1245
+ generated: undefined;
1246
+ }, {}>;
1247
+ id: drizzle_orm_mssql_core.MsSqlColumn<{
1248
+ name: string;
1249
+ tableName: "TMAllowlist";
1250
+ dataType: "number int53";
1251
+ data: number;
1252
+ driverParam: string;
1253
+ notNull: true;
1254
+ hasDefault: true;
1255
+ isPrimaryKey: false;
1256
+ isAutoincrement: false;
1257
+ hasRuntimeDefault: false;
1258
+ enumValues: undefined;
1259
+ baseColumn: never;
1260
+ identity: undefined;
1261
+ generated: {};
1262
+ }, {}>;
1263
+ tenantId: drizzle_orm_mssql_core.MsSqlColumn<{
1264
+ name: string;
1265
+ tableName: "TMAllowlist";
1266
+ dataType: "string";
1267
+ data: string;
1268
+ driverParam: string | number;
1269
+ notNull: true;
1270
+ hasDefault: false;
1271
+ isPrimaryKey: false;
1272
+ isAutoincrement: false;
1273
+ hasRuntimeDefault: false;
1274
+ enumValues: [string, ...string[]];
1275
+ baseColumn: never;
1276
+ identity: undefined;
1277
+ generated: undefined;
1278
+ }, {}>;
1279
+ pattern: drizzle_orm_mssql_core.MsSqlColumn<{
1280
+ name: string;
1281
+ tableName: "TMAllowlist";
1282
+ dataType: "string";
1283
+ data: string;
1284
+ driverParam: string | number;
1285
+ notNull: true;
1286
+ hasDefault: false;
1287
+ isPrimaryKey: false;
1288
+ isAutoincrement: false;
1289
+ hasRuntimeDefault: false;
1290
+ enumValues: [string, ...string[]];
1291
+ baseColumn: never;
1292
+ identity: undefined;
1293
+ generated: undefined;
1294
+ }, {}>;
1295
+ type: drizzle_orm_mssql_core.MsSqlColumn<{
1296
+ name: string;
1297
+ tableName: "TMAllowlist";
1298
+ dataType: "string";
1299
+ data: string;
1300
+ driverParam: string | number;
1301
+ notNull: true;
1302
+ hasDefault: false;
1303
+ isPrimaryKey: false;
1304
+ isAutoincrement: false;
1305
+ hasRuntimeDefault: false;
1306
+ enumValues: [string, ...string[]];
1307
+ baseColumn: never;
1308
+ identity: undefined;
1309
+ generated: undefined;
1310
+ }, {}>;
1311
+ description: drizzle_orm_mssql_core.MsSqlColumn<{
1312
+ name: string;
1313
+ tableName: "TMAllowlist";
1314
+ dataType: "string";
1315
+ data: string;
1316
+ driverParam: string | number;
1317
+ notNull: false;
1318
+ hasDefault: false;
1319
+ isPrimaryKey: false;
1320
+ isAutoincrement: false;
1321
+ hasRuntimeDefault: false;
1322
+ enumValues: [string, ...string[]];
1323
+ baseColumn: never;
1324
+ identity: undefined;
1325
+ generated: undefined;
1326
+ }, {}>;
1327
+ };
1328
+ dialect: "mssql";
1329
+ }>;
1330
+ };
1331
+ /** Explicit alias for the tables `defineAuthDirectorySchema` returns (avoids TS2742). 明示型。 */
1332
+ type AuthDirectoryTables = {
1333
+ TDGroupExternalLink: ReturnType<typeof defineAuthDirectorySchema>["TDGroupExternalLink"];
1334
+ TDGroupMemberEmail: ReturnType<typeof defineAuthDirectorySchema>["TDGroupMemberEmail"];
1335
+ TDTenantMembershipGroup: ReturnType<typeof defineAuthDirectorySchema>["TDTenantMembershipGroup"];
1336
+ TDAllowlistGroup: ReturnType<typeof defineAuthDirectorySchema>["TDAllowlistGroup"];
1337
+ TDDirectorySyncRequest: ReturnType<typeof defineAuthDirectorySchema>["TDDirectorySyncRequest"];
1338
+ TDDirectoryCatalog: ReturnType<typeof defineAuthDirectorySchema>["TDDirectoryCatalog"];
1339
+ TMDirectoryCatalogObservation: ReturnType<typeof defineAuthDirectorySchema>["TMDirectoryCatalogObservation"];
1340
+ TMAllowlist: ReturnType<typeof defineAuthDirectorySchema>["TMAllowlist"];
1341
+ };
1342
+
1343
+ /**
1344
+ * @module catalog-store
1345
+ * @description The upstream group list, written by the sync job and read by the web.
1346
+ * 同期ジョブが書き、web が読む、上流のグループ一覧。
1347
+ *
1348
+ * ⚠️ **この表と、このモジュールが在る理由は境界である。** ドメイン全体委任の資格情報は組織の
1349
+ * ディレクトリを丸ごと読めるため、外部からの要求を終端する web プロセスには置かない。上流へ
1350
+ * 届くのは同期ジョブだけとし、web は「ジョブが書いた行」を読む。管理面の選択欄は利便のための
1351
+ * 読み取りであり、その利便のために資格情報の置き場所を動かしてよい理由にはならない。
1352
+ *
1353
+ * ここには生産側 (`replace`) と消費側 (`read`) の両方が居る。プロセスを分ける仕掛けは
1354
+ * import の可否ではなく **資格情報を持つかどうか** であり、`replace` を呼べても上流を
1355
+ * 読めないプロセスは書くべき内容を持てない。
1356
+ */
1357
+
1358
+ /**
1359
+ * One upstream group, as observed. 観測した上流グループ 1 件。
1360
+ *
1361
+ * ⚠️ 別名ではなく `@aiquants/auth-core` の型そのものである。層ごとに「同じ意味の別の型」を宣言すると、
1362
+ * 構造的部分型のせいでどの境界も通ってしまい、上流へ足した欄が途中で黙って捨てられる。
1363
+ */
1364
+ type DirectoryCatalogEntry = AuthDirectoryCatalogGroup;
1365
+ /** What the web reads. web が読む内容 ({@link AuthDirectoryCatalogSnapshot} の項目名違いではなく同一契約)。 */
1366
+ type DirectoryCatalogSnapshot = AuthDirectoryCatalogSnapshot;
1367
+ /** Options a reader needs. 読み手が必要とするオプション。 */
1368
+ type DirectoryCatalogReaderOptions = {
1369
+ ensureConnection?: EnsureConnection;
1370
+ };
1371
+ /**
1372
+ * Options a writer needs. 書き手が必要とするオプション。
1373
+ *
1374
+ * ⚠️ 読み手にこれを要求してはならない。読むだけの呼び出し側に時計と監査主体を持たせると、
1375
+ * その場で `replace` を呼ぶ経路が「既に手元にあるもの」になり、上流を読めないプロセスが
1376
+ * 観測時刻を詐称できてしまう。だから生成関数を 2 つに分けている。
1377
+ */
1378
+ type DirectoryCatalogWriterOptions = DirectoryCatalogReaderOptions & {
1379
+ /** Injected clock — one observation must carry one timestamp. 注入時計。 */
1380
+ now: () => Date;
1381
+ /** Who the audit columns record. 監査列に残す主体。 */
1382
+ actor: string;
1383
+ };
1384
+ /**
1385
+ * Creates the write side of the catalog. 候補一覧の **書き手** を生成する処理。
1386
+ *
1387
+ * ⚠️ これを呼べるのは上流の資格情報を持つプロセスだけであるべきである。読むだけの側は
1388
+ * {@link createDirectoryCatalogReader} を使う — 分けているのは整理のためではなく、「読むだけの
1389
+ * 呼び出し側の手元に `replace` が在る」状態そのものを作らないためである。
1390
+ *
1391
+ * @param dbUnknown - Drizzle database instance. drizzle のデータベースインスタンス。
1392
+ * @param tables - Directory tables (owns the catalog). ディレクトリ表 (候補一覧の所有者)。
1393
+ * @param opts - Connection, clock and actor. 接続・時計・監査主体。
1394
+ * @returns The `replace` operation. 置き換え操作。
1395
+ */
1396
+ declare function createDirectoryCatalogWriter(dbUnknown: unknown, tables: AuthDirectoryTables, opts: DirectoryCatalogWriterOptions): {
1397
+ /**
1398
+ * Replaces this tenant's catalog with what the upstream currently answers.
1399
+ * このテナントの候補一覧を、上流の現在の答えで置き換える処理。
1400
+ *
1401
+ * ⚠️ 差分更新ではなく **全置換** である。消えたグループを残すと、管理面はそれを選ばせ続け、
1402
+ * 選んだ運用者は 10 分後に「上流に無い」と言われる。1 つのトランザクションで消してから
1403
+ * 書くことで、途中の状態を web に読ませない。
1404
+ *
1405
+ * @param tenantId - Owner tenant. 所有テナント。
1406
+ * @param provider - Upstream provider key. 上流プロバイダのキー。
1407
+ * @param entries - The upstream's current answer. 上流の現在の答え。
1408
+ * @returns How many rows were written. 書き込んだ行数。
1409
+ */
1410
+ replace(tenantId: string, provider: string, entries: readonly DirectoryCatalogEntry[]): Promise<number>;
1411
+ };
1412
+ /**
1413
+ * Creates the read side of the catalog. 候補一覧の **読み手** を生成する処理。
1414
+ *
1415
+ * 時計も監査主体も要らない。要らないものを求めないことが、この関数がここに在る理由である。
1416
+ *
1417
+ * @param dbUnknown - Drizzle database instance. drizzle のデータベースインスタンス。
1418
+ * @param tables - Directory tables (owns the catalog). ディレクトリ表 (候補一覧の所有者)。
1419
+ * @param opts - Connection handling. 接続の扱い。
1420
+ * @returns The `read` operation. 読み取り操作。
1421
+ */
1422
+ declare function createDirectoryCatalogReader(dbUnknown: unknown, tables: AuthDirectoryTables, opts?: DirectoryCatalogReaderOptions): {
1423
+ /**
1424
+ * Reads this tenant's catalog. このテナントの候補一覧を読む処理。
1425
+ *
1426
+ * ⚠️ **観測の有無は行数から導かない。** 上流を正しく読めて答えが 0 件だった配備 (借用先の
1427
+ * 権限が狭い、対象 OU が空) と、同期が一度も走っていない配備は、候補一覧の行数ではどちらも
1428
+ * 0 である。前者を「同期が動いていない」と診断すると、運用は健全な部品を調べに行く。
1429
+ * `observedAt` は観測記録そのものから取り、`null` は **一度も観測していない** だけを意味する。
1430
+ *
1431
+ * @param tenantId - Owner tenant. 所有テナント。
1432
+ * @param provider - Upstream provider key. 上流プロバイダのキー。
1433
+ * @returns The groups and the observation time. グループと観測時刻。
1434
+ */
1435
+ read(tenantId: string, provider: string): Promise<DirectoryCatalogSnapshot>;
1436
+ };
1437
+
1438
+ /**
1439
+ * @module guards
1440
+ * @description Keeps administrative roles off groups whose membership an upstream directory owns.
1441
+ * 所属の所有者が上流ディレクトリにあるグループへ、管理ロールが付かないようにする。
1442
+ *
1443
+ * **不変条件は状態の性質であって、遷移の性質ではない。**
1444
+ *
1445
+ * 最初の実装は `assignGroupRole` だけを差し替えていた。しかしそれは「外部グループへ管理ロールを
1446
+ * 割り当てる」という 1 つの遷移を塞いだにすぎず、**無害なロールを外部グループへ割り当ててから、
1447
+ * そのロールを管理ロールへ昇格させる**という順序が開いたままだった。不変条件を守るには、それを
1448
+ * 崩し得るすべての遷移を塞ぐ必要がある。
1449
+ *
1450
+ * 崩し得る遷移は 3 つある。
1451
+ *
1452
+ * | # | 遷移 | 塞ぐもの |
1453
+ * | --- | --- | --- |
1454
+ * | 1 | 外部グループが管理ロールを得る | `assignGroupRole` |
1455
+ * | 2 | 外部グループが持つロールが管理ロールになる | `createGrant` |
1456
+ * | 3 | 管理ロールを持つグループが外部連携される | `assertGroupMayBeLinked` |
1457
+ * | 4 | 外部グループが持つロールの付与が差し替わって管理ロールになる | `updateGrant` |
1458
+ *
1459
+ * ⚠️ `updateGrant` を「塞ぐ必要がない」と読んではならない。行数だけを見る論法 —
1460
+ * 「`(role_id, resource_id, action)` は一意だから、アクションを差し替えても集合は増えない」 —
1461
+ * は **ロール 1 つの中でしか成り立たない**。判定はこのファイルのどの遷移でも
1462
+ * **グループの全ロールの合併** で行う。ロール 1 が `{read, create}`、ロール 2 が
1463
+ * `{read, update}` を持つ外部グループでは合併は 3 アクションで、どちらの割当も正当に通る。
1464
+ * そこでロール 2 の `read` を `delete` へ差し替えると、ロール 2 の行数は 2 のままなのに
1465
+ * 合併は 4 アクションに達し、上流のディレクトリ管理者がこの配備の管理者を決められるようになる。
1466
+ *
1467
+ * `setRoleActive` も塞ぐ必要がない。遷移 1 の判定はロールの有効・無効を見ないため、無効な管理
1468
+ * ロールも外部グループへは割り当てられない。したがって「無効な管理ロールを外部グループが保持した
1469
+ * まま有効化される」という状態には到達し得ない。
1470
+ *
1471
+ * **なぜ認可パッケージの中で判定しないのか。** 判定を `@aiquants/authz-drizzle` の管理ストアへ
1472
+ * 埋め込むと、認可パッケージがグループの「出所」という identity 側の概念を知ることになる。依存の
1473
+ * 向きは auth → authz の一方通行で保ちたいので、包む側 (identity 側) から課す。
1474
+ *
1475
+ * **なぜ禁止するのか。** 外部グループの所属は上流の管理者が変更できる。そこへ管理ロールを付けると、
1476
+ * 上流側の 1 操作がこの配備の管理者を増やしたり、逆に最後の管理者を奪ったりする。ブレークグラス
1477
+ * 経路は常にローカルに残しておく必要がある。
1478
+ */
1479
+
1480
+ /** The link lookup this guard needs. 本ガードが必要とするリンク参照。 */
1481
+ type ExternalLinkLookup = {
1482
+ /**
1483
+ * The upstream key of the group, or `null` when its membership is locally managed.
1484
+ * そのグループの上流キー。ローカル管理なら `null`。
1485
+ *
1486
+ * 「外部かどうか」と「拒否メッセージに出す名前」を 1 回の照会で返す。判定のためだけに
1487
+ * 引いて、名前のためにもう一度引く形にすると、拒否のたびに往復が 2 倍になる。
1488
+ *
1489
+ * ⚠️ `runner` を渡された実装は、**必ずその上で** 引くこと。別接続で引くと、この読み取りの
1490
+ * 結果を前提に書き込む呼び出し側にとって前提が保たれない (読んだ後・書く前に連携が作られる)。
1491
+ */
1492
+ externalGroupKey(tenantId: string, groupId: number, runner?: unknown): Promise<string | null>;
1493
+ };
1494
+ /** Configuration of the guard. ガードの構成。 */
1495
+ type ExternalGroupGuardOptions = {
1496
+ linkLookup: ExternalLinkLookup;
1497
+ /** App key the administrative resource belongs to. 管理リソースが属するアプリキー。 */
1498
+ adminAppKey: string;
1499
+ /** Resource key that grants administration. 管理を与えるリソースキー。 */
1500
+ adminResourceKey: string;
1501
+ /**
1502
+ * Refusal message. 拒否メッセージ。
1503
+ *
1504
+ * @param roleKey Role that would have conferred administration. 管理権を与えることになるロール。
1505
+ * @param groupKey Group that would have held it. それを保持することになるグループ。
1506
+ */
1507
+ message: (roleKey: string, groupKey: string) => string;
1508
+ };
1509
+ /** Default refusal message (Japanese, matching the admin UI's locale). 既定の拒否メッセージ。 */
1510
+ declare const defaultExternalGroupGuardMessage: (roleKey: string, groupKey: string) => string;
1511
+ /**
1512
+ * Wraps an authorization admin store so administrative power cannot reach an external group.
1513
+ * 外部グループへ管理権が届かないよう認可管理ストアを包む処理。
1514
+ *
1515
+ * `assignGroupRole` と `createGrant` を差し替え、残りは透過的に委譲する。3 番目の方向
1516
+ * (連携の開始) は {@link assertGroupMayBeLinked} が担い、リンクを作る側が必ず通す。
1517
+ *
1518
+ * @param store Store to wrap. 包む対象のストア。
1519
+ * @param opts Link lookup and administrative resource identity. リンク参照と管理リソースの同定。
1520
+ * @returns A store with the same surface. 同一表面のストア。
1521
+ */
1522
+ declare function withExternalGroupGuards<R>(store: AuthzAdminStore<R>, opts: ExternalGroupGuardOptions): AuthzAdminStore<R>;
1523
+ /**
1524
+ * Refuses to link a group that already holds an administrative role.
1525
+ * 既に管理ロールを保持しているグループの外部連携を拒否する処理。
1526
+ *
1527
+ * 3 つ目の方向。これを通さずにリンクを作ると、ローカルで管理ロールを付けてから連携させる経路が
1528
+ * 残る。{@link withExternalGroupGuards} と違って呼び出し側が忘れ得るため、リンクを作る側の
1529
+ * 構成に必須ポートとして組み込むこと。
1530
+ *
1531
+ * @param store Authorization admin store used to read the grants. 付与を読む認可管理ストア。
1532
+ * @param tenantId Tenant the group belongs to. グループが属するテナント。
1533
+ * @param groupId Group about to be linked. 連携しようとしているグループ。
1534
+ * @param opts Administrative resource identity and refusal message. 管理リソースの同定と文言。
1535
+ * @returns Nothing; returns normally only when linking is allowed. 許可される場合のみ正常復帰。
1536
+ * @throws {Error} When the group holds the administrative role. 管理ロールを保持している場合。
1537
+ */
1538
+ declare function assertGroupMayBeLinked(store: AuthzAdminStore, tenantId: string, groupId: number, opts: Pick<ExternalGroupGuardOptions, "adminAppKey" | "adminResourceKey" | "message">): Promise<void>;
1539
+
1540
+ /**
1541
+ * Resolves which member-ledger rows belong to one identity.
1542
+ * 台帳のどの行が 1 人の identity のものかを解決するモジュール。
1543
+ *
1544
+ * 台帳 (`TDGroupMemberEmail`) は「上流に居る人」を、ローカルユーザーへ解決できているかに
1545
+ * 関わらず保持する。ここはその台帳から本人の行だけを取り出す唯一の場所であり、帰属証明
1546
+ * (`listLedgerGroupIds`) と初回ログイン時の昇格 (`promoteLedgerForUser`) は同じ答えを見る。
1547
+ * 突合規則が 2 箇所にあると、片方だけが「本人」と認めた瞬間に権限が食い違う。
1548
+ */
1549
+
1550
+ /** One ledger row attributed to an identity. identity に帰属した台帳 1 行。 */
1551
+ type LedgerIdentityRow = {
1552
+ ledgerId: number;
1553
+ groupId: number;
1554
+ /** Inherited from the parent group — the ledger has no tenant column. 親グループ由来。 */
1555
+ tenantId: string;
1556
+ };
1557
+ /** The two tables this resolution spans. この解決が跨る 2 表。 */
1558
+ type LedgerIdentityTables = {
1559
+ /** `TDGroupMemberEmail`. */
1560
+ ledger: AuthDirectoryTables["TDGroupMemberEmail"];
1561
+ /** `TMGroup` — owns the tenant and the active flag. テナントと有効フラグの所有者。 */
1562
+ group: AuthzTables["TMGroup"];
1563
+ };
1564
+ /**
1565
+ * Finds every ledger row that belongs to the identity, within one tenant.
1566
+ * 1 テナント内で、その identity に属する台帳行をすべて探す処理。
1567
+ *
1568
+ * 上流 ID の一致は無条件に本人。アドレスの一致は、その行が **別の** 上流 ID に紐付いていない
1569
+ * ときだけ本人である。単純な `OR` にすると後者の条件が消え、退職者のアドレスを再利用した
1570
+ * 新入社員が前任者の所属をそのまま継承する。
1571
+ *
1572
+ * ⚠️ この優先順位は **行ごと** に適用する。「ID で当たったらアドレスは見ない」という打ち切りは
1573
+ * 誤りで、同じ人物の行が ID 付きと ID 無しに混在すると ID 無しの行が丸ごと落ちる。
1574
+ *
1575
+ * @param db - Connected database surface. 接続済みの DB 面。
1576
+ * @param tables - The ledger and its parent group table. 台帳と親グループ表。
1577
+ * @param args - Tenant scope and the identity to match. テナント範囲と突合対象の identity。
1578
+ * @returns Matching rows, deduplicated and ordered by ledger id. 重複排除・台帳 ID 昇順の一致行。
1579
+ */
1580
+ declare function findLedgerRowsForIdentity(db: DirectoryDb, tables: LedgerIdentityTables, args: {
1581
+ tenantId: string;
1582
+ externalUserId: string | null;
1583
+ email: string;
1584
+ }): Promise<LedgerIdentityRow[]>;
1585
+
1586
+ /**
1587
+ * @module link-store
1588
+ * @description Reads and writes the identity-side tables: external group links, the upstream member
1589
+ * ledger, tenant membership proof, group-based login allowlist, and the sync request queue.
1590
+ * identity 側テーブルの読み書き。外部グループリンク・上流メンバー台帳・テナント帰属証明・
1591
+ * グループ単位のログイン許可・同期要求キュー。
1592
+ *
1593
+ * **テナントの扱いは 2 種類ある。混同すると事故になる。**
1594
+ *
1595
+ * - **絞り込み条件**: リンク・帰属証明グループ。読み書きの双方で SQL 述語になる。
1596
+ * - **所有者**: グループ許可。管理 UI の編集スコープを決めるだけで、**ログイン判定では
1597
+ * 述語にしない** — 認証はテナントの確定より前に起きるため、判定の時点でテナントは存在しない。
1598
+ * このモジュールでは {@link createDirectoryLinkStore} の `isEmailAllowedByGroup` だけが
1599
+ * その例外であり、意図的に全テナントを対象とする。
1600
+ */
1601
+
1602
+ /**
1603
+ * Proves a group may become externally sourced. グループを外部供給にしてよいことの証明。
1604
+ *
1605
+ * `assertGroupMayBeLinked` を呼ぶだけのポートである。自由関数のままだと呼び出し側が忘れられ、
1606
+ * 「ローカルで管理ロールを付けてから連携させる」経路が残る。必須の構成項目にすることで、
1607
+ * 忘れることを構造的に不可能にする。
1608
+ */
1609
+ type LinkableAssertion = {
1610
+ /**
1611
+ * Owns a transaction, proves the group may be linked on it, then runs `write` on the same runner.
1612
+ * トランザクションを所有し、その上で連携可能であることを証明してから、同じランナーで `write` を実行する処理。
1613
+ *
1614
+ * ⚠️ **証明だけを返す形にしてはならない。** 証明と連携の作成が別のトランザクションだと、
1615
+ * 証明を通った直後・リンクを作る前に、そのグループへ管理ロールを割り当てる操作が
1616
+ * 割り込める。割り当て側の検査は「まだリンクが無い」と見て通り、連携側の検査は
1617
+ * 「まだ管理ロールが無い」と見て通る — **どちらも単独では正しく、結果だけが不変条件を破る**。
1618
+ *
1619
+ * @param tenantId - Tenant that owns the group. グループを所有するテナント。
1171
1620
  * @param groupId - Group about to become externally sourced. 外部供給にしようとしているグループ。
1172
1621
  * @param write - The link creation, run on the proving transaction. 証明したトランザクション上で行う連携作成。
1173
1622
  * @returns Nothing. 戻り値なし。
@@ -1224,6 +1673,14 @@ type LinkGroupInput = {
1224
1673
  externalKey: string;
1225
1674
  membershipMode: AuthDirectoryMembershipMode;
1226
1675
  };
1676
+ /**
1677
+ * What `requestSync` did. `requestSync` が行ったこと。
1678
+ *
1679
+ * ⚠️ `void` にしてはならない。同じ範囲の要求が既に待っているとき、積み増さないのは正しいが、
1680
+ * それを成功と区別せずに返すと、押しても反応しない画面ができる — しかも詰まった 1 行が以後の
1681
+ * 押下すべてを飲み込む。呼び出し側が運用者へ言えるだけの情報を返す。
1682
+ */
1683
+ type SyncRequestOutcome = "queued" | "already-pending";
1227
1684
  /**
1228
1685
  * Creates the identity-side data store.
1229
1686
  * identity 側のデータストアを生成する処理。
@@ -1277,7 +1734,7 @@ declare function createDirectoryLinkStore(dbUnknown: unknown, tables: AuthDirect
1277
1734
  *
1278
1735
  * @param groupId `null` requests every group of the tenant. `null` はテナント全件。
1279
1736
  */
1280
- requestSync(tenantId: string, groupId: number | null, requestedBy: string): Promise<void>;
1737
+ requestSync(tenantId: string, groupId: number | null, requestedBy: string): Promise<SyncRequestOutcome>;
1281
1738
  /**
1282
1739
  * Summarizes synchronization health for the admin header.
1283
1740
  * 管理ヘッダー向けに同期の健全性を要約する処理。
@@ -1312,8 +1769,10 @@ declare function createDirectoryLinkStore(dbUnknown: unknown, tables: AuthDirect
1312
1769
  * @param isBreakGlass - True for the locally-managed recovery path. 復帰経路なら true。
1313
1770
  * @param actor - Who performed it. 実行者。
1314
1771
  * @returns Nothing. 戻り値なし。
1315
- * @throws {Error} When the group is not in the tenant, or a break-glass group is linked.
1316
- * 自テナントに無い場合、または break-glass に連携済みグループを指定した場合。
1772
+ * @throws {Error} When the group is not in the tenant, when a break-glass group is linked,
1773
+ * or when the result would be a proof with no break-glass path.
1774
+ * 自テナントに無い場合、break-glass に連携済みグループを指定した場合、または結果が
1775
+ * 「証明はあるが復帰経路が無い」方針になる場合。
1317
1776
  */
1318
1777
  setTenantMembershipGroup(tenantId: string, groupId: number, isBreakGlass: boolean, actor: string): Promise<void>;
1319
1778
  /**
@@ -1505,6 +1964,8 @@ declare function createPromotionRepository(dbUnknown: unknown, tables: AuthDirec
1505
1964
  * 述語) が使うため、分割の基準にはここから余裕を引いた値を用いる。
1506
1965
  */
1507
1966
  declare const MAX_PARAMETERS = 2100;
1967
+ /** Parameters reserved for the statement's own scalar values. 文自身のスカラー値のための予約枠。 */
1968
+ declare const RESERVED_PARAMETERS = 100;
1508
1969
  /**
1509
1970
  * Maximum ids per `IN` list of single-column predicates. 単列述語の `IN` 句 1 回あたりの最大 ID 数。
1510
1971
  *
@@ -1548,18 +2009,47 @@ declare function chunkByParameters<T>(items: readonly T[], parametersPerItem: nu
1548
2009
  * 既定は日本語 (このリポジトリの管理 UI に合わせる)。
1549
2010
  */
1550
2011
 
2012
+ /**
2013
+ * Why a read or a write failed, classified for display. 読み書きが失敗した理由 (表示用の分類)。
2014
+ *
2015
+ * ⚠️ **例外文をそのまま `sync_error` へ入れてはならない。** この列は管理 UI のバナーとして
2016
+ * **全タブに** 出るため、閲覧権限しか持たない主体が上流の生の応答本文 (サービスアカウントの
2017
+ * アドレス、内部 URL、資格情報の断片) や DB の接続文字列・スキーマ名を読めてしまう。しかも
2018
+ * それは、候補一覧の **グループ名すら** 見せないと決めた相手である。原文はログへ、画面へは分類を。
2019
+ */
2020
+ type SyncFailureCause = "denied" | "rateLimited" | "upstreamError" | "timeout" | "localWriteError" | "unknown";
2021
+ /**
2022
+ * Classifies a thrown value without quoting it. 投げられた値を、引用せずに分類する処理。
2023
+ *
2024
+ * 上流アダプタの型に依存しないよう、`status` という数値プロパティの有無だけで判定する。
2025
+ * 判別できないものは `unknown` へ倒す (推測した分類を出すより、判らないと言う方が正しい)。
2026
+ *
2027
+ * @param error - The thrown value. 投げられた値。
2028
+ * @returns The display classification. 表示用の分類。
2029
+ */
2030
+ declare function classifySyncFailure(error: unknown): SyncFailureCause;
1551
2031
  /** The messages one synchronization can produce. 同期が生成し得る文言。 */
1552
2032
  type SyncMessages = {
1553
2033
  /** The upstream could not be read at all. 上流をまったく読めなかった。 */
1554
- upstreamUnreadable: (reason: string) => string;
2034
+ upstreamUnreadable: (cause: SyncFailureCause) => string;
1555
2035
  /** The local write failed. ローカルへの書込が失敗した。 */
1556
- localWriteFailed: (reason: string) => string;
2036
+ localWriteFailed: (cause: SyncFailureCause) => string;
1557
2037
  /** The resolver returned fewer members than the upstream listed. 突合が件数を減らした。 */
1558
2038
  resolverIncomplete: (upstream: number, resolved: number) => string;
1559
2039
  /** The group is missing upstream but not yet deactivated. 消滅の観測 (未確定)。 */
1560
2040
  groupMissing: (externalKey: string, observations: number, threshold: number) => string;
1561
2041
  /** The group has been deactivated after repeated absence. 消滅確定・無効化。 */
1562
2042
  groupVanished: (externalKey: string, observations: number) => string;
2043
+ /**
2044
+ * Removals whose write failed for a transient reason. 一時的な理由で書けなかった削除。
2045
+ *
2046
+ * ⚠️ 拒否とは別の文言にする。「最後の管理者ガードが止めた」は人手を呼ぶ答えであり、
2047
+ * デッドロックや接続断に対して同じ文言を出すと、待てば直る事象に人を動かすことになる。
2048
+ */
2049
+ removalWritesFailed: (failures: readonly {
2050
+ userId: number;
2051
+ cause: SyncFailureCause;
2052
+ }[]) => string;
1563
2053
  /** The reconciler refused the plan. 突合が plan を拒否した。 */
1564
2054
  planRefused: (abort: SyncAbort) => string;
1565
2055
  /** The lockout guard refused some removals. ロックアウトガードが一部の削除を拒否した。 */
@@ -1570,7 +2060,6 @@ type SyncMessages = {
1570
2060
  /** The link belongs to a different provider. 他 provider のリンクだった。 */
1571
2061
  foreignProvider: (linkProvider: string, runnerProvider: string) => string;
1572
2062
  };
1573
- /** Default Japanese messages. 既定の日本語文言。 */
1574
2063
  declare const defaultSyncMessages: SyncMessages;
1575
2064
 
1576
2065
  /**
@@ -1607,6 +2096,21 @@ type SyncLink = {
1607
2096
  */
1608
2097
  vanishedObservations: number;
1609
2098
  };
2099
+ /**
2100
+ * Which links a run covers. 1 回の実行が対象にするリンクの範囲。
2101
+ *
2102
+ * ⚠️ `tenantId` は **必須** である。1 つのジョブが持つ上流の資格情報は 1 つのテナントに対応する。
2103
+ * 範囲を絞らないと、別テナントのリンクを読んだうえで **自分の資格情報で得た結果をその別テナントへ
2104
+ * 書き込む**。上流が 404 を返せば (借用先の外にあるグループでは通常そうなる) 消滅と判定して
2105
+ * 相手のグループを無効化し、外部 ID が偶然一致すれば相手のグループへ自分の所属を射影する。
2106
+ * 要求した側の権限は一切関係しないため、既定値 1 つで越境が成立する形にしてはならない。
2107
+ * 同じ理由で `createSyncRequestQueue` も構築時にテナントを固定する。
2108
+ */
2109
+ type SyncScope = {
2110
+ tenantId: string;
2111
+ /** Restrict further to one group within the tenant. テナント内の 1 グループへさらに絞る。 */
2112
+ groupId?: number;
2113
+ };
1610
2114
  /** Why one group's synchronization ended the way it did. 1 グループの同期がどう終わったか。 */
1611
2115
  type SyncOutcome = {
1612
2116
  kind: "applied";
@@ -1635,6 +2139,17 @@ type SyncOutcome = {
1635
2139
  kind: "vanished";
1636
2140
  link: SyncLink;
1637
2141
  }
2142
+ /**
2143
+ * Still missing, and already deactivated on an earlier run. 消滅済みで、以前の実行で無効化済み。
2144
+ *
2145
+ * ⚠️ `vanished` と分ける理由は、**運用通知が状態ではなく変化に反応するべき** だからである。
2146
+ * 同じ 1 件の消滅を掃引のたびに通知すると、本当に新しい消滅がその中に埋もれる。この結末は
2147
+ * 「今回の掃引では何も変わっていない」ことの表明であり、健全ではないが新規でもない。
2148
+ */
2149
+ | {
2150
+ kind: "already-vanished";
2151
+ link: SyncLink;
2152
+ }
1638
2153
  /** Reading upstream failed. 上流の読み取りが失敗した。 */
1639
2154
  | {
1640
2155
  kind: "upstream-failed";
@@ -1651,7 +2166,30 @@ type SyncOutcome = {
1651
2166
  | {
1652
2167
  kind: "foreign-provider";
1653
2168
  link: SyncLink;
2169
+ }
2170
+ /**
2171
+ * The plan was applied only in part. plan の一部しか適用できなかった。
2172
+ *
2173
+ * ⚠️ `applied` と分ける。拒否や書込失敗が残ったまま「適用済み」と答えると、上流が外した人物が
2174
+ * グループに残っているのに、鮮度は健全・通知は無音・要求キューの結果は成功になる — 収束して
2175
+ * いないことを示す信号がどこにも出ない。
2176
+ */
2177
+ | {
2178
+ kind: "not-converged";
2179
+ link: SyncLink;
2180
+ plan: GroupSyncPlan;
2181
+ refusals: RemovalOutcome[];
2182
+ failures: RemovalFailure[];
1654
2183
  };
2184
+ /**
2185
+ * Outcomes that mean something changed for the worse on this run. この実行で新たに悪化した結末。
2186
+ *
2187
+ * ⚠️ **状態ではなく変化を数える。** `already-vanished` は不健全な状態ではあるが、以前の実行で
2188
+ * 既に記録・通知済みであり、今回の掃引では何も変わっていない。ここに入れると同じ 1 件が周期ぶん
2189
+ * 通知され、本当に新しい消滅が埋もれる。「今どれだけ壊れているか」を数えたい呼び出し側は
2190
+ * {@link UNHEALTHY_SYNC_OUTCOMES} を使う。
2191
+ */
2192
+ declare const NEWLY_UNHEALTHY_SYNC_OUTCOMES: readonly SyncOutcome["kind"][];
1655
2193
  /** Outcomes that mean the group is not in a healthy, converged state. 健全でない結末。 */
1656
2194
  declare const UNHEALTHY_SYNC_OUTCOMES: readonly SyncOutcome["kind"][];
1657
2195
  /** What happened to one guarded removal. ガード付き削除 1 件の結末。 */
@@ -1660,10 +2198,24 @@ type RemovalOutcome = {
1660
2198
  applied: boolean;
1661
2199
  refusal?: string;
1662
2200
  };
2201
+ /**
2202
+ * One removal the write itself could not complete. 書き込み自体が完了しなかった削除 1 件。
2203
+ *
2204
+ * ⚠️ **拒否とは別に数える。** 「最後の管理者を失う」はガードの判断であり、運用者への答えは
2205
+ * 「別の管理者を立てよ」である。一方でデッドロックの犠牲や接続断は待てば直るものであり、
2206
+ * 答えは「再試行せよ」である。両方を同じ袋に入れると、待てば直る事象に対して人手を呼び、
2207
+ * 人手が要る事象を放置することになる。
2208
+ */
2209
+ type RemovalFailure = {
2210
+ userId: number;
2211
+ cause: SyncFailureCause;
2212
+ };
1663
2213
  /** What `applyPlan` actually managed to do. `applyPlan` が実際に行えたこと。 */
1664
2214
  type ApplyReport = {
1665
2215
  /** Removals the administrator lockout guard refused. ロックアウトガードが拒否した削除。 */
1666
2216
  refusals: RemovalOutcome[];
2217
+ /** Removals whose write failed for a transient reason. 一時的な理由で書けなかった削除。 */
2218
+ failures: RemovalFailure[];
1667
2219
  };
1668
2220
  /**
1669
2221
  * Storage operations the synchronization performs.
@@ -1678,10 +2230,7 @@ type DirectorySyncRepository = {
1678
2230
  *
1679
2231
  * @param scope Restrict to one tenant, or one group within it. テナント、または特定グループへ限定。
1680
2232
  */
1681
- listSyncableLinks(scope: {
1682
- tenantId?: string;
1683
- groupId?: number;
1684
- }): Promise<SyncLink[]>;
2233
+ listSyncableLinks(scope: SyncScope): Promise<SyncLink[]>;
1685
2234
  /** Reads the local ledger and the real membership for one group. 台帳と実所属を読む処理。 */
1686
2235
  loadGroupSnapshot(link: SyncLink): Promise<LocalGroupSnapshot>;
1687
2236
  /**
@@ -1708,6 +2257,23 @@ type DirectorySyncRepository = {
1708
2257
  */
1709
2258
  applyPlan(link: SyncLink, plan: GroupSyncPlan, observedAt: Date): Promise<ApplyReport>;
1710
2259
  /** Records a successful run and reactivates the group if it was deactivated. 成功の記録。 */
2260
+ /**
2261
+ * Persists the upstream's stable identity before any membership is applied.
2262
+ * 所属を適用する **前** に、上流の安定 ID を確定させる処理。
2263
+ *
2264
+ * ⚠️ **順序がこの操作の意味である。** 管理面は、候補一覧が引けない配備ではアドレスを暫定の
2265
+ * 安定 ID として保存する。同じ上流グループを別の綴り (アドレスと別名、あるいは本当の ID) で
2266
+ * 2 回リンクできてしまい、`UNIQUE (tenant, provider, external_id)` も文字列が違うので通る。
2267
+ * 確定を所属の適用より前に置くことで、重複したリンクは **何も投影しないまま** 一意制約に
2268
+ * 当たって落ちる。後に置くと、二重の所属が先に commit され、両方のロールが配られてから
2269
+ * 書き戻しだけが失敗し続ける。
2270
+ *
2271
+ * @param link - The link being synchronized. 同期中のリンク。
2272
+ * @param upstreamGroup - What the upstream answered. 上流が答えた内容。
2273
+ * @returns Nothing. 戻り値なし。
2274
+ * @throws When another link already owns this upstream group. 同じ上流を別のリンクが持つ場合。
2275
+ */
2276
+ resolveExternalIdentity(link: SyncLink, upstreamGroup: DirectoryGroup): Promise<void>;
1711
2277
  recordSuccess(link: SyncLink, group: DirectoryGroup, syncedAt: Date): Promise<void>;
1712
2278
  /** Records a failure reason against the link, leaving membership untouched. 失敗を記録する処理。 */
1713
2279
  recordFailure(link: SyncLink, reason: string): Promise<void>;
@@ -1778,6 +2344,14 @@ type DirectorySyncRunResult = {
1778
2344
  * 最も重い結末であり、それを「0 unhealthy」と報告しては監視の意味がない。
1779
2345
  */
1780
2346
  unhealthyCount: number;
2347
+ /**
2348
+ * Links that became unhealthy on this run. この実行で新たに不健全になったリンク数。
2349
+ *
2350
+ * ⚠️ 通知はこちらで判断する。`unhealthyCount` は状態の量なので、消滅済みのグループが 1 つ
2351
+ * 残っているだけで毎周期 1 以上になり、同じ 1 件を周期ぶん通知し続ける — 本当に新しい
2352
+ * 消滅がその中に埋もれる。数えるのは状態、知らせるのは変化である。
2353
+ */
2354
+ newlyUnhealthyCount: number;
1781
2355
  };
1782
2356
  /**
1783
2357
  * Creates a synchronization runner. 同期ランナーを生成する処理。
@@ -1795,13 +2369,10 @@ declare function createDirectorySync(config: DirectorySyncConfig): {
1795
2369
  * 弾かれたグループだけが失敗して部分的に古い状態が残る。1 グループの失敗は他へ
1796
2370
  * 波及させず、実行自体は最後まで続ける。
1797
2371
  *
1798
- * @param scope Restrict to one tenant, or one group within it. 対象範囲。
2372
+ * @param scope Which tenant (and optionally which group) to cover. 対象テナントと範囲。
1799
2373
  * @returns Every outcome, and how many ended unhealthy. 全結果と不健全件数。
1800
2374
  */
1801
- run(scope?: {
1802
- tenantId?: string;
1803
- groupId?: number;
1804
- }): Promise<DirectorySyncRunResult>;
2375
+ run(scope: SyncScope): Promise<DirectorySyncRunResult>;
1805
2376
  };
1806
2377
 
1807
2378
  /** One claimed request. 取得済みの要求 1 件。 */
@@ -1812,6 +2383,23 @@ type ClaimedSyncRequest = {
1812
2383
  groupId: number | null;
1813
2384
  requestedBy: string;
1814
2385
  };
2386
+ /**
2387
+ * Requests that are not progressing. 進んでいない要求。
2388
+ *
2389
+ * ⚠️ 2 つを分けて数える。`stranded` は「掴まれたが終わっていない」— ジョブが落ちた跡である。
2390
+ * `unclaimed` は「誰にも掴まれていない」— **そのテナントを担当するジョブが居ない** 跡であり、
2391
+ * 掴まれた行だけを見ていると丸ごと見落とす。重複抑止が在るため、後者は 1 行で以後の要求すべてを
2392
+ * 黙って捨てる。
2393
+ */
2394
+ type StrandedRequests = {
2395
+ stranded: number[];
2396
+ unclaimed: number[];
2397
+ };
2398
+ /**
2399
+ * Recorded when a request is closed because the process that claimed it never finished it.
2400
+ * 掴んだプロセスが終わりを書かなかったために閉じた要求へ記録される理由。
2401
+ */
2402
+ declare const STRANDED_REASON = "The job that claimed this request ended without recording an outcome; whether the synchronization ran is unknown.";
1815
2403
  /** How a claimed request ended. 取得した要求の終わり方。 */
1816
2404
  type SyncRequestResult = {
1817
2405
  result: "succeeded" | "failed";
@@ -1820,8 +2408,30 @@ type SyncRequestResult = {
1820
2408
  /** Options for {@link createSyncRequestQueue}. キュー消費側の生成オプション。 */
1821
2409
  type SyncRequestQueueOptions = {
1822
2410
  ensureConnection?: EnsureConnection;
2411
+ /**
2412
+ * The only tenant this consumer may claim for. この消費側が取得してよい唯一のテナント。
2413
+ *
2414
+ * ⚠️ **必須である。** 1 つのジョブが持つ上流の資格情報は 1 つのテナントに対応する。範囲を
2415
+ * 絞らないと、別テナントの要求を掴んだジョブが **自分の資格情報で読んだ結果を、その別テナントへ
2416
+ * 書き込む**。要求を出した側の権限は一切関係しないため、これは要求 1 つで越境が起きる形になる。
2417
+ *
2418
+ * ⚠️ その代償として、**担当ジョブの居ないテナントの要求は誰にも観測されない**。`listStranded`
2419
+ * もこのテナントに絞られる以上、消費側が存在しないテナントを数える主体はどこにも居ない。
2420
+ * 見えるのは web 側だけである — `getStatus().pendingRequestCount` が管理面のバナーに出て、
2421
+ * `requestSync` は 2 度目以降を `already-pending` として返す。配備時は、リンクを持つ
2422
+ * すべてのテナントにジョブを 1 つずつ立てること。
2423
+ */
2424
+ tenantId: string;
1823
2425
  /** Injected clock — the run's timestamps must all come from one source. 注入時計。 */
1824
2426
  now: () => Date;
2427
+ /**
2428
+ * Reports a request that could not be closed. 閉じられなかった要求を報告する処理。
2429
+ *
2430
+ * ⚠️ {@link createSyncRequestQueue.abandonStranded} は 1 件の失敗で残りを諦めない。
2431
+ * 諦めない以上、失敗そのものが黙って消える経路になってはならない — 記録の宛先は
2432
+ * 呼び出し側が持っているので、ここから渡す。省略時は何もしない。
2433
+ */
2434
+ onCloseFailed?: (id: number, error: unknown) => void;
1825
2435
  /**
1826
2436
  * Longest an error message may be, matching the column. 列に合わせたエラー文の上限長。
1827
2437
  *
@@ -1859,15 +2469,39 @@ declare function createSyncRequestQueue(dbUnknown: unknown, tables: AuthDirector
1859
2469
  */
1860
2470
  finish(id: number, outcome: SyncRequestResult): Promise<void>;
1861
2471
  /**
1862
- * Lists requests claimed long ago but never finished. 取得後に完了しなかった要求の一覧。
2472
+ * Lists requests that are not progressing. 進んでいない要求の一覧。
1863
2473
  *
1864
2474
  * ジョブが落ちると、行は取得済みのまま完了しない。放置すると要求した人はいつまでも
1865
2475
  * 結果を見られないので、運用が拾えるよう明示的に数えられる形にしておく。
1866
2476
  *
1867
- * @param olderThan - Cutoff for `picked_at`. `picked_at` の締切時刻。
1868
- * @returns The stranded request ids. 取り残された要求の ID。
2477
+ * ⚠️ **掴まれていない古い行も数える。** 取得済みだけを見ると、そのテナントを担当する
2478
+ * ジョブが居ない場合が丸ごと盲点になる その行は永久に処理されないのに、重複抑止の
2479
+ * せいで以後の「今すぐ同期」まで黙って捨てられる。
2480
+ *
2481
+ * @param olderThan - Cutoff for both timestamps. 両方の時刻に対する締切時刻。
2482
+ * @returns Stranded and unclaimed request ids. 取り残された要求と、掴まれていない要求の ID。
2483
+ */
2484
+ listStranded(olderThan: Date): Promise<StrandedRequests>;
2485
+ /**
2486
+ * Closes stranded requests as failed, so the queue can move again.
2487
+ * 取り残された要求を失敗として閉じ、キューを再び動かす処理。
2488
+ *
2489
+ * ⚠️ **掴んだ行を未処理へ戻さない。** 戻すと `UQ_..._pending` (未処理行の一意制約) に
2490
+ * 衝突し得る — 同じ範囲の新しい要求が既に並んでいれば、戻す操作そのものが失敗する。
2491
+ * 失敗として閉じれば、監査記録は残り、要求した人は結果 (失敗) を見られ、次の「今すぐ
2492
+ * 同期」は重複抑止に掛からない。
2493
+ *
2494
+ * ⚠️ **走ったかどうかは分からない。** 記録する理由も、成否ではなく「掴んだプロセスが
2495
+ * 終わりを書かなかった」という観測事実だけにする。推測を記録に混ぜない。
2496
+ *
2497
+ * ⚠️ 掴まれていない行 (`unclaimed`) には触れない。あれはジョブが落ちた跡ではなく
2498
+ * **担当ジョブが居ない** 跡であり、閉じると原因が消えて再発し続ける。
2499
+ *
2500
+ * @param olderThan - Requests picked before this are considered abandoned.
2501
+ * これより前に掴まれた要求を放棄と見なす締切時刻。
2502
+ * @returns The ids closed. 閉じた要求の ID。
1869
2503
  */
1870
- listStranded(olderThan: Date): Promise<number[]>;
2504
+ abandonStranded(olderThan: Date): Promise<number[]>;
1871
2505
  };
1872
2506
 
1873
2507
  /**
@@ -1886,6 +2520,15 @@ type MemberResolverPort = {
1886
2520
  /** Options for the synchronization repository. 同期リポジトリの構成。 */
1887
2521
  type SyncRepositoryOptions = {
1888
2522
  ensureConnection?: EnsureConnection;
2523
+ /**
2524
+ * Wall-clock supplier. 実行の時刻を供給する処理。
2525
+ *
2526
+ * ⚠️ 走査側 (`createDirectorySync`) は時計を注入されており、「1 回の実行の中で観測時刻と
2527
+ * 同期時刻が別々に進むと、どの行がどの実行で書かれたのか後から辿れなくなる」という理由が
2528
+ * 書いてある。記憶域側だけが `new Date()` を直に読むと、**同じ実行の中で 2 つの時計が進む** —
2529
+ * 失敗の記録と成功の記録が別の時刻を持ち、その理由は誰にも判らない。省略時は実時計。
2530
+ */
2531
+ now?: () => Date;
1889
2532
  memberResolver: MemberResolverPort;
1890
2533
  /** Lockout guard from `@aiquants/authz-drizzle`. 認可側のロックアウトガード。 */
1891
2534
  lockoutGuard: ReturnType<typeof createAdminLockoutGuard>;
@@ -1910,4 +2553,4 @@ type SyncRepositoryOptions = {
1910
2553
  */
1911
2554
  declare function createDirectorySyncRepository(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: SyncRepositoryOptions): DirectorySyncRepository;
1912
2555
 
1913
- export { AUTH_DIRECTORY_PROVIDER_COLUMN, AUTH_DIRECTORY_TENANT_SCOPED_TABLES, type ApplyReport, type AuthDirectoryTables, type ClaimedSyncRequest, type DeleteChain, type DirectoryDb, type DirectoryLinkRow, type DirectoryLinkStoreOptions, type DirectorySyncConfig, type DirectorySyncRepository, type DirectorySyncRunResult, type EnsureConnection, type ExternalGroupGuardOptions, type ExternalLinkLookup, type InsertChain, type LedgerIdentityRow, type LedgerIdentityTables, type LinkGroupInput, type LinkableAssertion, MAX_IN_LIST, MAX_PARAMETERS, MEMBERSHIP_MODES, type MemberResolverPort, type PromotionRepository, type PromotionRepositoryOptions, type PromotionResult, type ReferencedTable, type RemovalOutcome, type SelectChain, type SyncLink, type SyncLoggerPort, type SyncMessages, type SyncOutcome, type SyncRepositoryOptions, type SyncRequestQueueOptions, type SyncRequestResult, UNHEALTHY_SYNC_OUTCOMES, type UpdateChain, assertGroupMayBeLinked, assertMembershipMode, chunk, chunkByParameters, createDirectoryLinkStore, createDirectorySync, createDirectorySyncRepository, createPromotionRepository, createSyncRequestQueue, defaultExternalGroupGuardMessage, defaultSyncMessages, defineAuthDirectorySchema, findLedgerRowsForIdentity, promoteLedgerForUser, withExternalGroupGuards };
2556
+ export { AUTH_DIRECTORY_PROVIDER_COLUMN, AUTH_DIRECTORY_TENANT_SCOPED_TABLES, type ApplyReport, type AuthDirectoryTables, type ClaimedSyncRequest, type DeleteChain, type DirectoryCatalogEntry, type DirectoryCatalogReaderOptions, type DirectoryCatalogSnapshot, type DirectoryCatalogWriterOptions, type DirectoryDb, type DirectoryLinkRow, type DirectoryLinkStoreOptions, type DirectorySyncConfig, type DirectorySyncRepository, type DirectorySyncRunResult, type EnsureConnection, type ExternalGroupGuardOptions, type ExternalLinkLookup, type InsertChain, type LedgerIdentityRow, type LedgerIdentityTables, type LinkGroupInput, type LinkableAssertion, MAX_IN_LIST, MAX_PARAMETERS, MEMBERSHIP_MODES, type MemberResolverPort, NEWLY_UNHEALTHY_SYNC_OUTCOMES, type PromotionRepository, type PromotionRepositoryOptions, type PromotionResult, RESERVED_PARAMETERS, type ReferencedTable, type RemovalFailure, type RemovalOutcome, STRANDED_REASON, type SelectChain, type StrandedRequests, type SyncFailureCause, type SyncLink, type SyncLoggerPort, type SyncMessages, type SyncOutcome, type SyncRepositoryOptions, type SyncRequestOutcome, type SyncRequestQueueOptions, type SyncRequestResult, type SyncScope, UNHEALTHY_SYNC_OUTCOMES, type UpdateChain, assertGroupMayBeLinked, assertMembershipMode, chunk, chunkByParameters, classifySyncFailure, createDirectoryCatalogReader, createDirectoryCatalogWriter, createDirectoryLinkStore, createDirectorySync, createDirectorySyncRepository, createPromotionRepository, createSyncRequestQueue, defaultExternalGroupGuardMessage, defaultSyncMessages, defineAuthDirectorySchema, findLedgerRowsForIdentity, promoteLedgerForUser, toInstant, withExternalGroupGuards };