@aiquants/auth-directory-drizzle 0.9.0 → 0.11.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/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # @aiquants/auth-directory-drizzle
2
2
 
3
3
  Drizzle (SQL Server) adapter for [`@aiquants/auth-directory-core`](../auth-directory-core). It owns
4
- the **identity-side** tables and the four surfaces that operate on them.
4
+ the **identity-side** tables, the four surfaces that operate on them, and the member resolver the
5
+ synchronization and the link store share, which reads the host's own user tables.
5
6
 
6
7
  ## Why these tables are not in `@aiquants/authz-drizzle`
7
8
 
@@ -40,11 +41,60 @@ sign-in.
40
41
  | Export | Role |
41
42
  | --- | --- |
42
43
  | `defineAuthDirectorySchema` | Table factory (schema name and referenced tables injected) |
43
- | `createDirectoryLinkStore` | Links, tenant membership proof, group allowlist, sync queue, health |
44
+ | `createDirectoryLinkStore` | Links, tenant membership proof, group allowlist, sync queue, health, unlinked members by reason |
44
45
  | `createDirectorySyncRepository` | Storage operations for one sync run; **removals go through the authz lockout guard** |
46
+ | `createOpenidMemberResolver` | The identity rule for hosts that map OpenID subjects to users in a table, shared by the sync and the link store |
45
47
  | `assertMembershipMode` | The single reader of `membership_mode` — no reader may default it |
46
48
  | `promoteLedgerForUser` | First-sign-in promotion of ledger rows to real memberships |
47
49
  | `withExternalGroupGuards` / `assertGroupMayBeLinked` | Keep administrative roles off externally-sourced groups, in both directions |
50
+ | `DirectoryOperatorError` | The error every refusal above is thrown as; its message is written for the operator |
51
+ | `defaultDirectoryLinkRefusals` | Japanese default for the link store's required `refusals` (break-glass link, allowlist on a local group) |
52
+
53
+ ### Refusals are written for the operator
54
+
55
+ Refusing to link a group that holds an administrative role, to link a break-glass proof group, or to
56
+ let a locally managed group gate sign-in are business reasons the operator has to read — without
57
+ them the action just fails. An admin console that hides undeclared error text (driver errors carry
58
+ table names, constraint names and hosts) would turn each of them into a generic "see the server log".
59
+ So every refusal is a `DirectoryOperatorError`, which carries `operatorFacing: true` — the mark
60
+ `@aiquants/auth-react-router/admin` reads — without this package depending on the UI package.
61
+
62
+ The text comes from the deployment: the guard's `message` and the link store's required `refusals`
63
+ (`defaultDirectoryLinkRefusals` is the Japanese default). Each names the group by its key, which is
64
+ what the operator sees on screen. Failures that are not refusals — a group outside the tenant, a
65
+ failed read, a unique-key violation — carry no mark.
66
+
67
+ ### Unlinked members are counted by reason, with the synchronization's own resolver
68
+
69
+ The ledger keeps every upstream member, including people the identity rule links to no local user.
70
+ One count under one label is false for some of them: a deactivated user, two users sharing an
71
+ address and someone who never signed in are different facts, cleared in different ways. So the link
72
+ store counts them by reason (`AuthDirectoryUnresolvedCounts` from `@aiquants/auth-core`):
73
+
74
+ | Reason | What the rule finds | Cleared by |
75
+ | --- | --- | --- |
76
+ | `ambiguous` | Two or more active users behind the upstream id's live mappings, or, without such a mapping, behind the address | Tidying the users |
77
+ | `claimedByAnother` | One active user for the address, whose live mappings name another upstream id | Confirming who the person is |
78
+ | `pendingSync` | A user now, while the ledger row still holds none | The next synchronization |
79
+ | `deactivated` | A mapping or an address user, but only inactive ones | Reactivation (nothing, when deliberate) |
80
+ | `unregistered` | Nothing, in any state | The person's first sign-in |
81
+
82
+ `listLinks` and `unresolvedByGroup(tenantId)` read the ledger rows with no resolved user and pass
83
+ them, at read time, through the `memberResolver` the store is given — **the same resolver the
84
+ synchronization uses**. The reason is never stored: it changes when a user is reactivated or
85
+ duplicates are merged, and a stored reason would then be false. A resolver answers every member in
86
+ the order asked; the store refuses to count an answer of another length or order.
87
+
88
+ `createOpenidMemberResolver(db, binding, { ensureConnection })` is that resolver for hosts that map
89
+ OpenID subjects to users in a table. `binding` names the user table and the mapping table through
90
+ their drizzle columns (`id`, `email`, `isActive`; `subject`, `userId`, `isActive`), so the package
91
+ holds no table names. It decides in this order, and the first match answers:
92
+
93
+ 1. The upstream id's live mappings (mapping and user both active): one user links, more are `ambiguous`.
94
+ 2. The address's active users: more than one is `ambiguous`; one whose live mappings name another
95
+ upstream id is `claimedByAnother`; otherwise that user links.
96
+ 3. A mapping or an address user that exists only in an inactive state is `deactivated`.
97
+ 4. Anything else is `unregistered`.
48
98
 
49
99
  ### Removals go through the lockout guard, and one refusal does not stop the group
50
100
 
@@ -101,7 +151,8 @@ This adapter accepts drizzle through structural types, so `tsc` checks neither t
101
151
  builder calls nor the presence of a tenant predicate: putting `.innerJoin()` before `.from()`,
102
152
  dropping `.output()`, or forgetting `WHERE tenant_id = ?` all type-check. `link-store.spec.ts` and
103
153
  `sync-repository.spec.ts` therefore record the actual call sequence against a fake builder and walk
104
- each condition for the columns it references.
154
+ each condition for the columns it references. The link store's tenant-boundary table lists every
155
+ store method, and a method missing from it fails the suite.
105
156
 
106
157
  `sync-repository.ts` is the only module that deletes rows, so it is tested directly rather than
107
158
  through the orchestration's fake repository — an orchestration test proves nothing about whether the
@@ -113,12 +164,16 @@ direction, ignoring `appKey` when deciding what is administrative, dropping `is_
113
164
  login gate, collapsing the identity precedence back to an `OR`, treating a removal-only run as
114
165
  "unchanged", and skipping the resolver completeness check.
115
166
 
167
+ `openid-member-resolver.spec.ts` binds the resolver to tables whose drizzle names differ from their
168
+ SQL names, so a resolver that reads a column by name instead of through the binding fails. It walks
169
+ every reason, the order of the decision, and the bound parameters of each of the three reads.
170
+
116
171
  ## Install
117
172
 
118
173
  ```bash
119
174
  pnpm add @aiquants/auth-directory-drizzle @aiquants/auth-directory-core @aiquants/auth-core @aiquants/authz-core @aiquants/authz-drizzle drizzle-orm zod
120
175
  ```
121
176
 
122
- All five `@aiquants/*` peers are **value** imports and every one is listed in this package's tsup
177
+ All four `@aiquants/*` peers are **value** imports and every one is listed in this package's tsup
123
178
  `external`, so omitting any of them yields `ERR_MODULE_NOT_FOUND` at runtime rather than a type
124
179
  error. `zod` is required transitively by `@aiquants/authz-core`.
package/dist/index.d.mts CHANGED
@@ -1,10 +1,10 @@
1
- import { AuthDirectoryCatalogGroup, AuthDirectoryCatalogSnapshot, AuthGroupExternalLink, AuthDirectoryMembershipMode, AuthDirectoryStatus, AuthAllowlistEntry } from "@aiquants/auth-core";
1
+ import { AuthDirectoryCatalogGroup, AuthDirectoryCatalogSnapshot, AuthDirectoryUnresolvedReason, AuthGroupExternalLink, AuthDirectoryMembershipMode as AuthDirectoryMembershipMode$1, AuthDirectoryUnresolvedCounts, AuthDirectoryStatus, AuthAllowlistEntry } from "@aiquants/auth-core";
2
2
  import { TransactionRetryOptions } from "@aiquants/mssql";
3
3
  import * as drizzle_orm_mssql_core from "drizzle-orm/mssql-core";
4
4
  import { AnyMsSqlColumn } from "drizzle-orm/mssql-core";
5
5
  import { AuthzAdminStore } from "@aiquants/authz-core";
6
6
  import { AuthzTables, createAdminLockoutGuard } from "@aiquants/authz-drizzle";
7
- import { TenantMembershipPolicy, SyncAbort, AuthDirectoryMembershipMode as AuthDirectoryMembershipMode$1, LocalGroupSnapshot, DirectoryMember, ResolvedDirectoryMember, GroupSyncPlan, DirectoryGroup, DirectoryProvider, BlastRadiusPolicy } from "@aiquants/auth-directory-core";
7
+ import { SyncAbort, DirectoryMember, AuthDirectoryMembershipMode, LocalGroupSnapshot, GroupSyncPlan, DirectoryGroup, TenantMembershipPolicy, DirectoryProvider, BlastRadiusPolicy } from "@aiquants/auth-directory-core";
8
8
  type Rows<T> = PromiseLike<T[]>;
9
9
  interface SelectChain<T> extends Rows<T> {
10
10
  from(t: unknown): SelectChain<T>;
@@ -1461,86 +1461,6 @@ declare function findLedgerRowsForIdentity(db: DirectoryDb, tables: LedgerIdenti
1461
1461
  externalUserId: string | null;
1462
1462
  email: string;
1463
1463
  }): Promise<LedgerIdentityRow[]>;
1464
- type LinkableAssertion = {
1465
- assertLinkableAndWrite(tenantId: string, groupId: number, write: (runner: unknown) => Promise<void>): Promise<void>;
1466
- };
1467
- type DirectoryLinkStoreOptions = TransactionalOptions & {
1468
- linkable: LinkableAssertion;
1469
- ensureConnection?: EnsureConnection;
1470
- stalenessThresholdSec: number;
1471
- isEnabled: boolean;
1472
- provider: string;
1473
- policyCacheTtlMs?: number;
1474
- };
1475
- type DirectoryLinkRow = AuthGroupExternalLink & {
1476
- linkId: number;
1477
- groupId: number;
1478
- tenantId: string;
1479
- };
1480
- type LinkGroupInput = {
1481
- provider: string;
1482
- externalId: string;
1483
- externalKey: string;
1484
- membershipMode: AuthDirectoryMembershipMode;
1485
- };
1486
- type SyncRequestOutcome = "queued" | "already-pending";
1487
- declare function createDirectoryLinkStore(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: DirectoryLinkStoreOptions): {
1488
- listLinks(tenantId: string): Promise<DirectoryLinkRow[]>;
1489
- externalGroupKey(tenantId: string, groupId: number, runner?: unknown): Promise<string | null>;
1490
- linkGroup(tenantId: string, groupId: number, input: LinkGroupInput, actor: string): Promise<void>;
1491
- unlinkGroup(tenantId: string, groupId: number): Promise<void>;
1492
- setSyncEnabled(tenantId: string, groupId: number, isSyncEnabled: boolean, actor: string): Promise<void>;
1493
- requestSync(tenantId: string, groupId: number | null, requestedBy: string): Promise<SyncRequestOutcome>;
1494
- getStatus(tenantId: string): Promise<AuthDirectoryStatus>;
1495
- loadTenantMembershipPolicy(tenantId: string): Promise<TenantMembershipPolicy>;
1496
- setTenantMembershipGroup(tenantId: string, groupId: number, isBreakGlass: boolean, actor: string): Promise<void>;
1497
- removeTenantMembershipGroup(tenantId: string, groupId: number): Promise<void>;
1498
- setGroupActive(tenantId: string, groupId: number, isActive: boolean, actor: string): Promise<void>;
1499
- listLedgerGroupIds(tenantId: string, identity: {
1500
- externalUserId: string | null;
1501
- email: string | null | undefined;
1502
- }): Promise<number[]>;
1503
- isEmailAllowedByGroup(email: string | null | undefined): Promise<boolean>;
1504
- listAllowlistGroups(tenantId: string): Promise<AuthAllowlistEntry[]>;
1505
- addAllowlistGroup(tenantId: string, groupId: number, description: string | undefined, actor: string): Promise<void>;
1506
- removeAllowlistGroup(tenantId: string, id: number): Promise<void>;
1507
- countProjectedMembers(tenantId: string, groupId: number): Promise<number>;
1508
- };
1509
- declare const MEMBERSHIP_MODES: readonly AuthDirectoryMembershipMode[];
1510
- declare function assertMembershipMode(value: string): AuthDirectoryMembershipMode;
1511
- type PromotionRepository = {
1512
- findLedgerMatches(args: {
1513
- tenantId: string;
1514
- externalUserId: string | null;
1515
- email: string;
1516
- }): Promise<Array<{
1517
- ledgerId: number;
1518
- groupId: number;
1519
- tenantId: string;
1520
- }>>;
1521
- attachUserToGroups(args: {
1522
- tenantId: string;
1523
- userId: number;
1524
- ledgerIds: number[];
1525
- groupIds: number[];
1526
- }): Promise<number[]>;
1527
- };
1528
- type PromotionResult = {
1529
- addedGroupIds: number[];
1530
- matchedLedgerIds: number[];
1531
- };
1532
- declare function promoteLedgerForUser(repository: PromotionRepository, args: {
1533
- tenantId: string;
1534
- userId: number;
1535
- externalUserId: string | null;
1536
- email: string | null | undefined;
1537
- }): Promise<PromotionResult>;
1538
- type PromotionRepositoryOptions = TransactionalOptions & {
1539
- ensureConnection?: EnsureConnection;
1540
- actor: string;
1541
- now?: () => Date;
1542
- };
1543
- declare function createPromotionRepository(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: PromotionRepositoryOptions): PromotionRepository;
1544
1464
  type SyncFailureCause = "denied" | "rateLimited" | "upstreamError" | "timeout" | "localWriteError" | "unknown";
1545
1465
  declare function classifySyncFailure(error: unknown): SyncFailureCause;
1546
1466
  type SyncMessages = {
@@ -1561,6 +1481,14 @@ type SyncMessages = {
1561
1481
  foreignProvider: (linkProvider: string, runnerProvider: string) => string;
1562
1482
  };
1563
1483
  declare const defaultSyncMessages: SyncMessages;
1484
+ type MemberResolutionFailure = Exclude<AuthDirectoryUnresolvedReason, "pendingSync">;
1485
+ type ClassifiedDirectoryMember = DirectoryMember & ({
1486
+ resolvedUserId: number;
1487
+ unresolvedReason: null;
1488
+ } | {
1489
+ resolvedUserId: null;
1490
+ unresolvedReason: MemberResolutionFailure;
1491
+ });
1564
1492
  type SyncLink = {
1565
1493
  linkId: number;
1566
1494
  groupId: number;
@@ -1569,7 +1497,7 @@ type SyncLink = {
1569
1497
  provider: string;
1570
1498
  externalId: string;
1571
1499
  externalKey: string;
1572
- membershipMode: AuthDirectoryMembershipMode$1;
1500
+ membershipMode: AuthDirectoryMembershipMode;
1573
1501
  isGroupActive: boolean;
1574
1502
  vanishedObservations: number;
1575
1503
  };
@@ -1634,7 +1562,7 @@ type ApplyReport = {
1634
1562
  type DirectorySyncRepository = {
1635
1563
  listSyncableLinks(scope: SyncScope): Promise<SyncLink[]>;
1636
1564
  loadGroupSnapshot(link: SyncLink): Promise<LocalGroupSnapshot>;
1637
- resolveMembers(members: readonly DirectoryMember[]): Promise<ResolvedDirectoryMember[]>;
1565
+ resolveMembers(members: readonly DirectoryMember[]): Promise<ClassifiedDirectoryMember[]>;
1638
1566
  applyPlan(link: SyncLink, plan: GroupSyncPlan, observedAt: Date): Promise<ApplyReport>;
1639
1567
  resolveExternalIdentity(link: SyncLink, upstreamGroup: DirectoryGroup): Promise<void>;
1640
1568
  recordSuccess(link: SyncLink, group: DirectoryGroup, syncedAt: Date): Promise<void>;
@@ -1647,6 +1575,127 @@ type SyncLoggerPort = {
1647
1575
  warn(message: string, ...args: unknown[]): void;
1648
1576
  error(message: string, ...args: unknown[]): void;
1649
1577
  };
1578
+ type MemberResolverPort = {
1579
+ resolve(members: readonly DirectoryMember[]): Promise<ClassifiedDirectoryMember[]>;
1580
+ };
1581
+ type SyncRepositoryOptions = TransactionalOptions & {
1582
+ ensureConnection?: EnsureConnection;
1583
+ now?: () => Date;
1584
+ memberResolver: MemberResolverPort;
1585
+ lockoutGuard: ReturnType<typeof createAdminLockoutGuard>;
1586
+ actor: string;
1587
+ };
1588
+ declare function createDirectorySyncRepository(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: SyncRepositoryOptions): DirectorySyncRepository;
1589
+ type LinkableAssertion = {
1590
+ assertLinkableAndWrite(tenantId: string, groupId: number, write: (runner: unknown) => Promise<void>): Promise<void>;
1591
+ };
1592
+ type DirectoryLinkRefusals = {
1593
+ breakGlassLink: (groupKey: string) => string;
1594
+ allowlistNeedsLink: (groupKey: string) => string;
1595
+ };
1596
+ declare const defaultDirectoryLinkRefusals: DirectoryLinkRefusals;
1597
+ type DirectoryLinkStoreOptions = TransactionalOptions & {
1598
+ linkable: LinkableAssertion;
1599
+ refusals: DirectoryLinkRefusals;
1600
+ ensureConnection?: EnsureConnection;
1601
+ memberResolver: MemberResolverPort;
1602
+ stalenessThresholdSec: number;
1603
+ isEnabled: boolean;
1604
+ provider: string;
1605
+ policyCacheTtlMs?: number;
1606
+ };
1607
+ type DirectoryLinkRow = AuthGroupExternalLink & {
1608
+ linkId: number;
1609
+ groupId: number;
1610
+ tenantId: string;
1611
+ };
1612
+ type LinkGroupInput = {
1613
+ provider: string;
1614
+ externalId: string;
1615
+ externalKey: string;
1616
+ membershipMode: AuthDirectoryMembershipMode$1;
1617
+ };
1618
+ type SyncRequestOutcome = "queued" | "already-pending";
1619
+ declare function createDirectoryLinkStore(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: DirectoryLinkStoreOptions): {
1620
+ listLinks(tenantId: string): Promise<DirectoryLinkRow[]>;
1621
+ unresolvedByGroup(tenantId: string): Promise<ReadonlyMap<number, AuthDirectoryUnresolvedCounts>>;
1622
+ externalGroupKey(tenantId: string, groupId: number, runner?: unknown): Promise<string | null>;
1623
+ linkGroup(tenantId: string, groupId: number, input: LinkGroupInput, actor: string): Promise<void>;
1624
+ unlinkGroup(tenantId: string, groupId: number): Promise<void>;
1625
+ setSyncEnabled(tenantId: string, groupId: number, isSyncEnabled: boolean, actor: string): Promise<void>;
1626
+ requestSync(tenantId: string, groupId: number | null, requestedBy: string): Promise<SyncRequestOutcome>;
1627
+ getStatus(tenantId: string): Promise<AuthDirectoryStatus>;
1628
+ loadTenantMembershipPolicy(tenantId: string): Promise<TenantMembershipPolicy>;
1629
+ setTenantMembershipGroup(tenantId: string, groupId: number, isBreakGlass: boolean, actor: string): Promise<void>;
1630
+ removeTenantMembershipGroup(tenantId: string, groupId: number): Promise<void>;
1631
+ setGroupActive(tenantId: string, groupId: number, isActive: boolean, actor: string): Promise<void>;
1632
+ listLedgerGroupIds(tenantId: string, identity: {
1633
+ externalUserId: string | null;
1634
+ email: string | null | undefined;
1635
+ }): Promise<number[]>;
1636
+ isEmailAllowedByGroup(email: string | null | undefined): Promise<boolean>;
1637
+ listAllowlistGroups(tenantId: string): Promise<AuthAllowlistEntry[]>;
1638
+ addAllowlistGroup(tenantId: string, groupId: number, description: string | undefined, actor: string): Promise<void>;
1639
+ removeAllowlistGroup(tenantId: string, id: number): Promise<void>;
1640
+ countProjectedMembers(tenantId: string, groupId: number): Promise<number>;
1641
+ };
1642
+ declare const MEMBERSHIP_MODES: readonly AuthDirectoryMembershipMode$1[];
1643
+ declare function assertMembershipMode(value: string): AuthDirectoryMembershipMode$1;
1644
+ type OpenidIdentityBinding = {
1645
+ users: {
1646
+ table: unknown;
1647
+ id: AnyMsSqlColumn;
1648
+ email: AnyMsSqlColumn;
1649
+ isActive: AnyMsSqlColumn;
1650
+ };
1651
+ subjects: {
1652
+ table: unknown;
1653
+ subject: AnyMsSqlColumn;
1654
+ userId: AnyMsSqlColumn;
1655
+ isActive: AnyMsSqlColumn;
1656
+ };
1657
+ };
1658
+ type OpenidMemberResolverOptions = {
1659
+ ensureConnection?: EnsureConnection;
1660
+ };
1661
+ declare function createOpenidMemberResolver(dbUnknown: unknown, binding: OpenidIdentityBinding, opts: OpenidMemberResolverOptions): MemberResolverPort;
1662
+ declare class DirectoryOperatorError extends Error {
1663
+ readonly operatorFacing = true;
1664
+ constructor(message: string);
1665
+ }
1666
+ type PromotionRepository = {
1667
+ findLedgerMatches(args: {
1668
+ tenantId: string;
1669
+ externalUserId: string | null;
1670
+ email: string;
1671
+ }): Promise<Array<{
1672
+ ledgerId: number;
1673
+ groupId: number;
1674
+ tenantId: string;
1675
+ }>>;
1676
+ attachUserToGroups(args: {
1677
+ tenantId: string;
1678
+ userId: number;
1679
+ ledgerIds: number[];
1680
+ groupIds: number[];
1681
+ }): Promise<number[]>;
1682
+ };
1683
+ type PromotionResult = {
1684
+ addedGroupIds: number[];
1685
+ matchedLedgerIds: number[];
1686
+ };
1687
+ declare function promoteLedgerForUser(repository: PromotionRepository, args: {
1688
+ tenantId: string;
1689
+ userId: number;
1690
+ externalUserId: string | null;
1691
+ email: string | null | undefined;
1692
+ }): Promise<PromotionResult>;
1693
+ type PromotionRepositoryOptions = TransactionalOptions & {
1694
+ ensureConnection?: EnsureConnection;
1695
+ actor: string;
1696
+ now?: () => Date;
1697
+ };
1698
+ declare function createPromotionRepository(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: PromotionRepositoryOptions): PromotionRepository;
1650
1699
  type DirectorySyncConfig = {
1651
1700
  provider: string;
1652
1701
  directory: DirectoryProvider;
@@ -1694,15 +1743,4 @@ declare function createSyncRequestQueue(dbUnknown: unknown, tables: AuthDirector
1694
1743
  listStranded(olderThan: Date): Promise<StrandedRequests>;
1695
1744
  abandonStranded(olderThan: Date): Promise<number[]>;
1696
1745
  };
1697
- type MemberResolverPort = {
1698
- resolve(members: readonly DirectoryMember[]): Promise<ResolvedDirectoryMember[]>;
1699
- };
1700
- type SyncRepositoryOptions = TransactionalOptions & {
1701
- ensureConnection?: EnsureConnection;
1702
- now?: () => Date;
1703
- memberResolver: MemberResolverPort;
1704
- lockoutGuard: ReturnType<typeof createAdminLockoutGuard>;
1705
- actor: string;
1706
- };
1707
- declare function createDirectorySyncRepository(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: SyncRepositoryOptions): DirectorySyncRepository;
1708
- 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, MEMBERSHIP_MODES, type MemberResolverPort, NEWLY_UNHEALTHY_SYNC_OUTCOMES, type PromotionRepository, type PromotionRepositoryOptions, type PromotionResult, 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, type TransactionalOptions, UNHEALTHY_SYNC_OUTCOMES, type UpdateChain, assertGroupMayBeLinked, assertMembershipMode, classifySyncFailure, createDirectoryCatalogReader, createDirectoryCatalogWriter, createDirectoryLinkStore, createDirectorySync, createDirectorySyncRepository, createPromotionRepository, createSyncRequestQueue, defaultExternalGroupGuardMessage, defaultSyncMessages, defineAuthDirectorySchema, findLedgerRowsForIdentity, promoteLedgerForUser, transactionRunner, withExternalGroupGuards };
1746
+ export { AUTH_DIRECTORY_PROVIDER_COLUMN, AUTH_DIRECTORY_TENANT_SCOPED_TABLES, type ApplyReport, type AuthDirectoryTables, type ClaimedSyncRequest, type ClassifiedDirectoryMember, type DeleteChain, type DirectoryCatalogEntry, type DirectoryCatalogReaderOptions, type DirectoryCatalogSnapshot, type DirectoryCatalogWriterOptions, type DirectoryDb, type DirectoryLinkRefusals, type DirectoryLinkRow, type DirectoryLinkStoreOptions, DirectoryOperatorError, type DirectorySyncConfig, type DirectorySyncRepository, type DirectorySyncRunResult, type EnsureConnection, type ExternalGroupGuardOptions, type ExternalLinkLookup, type InsertChain, type LedgerIdentityRow, type LedgerIdentityTables, type LinkGroupInput, type LinkableAssertion, MEMBERSHIP_MODES, type MemberResolutionFailure, type MemberResolverPort, NEWLY_UNHEALTHY_SYNC_OUTCOMES, type OpenidIdentityBinding, type OpenidMemberResolverOptions, type PromotionRepository, type PromotionRepositoryOptions, type PromotionResult, 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, type TransactionalOptions, UNHEALTHY_SYNC_OUTCOMES, type UpdateChain, assertGroupMayBeLinked, assertMembershipMode, classifySyncFailure, createDirectoryCatalogReader, createDirectoryCatalogWriter, createDirectoryLinkStore, createDirectorySync, createDirectorySyncRepository, createOpenidMemberResolver, createPromotionRepository, createSyncRequestQueue, defaultDirectoryLinkRefusals, defaultExternalGroupGuardMessage, defaultSyncMessages, defineAuthDirectorySchema, findLedgerRowsForIdentity, promoteLedgerForUser, transactionRunner, withExternalGroupGuards };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { AuthDirectoryCatalogGroup, AuthDirectoryCatalogSnapshot, AuthGroupExternalLink, AuthDirectoryMembershipMode, AuthDirectoryStatus, AuthAllowlistEntry } from "@aiquants/auth-core";
1
+ import { AuthDirectoryCatalogGroup, AuthDirectoryCatalogSnapshot, AuthDirectoryUnresolvedReason, AuthGroupExternalLink, AuthDirectoryMembershipMode as AuthDirectoryMembershipMode$1, AuthDirectoryUnresolvedCounts, AuthDirectoryStatus, AuthAllowlistEntry } from "@aiquants/auth-core";
2
2
  import { TransactionRetryOptions } from "@aiquants/mssql";
3
3
  import * as drizzle_orm_mssql_core from "drizzle-orm/mssql-core";
4
4
  import { AnyMsSqlColumn } from "drizzle-orm/mssql-core";
5
5
  import { AuthzAdminStore } from "@aiquants/authz-core";
6
6
  import { AuthzTables, createAdminLockoutGuard } from "@aiquants/authz-drizzle";
7
- import { TenantMembershipPolicy, SyncAbort, AuthDirectoryMembershipMode as AuthDirectoryMembershipMode$1, LocalGroupSnapshot, DirectoryMember, ResolvedDirectoryMember, GroupSyncPlan, DirectoryGroup, DirectoryProvider, BlastRadiusPolicy } from "@aiquants/auth-directory-core";
7
+ import { SyncAbort, DirectoryMember, AuthDirectoryMembershipMode, LocalGroupSnapshot, GroupSyncPlan, DirectoryGroup, TenantMembershipPolicy, DirectoryProvider, BlastRadiusPolicy } from "@aiquants/auth-directory-core";
8
8
  type Rows<T> = PromiseLike<T[]>;
9
9
  interface SelectChain<T> extends Rows<T> {
10
10
  from(t: unknown): SelectChain<T>;
@@ -1461,86 +1461,6 @@ declare function findLedgerRowsForIdentity(db: DirectoryDb, tables: LedgerIdenti
1461
1461
  externalUserId: string | null;
1462
1462
  email: string;
1463
1463
  }): Promise<LedgerIdentityRow[]>;
1464
- type LinkableAssertion = {
1465
- assertLinkableAndWrite(tenantId: string, groupId: number, write: (runner: unknown) => Promise<void>): Promise<void>;
1466
- };
1467
- type DirectoryLinkStoreOptions = TransactionalOptions & {
1468
- linkable: LinkableAssertion;
1469
- ensureConnection?: EnsureConnection;
1470
- stalenessThresholdSec: number;
1471
- isEnabled: boolean;
1472
- provider: string;
1473
- policyCacheTtlMs?: number;
1474
- };
1475
- type DirectoryLinkRow = AuthGroupExternalLink & {
1476
- linkId: number;
1477
- groupId: number;
1478
- tenantId: string;
1479
- };
1480
- type LinkGroupInput = {
1481
- provider: string;
1482
- externalId: string;
1483
- externalKey: string;
1484
- membershipMode: AuthDirectoryMembershipMode;
1485
- };
1486
- type SyncRequestOutcome = "queued" | "already-pending";
1487
- declare function createDirectoryLinkStore(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: DirectoryLinkStoreOptions): {
1488
- listLinks(tenantId: string): Promise<DirectoryLinkRow[]>;
1489
- externalGroupKey(tenantId: string, groupId: number, runner?: unknown): Promise<string | null>;
1490
- linkGroup(tenantId: string, groupId: number, input: LinkGroupInput, actor: string): Promise<void>;
1491
- unlinkGroup(tenantId: string, groupId: number): Promise<void>;
1492
- setSyncEnabled(tenantId: string, groupId: number, isSyncEnabled: boolean, actor: string): Promise<void>;
1493
- requestSync(tenantId: string, groupId: number | null, requestedBy: string): Promise<SyncRequestOutcome>;
1494
- getStatus(tenantId: string): Promise<AuthDirectoryStatus>;
1495
- loadTenantMembershipPolicy(tenantId: string): Promise<TenantMembershipPolicy>;
1496
- setTenantMembershipGroup(tenantId: string, groupId: number, isBreakGlass: boolean, actor: string): Promise<void>;
1497
- removeTenantMembershipGroup(tenantId: string, groupId: number): Promise<void>;
1498
- setGroupActive(tenantId: string, groupId: number, isActive: boolean, actor: string): Promise<void>;
1499
- listLedgerGroupIds(tenantId: string, identity: {
1500
- externalUserId: string | null;
1501
- email: string | null | undefined;
1502
- }): Promise<number[]>;
1503
- isEmailAllowedByGroup(email: string | null | undefined): Promise<boolean>;
1504
- listAllowlistGroups(tenantId: string): Promise<AuthAllowlistEntry[]>;
1505
- addAllowlistGroup(tenantId: string, groupId: number, description: string | undefined, actor: string): Promise<void>;
1506
- removeAllowlistGroup(tenantId: string, id: number): Promise<void>;
1507
- countProjectedMembers(tenantId: string, groupId: number): Promise<number>;
1508
- };
1509
- declare const MEMBERSHIP_MODES: readonly AuthDirectoryMembershipMode[];
1510
- declare function assertMembershipMode(value: string): AuthDirectoryMembershipMode;
1511
- type PromotionRepository = {
1512
- findLedgerMatches(args: {
1513
- tenantId: string;
1514
- externalUserId: string | null;
1515
- email: string;
1516
- }): Promise<Array<{
1517
- ledgerId: number;
1518
- groupId: number;
1519
- tenantId: string;
1520
- }>>;
1521
- attachUserToGroups(args: {
1522
- tenantId: string;
1523
- userId: number;
1524
- ledgerIds: number[];
1525
- groupIds: number[];
1526
- }): Promise<number[]>;
1527
- };
1528
- type PromotionResult = {
1529
- addedGroupIds: number[];
1530
- matchedLedgerIds: number[];
1531
- };
1532
- declare function promoteLedgerForUser(repository: PromotionRepository, args: {
1533
- tenantId: string;
1534
- userId: number;
1535
- externalUserId: string | null;
1536
- email: string | null | undefined;
1537
- }): Promise<PromotionResult>;
1538
- type PromotionRepositoryOptions = TransactionalOptions & {
1539
- ensureConnection?: EnsureConnection;
1540
- actor: string;
1541
- now?: () => Date;
1542
- };
1543
- declare function createPromotionRepository(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: PromotionRepositoryOptions): PromotionRepository;
1544
1464
  type SyncFailureCause = "denied" | "rateLimited" | "upstreamError" | "timeout" | "localWriteError" | "unknown";
1545
1465
  declare function classifySyncFailure(error: unknown): SyncFailureCause;
1546
1466
  type SyncMessages = {
@@ -1561,6 +1481,14 @@ type SyncMessages = {
1561
1481
  foreignProvider: (linkProvider: string, runnerProvider: string) => string;
1562
1482
  };
1563
1483
  declare const defaultSyncMessages: SyncMessages;
1484
+ type MemberResolutionFailure = Exclude<AuthDirectoryUnresolvedReason, "pendingSync">;
1485
+ type ClassifiedDirectoryMember = DirectoryMember & ({
1486
+ resolvedUserId: number;
1487
+ unresolvedReason: null;
1488
+ } | {
1489
+ resolvedUserId: null;
1490
+ unresolvedReason: MemberResolutionFailure;
1491
+ });
1564
1492
  type SyncLink = {
1565
1493
  linkId: number;
1566
1494
  groupId: number;
@@ -1569,7 +1497,7 @@ type SyncLink = {
1569
1497
  provider: string;
1570
1498
  externalId: string;
1571
1499
  externalKey: string;
1572
- membershipMode: AuthDirectoryMembershipMode$1;
1500
+ membershipMode: AuthDirectoryMembershipMode;
1573
1501
  isGroupActive: boolean;
1574
1502
  vanishedObservations: number;
1575
1503
  };
@@ -1634,7 +1562,7 @@ type ApplyReport = {
1634
1562
  type DirectorySyncRepository = {
1635
1563
  listSyncableLinks(scope: SyncScope): Promise<SyncLink[]>;
1636
1564
  loadGroupSnapshot(link: SyncLink): Promise<LocalGroupSnapshot>;
1637
- resolveMembers(members: readonly DirectoryMember[]): Promise<ResolvedDirectoryMember[]>;
1565
+ resolveMembers(members: readonly DirectoryMember[]): Promise<ClassifiedDirectoryMember[]>;
1638
1566
  applyPlan(link: SyncLink, plan: GroupSyncPlan, observedAt: Date): Promise<ApplyReport>;
1639
1567
  resolveExternalIdentity(link: SyncLink, upstreamGroup: DirectoryGroup): Promise<void>;
1640
1568
  recordSuccess(link: SyncLink, group: DirectoryGroup, syncedAt: Date): Promise<void>;
@@ -1647,6 +1575,127 @@ type SyncLoggerPort = {
1647
1575
  warn(message: string, ...args: unknown[]): void;
1648
1576
  error(message: string, ...args: unknown[]): void;
1649
1577
  };
1578
+ type MemberResolverPort = {
1579
+ resolve(members: readonly DirectoryMember[]): Promise<ClassifiedDirectoryMember[]>;
1580
+ };
1581
+ type SyncRepositoryOptions = TransactionalOptions & {
1582
+ ensureConnection?: EnsureConnection;
1583
+ now?: () => Date;
1584
+ memberResolver: MemberResolverPort;
1585
+ lockoutGuard: ReturnType<typeof createAdminLockoutGuard>;
1586
+ actor: string;
1587
+ };
1588
+ declare function createDirectorySyncRepository(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: SyncRepositoryOptions): DirectorySyncRepository;
1589
+ type LinkableAssertion = {
1590
+ assertLinkableAndWrite(tenantId: string, groupId: number, write: (runner: unknown) => Promise<void>): Promise<void>;
1591
+ };
1592
+ type DirectoryLinkRefusals = {
1593
+ breakGlassLink: (groupKey: string) => string;
1594
+ allowlistNeedsLink: (groupKey: string) => string;
1595
+ };
1596
+ declare const defaultDirectoryLinkRefusals: DirectoryLinkRefusals;
1597
+ type DirectoryLinkStoreOptions = TransactionalOptions & {
1598
+ linkable: LinkableAssertion;
1599
+ refusals: DirectoryLinkRefusals;
1600
+ ensureConnection?: EnsureConnection;
1601
+ memberResolver: MemberResolverPort;
1602
+ stalenessThresholdSec: number;
1603
+ isEnabled: boolean;
1604
+ provider: string;
1605
+ policyCacheTtlMs?: number;
1606
+ };
1607
+ type DirectoryLinkRow = AuthGroupExternalLink & {
1608
+ linkId: number;
1609
+ groupId: number;
1610
+ tenantId: string;
1611
+ };
1612
+ type LinkGroupInput = {
1613
+ provider: string;
1614
+ externalId: string;
1615
+ externalKey: string;
1616
+ membershipMode: AuthDirectoryMembershipMode$1;
1617
+ };
1618
+ type SyncRequestOutcome = "queued" | "already-pending";
1619
+ declare function createDirectoryLinkStore(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: DirectoryLinkStoreOptions): {
1620
+ listLinks(tenantId: string): Promise<DirectoryLinkRow[]>;
1621
+ unresolvedByGroup(tenantId: string): Promise<ReadonlyMap<number, AuthDirectoryUnresolvedCounts>>;
1622
+ externalGroupKey(tenantId: string, groupId: number, runner?: unknown): Promise<string | null>;
1623
+ linkGroup(tenantId: string, groupId: number, input: LinkGroupInput, actor: string): Promise<void>;
1624
+ unlinkGroup(tenantId: string, groupId: number): Promise<void>;
1625
+ setSyncEnabled(tenantId: string, groupId: number, isSyncEnabled: boolean, actor: string): Promise<void>;
1626
+ requestSync(tenantId: string, groupId: number | null, requestedBy: string): Promise<SyncRequestOutcome>;
1627
+ getStatus(tenantId: string): Promise<AuthDirectoryStatus>;
1628
+ loadTenantMembershipPolicy(tenantId: string): Promise<TenantMembershipPolicy>;
1629
+ setTenantMembershipGroup(tenantId: string, groupId: number, isBreakGlass: boolean, actor: string): Promise<void>;
1630
+ removeTenantMembershipGroup(tenantId: string, groupId: number): Promise<void>;
1631
+ setGroupActive(tenantId: string, groupId: number, isActive: boolean, actor: string): Promise<void>;
1632
+ listLedgerGroupIds(tenantId: string, identity: {
1633
+ externalUserId: string | null;
1634
+ email: string | null | undefined;
1635
+ }): Promise<number[]>;
1636
+ isEmailAllowedByGroup(email: string | null | undefined): Promise<boolean>;
1637
+ listAllowlistGroups(tenantId: string): Promise<AuthAllowlistEntry[]>;
1638
+ addAllowlistGroup(tenantId: string, groupId: number, description: string | undefined, actor: string): Promise<void>;
1639
+ removeAllowlistGroup(tenantId: string, id: number): Promise<void>;
1640
+ countProjectedMembers(tenantId: string, groupId: number): Promise<number>;
1641
+ };
1642
+ declare const MEMBERSHIP_MODES: readonly AuthDirectoryMembershipMode$1[];
1643
+ declare function assertMembershipMode(value: string): AuthDirectoryMembershipMode$1;
1644
+ type OpenidIdentityBinding = {
1645
+ users: {
1646
+ table: unknown;
1647
+ id: AnyMsSqlColumn;
1648
+ email: AnyMsSqlColumn;
1649
+ isActive: AnyMsSqlColumn;
1650
+ };
1651
+ subjects: {
1652
+ table: unknown;
1653
+ subject: AnyMsSqlColumn;
1654
+ userId: AnyMsSqlColumn;
1655
+ isActive: AnyMsSqlColumn;
1656
+ };
1657
+ };
1658
+ type OpenidMemberResolverOptions = {
1659
+ ensureConnection?: EnsureConnection;
1660
+ };
1661
+ declare function createOpenidMemberResolver(dbUnknown: unknown, binding: OpenidIdentityBinding, opts: OpenidMemberResolverOptions): MemberResolverPort;
1662
+ declare class DirectoryOperatorError extends Error {
1663
+ readonly operatorFacing = true;
1664
+ constructor(message: string);
1665
+ }
1666
+ type PromotionRepository = {
1667
+ findLedgerMatches(args: {
1668
+ tenantId: string;
1669
+ externalUserId: string | null;
1670
+ email: string;
1671
+ }): Promise<Array<{
1672
+ ledgerId: number;
1673
+ groupId: number;
1674
+ tenantId: string;
1675
+ }>>;
1676
+ attachUserToGroups(args: {
1677
+ tenantId: string;
1678
+ userId: number;
1679
+ ledgerIds: number[];
1680
+ groupIds: number[];
1681
+ }): Promise<number[]>;
1682
+ };
1683
+ type PromotionResult = {
1684
+ addedGroupIds: number[];
1685
+ matchedLedgerIds: number[];
1686
+ };
1687
+ declare function promoteLedgerForUser(repository: PromotionRepository, args: {
1688
+ tenantId: string;
1689
+ userId: number;
1690
+ externalUserId: string | null;
1691
+ email: string | null | undefined;
1692
+ }): Promise<PromotionResult>;
1693
+ type PromotionRepositoryOptions = TransactionalOptions & {
1694
+ ensureConnection?: EnsureConnection;
1695
+ actor: string;
1696
+ now?: () => Date;
1697
+ };
1698
+ declare function createPromotionRepository(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: PromotionRepositoryOptions): PromotionRepository;
1650
1699
  type DirectorySyncConfig = {
1651
1700
  provider: string;
1652
1701
  directory: DirectoryProvider;
@@ -1694,15 +1743,4 @@ declare function createSyncRequestQueue(dbUnknown: unknown, tables: AuthDirector
1694
1743
  listStranded(olderThan: Date): Promise<StrandedRequests>;
1695
1744
  abandonStranded(olderThan: Date): Promise<number[]>;
1696
1745
  };
1697
- type MemberResolverPort = {
1698
- resolve(members: readonly DirectoryMember[]): Promise<ResolvedDirectoryMember[]>;
1699
- };
1700
- type SyncRepositoryOptions = TransactionalOptions & {
1701
- ensureConnection?: EnsureConnection;
1702
- now?: () => Date;
1703
- memberResolver: MemberResolverPort;
1704
- lockoutGuard: ReturnType<typeof createAdminLockoutGuard>;
1705
- actor: string;
1706
- };
1707
- declare function createDirectorySyncRepository(dbUnknown: unknown, tables: AuthDirectoryTables, authzTables: AuthzTables, opts: SyncRepositoryOptions): DirectorySyncRepository;
1708
- 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, MEMBERSHIP_MODES, type MemberResolverPort, NEWLY_UNHEALTHY_SYNC_OUTCOMES, type PromotionRepository, type PromotionRepositoryOptions, type PromotionResult, 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, type TransactionalOptions, UNHEALTHY_SYNC_OUTCOMES, type UpdateChain, assertGroupMayBeLinked, assertMembershipMode, classifySyncFailure, createDirectoryCatalogReader, createDirectoryCatalogWriter, createDirectoryLinkStore, createDirectorySync, createDirectorySyncRepository, createPromotionRepository, createSyncRequestQueue, defaultExternalGroupGuardMessage, defaultSyncMessages, defineAuthDirectorySchema, findLedgerRowsForIdentity, promoteLedgerForUser, transactionRunner, withExternalGroupGuards };
1746
+ export { AUTH_DIRECTORY_PROVIDER_COLUMN, AUTH_DIRECTORY_TENANT_SCOPED_TABLES, type ApplyReport, type AuthDirectoryTables, type ClaimedSyncRequest, type ClassifiedDirectoryMember, type DeleteChain, type DirectoryCatalogEntry, type DirectoryCatalogReaderOptions, type DirectoryCatalogSnapshot, type DirectoryCatalogWriterOptions, type DirectoryDb, type DirectoryLinkRefusals, type DirectoryLinkRow, type DirectoryLinkStoreOptions, DirectoryOperatorError, type DirectorySyncConfig, type DirectorySyncRepository, type DirectorySyncRunResult, type EnsureConnection, type ExternalGroupGuardOptions, type ExternalLinkLookup, type InsertChain, type LedgerIdentityRow, type LedgerIdentityTables, type LinkGroupInput, type LinkableAssertion, MEMBERSHIP_MODES, type MemberResolutionFailure, type MemberResolverPort, NEWLY_UNHEALTHY_SYNC_OUTCOMES, type OpenidIdentityBinding, type OpenidMemberResolverOptions, type PromotionRepository, type PromotionRepositoryOptions, type PromotionResult, 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, type TransactionalOptions, UNHEALTHY_SYNC_OUTCOMES, type UpdateChain, assertGroupMayBeLinked, assertMembershipMode, classifySyncFailure, createDirectoryCatalogReader, createDirectoryCatalogWriter, createDirectoryLinkStore, createDirectorySync, createDirectorySyncRepository, createOpenidMemberResolver, createPromotionRepository, createSyncRequestQueue, defaultDirectoryLinkRefusals, defaultExternalGroupGuardMessage, defaultSyncMessages, defineAuthDirectorySchema, findLedgerRowsForIdentity, promoteLedgerForUser, transactionRunner, withExternalGroupGuards };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var ae=Object.defineProperty;var Ge=Object.getOwnPropertyDescriptor;var Ee=Object.getOwnPropertyNames;var Me=Object.prototype.hasOwnProperty;var Ce=(e,n)=>{for(var m in n)ae(e,m,{get:n[m],enumerable:!0})},Pe=(e,n,m,d)=>{if(n&&typeof n=="object"||typeof n=="function")for(let l of Ee(n))!Me.call(e,l)&&l!==m&&ae(e,l,{get:()=>n[l],enumerable:!(d=Ge(n,l))||d.enumerable});return e};var _e=e=>Pe(ae({},"__esModule",{value:!0}),e);var rr={};Ce(rr,{AUTH_DIRECTORY_PROVIDER_COLUMN:()=>ne,AUTH_DIRECTORY_TENANT_SCOPED_TABLES:()=>He,MEMBERSHIP_MODES:()=>ue,NEWLY_UNHEALTHY_SYNC_OUTCOMES:()=>oe,STRANDED_REASON:()=>Ae,UNHEALTHY_SYNC_OUTCOMES:()=>me,assertGroupMayBeLinked:()=>Ue,assertMembershipMode:()=>U,classifySyncFailure:()=>H,createDirectoryCatalogReader:()=>$e,createDirectoryCatalogWriter:()=>Oe,createDirectoryLinkStore:()=>ze,createDirectorySync:()=>Ye,createDirectorySyncRepository:()=>er,createPromotionRepository:()=>We,createSyncRequestQueue:()=>Ve,defaultExternalGroupGuardMessage:()=>Ne,defaultSyncMessages:()=>pe,defineAuthDirectorySchema:()=>Je,findLedgerRowsForIdentity:()=>W,promoteLedgerForUser:()=>Fe,transactionRunner:()=>K,withExternalGroupGuards:()=>qe});module.exports=_e(rr);var se=require("@aiquants/authz-core"),Q=require("@aiquants/mssql"),_=require("drizzle-orm");var ge=require("@aiquants/mssql");function K(e,n){return m=>(0,ge.runWithTransactionRetry)(d=>e.transaction(l=>d(()=>m(l))),n.transactionRetry)}var Le=10,O={tenantId:64,provider:32,externalId:256,externalKey:320,name:256,description:1e3,actor:50},he=2147483647;function Ke(e){if(e===void 0||!Number.isFinite(e))return null;let n=Math.trunc(e);return n<0?null:n>he?he:n}function F(e,n,m){let d=e.trim();if(d==="")throw new TypeError(`[@aiquants/auth-directory-drizzle] ${m} must not be blank`);if(d.length>n)throw new TypeError(`[@aiquants/auth-directory-drizzle] ${m} exceeds its column length (${n}); truncating it would change which rows it owns`);return d}function Oe(e,n,m){let d=e,l=m.ensureConnection??(async()=>{}),s=K(d,m),f=n.TDDirectoryCatalog,v=n.TMDirectoryCatalogObservation,w=h=>{let a=new Map;for(let t of h)a.set(t.externalId,t);return[...a.values()]};return{async replace(h,a,t){await l();let o=F((0,se.assertTenantId)(h,"replace(tenantId)"),O.tenantId,"tenantId"),r=F(a,O.provider,"provider"),g=F(m.actor,O.actor,"actor"),u=m.now(),I=w(t.map(D=>({tenantId:o,provider:r,externalId:D.externalId.slice(0,O.externalId),externalKey:D.externalKey.slice(0,O.externalKey),name:D.name.slice(0,O.name),description:D.description===void 0?null:D.description.slice(0,O.description),memberCount:Ke(D.memberCount),observedAt:u,createdAt:u,createdBy:g})));return await s(async D=>{await D.delete(f).where((0,_.and)((0,_.eq)(f.tenantId,o),(0,_.eq)(f.provider,r)));for(let A of(0,Q.chunkByParameters)(I,Le))await D.insert(f).values(A);await D.delete(v).where((0,_.and)((0,_.eq)(v.tenantId,o),(0,_.eq)(v.provider,r))),await D.insert(v).values([{tenantId:o,provider:r,observedAt:u,entryCount:I.length,createdAt:u,createdBy:g,updatedAt:u,updatedBy:g}])}),I.length}}}function $e(e,n,m={}){let d=e,l=m.ensureConnection??(async()=>{}),s=n.TDDirectoryCatalog,f=n.TMDirectoryCatalogObservation;return{async read(v,w){await l();let h=F((0,se.assertTenantId)(v,"read(tenantId)"),O.tenantId,"tenantId"),a=F(w,O.provider,"provider"),t=await d.select({observedAt:f.observedAt,entryCount:f.entryCount,externalId:s.externalId,externalKey:s.externalKey,name:s.name,description:s.description,memberCount:s.memberCount}).from(f).leftJoin(s,(0,_.and)((0,_.eq)(s.tenantId,f.tenantId),(0,_.eq)(s.provider,f.provider))).where((0,_.and)((0,_.eq)(f.tenantId,h),(0,_.eq)(f.provider,a))).orderBy((0,_.asc)(s.name)),o=t.filter(u=>u.externalId!==null).map(u=>({externalId:String(u.externalId),externalKey:String(u.externalKey),name:String(u.name),...u.description===null?{}:{description:u.description},...u.memberCount===null?{}:{memberCount:Number(u.memberCount)}})),r=t[0];if(r===void 0)return{groups:o,observedAt:null};if(Number(r.entryCount)!==o.length)throw new Error(`[@aiquants/auth-directory-drizzle] catalog is inconsistent for (${h}, ${a}): the observation records ${r.entryCount} entries but ${o.length} rows are present`);let g=(0,Q.toInstant)(r.observedAt);if(!Number.isFinite(g.getTime()))throw new Error(`[@aiquants/auth-directory-drizzle] catalog observation for (${h}, ${a}) has an unreadable observed_at`);return{groups:o,observedAt:g}}}}var be=require("@aiquants/authz-core"),Ne=(e,n)=>`\u5916\u90E8\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u9023\u643A\u30B0\u30EB\u30FC\u30D7 (${n}) \u306B\u306F\u7BA1\u7406\u30ED\u30FC\u30EB (${e}) \u3092\u6301\u305F\u305B\u3089\u308C\u307E\u305B\u3093\u3002\u4E0A\u6D41\u5074\u306E\u6240\u5C5E\u5909\u66F4\u304C\u305D\u306E\u307E\u307E\u7BA1\u7406\u8005\u306E\u5897\u6E1B\u306B\u306A\u308B\u305F\u3081\u3001\u7BA1\u7406\u8005\u7D4C\u8DEF\u306F\u30ED\u30FC\u30AB\u30EB\u7BA1\u7406\u306E\u30B0\u30EB\u30FC\u30D7\u306B\u9650\u5B9A\u3057\u3066\u3044\u307E\u3059\u3002`;function q(e,n,m,d){return new Set(e.filter(l=>n.has(l.roleId)&&l.appKey===m&&l.resourceKey===d).map(l=>l.action))}function Z(e,n){return new Set(e.filter(m=>m.groupId===n).map(m=>m.roleId))}var j=e=>be.AUTHZ_ACTIONS.every(n=>e.has(n)),ee=(e,n)=>e.find(m=>m.roleId===n)?.roleKey??`#${n}`;function X(e,n,m,d){let l=[...n].find(f=>j(q(e,new Set([f]),m,d)));return l!==void 0?ee(e,l):[...n].filter(f=>q(e,new Set([f]),m,d).size>0).map(f=>ee(e,f)).join(" + ")}function qe(e,n){return fe(e,n)}function fe(e,n){let m=e;return{...e,withTransaction:(d,l)=>m.withTransaction((s,f)=>d(fe(s,n),f),l),async assignGroupRole(d,l,s,f){await m.withTransaction(async(v,w)=>{let h=await v.listGrants(d),a=Z(await v.listGroupRoles(d),l);if(a.add(s),j(q(h,a,n.adminAppKey,n.adminResourceKey))){let t=await n.linkLookup.externalGroupKey(d,l,w);if(t!==null)throw new Error(n.message(X(h,a,n.adminAppKey,n.adminResourceKey),t))}await v.assignGroupRole(d,l,s,f)})},async updateGrant(d,l,s,f){let v=s.action;if(v===void 0)return m.updateGrant(d,l,s,f);await m.withTransaction(async(w,h)=>{let a=await w.listGrants(d),t=a.find(g=>g.id===l);if(t===void 0)return w.updateGrant(d,l,s,f);let r=(await w.listResources(d)).find(g=>g.id===t.resourceId);if(r?.appKey===n.adminAppKey&&r?.resourceKey===n.adminResourceKey){let g=await w.listGroupRoles(d);for(let u of new Set(g.filter(I=>I.roleId===t.roleId).map(I=>I.groupId))){let I=Z(g,u),D=q(a.filter(x=>x.id!==l),I,n.adminAppKey,n.adminResourceKey);if(I.has(t.roleId)&&D.add(v),!j(D))continue;let A=await n.linkLookup.externalGroupKey(d,u,h);if(A!==null)throw new Error(n.message(X(a,I,n.adminAppKey,n.adminResourceKey)||ee(a,t.roleId),A))}}await w.updateGrant(d,l,s,f)})},async createGrant(d,l,s){await m.withTransaction(async(f,v)=>{let h=(await f.listResources(d)).find(a=>a.id===l.resourceId);if(h?.appKey===n.adminAppKey&&h?.resourceKey===n.adminResourceKey){let a=await f.listGrants(d),t=await f.listGroupRoles(d);for(let o of new Set(t.filter(r=>r.roleId===l.roleId).map(r=>r.groupId))){let r=q(a,Z(t,o),n.adminAppKey,n.adminResourceKey);if(r.add(l.action),!j(r))continue;let g=await n.linkLookup.externalGroupKey(d,o,v);if(g!==null)throw new Error(n.message(X(a,Z(t,o),n.adminAppKey,n.adminResourceKey)||ee(a,l.roleId),g))}}await f.createGrant(d,l,s)})}}}async function Ue(e,n,m,d){let l=(await e.listGroupRoles(n)).filter(w=>w.groupId===m);if(l.length===0)return;let s=await e.listGrants(n),f=new Set(l.map(w=>w.roleId));if(!j(q(s,f,d.adminAppKey,d.adminResourceKey)))return;let v=l[0];if(v)throw new Error(d.message(X(s,f,d.adminAppKey,d.adminResourceKey),v.groupKey))}var $=require("drizzle-orm");async function W(e,n,m){let{ledger:d,group:l}=n,{tenantId:s,externalUserId:f,email:v}=m;if(f===null&&v==="")return[];let w={ledgerId:d.id,groupId:d.groupId,tenantId:l.tenantId,externalUserId:d.externalUserId},h=async g=>(await e.select(w).from(d).innerJoin(l,(0,$.and)((0,$.eq)(l.id,d.groupId),(0,$.eq)(l.tenantId,s),(0,$.eq)(l.isActive,!0))).where(g)).map(I=>({ledgerId:Number(I.ledgerId),groupId:Number(I.groupId),tenantId:String(I.tenantId),externalUserId:I.externalUserId===null?null:String(I.externalUserId)})),a=g=>{let u=new Map;for(let I of g)u.set(I.ledgerId,{ledgerId:I.ledgerId,groupId:I.groupId,tenantId:I.tenantId});return[...u.values()].sort((I,D)=>I.ledgerId-D.ledgerId)},t=f===null?[]:await h((0,$.eq)(d.externalUserId,f));if(v==="")return a(t);let o=await h((0,$.eq)(d.email,v)),r=g=>g.externalUserId!==null&&g.externalUserId!==f;return a([...t,...o.filter(g=>!r(g))])}var re=require("@aiquants/auth-core"),we=require("@aiquants/auth-directory-core"),C=require("@aiquants/authz-core"),de=require("@aiquants/mssql"),c=require("drizzle-orm");var ue=["direct","transitive"];function U(e){if(!ue.includes(e))throw new TypeError(`[@aiquants/auth-directory-drizzle] membershipMode must be one of ${ue.join(" | ")}, received ${JSON.stringify(e)}`);return e}function Be(e){let n=typeof e=="object"&&e!==null&&"number"in e?e.number:void 0;return n===2601||n===2627}function ze(e,n,m,d){let l=e,s=d.ensureConnection??(async()=>{}),f=K(l,d),v=d.policyCacheTtlMs??0;if(!Number.isFinite(v)||v<0)throw new TypeError(`[@aiquants/auth-directory-drizzle] policyCacheTtlMs must be a non-negative number, received ${String(d.policyCacheTtlMs)}`);let w=new Map,h=i=>w.delete(i),{TDGroupExternalLink:a,TDGroupMemberEmail:t,TDTenantMembershipGroup:o,TDAllowlistGroup:r,TDDirectorySyncRequest:g}=n,{TMGroup:u,TDGroupMember:I}=m;async function D(i,y,b){if((await i.select({groupId:o.groupId}).from(o).where((0,c.and)((0,c.eq)(o.tenantId,y),(0,c.eq)(o.groupId,b),(0,c.eq)(o.isBreakGlass,!0)))).length>0)throw new Error(`Group ${b} is a break-glass proof group; a break-glass proof path must be one the synchronization can never touch.`)}async function A(i,y,b){let[S]=await i.select({id:u.id}).from(u).where((0,c.and)((0,c.eq)(u.id,b),(0,c.eq)(u.tenantId,y)));if(!S)throw new Error(`Group not found in tenant: ${b}`)}function x(i,y){return{linkId:Number(i.linkId),groupId:Number(i.groupId),tenantId:String(i.tenantId),provider:String(i.provider),externalId:String(i.externalId),externalKey:String(i.externalKey),membershipMode:U(String(i.membershipMode)),isSyncEnabled:!!i.isSyncEnabled,...i.syncedAt?{syncedAt:(0,de.toInstant)(i.syncedAt).toISOString()}:{},...i.syncError?{syncError:String(i.syncError)}:{},unresolvedCount:y}}async function G(i,y){let b=await i.select({groupId:t.groupId,n:(0,c.count)()}).from(t).innerJoin(u,(0,c.and)((0,c.eq)(u.id,t.groupId),(0,c.eq)(u.tenantId,y))).where((0,c.isNull)(t.resolvedUserId)).groupBy(t.groupId);return new Map(b.map(S=>[Number(S.groupId),Number(S.n)]))}return{async listLinks(i){(0,C.assertTenantId)(i,"listLinks(tenantId)"),await s();let y=await l.select({linkId:a.id,groupId:a.groupId,tenantId:a.tenantId,provider:a.provider,externalId:a.externalId,externalKey:a.externalKey,membershipMode:a.membershipMode,isSyncEnabled:a.isSyncEnabled,syncedAt:a.syncedAt,syncError:a.syncError}).from(a).where((0,c.eq)(a.tenantId,i)).orderBy((0,c.asc)(a.groupId)),b=await G(l,i);return y.map(S=>((0,C.assertTenantMatch)(i,String(S.tenantId),"listLinks(row.tenantId)"),x(S,b.get(Number(S.groupId))??0)))},async externalGroupKey(i,y,b){(0,C.assertTenantId)(i,"externalGroupKey(tenantId)"),await s();let[S]=await(b??l).select({externalKey:a.externalKey}).from(a).where((0,c.and)((0,c.eq)(a.groupId,y),(0,c.eq)(a.tenantId,i)));return S?String(S.externalKey):null},async linkGroup(i,y,b,S){(0,C.assertTenantId)(i,"linkGroup(tenantId)");let k=U(b.membershipMode),R=b.externalId.trim(),P=(0,re.normalizeAuthEmail)(b.externalKey);if(R===""||P==="")throw new TypeError("[@aiquants/auth-directory-drizzle] externalId and externalKey must be non-empty");await s(),await d.linkable.assertLinkableAndWrite(i,y,async V=>{let E=V;await A(E,i,y),await D(E,i,y),await E.insert(a).output().values({groupId:y,tenantId:i,provider:b.provider,externalId:R,externalKey:P,membershipMode:k,isSyncEnabled:!0,vanishedObservations:0,createdAt:new Date,createdBy:S})}),h(i)},async unlinkGroup(i,y){(0,C.assertTenantId)(i,"unlinkGroup(tenantId)"),await s(),await f(async b=>{await A(b,i,y),await b.delete(t).where((0,c.eq)(t.groupId,y)),await b.delete(r).where((0,c.and)((0,c.eq)(r.groupId,y),(0,c.eq)(r.tenantId,i))),await b.delete(a).where((0,c.and)((0,c.eq)(a.groupId,y),(0,c.eq)(a.tenantId,i)))}),h(i)},async setSyncEnabled(i,y,b,S){(0,C.assertTenantId)(i,"setSyncEnabled(tenantId)"),await s(),await l.update(a).set({isSyncEnabled:b,updatedAt:new Date,updatedBy:S}).output().where((0,c.and)((0,c.eq)(a.groupId,y),(0,c.eq)(a.tenantId,i)))},async requestSync(i,y,b){return(0,C.assertTenantId)(i,"requestSync(tenantId)"),await s(),l.transaction(async S=>{if(y!==null&&await A(S,i,y),(await S.select({id:g.id}).from(g).where((0,c.and)((0,c.eq)(g.tenantId,i),y===null?(0,c.isNull)(g.groupId):(0,c.eq)(g.groupId,y),(0,c.isNull)(g.pickedAt)))).length>0)return"already-pending";try{await S.insert(g).output().values({tenantId:i,groupId:y,requestedBy:b,requestedAt:new Date,createdAt:new Date,createdBy:b})}catch(R){if(!Be(R))throw R;return"already-pending"}return"queued"})},async getStatus(i){(0,C.assertTenantId)(i,"getStatus(tenantId)"),await s();let y=await l.select({groupId:a.groupId,isSyncEnabled:a.isSyncEnabled,syncedAt:a.syncedAt,syncError:a.syncError}).from(a).where((0,c.eq)(a.tenantId,i)),[b]=await l.select({n:(0,c.count)()}).from(g).where((0,c.and)((0,c.eq)(g.tenantId,i),(0,c.isNull)(g.finishedAt))),S=y.filter(E=>!!E.isSyncEnabled),k=S.map(E=>E.syncedAt?(0,de.toInstant)(E.syncedAt).getTime():null),R=k.length>0&&k.every(E=>E!==null)?Math.min(...k):null,V=y.length>0&&S.length===0||S.length>0&&(R===null||Date.now()-R>d.stalenessThresholdSec*1e3);return{isEnabled:d.isEnabled,provider:d.provider,linkedGroupCount:y.length,...R!==null?{oldestSyncedAt:new Date(R).toISOString()}:{},isStale:V,pausedGroupCount:y.length-S.length,stalenessThresholdSec:d.stalenessThresholdSec,erroredGroupIds:y.filter(E=>!!E.syncError).map(E=>Number(E.groupId)).sort((E,Re)=>E-Re),pendingRequestCount:Number(b?.n??0)}},async loadTenantMembershipPolicy(i){(0,C.assertTenantId)(i,"loadTenantMembershipPolicy(tenantId)");let y=v>0?w.get(i):void 0;if(y&&Date.now()-y.loadedAt<v)return y.policy;await s();let b=await l.select({groupId:o.groupId,isBreakGlass:o.isBreakGlass,externalLinkId:a.id}).from(o).innerJoin(u,(0,c.and)((0,c.eq)(u.id,o.groupId),(0,c.eq)(u.tenantId,i))).leftJoin(a,(0,c.and)((0,c.eq)(a.groupId,o.groupId),(0,c.eq)(a.tenantId,i))).where((0,c.eq)(o.tenantId,i));if(b.length===0){let P={kind:"no-proof-required"};return v>0&&w.set(i,{policy:P,loadedAt:Date.now()}),P}let S=b.map(P=>Number(P.groupId)),k=b.filter(P=>!!P.isBreakGlass);for(let P of k)if(P.externalLinkId!==null)throw new Error(`Break-glass group ${Number(P.groupId)} is externally sourced; a break-glass proof path must be one the synchronization can never touch.`);let R={kind:"require-proof",proofGroupIds:S,breakGlassGroupIds:k.map(P=>Number(P.groupId))};return(0,we.assertTenantMembershipPolicy)(R,`tenant ${i}`),v>0&&w.set(i,{policy:R,loadedAt:Date.now()}),R},async setTenantMembershipGroup(i,y,b,S){(0,C.assertTenantId)(i,"setTenantMembershipGroup(tenantId)"),await s(),await f(async k=>{if(await A(k,i,y),b){let[P]=await k.select({id:a.id}).from(a).where((0,c.and)((0,c.eq)(a.tenantId,i),(0,c.eq)(a.groupId,y)));if(P)throw new Error(`Group ${y} is externally sourced; a break-glass proof path must be one the synchronization can never touch.`)}let R=await k.select({groupId:o.groupId}).from(o).where((0,c.and)((0,c.eq)(o.tenantId,i),(0,c.eq)(o.groupId,y)));if(!b&&!(await k.select({groupId:o.groupId,isBreakGlass:o.isBreakGlass}).from(o).where((0,c.eq)(o.tenantId,i))).filter(E=>Number(E.groupId)!==y).some(E=>!!E.isBreakGlass))throw new Error(`Declaring group ${y} as ordinary proof would leave tenant ${i} with a membership proof but no break-glass path; declare a locally-managed break-glass group first.`);if(R.length>0){await k.update(o).set({isBreakGlass:b,updatedAt:new Date,updatedBy:S}).output().where((0,c.and)((0,c.eq)(o.tenantId,i),(0,c.eq)(o.groupId,y)));return}await k.insert(o).output().values({tenantId:i,groupId:y,isBreakGlass:b,createdAt:new Date,createdBy:S})}),h(i)},async removeTenantMembershipGroup(i,y){(0,C.assertTenantId)(i,"removeTenantMembershipGroup(tenantId)"),await s(),await f(async b=>{let k=(await b.select({groupId:o.groupId,isBreakGlass:o.isBreakGlass}).from(o).where((0,c.eq)(o.tenantId,i))).filter(R=>Number(R.groupId)!==y);if(k.length>0&&!k.some(R=>!!R.isBreakGlass))throw new Error(`Removing group ${y} would leave tenant ${i} with a membership proof but no break-glass path.`);await b.delete(o).where((0,c.and)((0,c.eq)(o.tenantId,i),(0,c.eq)(o.groupId,y)))}),h(i)},async setGroupActive(i,y,b,S){(0,C.assertTenantId)(i,"setGroupActive(tenantId)"),await s(),await l.update(u).set({isActive:b,updatedAt:new Date,updatedBy:S}).output().where((0,c.and)((0,c.eq)(u.id,y),(0,c.eq)(u.tenantId,i)))},async listLedgerGroupIds(i,y){if((0,C.assertTenantId)(i,"listLedgerGroupIds(tenantId)"),!d.isEnabled)return[];await s();let b=await W(l,{ledger:t,group:u},{tenantId:i,externalUserId:y.externalUserId?.trim()||null,email:(0,re.normalizeAuthEmail)(y.email)});return[...new Set(b.map(S=>S.groupId))].sort((S,k)=>S-k)},async isEmailAllowedByGroup(i){if(!d.isEnabled)return!1;let y=(0,re.normalizeAuthEmail)(i);if(y==="")return!1;await s();let[b]=await l.select({id:t.id}).from(t).innerJoin(r,(0,c.eq)(r.groupId,t.groupId)).innerJoin(u,(0,c.and)((0,c.eq)(u.id,t.groupId),(0,c.eq)(u.isActive,!0))).where((0,c.eq)(t.email,y));return!!b},async listAllowlistGroups(i){return(0,C.assertTenantId)(i,"listAllowlistGroups(tenantId)"),await s(),(await l.select({id:r.id,groupId:r.groupId,groupKey:u.groupKey,groupName:u.name,description:r.description}).from(r).innerJoin(u,(0,c.and)((0,c.eq)(u.id,r.groupId),(0,c.eq)(u.tenantId,i))).where((0,c.eq)(r.tenantId,i)).orderBy((0,c.asc)(u.name))).map(b=>({id:Number(b.id),type:"group",groupId:Number(b.groupId),groupKey:String(b.groupKey),groupName:String(b.groupName),...b.description?{description:String(b.description)}:{},owner:{kind:"tenant",tenantId:i},readOnly:!1}))},async addAllowlistGroup(i,y,b,S){(0,C.assertTenantId)(i,"addAllowlistGroup(tenantId)"),await s(),await f(async k=>{await A(k,i,y);let[R]=await k.select({id:a.id}).from(a).where((0,c.and)((0,c.eq)(a.groupId,y),(0,c.eq)(a.tenantId,i)));if(!R)throw new Error(`Group ${y} is locally managed; only externally-sourced groups can gate sign-in, because the decision reads the upstream member ledger.`);await k.insert(r).output().values({tenantId:i,groupId:y,description:b??null,createdAt:new Date,createdBy:S})})},async removeAllowlistGroup(i,y){(0,C.assertTenantId)(i,"removeAllowlistGroup(tenantId)"),await s(),await l.delete(r).where((0,c.and)((0,c.eq)(r.id,y),(0,c.eq)(r.tenantId,i)))},async countProjectedMembers(i,y){(0,C.assertTenantId)(i,"countProjectedMembers(tenantId)"),await s();let[b]=await l.select({n:(0,c.count)()}).from(I).innerJoin(u,(0,c.and)((0,c.eq)(u.id,I.groupId),(0,c.eq)(u.tenantId,i))).where((0,c.eq)(I.groupId,y));return Number(b?.n??0)}}}var Ie=require("@aiquants/auth-core"),te=require("@aiquants/authz-core");async function Fe(e,n){let m=(0,te.assertTenantId)(n.tenantId,"promoteLedgerForUser(tenantId)");if(!Number.isInteger(n.userId)||n.userId<=0)throw new TypeError(`[@aiquants/auth-directory-drizzle] userId must be a positive integer, received ${String(n.userId)}`);let d=(0,Ie.normalizeAuthEmail)(n.email),l=n.externalUserId?.trim()||null;if(l===null&&d==="")return{addedGroupIds:[],matchedLedgerIds:[]};let s=await e.findLedgerMatches({tenantId:m,externalUserId:l,email:d});if(s.length===0)return{addedGroupIds:[],matchedLedgerIds:[]};for(let h of s)(0,te.assertTenantMatch)(m,h.tenantId,"promoteLedgerForUser(findLedgerMatches row)");let f=[...new Set(s.map(h=>h.ledgerId))].sort((h,a)=>h-a),v=[...new Set(s.map(h=>h.groupId))].sort((h,a)=>h-a);return{addedGroupIds:[...await e.attachUserToGroups({tenantId:m,userId:n.userId,ledgerIds:f,groupIds:v})].sort((h,a)=>h-a),matchedLedgerIds:f}}var le=require("@aiquants/authz-core"),B=require("@aiquants/mssql"),ve=require("@aiquants/mssql/drizzle"),L=require("drizzle-orm");var je=4;function We(e,n,m,d){let l=e,s=d.ensureConnection??(async()=>{}),f=K(l,d),v=d.now??(()=>new Date),w=n.TDGroupMemberEmail,{TMGroup:h,TDGroupMember:a}=m;return{async findLedgerMatches(t){return(0,le.assertTenantId)(t.tenantId,"findLedgerMatches(tenantId)"),await s(),W(l,{ledger:w,group:h},t)},async attachUserToGroups({tenantId:t,userId:o,ledgerIds:r,groupIds:g}){return(0,le.assertTenantId)(t,"attachUserToGroups(tenantId)"),await s(),r.length===0?[]:f(async u=>{let I=new Set;for(let G of(0,B.chunk)(g)){let i=await u.select({id:h.id}).from(h).where((0,L.and)((0,L.inArray)(h.id,G),(0,L.eq)(h.tenantId,t),(0,L.eq)(h.isActive,!0)));for(let y of i)I.add(Number(y.id))}let D=g.filter(G=>I.has(G));if(D.length===0)return[];let A=new Set;for(let G of(0,B.chunk)(D)){let i=await u.select({groupId:a.groupId}).from(a).where((0,L.and)((0,L.eq)(a.userId,o),(0,L.inArray)(a.groupId,G)));for(let y of i)A.add(Number(y.groupId))}let x=D.filter(G=>!A.has(G));for(let G of(0,B.chunk)(r))await u.update(w).set({resolvedUserId:o}).output().where((0,L.and)((0,L.inArray)(w.id,G),(0,ve.inJsonList)(w.groupId,D)));if(x.length>0){let G=v();for(let i of(0,B.chunkByParameters)(x,je))await u.insert(a).output().values(i.map(y=>({groupId:y,userId:o,createdAt:G,createdBy:d.actor})))}return x})}}}var N=require("@aiquants/authz-drizzle"),p=require("drizzle-orm/mssql-core"),ne="provider",He=["TDGroupExternalLink","TDTenantMembershipGroup","TDAllowlistGroup","TDDirectorySyncRequest","TDDirectoryCatalog","TMDirectoryCatalogObservation","TMAllowlist"];function Je(e,n){let m=(0,p.mssqlSchema)(e),d=()=>({createdAt:(0,p.datetime2)("created_at"),createdBy:(0,p.nvarchar)("created_by",{length:50}),updatedAt:(0,p.datetime2)("updated_at"),updatedBy:(0,p.nvarchar)("updated_by",{length:50})}),l=()=>({createdAt:(0,p.datetime2)("created_at"),createdBy:(0,p.nvarchar)("created_by",{length:50})}),s=m.table("TDGroupExternalLink",{id:(0,p.bigint)("id",{mode:"number"}).identity().notNull(),groupId:(0,p.bigint)("group_id",{mode:"number"}).notNull(),tenantId:(0,p.nvarchar)(N.AUTHZ_TENANT_COLUMN,{length:64}).notNull(),provider:(0,p.nvarchar)(ne,{length:32}).notNull(),externalId:(0,p.nvarchar)("external_id",{length:256}).notNull(),externalKey:(0,p.nvarchar)("external_key",{length:320}).notNull(),membershipMode:(0,p.nvarchar)("membership_mode",{length:16}).notNull(),isSyncEnabled:(0,p.bit)("is_sync_enabled").notNull(),syncedAt:(0,p.datetime2)("synced_at"),syncError:(0,p.nvarchar)("sync_error",{length:1e3}),vanishedObservations:(0,p.int)("vanished_observations").notNull(),...d()},r=>[(0,p.primaryKey)({name:`${e}_TDGroupExternalLink_pk`,columns:[r.id]}),(0,p.uniqueIndex)(`${e}_TDGroupExternalLink_group_uq`).on(r.groupId),(0,p.uniqueIndex)(`${e}_TDGroupExternalLink_external_uq`).on(r.tenantId,r.provider,r.externalId),(0,p.foreignKey)({name:`${e}_TDGroupExternalLink_group_id_fk`,columns:[r.groupId],foreignColumns:[n.groupTable.id]})]),f=m.table("TDGroupMemberEmail",{id:(0,p.bigint)("id",{mode:"number"}).identity().notNull(),groupId:(0,p.bigint)("group_id",{mode:"number"}).notNull(),email:(0,p.nvarchar)("email",{length:320}).notNull(),externalUserId:(0,p.nvarchar)("external_user_id",{length:128}),resolvedUserId:(0,p.bigint)("resolved_user_id",{mode:"number"}),observedAt:(0,p.datetime2)("observed_at").notNull(),...d()},r=>[(0,p.primaryKey)({name:`${e}_TDGroupMemberEmail_pk`,columns:[r.id]}),(0,p.uniqueIndex)(`${e}_TDGroupMemberEmail_group_email_uq`).on(r.groupId,r.email),(0,p.foreignKey)({name:`${e}_TDGroupMemberEmail_group_id_fk`,columns:[r.groupId],foreignColumns:[n.groupTable.id]})]),v=m.table("TDTenantMembershipGroup",{tenantId:(0,p.nvarchar)(N.AUTHZ_TENANT_COLUMN,{length:64}).notNull(),groupId:(0,p.bigint)("group_id",{mode:"number"}).notNull(),isBreakGlass:(0,p.bit)("is_break_glass").notNull(),...l()},r=>[(0,p.primaryKey)({name:`${e}_TDTenantMembershipGroup_pk`,columns:[r.tenantId,r.groupId]}),(0,p.foreignKey)({name:`${e}_TDTenantMembershipGroup_group_id_fk`,columns:[r.groupId],foreignColumns:[n.groupTable.id]})]),w=m.table("TDAllowlistGroup",{id:(0,p.bigint)("id",{mode:"number"}).identity().notNull(),tenantId:(0,p.nvarchar)(N.AUTHZ_TENANT_COLUMN,{length:64}).notNull(),groupId:(0,p.bigint)("group_id",{mode:"number"}).notNull(),description:(0,p.nvarchar)("description",{length:512}),...l()},r=>[(0,p.primaryKey)({name:`${e}_TDAllowlistGroup_pk`,columns:[r.id]}),(0,p.uniqueIndex)(`${e}_TDAllowlistGroup_tenant_group_uq`).on(r.tenantId,r.groupId),(0,p.foreignKey)({name:`${e}_TDAllowlistGroup_group_id_fk`,columns:[r.groupId],foreignColumns:[n.groupTable.id]})]),h=m.table("TDDirectorySyncRequest",{id:(0,p.bigint)("id",{mode:"number"}).identity().notNull(),tenantId:(0,p.nvarchar)(N.AUTHZ_TENANT_COLUMN,{length:64}).notNull(),groupId:(0,p.bigint)("group_id",{mode:"number"}),requestedBy:(0,p.nvarchar)("requested_by",{length:320}).notNull(),requestedAt:(0,p.datetime2)("requested_at").notNull(),pickedAt:(0,p.datetime2)("picked_at"),finishedAt:(0,p.datetime2)("finished_at"),result:(0,p.nvarchar)("result",{length:32}),error:(0,p.nvarchar)("error",{length:2e3}),...l()},r=>[(0,p.primaryKey)({name:`${e}_TDDirectorySyncRequest_pk`,columns:[r.id]})]),a=m.table("TMAllowlist",{id:(0,p.bigint)("id",{mode:"number"}).identity().notNull(),tenantId:(0,p.nvarchar)(N.AUTHZ_TENANT_COLUMN,{length:64}).notNull(),pattern:(0,p.nvarchar)("pattern",{length:256}).notNull(),type:(0,p.nvarchar)("type",{length:32}).notNull(),description:(0,p.nvarchar)("description",{length:512}),...l()},r=>[(0,p.primaryKey)({name:`${e}_TMAllowlist_pk`,columns:[r.id]}),(0,p.uniqueIndex)(`${e}_TMAllowlist_pattern_uq`).on(r.tenantId,r.pattern)]),t=m.table("TDDirectoryCatalog",{id:(0,p.bigint)("id",{mode:"number"}).identity().notNull(),tenantId:(0,p.nvarchar)(N.AUTHZ_TENANT_COLUMN,{length:64}).notNull(),provider:(0,p.nvarchar)(ne,{length:32}).notNull(),externalId:(0,p.nvarchar)("external_id",{length:256}).notNull(),externalKey:(0,p.nvarchar)("external_key",{length:320}).notNull(),name:(0,p.nvarchar)("name",{length:256}).notNull(),description:(0,p.nvarchar)("description",{length:1e3}),memberCount:(0,p.int)("member_count"),observedAt:(0,p.datetime2)("observed_at").notNull(),...l()},r=>[(0,p.primaryKey)({name:`${e}_TDDirectoryCatalog_pk`,columns:[r.id]}),(0,p.uniqueIndex)(`${e}_TDDirectoryCatalog_external_uq`).on(r.tenantId,r.provider,r.externalId)]),o=m.table("TMDirectoryCatalogObservation",{id:(0,p.bigint)("id",{mode:"number"}).identity().notNull(),tenantId:(0,p.nvarchar)(N.AUTHZ_TENANT_COLUMN,{length:64}).notNull(),provider:(0,p.nvarchar)(ne,{length:32}).notNull(),observedAt:(0,p.datetime2)("observed_at").notNull(),entryCount:(0,p.int)("entry_count").notNull(),...d()},r=>[(0,p.primaryKey)({name:`${e}_TMDirectoryCatalogObservation_pk`,columns:[r.id]}),(0,p.uniqueIndex)(`${e}_TMDirectoryCatalogObservation_target_uq`).on(r.tenantId,r.provider)]);return{TDGroupExternalLink:s,TDGroupMemberEmail:f,TDTenantMembershipGroup:v,TDAllowlistGroup:w,TDDirectorySyncRequest:h,TDDirectoryCatalog:t,TMDirectoryCatalogObservation:o,TMAllowlist:a}}var ie=require("@aiquants/auth-directory-core");function H(e){if(e instanceof Error&&(e.name==="AbortError"||e.name==="TimeoutError"))return"timeout";let n=typeof e=="object"&&e!==null&&"status"in e?e.status:void 0;return typeof n!="number"?"unknown":n===401||n===403?"denied":n===429||n===408?"rateLimited":n>=500?"upstreamError":"unknown"}var Se=e=>`${e.length} \u4EF6`,ce={denied:"\u8CC7\u683C\u60C5\u5831\u304C\u62D2\u5426\u3055\u308C\u307E\u3057\u305F",rateLimited:"\u547C\u3073\u51FA\u3057\u304C\u5236\u9650\u3055\u308C\u3066\u3044\u307E\u3059",upstreamError:"\u76F8\u624B\u5074\u304C\u5185\u90E8\u30A8\u30E9\u30FC\u3092\u8FD4\u3057\u307E\u3057\u305F",timeout:"\u5FDC\u7B54\u304C\u3042\u308A\u307E\u305B\u3093\u3067\u3057\u305F",localWriteError:"\u66F8\u304D\u8FBC\u307F\u304C\u62D2\u5426\u3055\u308C\u307E\u3057\u305F",unknown:"\u8A73\u7D30\u306F\u30B5\u30FC\u30D0\u30FC\u306E\u30ED\u30B0\u306B\u51FA\u3066\u3044\u307E\u3059"},pe={upstreamUnreadable:e=>`\u4E0A\u6D41\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306E\u8AAD\u307F\u53D6\u308A\u306B\u5931\u6557\u3057\u307E\u3057\u305F (${ce[e]})\u3002`,localWriteFailed:e=>`\u30ED\u30FC\u30AB\u30EB\u3078\u306E\u66F8\u304D\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F (${ce[e]})\u3002`,resolverIncomplete:(e,n)=>`\u4E0A\u6D41\u304C ${e} \u4EF6\u306E\u30E1\u30F3\u30D0\u30FC\u3092\u8FD4\u3057\u307E\u3057\u305F\u304C\u3001\u7A81\u5408\u7D50\u679C\u306F ${n} \u4EF6\u3067\u3057\u305F\u3002\u4EF6\u6570\u304C\u6E1B\u308B\u7A81\u5408\u306F\u3001\u89E3\u6C7A\u3067\u304D\u306A\u304B\u3063\u305F\u4EBA\u7269\u3092\u300C\u4E0A\u6D41\u304B\u3089\u6D88\u3048\u305F\u300D\u3068\u3057\u3066\u524A\u9664\u3057\u3066\u3057\u307E\u3046\u305F\u3081\u4E2D\u65AD\u3057\u307E\u3057\u305F\u3002\u89E3\u6C7A\u3067\u304D\u306A\u3044\u30E1\u30F3\u30D0\u30FC\u306F\u7701\u7565\u305B\u305A resolvedUserId: null \u3067\u8FD4\u3059\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`,groupMissing:(e,n,m)=>`\u4E0A\u6D41\u3067\u30B0\u30EB\u30FC\u30D7\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (${e})\u3002\u9023\u7D9A ${n} \u56DE\u76EE\u3067\u3059\u3002${m} \u56DE\u306B\u9054\u3057\u305F\u6642\u70B9\u3067\u30B0\u30EB\u30FC\u30D7\u3092\u7121\u52B9\u5316\u3057\u307E\u3059\u3002`,groupVanished:(e,n)=>`\u4E0A\u6D41\u3067\u30B0\u30EB\u30FC\u30D7\u304C ${n} \u56DE\u9023\u7D9A\u3067\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F (${e})\u3002\u30B0\u30EB\u30FC\u30D7\u3092\u7121\u52B9\u5316\u3057\u307E\u3057\u305F\u3002\u4E0A\u6D41\u304C\u5FA9\u65E7\u3059\u308C\u3070\u6B21\u56DE\u306E\u540C\u671F\u3067\u81EA\u52D5\u7684\u306B\u518D\u6709\u52B9\u5316\u3055\u308C\u307E\u3059\u3002`,planRefused:e=>{if(e.reason==="invalid-upstream-data")return`\u540C\u671F\u3092\u4E2D\u65AD\u3057\u307E\u3057\u305F: \u4E0A\u6D41\u306E\u30E1\u30F3\u30D0\u30FC\u4E00\u89A7\u306B\u6271\u3048\u306A\u3044\u30A2\u30C9\u30EC\u30B9\u304C ${Se(e.offendingEmails)} \u542B\u307E\u308C\u3066\u3044\u307E\u3059\u3002\u5BFE\u8C61\u306F\u30B5\u30FC\u30D0\u30FC\u306E\u30ED\u30B0\u306B\u51FA\u3066\u3044\u307E\u3059\u3002`;let n=e.axis==="membership"?"\u6A29\u9650\u3092\u5931\u3046\u4EBA\u6570":"\u30B5\u30A4\u30F3\u30A4\u30F3\u3067\u304D\u306A\u304F\u306A\u308B\u4EBA\u6570",m=e.axis==="membership"?e.withheldProjectionRemoves.map(String):e.withheldLedgerDeletes;return[`\u540C\u671F\u3092\u4E2D\u65AD\u3057\u307E\u3057\u305F: ${n}\u304C ${e.population} \u4EBA\u4E2D ${e.removalCount} \u4EBA\u306B\u306A\u308A\u307E\u3059`,`(\u4E0A\u9650: ${e.limits.maxRemovalCount} \u4EF6 / \u6BD4\u7387 ${e.limits.maxRemovalRatio})\u3002`,`\u5DEE\u3057\u6B62\u3081\u305F\u5BFE\u8C61: ${Se(m)} (\u4E00\u89A7\u306F\u30B5\u30FC\u30D0\u30FC\u306E\u30ED\u30B0\u306B\u51FA\u3066\u3044\u307E\u3059)\u3002`,"\u4E0A\u6D41\u5074\u306E\u8A2D\u5B9A\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u610F\u56F3\u3057\u305F\u5909\u66F4\u3067\u3042\u308C\u3070\u3001\u3053\u306E\u8EF8\u306E\u4E0A\u9650\u3060\u3051\u3092\u4E00\u6642\u7684\u306B\u5F15\u304D\u4E0A\u3052\u3066\u518D\u5B9F\u884C\u3057\u307E\u3059\u3002"].join(" ")},removalWritesFailed:e=>`${e.length} \u4EF6\u306E\u6240\u5C5E\u89E3\u9664\u304C\u66F8\u304D\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F (${[...new Set(e.map(n=>ce[n.cause]))].join(" / ")})\u3002\u6B21\u56DE\u306E\u5B9F\u884C\u3067\u518D\u8A66\u884C\u3057\u307E\u3059\u3002`,removalsRefused:e=>`${e.length} \u4EF6\u306E\u6240\u5C5E\u89E3\u9664\u304C\u6700\u5F8C\u306E\u7BA1\u7406\u8005\u30AC\u30FC\u30C9\u306B\u3088\u308A\u62D2\u5426\u3055\u308C\u307E\u3057\u305F (\u30E6\u30FC\u30B6\u30FC ${e.map(n=>n.userId).join(", ")})\u3002\u6B8B\u308A\u306E\u5909\u66F4\u306F\u9069\u7528\u6E08\u307F\u3067\u3059\u3002`,foreignProvider:(e,n)=>`\u3053\u306E\u30EA\u30F3\u30AF\u306E\u4F9B\u7D66\u5143\u306F ${e} \u3067\u3059\u304C\u3001\u5B9F\u884C\u4E2D\u306E\u540C\u671F\u306F ${n} \u7528\u3067\u3059\u3002\u5BFE\u5FDC\u3059\u308B\u540C\u671F\u30B8\u30E7\u30D6\u304B\u3089\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002`};var oe=["aborted","missing","vanished","upstream-failed","local-failed","foreign-provider","not-converged"],me=[...oe,"already-vanished"];function Ye(e){let{directory:n,repository:m,policy:d,now:l,logger:s}=e,f=e.messages??pe;if((0,ie.assertBlastRadiusPolicy)(d),!Number.isInteger(e.vanishedThreshold)||e.vanishedThreshold<1)throw new TypeError(`[@aiquants/auth-directory-drizzle] vanishedThreshold must be a positive integer, received ${String(e.vanishedThreshold)}`);if(typeof e.provider!="string"||e.provider.trim()==="")throw new TypeError("[@aiquants/auth-directory-drizzle] provider must be a non-empty string so links of other providers can be told apart");async function v(t,o){try{await m.recordFailure(t,o)}catch(r){s.error(`[auth-directory] could not record the failure for ${t.externalKey}:`,r)}}async function w(t){try{let o=await n.getGroup(t.externalId);return o===null?{kind:"missing"}:{kind:"ok",group:o,members:await n.listGroupMembers(o,t.membershipMode)}}catch(o){return{kind:"failed",error:o instanceof Error?o:new Error(String(o))}}}async function h(t){if(t.vanishedObservations>=e.vanishedThreshold&&!t.isGroupActive)return s.warn(`[auth-directory] ${t.externalKey} still missing upstream (already deactivated after ${t.vanishedObservations} absences)`),{kind:"already-vanished",link:t};let o=await m.recordVanishedObservation(t,f.groupMissing(t.externalKey,t.vanishedObservations+1,e.vanishedThreshold));return o<e.vanishedThreshold?(s.warn(`[auth-directory] ${t.externalKey} missing upstream (${o}/${e.vanishedThreshold})`),{kind:"missing",link:t,observations:o}):(await m.markGroupInactive(t),await v(t,f.groupVanished(t.externalKey,o)),s.warn(`[auth-directory] ${t.externalKey} deactivated after ${o} consecutive absences`),{kind:"vanished",link:t})}async function a(t){if(t.provider!==e.provider)return await v(t,f.foreignProvider(t.provider,e.provider)),s.warn(`[auth-directory] skipping ${t.externalKey}: belongs to provider ${t.provider}`),{kind:"foreign-provider",link:t};let o=await w(t);if(o.kind==="missing")return h(t);if(o.kind==="failed")return await v(t,f.upstreamUnreadable(H(o.error))),s.error(`[auth-directory] upstream read failed for ${t.externalKey}:`,o.error),{kind:"upstream-failed",link:t,error:o.error};try{await m.resolveExternalIdentity(t,o.group);let[r,g]=await Promise.all([m.loadGroupSnapshot(t),m.resolveMembers(o.members)]);if(g.length!==o.members.length){let x=f.resolverIncomplete(o.members.length,g.length);return await v(t,x),s.error(`[auth-directory] ${x}`),{kind:"local-failed",link:t,error:new Error(x)}}let u=(0,ie.reconcile)(r,g,d);if(u.aborted){await v(t,f.planRefused(u.aborted));let x=u.aborted.reason==="invalid-upstream-data"?u.aborted.offendingEmails:u.aborted.axis==="membership"?u.aborted.withheldProjectionRemoves.map(String):u.aborted.withheldLedgerDeletes;return s.warn(`[auth-directory] plan refused for ${t.externalKey}: ${u.aborted.reason}; withheld: ${x.join(", ")}`),{kind:"aborted",link:t,plan:u}}let I=u.ledgerUpserts.length>0||u.ledgerDeletes.length>0||u.projectionAdds.length>0||u.projectionRemoves.length>0,D=l(),A={refusals:[],failures:[]};return I&&(A=await m.applyPlan(t,u,D)),await m.recordSuccess(t,o.group,D),A.refusals.length>0&&(await v(t,f.removalsRefused(A.refusals)),s.warn(`[auth-directory] ${A.refusals.length} removal(s) refused for ${t.externalKey}: users ${A.refusals.map(x=>x.userId).join(", ")}`)),A.failures.length>0&&(await v(t,f.removalWritesFailed(A.failures)),s.warn(`[auth-directory] ${A.failures.length} removal(s) could not be written for ${t.externalKey}: users ${A.failures.map(x=>x.userId).join(", ")}`)),u.unresolvedEmails.length>0&&s.info(`[auth-directory] ${t.externalKey}: ${u.unresolvedEmails.length} upstream member(s) have no local user yet`),A.refusals.length>0||A.failures.length>0?{kind:"not-converged",link:t,plan:u,refusals:A.refusals,failures:A.failures}:I?{kind:"applied",link:t,plan:u}:{kind:"unchanged",link:t}}catch(r){let g=r instanceof Error?r:new Error(String(r));return await v(t,f.localWriteFailed("localWriteError")),s.error(`[auth-directory] local write failed for ${t.externalKey}:`,g),{kind:"local-failed",link:t,error:g}}}return{syncOne:a,async run(t){let o=await m.listSyncableLinks(t),r=[];for(let I of o)try{r.push(await a(I))}catch(D){let A=D instanceof Error?D:new Error(String(D));s.error(`[auth-directory] ${I.externalKey} ended abnormally:`,A),r.push({kind:"local-failed",link:I,error:A})}let g=r.filter(I=>me.includes(I.kind)).length,u=r.filter(I=>oe.includes(I.kind)).length;return s.info(`[auth-directory] run finished: ${r.length} link(s), ${g} unhealthy (${u} new)`),{outcomes:r,unhealthyCount:g,newlyUnhealthyCount:u}}}}var Te=require("@aiquants/authz-core"),De=require("@aiquants/mssql"),M=require("drizzle-orm"),Ae="The job that claimed this request ended without recording an outcome; whether the synchronization ran is unknown.",ye=2e3;function Ve(e,n,m){let d=e,l=m.ensureConnection??(async()=>{}),s=n.TDDirectorySyncRequest,f=(0,Te.assertTenantId)(m.tenantId,"createSyncRequestQueue(tenantId)"),v=m.maxErrorLength??ye;if(!Number.isInteger(v)||v<1||v>ye)throw new TypeError(`[@aiquants/auth-directory-drizzle] maxErrorLength must be an integer within 1..${ye}, received ${String(m.maxErrorLength)}`);let w={async claim(){await l();let h=await d.select({id:s.id}).from(s).where((0,M.and)((0,M.eq)(s.tenantId,f),(0,M.isNull)(s.pickedAt))).orderBy((0,M.asc)(s.id));for(let a of h){let o=(await d.update(s).set({pickedAt:m.now()}).output().where((0,M.and)((0,M.eq)(s.tenantId,f),(0,M.eq)(s.id,Number(a.id)),(0,M.isNull)(s.pickedAt))))?.[0];if(!o)continue;if(o.requestedBy===void 0||o.requestedBy===null)throw new TypeError(`[@aiquants/auth-directory-drizzle] the driver returned a row without requestedBy for request ${String(o.id)}`);let r=o.groupId??null;return{id:Number(o.id),tenantId:f,groupId:r===null?null:Number(r),requestedBy:String(o.requestedBy)}}return null},async finish(h,a){await l();let t=a.error?a.error.slice(0,v):null;return!!(await d.update(s).set({finishedAt:m.now(),result:a.result,error:t}).output().where((0,M.and)((0,M.eq)(s.tenantId,f),(0,M.eq)(s.id,h),(0,M.isNull)(s.finishedAt))))?.length},async listStranded(h){await l();let a=await d.select({id:s.id,pickedAt:s.pickedAt,requestedAt:s.requestedAt}).from(s).where((0,M.and)((0,M.eq)(s.tenantId,f),(0,M.isNull)(s.finishedAt))).orderBy((0,M.asc)(s.id)),t=o=>{let r=(0,De.toInstant)(o);return Number.isNaN(r.getTime())||r<h};return{stranded:a.filter(o=>o.pickedAt!==null&&t(o.pickedAt)).map(o=>Number(o.id)),unclaimed:a.filter(o=>o.pickedAt===null&&t(o.requestedAt)).map(o=>Number(o.id))}},async abandonStranded(h){let{stranded:a}=await w.listStranded(h),t=[];for(let o of a)try{await w.finish(o,{result:"failed",error:Ae})&&t.push(o)}catch(r){m.onCloseFailed?.(o,r)}return t}};return w}var J=require("@aiquants/auth-core"),Y=require("@aiquants/authz-core"),z=require("@aiquants/mssql"),ke=require("@aiquants/mssql/drizzle"),T=require("drizzle-orm");var Qe=7,Ze=4,xe=1e3;function Xe(e){if(!(e instanceof Error))return!1;let n=e;return n.number===void 0&&n.code===void 0&&e.name==="Error"}function er(e,n,m,d){let l=e,s=d.ensureConnection??(async()=>{}),f=K(l,d),v=d.now??(()=>new Date),{TDGroupExternalLink:w,TDGroupMemberEmail:h}=n,{TMGroup:a,TDGroupMember:t}=m,o=d.actor;return{async listSyncableLinks(r){await s();let g=(0,Y.assertTenantId)(r.tenantId,"listSyncableLinks(tenantId)"),u=[(0,T.eq)(w.isSyncEnabled,!0),(0,T.eq)(w.tenantId,g)];return r.groupId!==void 0&&u.push((0,T.eq)(w.groupId,r.groupId)),(await l.select({linkId:w.id,groupId:w.groupId,linkTenantId:w.tenantId,groupTenantId:a.tenantId,groupKey:a.groupKey,isGroupActive:a.isActive,provider:w.provider,externalId:w.externalId,externalKey:w.externalKey,membershipMode:w.membershipMode,vanishedObservations:w.vanishedObservations}).from(w).innerJoin(a,(0,T.and)((0,T.eq)(a.id,w.groupId),(0,T.eq)(a.tenantId,g))).where((0,T.and)(...u))).map(D=>((0,Y.assertTenantMatch)(String(D.groupTenantId),String(D.linkTenantId),"listSyncableLinks(link.tenantId vs TMGroup.tenant_id)"),{linkId:Number(D.linkId),groupId:Number(D.groupId),groupKey:String(D.groupKey),tenantId:String(D.groupTenantId),provider:String(D.provider),externalId:String(D.externalId),externalKey:String(D.externalKey),membershipMode:U(String(D.membershipMode)),isGroupActive:!!D.isGroupActive,vanishedObservations:Number(D.vanishedObservations??0)}))},async loadGroupSnapshot(r){await s();let[g,u]=await Promise.all([l.select({email:h.email,externalUserId:h.externalUserId,resolvedUserId:h.resolvedUserId}).from(h).where((0,T.eq)(h.groupId,r.groupId)),l.select({userId:t.userId}).from(t).where((0,T.eq)(t.groupId,r.groupId))]);return{ledger:g.map(I=>({email:String(I.email),...I.externalUserId?{externalUserId:String(I.externalUserId)}:{},resolvedUserId:I.resolvedUserId===null?null:Number(I.resolvedUserId)})),projectedUserIds:u.map(I=>Number(I.userId))}},resolveMembers(r){return d.memberResolver.resolve(r)},async applyPlan(r,g,u){(0,Y.assertTenantId)(r.tenantId,"applyPlan(link.tenantId)"),await s(),(g.ledgerUpserts.length>0||g.projectionAdds.length>0)&&await f(async x=>{let G=g.ledgerUpserts.map(b=>({...b,email:(0,J.normalizeAuthEmail)(b.email)})),i=new Map;for(let b of(0,z.chunk)(G.map(S=>S.email))){let S=await x.select({id:h.id,email:h.email}).from(h).where((0,T.and)((0,T.eq)(h.groupId,r.groupId),(0,T.inArray)(h.email,b)));for(let k of S)i.set(String(k.email),Number(k.id))}let y=G.filter(b=>!i.has(b.email));for(let b of(0,z.chunkByParameters)(y,Qe))await x.insert(h).output().values(b.map(S=>({groupId:r.groupId,email:S.email,externalUserId:S.externalUserId??null,resolvedUserId:S.resolvedUserId,observedAt:u,createdAt:u,createdBy:o})));for(let b of G){let S=i.get(b.email);S!==void 0&&await x.update(h).set({externalUserId:b.externalUserId??null,resolvedUserId:b.resolvedUserId,observedAt:u,updatedAt:u,updatedBy:o}).output().where((0,T.eq)(h.id,S))}for(let b of(0,z.chunkByParameters)(g.projectionAdds,Ze)){let S=new Set((await x.select({userId:t.userId}).from(t).where((0,T.and)((0,T.eq)(t.groupId,r.groupId),(0,T.inArray)(t.userId,b)))).map(R=>Number(R.userId))),k=b.filter(R=>!S.has(R));k.length>0&&await x.insert(t).output().values(k.map(R=>({groupId:r.groupId,userId:R,createdAt:u,createdBy:o})))}});let I=[],D=[];for(let x of g.projectionRemoves)try{await d.lockoutGuard.guardedWrite(r.tenantId,{removeGroupMembership:{groupId:r.groupId,userId:x}},"remove-group-member",G=>G.delete(t).where((0,T.and)((0,T.eq)(t.groupId,r.groupId),(0,T.eq)(t.userId,x))))}catch(G){Xe(G)?I.push({userId:x,applied:!1,refusal:G.message}):D.push({userId:x,cause:H(G)})}let A=I.map(x=>x.userId);return g.ledgerDeletes.length>0&&await f(async x=>{for(let G of(0,z.chunk)(g.ledgerDeletes)){let i=A.length>0?(0,T.or)((0,T.isNull)(h.resolvedUserId),(0,ke.notInJsonList)(h.resolvedUserId,A)):void 0;await x.delete(h).where((0,T.and)((0,T.eq)(h.groupId,r.groupId),(0,T.inArray)(h.email,G),...i?[i]:[]))}}),{refusals:I,failures:D}},async resolveExternalIdentity(r,g){await s(),!(r.externalId===g.externalId&&r.externalKey===(0,J.normalizeAuthEmail)(g.externalKey))&&await l.update(w).set({externalId:g.externalId,externalKey:(0,J.normalizeAuthEmail)(g.externalKey)}).output().where((0,T.and)((0,T.eq)(w.id,r.linkId),(0,T.eq)(w.tenantId,r.tenantId)))},async recordSuccess(r,g,u){await s(),await f(async I=>{await I.update(w).set({syncedAt:u,syncError:null,vanishedObservations:0,externalId:g.externalId,externalKey:(0,J.normalizeAuthEmail)(g.externalKey),updatedAt:u,updatedBy:o}).output().where((0,T.and)((0,T.eq)(w.id,r.linkId),(0,T.eq)(w.tenantId,r.tenantId))),r.isGroupActive||await I.update(a).set({isActive:!0,updatedAt:u,updatedBy:o}).output().where((0,T.and)((0,T.eq)(a.id,r.groupId),(0,T.eq)(a.tenantId,r.tenantId)))})},async recordFailure(r,g){await s(),await l.update(w).set({syncError:g.slice(0,xe),updatedAt:v(),updatedBy:o}).output().where((0,T.and)((0,T.eq)(w.id,r.linkId),(0,T.eq)(w.tenantId,r.tenantId)))},async recordVanishedObservation(r,g){await s();let u=r.vanishedObservations+1;return await l.update(w).set({vanishedObservations:u,syncError:g.slice(0,xe),updatedAt:v(),updatedBy:o}).output().where((0,T.and)((0,T.eq)(w.id,r.linkId),(0,T.eq)(w.tenantId,r.tenantId))),u},async markGroupInactive(r){await s(),await l.update(a).set({isActive:!1,updatedAt:v(),updatedBy:o}).output().where((0,T.and)((0,T.eq)(a.id,r.groupId),(0,T.eq)(a.tenantId,r.tenantId),(0,T.eq)(a.isActive,!0)))}}}0&&(module.exports={AUTH_DIRECTORY_PROVIDER_COLUMN,AUTH_DIRECTORY_TENANT_SCOPED_TABLES,MEMBERSHIP_MODES,NEWLY_UNHEALTHY_SYNC_OUTCOMES,STRANDED_REASON,UNHEALTHY_SYNC_OUTCOMES,assertGroupMayBeLinked,assertMembershipMode,classifySyncFailure,createDirectoryCatalogReader,createDirectoryCatalogWriter,createDirectoryLinkStore,createDirectorySync,createDirectorySyncRepository,createPromotionRepository,createSyncRequestQueue,defaultExternalGroupGuardMessage,defaultSyncMessages,defineAuthDirectorySchema,findLedgerRowsForIdentity,promoteLedgerForUser,transactionRunner,withExternalGroupGuards});
1
+ "use strict";var me=Object.defineProperty;var Oe=Object.getOwnPropertyDescriptor;var Ue=Object.getOwnPropertyNames;var $e=Object.prototype.hasOwnProperty;var Ne=(e,t)=>{for(var c in t)me(e,c,{get:t[c],enumerable:!0})},qe=(e,t,c,d)=>{if(t&&typeof t=="object"||typeof t=="function")for(let u of Ue(t))!$e.call(e,u)&&u!==c&&me(e,u,{get:()=>t[u],enumerable:!(d=Oe(t,u))||d.enumerable});return e};var ze=e=>qe(me({},"__esModule",{value:!0}),e);var pr={};Ne(pr,{AUTH_DIRECTORY_PROVIDER_COLUMN:()=>de,AUTH_DIRECTORY_TENANT_SCOPED_TABLES:()=>nr,DirectoryOperatorError:()=>K,MEMBERSHIP_MODES:()=>ge,NEWLY_UNHEALTHY_SYNC_OUTCOMES:()=>le,STRANDED_REASON:()=>_e,UNHEALTHY_SYNC_OUTCOMES:()=>Se,assertGroupMayBeLinked:()=>Je,assertMembershipMode:()=>W,classifySyncFailure:()=>Z,createDirectoryCatalogReader:()=>We,createDirectoryCatalogWriter:()=>je,createDirectoryLinkStore:()=>Ze,createDirectorySync:()=>ar,createDirectorySyncRepository:()=>cr,createOpenidMemberResolver:()=>Xe,createPromotionRepository:()=>tr,createSyncRequestQueue:()=>sr,defaultDirectoryLinkRefusals:()=>Ye,defaultExternalGroupGuardMessage:()=>He,defaultSyncMessages:()=>ve,defineAuthDirectorySchema:()=>or,findLedgerRowsForIdentity:()=>Q,promoteLedgerForUser:()=>er,transactionRunner:()=>U,withExternalGroupGuards:()=>Ve});module.exports=ze(pr);var ye=require("@aiquants/authz-core"),te=require("@aiquants/mssql"),_=require("drizzle-orm");var Ae=require("@aiquants/mssql");function U(e,t){return c=>(0,Ae.runWithTransactionRetry)(d=>e.transaction(u=>d(()=>c(u))),t.transactionRetry)}var Be=10,$={tenantId:64,provider:32,externalId:256,externalKey:320,name:256,description:1e3,actor:50},Te=2147483647;function Fe(e){if(e===void 0||!Number.isFinite(e))return null;let t=Math.trunc(e);return t<0?null:t>Te?Te:t}function J(e,t,c){let d=e.trim();if(d==="")throw new TypeError(`[@aiquants/auth-directory-drizzle] ${c} must not be blank`);if(d.length>t)throw new TypeError(`[@aiquants/auth-directory-drizzle] ${c} exceeds its column length (${t}); truncating it would change which rows it owns`);return d}function je(e,t,c){let d=e,u=c.ensureConnection??(async()=>{}),a=U(d,c),y=t.TDDirectoryCatalog,D=t.TMDirectoryCatalogObservation,I=f=>{let s=new Map;for(let n of f)s.set(n.externalId,n);return[...s.values()]};return{async replace(f,s,n){await u();let i=J((0,ye.assertTenantId)(f,"replace(tenantId)"),$.tenantId,"tenantId"),r=J(s,$.provider,"provider"),b=J(c.actor,$.actor,"actor"),l=c.now(),S=I(n.map(w=>({tenantId:i,provider:r,externalId:w.externalId.slice(0,$.externalId),externalKey:w.externalKey.slice(0,$.externalKey),name:w.name.slice(0,$.name),description:w.description===void 0?null:w.description.slice(0,$.description),memberCount:Fe(w.memberCount),observedAt:l,createdAt:l,createdBy:b})));return await a(async w=>{await w.delete(y).where((0,_.and)((0,_.eq)(y.tenantId,i),(0,_.eq)(y.provider,r)));for(let x of(0,te.chunkByParameters)(S,Be))await w.insert(y).values(x);await w.delete(D).where((0,_.and)((0,_.eq)(D.tenantId,i),(0,_.eq)(D.provider,r))),await w.insert(D).values([{tenantId:i,provider:r,observedAt:l,entryCount:S.length,createdAt:l,createdBy:b,updatedAt:l,updatedBy:b}])}),S.length}}}function We(e,t,c={}){let d=e,u=c.ensureConnection??(async()=>{}),a=t.TDDirectoryCatalog,y=t.TMDirectoryCatalogObservation;return{async read(D,I){await u();let f=J((0,ye.assertTenantId)(D,"read(tenantId)"),$.tenantId,"tenantId"),s=J(I,$.provider,"provider"),n=await d.select({observedAt:y.observedAt,entryCount:y.entryCount,externalId:a.externalId,externalKey:a.externalKey,name:a.name,description:a.description,memberCount:a.memberCount}).from(y).leftJoin(a,(0,_.and)((0,_.eq)(a.tenantId,y.tenantId),(0,_.eq)(a.provider,y.provider))).where((0,_.and)((0,_.eq)(y.tenantId,f),(0,_.eq)(y.provider,s))).orderBy((0,_.asc)(a.name)),i=n.filter(l=>l.externalId!==null).map(l=>({externalId:String(l.externalId),externalKey:String(l.externalKey),name:String(l.name),...l.description===null?{}:{description:l.description},...l.memberCount===null?{}:{memberCount:Number(l.memberCount)}})),r=n[0];if(r===void 0)return{groups:i,observedAt:null};if(Number(r.entryCount)!==i.length)throw new Error(`[@aiquants/auth-directory-drizzle] catalog is inconsistent for (${f}, ${s}): the observation records ${r.entryCount} entries but ${i.length} rows are present`);let b=(0,te.toInstant)(r.observedAt);if(!Number.isFinite(b.getTime()))throw new Error(`[@aiquants/auth-directory-drizzle] catalog observation for (${f}, ${s}) has an unreadable observed_at`);return{groups:i,observedAt:b}}}}var ke=require("@aiquants/authz-core");var K=class extends Error{constructor(c){super(c);this.operatorFacing=!0;this.name="DirectoryOperatorError"}};var He=(e,t)=>`\u5916\u90E8\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u9023\u643A\u30B0\u30EB\u30FC\u30D7 (${t}) \u306B\u306F\u7BA1\u7406\u30ED\u30FC\u30EB (${e}) \u3092\u6301\u305F\u305B\u3089\u308C\u307E\u305B\u3093\u3002\u4E0A\u6D41\u5074\u306E\u6240\u5C5E\u5909\u66F4\u304C\u305D\u306E\u307E\u307E\u7BA1\u7406\u8005\u306E\u5897\u6E1B\u306B\u306A\u308B\u305F\u3081\u3001\u7BA1\u7406\u8005\u7D4C\u8DEF\u306F\u30ED\u30FC\u30AB\u30EB\u7BA1\u7406\u306E\u30B0\u30EB\u30FC\u30D7\u306B\u9650\u5B9A\u3057\u3066\u3044\u307E\u3059\u3002`;function j(e,t,c,d){return new Set(e.filter(u=>t.has(u.roleId)&&u.appKey===c&&u.resourceKey===d).map(u=>u.action))}function ne(e,t){return new Set(e.filter(c=>c.groupId===t).map(c=>c.roleId))}var Y=e=>ke.AUTHZ_ACTIONS.every(t=>e.has(t)),ie=(e,t)=>e.find(c=>c.roleId===t)?.roleKey??`#${t}`;function oe(e,t,c,d){let u=[...t].find(y=>Y(j(e,new Set([y]),c,d)));return u!==void 0?ie(e,u):[...t].filter(y=>j(e,new Set([y]),c,d).size>0).map(y=>ie(e,y)).join(" + ")}function Ve(e,t){return xe(e,t)}function xe(e,t){let c=e;return{...e,withTransaction:(d,u)=>c.withTransaction((a,y)=>d(xe(a,t),y),u),async assignGroupRole(d,u,a,y){await c.withTransaction(async(D,I)=>{let f=await D.listGrants(d),s=ne(await D.listGroupRoles(d),u);if(s.add(a),Y(j(f,s,t.adminAppKey,t.adminResourceKey))){let n=await t.linkLookup.externalGroupKey(d,u,I);if(n!==null)throw new K(t.message(oe(f,s,t.adminAppKey,t.adminResourceKey),n))}await D.assignGroupRole(d,u,a,y)})},async updateGrant(d,u,a,y){let D=a.action;if(D===void 0)return c.updateGrant(d,u,a,y);await c.withTransaction(async(I,f)=>{let s=await I.listGrants(d),n=s.find(b=>b.id===u);if(n===void 0)return I.updateGrant(d,u,a,y);let r=(await I.listResources(d)).find(b=>b.id===n.resourceId);if(r?.appKey===t.adminAppKey&&r?.resourceKey===t.adminResourceKey){let b=await I.listGroupRoles(d);for(let l of new Set(b.filter(S=>S.roleId===n.roleId).map(S=>S.groupId))){let S=ne(b,l),w=j(s.filter(k=>k.id!==u),S,t.adminAppKey,t.adminResourceKey);if(S.has(n.roleId)&&w.add(D),!Y(w))continue;let x=await t.linkLookup.externalGroupKey(d,l,f);if(x!==null)throw new K(t.message(oe(s,S,t.adminAppKey,t.adminResourceKey)||ie(s,n.roleId),x))}}await I.updateGrant(d,u,a,y)})},async createGrant(d,u,a){await c.withTransaction(async(y,D)=>{let f=(await y.listResources(d)).find(s=>s.id===u.resourceId);if(f?.appKey===t.adminAppKey&&f?.resourceKey===t.adminResourceKey){let s=await y.listGrants(d),n=await y.listGroupRoles(d);for(let i of new Set(n.filter(r=>r.roleId===u.roleId).map(r=>r.groupId))){let r=j(s,ne(n,i),t.adminAppKey,t.adminResourceKey);if(r.add(u.action),!Y(r))continue;let b=await t.linkLookup.externalGroupKey(d,i,D);if(b!==null)throw new K(t.message(oe(s,ne(n,i),t.adminAppKey,t.adminResourceKey)||ie(s,u.roleId),b))}}await y.createGrant(d,u,a)})}}}async function Je(e,t,c,d){let u=(await e.listGroupRoles(t)).filter(I=>I.groupId===c);if(u.length===0)return;let a=await e.listGrants(t),y=new Set(u.map(I=>I.roleId));if(!Y(j(a,y,d.adminAppKey,d.adminResourceKey)))return;let D=u[0];if(D)throw new K(d.message(oe(a,y,d.adminAppKey,d.adminResourceKey),D.groupKey))}var z=require("drizzle-orm");async function Q(e,t,c){let{ledger:d,group:u}=t,{tenantId:a,externalUserId:y,email:D}=c;if(y===null&&D==="")return[];let I={ledgerId:d.id,groupId:d.groupId,tenantId:u.tenantId,externalUserId:d.externalUserId},f=async b=>(await e.select(I).from(d).innerJoin(u,(0,z.and)((0,z.eq)(u.id,d.groupId),(0,z.eq)(u.tenantId,a),(0,z.eq)(u.isActive,!0))).where(b)).map(S=>({ledgerId:Number(S.ledgerId),groupId:Number(S.groupId),tenantId:String(S.tenantId),externalUserId:S.externalUserId===null?null:String(S.externalUserId)})),s=b=>{let l=new Map;for(let S of b)l.set(S.ledgerId,{ledgerId:S.ledgerId,groupId:S.groupId,tenantId:S.tenantId});return[...l.values()].sort((S,w)=>S.ledgerId-w.ledgerId)},n=y===null?[]:await f((0,z.eq)(d.externalUserId,y));if(D==="")return s(n);let i=await f((0,z.eq)(d.email,D)),r=b=>b.externalUserId!==null&&b.externalUserId!==y;return s([...n,...i.filter(b=>!r(b))])}var N=require("@aiquants/auth-core"),Re=require("@aiquants/auth-directory-core"),C=require("@aiquants/authz-core"),he=require("@aiquants/mssql"),m=require("drizzle-orm");var ge=["direct","transitive"];function W(e){if(!ge.includes(e))throw new TypeError(`[@aiquants/auth-directory-drizzle] membershipMode must be one of ${ge.join(" | ")}, received ${JSON.stringify(e)}`);return e}var Ye={breakGlassLink:e=>`\u30B0\u30EB\u30FC\u30D7 (${e}) \u306F\u7DCA\u6025\u7528 (break-glass) \u306E\u30C6\u30CA\u30F3\u30C8\u5E30\u5C5E\u306E\u7D4C\u8DEF\u306B\u6307\u5B9A\u3055\u308C\u3066\u3044\u308B\u305F\u3081\u3001\u4E0A\u6D41\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3068\u9023\u643A\u3067\u304D\u307E\u305B\u3093\u3002\u3053\u306E\u7D4C\u8DEF\u306F\u3001\u540C\u671F\u304C\u89E6\u308C\u306A\u3044\u30ED\u30FC\u30AB\u30EB\u7BA1\u7406\u306E\u30B0\u30EB\u30FC\u30D7\u306B\u9650\u3063\u3066\u3044\u307E\u3059\u3002`,allowlistNeedsLink:e=>`\u30B0\u30EB\u30FC\u30D7 (${e}) \u306F\u30ED\u30FC\u30AB\u30EB\u7BA1\u7406\u306E\u305F\u3081\u3001\u30B5\u30A4\u30F3\u30A4\u30F3\u8A31\u53EF\u306B\u4F7F\u3048\u307E\u305B\u3093\u3002\u30B0\u30EB\u30FC\u30D7\u5358\u4F4D\u306E\u30B5\u30A4\u30F3\u30A4\u30F3\u8A31\u53EF\u306F\u4E0A\u6D41\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306E\u30E1\u30F3\u30D0\u30FC\u53F0\u5E33\u3067\u5224\u5B9A\u3059\u308B\u306E\u3067\u3001\u4E0A\u6D41\u3068\u9023\u643A\u3057\u305F\u30B0\u30EB\u30FC\u30D7\u3060\u3051\u3092\u6307\u5B9A\u3067\u304D\u307E\u3059\u3002`};function Qe(e){let t=typeof e=="object"&&e!==null&&"number"in e?e.number:void 0;return t===2601||t===2627}function Ze(e,t,c,d){let u=e,a=d.ensureConnection??(async()=>{}),y=U(u,d),D=d.policyCacheTtlMs??0;if(!Number.isFinite(D)||D<0)throw new TypeError(`[@aiquants/auth-directory-drizzle] policyCacheTtlMs must be a non-negative number, received ${String(d.policyCacheTtlMs)}`);let I=new Map,f=o=>I.delete(o),{TDGroupExternalLink:s,TDGroupMemberEmail:n,TDTenantMembershipGroup:i,TDAllowlistGroup:r,TDDirectorySyncRequest:b}=t,{TMGroup:l,TDGroupMember:S}=c;async function w(o,g,h,v){if((await o.select({groupId:i.groupId}).from(i).where((0,m.and)((0,m.eq)(i.tenantId,g),(0,m.eq)(i.groupId,h),(0,m.eq)(i.isBreakGlass,!0)))).length>0)throw new K(d.refusals.breakGlassLink(v))}async function x(o,g,h){let[v]=await o.select({groupKey:l.groupKey}).from(l).where((0,m.and)((0,m.eq)(l.id,h),(0,m.eq)(l.tenantId,g)));if(!v)throw new Error(`Group not found in tenant: ${h}`);return String(v.groupKey)}function k(o,g){return{linkId:Number(o.linkId),groupId:Number(o.groupId),tenantId:String(o.tenantId),provider:String(o.provider),externalId:String(o.externalId),externalKey:String(o.externalKey),membershipMode:W(String(o.membershipMode)),isSyncEnabled:!!o.isSyncEnabled,...o.syncedAt?{syncedAt:(0,he.toInstant)(o.syncedAt).toISOString()}:{},...o.syncError?{syncError:String(o.syncError)}:{},unresolved:g}}async function A(o){let g=await u.select({groupId:n.groupId,email:n.email,externalUserId:n.externalUserId}).from(n).innerJoin(l,(0,m.and)((0,m.eq)(l.id,n.groupId),(0,m.eq)(l.tenantId,o))).where((0,m.isNull)(n.resolvedUserId));if(g.length===0)return new Map;let h=g.map(M=>({email:String(M.email),...M.externalUserId?{externalUserId:String(M.externalUserId)}:{}})),v=await d.memberResolver.resolve(h);if(v.length!==h.length)throw new Error(`[@aiquants/auth-directory-drizzle] memberResolver returned ${v.length} member(s) for ${h.length}`);let R=new Map;return g.forEach((M,E)=>{let O=v[E];if((0,N.normalizeAuthEmail)(O.email)!==(0,N.normalizeAuthEmail)(h[E].email))throw new Error(`[@aiquants/auth-directory-drizzle] memberResolver answered ${O.email} at position ${E}, where ${h[E].email} was asked`);let G=O.resolvedUserId===null?O.unresolvedReason:"pendingSync",F=Number(M.groupId),re=R.get(F);re===void 0&&(re={...N.AUTH_DIRECTORY_NO_UNRESOLVED},R.set(F,re)),re[G]+=1}),R}return{async listLinks(o){(0,C.assertTenantId)(o,"listLinks(tenantId)"),await a();let g=await u.select({linkId:s.id,groupId:s.groupId,tenantId:s.tenantId,provider:s.provider,externalId:s.externalId,externalKey:s.externalKey,membershipMode:s.membershipMode,isSyncEnabled:s.isSyncEnabled,syncedAt:s.syncedAt,syncError:s.syncError}).from(s).where((0,m.eq)(s.tenantId,o)).orderBy((0,m.asc)(s.groupId)),h=await A(o);return g.map(v=>((0,C.assertTenantMatch)(o,String(v.tenantId),"listLinks(row.tenantId)"),k(v,h.get(Number(v.groupId))??N.AUTH_DIRECTORY_NO_UNRESOLVED)))},async unresolvedByGroup(o){return(0,C.assertTenantId)(o,"unresolvedByGroup(tenantId)"),await a(),A(o)},async externalGroupKey(o,g,h){(0,C.assertTenantId)(o,"externalGroupKey(tenantId)"),await a();let[v]=await(h??u).select({externalKey:s.externalKey}).from(s).where((0,m.and)((0,m.eq)(s.groupId,g),(0,m.eq)(s.tenantId,o)));return v?String(v.externalKey):null},async linkGroup(o,g,h,v){(0,C.assertTenantId)(o,"linkGroup(tenantId)");let R=W(h.membershipMode),M=h.externalId.trim(),E=(0,N.normalizeAuthEmail)(h.externalKey);if(M===""||E==="")throw new TypeError("[@aiquants/auth-directory-drizzle] externalId and externalKey must be non-empty");await a(),await d.linkable.assertLinkableAndWrite(o,g,async O=>{let G=O,F=await x(G,o,g);await w(G,o,g,F),await G.insert(s).output().values({groupId:g,tenantId:o,provider:h.provider,externalId:M,externalKey:E,membershipMode:R,isSyncEnabled:!0,vanishedObservations:0,createdAt:new Date,createdBy:v})}),f(o)},async unlinkGroup(o,g){(0,C.assertTenantId)(o,"unlinkGroup(tenantId)"),await a(),await y(async h=>{await x(h,o,g),await h.delete(n).where((0,m.eq)(n.groupId,g)),await h.delete(r).where((0,m.and)((0,m.eq)(r.groupId,g),(0,m.eq)(r.tenantId,o))),await h.delete(s).where((0,m.and)((0,m.eq)(s.groupId,g),(0,m.eq)(s.tenantId,o)))}),f(o)},async setSyncEnabled(o,g,h,v){(0,C.assertTenantId)(o,"setSyncEnabled(tenantId)"),await a(),await u.update(s).set({isSyncEnabled:h,updatedAt:new Date,updatedBy:v}).output().where((0,m.and)((0,m.eq)(s.groupId,g),(0,m.eq)(s.tenantId,o)))},async requestSync(o,g,h){return(0,C.assertTenantId)(o,"requestSync(tenantId)"),await a(),u.transaction(async v=>{if(g!==null&&await x(v,o,g),(await v.select({id:b.id}).from(b).where((0,m.and)((0,m.eq)(b.tenantId,o),g===null?(0,m.isNull)(b.groupId):(0,m.eq)(b.groupId,g),(0,m.isNull)(b.pickedAt)))).length>0)return"already-pending";try{await v.insert(b).output().values({tenantId:o,groupId:g,requestedBy:h,requestedAt:new Date,createdAt:new Date,createdBy:h})}catch(M){if(!Qe(M))throw M;return"already-pending"}return"queued"})},async getStatus(o){(0,C.assertTenantId)(o,"getStatus(tenantId)"),await a();let g=await u.select({groupId:s.groupId,isSyncEnabled:s.isSyncEnabled,syncedAt:s.syncedAt,syncError:s.syncError}).from(s).where((0,m.eq)(s.tenantId,o)),[h]=await u.select({n:(0,m.count)()}).from(b).where((0,m.and)((0,m.eq)(b.tenantId,o),(0,m.isNull)(b.finishedAt))),v=g.filter(G=>!!G.isSyncEnabled),R=v.map(G=>G.syncedAt?(0,he.toInstant)(G.syncedAt).getTime():null),M=R.length>0&&R.every(G=>G!==null)?Math.min(...R):null,O=g.length>0&&v.length===0||v.length>0&&(M===null||Date.now()-M>d.stalenessThresholdSec*1e3);return{isEnabled:d.isEnabled,provider:d.provider,linkedGroupCount:g.length,...M!==null?{oldestSyncedAt:new Date(M).toISOString()}:{},isStale:O,pausedGroupCount:g.length-v.length,stalenessThresholdSec:d.stalenessThresholdSec,erroredGroupIds:g.filter(G=>!!G.syncError).map(G=>Number(G.groupId)).sort((G,F)=>G-F),pendingRequestCount:Number(h?.n??0)}},async loadTenantMembershipPolicy(o){(0,C.assertTenantId)(o,"loadTenantMembershipPolicy(tenantId)");let g=D>0?I.get(o):void 0;if(g&&Date.now()-g.loadedAt<D)return g.policy;await a();let h=await u.select({groupId:i.groupId,isBreakGlass:i.isBreakGlass,externalLinkId:s.id}).from(i).innerJoin(l,(0,m.and)((0,m.eq)(l.id,i.groupId),(0,m.eq)(l.tenantId,o))).leftJoin(s,(0,m.and)((0,m.eq)(s.groupId,i.groupId),(0,m.eq)(s.tenantId,o))).where((0,m.eq)(i.tenantId,o));if(h.length===0){let E={kind:"no-proof-required"};return D>0&&I.set(o,{policy:E,loadedAt:Date.now()}),E}let v=h.map(E=>Number(E.groupId)),R=h.filter(E=>!!E.isBreakGlass);for(let E of R)if(E.externalLinkId!==null)throw new Error(`Break-glass group ${Number(E.groupId)} is externally sourced; a break-glass proof path must be one the synchronization can never touch.`);let M={kind:"require-proof",proofGroupIds:v,breakGlassGroupIds:R.map(E=>Number(E.groupId))};return(0,Re.assertTenantMembershipPolicy)(M,`tenant ${o}`),D>0&&I.set(o,{policy:M,loadedAt:Date.now()}),M},async setTenantMembershipGroup(o,g,h,v){(0,C.assertTenantId)(o,"setTenantMembershipGroup(tenantId)"),await a(),await y(async R=>{if(await x(R,o,g),h){let[E]=await R.select({id:s.id}).from(s).where((0,m.and)((0,m.eq)(s.tenantId,o),(0,m.eq)(s.groupId,g)));if(E)throw new Error(`Group ${g} is externally sourced; a break-glass proof path must be one the synchronization can never touch.`)}let M=await R.select({groupId:i.groupId}).from(i).where((0,m.and)((0,m.eq)(i.tenantId,o),(0,m.eq)(i.groupId,g)));if(!h&&!(await R.select({groupId:i.groupId,isBreakGlass:i.isBreakGlass}).from(i).where((0,m.eq)(i.tenantId,o))).filter(G=>Number(G.groupId)!==g).some(G=>!!G.isBreakGlass))throw new Error(`Declaring group ${g} as ordinary proof would leave tenant ${o} with a membership proof but no break-glass path; declare a locally-managed break-glass group first.`);if(M.length>0){await R.update(i).set({isBreakGlass:h,updatedAt:new Date,updatedBy:v}).output().where((0,m.and)((0,m.eq)(i.tenantId,o),(0,m.eq)(i.groupId,g)));return}await R.insert(i).output().values({tenantId:o,groupId:g,isBreakGlass:h,createdAt:new Date,createdBy:v})}),f(o)},async removeTenantMembershipGroup(o,g){(0,C.assertTenantId)(o,"removeTenantMembershipGroup(tenantId)"),await a(),await y(async h=>{let R=(await h.select({groupId:i.groupId,isBreakGlass:i.isBreakGlass}).from(i).where((0,m.eq)(i.tenantId,o))).filter(M=>Number(M.groupId)!==g);if(R.length>0&&!R.some(M=>!!M.isBreakGlass))throw new Error(`Removing group ${g} would leave tenant ${o} with a membership proof but no break-glass path.`);await h.delete(i).where((0,m.and)((0,m.eq)(i.tenantId,o),(0,m.eq)(i.groupId,g)))}),f(o)},async setGroupActive(o,g,h,v){(0,C.assertTenantId)(o,"setGroupActive(tenantId)"),await a(),await u.update(l).set({isActive:h,updatedAt:new Date,updatedBy:v}).output().where((0,m.and)((0,m.eq)(l.id,g),(0,m.eq)(l.tenantId,o)))},async listLedgerGroupIds(o,g){if((0,C.assertTenantId)(o,"listLedgerGroupIds(tenantId)"),!d.isEnabled)return[];await a();let h=await Q(u,{ledger:n,group:l},{tenantId:o,externalUserId:g.externalUserId?.trim()||null,email:(0,N.normalizeAuthEmail)(g.email)});return[...new Set(h.map(v=>v.groupId))].sort((v,R)=>v-R)},async isEmailAllowedByGroup(o){if(!d.isEnabled)return!1;let g=(0,N.normalizeAuthEmail)(o);if(g==="")return!1;await a();let[h]=await u.select({id:n.id}).from(n).innerJoin(r,(0,m.eq)(r.groupId,n.groupId)).innerJoin(l,(0,m.and)((0,m.eq)(l.id,n.groupId),(0,m.eq)(l.isActive,!0))).where((0,m.eq)(n.email,g));return!!h},async listAllowlistGroups(o){return(0,C.assertTenantId)(o,"listAllowlistGroups(tenantId)"),await a(),(await u.select({id:r.id,groupId:r.groupId,groupKey:l.groupKey,groupName:l.name,description:r.description}).from(r).innerJoin(l,(0,m.and)((0,m.eq)(l.id,r.groupId),(0,m.eq)(l.tenantId,o))).where((0,m.eq)(r.tenantId,o)).orderBy((0,m.asc)(l.name))).map(h=>({id:Number(h.id),type:"group",groupId:Number(h.groupId),groupKey:String(h.groupKey),groupName:String(h.groupName),...h.description?{description:String(h.description)}:{},owner:{kind:"tenant",tenantId:o},readOnly:!1}))},async addAllowlistGroup(o,g,h,v){(0,C.assertTenantId)(o,"addAllowlistGroup(tenantId)"),await a(),await y(async R=>{let M=await x(R,o,g),[E]=await R.select({id:s.id}).from(s).where((0,m.and)((0,m.eq)(s.groupId,g),(0,m.eq)(s.tenantId,o)));if(!E)throw new K(d.refusals.allowlistNeedsLink(M));await R.insert(r).output().values({tenantId:o,groupId:g,description:h??null,createdAt:new Date,createdBy:v})})},async removeAllowlistGroup(o,g){(0,C.assertTenantId)(o,"removeAllowlistGroup(tenantId)"),await a(),await u.delete(r).where((0,m.and)((0,m.eq)(r.id,g),(0,m.eq)(r.tenantId,o)))},async countProjectedMembers(o,g){(0,C.assertTenantId)(o,"countProjectedMembers(tenantId)"),await a();let[h]=await u.select({n:(0,m.count)()}).from(S).innerJoin(l,(0,m.and)((0,m.eq)(l.id,S.groupId),(0,m.eq)(l.tenantId,o))).where((0,m.eq)(S.groupId,g));return Number(h?.n??0)}}}var ae=require("@aiquants/auth-core"),se=require("@aiquants/mssql"),q=require("drizzle-orm");function be(e,t,c){let d=e.get(t);d===void 0?e.set(t,new Set([c])):d.add(c)}function fe(e){let[t,...c]=e;if(t===void 0||c.length>0)throw new Error(`[@aiquants/auth-directory-drizzle] expected exactly one value, received ${e.size}`);return t}function Xe(e,t,c){let d=e,u=c.ensureConnection??(async()=>{}),{users:a,subjects:y}=t;return{async resolve(D){if(D.length===0)return[];await u();let I=[...new Set(D.map(w=>w.externalUserId?.trim()).filter(w=>!!w))],f=[...new Set(D.map(w=>(0,ae.normalizeAuthEmail)(w.email)).filter(w=>w!==""))],s=new Map,n=new Set;for(let w of(0,se.chunk)(I)){let x=await d.select({subject:y.subject,userId:y.userId,mappingActive:y.isActive,userActive:a.isActive}).from(y.table).innerJoin(a.table,(0,q.eq)(a.id,y.userId)).where((0,q.inArray)(y.subject,w));for(let k of x){let A=String(k.subject);n.add(A),k.mappingActive&&k.userActive&&be(s,A,Number(k.userId))}}let i=new Map,r=new Set;for(let w of(0,se.chunk)(f)){let x=await d.select({id:a.id,email:a.email,isActive:a.isActive}).from(a.table).where((0,q.inArray)(a.email,w));for(let k of x){let A=(0,ae.normalizeAuthEmail)(String(k.email));r.add(A),k.isActive&&be(i,A,Number(k.id))}}let b=[...i.values()].filter(w=>w.size===1).map(fe),l=new Map;for(let w of(0,se.chunk)([...new Set(b)])){let x=await d.select({userId:y.userId,subject:y.subject}).from(y.table).where((0,q.and)((0,q.inArray)(y.userId,w),(0,q.eq)(y.isActive,!0)));for(let k of x)be(l,Number(k.userId),String(k.subject))}let S=w=>{let x=v=>({...w,resolvedUserId:v,unresolvedReason:null}),k=v=>({...w,resolvedUserId:null,unresolvedReason:v}),A=w.externalUserId?.trim(),o=(0,ae.normalizeAuthEmail)(w.email),g=A?s.get(A):void 0;if(g!==void 0)return g.size===1?x(fe(g)):k("ambiguous");let h=i.get(o);if(h!==void 0){if(h.size>1)return k("ambiguous");let v=fe(h),R=l.get(v);return A&&R!==void 0&&!R.has(A)?k("claimedByAnother"):x(v)}return A&&n.has(A)||r.has(o)?k("deactivated"):k("unregistered")};return D.map(S)}}}var Me=require("@aiquants/auth-core"),ue=require("@aiquants/authz-core");async function er(e,t){let c=(0,ue.assertTenantId)(t.tenantId,"promoteLedgerForUser(tenantId)");if(!Number.isInteger(t.userId)||t.userId<=0)throw new TypeError(`[@aiquants/auth-directory-drizzle] userId must be a positive integer, received ${String(t.userId)}`);let d=(0,Me.normalizeAuthEmail)(t.email),u=t.externalUserId?.trim()||null;if(u===null&&d==="")return{addedGroupIds:[],matchedLedgerIds:[]};let a=await e.findLedgerMatches({tenantId:c,externalUserId:u,email:d});if(a.length===0)return{addedGroupIds:[],matchedLedgerIds:[]};for(let f of a)(0,ue.assertTenantMatch)(c,f.tenantId,"promoteLedgerForUser(findLedgerMatches row)");let y=[...new Set(a.map(f=>f.ledgerId))].sort((f,s)=>f-s),D=[...new Set(a.map(f=>f.groupId))].sort((f,s)=>f-s);return{addedGroupIds:[...await e.attachUserToGroups({tenantId:c,userId:t.userId,ledgerIds:y,groupIds:D})].sort((f,s)=>f-s),matchedLedgerIds:y}}var we=require("@aiquants/authz-core"),H=require("@aiquants/mssql"),Ee=require("@aiquants/mssql/drizzle"),L=require("drizzle-orm");var rr=4;function tr(e,t,c,d){let u=e,a=d.ensureConnection??(async()=>{}),y=U(u,d),D=d.now??(()=>new Date),I=t.TDGroupMemberEmail,{TMGroup:f,TDGroupMember:s}=c;return{async findLedgerMatches(n){return(0,we.assertTenantId)(n.tenantId,"findLedgerMatches(tenantId)"),await a(),Q(u,{ledger:I,group:f},n)},async attachUserToGroups({tenantId:n,userId:i,ledgerIds:r,groupIds:b}){return(0,we.assertTenantId)(n,"attachUserToGroups(tenantId)"),await a(),r.length===0?[]:y(async l=>{let S=new Set;for(let A of(0,H.chunk)(b)){let o=await l.select({id:f.id}).from(f).where((0,L.and)((0,L.inArray)(f.id,A),(0,L.eq)(f.tenantId,n),(0,L.eq)(f.isActive,!0)));for(let g of o)S.add(Number(g.id))}let w=b.filter(A=>S.has(A));if(w.length===0)return[];let x=new Set;for(let A of(0,H.chunk)(w)){let o=await l.select({groupId:s.groupId}).from(s).where((0,L.and)((0,L.eq)(s.userId,i),(0,L.inArray)(s.groupId,A)));for(let g of o)x.add(Number(g.groupId))}let k=w.filter(A=>!x.has(A));for(let A of(0,H.chunk)(r))await l.update(I).set({resolvedUserId:i}).output().where((0,L.and)((0,L.inArray)(I.id,A),(0,Ee.inJsonList)(I.groupId,w)));if(k.length>0){let A=D();for(let o of(0,H.chunkByParameters)(k,rr))await l.insert(s).output().values(o.map(g=>({groupId:g,userId:i,createdAt:A,createdBy:d.actor})))}return k})}}}var B=require("@aiquants/authz-drizzle"),p=require("drizzle-orm/mssql-core"),de="provider",nr=["TDGroupExternalLink","TDTenantMembershipGroup","TDAllowlistGroup","TDDirectorySyncRequest","TDDirectoryCatalog","TMDirectoryCatalogObservation","TMAllowlist"];function or(e,t){let c=(0,p.mssqlSchema)(e),d=()=>({createdAt:(0,p.datetime2)("created_at"),createdBy:(0,p.nvarchar)("created_by",{length:50}),updatedAt:(0,p.datetime2)("updated_at"),updatedBy:(0,p.nvarchar)("updated_by",{length:50})}),u=()=>({createdAt:(0,p.datetime2)("created_at"),createdBy:(0,p.nvarchar)("created_by",{length:50})}),a=c.table("TDGroupExternalLink",{id:(0,p.bigint)("id",{mode:"number"}).identity().notNull(),groupId:(0,p.bigint)("group_id",{mode:"number"}).notNull(),tenantId:(0,p.nvarchar)(B.AUTHZ_TENANT_COLUMN,{length:64}).notNull(),provider:(0,p.nvarchar)(de,{length:32}).notNull(),externalId:(0,p.nvarchar)("external_id",{length:256}).notNull(),externalKey:(0,p.nvarchar)("external_key",{length:320}).notNull(),membershipMode:(0,p.nvarchar)("membership_mode",{length:16}).notNull(),isSyncEnabled:(0,p.bit)("is_sync_enabled").notNull(),syncedAt:(0,p.datetime2)("synced_at"),syncError:(0,p.nvarchar)("sync_error",{length:1e3}),vanishedObservations:(0,p.int)("vanished_observations").notNull(),...d()},r=>[(0,p.primaryKey)({name:`${e}_TDGroupExternalLink_pk`,columns:[r.id]}),(0,p.uniqueIndex)(`${e}_TDGroupExternalLink_group_uq`).on(r.groupId),(0,p.uniqueIndex)(`${e}_TDGroupExternalLink_external_uq`).on(r.tenantId,r.provider,r.externalId),(0,p.foreignKey)({name:`${e}_TDGroupExternalLink_group_id_fk`,columns:[r.groupId],foreignColumns:[t.groupTable.id]})]),y=c.table("TDGroupMemberEmail",{id:(0,p.bigint)("id",{mode:"number"}).identity().notNull(),groupId:(0,p.bigint)("group_id",{mode:"number"}).notNull(),email:(0,p.nvarchar)("email",{length:320}).notNull(),externalUserId:(0,p.nvarchar)("external_user_id",{length:128}),resolvedUserId:(0,p.bigint)("resolved_user_id",{mode:"number"}),observedAt:(0,p.datetime2)("observed_at").notNull(),...d()},r=>[(0,p.primaryKey)({name:`${e}_TDGroupMemberEmail_pk`,columns:[r.id]}),(0,p.uniqueIndex)(`${e}_TDGroupMemberEmail_group_email_uq`).on(r.groupId,r.email),(0,p.foreignKey)({name:`${e}_TDGroupMemberEmail_group_id_fk`,columns:[r.groupId],foreignColumns:[t.groupTable.id]})]),D=c.table("TDTenantMembershipGroup",{tenantId:(0,p.nvarchar)(B.AUTHZ_TENANT_COLUMN,{length:64}).notNull(),groupId:(0,p.bigint)("group_id",{mode:"number"}).notNull(),isBreakGlass:(0,p.bit)("is_break_glass").notNull(),...u()},r=>[(0,p.primaryKey)({name:`${e}_TDTenantMembershipGroup_pk`,columns:[r.tenantId,r.groupId]}),(0,p.foreignKey)({name:`${e}_TDTenantMembershipGroup_group_id_fk`,columns:[r.groupId],foreignColumns:[t.groupTable.id]})]),I=c.table("TDAllowlistGroup",{id:(0,p.bigint)("id",{mode:"number"}).identity().notNull(),tenantId:(0,p.nvarchar)(B.AUTHZ_TENANT_COLUMN,{length:64}).notNull(),groupId:(0,p.bigint)("group_id",{mode:"number"}).notNull(),description:(0,p.nvarchar)("description",{length:512}),...u()},r=>[(0,p.primaryKey)({name:`${e}_TDAllowlistGroup_pk`,columns:[r.id]}),(0,p.uniqueIndex)(`${e}_TDAllowlistGroup_tenant_group_uq`).on(r.tenantId,r.groupId),(0,p.foreignKey)({name:`${e}_TDAllowlistGroup_group_id_fk`,columns:[r.groupId],foreignColumns:[t.groupTable.id]})]),f=c.table("TDDirectorySyncRequest",{id:(0,p.bigint)("id",{mode:"number"}).identity().notNull(),tenantId:(0,p.nvarchar)(B.AUTHZ_TENANT_COLUMN,{length:64}).notNull(),groupId:(0,p.bigint)("group_id",{mode:"number"}),requestedBy:(0,p.nvarchar)("requested_by",{length:320}).notNull(),requestedAt:(0,p.datetime2)("requested_at").notNull(),pickedAt:(0,p.datetime2)("picked_at"),finishedAt:(0,p.datetime2)("finished_at"),result:(0,p.nvarchar)("result",{length:32}),error:(0,p.nvarchar)("error",{length:2e3}),...u()},r=>[(0,p.primaryKey)({name:`${e}_TDDirectorySyncRequest_pk`,columns:[r.id]})]),s=c.table("TMAllowlist",{id:(0,p.bigint)("id",{mode:"number"}).identity().notNull(),tenantId:(0,p.nvarchar)(B.AUTHZ_TENANT_COLUMN,{length:64}).notNull(),pattern:(0,p.nvarchar)("pattern",{length:256}).notNull(),type:(0,p.nvarchar)("type",{length:32}).notNull(),description:(0,p.nvarchar)("description",{length:512}),...u()},r=>[(0,p.primaryKey)({name:`${e}_TMAllowlist_pk`,columns:[r.id]}),(0,p.uniqueIndex)(`${e}_TMAllowlist_pattern_uq`).on(r.tenantId,r.pattern)]),n=c.table("TDDirectoryCatalog",{id:(0,p.bigint)("id",{mode:"number"}).identity().notNull(),tenantId:(0,p.nvarchar)(B.AUTHZ_TENANT_COLUMN,{length:64}).notNull(),provider:(0,p.nvarchar)(de,{length:32}).notNull(),externalId:(0,p.nvarchar)("external_id",{length:256}).notNull(),externalKey:(0,p.nvarchar)("external_key",{length:320}).notNull(),name:(0,p.nvarchar)("name",{length:256}).notNull(),description:(0,p.nvarchar)("description",{length:1e3}),memberCount:(0,p.int)("member_count"),observedAt:(0,p.datetime2)("observed_at").notNull(),...u()},r=>[(0,p.primaryKey)({name:`${e}_TDDirectoryCatalog_pk`,columns:[r.id]}),(0,p.uniqueIndex)(`${e}_TDDirectoryCatalog_external_uq`).on(r.tenantId,r.provider,r.externalId)]),i=c.table("TMDirectoryCatalogObservation",{id:(0,p.bigint)("id",{mode:"number"}).identity().notNull(),tenantId:(0,p.nvarchar)(B.AUTHZ_TENANT_COLUMN,{length:64}).notNull(),provider:(0,p.nvarchar)(de,{length:32}).notNull(),observedAt:(0,p.datetime2)("observed_at").notNull(),entryCount:(0,p.int)("entry_count").notNull(),...d()},r=>[(0,p.primaryKey)({name:`${e}_TMDirectoryCatalogObservation_pk`,columns:[r.id]}),(0,p.uniqueIndex)(`${e}_TMDirectoryCatalogObservation_target_uq`).on(r.tenantId,r.provider)]);return{TDGroupExternalLink:a,TDGroupMemberEmail:y,TDTenantMembershipGroup:D,TDAllowlistGroup:I,TDDirectorySyncRequest:f,TDDirectoryCatalog:n,TMDirectoryCatalogObservation:i,TMAllowlist:s}}var ce=require("@aiquants/auth-core"),pe=require("@aiquants/auth-directory-core");function Z(e){if(e instanceof Error&&(e.name==="AbortError"||e.name==="TimeoutError"))return"timeout";let t=typeof e=="object"&&e!==null&&"status"in e?e.status:void 0;return typeof t!="number"?"unknown":t===401||t===403?"denied":t===429||t===408?"rateLimited":t>=500?"upstreamError":"unknown"}var Ge=e=>`${e.length} \u4EF6`,Ie={denied:"\u8CC7\u683C\u60C5\u5831\u304C\u62D2\u5426\u3055\u308C\u307E\u3057\u305F",rateLimited:"\u547C\u3073\u51FA\u3057\u304C\u5236\u9650\u3055\u308C\u3066\u3044\u307E\u3059",upstreamError:"\u76F8\u624B\u5074\u304C\u5185\u90E8\u30A8\u30E9\u30FC\u3092\u8FD4\u3057\u307E\u3057\u305F",timeout:"\u5FDC\u7B54\u304C\u3042\u308A\u307E\u305B\u3093\u3067\u3057\u305F",localWriteError:"\u66F8\u304D\u8FBC\u307F\u304C\u62D2\u5426\u3055\u308C\u307E\u3057\u305F",unknown:"\u8A73\u7D30\u306F\u30B5\u30FC\u30D0\u30FC\u306E\u30ED\u30B0\u306B\u51FA\u3066\u3044\u307E\u3059"},ve={upstreamUnreadable:e=>`\u4E0A\u6D41\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306E\u8AAD\u307F\u53D6\u308A\u306B\u5931\u6557\u3057\u307E\u3057\u305F (${Ie[e]})\u3002`,localWriteFailed:e=>`\u30ED\u30FC\u30AB\u30EB\u3078\u306E\u66F8\u304D\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F (${Ie[e]})\u3002`,resolverIncomplete:(e,t)=>`\u4E0A\u6D41\u304C ${e} \u4EF6\u306E\u30E1\u30F3\u30D0\u30FC\u3092\u8FD4\u3057\u307E\u3057\u305F\u304C\u3001\u7A81\u5408\u7D50\u679C\u306F ${t} \u4EF6\u3067\u3057\u305F\u3002\u4EF6\u6570\u304C\u6E1B\u308B\u7A81\u5408\u306F\u3001\u89E3\u6C7A\u3067\u304D\u306A\u304B\u3063\u305F\u4EBA\u7269\u3092\u300C\u4E0A\u6D41\u304B\u3089\u6D88\u3048\u305F\u300D\u3068\u3057\u3066\u524A\u9664\u3057\u3066\u3057\u307E\u3046\u305F\u3081\u4E2D\u65AD\u3057\u307E\u3057\u305F\u3002\u89E3\u6C7A\u3067\u304D\u306A\u3044\u30E1\u30F3\u30D0\u30FC\u306F\u7701\u7565\u305B\u305A resolvedUserId: null \u3067\u8FD4\u3059\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`,groupMissing:(e,t,c)=>`\u4E0A\u6D41\u3067\u30B0\u30EB\u30FC\u30D7\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (${e})\u3002\u9023\u7D9A ${t} \u56DE\u76EE\u3067\u3059\u3002${c} \u56DE\u306B\u9054\u3057\u305F\u6642\u70B9\u3067\u30B0\u30EB\u30FC\u30D7\u3092\u7121\u52B9\u5316\u3057\u307E\u3059\u3002`,groupVanished:(e,t)=>`\u4E0A\u6D41\u3067\u30B0\u30EB\u30FC\u30D7\u304C ${t} \u56DE\u9023\u7D9A\u3067\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F (${e})\u3002\u30B0\u30EB\u30FC\u30D7\u3092\u7121\u52B9\u5316\u3057\u307E\u3057\u305F\u3002\u4E0A\u6D41\u304C\u5FA9\u65E7\u3059\u308C\u3070\u6B21\u56DE\u306E\u540C\u671F\u3067\u81EA\u52D5\u7684\u306B\u518D\u6709\u52B9\u5316\u3055\u308C\u307E\u3059\u3002`,planRefused:e=>{if(e.reason==="invalid-upstream-data")return`\u540C\u671F\u3092\u4E2D\u65AD\u3057\u307E\u3057\u305F: \u4E0A\u6D41\u306E\u30E1\u30F3\u30D0\u30FC\u4E00\u89A7\u306B\u6271\u3048\u306A\u3044\u30A2\u30C9\u30EC\u30B9\u304C ${Ge(e.offendingEmails)} \u542B\u307E\u308C\u3066\u3044\u307E\u3059\u3002\u5BFE\u8C61\u306F\u30B5\u30FC\u30D0\u30FC\u306E\u30ED\u30B0\u306B\u51FA\u3066\u3044\u307E\u3059\u3002`;let t=e.axis==="membership"?"\u6A29\u9650\u3092\u5931\u3046\u4EBA\u6570":"\u30B5\u30A4\u30F3\u30A4\u30F3\u3067\u304D\u306A\u304F\u306A\u308B\u4EBA\u6570",c=e.axis==="membership"?e.withheldProjectionRemoves.map(String):e.withheldLedgerDeletes;return[`\u540C\u671F\u3092\u4E2D\u65AD\u3057\u307E\u3057\u305F: ${t}\u304C ${e.population} \u4EBA\u4E2D ${e.removalCount} \u4EBA\u306B\u306A\u308A\u307E\u3059`,`(\u4E0A\u9650: ${e.limits.maxRemovalCount} \u4EF6 / \u6BD4\u7387 ${e.limits.maxRemovalRatio})\u3002`,`\u5DEE\u3057\u6B62\u3081\u305F\u5BFE\u8C61: ${Ge(c)} (\u4E00\u89A7\u306F\u30B5\u30FC\u30D0\u30FC\u306E\u30ED\u30B0\u306B\u51FA\u3066\u3044\u307E\u3059)\u3002`,"\u4E0A\u6D41\u5074\u306E\u8A2D\u5B9A\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u610F\u56F3\u3057\u305F\u5909\u66F4\u3067\u3042\u308C\u3070\u3001\u3053\u306E\u8EF8\u306E\u4E0A\u9650\u3060\u3051\u3092\u4E00\u6642\u7684\u306B\u5F15\u304D\u4E0A\u3052\u3066\u518D\u5B9F\u884C\u3057\u307E\u3059\u3002"].join(" ")},removalWritesFailed:e=>`${e.length} \u4EF6\u306E\u6240\u5C5E\u89E3\u9664\u304C\u66F8\u304D\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F (${[...new Set(e.map(t=>Ie[t.cause]))].join(" / ")})\u3002\u6B21\u56DE\u306E\u5B9F\u884C\u3067\u518D\u8A66\u884C\u3057\u307E\u3059\u3002`,removalsRefused:e=>`${e.length} \u4EF6\u306E\u6240\u5C5E\u89E3\u9664\u304C\u6700\u5F8C\u306E\u7BA1\u7406\u8005\u30AC\u30FC\u30C9\u306B\u3088\u308A\u62D2\u5426\u3055\u308C\u307E\u3057\u305F (\u30E6\u30FC\u30B6\u30FC ${e.map(t=>t.userId).join(", ")})\u3002\u6B8B\u308A\u306E\u5909\u66F4\u306F\u9069\u7528\u6E08\u307F\u3067\u3059\u3002`,foreignProvider:(e,t)=>`\u3053\u306E\u30EA\u30F3\u30AF\u306E\u4F9B\u7D66\u5143\u306F ${e} \u3067\u3059\u304C\u3001\u5B9F\u884C\u4E2D\u306E\u540C\u671F\u306F ${t} \u7528\u3067\u3059\u3002\u5BFE\u5FDC\u3059\u308B\u540C\u671F\u30B8\u30E7\u30D6\u304B\u3089\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002`};var le=["aborted","missing","vanished","upstream-failed","local-failed","foreign-provider","not-converged"],Se=[...le,"already-vanished"];function ir(e){let t=new Map;for(let u of e)u.resolvedUserId===null&&t.set((0,ce.normalizeAuthEmail)(u.email),u.unresolvedReason);let c=[...t.values()],d=ce.AUTH_DIRECTORY_UNRESOLVED_REASONS.map(u=>[u,c.filter(a=>a===u).length]).filter(([,u])=>u>0).map(([u,a])=>`${u}: ${a}`).join(", ");return{total:t.size,breakdown:d}}function ar(e){let{directory:t,repository:c,policy:d,now:u,logger:a}=e,y=e.messages??ve;if((0,pe.assertBlastRadiusPolicy)(d),!Number.isInteger(e.vanishedThreshold)||e.vanishedThreshold<1)throw new TypeError(`[@aiquants/auth-directory-drizzle] vanishedThreshold must be a positive integer, received ${String(e.vanishedThreshold)}`);if(typeof e.provider!="string"||e.provider.trim()==="")throw new TypeError("[@aiquants/auth-directory-drizzle] provider must be a non-empty string so links of other providers can be told apart");async function D(n,i){try{await c.recordFailure(n,i)}catch(r){a.error(`[auth-directory] could not record the failure for ${n.externalKey}:`,r)}}async function I(n){try{let i=await t.getGroup(n.externalId);return i===null?{kind:"missing"}:{kind:"ok",group:i,members:await t.listGroupMembers(i,n.membershipMode)}}catch(i){return{kind:"failed",error:i instanceof Error?i:new Error(String(i))}}}async function f(n){if(n.vanishedObservations>=e.vanishedThreshold&&!n.isGroupActive)return a.warn(`[auth-directory] ${n.externalKey} still missing upstream (already deactivated after ${n.vanishedObservations} absences)`),{kind:"already-vanished",link:n};let i=await c.recordVanishedObservation(n,y.groupMissing(n.externalKey,n.vanishedObservations+1,e.vanishedThreshold));return i<e.vanishedThreshold?(a.warn(`[auth-directory] ${n.externalKey} missing upstream (${i}/${e.vanishedThreshold})`),{kind:"missing",link:n,observations:i}):(await c.markGroupInactive(n),await D(n,y.groupVanished(n.externalKey,i)),a.warn(`[auth-directory] ${n.externalKey} deactivated after ${i} consecutive absences`),{kind:"vanished",link:n})}async function s(n){if(n.provider!==e.provider)return await D(n,y.foreignProvider(n.provider,e.provider)),a.warn(`[auth-directory] skipping ${n.externalKey}: belongs to provider ${n.provider}`),{kind:"foreign-provider",link:n};let i=await I(n);if(i.kind==="missing")return f(n);if(i.kind==="failed")return await D(n,y.upstreamUnreadable(Z(i.error))),a.error(`[auth-directory] upstream read failed for ${n.externalKey}:`,i.error),{kind:"upstream-failed",link:n,error:i.error};try{await c.resolveExternalIdentity(n,i.group);let[r,b]=await Promise.all([c.loadGroupSnapshot(n),c.resolveMembers(i.members)]);if(b.length!==i.members.length){let A=y.resolverIncomplete(i.members.length,b.length);return await D(n,A),a.error(`[auth-directory] ${A}`),{kind:"local-failed",link:n,error:new Error(A)}}let l=(0,pe.reconcile)(r,b,d);if(l.aborted){await D(n,y.planRefused(l.aborted));let A=l.aborted.reason==="invalid-upstream-data"?l.aborted.offendingEmails:l.aborted.axis==="membership"?l.aborted.withheldProjectionRemoves.map(String):l.aborted.withheldLedgerDeletes;return a.warn(`[auth-directory] plan refused for ${n.externalKey}: ${l.aborted.reason}; withheld: ${A.join(", ")}`),{kind:"aborted",link:n,plan:l}}let S=l.ledgerUpserts.length>0||l.ledgerDeletes.length>0||l.projectionAdds.length>0||l.projectionRemoves.length>0,w=u(),x={refusals:[],failures:[]};S&&(x=await c.applyPlan(n,l,w)),await c.recordSuccess(n,i.group,w),x.refusals.length>0&&(await D(n,y.removalsRefused(x.refusals)),a.warn(`[auth-directory] ${x.refusals.length} removal(s) refused for ${n.externalKey}: users ${x.refusals.map(A=>A.userId).join(", ")}`)),x.failures.length>0&&(await D(n,y.removalWritesFailed(x.failures)),a.warn(`[auth-directory] ${x.failures.length} removal(s) could not be written for ${n.externalKey}: users ${x.failures.map(A=>A.userId).join(", ")}`));let k=ir(b);return k.total>0&&a.info(`[auth-directory] ${n.externalKey}: ${k.total} upstream member(s) are not linked to a local user (${k.breakdown})`),x.refusals.length>0||x.failures.length>0?{kind:"not-converged",link:n,plan:l,refusals:x.refusals,failures:x.failures}:S?{kind:"applied",link:n,plan:l}:{kind:"unchanged",link:n}}catch(r){let b=r instanceof Error?r:new Error(String(r));return await D(n,y.localWriteFailed("localWriteError")),a.error(`[auth-directory] local write failed for ${n.externalKey}:`,b),{kind:"local-failed",link:n,error:b}}}return{syncOne:s,async run(n){let i=await c.listSyncableLinks(n),r=[];for(let S of i)try{r.push(await s(S))}catch(w){let x=w instanceof Error?w:new Error(String(w));a.error(`[auth-directory] ${S.externalKey} ended abnormally:`,x),r.push({kind:"local-failed",link:S,error:x})}let b=r.filter(S=>Se.includes(S.kind)).length,l=r.filter(S=>le.includes(S.kind)).length;return a.info(`[auth-directory] run finished: ${r.length} link(s), ${b} unhealthy (${l} new)`),{outcomes:r,unhealthyCount:b,newlyUnhealthyCount:l}}}}var Ce=require("@aiquants/authz-core"),Pe=require("@aiquants/mssql"),P=require("drizzle-orm"),_e="The job that claimed this request ended without recording an outcome; whether the synchronization ran is unknown.",De=2e3;function sr(e,t,c){let d=e,u=c.ensureConnection??(async()=>{}),a=t.TDDirectorySyncRequest,y=(0,Ce.assertTenantId)(c.tenantId,"createSyncRequestQueue(tenantId)"),D=c.maxErrorLength??De;if(!Number.isInteger(D)||D<1||D>De)throw new TypeError(`[@aiquants/auth-directory-drizzle] maxErrorLength must be an integer within 1..${De}, received ${String(c.maxErrorLength)}`);let I={async claim(){await u();let f=await d.select({id:a.id}).from(a).where((0,P.and)((0,P.eq)(a.tenantId,y),(0,P.isNull)(a.pickedAt))).orderBy((0,P.asc)(a.id));for(let s of f){let i=(await d.update(a).set({pickedAt:c.now()}).output().where((0,P.and)((0,P.eq)(a.tenantId,y),(0,P.eq)(a.id,Number(s.id)),(0,P.isNull)(a.pickedAt))))?.[0];if(!i)continue;if(i.requestedBy===void 0||i.requestedBy===null)throw new TypeError(`[@aiquants/auth-directory-drizzle] the driver returned a row without requestedBy for request ${String(i.id)}`);let r=i.groupId??null;return{id:Number(i.id),tenantId:y,groupId:r===null?null:Number(r),requestedBy:String(i.requestedBy)}}return null},async finish(f,s){await u();let n=s.error?s.error.slice(0,D):null;return!!(await d.update(a).set({finishedAt:c.now(),result:s.result,error:n}).output().where((0,P.and)((0,P.eq)(a.tenantId,y),(0,P.eq)(a.id,f),(0,P.isNull)(a.finishedAt))))?.length},async listStranded(f){await u();let s=await d.select({id:a.id,pickedAt:a.pickedAt,requestedAt:a.requestedAt}).from(a).where((0,P.and)((0,P.eq)(a.tenantId,y),(0,P.isNull)(a.finishedAt))).orderBy((0,P.asc)(a.id)),n=i=>{let r=(0,Pe.toInstant)(i);return Number.isNaN(r.getTime())||r<f};return{stranded:s.filter(i=>i.pickedAt!==null&&n(i.pickedAt)).map(i=>Number(i.id)),unclaimed:s.filter(i=>i.pickedAt===null&&n(i.requestedAt)).map(i=>Number(i.id))}},async abandonStranded(f){let{stranded:s}=await I.listStranded(f),n=[];for(let i of s)try{await I.finish(i,{result:"failed",error:_e})&&n.push(i)}catch(r){c.onCloseFailed?.(i,r)}return n}};return I}var X=require("@aiquants/auth-core"),ee=require("@aiquants/authz-core"),V=require("@aiquants/mssql"),Ke=require("@aiquants/mssql/drizzle"),T=require("drizzle-orm");var ur=7,dr=4,Le=1e3;function lr(e){if(!(e instanceof Error))return!1;let t=e;return t.number===void 0&&t.code===void 0&&e.name==="Error"}function cr(e,t,c,d){let u=e,a=d.ensureConnection??(async()=>{}),y=U(u,d),D=d.now??(()=>new Date),{TDGroupExternalLink:I,TDGroupMemberEmail:f}=t,{TMGroup:s,TDGroupMember:n}=c,i=d.actor;return{async listSyncableLinks(r){await a();let b=(0,ee.assertTenantId)(r.tenantId,"listSyncableLinks(tenantId)"),l=[(0,T.eq)(I.isSyncEnabled,!0),(0,T.eq)(I.tenantId,b)];return r.groupId!==void 0&&l.push((0,T.eq)(I.groupId,r.groupId)),(await u.select({linkId:I.id,groupId:I.groupId,linkTenantId:I.tenantId,groupTenantId:s.tenantId,groupKey:s.groupKey,isGroupActive:s.isActive,provider:I.provider,externalId:I.externalId,externalKey:I.externalKey,membershipMode:I.membershipMode,vanishedObservations:I.vanishedObservations}).from(I).innerJoin(s,(0,T.and)((0,T.eq)(s.id,I.groupId),(0,T.eq)(s.tenantId,b))).where((0,T.and)(...l))).map(w=>((0,ee.assertTenantMatch)(String(w.groupTenantId),String(w.linkTenantId),"listSyncableLinks(link.tenantId vs TMGroup.tenant_id)"),{linkId:Number(w.linkId),groupId:Number(w.groupId),groupKey:String(w.groupKey),tenantId:String(w.groupTenantId),provider:String(w.provider),externalId:String(w.externalId),externalKey:String(w.externalKey),membershipMode:W(String(w.membershipMode)),isGroupActive:!!w.isGroupActive,vanishedObservations:Number(w.vanishedObservations??0)}))},async loadGroupSnapshot(r){await a();let[b,l]=await Promise.all([u.select({email:f.email,externalUserId:f.externalUserId,resolvedUserId:f.resolvedUserId}).from(f).where((0,T.eq)(f.groupId,r.groupId)),u.select({userId:n.userId}).from(n).where((0,T.eq)(n.groupId,r.groupId))]);return{ledger:b.map(S=>({email:String(S.email),...S.externalUserId?{externalUserId:String(S.externalUserId)}:{},resolvedUserId:S.resolvedUserId===null?null:Number(S.resolvedUserId)})),projectedUserIds:l.map(S=>Number(S.userId))}},resolveMembers(r){return d.memberResolver.resolve(r)},async applyPlan(r,b,l){(0,ee.assertTenantId)(r.tenantId,"applyPlan(link.tenantId)"),await a(),(b.ledgerUpserts.length>0||b.projectionAdds.length>0)&&await y(async k=>{let A=b.ledgerUpserts.map(h=>({...h,email:(0,X.normalizeAuthEmail)(h.email)})),o=new Map;for(let h of(0,V.chunk)(A.map(v=>v.email))){let v=await k.select({id:f.id,email:f.email}).from(f).where((0,T.and)((0,T.eq)(f.groupId,r.groupId),(0,T.inArray)(f.email,h)));for(let R of v)o.set(String(R.email),Number(R.id))}let g=A.filter(h=>!o.has(h.email));for(let h of(0,V.chunkByParameters)(g,ur))await k.insert(f).output().values(h.map(v=>({groupId:r.groupId,email:v.email,externalUserId:v.externalUserId??null,resolvedUserId:v.resolvedUserId,observedAt:l,createdAt:l,createdBy:i})));for(let h of A){let v=o.get(h.email);v!==void 0&&await k.update(f).set({externalUserId:h.externalUserId??null,resolvedUserId:h.resolvedUserId,observedAt:l,updatedAt:l,updatedBy:i}).output().where((0,T.eq)(f.id,v))}for(let h of(0,V.chunkByParameters)(b.projectionAdds,dr)){let v=new Set((await k.select({userId:n.userId}).from(n).where((0,T.and)((0,T.eq)(n.groupId,r.groupId),(0,T.inArray)(n.userId,h)))).map(M=>Number(M.userId))),R=h.filter(M=>!v.has(M));R.length>0&&await k.insert(n).output().values(R.map(M=>({groupId:r.groupId,userId:M,createdAt:l,createdBy:i})))}});let S=[],w=[];for(let k of b.projectionRemoves)try{await d.lockoutGuard.guardedWrite(r.tenantId,{removeGroupMembership:{groupId:r.groupId,userId:k}},"remove-group-member",A=>A.delete(n).where((0,T.and)((0,T.eq)(n.groupId,r.groupId),(0,T.eq)(n.userId,k))))}catch(A){lr(A)?S.push({userId:k,applied:!1,refusal:A.message}):w.push({userId:k,cause:Z(A)})}let x=S.map(k=>k.userId);return b.ledgerDeletes.length>0&&await y(async k=>{for(let A of(0,V.chunk)(b.ledgerDeletes)){let o=x.length>0?(0,T.or)((0,T.isNull)(f.resolvedUserId),(0,Ke.notInJsonList)(f.resolvedUserId,x)):void 0;await k.delete(f).where((0,T.and)((0,T.eq)(f.groupId,r.groupId),(0,T.inArray)(f.email,A),...o?[o]:[]))}}),{refusals:S,failures:w}},async resolveExternalIdentity(r,b){await a(),!(r.externalId===b.externalId&&r.externalKey===(0,X.normalizeAuthEmail)(b.externalKey))&&await u.update(I).set({externalId:b.externalId,externalKey:(0,X.normalizeAuthEmail)(b.externalKey)}).output().where((0,T.and)((0,T.eq)(I.id,r.linkId),(0,T.eq)(I.tenantId,r.tenantId)))},async recordSuccess(r,b,l){await a(),await y(async S=>{await S.update(I).set({syncedAt:l,syncError:null,vanishedObservations:0,externalId:b.externalId,externalKey:(0,X.normalizeAuthEmail)(b.externalKey),updatedAt:l,updatedBy:i}).output().where((0,T.and)((0,T.eq)(I.id,r.linkId),(0,T.eq)(I.tenantId,r.tenantId))),r.isGroupActive||await S.update(s).set({isActive:!0,updatedAt:l,updatedBy:i}).output().where((0,T.and)((0,T.eq)(s.id,r.groupId),(0,T.eq)(s.tenantId,r.tenantId)))})},async recordFailure(r,b){await a(),await u.update(I).set({syncError:b.slice(0,Le),updatedAt:D(),updatedBy:i}).output().where((0,T.and)((0,T.eq)(I.id,r.linkId),(0,T.eq)(I.tenantId,r.tenantId)))},async recordVanishedObservation(r,b){await a();let l=r.vanishedObservations+1;return await u.update(I).set({vanishedObservations:l,syncError:b.slice(0,Le),updatedAt:D(),updatedBy:i}).output().where((0,T.and)((0,T.eq)(I.id,r.linkId),(0,T.eq)(I.tenantId,r.tenantId))),l},async markGroupInactive(r){await a(),await u.update(s).set({isActive:!1,updatedAt:D(),updatedBy:i}).output().where((0,T.and)((0,T.eq)(s.id,r.groupId),(0,T.eq)(s.tenantId,r.tenantId),(0,T.eq)(s.isActive,!0)))}}}0&&(module.exports={AUTH_DIRECTORY_PROVIDER_COLUMN,AUTH_DIRECTORY_TENANT_SCOPED_TABLES,DirectoryOperatorError,MEMBERSHIP_MODES,NEWLY_UNHEALTHY_SYNC_OUTCOMES,STRANDED_REASON,UNHEALTHY_SYNC_OUTCOMES,assertGroupMayBeLinked,assertMembershipMode,classifySyncFailure,createDirectoryCatalogReader,createDirectoryCatalogWriter,createDirectoryLinkStore,createDirectorySync,createDirectorySyncRepository,createOpenidMemberResolver,createPromotionRepository,createSyncRequestQueue,defaultDirectoryLinkRefusals,defaultExternalGroupGuardMessage,defaultSyncMessages,defineAuthDirectorySchema,findLedgerRowsForIdentity,promoteLedgerForUser,transactionRunner,withExternalGroupGuards});
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{assertTenantId as Ie}from"@aiquants/authz-core";import{chunkByParameters as Oe,toInstant as $e}from"@aiquants/mssql";import{and as V,asc as Ne,eq as N}from"drizzle-orm";import{runWithTransactionRetry as Ke}from"@aiquants/mssql";function $(r,a){return m=>Ke(d=>r.transaction(l=>d(()=>m(l))),a.transactionRetry)}var qe=10,O={tenantId:64,provider:32,externalId:256,externalKey:320,name:256,description:1e3,actor:50},we=2147483647;function Ue(r){if(r===void 0||!Number.isFinite(r))return null;let a=Math.trunc(r);return a<0?null:a>we?we:a}function j(r,a,m){let d=r.trim();if(d==="")throw new TypeError(`[@aiquants/auth-directory-drizzle] ${m} must not be blank`);if(d.length>a)throw new TypeError(`[@aiquants/auth-directory-drizzle] ${m} exceeds its column length (${a}); truncating it would change which rows it owns`);return d}function fr(r,a,m){let d=r,l=m.ensureConnection??(async()=>{}),s=$(d,m),h=a.TDDirectoryCatalog,w=a.TMDirectoryCatalogObservation,b=y=>{let i=new Map;for(let t of y)i.set(t.externalId,t);return[...i.values()]};return{async replace(y,i,t){await l();let n=j(Ie(y,"replace(tenantId)"),O.tenantId,"tenantId"),e=j(i,O.provider,"provider"),p=j(m.actor,O.actor,"actor"),u=m.now(),f=b(t.map(S=>({tenantId:n,provider:e,externalId:S.externalId.slice(0,O.externalId),externalKey:S.externalKey.slice(0,O.externalKey),name:S.name.slice(0,O.name),description:S.description===void 0?null:S.description.slice(0,O.description),memberCount:Ue(S.memberCount),observedAt:u,createdAt:u,createdBy:p})));return await s(async S=>{await S.delete(h).where(V(N(h.tenantId,n),N(h.provider,e)));for(let T of Oe(f,qe))await S.insert(h).values(T);await S.delete(w).where(V(N(w.tenantId,n),N(w.provider,e))),await S.insert(w).values([{tenantId:n,provider:e,observedAt:u,entryCount:f.length,createdAt:u,createdBy:p,updatedAt:u,updatedBy:p}])}),f.length}}}function wr(r,a,m={}){let d=r,l=m.ensureConnection??(async()=>{}),s=a.TDDirectoryCatalog,h=a.TMDirectoryCatalogObservation;return{async read(w,b){await l();let y=j(Ie(w,"read(tenantId)"),O.tenantId,"tenantId"),i=j(b,O.provider,"provider"),t=await d.select({observedAt:h.observedAt,entryCount:h.entryCount,externalId:s.externalId,externalKey:s.externalKey,name:s.name,description:s.description,memberCount:s.memberCount}).from(h).leftJoin(s,V(N(s.tenantId,h.tenantId),N(s.provider,h.provider))).where(V(N(h.tenantId,y),N(h.provider,i))).orderBy(Ne(s.name)),n=t.filter(u=>u.externalId!==null).map(u=>({externalId:String(u.externalId),externalKey:String(u.externalKey),name:String(u.name),...u.description===null?{}:{description:u.description},...u.memberCount===null?{}:{memberCount:Number(u.memberCount)}})),e=t[0];if(e===void 0)return{groups:n,observedAt:null};if(Number(e.entryCount)!==n.length)throw new Error(`[@aiquants/auth-directory-drizzle] catalog is inconsistent for (${y}, ${i}): the observation records ${e.entryCount} entries but ${n.length} rows are present`);let p=$e(e.observedAt);if(!Number.isFinite(p.getTime()))throw new Error(`[@aiquants/auth-directory-drizzle] catalog observation for (${y}, ${i}) has an unreadable observed_at`);return{groups:n,observedAt:p}}}}import{AUTHZ_ACTIONS as Be}from"@aiquants/authz-core";var Sr=(r,a)=>`\u5916\u90E8\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u9023\u643A\u30B0\u30EB\u30FC\u30D7 (${a}) \u306B\u306F\u7BA1\u7406\u30ED\u30FC\u30EB (${r}) \u3092\u6301\u305F\u305B\u3089\u308C\u307E\u305B\u3093\u3002\u4E0A\u6D41\u5074\u306E\u6240\u5C5E\u5909\u66F4\u304C\u305D\u306E\u307E\u307E\u7BA1\u7406\u8005\u306E\u5897\u6E1B\u306B\u306A\u308B\u305F\u3081\u3001\u7BA1\u7406\u8005\u7D4C\u8DEF\u306F\u30ED\u30FC\u30AB\u30EB\u7BA1\u7406\u306E\u30B0\u30EB\u30FC\u30D7\u306B\u9650\u5B9A\u3057\u3066\u3044\u307E\u3059\u3002`;function z(r,a,m,d){return new Set(r.filter(l=>a.has(l.roleId)&&l.appKey===m&&l.resourceKey===d).map(l=>l.action))}function Q(r,a){return new Set(r.filter(m=>m.groupId===a).map(m=>m.roleId))}var W=r=>Be.every(a=>r.has(a)),X=(r,a)=>r.find(m=>m.roleId===a)?.roleKey??`#${a}`;function Z(r,a,m,d){let l=[...a].find(h=>W(z(r,new Set([h]),m,d)));return l!==void 0?X(r,l):[...a].filter(h=>z(r,new Set([h]),m,d).size>0).map(h=>X(r,h)).join(" + ")}function Tr(r,a){return ve(r,a)}function ve(r,a){let m=r;return{...r,withTransaction:(d,l)=>m.withTransaction((s,h)=>d(ve(s,a),h),l),async assignGroupRole(d,l,s,h){await m.withTransaction(async(w,b)=>{let y=await w.listGrants(d),i=Q(await w.listGroupRoles(d),l);if(i.add(s),W(z(y,i,a.adminAppKey,a.adminResourceKey))){let t=await a.linkLookup.externalGroupKey(d,l,b);if(t!==null)throw new Error(a.message(Z(y,i,a.adminAppKey,a.adminResourceKey),t))}await w.assignGroupRole(d,l,s,h)})},async updateGrant(d,l,s,h){let w=s.action;if(w===void 0)return m.updateGrant(d,l,s,h);await m.withTransaction(async(b,y)=>{let i=await b.listGrants(d),t=i.find(p=>p.id===l);if(t===void 0)return b.updateGrant(d,l,s,h);let e=(await b.listResources(d)).find(p=>p.id===t.resourceId);if(e?.appKey===a.adminAppKey&&e?.resourceKey===a.adminResourceKey){let p=await b.listGroupRoles(d);for(let u of new Set(p.filter(f=>f.roleId===t.roleId).map(f=>f.groupId))){let f=Q(p,u),S=z(i.filter(D=>D.id!==l),f,a.adminAppKey,a.adminResourceKey);if(f.has(t.roleId)&&S.add(w),!W(S))continue;let T=await a.linkLookup.externalGroupKey(d,u,y);if(T!==null)throw new Error(a.message(Z(i,f,a.adminAppKey,a.adminResourceKey)||X(i,t.roleId),T))}}await b.updateGrant(d,l,s,h)})},async createGrant(d,l,s){await m.withTransaction(async(h,w)=>{let y=(await h.listResources(d)).find(i=>i.id===l.resourceId);if(y?.appKey===a.adminAppKey&&y?.resourceKey===a.adminResourceKey){let i=await h.listGrants(d),t=await h.listGroupRoles(d);for(let n of new Set(t.filter(e=>e.roleId===l.roleId).map(e=>e.groupId))){let e=z(i,Q(t,n),a.adminAppKey,a.adminResourceKey);if(e.add(l.action),!W(e))continue;let p=await a.linkLookup.externalGroupKey(d,n,w);if(p!==null)throw new Error(a.message(Z(i,Q(t,n),a.adminAppKey,a.adminResourceKey)||X(i,l.roleId),p))}}await h.createGrant(d,l,s)})}}}async function Dr(r,a,m,d){let l=(await r.listGroupRoles(a)).filter(b=>b.groupId===m);if(l.length===0)return;let s=await r.listGrants(a),h=new Set(l.map(b=>b.roleId));if(!W(z(s,h,d.adminAppKey,d.adminResourceKey)))return;let w=l[0];if(w)throw new Error(d.message(Z(s,h,d.adminAppKey,d.adminResourceKey),w.groupKey))}import{and as ze,eq as H}from"drizzle-orm";async function ee(r,a,m){let{ledger:d,group:l}=a,{tenantId:s,externalUserId:h,email:w}=m;if(h===null&&w==="")return[];let b={ledgerId:d.id,groupId:d.groupId,tenantId:l.tenantId,externalUserId:d.externalUserId},y=async p=>(await r.select(b).from(d).innerJoin(l,ze(H(l.id,d.groupId),H(l.tenantId,s),H(l.isActive,!0))).where(p)).map(f=>({ledgerId:Number(f.ledgerId),groupId:Number(f.groupId),tenantId:String(f.tenantId),externalUserId:f.externalUserId===null?null:String(f.externalUserId)})),i=p=>{let u=new Map;for(let f of p)u.set(f.ledgerId,{ledgerId:f.ledgerId,groupId:f.groupId,tenantId:f.tenantId});return[...u.values()].sort((f,S)=>f.ledgerId-S.ledgerId)},t=h===null?[]:await y(H(d.externalUserId,h));if(w==="")return i(t);let n=await y(H(d.email,w)),e=p=>p.externalUserId!==null&&p.externalUserId!==h;return i([...t,...n.filter(p=>!e(p))])}import{normalizeAuthEmail as se}from"@aiquants/auth-core";import{assertTenantMembershipPolicy as Fe}from"@aiquants/auth-directory-core";import{assertTenantId as P,assertTenantMatch as je}from"@aiquants/authz-core";import{toInstant as Te}from"@aiquants/mssql";import{and as E,asc as De,count as ue,eq as v,isNull as re}from"drizzle-orm";var Se=["direct","transitive"];function J(r){if(!Se.includes(r))throw new TypeError(`[@aiquants/auth-directory-drizzle] membershipMode must be one of ${Se.join(" | ")}, received ${JSON.stringify(r)}`);return r}function We(r){let a=typeof r=="object"&&r!==null&&"number"in r?r.number:void 0;return a===2601||a===2627}function Or(r,a,m,d){let l=r,s=d.ensureConnection??(async()=>{}),h=$(l,d),w=d.policyCacheTtlMs??0;if(!Number.isFinite(w)||w<0)throw new TypeError(`[@aiquants/auth-directory-drizzle] policyCacheTtlMs must be a non-negative number, received ${String(d.policyCacheTtlMs)}`);let b=new Map,y=o=>b.delete(o),{TDGroupExternalLink:i,TDGroupMemberEmail:t,TDTenantMembershipGroup:n,TDAllowlistGroup:e,TDDirectorySyncRequest:p}=a,{TMGroup:u,TDGroupMember:f}=m;async function S(o,c,g){if((await o.select({groupId:n.groupId}).from(n).where(E(v(n.tenantId,c),v(n.groupId,g),v(n.isBreakGlass,!0)))).length>0)throw new Error(`Group ${g} is a break-glass proof group; a break-glass proof path must be one the synchronization can never touch.`)}async function T(o,c,g){let[I]=await o.select({id:u.id}).from(u).where(E(v(u.id,g),v(u.tenantId,c)));if(!I)throw new Error(`Group not found in tenant: ${g}`)}function D(o,c){return{linkId:Number(o.linkId),groupId:Number(o.groupId),tenantId:String(o.tenantId),provider:String(o.provider),externalId:String(o.externalId),externalKey:String(o.externalKey),membershipMode:J(String(o.membershipMode)),isSyncEnabled:!!o.isSyncEnabled,...o.syncedAt?{syncedAt:Te(o.syncedAt).toISOString()}:{},...o.syncError?{syncError:String(o.syncError)}:{},unresolvedCount:c}}async function G(o,c){let g=await o.select({groupId:t.groupId,n:ue()}).from(t).innerJoin(u,E(v(u.id,t.groupId),v(u.tenantId,c))).where(re(t.resolvedUserId)).groupBy(t.groupId);return new Map(g.map(I=>[Number(I.groupId),Number(I.n)]))}return{async listLinks(o){P(o,"listLinks(tenantId)"),await s();let c=await l.select({linkId:i.id,groupId:i.groupId,tenantId:i.tenantId,provider:i.provider,externalId:i.externalId,externalKey:i.externalKey,membershipMode:i.membershipMode,isSyncEnabled:i.isSyncEnabled,syncedAt:i.syncedAt,syncError:i.syncError}).from(i).where(v(i.tenantId,o)).orderBy(De(i.groupId)),g=await G(l,o);return c.map(I=>(je(o,String(I.tenantId),"listLinks(row.tenantId)"),D(I,g.get(Number(I.groupId))??0)))},async externalGroupKey(o,c,g){P(o,"externalGroupKey(tenantId)"),await s();let[I]=await(g??l).select({externalKey:i.externalKey}).from(i).where(E(v(i.groupId,c),v(i.tenantId,o)));return I?String(I.externalKey):null},async linkGroup(o,c,g,I){P(o,"linkGroup(tenantId)");let A=J(g.membershipMode),k=g.externalId.trim(),C=se(g.externalKey);if(k===""||C==="")throw new TypeError("[@aiquants/auth-directory-drizzle] externalId and externalKey must be non-empty");await s(),await d.linkable.assertLinkableAndWrite(o,c,async Y=>{let M=Y;await T(M,o,c),await S(M,o,c),await M.insert(i).output().values({groupId:c,tenantId:o,provider:g.provider,externalId:k,externalKey:C,membershipMode:A,isSyncEnabled:!0,vanishedObservations:0,createdAt:new Date,createdBy:I})}),y(o)},async unlinkGroup(o,c){P(o,"unlinkGroup(tenantId)"),await s(),await h(async g=>{await T(g,o,c),await g.delete(t).where(v(t.groupId,c)),await g.delete(e).where(E(v(e.groupId,c),v(e.tenantId,o))),await g.delete(i).where(E(v(i.groupId,c),v(i.tenantId,o)))}),y(o)},async setSyncEnabled(o,c,g,I){P(o,"setSyncEnabled(tenantId)"),await s(),await l.update(i).set({isSyncEnabled:g,updatedAt:new Date,updatedBy:I}).output().where(E(v(i.groupId,c),v(i.tenantId,o)))},async requestSync(o,c,g){return P(o,"requestSync(tenantId)"),await s(),l.transaction(async I=>{if(c!==null&&await T(I,o,c),(await I.select({id:p.id}).from(p).where(E(v(p.tenantId,o),c===null?re(p.groupId):v(p.groupId,c),re(p.pickedAt)))).length>0)return"already-pending";try{await I.insert(p).output().values({tenantId:o,groupId:c,requestedBy:g,requestedAt:new Date,createdAt:new Date,createdBy:g})}catch(k){if(!We(k))throw k;return"already-pending"}return"queued"})},async getStatus(o){P(o,"getStatus(tenantId)"),await s();let c=await l.select({groupId:i.groupId,isSyncEnabled:i.isSyncEnabled,syncedAt:i.syncedAt,syncError:i.syncError}).from(i).where(v(i.tenantId,o)),[g]=await l.select({n:ue()}).from(p).where(E(v(p.tenantId,o),re(p.finishedAt))),I=c.filter(M=>!!M.isSyncEnabled),A=I.map(M=>M.syncedAt?Te(M.syncedAt).getTime():null),k=A.length>0&&A.every(M=>M!==null)?Math.min(...A):null,Y=c.length>0&&I.length===0||I.length>0&&(k===null||Date.now()-k>d.stalenessThresholdSec*1e3);return{isEnabled:d.isEnabled,provider:d.provider,linkedGroupCount:c.length,...k!==null?{oldestSyncedAt:new Date(k).toISOString()}:{},isStale:Y,pausedGroupCount:c.length-I.length,stalenessThresholdSec:d.stalenessThresholdSec,erroredGroupIds:c.filter(M=>!!M.syncError).map(M=>Number(M.groupId)).sort((M,Le)=>M-Le),pendingRequestCount:Number(g?.n??0)}},async loadTenantMembershipPolicy(o){P(o,"loadTenantMembershipPolicy(tenantId)");let c=w>0?b.get(o):void 0;if(c&&Date.now()-c.loadedAt<w)return c.policy;await s();let g=await l.select({groupId:n.groupId,isBreakGlass:n.isBreakGlass,externalLinkId:i.id}).from(n).innerJoin(u,E(v(u.id,n.groupId),v(u.tenantId,o))).leftJoin(i,E(v(i.groupId,n.groupId),v(i.tenantId,o))).where(v(n.tenantId,o));if(g.length===0){let C={kind:"no-proof-required"};return w>0&&b.set(o,{policy:C,loadedAt:Date.now()}),C}let I=g.map(C=>Number(C.groupId)),A=g.filter(C=>!!C.isBreakGlass);for(let C of A)if(C.externalLinkId!==null)throw new Error(`Break-glass group ${Number(C.groupId)} is externally sourced; a break-glass proof path must be one the synchronization can never touch.`);let k={kind:"require-proof",proofGroupIds:I,breakGlassGroupIds:A.map(C=>Number(C.groupId))};return Fe(k,`tenant ${o}`),w>0&&b.set(o,{policy:k,loadedAt:Date.now()}),k},async setTenantMembershipGroup(o,c,g,I){P(o,"setTenantMembershipGroup(tenantId)"),await s(),await h(async A=>{if(await T(A,o,c),g){let[C]=await A.select({id:i.id}).from(i).where(E(v(i.tenantId,o),v(i.groupId,c)));if(C)throw new Error(`Group ${c} is externally sourced; a break-glass proof path must be one the synchronization can never touch.`)}let k=await A.select({groupId:n.groupId}).from(n).where(E(v(n.tenantId,o),v(n.groupId,c)));if(!g&&!(await A.select({groupId:n.groupId,isBreakGlass:n.isBreakGlass}).from(n).where(v(n.tenantId,o))).filter(M=>Number(M.groupId)!==c).some(M=>!!M.isBreakGlass))throw new Error(`Declaring group ${c} as ordinary proof would leave tenant ${o} with a membership proof but no break-glass path; declare a locally-managed break-glass group first.`);if(k.length>0){await A.update(n).set({isBreakGlass:g,updatedAt:new Date,updatedBy:I}).output().where(E(v(n.tenantId,o),v(n.groupId,c)));return}await A.insert(n).output().values({tenantId:o,groupId:c,isBreakGlass:g,createdAt:new Date,createdBy:I})}),y(o)},async removeTenantMembershipGroup(o,c){P(o,"removeTenantMembershipGroup(tenantId)"),await s(),await h(async g=>{let A=(await g.select({groupId:n.groupId,isBreakGlass:n.isBreakGlass}).from(n).where(v(n.tenantId,o))).filter(k=>Number(k.groupId)!==c);if(A.length>0&&!A.some(k=>!!k.isBreakGlass))throw new Error(`Removing group ${c} would leave tenant ${o} with a membership proof but no break-glass path.`);await g.delete(n).where(E(v(n.tenantId,o),v(n.groupId,c)))}),y(o)},async setGroupActive(o,c,g,I){P(o,"setGroupActive(tenantId)"),await s(),await l.update(u).set({isActive:g,updatedAt:new Date,updatedBy:I}).output().where(E(v(u.id,c),v(u.tenantId,o)))},async listLedgerGroupIds(o,c){if(P(o,"listLedgerGroupIds(tenantId)"),!d.isEnabled)return[];await s();let g=await ee(l,{ledger:t,group:u},{tenantId:o,externalUserId:c.externalUserId?.trim()||null,email:se(c.email)});return[...new Set(g.map(I=>I.groupId))].sort((I,A)=>I-A)},async isEmailAllowedByGroup(o){if(!d.isEnabled)return!1;let c=se(o);if(c==="")return!1;await s();let[g]=await l.select({id:t.id}).from(t).innerJoin(e,v(e.groupId,t.groupId)).innerJoin(u,E(v(u.id,t.groupId),v(u.isActive,!0))).where(v(t.email,c));return!!g},async listAllowlistGroups(o){return P(o,"listAllowlistGroups(tenantId)"),await s(),(await l.select({id:e.id,groupId:e.groupId,groupKey:u.groupKey,groupName:u.name,description:e.description}).from(e).innerJoin(u,E(v(u.id,e.groupId),v(u.tenantId,o))).where(v(e.tenantId,o)).orderBy(De(u.name))).map(g=>({id:Number(g.id),type:"group",groupId:Number(g.groupId),groupKey:String(g.groupKey),groupName:String(g.groupName),...g.description?{description:String(g.description)}:{},owner:{kind:"tenant",tenantId:o},readOnly:!1}))},async addAllowlistGroup(o,c,g,I){P(o,"addAllowlistGroup(tenantId)"),await s(),await h(async A=>{await T(A,o,c);let[k]=await A.select({id:i.id}).from(i).where(E(v(i.groupId,c),v(i.tenantId,o)));if(!k)throw new Error(`Group ${c} is locally managed; only externally-sourced groups can gate sign-in, because the decision reads the upstream member ledger.`);await A.insert(e).output().values({tenantId:o,groupId:c,description:g??null,createdAt:new Date,createdBy:I})})},async removeAllowlistGroup(o,c){P(o,"removeAllowlistGroup(tenantId)"),await s(),await l.delete(e).where(E(v(e.id,c),v(e.tenantId,o)))},async countProjectedMembers(o,c){P(o,"countProjectedMembers(tenantId)"),await s();let[g]=await l.select({n:ue()}).from(f).innerJoin(u,E(v(u.id,f.groupId),v(u.tenantId,o))).where(v(f.groupId,c));return Number(g?.n??0)}}}import{normalizeAuthEmail as He}from"@aiquants/auth-core";import{assertTenantId as Je,assertTenantMatch as Ye}from"@aiquants/authz-core";async function Ur(r,a){let m=Je(a.tenantId,"promoteLedgerForUser(tenantId)");if(!Number.isInteger(a.userId)||a.userId<=0)throw new TypeError(`[@aiquants/auth-directory-drizzle] userId must be a positive integer, received ${String(a.userId)}`);let d=He(a.email),l=a.externalUserId?.trim()||null;if(l===null&&d==="")return{addedGroupIds:[],matchedLedgerIds:[]};let s=await r.findLedgerMatches({tenantId:m,externalUserId:l,email:d});if(s.length===0)return{addedGroupIds:[],matchedLedgerIds:[]};for(let y of s)Ye(m,y.tenantId,"promoteLedgerForUser(findLedgerMatches row)");let h=[...new Set(s.map(y=>y.ledgerId))].sort((y,i)=>y-i),w=[...new Set(s.map(y=>y.groupId))].sort((y,i)=>y-i);return{addedGroupIds:[...await r.attachUserToGroups({tenantId:m,userId:a.userId,ledgerIds:h,groupIds:w})].sort((y,i)=>y-i),matchedLedgerIds:h}}import{assertTenantId as Ae}from"@aiquants/authz-core";import{chunk as de,chunkByParameters as Ve}from"@aiquants/mssql";import{inJsonList as Qe}from"@aiquants/mssql/drizzle";import{and as le,eq as ce,inArray as pe}from"drizzle-orm";var Ze=4;function Yr(r,a,m,d){let l=r,s=d.ensureConnection??(async()=>{}),h=$(l,d),w=d.now??(()=>new Date),b=a.TDGroupMemberEmail,{TMGroup:y,TDGroupMember:i}=m;return{async findLedgerMatches(t){return Ae(t.tenantId,"findLedgerMatches(tenantId)"),await s(),ee(l,{ledger:b,group:y},t)},async attachUserToGroups({tenantId:t,userId:n,ledgerIds:e,groupIds:p}){return Ae(t,"attachUserToGroups(tenantId)"),await s(),e.length===0?[]:h(async u=>{let f=new Set;for(let G of de(p)){let o=await u.select({id:y.id}).from(y).where(le(pe(y.id,G),ce(y.tenantId,t),ce(y.isActive,!0)));for(let c of o)f.add(Number(c.id))}let S=p.filter(G=>f.has(G));if(S.length===0)return[];let T=new Set;for(let G of de(S)){let o=await u.select({groupId:i.groupId}).from(i).where(le(ce(i.userId,n),pe(i.groupId,G)));for(let c of o)T.add(Number(c.groupId))}let D=S.filter(G=>!T.has(G));for(let G of de(e))await u.update(b).set({resolvedUserId:n}).output().where(le(pe(b.id,G),Qe(b.groupId,S)));if(D.length>0){let G=w();for(let o of Ve(D,Ze))await u.insert(i).output().values(o.map(c=>({groupId:c,userId:n,createdAt:G,createdBy:d.actor})))}return D})}}}import{AUTHZ_TENANT_COLUMN as U}from"@aiquants/authz-drizzle";import{bigint as _,bit as xe,datetime2 as K,foreignKey as te,int as me,mssqlSchema as Xe,nvarchar as x,primaryKey as q,uniqueIndex as B}from"drizzle-orm/mssql-core";var ye="provider",Xr=["TDGroupExternalLink","TDTenantMembershipGroup","TDAllowlistGroup","TDDirectorySyncRequest","TDDirectoryCatalog","TMDirectoryCatalogObservation","TMAllowlist"];function et(r,a){let m=Xe(r),d=()=>({createdAt:K("created_at"),createdBy:x("created_by",{length:50}),updatedAt:K("updated_at"),updatedBy:x("updated_by",{length:50})}),l=()=>({createdAt:K("created_at"),createdBy:x("created_by",{length:50})}),s=m.table("TDGroupExternalLink",{id:_("id",{mode:"number"}).identity().notNull(),groupId:_("group_id",{mode:"number"}).notNull(),tenantId:x(U,{length:64}).notNull(),provider:x(ye,{length:32}).notNull(),externalId:x("external_id",{length:256}).notNull(),externalKey:x("external_key",{length:320}).notNull(),membershipMode:x("membership_mode",{length:16}).notNull(),isSyncEnabled:xe("is_sync_enabled").notNull(),syncedAt:K("synced_at"),syncError:x("sync_error",{length:1e3}),vanishedObservations:me("vanished_observations").notNull(),...d()},e=>[q({name:`${r}_TDGroupExternalLink_pk`,columns:[e.id]}),B(`${r}_TDGroupExternalLink_group_uq`).on(e.groupId),B(`${r}_TDGroupExternalLink_external_uq`).on(e.tenantId,e.provider,e.externalId),te({name:`${r}_TDGroupExternalLink_group_id_fk`,columns:[e.groupId],foreignColumns:[a.groupTable.id]})]),h=m.table("TDGroupMemberEmail",{id:_("id",{mode:"number"}).identity().notNull(),groupId:_("group_id",{mode:"number"}).notNull(),email:x("email",{length:320}).notNull(),externalUserId:x("external_user_id",{length:128}),resolvedUserId:_("resolved_user_id",{mode:"number"}),observedAt:K("observed_at").notNull(),...d()},e=>[q({name:`${r}_TDGroupMemberEmail_pk`,columns:[e.id]}),B(`${r}_TDGroupMemberEmail_group_email_uq`).on(e.groupId,e.email),te({name:`${r}_TDGroupMemberEmail_group_id_fk`,columns:[e.groupId],foreignColumns:[a.groupTable.id]})]),w=m.table("TDTenantMembershipGroup",{tenantId:x(U,{length:64}).notNull(),groupId:_("group_id",{mode:"number"}).notNull(),isBreakGlass:xe("is_break_glass").notNull(),...l()},e=>[q({name:`${r}_TDTenantMembershipGroup_pk`,columns:[e.tenantId,e.groupId]}),te({name:`${r}_TDTenantMembershipGroup_group_id_fk`,columns:[e.groupId],foreignColumns:[a.groupTable.id]})]),b=m.table("TDAllowlistGroup",{id:_("id",{mode:"number"}).identity().notNull(),tenantId:x(U,{length:64}).notNull(),groupId:_("group_id",{mode:"number"}).notNull(),description:x("description",{length:512}),...l()},e=>[q({name:`${r}_TDAllowlistGroup_pk`,columns:[e.id]}),B(`${r}_TDAllowlistGroup_tenant_group_uq`).on(e.tenantId,e.groupId),te({name:`${r}_TDAllowlistGroup_group_id_fk`,columns:[e.groupId],foreignColumns:[a.groupTable.id]})]),y=m.table("TDDirectorySyncRequest",{id:_("id",{mode:"number"}).identity().notNull(),tenantId:x(U,{length:64}).notNull(),groupId:_("group_id",{mode:"number"}),requestedBy:x("requested_by",{length:320}).notNull(),requestedAt:K("requested_at").notNull(),pickedAt:K("picked_at"),finishedAt:K("finished_at"),result:x("result",{length:32}),error:x("error",{length:2e3}),...l()},e=>[q({name:`${r}_TDDirectorySyncRequest_pk`,columns:[e.id]})]),i=m.table("TMAllowlist",{id:_("id",{mode:"number"}).identity().notNull(),tenantId:x(U,{length:64}).notNull(),pattern:x("pattern",{length:256}).notNull(),type:x("type",{length:32}).notNull(),description:x("description",{length:512}),...l()},e=>[q({name:`${r}_TMAllowlist_pk`,columns:[e.id]}),B(`${r}_TMAllowlist_pattern_uq`).on(e.tenantId,e.pattern)]),t=m.table("TDDirectoryCatalog",{id:_("id",{mode:"number"}).identity().notNull(),tenantId:x(U,{length:64}).notNull(),provider:x(ye,{length:32}).notNull(),externalId:x("external_id",{length:256}).notNull(),externalKey:x("external_key",{length:320}).notNull(),name:x("name",{length:256}).notNull(),description:x("description",{length:1e3}),memberCount:me("member_count"),observedAt:K("observed_at").notNull(),...l()},e=>[q({name:`${r}_TDDirectoryCatalog_pk`,columns:[e.id]}),B(`${r}_TDDirectoryCatalog_external_uq`).on(e.tenantId,e.provider,e.externalId)]),n=m.table("TMDirectoryCatalogObservation",{id:_("id",{mode:"number"}).identity().notNull(),tenantId:x(U,{length:64}).notNull(),provider:x(ye,{length:32}).notNull(),observedAt:K("observed_at").notNull(),entryCount:me("entry_count").notNull(),...d()},e=>[q({name:`${r}_TMDirectoryCatalogObservation_pk`,columns:[e.id]}),B(`${r}_TMDirectoryCatalogObservation_target_uq`).on(e.tenantId,e.provider)]);return{TDGroupExternalLink:s,TDGroupMemberEmail:h,TDTenantMembershipGroup:w,TDAllowlistGroup:b,TDDirectorySyncRequest:y,TDDirectoryCatalog:t,TMDirectoryCatalogObservation:n,TMAllowlist:i}}import{assertBlastRadiusPolicy as er,reconcile as rr}from"@aiquants/auth-directory-core";function ne(r){if(r instanceof Error&&(r.name==="AbortError"||r.name==="TimeoutError"))return"timeout";let a=typeof r=="object"&&r!==null&&"status"in r?r.status:void 0;return typeof a!="number"?"unknown":a===401||a===403?"denied":a===429||a===408?"rateLimited":a>=500?"upstreamError":"unknown"}var ke=r=>`${r.length} \u4EF6`,ge={denied:"\u8CC7\u683C\u60C5\u5831\u304C\u62D2\u5426\u3055\u308C\u307E\u3057\u305F",rateLimited:"\u547C\u3073\u51FA\u3057\u304C\u5236\u9650\u3055\u308C\u3066\u3044\u307E\u3059",upstreamError:"\u76F8\u624B\u5074\u304C\u5185\u90E8\u30A8\u30E9\u30FC\u3092\u8FD4\u3057\u307E\u3057\u305F",timeout:"\u5FDC\u7B54\u304C\u3042\u308A\u307E\u305B\u3093\u3067\u3057\u305F",localWriteError:"\u66F8\u304D\u8FBC\u307F\u304C\u62D2\u5426\u3055\u308C\u307E\u3057\u305F",unknown:"\u8A73\u7D30\u306F\u30B5\u30FC\u30D0\u30FC\u306E\u30ED\u30B0\u306B\u51FA\u3066\u3044\u307E\u3059"},Re={upstreamUnreadable:r=>`\u4E0A\u6D41\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306E\u8AAD\u307F\u53D6\u308A\u306B\u5931\u6557\u3057\u307E\u3057\u305F (${ge[r]})\u3002`,localWriteFailed:r=>`\u30ED\u30FC\u30AB\u30EB\u3078\u306E\u66F8\u304D\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F (${ge[r]})\u3002`,resolverIncomplete:(r,a)=>`\u4E0A\u6D41\u304C ${r} \u4EF6\u306E\u30E1\u30F3\u30D0\u30FC\u3092\u8FD4\u3057\u307E\u3057\u305F\u304C\u3001\u7A81\u5408\u7D50\u679C\u306F ${a} \u4EF6\u3067\u3057\u305F\u3002\u4EF6\u6570\u304C\u6E1B\u308B\u7A81\u5408\u306F\u3001\u89E3\u6C7A\u3067\u304D\u306A\u304B\u3063\u305F\u4EBA\u7269\u3092\u300C\u4E0A\u6D41\u304B\u3089\u6D88\u3048\u305F\u300D\u3068\u3057\u3066\u524A\u9664\u3057\u3066\u3057\u307E\u3046\u305F\u3081\u4E2D\u65AD\u3057\u307E\u3057\u305F\u3002\u89E3\u6C7A\u3067\u304D\u306A\u3044\u30E1\u30F3\u30D0\u30FC\u306F\u7701\u7565\u305B\u305A resolvedUserId: null \u3067\u8FD4\u3059\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`,groupMissing:(r,a,m)=>`\u4E0A\u6D41\u3067\u30B0\u30EB\u30FC\u30D7\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (${r})\u3002\u9023\u7D9A ${a} \u56DE\u76EE\u3067\u3059\u3002${m} \u56DE\u306B\u9054\u3057\u305F\u6642\u70B9\u3067\u30B0\u30EB\u30FC\u30D7\u3092\u7121\u52B9\u5316\u3057\u307E\u3059\u3002`,groupVanished:(r,a)=>`\u4E0A\u6D41\u3067\u30B0\u30EB\u30FC\u30D7\u304C ${a} \u56DE\u9023\u7D9A\u3067\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F (${r})\u3002\u30B0\u30EB\u30FC\u30D7\u3092\u7121\u52B9\u5316\u3057\u307E\u3057\u305F\u3002\u4E0A\u6D41\u304C\u5FA9\u65E7\u3059\u308C\u3070\u6B21\u56DE\u306E\u540C\u671F\u3067\u81EA\u52D5\u7684\u306B\u518D\u6709\u52B9\u5316\u3055\u308C\u307E\u3059\u3002`,planRefused:r=>{if(r.reason==="invalid-upstream-data")return`\u540C\u671F\u3092\u4E2D\u65AD\u3057\u307E\u3057\u305F: \u4E0A\u6D41\u306E\u30E1\u30F3\u30D0\u30FC\u4E00\u89A7\u306B\u6271\u3048\u306A\u3044\u30A2\u30C9\u30EC\u30B9\u304C ${ke(r.offendingEmails)} \u542B\u307E\u308C\u3066\u3044\u307E\u3059\u3002\u5BFE\u8C61\u306F\u30B5\u30FC\u30D0\u30FC\u306E\u30ED\u30B0\u306B\u51FA\u3066\u3044\u307E\u3059\u3002`;let a=r.axis==="membership"?"\u6A29\u9650\u3092\u5931\u3046\u4EBA\u6570":"\u30B5\u30A4\u30F3\u30A4\u30F3\u3067\u304D\u306A\u304F\u306A\u308B\u4EBA\u6570",m=r.axis==="membership"?r.withheldProjectionRemoves.map(String):r.withheldLedgerDeletes;return[`\u540C\u671F\u3092\u4E2D\u65AD\u3057\u307E\u3057\u305F: ${a}\u304C ${r.population} \u4EBA\u4E2D ${r.removalCount} \u4EBA\u306B\u306A\u308A\u307E\u3059`,`(\u4E0A\u9650: ${r.limits.maxRemovalCount} \u4EF6 / \u6BD4\u7387 ${r.limits.maxRemovalRatio})\u3002`,`\u5DEE\u3057\u6B62\u3081\u305F\u5BFE\u8C61: ${ke(m)} (\u4E00\u89A7\u306F\u30B5\u30FC\u30D0\u30FC\u306E\u30ED\u30B0\u306B\u51FA\u3066\u3044\u307E\u3059)\u3002`,"\u4E0A\u6D41\u5074\u306E\u8A2D\u5B9A\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u610F\u56F3\u3057\u305F\u5909\u66F4\u3067\u3042\u308C\u3070\u3001\u3053\u306E\u8EF8\u306E\u4E0A\u9650\u3060\u3051\u3092\u4E00\u6642\u7684\u306B\u5F15\u304D\u4E0A\u3052\u3066\u518D\u5B9F\u884C\u3057\u307E\u3059\u3002"].join(" ")},removalWritesFailed:r=>`${r.length} \u4EF6\u306E\u6240\u5C5E\u89E3\u9664\u304C\u66F8\u304D\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F (${[...new Set(r.map(a=>ge[a.cause]))].join(" / ")})\u3002\u6B21\u56DE\u306E\u5B9F\u884C\u3067\u518D\u8A66\u884C\u3057\u307E\u3059\u3002`,removalsRefused:r=>`${r.length} \u4EF6\u306E\u6240\u5C5E\u89E3\u9664\u304C\u6700\u5F8C\u306E\u7BA1\u7406\u8005\u30AC\u30FC\u30C9\u306B\u3088\u308A\u62D2\u5426\u3055\u308C\u307E\u3057\u305F (\u30E6\u30FC\u30B6\u30FC ${r.map(a=>a.userId).join(", ")})\u3002\u6B8B\u308A\u306E\u5909\u66F4\u306F\u9069\u7528\u6E08\u307F\u3067\u3059\u3002`,foreignProvider:(r,a)=>`\u3053\u306E\u30EA\u30F3\u30AF\u306E\u4F9B\u7D66\u5143\u306F ${r} \u3067\u3059\u304C\u3001\u5B9F\u884C\u4E2D\u306E\u540C\u671F\u306F ${a} \u7528\u3067\u3059\u3002\u5BFE\u5FDC\u3059\u308B\u540C\u671F\u30B8\u30E7\u30D6\u304B\u3089\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002`};var he=["aborted","missing","vanished","upstream-failed","local-failed","foreign-provider","not-converged"],Ge=[...he,"already-vanished"];function st(r){let{directory:a,repository:m,policy:d,now:l,logger:s}=r,h=r.messages??Re;if(er(d),!Number.isInteger(r.vanishedThreshold)||r.vanishedThreshold<1)throw new TypeError(`[@aiquants/auth-directory-drizzle] vanishedThreshold must be a positive integer, received ${String(r.vanishedThreshold)}`);if(typeof r.provider!="string"||r.provider.trim()==="")throw new TypeError("[@aiquants/auth-directory-drizzle] provider must be a non-empty string so links of other providers can be told apart");async function w(t,n){try{await m.recordFailure(t,n)}catch(e){s.error(`[auth-directory] could not record the failure for ${t.externalKey}:`,e)}}async function b(t){try{let n=await a.getGroup(t.externalId);return n===null?{kind:"missing"}:{kind:"ok",group:n,members:await a.listGroupMembers(n,t.membershipMode)}}catch(n){return{kind:"failed",error:n instanceof Error?n:new Error(String(n))}}}async function y(t){if(t.vanishedObservations>=r.vanishedThreshold&&!t.isGroupActive)return s.warn(`[auth-directory] ${t.externalKey} still missing upstream (already deactivated after ${t.vanishedObservations} absences)`),{kind:"already-vanished",link:t};let n=await m.recordVanishedObservation(t,h.groupMissing(t.externalKey,t.vanishedObservations+1,r.vanishedThreshold));return n<r.vanishedThreshold?(s.warn(`[auth-directory] ${t.externalKey} missing upstream (${n}/${r.vanishedThreshold})`),{kind:"missing",link:t,observations:n}):(await m.markGroupInactive(t),await w(t,h.groupVanished(t.externalKey,n)),s.warn(`[auth-directory] ${t.externalKey} deactivated after ${n} consecutive absences`),{kind:"vanished",link:t})}async function i(t){if(t.provider!==r.provider)return await w(t,h.foreignProvider(t.provider,r.provider)),s.warn(`[auth-directory] skipping ${t.externalKey}: belongs to provider ${t.provider}`),{kind:"foreign-provider",link:t};let n=await b(t);if(n.kind==="missing")return y(t);if(n.kind==="failed")return await w(t,h.upstreamUnreadable(ne(n.error))),s.error(`[auth-directory] upstream read failed for ${t.externalKey}:`,n.error),{kind:"upstream-failed",link:t,error:n.error};try{await m.resolveExternalIdentity(t,n.group);let[e,p]=await Promise.all([m.loadGroupSnapshot(t),m.resolveMembers(n.members)]);if(p.length!==n.members.length){let D=h.resolverIncomplete(n.members.length,p.length);return await w(t,D),s.error(`[auth-directory] ${D}`),{kind:"local-failed",link:t,error:new Error(D)}}let u=rr(e,p,d);if(u.aborted){await w(t,h.planRefused(u.aborted));let D=u.aborted.reason==="invalid-upstream-data"?u.aborted.offendingEmails:u.aborted.axis==="membership"?u.aborted.withheldProjectionRemoves.map(String):u.aborted.withheldLedgerDeletes;return s.warn(`[auth-directory] plan refused for ${t.externalKey}: ${u.aborted.reason}; withheld: ${D.join(", ")}`),{kind:"aborted",link:t,plan:u}}let f=u.ledgerUpserts.length>0||u.ledgerDeletes.length>0||u.projectionAdds.length>0||u.projectionRemoves.length>0,S=l(),T={refusals:[],failures:[]};return f&&(T=await m.applyPlan(t,u,S)),await m.recordSuccess(t,n.group,S),T.refusals.length>0&&(await w(t,h.removalsRefused(T.refusals)),s.warn(`[auth-directory] ${T.refusals.length} removal(s) refused for ${t.externalKey}: users ${T.refusals.map(D=>D.userId).join(", ")}`)),T.failures.length>0&&(await w(t,h.removalWritesFailed(T.failures)),s.warn(`[auth-directory] ${T.failures.length} removal(s) could not be written for ${t.externalKey}: users ${T.failures.map(D=>D.userId).join(", ")}`)),u.unresolvedEmails.length>0&&s.info(`[auth-directory] ${t.externalKey}: ${u.unresolvedEmails.length} upstream member(s) have no local user yet`),T.refusals.length>0||T.failures.length>0?{kind:"not-converged",link:t,plan:u,refusals:T.refusals,failures:T.failures}:f?{kind:"applied",link:t,plan:u}:{kind:"unchanged",link:t}}catch(e){let p=e instanceof Error?e:new Error(String(e));return await w(t,h.localWriteFailed("localWriteError")),s.error(`[auth-directory] local write failed for ${t.externalKey}:`,p),{kind:"local-failed",link:t,error:p}}}return{syncOne:i,async run(t){let n=await m.listSyncableLinks(t),e=[];for(let f of n)try{e.push(await i(f))}catch(S){let T=S instanceof Error?S:new Error(String(S));s.error(`[auth-directory] ${f.externalKey} ended abnormally:`,T),e.push({kind:"local-failed",link:f,error:T})}let p=e.filter(f=>Ge.includes(f.kind)).length,u=e.filter(f=>he.includes(f.kind)).length;return s.info(`[auth-directory] run finished: ${e.length} link(s), ${p} unhealthy (${u} new)`),{outcomes:e,unhealthyCount:p,newlyUnhealthyCount:u}}}}import{assertTenantId as tr}from"@aiquants/authz-core";import{toInstant as nr}from"@aiquants/mssql";import{and as oe,asc as Ee,eq as F,isNull as ie}from"drizzle-orm";var or="The job that claimed this request ended without recording an outcome; whether the synchronization ran is unknown.",be=2e3;function pt(r,a,m){let d=r,l=m.ensureConnection??(async()=>{}),s=a.TDDirectorySyncRequest,h=tr(m.tenantId,"createSyncRequestQueue(tenantId)"),w=m.maxErrorLength??be;if(!Number.isInteger(w)||w<1||w>be)throw new TypeError(`[@aiquants/auth-directory-drizzle] maxErrorLength must be an integer within 1..${be}, received ${String(m.maxErrorLength)}`);let b={async claim(){await l();let y=await d.select({id:s.id}).from(s).where(oe(F(s.tenantId,h),ie(s.pickedAt))).orderBy(Ee(s.id));for(let i of y){let n=(await d.update(s).set({pickedAt:m.now()}).output().where(oe(F(s.tenantId,h),F(s.id,Number(i.id)),ie(s.pickedAt))))?.[0];if(!n)continue;if(n.requestedBy===void 0||n.requestedBy===null)throw new TypeError(`[@aiquants/auth-directory-drizzle] the driver returned a row without requestedBy for request ${String(n.id)}`);let e=n.groupId??null;return{id:Number(n.id),tenantId:h,groupId:e===null?null:Number(e),requestedBy:String(n.requestedBy)}}return null},async finish(y,i){await l();let t=i.error?i.error.slice(0,w):null;return!!(await d.update(s).set({finishedAt:m.now(),result:i.result,error:t}).output().where(oe(F(s.tenantId,h),F(s.id,y),ie(s.finishedAt))))?.length},async listStranded(y){await l();let i=await d.select({id:s.id,pickedAt:s.pickedAt,requestedAt:s.requestedAt}).from(s).where(oe(F(s.tenantId,h),ie(s.finishedAt))).orderBy(Ee(s.id)),t=n=>{let e=nr(n);return Number.isNaN(e.getTime())||e<y};return{stranded:i.filter(n=>n.pickedAt!==null&&t(n.pickedAt)).map(n=>Number(n.id)),unclaimed:i.filter(n=>n.pickedAt===null&&t(n.requestedAt)).map(n=>Number(n.id))}},async abandonStranded(y){let{stranded:i}=await b.listStranded(y),t=[];for(let n of i)try{await b.finish(n,{result:"failed",error:or})&&t.push(n)}catch(e){m.onCloseFailed?.(n,e)}return t}};return b}import{normalizeAuthEmail as ae}from"@aiquants/auth-core";import{assertTenantId as Me,assertTenantMatch as ir}from"@aiquants/authz-core";import{chunk as Ce,chunkByParameters as Pe}from"@aiquants/mssql";import{notInJsonList as ar}from"@aiquants/mssql/drizzle";import{and as L,eq as R,inArray as fe,isNull as sr,or as ur}from"drizzle-orm";var dr=7,lr=4,_e=1e3;function cr(r){if(!(r instanceof Error))return!1;let a=r;return a.number===void 0&&a.code===void 0&&r.name==="Error"}function St(r,a,m,d){let l=r,s=d.ensureConnection??(async()=>{}),h=$(l,d),w=d.now??(()=>new Date),{TDGroupExternalLink:b,TDGroupMemberEmail:y}=a,{TMGroup:i,TDGroupMember:t}=m,n=d.actor;return{async listSyncableLinks(e){await s();let p=Me(e.tenantId,"listSyncableLinks(tenantId)"),u=[R(b.isSyncEnabled,!0),R(b.tenantId,p)];return e.groupId!==void 0&&u.push(R(b.groupId,e.groupId)),(await l.select({linkId:b.id,groupId:b.groupId,linkTenantId:b.tenantId,groupTenantId:i.tenantId,groupKey:i.groupKey,isGroupActive:i.isActive,provider:b.provider,externalId:b.externalId,externalKey:b.externalKey,membershipMode:b.membershipMode,vanishedObservations:b.vanishedObservations}).from(b).innerJoin(i,L(R(i.id,b.groupId),R(i.tenantId,p))).where(L(...u))).map(S=>(ir(String(S.groupTenantId),String(S.linkTenantId),"listSyncableLinks(link.tenantId vs TMGroup.tenant_id)"),{linkId:Number(S.linkId),groupId:Number(S.groupId),groupKey:String(S.groupKey),tenantId:String(S.groupTenantId),provider:String(S.provider),externalId:String(S.externalId),externalKey:String(S.externalKey),membershipMode:J(String(S.membershipMode)),isGroupActive:!!S.isGroupActive,vanishedObservations:Number(S.vanishedObservations??0)}))},async loadGroupSnapshot(e){await s();let[p,u]=await Promise.all([l.select({email:y.email,externalUserId:y.externalUserId,resolvedUserId:y.resolvedUserId}).from(y).where(R(y.groupId,e.groupId)),l.select({userId:t.userId}).from(t).where(R(t.groupId,e.groupId))]);return{ledger:p.map(f=>({email:String(f.email),...f.externalUserId?{externalUserId:String(f.externalUserId)}:{},resolvedUserId:f.resolvedUserId===null?null:Number(f.resolvedUserId)})),projectedUserIds:u.map(f=>Number(f.userId))}},resolveMembers(e){return d.memberResolver.resolve(e)},async applyPlan(e,p,u){Me(e.tenantId,"applyPlan(link.tenantId)"),await s(),(p.ledgerUpserts.length>0||p.projectionAdds.length>0)&&await h(async D=>{let G=p.ledgerUpserts.map(g=>({...g,email:ae(g.email)})),o=new Map;for(let g of Ce(G.map(I=>I.email))){let I=await D.select({id:y.id,email:y.email}).from(y).where(L(R(y.groupId,e.groupId),fe(y.email,g)));for(let A of I)o.set(String(A.email),Number(A.id))}let c=G.filter(g=>!o.has(g.email));for(let g of Pe(c,dr))await D.insert(y).output().values(g.map(I=>({groupId:e.groupId,email:I.email,externalUserId:I.externalUserId??null,resolvedUserId:I.resolvedUserId,observedAt:u,createdAt:u,createdBy:n})));for(let g of G){let I=o.get(g.email);I!==void 0&&await D.update(y).set({externalUserId:g.externalUserId??null,resolvedUserId:g.resolvedUserId,observedAt:u,updatedAt:u,updatedBy:n}).output().where(R(y.id,I))}for(let g of Pe(p.projectionAdds,lr)){let I=new Set((await D.select({userId:t.userId}).from(t).where(L(R(t.groupId,e.groupId),fe(t.userId,g)))).map(k=>Number(k.userId))),A=g.filter(k=>!I.has(k));A.length>0&&await D.insert(t).output().values(A.map(k=>({groupId:e.groupId,userId:k,createdAt:u,createdBy:n})))}});let f=[],S=[];for(let D of p.projectionRemoves)try{await d.lockoutGuard.guardedWrite(e.tenantId,{removeGroupMembership:{groupId:e.groupId,userId:D}},"remove-group-member",G=>G.delete(t).where(L(R(t.groupId,e.groupId),R(t.userId,D))))}catch(G){cr(G)?f.push({userId:D,applied:!1,refusal:G.message}):S.push({userId:D,cause:ne(G)})}let T=f.map(D=>D.userId);return p.ledgerDeletes.length>0&&await h(async D=>{for(let G of Ce(p.ledgerDeletes)){let o=T.length>0?ur(sr(y.resolvedUserId),ar(y.resolvedUserId,T)):void 0;await D.delete(y).where(L(R(y.groupId,e.groupId),fe(y.email,G),...o?[o]:[]))}}),{refusals:f,failures:S}},async resolveExternalIdentity(e,p){await s(),!(e.externalId===p.externalId&&e.externalKey===ae(p.externalKey))&&await l.update(b).set({externalId:p.externalId,externalKey:ae(p.externalKey)}).output().where(L(R(b.id,e.linkId),R(b.tenantId,e.tenantId)))},async recordSuccess(e,p,u){await s(),await h(async f=>{await f.update(b).set({syncedAt:u,syncError:null,vanishedObservations:0,externalId:p.externalId,externalKey:ae(p.externalKey),updatedAt:u,updatedBy:n}).output().where(L(R(b.id,e.linkId),R(b.tenantId,e.tenantId))),e.isGroupActive||await f.update(i).set({isActive:!0,updatedAt:u,updatedBy:n}).output().where(L(R(i.id,e.groupId),R(i.tenantId,e.tenantId)))})},async recordFailure(e,p){await s(),await l.update(b).set({syncError:p.slice(0,_e),updatedAt:w(),updatedBy:n}).output().where(L(R(b.id,e.linkId),R(b.tenantId,e.tenantId)))},async recordVanishedObservation(e,p){await s();let u=e.vanishedObservations+1;return await l.update(b).set({vanishedObservations:u,syncError:p.slice(0,_e),updatedAt:w(),updatedBy:n}).output().where(L(R(b.id,e.linkId),R(b.tenantId,e.tenantId))),u},async markGroupInactive(e){await s(),await l.update(i).set({isActive:!1,updatedAt:w(),updatedBy:n}).output().where(L(R(i.id,e.groupId),R(i.tenantId,e.tenantId),R(i.isActive,!0)))}}}export{ye as AUTH_DIRECTORY_PROVIDER_COLUMN,Xr as AUTH_DIRECTORY_TENANT_SCOPED_TABLES,Se as MEMBERSHIP_MODES,he as NEWLY_UNHEALTHY_SYNC_OUTCOMES,or as STRANDED_REASON,Ge as UNHEALTHY_SYNC_OUTCOMES,Dr as assertGroupMayBeLinked,J as assertMembershipMode,ne as classifySyncFailure,wr as createDirectoryCatalogReader,fr as createDirectoryCatalogWriter,Or as createDirectoryLinkStore,st as createDirectorySync,St as createDirectorySyncRepository,Yr as createPromotionRepository,pt as createSyncRequestQueue,Sr as defaultExternalGroupGuardMessage,Re as defaultSyncMessages,et as defineAuthDirectorySchema,ee as findLedgerRowsForIdentity,Ur as promoteLedgerForUser,$ as transactionRunner,Tr as withExternalGroupGuards};
1
+ import{assertTenantId as xe}from"@aiquants/authz-core";import{chunkByParameters as We,toInstant as He}from"@aiquants/mssql";import{and as ee,asc as Ve,eq as q}from"drizzle-orm";import{runWithTransactionRetry as je}from"@aiquants/mssql";function N(e,n){return c=>je(u=>e.transaction(d=>u(()=>c(d))),n.transactionRetry)}var Je=10,$={tenantId:64,provider:32,externalId:256,externalKey:320,name:256,description:1e3,actor:50},ke=2147483647;function Ye(e){if(e===void 0||!Number.isFinite(e))return null;let n=Math.trunc(e);return n<0?null:n>ke?ke:n}function V(e,n,c){let u=e.trim();if(u==="")throw new TypeError(`[@aiquants/auth-directory-drizzle] ${c} must not be blank`);if(u.length>n)throw new TypeError(`[@aiquants/auth-directory-drizzle] ${c} exceeds its column length (${n}); truncating it would change which rows it owns`);return u}function Gr(e,n,c){let u=e,d=c.ensureConnection??(async()=>{}),a=N(u,c),p=n.TDDirectoryCatalog,v=n.TMDirectoryCatalogObservation,f=h=>{let s=new Map;for(let t of h)s.set(t.externalId,t);return[...s.values()]};return{async replace(h,s,t){await d();let i=V(xe(h,"replace(tenantId)"),$.tenantId,"tenantId"),r=V(s,$.provider,"provider"),g=V(c.actor,$.actor,"actor"),l=c.now(),I=f(t.map(b=>({tenantId:i,provider:r,externalId:b.externalId.slice(0,$.externalId),externalKey:b.externalKey.slice(0,$.externalKey),name:b.name.slice(0,$.name),description:b.description===void 0?null:b.description.slice(0,$.description),memberCount:Ye(b.memberCount),observedAt:l,createdAt:l,createdBy:g})));return await a(async b=>{await b.delete(p).where(ee(q(p.tenantId,i),q(p.provider,r)));for(let T of We(I,Je))await b.insert(p).values(T);await b.delete(v).where(ee(q(v.tenantId,i),q(v.provider,r))),await b.insert(v).values([{tenantId:i,provider:r,observedAt:l,entryCount:I.length,createdAt:l,createdBy:g,updatedAt:l,updatedBy:g}])}),I.length}}}function Cr(e,n,c={}){let u=e,d=c.ensureConnection??(async()=>{}),a=n.TDDirectoryCatalog,p=n.TMDirectoryCatalogObservation;return{async read(v,f){await d();let h=V(xe(v,"read(tenantId)"),$.tenantId,"tenantId"),s=V(f,$.provider,"provider"),t=await u.select({observedAt:p.observedAt,entryCount:p.entryCount,externalId:a.externalId,externalKey:a.externalKey,name:a.name,description:a.description,memberCount:a.memberCount}).from(p).leftJoin(a,ee(q(a.tenantId,p.tenantId),q(a.provider,p.provider))).where(ee(q(p.tenantId,h),q(p.provider,s))).orderBy(Ve(a.name)),i=t.filter(l=>l.externalId!==null).map(l=>({externalId:String(l.externalId),externalKey:String(l.externalKey),name:String(l.name),...l.description===null?{}:{description:l.description},...l.memberCount===null?{}:{memberCount:Number(l.memberCount)}})),r=t[0];if(r===void 0)return{groups:i,observedAt:null};if(Number(r.entryCount)!==i.length)throw new Error(`[@aiquants/auth-directory-drizzle] catalog is inconsistent for (${h}, ${s}): the observation records ${r.entryCount} entries but ${i.length} rows are present`);let g=He(r.observedAt);if(!Number.isFinite(g.getTime()))throw new Error(`[@aiquants/auth-directory-drizzle] catalog observation for (${h}, ${s}) has an unreadable observed_at`);return{groups:i,observedAt:g}}}}import{AUTHZ_ACTIONS as Qe}from"@aiquants/authz-core";var K=class extends Error{constructor(c){super(c);this.operatorFacing=!0;this.name="DirectoryOperatorError"}};var Or=(e,n)=>`\u5916\u90E8\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u9023\u643A\u30B0\u30EB\u30FC\u30D7 (${n}) \u306B\u306F\u7BA1\u7406\u30ED\u30FC\u30EB (${e}) \u3092\u6301\u305F\u305B\u3089\u308C\u307E\u305B\u3093\u3002\u4E0A\u6D41\u5074\u306E\u6240\u5C5E\u5909\u66F4\u304C\u305D\u306E\u307E\u307E\u7BA1\u7406\u8005\u306E\u5897\u6E1B\u306B\u306A\u308B\u305F\u3081\u3001\u7BA1\u7406\u8005\u7D4C\u8DEF\u306F\u30ED\u30FC\u30AB\u30EB\u7BA1\u7406\u306E\u30B0\u30EB\u30FC\u30D7\u306B\u9650\u5B9A\u3057\u3066\u3044\u307E\u3059\u3002`;function W(e,n,c,u){return new Set(e.filter(d=>n.has(d.roleId)&&d.appKey===c&&d.resourceKey===u).map(d=>d.action))}function re(e,n){return new Set(e.filter(c=>c.groupId===n).map(c=>c.roleId))}var J=e=>Qe.every(n=>e.has(n)),ne=(e,n)=>e.find(c=>c.roleId===n)?.roleKey??`#${n}`;function te(e,n,c,u){let d=[...n].find(p=>J(W(e,new Set([p]),c,u)));return d!==void 0?ne(e,d):[...n].filter(p=>W(e,new Set([p]),c,u).size>0).map(p=>ne(e,p)).join(" + ")}function Ur(e,n){return Re(e,n)}function Re(e,n){let c=e;return{...e,withTransaction:(u,d)=>c.withTransaction((a,p)=>u(Re(a,n),p),d),async assignGroupRole(u,d,a,p){await c.withTransaction(async(v,f)=>{let h=await v.listGrants(u),s=re(await v.listGroupRoles(u),d);if(s.add(a),J(W(h,s,n.adminAppKey,n.adminResourceKey))){let t=await n.linkLookup.externalGroupKey(u,d,f);if(t!==null)throw new K(n.message(te(h,s,n.adminAppKey,n.adminResourceKey),t))}await v.assignGroupRole(u,d,a,p)})},async updateGrant(u,d,a,p){let v=a.action;if(v===void 0)return c.updateGrant(u,d,a,p);await c.withTransaction(async(f,h)=>{let s=await f.listGrants(u),t=s.find(g=>g.id===d);if(t===void 0)return f.updateGrant(u,d,a,p);let r=(await f.listResources(u)).find(g=>g.id===t.resourceId);if(r?.appKey===n.adminAppKey&&r?.resourceKey===n.adminResourceKey){let g=await f.listGroupRoles(u);for(let l of new Set(g.filter(I=>I.roleId===t.roleId).map(I=>I.groupId))){let I=re(g,l),b=W(s.filter(A=>A.id!==d),I,n.adminAppKey,n.adminResourceKey);if(I.has(t.roleId)&&b.add(v),!J(b))continue;let T=await n.linkLookup.externalGroupKey(u,l,h);if(T!==null)throw new K(n.message(te(s,I,n.adminAppKey,n.adminResourceKey)||ne(s,t.roleId),T))}}await f.updateGrant(u,d,a,p)})},async createGrant(u,d,a){await c.withTransaction(async(p,v)=>{let h=(await p.listResources(u)).find(s=>s.id===d.resourceId);if(h?.appKey===n.adminAppKey&&h?.resourceKey===n.adminResourceKey){let s=await p.listGrants(u),t=await p.listGroupRoles(u);for(let i of new Set(t.filter(r=>r.roleId===d.roleId).map(r=>r.groupId))){let r=W(s,re(t,i),n.adminAppKey,n.adminResourceKey);if(r.add(d.action),!J(r))continue;let g=await n.linkLookup.externalGroupKey(u,i,v);if(g!==null)throw new K(n.message(te(s,re(t,i),n.adminAppKey,n.adminResourceKey)||ne(s,d.roleId),g))}}await p.createGrant(u,d,a)})}}}async function $r(e,n,c,u){let d=(await e.listGroupRoles(n)).filter(f=>f.groupId===c);if(d.length===0)return;let a=await e.listGrants(n),p=new Set(d.map(f=>f.roleId));if(!J(W(a,p,u.adminAppKey,u.adminResourceKey)))return;let v=d[0];if(v)throw new K(u.message(te(a,p,u.adminAppKey,u.adminResourceKey),v.groupKey))}import{and as Ze,eq as Y}from"drizzle-orm";async function oe(e,n,c){let{ledger:u,group:d}=n,{tenantId:a,externalUserId:p,email:v}=c;if(p===null&&v==="")return[];let f={ledgerId:u.id,groupId:u.groupId,tenantId:d.tenantId,externalUserId:u.externalUserId},h=async g=>(await e.select(f).from(u).innerJoin(d,Ze(Y(d.id,u.groupId),Y(d.tenantId,a),Y(d.isActive,!0))).where(g)).map(I=>({ledgerId:Number(I.ledgerId),groupId:Number(I.groupId),tenantId:String(I.tenantId),externalUserId:I.externalUserId===null?null:String(I.externalUserId)})),s=g=>{let l=new Map;for(let I of g)l.set(I.ledgerId,{ledgerId:I.ledgerId,groupId:I.groupId,tenantId:I.tenantId});return[...l.values()].sort((I,b)=>I.ledgerId-b.ledgerId)},t=p===null?[]:await h(Y(u.externalUserId,p));if(v==="")return s(t);let i=await h(Y(u.email,v)),r=g=>g.externalUserId!==null&&g.externalUserId!==p;return s([...t,...i.filter(g=>!r(g))])}import{AUTH_DIRECTORY_NO_UNRESOLVED as Ee,normalizeAuthEmail as Z}from"@aiquants/auth-core";import{assertTenantMembershipPolicy as Xe}from"@aiquants/auth-directory-core";import{assertTenantId as P,assertTenantMatch as er}from"@aiquants/authz-core";import{toInstant as Ge}from"@aiquants/mssql";import{and as C,asc as Ce,count as Pe,eq as S,isNull as ie}from"drizzle-orm";var Me=["direct","transitive"];function Q(e){if(!Me.includes(e))throw new TypeError(`[@aiquants/auth-directory-drizzle] membershipMode must be one of ${Me.join(" | ")}, received ${JSON.stringify(e)}`);return e}var Xr={breakGlassLink:e=>`\u30B0\u30EB\u30FC\u30D7 (${e}) \u306F\u7DCA\u6025\u7528 (break-glass) \u306E\u30C6\u30CA\u30F3\u30C8\u5E30\u5C5E\u306E\u7D4C\u8DEF\u306B\u6307\u5B9A\u3055\u308C\u3066\u3044\u308B\u305F\u3081\u3001\u4E0A\u6D41\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3068\u9023\u643A\u3067\u304D\u307E\u305B\u3093\u3002\u3053\u306E\u7D4C\u8DEF\u306F\u3001\u540C\u671F\u304C\u89E6\u308C\u306A\u3044\u30ED\u30FC\u30AB\u30EB\u7BA1\u7406\u306E\u30B0\u30EB\u30FC\u30D7\u306B\u9650\u3063\u3066\u3044\u307E\u3059\u3002`,allowlistNeedsLink:e=>`\u30B0\u30EB\u30FC\u30D7 (${e}) \u306F\u30ED\u30FC\u30AB\u30EB\u7BA1\u7406\u306E\u305F\u3081\u3001\u30B5\u30A4\u30F3\u30A4\u30F3\u8A31\u53EF\u306B\u4F7F\u3048\u307E\u305B\u3093\u3002\u30B0\u30EB\u30FC\u30D7\u5358\u4F4D\u306E\u30B5\u30A4\u30F3\u30A4\u30F3\u8A31\u53EF\u306F\u4E0A\u6D41\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306E\u30E1\u30F3\u30D0\u30FC\u53F0\u5E33\u3067\u5224\u5B9A\u3059\u308B\u306E\u3067\u3001\u4E0A\u6D41\u3068\u9023\u643A\u3057\u305F\u30B0\u30EB\u30FC\u30D7\u3060\u3051\u3092\u6307\u5B9A\u3067\u304D\u307E\u3059\u3002`};function rr(e){let n=typeof e=="object"&&e!==null&&"number"in e?e.number:void 0;return n===2601||n===2627}function et(e,n,c,u){let d=e,a=u.ensureConnection??(async()=>{}),p=N(d,u),v=u.policyCacheTtlMs??0;if(!Number.isFinite(v)||v<0)throw new TypeError(`[@aiquants/auth-directory-drizzle] policyCacheTtlMs must be a non-negative number, received ${String(u.policyCacheTtlMs)}`);let f=new Map,h=o=>f.delete(o),{TDGroupExternalLink:s,TDGroupMemberEmail:t,TDTenantMembershipGroup:i,TDAllowlistGroup:r,TDDirectorySyncRequest:g}=n,{TMGroup:l,TDGroupMember:I}=c;async function b(o,m,y,w){if((await o.select({groupId:i.groupId}).from(i).where(C(S(i.tenantId,m),S(i.groupId,y),S(i.isBreakGlass,!0)))).length>0)throw new K(u.refusals.breakGlassLink(w))}async function T(o,m,y){let[w]=await o.select({groupKey:l.groupKey}).from(l).where(C(S(l.id,y),S(l.tenantId,m)));if(!w)throw new Error(`Group not found in tenant: ${y}`);return String(w.groupKey)}function A(o,m){return{linkId:Number(o.linkId),groupId:Number(o.groupId),tenantId:String(o.tenantId),provider:String(o.provider),externalId:String(o.externalId),externalKey:String(o.externalKey),membershipMode:Q(String(o.membershipMode)),isSyncEnabled:!!o.isSyncEnabled,...o.syncedAt?{syncedAt:Ge(o.syncedAt).toISOString()}:{},...o.syncError?{syncError:String(o.syncError)}:{},unresolved:m}}async function D(o){let m=await d.select({groupId:t.groupId,email:t.email,externalUserId:t.externalUserId}).from(t).innerJoin(l,C(S(l.id,t.groupId),S(l.tenantId,o))).where(ie(t.resolvedUserId));if(m.length===0)return new Map;let y=m.map(x=>({email:String(x.email),...x.externalUserId?{externalUserId:String(x.externalUserId)}:{}})),w=await u.memberResolver.resolve(y);if(w.length!==y.length)throw new Error(`[@aiquants/auth-directory-drizzle] memberResolver returned ${w.length} member(s) for ${y.length}`);let k=new Map;return m.forEach((x,E)=>{let U=w[E];if(Z(U.email)!==Z(y[E].email))throw new Error(`[@aiquants/auth-directory-drizzle] memberResolver answered ${U.email} at position ${E}, where ${y[E].email} was asked`);let G=U.resolvedUserId===null?U.unresolvedReason:"pendingSync",j=Number(x.groupId),X=k.get(j);X===void 0&&(X={...Ee},k.set(j,X)),X[G]+=1}),k}return{async listLinks(o){P(o,"listLinks(tenantId)"),await a();let m=await d.select({linkId:s.id,groupId:s.groupId,tenantId:s.tenantId,provider:s.provider,externalId:s.externalId,externalKey:s.externalKey,membershipMode:s.membershipMode,isSyncEnabled:s.isSyncEnabled,syncedAt:s.syncedAt,syncError:s.syncError}).from(s).where(S(s.tenantId,o)).orderBy(Ce(s.groupId)),y=await D(o);return m.map(w=>(er(o,String(w.tenantId),"listLinks(row.tenantId)"),A(w,y.get(Number(w.groupId))??Ee)))},async unresolvedByGroup(o){return P(o,"unresolvedByGroup(tenantId)"),await a(),D(o)},async externalGroupKey(o,m,y){P(o,"externalGroupKey(tenantId)"),await a();let[w]=await(y??d).select({externalKey:s.externalKey}).from(s).where(C(S(s.groupId,m),S(s.tenantId,o)));return w?String(w.externalKey):null},async linkGroup(o,m,y,w){P(o,"linkGroup(tenantId)");let k=Q(y.membershipMode),x=y.externalId.trim(),E=Z(y.externalKey);if(x===""||E==="")throw new TypeError("[@aiquants/auth-directory-drizzle] externalId and externalKey must be non-empty");await a(),await u.linkable.assertLinkableAndWrite(o,m,async U=>{let G=U,j=await T(G,o,m);await b(G,o,m,j),await G.insert(s).output().values({groupId:m,tenantId:o,provider:y.provider,externalId:x,externalKey:E,membershipMode:k,isSyncEnabled:!0,vanishedObservations:0,createdAt:new Date,createdBy:w})}),h(o)},async unlinkGroup(o,m){P(o,"unlinkGroup(tenantId)"),await a(),await p(async y=>{await T(y,o,m),await y.delete(t).where(S(t.groupId,m)),await y.delete(r).where(C(S(r.groupId,m),S(r.tenantId,o))),await y.delete(s).where(C(S(s.groupId,m),S(s.tenantId,o)))}),h(o)},async setSyncEnabled(o,m,y,w){P(o,"setSyncEnabled(tenantId)"),await a(),await d.update(s).set({isSyncEnabled:y,updatedAt:new Date,updatedBy:w}).output().where(C(S(s.groupId,m),S(s.tenantId,o)))},async requestSync(o,m,y){return P(o,"requestSync(tenantId)"),await a(),d.transaction(async w=>{if(m!==null&&await T(w,o,m),(await w.select({id:g.id}).from(g).where(C(S(g.tenantId,o),m===null?ie(g.groupId):S(g.groupId,m),ie(g.pickedAt)))).length>0)return"already-pending";try{await w.insert(g).output().values({tenantId:o,groupId:m,requestedBy:y,requestedAt:new Date,createdAt:new Date,createdBy:y})}catch(x){if(!rr(x))throw x;return"already-pending"}return"queued"})},async getStatus(o){P(o,"getStatus(tenantId)"),await a();let m=await d.select({groupId:s.groupId,isSyncEnabled:s.isSyncEnabled,syncedAt:s.syncedAt,syncError:s.syncError}).from(s).where(S(s.tenantId,o)),[y]=await d.select({n:Pe()}).from(g).where(C(S(g.tenantId,o),ie(g.finishedAt))),w=m.filter(G=>!!G.isSyncEnabled),k=w.map(G=>G.syncedAt?Ge(G.syncedAt).getTime():null),x=k.length>0&&k.every(G=>G!==null)?Math.min(...k):null,U=m.length>0&&w.length===0||w.length>0&&(x===null||Date.now()-x>u.stalenessThresholdSec*1e3);return{isEnabled:u.isEnabled,provider:u.provider,linkedGroupCount:m.length,...x!==null?{oldestSyncedAt:new Date(x).toISOString()}:{},isStale:U,pausedGroupCount:m.length-w.length,stalenessThresholdSec:u.stalenessThresholdSec,erroredGroupIds:m.filter(G=>!!G.syncError).map(G=>Number(G.groupId)).sort((G,j)=>G-j),pendingRequestCount:Number(y?.n??0)}},async loadTenantMembershipPolicy(o){P(o,"loadTenantMembershipPolicy(tenantId)");let m=v>0?f.get(o):void 0;if(m&&Date.now()-m.loadedAt<v)return m.policy;await a();let y=await d.select({groupId:i.groupId,isBreakGlass:i.isBreakGlass,externalLinkId:s.id}).from(i).innerJoin(l,C(S(l.id,i.groupId),S(l.tenantId,o))).leftJoin(s,C(S(s.groupId,i.groupId),S(s.tenantId,o))).where(S(i.tenantId,o));if(y.length===0){let E={kind:"no-proof-required"};return v>0&&f.set(o,{policy:E,loadedAt:Date.now()}),E}let w=y.map(E=>Number(E.groupId)),k=y.filter(E=>!!E.isBreakGlass);for(let E of k)if(E.externalLinkId!==null)throw new Error(`Break-glass group ${Number(E.groupId)} is externally sourced; a break-glass proof path must be one the synchronization can never touch.`);let x={kind:"require-proof",proofGroupIds:w,breakGlassGroupIds:k.map(E=>Number(E.groupId))};return Xe(x,`tenant ${o}`),v>0&&f.set(o,{policy:x,loadedAt:Date.now()}),x},async setTenantMembershipGroup(o,m,y,w){P(o,"setTenantMembershipGroup(tenantId)"),await a(),await p(async k=>{if(await T(k,o,m),y){let[E]=await k.select({id:s.id}).from(s).where(C(S(s.tenantId,o),S(s.groupId,m)));if(E)throw new Error(`Group ${m} is externally sourced; a break-glass proof path must be one the synchronization can never touch.`)}let x=await k.select({groupId:i.groupId}).from(i).where(C(S(i.tenantId,o),S(i.groupId,m)));if(!y&&!(await k.select({groupId:i.groupId,isBreakGlass:i.isBreakGlass}).from(i).where(S(i.tenantId,o))).filter(G=>Number(G.groupId)!==m).some(G=>!!G.isBreakGlass))throw new Error(`Declaring group ${m} as ordinary proof would leave tenant ${o} with a membership proof but no break-glass path; declare a locally-managed break-glass group first.`);if(x.length>0){await k.update(i).set({isBreakGlass:y,updatedAt:new Date,updatedBy:w}).output().where(C(S(i.tenantId,o),S(i.groupId,m)));return}await k.insert(i).output().values({tenantId:o,groupId:m,isBreakGlass:y,createdAt:new Date,createdBy:w})}),h(o)},async removeTenantMembershipGroup(o,m){P(o,"removeTenantMembershipGroup(tenantId)"),await a(),await p(async y=>{let k=(await y.select({groupId:i.groupId,isBreakGlass:i.isBreakGlass}).from(i).where(S(i.tenantId,o))).filter(x=>Number(x.groupId)!==m);if(k.length>0&&!k.some(x=>!!x.isBreakGlass))throw new Error(`Removing group ${m} would leave tenant ${o} with a membership proof but no break-glass path.`);await y.delete(i).where(C(S(i.tenantId,o),S(i.groupId,m)))}),h(o)},async setGroupActive(o,m,y,w){P(o,"setGroupActive(tenantId)"),await a(),await d.update(l).set({isActive:y,updatedAt:new Date,updatedBy:w}).output().where(C(S(l.id,m),S(l.tenantId,o)))},async listLedgerGroupIds(o,m){if(P(o,"listLedgerGroupIds(tenantId)"),!u.isEnabled)return[];await a();let y=await oe(d,{ledger:t,group:l},{tenantId:o,externalUserId:m.externalUserId?.trim()||null,email:Z(m.email)});return[...new Set(y.map(w=>w.groupId))].sort((w,k)=>w-k)},async isEmailAllowedByGroup(o){if(!u.isEnabled)return!1;let m=Z(o);if(m==="")return!1;await a();let[y]=await d.select({id:t.id}).from(t).innerJoin(r,S(r.groupId,t.groupId)).innerJoin(l,C(S(l.id,t.groupId),S(l.isActive,!0))).where(S(t.email,m));return!!y},async listAllowlistGroups(o){return P(o,"listAllowlistGroups(tenantId)"),await a(),(await d.select({id:r.id,groupId:r.groupId,groupKey:l.groupKey,groupName:l.name,description:r.description}).from(r).innerJoin(l,C(S(l.id,r.groupId),S(l.tenantId,o))).where(S(r.tenantId,o)).orderBy(Ce(l.name))).map(y=>({id:Number(y.id),type:"group",groupId:Number(y.groupId),groupKey:String(y.groupKey),groupName:String(y.groupName),...y.description?{description:String(y.description)}:{},owner:{kind:"tenant",tenantId:o},readOnly:!1}))},async addAllowlistGroup(o,m,y,w){P(o,"addAllowlistGroup(tenantId)"),await a(),await p(async k=>{let x=await T(k,o,m),[E]=await k.select({id:s.id}).from(s).where(C(S(s.groupId,m),S(s.tenantId,o)));if(!E)throw new K(u.refusals.allowlistNeedsLink(x));await k.insert(r).output().values({tenantId:o,groupId:m,description:y??null,createdAt:new Date,createdBy:w})})},async removeAllowlistGroup(o,m){P(o,"removeAllowlistGroup(tenantId)"),await a(),await d.delete(r).where(C(S(r.id,m),S(r.tenantId,o)))},async countProjectedMembers(o,m){P(o,"countProjectedMembers(tenantId)"),await a();let[y]=await d.select({n:Pe()}).from(I).innerJoin(l,C(S(l.id,I.groupId),S(l.tenantId,o))).where(S(I.groupId,m));return Number(y?.n??0)}}}import{normalizeAuthEmail as ce}from"@aiquants/auth-core";import{chunk as pe}from"@aiquants/mssql";import{and as tr,eq as _e,inArray as me}from"drizzle-orm";function ye(e,n,c){let u=e.get(n);u===void 0?e.set(n,new Set([c])):u.add(c)}function ge(e){let[n,...c]=e;if(n===void 0||c.length>0)throw new Error(`[@aiquants/auth-directory-drizzle] expected exactly one value, received ${e.size}`);return n}function it(e,n,c){let u=e,d=c.ensureConnection??(async()=>{}),{users:a,subjects:p}=n;return{async resolve(v){if(v.length===0)return[];await d();let f=[...new Set(v.map(b=>b.externalUserId?.trim()).filter(b=>!!b))],h=[...new Set(v.map(b=>ce(b.email)).filter(b=>b!==""))],s=new Map,t=new Set;for(let b of pe(f)){let T=await u.select({subject:p.subject,userId:p.userId,mappingActive:p.isActive,userActive:a.isActive}).from(p.table).innerJoin(a.table,_e(a.id,p.userId)).where(me(p.subject,b));for(let A of T){let D=String(A.subject);t.add(D),A.mappingActive&&A.userActive&&ye(s,D,Number(A.userId))}}let i=new Map,r=new Set;for(let b of pe(h)){let T=await u.select({id:a.id,email:a.email,isActive:a.isActive}).from(a.table).where(me(a.email,b));for(let A of T){let D=ce(String(A.email));r.add(D),A.isActive&&ye(i,D,Number(A.id))}}let g=[...i.values()].filter(b=>b.size===1).map(ge),l=new Map;for(let b of pe([...new Set(g)])){let T=await u.select({userId:p.userId,subject:p.subject}).from(p.table).where(tr(me(p.userId,b),_e(p.isActive,!0)));for(let A of T)ye(l,Number(A.userId),String(A.subject))}let I=b=>{let T=w=>({...b,resolvedUserId:w,unresolvedReason:null}),A=w=>({...b,resolvedUserId:null,unresolvedReason:w}),D=b.externalUserId?.trim(),o=ce(b.email),m=D?s.get(D):void 0;if(m!==void 0)return m.size===1?T(ge(m)):A("ambiguous");let y=i.get(o);if(y!==void 0){if(y.size>1)return A("ambiguous");let w=ge(y),k=l.get(w);return D&&k!==void 0&&!k.has(D)?A("claimedByAnother"):T(w)}return D&&t.has(D)||r.has(o)?A("deactivated"):A("unregistered")};return v.map(I)}}}import{normalizeAuthEmail as nr}from"@aiquants/auth-core";import{assertTenantId as or,assertTenantMatch as ir}from"@aiquants/authz-core";async function dt(e,n){let c=or(n.tenantId,"promoteLedgerForUser(tenantId)");if(!Number.isInteger(n.userId)||n.userId<=0)throw new TypeError(`[@aiquants/auth-directory-drizzle] userId must be a positive integer, received ${String(n.userId)}`);let u=nr(n.email),d=n.externalUserId?.trim()||null;if(d===null&&u==="")return{addedGroupIds:[],matchedLedgerIds:[]};let a=await e.findLedgerMatches({tenantId:c,externalUserId:d,email:u});if(a.length===0)return{addedGroupIds:[],matchedLedgerIds:[]};for(let h of a)ir(c,h.tenantId,"promoteLedgerForUser(findLedgerMatches row)");let p=[...new Set(a.map(h=>h.ledgerId))].sort((h,s)=>h-s),v=[...new Set(a.map(h=>h.groupId))].sort((h,s)=>h-s);return{addedGroupIds:[...await e.attachUserToGroups({tenantId:c,userId:n.userId,ledgerIds:p,groupIds:v})].sort((h,s)=>h-s),matchedLedgerIds:p}}import{assertTenantId as Le}from"@aiquants/authz-core";import{chunk as he,chunkByParameters as ar}from"@aiquants/mssql";import{inJsonList as sr}from"@aiquants/mssql/drizzle";import{and as be,eq as fe,inArray as we}from"drizzle-orm";var ur=4;function bt(e,n,c,u){let d=e,a=u.ensureConnection??(async()=>{}),p=N(d,u),v=u.now??(()=>new Date),f=n.TDGroupMemberEmail,{TMGroup:h,TDGroupMember:s}=c;return{async findLedgerMatches(t){return Le(t.tenantId,"findLedgerMatches(tenantId)"),await a(),oe(d,{ledger:f,group:h},t)},async attachUserToGroups({tenantId:t,userId:i,ledgerIds:r,groupIds:g}){return Le(t,"attachUserToGroups(tenantId)"),await a(),r.length===0?[]:p(async l=>{let I=new Set;for(let D of he(g)){let o=await l.select({id:h.id}).from(h).where(be(we(h.id,D),fe(h.tenantId,t),fe(h.isActive,!0)));for(let m of o)I.add(Number(m.id))}let b=g.filter(D=>I.has(D));if(b.length===0)return[];let T=new Set;for(let D of he(b)){let o=await l.select({groupId:s.groupId}).from(s).where(be(fe(s.userId,i),we(s.groupId,D)));for(let m of o)T.add(Number(m.groupId))}let A=b.filter(D=>!T.has(D));for(let D of he(r))await l.update(f).set({resolvedUserId:i}).output().where(be(we(f.id,D),sr(f.groupId,b)));if(A.length>0){let D=v();for(let o of ar(A,ur))await l.insert(s).output().values(o.map(m=>({groupId:m,userId:i,createdAt:D,createdBy:u.actor})))}return A})}}}import{AUTHZ_TENANT_COLUMN as B}from"@aiquants/authz-drizzle";import{bigint as _,bit as Ke,datetime2 as O,foreignKey as ae,int as Ie,mssqlSchema as dr,nvarchar as R,primaryKey as z,uniqueIndex as F}from"drizzle-orm/mssql-core";var ve="provider",vt=["TDGroupExternalLink","TDTenantMembershipGroup","TDAllowlistGroup","TDDirectorySyncRequest","TDDirectoryCatalog","TMDirectoryCatalogObservation","TMAllowlist"];function St(e,n){let c=dr(e),u=()=>({createdAt:O("created_at"),createdBy:R("created_by",{length:50}),updatedAt:O("updated_at"),updatedBy:R("updated_by",{length:50})}),d=()=>({createdAt:O("created_at"),createdBy:R("created_by",{length:50})}),a=c.table("TDGroupExternalLink",{id:_("id",{mode:"number"}).identity().notNull(),groupId:_("group_id",{mode:"number"}).notNull(),tenantId:R(B,{length:64}).notNull(),provider:R(ve,{length:32}).notNull(),externalId:R("external_id",{length:256}).notNull(),externalKey:R("external_key",{length:320}).notNull(),membershipMode:R("membership_mode",{length:16}).notNull(),isSyncEnabled:Ke("is_sync_enabled").notNull(),syncedAt:O("synced_at"),syncError:R("sync_error",{length:1e3}),vanishedObservations:Ie("vanished_observations").notNull(),...u()},r=>[z({name:`${e}_TDGroupExternalLink_pk`,columns:[r.id]}),F(`${e}_TDGroupExternalLink_group_uq`).on(r.groupId),F(`${e}_TDGroupExternalLink_external_uq`).on(r.tenantId,r.provider,r.externalId),ae({name:`${e}_TDGroupExternalLink_group_id_fk`,columns:[r.groupId],foreignColumns:[n.groupTable.id]})]),p=c.table("TDGroupMemberEmail",{id:_("id",{mode:"number"}).identity().notNull(),groupId:_("group_id",{mode:"number"}).notNull(),email:R("email",{length:320}).notNull(),externalUserId:R("external_user_id",{length:128}),resolvedUserId:_("resolved_user_id",{mode:"number"}),observedAt:O("observed_at").notNull(),...u()},r=>[z({name:`${e}_TDGroupMemberEmail_pk`,columns:[r.id]}),F(`${e}_TDGroupMemberEmail_group_email_uq`).on(r.groupId,r.email),ae({name:`${e}_TDGroupMemberEmail_group_id_fk`,columns:[r.groupId],foreignColumns:[n.groupTable.id]})]),v=c.table("TDTenantMembershipGroup",{tenantId:R(B,{length:64}).notNull(),groupId:_("group_id",{mode:"number"}).notNull(),isBreakGlass:Ke("is_break_glass").notNull(),...d()},r=>[z({name:`${e}_TDTenantMembershipGroup_pk`,columns:[r.tenantId,r.groupId]}),ae({name:`${e}_TDTenantMembershipGroup_group_id_fk`,columns:[r.groupId],foreignColumns:[n.groupTable.id]})]),f=c.table("TDAllowlistGroup",{id:_("id",{mode:"number"}).identity().notNull(),tenantId:R(B,{length:64}).notNull(),groupId:_("group_id",{mode:"number"}).notNull(),description:R("description",{length:512}),...d()},r=>[z({name:`${e}_TDAllowlistGroup_pk`,columns:[r.id]}),F(`${e}_TDAllowlistGroup_tenant_group_uq`).on(r.tenantId,r.groupId),ae({name:`${e}_TDAllowlistGroup_group_id_fk`,columns:[r.groupId],foreignColumns:[n.groupTable.id]})]),h=c.table("TDDirectorySyncRequest",{id:_("id",{mode:"number"}).identity().notNull(),tenantId:R(B,{length:64}).notNull(),groupId:_("group_id",{mode:"number"}),requestedBy:R("requested_by",{length:320}).notNull(),requestedAt:O("requested_at").notNull(),pickedAt:O("picked_at"),finishedAt:O("finished_at"),result:R("result",{length:32}),error:R("error",{length:2e3}),...d()},r=>[z({name:`${e}_TDDirectorySyncRequest_pk`,columns:[r.id]})]),s=c.table("TMAllowlist",{id:_("id",{mode:"number"}).identity().notNull(),tenantId:R(B,{length:64}).notNull(),pattern:R("pattern",{length:256}).notNull(),type:R("type",{length:32}).notNull(),description:R("description",{length:512}),...d()},r=>[z({name:`${e}_TMAllowlist_pk`,columns:[r.id]}),F(`${e}_TMAllowlist_pattern_uq`).on(r.tenantId,r.pattern)]),t=c.table("TDDirectoryCatalog",{id:_("id",{mode:"number"}).identity().notNull(),tenantId:R(B,{length:64}).notNull(),provider:R(ve,{length:32}).notNull(),externalId:R("external_id",{length:256}).notNull(),externalKey:R("external_key",{length:320}).notNull(),name:R("name",{length:256}).notNull(),description:R("description",{length:1e3}),memberCount:Ie("member_count"),observedAt:O("observed_at").notNull(),...d()},r=>[z({name:`${e}_TDDirectoryCatalog_pk`,columns:[r.id]}),F(`${e}_TDDirectoryCatalog_external_uq`).on(r.tenantId,r.provider,r.externalId)]),i=c.table("TMDirectoryCatalogObservation",{id:_("id",{mode:"number"}).identity().notNull(),tenantId:R(B,{length:64}).notNull(),provider:R(ve,{length:32}).notNull(),observedAt:O("observed_at").notNull(),entryCount:Ie("entry_count").notNull(),...u()},r=>[z({name:`${e}_TMDirectoryCatalogObservation_pk`,columns:[r.id]}),F(`${e}_TMDirectoryCatalogObservation_target_uq`).on(r.tenantId,r.provider)]);return{TDGroupExternalLink:a,TDGroupMemberEmail:p,TDTenantMembershipGroup:v,TDAllowlistGroup:f,TDDirectorySyncRequest:h,TDDirectoryCatalog:t,TMDirectoryCatalogObservation:i,TMAllowlist:s}}import{AUTH_DIRECTORY_UNRESOLVED_REASONS as lr,normalizeAuthEmail as cr}from"@aiquants/auth-core";import{assertBlastRadiusPolicy as pr,reconcile as mr}from"@aiquants/auth-directory-core";function se(e){if(e instanceof Error&&(e.name==="AbortError"||e.name==="TimeoutError"))return"timeout";let n=typeof e=="object"&&e!==null&&"status"in e?e.status:void 0;return typeof n!="number"?"unknown":n===401||n===403?"denied":n===429||n===408?"rateLimited":n>=500?"upstreamError":"unknown"}var Oe=e=>`${e.length} \u4EF6`,Se={denied:"\u8CC7\u683C\u60C5\u5831\u304C\u62D2\u5426\u3055\u308C\u307E\u3057\u305F",rateLimited:"\u547C\u3073\u51FA\u3057\u304C\u5236\u9650\u3055\u308C\u3066\u3044\u307E\u3059",upstreamError:"\u76F8\u624B\u5074\u304C\u5185\u90E8\u30A8\u30E9\u30FC\u3092\u8FD4\u3057\u307E\u3057\u305F",timeout:"\u5FDC\u7B54\u304C\u3042\u308A\u307E\u305B\u3093\u3067\u3057\u305F",localWriteError:"\u66F8\u304D\u8FBC\u307F\u304C\u62D2\u5426\u3055\u308C\u307E\u3057\u305F",unknown:"\u8A73\u7D30\u306F\u30B5\u30FC\u30D0\u30FC\u306E\u30ED\u30B0\u306B\u51FA\u3066\u3044\u307E\u3059"},Ue={upstreamUnreadable:e=>`\u4E0A\u6D41\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306E\u8AAD\u307F\u53D6\u308A\u306B\u5931\u6557\u3057\u307E\u3057\u305F (${Se[e]})\u3002`,localWriteFailed:e=>`\u30ED\u30FC\u30AB\u30EB\u3078\u306E\u66F8\u304D\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F (${Se[e]})\u3002`,resolverIncomplete:(e,n)=>`\u4E0A\u6D41\u304C ${e} \u4EF6\u306E\u30E1\u30F3\u30D0\u30FC\u3092\u8FD4\u3057\u307E\u3057\u305F\u304C\u3001\u7A81\u5408\u7D50\u679C\u306F ${n} \u4EF6\u3067\u3057\u305F\u3002\u4EF6\u6570\u304C\u6E1B\u308B\u7A81\u5408\u306F\u3001\u89E3\u6C7A\u3067\u304D\u306A\u304B\u3063\u305F\u4EBA\u7269\u3092\u300C\u4E0A\u6D41\u304B\u3089\u6D88\u3048\u305F\u300D\u3068\u3057\u3066\u524A\u9664\u3057\u3066\u3057\u307E\u3046\u305F\u3081\u4E2D\u65AD\u3057\u307E\u3057\u305F\u3002\u89E3\u6C7A\u3067\u304D\u306A\u3044\u30E1\u30F3\u30D0\u30FC\u306F\u7701\u7565\u305B\u305A resolvedUserId: null \u3067\u8FD4\u3059\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`,groupMissing:(e,n,c)=>`\u4E0A\u6D41\u3067\u30B0\u30EB\u30FC\u30D7\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (${e})\u3002\u9023\u7D9A ${n} \u56DE\u76EE\u3067\u3059\u3002${c} \u56DE\u306B\u9054\u3057\u305F\u6642\u70B9\u3067\u30B0\u30EB\u30FC\u30D7\u3092\u7121\u52B9\u5316\u3057\u307E\u3059\u3002`,groupVanished:(e,n)=>`\u4E0A\u6D41\u3067\u30B0\u30EB\u30FC\u30D7\u304C ${n} \u56DE\u9023\u7D9A\u3067\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F (${e})\u3002\u30B0\u30EB\u30FC\u30D7\u3092\u7121\u52B9\u5316\u3057\u307E\u3057\u305F\u3002\u4E0A\u6D41\u304C\u5FA9\u65E7\u3059\u308C\u3070\u6B21\u56DE\u306E\u540C\u671F\u3067\u81EA\u52D5\u7684\u306B\u518D\u6709\u52B9\u5316\u3055\u308C\u307E\u3059\u3002`,planRefused:e=>{if(e.reason==="invalid-upstream-data")return`\u540C\u671F\u3092\u4E2D\u65AD\u3057\u307E\u3057\u305F: \u4E0A\u6D41\u306E\u30E1\u30F3\u30D0\u30FC\u4E00\u89A7\u306B\u6271\u3048\u306A\u3044\u30A2\u30C9\u30EC\u30B9\u304C ${Oe(e.offendingEmails)} \u542B\u307E\u308C\u3066\u3044\u307E\u3059\u3002\u5BFE\u8C61\u306F\u30B5\u30FC\u30D0\u30FC\u306E\u30ED\u30B0\u306B\u51FA\u3066\u3044\u307E\u3059\u3002`;let n=e.axis==="membership"?"\u6A29\u9650\u3092\u5931\u3046\u4EBA\u6570":"\u30B5\u30A4\u30F3\u30A4\u30F3\u3067\u304D\u306A\u304F\u306A\u308B\u4EBA\u6570",c=e.axis==="membership"?e.withheldProjectionRemoves.map(String):e.withheldLedgerDeletes;return[`\u540C\u671F\u3092\u4E2D\u65AD\u3057\u307E\u3057\u305F: ${n}\u304C ${e.population} \u4EBA\u4E2D ${e.removalCount} \u4EBA\u306B\u306A\u308A\u307E\u3059`,`(\u4E0A\u9650: ${e.limits.maxRemovalCount} \u4EF6 / \u6BD4\u7387 ${e.limits.maxRemovalRatio})\u3002`,`\u5DEE\u3057\u6B62\u3081\u305F\u5BFE\u8C61: ${Oe(c)} (\u4E00\u89A7\u306F\u30B5\u30FC\u30D0\u30FC\u306E\u30ED\u30B0\u306B\u51FA\u3066\u3044\u307E\u3059)\u3002`,"\u4E0A\u6D41\u5074\u306E\u8A2D\u5B9A\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u610F\u56F3\u3057\u305F\u5909\u66F4\u3067\u3042\u308C\u3070\u3001\u3053\u306E\u8EF8\u306E\u4E0A\u9650\u3060\u3051\u3092\u4E00\u6642\u7684\u306B\u5F15\u304D\u4E0A\u3052\u3066\u518D\u5B9F\u884C\u3057\u307E\u3059\u3002"].join(" ")},removalWritesFailed:e=>`${e.length} \u4EF6\u306E\u6240\u5C5E\u89E3\u9664\u304C\u66F8\u304D\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F (${[...new Set(e.map(n=>Se[n.cause]))].join(" / ")})\u3002\u6B21\u56DE\u306E\u5B9F\u884C\u3067\u518D\u8A66\u884C\u3057\u307E\u3059\u3002`,removalsRefused:e=>`${e.length} \u4EF6\u306E\u6240\u5C5E\u89E3\u9664\u304C\u6700\u5F8C\u306E\u7BA1\u7406\u8005\u30AC\u30FC\u30C9\u306B\u3088\u308A\u62D2\u5426\u3055\u308C\u307E\u3057\u305F (\u30E6\u30FC\u30B6\u30FC ${e.map(n=>n.userId).join(", ")})\u3002\u6B8B\u308A\u306E\u5909\u66F4\u306F\u9069\u7528\u6E08\u307F\u3067\u3059\u3002`,foreignProvider:(e,n)=>`\u3053\u306E\u30EA\u30F3\u30AF\u306E\u4F9B\u7D66\u5143\u306F ${e} \u3067\u3059\u304C\u3001\u5B9F\u884C\u4E2D\u306E\u540C\u671F\u306F ${n} \u7528\u3067\u3059\u3002\u5BFE\u5FDC\u3059\u308B\u540C\u671F\u30B8\u30E7\u30D6\u304B\u3089\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002`};var De=["aborted","missing","vanished","upstream-failed","local-failed","foreign-provider","not-converged"],$e=[...De,"already-vanished"];function yr(e){let n=new Map;for(let d of e)d.resolvedUserId===null&&n.set(cr(d.email),d.unresolvedReason);let c=[...n.values()],u=lr.map(d=>[d,c.filter(a=>a===d).length]).filter(([,d])=>d>0).map(([d,a])=>`${d}: ${a}`).join(", ");return{total:n.size,breakdown:u}}function Et(e){let{directory:n,repository:c,policy:u,now:d,logger:a}=e,p=e.messages??Ue;if(pr(u),!Number.isInteger(e.vanishedThreshold)||e.vanishedThreshold<1)throw new TypeError(`[@aiquants/auth-directory-drizzle] vanishedThreshold must be a positive integer, received ${String(e.vanishedThreshold)}`);if(typeof e.provider!="string"||e.provider.trim()==="")throw new TypeError("[@aiquants/auth-directory-drizzle] provider must be a non-empty string so links of other providers can be told apart");async function v(t,i){try{await c.recordFailure(t,i)}catch(r){a.error(`[auth-directory] could not record the failure for ${t.externalKey}:`,r)}}async function f(t){try{let i=await n.getGroup(t.externalId);return i===null?{kind:"missing"}:{kind:"ok",group:i,members:await n.listGroupMembers(i,t.membershipMode)}}catch(i){return{kind:"failed",error:i instanceof Error?i:new Error(String(i))}}}async function h(t){if(t.vanishedObservations>=e.vanishedThreshold&&!t.isGroupActive)return a.warn(`[auth-directory] ${t.externalKey} still missing upstream (already deactivated after ${t.vanishedObservations} absences)`),{kind:"already-vanished",link:t};let i=await c.recordVanishedObservation(t,p.groupMissing(t.externalKey,t.vanishedObservations+1,e.vanishedThreshold));return i<e.vanishedThreshold?(a.warn(`[auth-directory] ${t.externalKey} missing upstream (${i}/${e.vanishedThreshold})`),{kind:"missing",link:t,observations:i}):(await c.markGroupInactive(t),await v(t,p.groupVanished(t.externalKey,i)),a.warn(`[auth-directory] ${t.externalKey} deactivated after ${i} consecutive absences`),{kind:"vanished",link:t})}async function s(t){if(t.provider!==e.provider)return await v(t,p.foreignProvider(t.provider,e.provider)),a.warn(`[auth-directory] skipping ${t.externalKey}: belongs to provider ${t.provider}`),{kind:"foreign-provider",link:t};let i=await f(t);if(i.kind==="missing")return h(t);if(i.kind==="failed")return await v(t,p.upstreamUnreadable(se(i.error))),a.error(`[auth-directory] upstream read failed for ${t.externalKey}:`,i.error),{kind:"upstream-failed",link:t,error:i.error};try{await c.resolveExternalIdentity(t,i.group);let[r,g]=await Promise.all([c.loadGroupSnapshot(t),c.resolveMembers(i.members)]);if(g.length!==i.members.length){let D=p.resolverIncomplete(i.members.length,g.length);return await v(t,D),a.error(`[auth-directory] ${D}`),{kind:"local-failed",link:t,error:new Error(D)}}let l=mr(r,g,u);if(l.aborted){await v(t,p.planRefused(l.aborted));let D=l.aborted.reason==="invalid-upstream-data"?l.aborted.offendingEmails:l.aborted.axis==="membership"?l.aborted.withheldProjectionRemoves.map(String):l.aborted.withheldLedgerDeletes;return a.warn(`[auth-directory] plan refused for ${t.externalKey}: ${l.aborted.reason}; withheld: ${D.join(", ")}`),{kind:"aborted",link:t,plan:l}}let I=l.ledgerUpserts.length>0||l.ledgerDeletes.length>0||l.projectionAdds.length>0||l.projectionRemoves.length>0,b=d(),T={refusals:[],failures:[]};I&&(T=await c.applyPlan(t,l,b)),await c.recordSuccess(t,i.group,b),T.refusals.length>0&&(await v(t,p.removalsRefused(T.refusals)),a.warn(`[auth-directory] ${T.refusals.length} removal(s) refused for ${t.externalKey}: users ${T.refusals.map(D=>D.userId).join(", ")}`)),T.failures.length>0&&(await v(t,p.removalWritesFailed(T.failures)),a.warn(`[auth-directory] ${T.failures.length} removal(s) could not be written for ${t.externalKey}: users ${T.failures.map(D=>D.userId).join(", ")}`));let A=yr(g);return A.total>0&&a.info(`[auth-directory] ${t.externalKey}: ${A.total} upstream member(s) are not linked to a local user (${A.breakdown})`),T.refusals.length>0||T.failures.length>0?{kind:"not-converged",link:t,plan:l,refusals:T.refusals,failures:T.failures}:I?{kind:"applied",link:t,plan:l}:{kind:"unchanged",link:t}}catch(r){let g=r instanceof Error?r:new Error(String(r));return await v(t,p.localWriteFailed("localWriteError")),a.error(`[auth-directory] local write failed for ${t.externalKey}:`,g),{kind:"local-failed",link:t,error:g}}}return{syncOne:s,async run(t){let i=await c.listSyncableLinks(t),r=[];for(let I of i)try{r.push(await s(I))}catch(b){let T=b instanceof Error?b:new Error(String(b));a.error(`[auth-directory] ${I.externalKey} ended abnormally:`,T),r.push({kind:"local-failed",link:I,error:T})}let g=r.filter(I=>$e.includes(I.kind)).length,l=r.filter(I=>De.includes(I.kind)).length;return a.info(`[auth-directory] run finished: ${r.length} link(s), ${g} unhealthy (${l} new)`),{outcomes:r,unhealthyCount:g,newlyUnhealthyCount:l}}}}import{assertTenantId as gr}from"@aiquants/authz-core";import{toInstant as hr}from"@aiquants/mssql";import{and as ue,asc as Ne,eq as H,isNull as de}from"drizzle-orm";var br="The job that claimed this request ended without recording an outcome; whether the synchronization ran is unknown.",Ae=2e3;function Lt(e,n,c){let u=e,d=c.ensureConnection??(async()=>{}),a=n.TDDirectorySyncRequest,p=gr(c.tenantId,"createSyncRequestQueue(tenantId)"),v=c.maxErrorLength??Ae;if(!Number.isInteger(v)||v<1||v>Ae)throw new TypeError(`[@aiquants/auth-directory-drizzle] maxErrorLength must be an integer within 1..${Ae}, received ${String(c.maxErrorLength)}`);let f={async claim(){await d();let h=await u.select({id:a.id}).from(a).where(ue(H(a.tenantId,p),de(a.pickedAt))).orderBy(Ne(a.id));for(let s of h){let i=(await u.update(a).set({pickedAt:c.now()}).output().where(ue(H(a.tenantId,p),H(a.id,Number(s.id)),de(a.pickedAt))))?.[0];if(!i)continue;if(i.requestedBy===void 0||i.requestedBy===null)throw new TypeError(`[@aiquants/auth-directory-drizzle] the driver returned a row without requestedBy for request ${String(i.id)}`);let r=i.groupId??null;return{id:Number(i.id),tenantId:p,groupId:r===null?null:Number(r),requestedBy:String(i.requestedBy)}}return null},async finish(h,s){await d();let t=s.error?s.error.slice(0,v):null;return!!(await u.update(a).set({finishedAt:c.now(),result:s.result,error:t}).output().where(ue(H(a.tenantId,p),H(a.id,h),de(a.finishedAt))))?.length},async listStranded(h){await d();let s=await u.select({id:a.id,pickedAt:a.pickedAt,requestedAt:a.requestedAt}).from(a).where(ue(H(a.tenantId,p),de(a.finishedAt))).orderBy(Ne(a.id)),t=i=>{let r=hr(i);return Number.isNaN(r.getTime())||r<h};return{stranded:s.filter(i=>i.pickedAt!==null&&t(i.pickedAt)).map(i=>Number(i.id)),unclaimed:s.filter(i=>i.pickedAt===null&&t(i.requestedAt)).map(i=>Number(i.id))}},async abandonStranded(h){let{stranded:s}=await f.listStranded(h),t=[];for(let i of s)try{await f.finish(i,{result:"failed",error:br})&&t.push(i)}catch(r){c.onCloseFailed?.(i,r)}return t}};return f}import{normalizeAuthEmail as le}from"@aiquants/auth-core";import{assertTenantId as qe,assertTenantMatch as fr}from"@aiquants/authz-core";import{chunk as ze,chunkByParameters as Be}from"@aiquants/mssql";import{notInJsonList as wr}from"@aiquants/mssql/drizzle";import{and as L,eq as M,inArray as Te,isNull as Ir,or as vr}from"drizzle-orm";var Sr=7,Dr=4,Fe=1e3;function Ar(e){if(!(e instanceof Error))return!1;let n=e;return n.number===void 0&&n.code===void 0&&e.name==="Error"}function jt(e,n,c,u){let d=e,a=u.ensureConnection??(async()=>{}),p=N(d,u),v=u.now??(()=>new Date),{TDGroupExternalLink:f,TDGroupMemberEmail:h}=n,{TMGroup:s,TDGroupMember:t}=c,i=u.actor;return{async listSyncableLinks(r){await a();let g=qe(r.tenantId,"listSyncableLinks(tenantId)"),l=[M(f.isSyncEnabled,!0),M(f.tenantId,g)];return r.groupId!==void 0&&l.push(M(f.groupId,r.groupId)),(await d.select({linkId:f.id,groupId:f.groupId,linkTenantId:f.tenantId,groupTenantId:s.tenantId,groupKey:s.groupKey,isGroupActive:s.isActive,provider:f.provider,externalId:f.externalId,externalKey:f.externalKey,membershipMode:f.membershipMode,vanishedObservations:f.vanishedObservations}).from(f).innerJoin(s,L(M(s.id,f.groupId),M(s.tenantId,g))).where(L(...l))).map(b=>(fr(String(b.groupTenantId),String(b.linkTenantId),"listSyncableLinks(link.tenantId vs TMGroup.tenant_id)"),{linkId:Number(b.linkId),groupId:Number(b.groupId),groupKey:String(b.groupKey),tenantId:String(b.groupTenantId),provider:String(b.provider),externalId:String(b.externalId),externalKey:String(b.externalKey),membershipMode:Q(String(b.membershipMode)),isGroupActive:!!b.isGroupActive,vanishedObservations:Number(b.vanishedObservations??0)}))},async loadGroupSnapshot(r){await a();let[g,l]=await Promise.all([d.select({email:h.email,externalUserId:h.externalUserId,resolvedUserId:h.resolvedUserId}).from(h).where(M(h.groupId,r.groupId)),d.select({userId:t.userId}).from(t).where(M(t.groupId,r.groupId))]);return{ledger:g.map(I=>({email:String(I.email),...I.externalUserId?{externalUserId:String(I.externalUserId)}:{},resolvedUserId:I.resolvedUserId===null?null:Number(I.resolvedUserId)})),projectedUserIds:l.map(I=>Number(I.userId))}},resolveMembers(r){return u.memberResolver.resolve(r)},async applyPlan(r,g,l){qe(r.tenantId,"applyPlan(link.tenantId)"),await a(),(g.ledgerUpserts.length>0||g.projectionAdds.length>0)&&await p(async A=>{let D=g.ledgerUpserts.map(y=>({...y,email:le(y.email)})),o=new Map;for(let y of ze(D.map(w=>w.email))){let w=await A.select({id:h.id,email:h.email}).from(h).where(L(M(h.groupId,r.groupId),Te(h.email,y)));for(let k of w)o.set(String(k.email),Number(k.id))}let m=D.filter(y=>!o.has(y.email));for(let y of Be(m,Sr))await A.insert(h).output().values(y.map(w=>({groupId:r.groupId,email:w.email,externalUserId:w.externalUserId??null,resolvedUserId:w.resolvedUserId,observedAt:l,createdAt:l,createdBy:i})));for(let y of D){let w=o.get(y.email);w!==void 0&&await A.update(h).set({externalUserId:y.externalUserId??null,resolvedUserId:y.resolvedUserId,observedAt:l,updatedAt:l,updatedBy:i}).output().where(M(h.id,w))}for(let y of Be(g.projectionAdds,Dr)){let w=new Set((await A.select({userId:t.userId}).from(t).where(L(M(t.groupId,r.groupId),Te(t.userId,y)))).map(x=>Number(x.userId))),k=y.filter(x=>!w.has(x));k.length>0&&await A.insert(t).output().values(k.map(x=>({groupId:r.groupId,userId:x,createdAt:l,createdBy:i})))}});let I=[],b=[];for(let A of g.projectionRemoves)try{await u.lockoutGuard.guardedWrite(r.tenantId,{removeGroupMembership:{groupId:r.groupId,userId:A}},"remove-group-member",D=>D.delete(t).where(L(M(t.groupId,r.groupId),M(t.userId,A))))}catch(D){Ar(D)?I.push({userId:A,applied:!1,refusal:D.message}):b.push({userId:A,cause:se(D)})}let T=I.map(A=>A.userId);return g.ledgerDeletes.length>0&&await p(async A=>{for(let D of ze(g.ledgerDeletes)){let o=T.length>0?vr(Ir(h.resolvedUserId),wr(h.resolvedUserId,T)):void 0;await A.delete(h).where(L(M(h.groupId,r.groupId),Te(h.email,D),...o?[o]:[]))}}),{refusals:I,failures:b}},async resolveExternalIdentity(r,g){await a(),!(r.externalId===g.externalId&&r.externalKey===le(g.externalKey))&&await d.update(f).set({externalId:g.externalId,externalKey:le(g.externalKey)}).output().where(L(M(f.id,r.linkId),M(f.tenantId,r.tenantId)))},async recordSuccess(r,g,l){await a(),await p(async I=>{await I.update(f).set({syncedAt:l,syncError:null,vanishedObservations:0,externalId:g.externalId,externalKey:le(g.externalKey),updatedAt:l,updatedBy:i}).output().where(L(M(f.id,r.linkId),M(f.tenantId,r.tenantId))),r.isGroupActive||await I.update(s).set({isActive:!0,updatedAt:l,updatedBy:i}).output().where(L(M(s.id,r.groupId),M(s.tenantId,r.tenantId)))})},async recordFailure(r,g){await a(),await d.update(f).set({syncError:g.slice(0,Fe),updatedAt:v(),updatedBy:i}).output().where(L(M(f.id,r.linkId),M(f.tenantId,r.tenantId)))},async recordVanishedObservation(r,g){await a();let l=r.vanishedObservations+1;return await d.update(f).set({vanishedObservations:l,syncError:g.slice(0,Fe),updatedAt:v(),updatedBy:i}).output().where(L(M(f.id,r.linkId),M(f.tenantId,r.tenantId))),l},async markGroupInactive(r){await a(),await d.update(s).set({isActive:!1,updatedAt:v(),updatedBy:i}).output().where(L(M(s.id,r.groupId),M(s.tenantId,r.tenantId),M(s.isActive,!0)))}}}export{ve as AUTH_DIRECTORY_PROVIDER_COLUMN,vt as AUTH_DIRECTORY_TENANT_SCOPED_TABLES,K as DirectoryOperatorError,Me as MEMBERSHIP_MODES,De as NEWLY_UNHEALTHY_SYNC_OUTCOMES,br as STRANDED_REASON,$e as UNHEALTHY_SYNC_OUTCOMES,$r as assertGroupMayBeLinked,Q as assertMembershipMode,se as classifySyncFailure,Cr as createDirectoryCatalogReader,Gr as createDirectoryCatalogWriter,et as createDirectoryLinkStore,Et as createDirectorySync,jt as createDirectorySyncRepository,it as createOpenidMemberResolver,bt as createPromotionRepository,Lt as createSyncRequestQueue,Xr as defaultDirectoryLinkRefusals,Or as defaultExternalGroupGuardMessage,Ue as defaultSyncMessages,St as defineAuthDirectorySchema,oe as findLedgerRowsForIdentity,dt as promoteLedgerForUser,N as transactionRunner,Ur as withExternalGroupGuards};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiquants/auth-directory-drizzle",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Drizzle (mssql) adapter for @aiquants/auth-directory-core: the identity-side tables (external group links, upstream member ledger, tenant membership groups, login allowlist), the synchronization writer that runs inside the authz lockout guard, and an admin-store decorator that keeps admin roles off externally-sourced groups.",
5
5
  "sideEffects": false,
6
6
  "main": "dist/index.js",
@@ -32,17 +32,17 @@
32
32
  },
33
33
  "license": "MIT",
34
34
  "peerDependencies": {
35
- "@aiquants/auth-core": "^0.6.1",
36
- "@aiquants/auth-directory-core": "^0.2.1",
35
+ "@aiquants/auth-core": "^0.7.0",
36
+ "@aiquants/auth-directory-core": "^0.3.0",
37
37
  "@aiquants/authz-core": "^0.6.1",
38
38
  "@aiquants/authz-drizzle": "^1.3.1",
39
39
  "drizzle-orm": ">=1.0.0-beta.4 <2.0.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@aiquants/auth-core": "0.6.1",
43
- "@aiquants/auth-directory-core": "0.2.1",
42
+ "@aiquants/auth-core": "0.7.0",
43
+ "@aiquants/auth-directory-core": "0.3.0",
44
44
  "@aiquants/authz-core": "0.6.1",
45
- "@aiquants/authz-drizzle": "1.3.1",
45
+ "@aiquants/authz-drizzle": "1.4.0",
46
46
  "@types/node": "^25.9.1",
47
47
  "drizzle-orm": "1.0.0-rc.4",
48
48
  "rimraf": "^6.1.2",
@@ -68,9 +68,9 @@
68
68
  "dev": "tsup --watch",
69
69
  "typecheck": "tsc --noEmit",
70
70
  "clean": "rimraf dist",
71
- "publish:patch": "pnpm run typecheck && pnpm run --if-present test && pnpm run build && node ../../.config/scripts/check-publish-leaks.mjs && pnpm version patch --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
72
- "publish:minor": "pnpm run typecheck && pnpm run --if-present test && pnpm run build && node ../../.config/scripts/check-publish-leaks.mjs && pnpm version minor --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
73
- "publish:major": "pnpm run typecheck && pnpm run --if-present test && pnpm run build && node ../../.config/scripts/check-publish-leaks.mjs && pnpm version major --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
71
+ "publish:patch": "pnpm run typecheck && pnpm run --if-present test && pnpm run build && node ../../.config/scripts/check-publish-leaks.mjs && node ../../.config/scripts/check-peer-floors.mjs --bump patch && pnpm version patch --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
72
+ "publish:minor": "pnpm run typecheck && pnpm run --if-present test && pnpm run build && node ../../.config/scripts/check-publish-leaks.mjs && node ../../.config/scripts/check-peer-floors.mjs --bump minor && pnpm version minor --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
73
+ "publish:major": "pnpm run typecheck && pnpm run --if-present test && pnpm run build && node ../../.config/scripts/check-publish-leaks.mjs && node ../../.config/scripts/check-peer-floors.mjs --bump major && pnpm version major --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
74
74
  "lint": "biome lint src/",
75
75
  "check": "biome check src/",
76
76
  "check:fix": "biome check --write src/",