@open-mercato/core 0.6.4-develop.4110.1.836aafde58 → 0.6.4-develop.4121.1.0d7f20d229

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.
@@ -34,25 +34,26 @@ async function resolveCanonicalStaffAuthContext(em, auth) {
34
34
  return null;
35
35
  }
36
36
  }
37
- if (sessionId !== null) {
38
- const session = await findOneWithDecryption(em, Session, { id: sessionId, deletedAt: null });
39
- if (!session) return null;
40
- if (session.expiresAt.getTime() < Date.now()) return null;
41
- }
42
- const user = await findOneWithDecryption(
37
+ const sessionPromise = sessionId !== null ? findOneWithDecryption(em, Session, { id: sessionId, deletedAt: null }) : Promise.resolve(null);
38
+ const userPromise = findOneWithDecryption(
43
39
  em,
44
40
  User,
45
41
  { id: subjectId, deletedAt: null },
46
42
  void 0,
47
43
  { tenantId: actorTenantId, organizationId: actorOrganizationId }
48
44
  );
45
+ const [session, user] = await Promise.all([sessionPromise, userPromise]);
46
+ if (sessionId !== null) {
47
+ if (!session) return null;
48
+ if (session.expiresAt.getTime() < Date.now()) return null;
49
+ }
49
50
  if (!user) return null;
50
51
  const currentTenantId = normalizeScopeId(user.tenantId ?? null);
51
52
  const currentOrganizationId = normalizeScopeId(user.organizationId ?? null);
52
53
  if (currentTenantId === INVALID_SCOPE || currentOrganizationId === INVALID_SCOPE || currentTenantId !== actorTenantId || currentOrganizationId !== actorOrganizationId) {
53
54
  return null;
54
55
  }
55
- const links = currentTenantId ? await findWithDecryption(
56
+ const linksPromise = currentTenantId ? findWithDecryption(
56
57
  em,
57
58
  UserRole,
58
59
  {
@@ -62,10 +63,12 @@ async function resolveCanonicalStaffAuthContext(em, auth) {
62
63
  },
63
64
  { populate: ["role"] },
64
65
  { tenantId: currentTenantId, organizationId: currentOrganizationId }
65
- ) : [];
66
+ ) : Promise.resolve([]);
67
+ const userAclSuperAdminPromise = currentTenantId ? userAclGrantsSuperAdmin(em, user.id, currentTenantId, currentOrganizationId) : Promise.resolve(false);
68
+ const [links, userAclSuperAdmin] = await Promise.all([linksPromise, userAclSuperAdminPromise]);
66
69
  const linkedRoles = links.map((link) => link.role).filter((role) => !!role);
67
70
  const roles = linkedRoles.map((role) => role.name).filter((name) => typeof name === "string" && name.trim().length > 0);
68
- const isSuperAdmin = currentTenantId ? await hasSuperAdminFlag(em, user.id, linkedRoles, currentTenantId, currentOrganizationId) : false;
71
+ const isSuperAdmin = currentTenantId ? userAclSuperAdmin || await roleAclGrantsSuperAdmin(em, linkedRoles, currentTenantId, currentOrganizationId) : false;
69
72
  return {
70
73
  ...auth,
71
74
  sub: user.id,
@@ -75,7 +78,7 @@ async function resolveCanonicalStaffAuthContext(em, auth) {
75
78
  isSuperAdmin
76
79
  };
77
80
  }
78
- async function hasSuperAdminFlag(em, userId, linkedRoles, tenantId, organizationId) {
81
+ async function userAclGrantsSuperAdmin(em, userId, tenantId, organizationId) {
79
82
  const userAcl = await findOneWithDecryption(
80
83
  em,
81
84
  UserAcl,
@@ -88,9 +91,9 @@ async function hasSuperAdminFlag(em, userId, linkedRoles, tenantId, organization
88
91
  void 0,
89
92
  { tenantId, organizationId }
90
93
  );
91
- if (userAcl && userAcl.isSuperAdmin === true) {
92
- return true;
93
- }
94
+ return !!(userAcl && userAcl.isSuperAdmin === true);
95
+ }
96
+ async function roleAclGrantsSuperAdmin(em, linkedRoles, tenantId, organizationId) {
94
97
  const roleIds = Array.from(
95
98
  new Set(
96
99
  linkedRoles.map((role) => role?.id ? String(role.id) : null).filter((id) => typeof id === "string" && id.length > 0)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/auth/lib/sessionIntegrity.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Role, RoleAcl, Session, User, UserAcl, UserRole } from '@open-mercato/core/modules/auth/data/entities'\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\nconst INVALID_SCOPE = Symbol('invalid-scope')\n\ntype NormalizedScopeId = string | null | typeof INVALID_SCOPE\n\nfunction normalizeScopeId(value: unknown): NormalizedScopeId {\n if (value === null || value === undefined) return null\n if (typeof value !== 'string') return INVALID_SCOPE\n const trimmed = value.trim()\n if (!trimmed) return null\n return UUID_RE.test(trimmed) ? trimmed : INVALID_SCOPE\n}\n\nfunction resolveActorTenantId(auth: NonNullable<AuthContext>): NormalizedScopeId {\n const actorTenantId = (auth as { actorTenantId?: unknown }).actorTenantId\n return normalizeScopeId(actorTenantId ?? auth.tenantId ?? null)\n}\n\nfunction resolveActorOrganizationId(auth: NonNullable<AuthContext>): NormalizedScopeId {\n const actorOrgId = (auth as { actorOrgId?: unknown }).actorOrgId\n return normalizeScopeId(actorOrgId ?? auth.orgId ?? null)\n}\n\nexport async function resolveCanonicalStaffAuthContext(\n em: EntityManager,\n auth: AuthContext,\n): Promise<AuthContext> {\n if (!auth) return null\n if (auth.isApiKey) return auth\n\n const subjectId = normalizeScopeId(auth.sub)\n const actorTenantId = resolveActorTenantId(auth)\n const actorOrganizationId = resolveActorOrganizationId(auth)\n if (\n subjectId === INVALID_SCOPE ||\n actorTenantId === INVALID_SCOPE ||\n actorOrganizationId === INVALID_SCOPE\n ) {\n return null\n }\n\n // Session binding: when the JWT carries an `sid` claim, require the referenced session to\n // still exist (not soft-deleted, not expired). This is what makes logout / password-reset\n // actually invalidate an already-issued JWT.\n //\n // Legacy tokens (pre-migration, without `sid`) are allowed through during the grace period\n // (controlled by JWT_LEGACY_GRACE_MINUTES) so that rolling deployments don't force-logout\n // every user. Once the grace period expires these tokens will fail signature verification\n // in `verifyJwt` before reaching this point.\n const sessionId = normalizeScopeId(typeof auth.sid === 'string' ? auth.sid : null)\n if (sessionId === INVALID_SCOPE) return null\n if (sessionId === null) {\n // Legacy token without sid \u2014 allow only if it was verified via the legacy fallback path.\n // The `_legacyToken` flag is set by `verifyJwt` when a token passes raw-secret verification\n // but fails audience-derived verification. Without this flag, reject.\n if ((auth as Record<string, unknown>)._legacyToken === true) {\n // Allow through without session validation \u2014 the token will expire naturally\n } else {\n return null\n }\n }\n if (sessionId !== null) {\n const session = await findOneWithDecryption(em, Session, { id: sessionId, deletedAt: null })\n if (!session) return null\n if (session.expiresAt.getTime() < Date.now()) return null\n }\n\n const user = await findOneWithDecryption(\n em,\n User,\n { id: subjectId, deletedAt: null },\n undefined,\n { tenantId: actorTenantId, organizationId: actorOrganizationId },\n )\n if (!user) return null\n\n const currentTenantId = normalizeScopeId(user.tenantId ?? null)\n const currentOrganizationId = normalizeScopeId(user.organizationId ?? null)\n if (\n currentTenantId === INVALID_SCOPE ||\n currentOrganizationId === INVALID_SCOPE ||\n currentTenantId !== actorTenantId ||\n currentOrganizationId !== actorOrganizationId\n ) {\n return null\n }\n\n const links = currentTenantId\n ? await findWithDecryption(\n em,\n UserRole,\n {\n user: user.id,\n deletedAt: null,\n role: { tenantId: currentTenantId, deletedAt: null } as unknown as Role,\n } as never,\n { populate: ['role'] },\n { tenantId: currentTenantId, organizationId: currentOrganizationId },\n )\n : []\n\n const linkedRoles = links\n .map((link) => link.role)\n .filter((role): role is Role => !!role)\n\n const roles = linkedRoles\n .map((role) => role.name)\n .filter((name): name is string => typeof name === 'string' && name.trim().length > 0)\n\n const isSuperAdmin = currentTenantId\n ? await hasSuperAdminFlag(em, user.id, linkedRoles, currentTenantId, currentOrganizationId)\n : false\n\n return {\n ...auth,\n sub: user.id,\n tenantId: currentTenantId,\n orgId: currentOrganizationId,\n roles,\n isSuperAdmin,\n }\n}\n\nasync function hasSuperAdminFlag(\n em: EntityManager,\n userId: string,\n linkedRoles: Role[],\n tenantId: string,\n organizationId: string | null,\n): Promise<boolean> {\n const userAcl = await findOneWithDecryption(\n em,\n UserAcl,\n {\n user: userId,\n tenantId,\n isSuperAdmin: true,\n deletedAt: null,\n } as never,\n undefined,\n { tenantId, organizationId },\n )\n if (userAcl && (userAcl as { isSuperAdmin?: boolean }).isSuperAdmin === true) {\n return true\n }\n\n const roleIds = Array.from(\n new Set(\n linkedRoles\n .map((role) => (role?.id ? String(role.id) : null))\n .filter((id): id is string => typeof id === 'string' && id.length > 0),\n ),\n )\n if (!roleIds.length) return false\n\n const roleAcl = await findOneWithDecryption(\n em,\n RoleAcl,\n {\n tenantId,\n isSuperAdmin: true,\n deletedAt: null,\n role: { $in: roleIds },\n } as never,\n undefined,\n { tenantId, organizationId },\n )\n return !!(roleAcl && (roleAcl as { isSuperAdmin?: boolean }).isSuperAdmin === true)\n}\n\nexport async function isAuthContextValid(\n em: EntityManager,\n auth: AuthContext,\n): Promise<boolean> {\n return (await resolveCanonicalStaffAuthContext(em, auth)) !== null\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Role, RoleAcl, Session, User, UserAcl, UserRole } from '@open-mercato/core/modules/auth/data/entities'\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\nconst INVALID_SCOPE = Symbol('invalid-scope')\n\ntype NormalizedScopeId = string | null | typeof INVALID_SCOPE\n\nfunction normalizeScopeId(value: unknown): NormalizedScopeId {\n if (value === null || value === undefined) return null\n if (typeof value !== 'string') return INVALID_SCOPE\n const trimmed = value.trim()\n if (!trimmed) return null\n return UUID_RE.test(trimmed) ? trimmed : INVALID_SCOPE\n}\n\nfunction resolveActorTenantId(auth: NonNullable<AuthContext>): NormalizedScopeId {\n const actorTenantId = (auth as { actorTenantId?: unknown }).actorTenantId\n return normalizeScopeId(actorTenantId ?? auth.tenantId ?? null)\n}\n\nfunction resolveActorOrganizationId(auth: NonNullable<AuthContext>): NormalizedScopeId {\n const actorOrgId = (auth as { actorOrgId?: unknown }).actorOrgId\n return normalizeScopeId(actorOrgId ?? auth.orgId ?? null)\n}\n\nexport async function resolveCanonicalStaffAuthContext(\n em: EntityManager,\n auth: AuthContext,\n): Promise<AuthContext> {\n if (!auth) return null\n if (auth.isApiKey) return auth\n\n const subjectId = normalizeScopeId(auth.sub)\n const actorTenantId = resolveActorTenantId(auth)\n const actorOrganizationId = resolveActorOrganizationId(auth)\n if (\n subjectId === INVALID_SCOPE ||\n actorTenantId === INVALID_SCOPE ||\n actorOrganizationId === INVALID_SCOPE\n ) {\n return null\n }\n\n // Session binding: when the JWT carries an `sid` claim, require the referenced session to\n // still exist (not soft-deleted, not expired). This is what makes logout / password-reset\n // actually invalidate an already-issued JWT.\n //\n // Legacy tokens (pre-migration, without `sid`) are allowed through during the grace period\n // (controlled by JWT_LEGACY_GRACE_MINUTES) so that rolling deployments don't force-logout\n // every user. Once the grace period expires these tokens will fail signature verification\n // in `verifyJwt` before reaching this point.\n const sessionId = normalizeScopeId(typeof auth.sid === 'string' ? auth.sid : null)\n if (sessionId === INVALID_SCOPE) return null\n if (sessionId === null) {\n // Legacy token without sid \u2014 allow only if it was verified via the legacy fallback path.\n // The `_legacyToken` flag is set by `verifyJwt` when a token passes raw-secret verification\n // but fails audience-derived verification. Without this flag, reject.\n if ((auth as Record<string, unknown>)._legacyToken === true) {\n // Allow through without session validation \u2014 the token will expire naturally\n } else {\n return null\n }\n }\n // The session-revocation check and the user load are independent (neither reads\n // the other's result), so they run concurrently to collapse two sequential DB\n // round-trips into one. The `em` here is a fresh request-scoped EntityManager\n // (resolved per request, never inside an explicit transaction), so concurrent\n // reads on it are safe.\n const sessionPromise = sessionId !== null\n ? findOneWithDecryption(em, Session, { id: sessionId, deletedAt: null })\n : Promise.resolve(null)\n const userPromise = findOneWithDecryption(\n em,\n User,\n { id: subjectId, deletedAt: null },\n undefined,\n { tenantId: actorTenantId, organizationId: actorOrganizationId },\n )\n const [session, user] = await Promise.all([sessionPromise, userPromise])\n\n if (sessionId !== null) {\n if (!session) return null\n if (session.expiresAt.getTime() < Date.now()) return null\n }\n\n if (!user) return null\n\n const currentTenantId = normalizeScopeId(user.tenantId ?? null)\n const currentOrganizationId = normalizeScopeId(user.organizationId ?? null)\n if (\n currentTenantId === INVALID_SCOPE ||\n currentOrganizationId === INVALID_SCOPE ||\n currentTenantId !== actorTenantId ||\n currentOrganizationId !== actorOrganizationId\n ) {\n return null\n }\n\n // Role links and the per-user super-admin flag are likewise independent, so they\n // run concurrently. The role-level super-admin lookup depends on the resolved\n // role ids, so it stays sequential after the links resolve (and is skipped\n // entirely when the per-user flag already grants super-admin).\n const linksPromise = currentTenantId\n ? findWithDecryption(\n em,\n UserRole,\n {\n user: user.id,\n deletedAt: null,\n role: { tenantId: currentTenantId, deletedAt: null } as unknown as Role,\n } as never,\n { populate: ['role'] },\n { tenantId: currentTenantId, organizationId: currentOrganizationId },\n )\n : Promise.resolve([] as UserRole[])\n const userAclSuperAdminPromise = currentTenantId\n ? userAclGrantsSuperAdmin(em, user.id, currentTenantId, currentOrganizationId)\n : Promise.resolve(false)\n const [links, userAclSuperAdmin] = await Promise.all([linksPromise, userAclSuperAdminPromise])\n\n const linkedRoles = links\n .map((link) => link.role)\n .filter((role): role is Role => !!role)\n\n const roles = linkedRoles\n .map((role) => role.name)\n .filter((name): name is string => typeof name === 'string' && name.trim().length > 0)\n\n const isSuperAdmin = currentTenantId\n ? userAclSuperAdmin || (await roleAclGrantsSuperAdmin(em, linkedRoles, currentTenantId, currentOrganizationId))\n : false\n\n return {\n ...auth,\n sub: user.id,\n tenantId: currentTenantId,\n orgId: currentOrganizationId,\n roles,\n isSuperAdmin,\n }\n}\n\nasync function userAclGrantsSuperAdmin(\n em: EntityManager,\n userId: string,\n tenantId: string,\n organizationId: string | null,\n): Promise<boolean> {\n const userAcl = await findOneWithDecryption(\n em,\n UserAcl,\n {\n user: userId,\n tenantId,\n isSuperAdmin: true,\n deletedAt: null,\n } as never,\n undefined,\n { tenantId, organizationId },\n )\n return !!(userAcl && (userAcl as { isSuperAdmin?: boolean }).isSuperAdmin === true)\n}\n\nasync function roleAclGrantsSuperAdmin(\n em: EntityManager,\n linkedRoles: Role[],\n tenantId: string,\n organizationId: string | null,\n): Promise<boolean> {\n const roleIds = Array.from(\n new Set(\n linkedRoles\n .map((role) => (role?.id ? String(role.id) : null))\n .filter((id): id is string => typeof id === 'string' && id.length > 0),\n ),\n )\n if (!roleIds.length) return false\n\n const roleAcl = await findOneWithDecryption(\n em,\n RoleAcl,\n {\n tenantId,\n isSuperAdmin: true,\n deletedAt: null,\n role: { $in: roleIds },\n } as never,\n undefined,\n { tenantId, organizationId },\n )\n return !!(roleAcl && (roleAcl as { isSuperAdmin?: boolean }).isSuperAdmin === true)\n}\n\nexport async function isAuthContextValid(\n em: EntityManager,\n auth: AuthContext,\n): Promise<boolean> {\n return (await resolveCanonicalStaffAuthContext(em, auth)) !== null\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,5 @@
1
1
  import { Currency, ExchangeRate } from "../data/entities.js";
2
+ const exchangeRateKey = (fromCurrencyCode, toCurrencyCode, date, source) => `${fromCurrencyCode}|${toCurrencyCode}|${date.getTime()}|${source}`;
2
3
  class RateFetchingService {
3
4
  constructor(em) {
4
5
  this.em = em;
@@ -57,21 +58,38 @@ class RateFetchingService {
57
58
  });
58
59
  }
59
60
  async storeRates(rates, scope) {
61
+ if (rates.length === 0) return 0;
60
62
  let stored = 0;
61
63
  await this.em.transactional(async (em) => {
64
+ const fromCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.fromCurrencyCode)));
65
+ const toCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.toCurrencyCode)));
66
+ const sources = Array.from(new Set(rates.map((rate) => rate.source)));
67
+ const dates = Array.from(new Set(rates.map((rate) => rate.date.getTime()))).map(
68
+ (time) => new Date(time)
69
+ );
70
+ const existingRates = await em.find(ExchangeRate, {
71
+ organizationId: scope.organizationId,
72
+ tenantId: scope.tenantId,
73
+ fromCurrencyCode: { $in: fromCurrencyCodes },
74
+ toCurrencyCode: { $in: toCurrencyCodes },
75
+ date: { $in: dates },
76
+ source: { $in: sources }
77
+ });
78
+ const existingByKey = /* @__PURE__ */ new Map();
79
+ for (const existing of existingRates) {
80
+ existingByKey.set(
81
+ exchangeRateKey(existing.fromCurrencyCode, existing.toCurrencyCode, existing.date, existing.source),
82
+ existing
83
+ );
84
+ }
85
+ const now = /* @__PURE__ */ new Date();
62
86
  for (const rate of rates) {
63
- const existing = await em.findOne(ExchangeRate, {
64
- organizationId: scope.organizationId,
65
- tenantId: scope.tenantId,
66
- fromCurrencyCode: rate.fromCurrencyCode,
67
- toCurrencyCode: rate.toCurrencyCode,
68
- date: rate.date,
69
- source: rate.source
70
- });
87
+ const key = exchangeRateKey(rate.fromCurrencyCode, rate.toCurrencyCode, rate.date, rate.source);
88
+ const existing = existingByKey.get(key);
71
89
  if (existing) {
72
90
  existing.rate = rate.rate;
73
91
  existing.type = rate.type ?? null;
74
- existing.updatedAt = /* @__PURE__ */ new Date();
92
+ existing.updatedAt = now;
75
93
  em.persist(existing);
76
94
  } else {
77
95
  const newRate = em.create(ExchangeRate, {
@@ -84,10 +102,11 @@ class RateFetchingService {
84
102
  source: rate.source,
85
103
  type: rate.type ?? null,
86
104
  isActive: true,
87
- createdAt: /* @__PURE__ */ new Date(),
88
- updatedAt: /* @__PURE__ */ new Date()
105
+ createdAt: now,
106
+ updatedAt: now
89
107
  });
90
108
  em.persist(newRate);
109
+ existingByKey.set(key, newRate);
91
110
  }
92
111
  stored++;
93
112
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/currencies/services/rateFetchingService.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { RateProvider, RateProviderResult } from './providers/base'\nimport { Currency, ExchangeRate } from '../data/entities'\n\nexport interface FetchResult {\n totalFetched: number\n byProvider: Record<string, { count: number; errors?: string[] }>\n errors: string[]\n}\n\nexport interface FetchOptions {\n providers?: string[]\n forceUpdate?: boolean\n}\n\nexport class RateFetchingService {\n private providers: Map<string, RateProvider>\n\n constructor(private em: EntityManager) {\n this.providers = new Map()\n }\n\n /**\n * Register a rate provider\n */\n registerProvider(provider: RateProvider): void {\n this.providers.set(provider.source, provider)\n }\n\n async fetchRatesForDate(\n date: Date,\n scope: { tenantId: string; organizationId: string },\n options: FetchOptions = {}\n ): Promise<FetchResult> {\n const result: FetchResult = {\n totalFetched: 0,\n byProvider: {},\n errors: [],\n }\n\n // Get existing currencies for validation\n const existingCurrencies = await this.getExistingCurrencies(scope)\n const currencyCodeSet = new Set(existingCurrencies.map((c) => c.code))\n\n // Determine which providers to use\n const providerList = options.providers?.length\n ? options.providers\n : Array.from(this.providers.keys())\n\n for (const providerSource of providerList) {\n const provider = this.providers.get(providerSource)\n\n if (!provider) {\n result.errors.push(`Unknown provider: ${providerSource}`)\n continue\n }\n\n if (!provider.isAvailable()) {\n result.errors.push(`Provider not available: ${providerSource}`)\n continue\n }\n\n try {\n const rates = await provider.fetchRates(date, scope, currencyCodeSet)\n\n // Filter: only currencies that exist in both directions\n const validRates = rates.filter(\n (r) =>\n currencyCodeSet.has(r.fromCurrencyCode) &&\n currencyCodeSet.has(r.toCurrencyCode)\n )\n\n const stored = await this.storeRates(validRates, scope)\n\n result.byProvider[providerSource] = { count: stored }\n result.totalFetched += stored\n } catch (err: any) {\n const errorMsg = `${providerSource}: ${err.message}`\n result.errors.push(errorMsg)\n result.byProvider[providerSource] = {\n count: 0,\n errors: [err.message],\n }\n }\n }\n\n return result\n }\n\n private async getExistingCurrencies(scope: {\n tenantId: string\n organizationId: string\n }): Promise<Currency[]> {\n return this.em.find(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isActive: true,\n deletedAt: null,\n })\n }\n\n private async storeRates(\n rates: RateProviderResult[],\n scope: { tenantId: string; organizationId: string }\n ): Promise<number> {\n let stored = 0\n\n await this.em.transactional(async (em) => {\n for (const rate of rates) {\n // Check if rate already exists\n const existing = await em.findOne(ExchangeRate, {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n fromCurrencyCode: rate.fromCurrencyCode,\n toCurrencyCode: rate.toCurrencyCode,\n date: rate.date,\n source: rate.source,\n })\n\n if (existing) {\n // Update existing rate\n existing.rate = rate.rate\n existing.type = rate.type ?? null\n existing.updatedAt = new Date()\n em.persist(existing)\n } else {\n // Create new rate\n const newRate = em.create(ExchangeRate, {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n fromCurrencyCode: rate.fromCurrencyCode,\n toCurrencyCode: rate.toCurrencyCode,\n rate: rate.rate,\n date: rate.date,\n source: rate.source,\n type: rate.type ?? null,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(newRate)\n }\n\n stored++\n }\n \n // Flush all changes at once\n await em.flush()\n })\n\n return stored\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { RateProvider, RateProviderResult } from './providers/base'\nimport { Currency, ExchangeRate } from '../data/entities'\n\nexport interface FetchResult {\n totalFetched: number\n byProvider: Record<string, { count: number; errors?: string[] }>\n errors: string[]\n}\n\nexport interface FetchOptions {\n providers?: string[]\n forceUpdate?: boolean\n}\n\nconst exchangeRateKey = (\n fromCurrencyCode: string,\n toCurrencyCode: string,\n date: Date,\n source: string\n): string => `${fromCurrencyCode}|${toCurrencyCode}|${date.getTime()}|${source}`\n\nexport class RateFetchingService {\n private providers: Map<string, RateProvider>\n\n constructor(private em: EntityManager) {\n this.providers = new Map()\n }\n\n /**\n * Register a rate provider\n */\n registerProvider(provider: RateProvider): void {\n this.providers.set(provider.source, provider)\n }\n\n async fetchRatesForDate(\n date: Date,\n scope: { tenantId: string; organizationId: string },\n options: FetchOptions = {}\n ): Promise<FetchResult> {\n const result: FetchResult = {\n totalFetched: 0,\n byProvider: {},\n errors: [],\n }\n\n // Get existing currencies for validation\n const existingCurrencies = await this.getExistingCurrencies(scope)\n const currencyCodeSet = new Set(existingCurrencies.map((c) => c.code))\n\n // Determine which providers to use\n const providerList = options.providers?.length\n ? options.providers\n : Array.from(this.providers.keys())\n\n for (const providerSource of providerList) {\n const provider = this.providers.get(providerSource)\n\n if (!provider) {\n result.errors.push(`Unknown provider: ${providerSource}`)\n continue\n }\n\n if (!provider.isAvailable()) {\n result.errors.push(`Provider not available: ${providerSource}`)\n continue\n }\n\n try {\n const rates = await provider.fetchRates(date, scope, currencyCodeSet)\n\n // Filter: only currencies that exist in both directions\n const validRates = rates.filter(\n (r) =>\n currencyCodeSet.has(r.fromCurrencyCode) &&\n currencyCodeSet.has(r.toCurrencyCode)\n )\n\n const stored = await this.storeRates(validRates, scope)\n\n result.byProvider[providerSource] = { count: stored }\n result.totalFetched += stored\n } catch (err: any) {\n const errorMsg = `${providerSource}: ${err.message}`\n result.errors.push(errorMsg)\n result.byProvider[providerSource] = {\n count: 0,\n errors: [err.message],\n }\n }\n }\n\n return result\n }\n\n private async getExistingCurrencies(scope: {\n tenantId: string\n organizationId: string\n }): Promise<Currency[]> {\n return this.em.find(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isActive: true,\n deletedAt: null,\n })\n }\n\n private async storeRates(\n rates: RateProviderResult[],\n scope: { tenantId: string; organizationId: string }\n ): Promise<number> {\n if (rates.length === 0) return 0\n\n let stored = 0\n\n await this.em.transactional(async (em) => {\n // Prefetch every existing rate that could match this batch in a single query,\n // then index by composite key so the per-rate loop never hits the database.\n const fromCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.fromCurrencyCode)))\n const toCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.toCurrencyCode)))\n const sources = Array.from(new Set(rates.map((rate) => rate.source)))\n const dates = Array.from(new Set(rates.map((rate) => rate.date.getTime()))).map(\n (time) => new Date(time)\n )\n\n const existingRates = await em.find(ExchangeRate, {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n fromCurrencyCode: { $in: fromCurrencyCodes },\n toCurrencyCode: { $in: toCurrencyCodes },\n date: { $in: dates },\n source: { $in: sources },\n })\n\n const existingByKey = new Map<string, ExchangeRate>()\n for (const existing of existingRates) {\n existingByKey.set(\n exchangeRateKey(existing.fromCurrencyCode, existing.toCurrencyCode, existing.date, existing.source),\n existing\n )\n }\n\n const now = new Date()\n for (const rate of rates) {\n const key = exchangeRateKey(rate.fromCurrencyCode, rate.toCurrencyCode, rate.date, rate.source)\n const existing = existingByKey.get(key)\n\n if (existing) {\n // Update existing rate\n existing.rate = rate.rate\n existing.type = rate.type ?? null\n existing.updatedAt = now\n em.persist(existing)\n } else {\n // Create new rate\n const newRate = em.create(ExchangeRate, {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n fromCurrencyCode: rate.fromCurrencyCode,\n toCurrencyCode: rate.toCurrencyCode,\n rate: rate.rate,\n date: rate.date,\n source: rate.source,\n type: rate.type ?? null,\n isActive: true,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(newRate)\n // Track so duplicate keys within the same batch update in memory instead of double-inserting.\n existingByKey.set(key, newRate)\n }\n\n stored++\n }\n\n // Flush all changes at once\n await em.flush()\n })\n\n return stored\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -117,16 +117,21 @@ async function findMatchingEntityIdsBySearchTokensAcrossSources({
117
117
  const trimmed = query.trim();
118
118
  if (!trimmed) return null;
119
119
  const enrichedSources = await enrichSearchSourcesWithCustomFieldTokens(ctx, sources);
120
+ const perSource = await Promise.all(
121
+ enrichedSources.map(async (source) => {
122
+ const rawIds = await findSearchTokenEntityIds({
123
+ ctx,
124
+ entityType: source.entityType,
125
+ fields: source.fields,
126
+ query: trimmed
127
+ });
128
+ if (rawIds === null) return null;
129
+ return source.mapToEntityIds ? await mapScopedEntityIds({ ctx, ids: rawIds, config: source.mapToEntityIds }) : rawIds;
130
+ })
131
+ );
120
132
  const matchedIds = /* @__PURE__ */ new Set();
121
- for (const source of enrichedSources) {
122
- const rawIds = await findSearchTokenEntityIds({
123
- ctx,
124
- entityType: source.entityType,
125
- fields: source.fields,
126
- query: trimmed
127
- });
128
- if (rawIds === null) return null;
129
- const entityIds = source.mapToEntityIds ? await mapScopedEntityIds({ ctx, ids: rawIds, config: source.mapToEntityIds }) : rawIds;
133
+ for (const entityIds of perSource) {
134
+ if (entityIds === null) return null;
130
135
  entityIds.forEach((id) => matchedIds.add(id));
131
136
  }
132
137
  return Array.from(matchedIds);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/api/utils.ts"],
4
- "sourcesContent": ["import { createScopedApiHelpers } from '@open-mercato/shared/lib/api/scoped'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { QueryCustomFieldSource, QueryJoinEdge, QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'\nimport { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'\nimport { SortDir } from '@open-mercato/shared/lib/query/types'\n\nconst { withScopedPayload, parseScopedCommandInput } = createScopedApiHelpers({\n messages: {\n tenantRequired: { key: 'customers.errors.tenant_required', fallback: 'Tenant context is required' },\n organizationRequired: { key: 'customers.errors.organization_required', fallback: 'Organization context is required' },\n },\n})\n\nconst NO_MATCH_ID = '00000000-0000-0000-0000-000000000000'\n\ntype SearchTokenMatchInput = {\n ctx: CrudCtx\n entityType: string\n fields: string[]\n query: string\n}\n\ntype SearchTokenSource = {\n entityType: string\n fields: string[]\n mapToEntityIds?: {\n table: string\n sourceColumn?: string\n targetColumn: string\n tenantColumn?: string\n organizationColumn?: string\n }\n}\n\nasync function enrichSearchSourcesWithCustomFieldTokens(\n ctx: CrudCtx,\n sources: SearchTokenSource[],\n): Promise<SearchTokenSource[]> {\n const entityTypes = Array.from(\n new Set(\n sources\n .map((source) => source.entityType)\n .filter((value): value is string => typeof value === 'string' && value.length > 0),\n ),\n )\n if (!entityTypes.length) return sources\n\n const em = ctx.container.resolve('em') as EntityManager\n const db = em.getKysely<any>() as any\n let defsQuery = db\n .selectFrom('custom_field_defs')\n .select(['entity_id', 'key', 'kind'])\n .where('entity_id', 'in', entityTypes)\n .where('is_active', '=', true)\n\n const tenantScope = ctx.auth?.tenantId ?? null\n defsQuery = defsQuery.where((eb: any) => eb.or([\n eb('tenant_id', '=', tenantScope),\n eb('tenant_id', 'is', null),\n ]))\n\n if (ctx.selectedOrganizationId) {\n defsQuery = defsQuery.where((eb: any) => eb.or([\n eb('organization_id', '=', ctx.selectedOrganizationId),\n eb('organization_id', 'is', null),\n ]))\n } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {\n defsQuery = defsQuery.where((eb: any) => eb.or([\n eb('organization_id', 'in', ctx.organizationIds),\n eb('organization_id', 'is', null),\n ]))\n }\n\n const customFieldKeysByEntity = new Map<string, Set<string>>()\n const rows = await defsQuery.execute()\n for (const row of rows as Array<{ entity_id?: unknown; key?: unknown; kind?: unknown }>) {\n if (row.kind === 'attachment') continue\n const entityType = typeof row.entity_id === 'string' ? row.entity_id : null\n const key = typeof row.key === 'string' ? row.key.trim() : ''\n if (!entityType || !key) continue\n const bucket = customFieldKeysByEntity.get(entityType) ?? new Set<string>()\n bucket.add(`cf:${key}`)\n customFieldKeysByEntity.set(entityType, bucket)\n }\n\n return sources.map((source) => {\n const customFieldKeys = customFieldKeysByEntity.get(source.entityType)\n return {\n ...source,\n fields: Array.from(new Set([\n 'search_text',\n ...source.fields,\n ...(customFieldKeys ? Array.from(customFieldKeys) : []),\n ])),\n }\n })\n}\n\nasync function findSearchTokenEntityIds({\n ctx,\n entityType,\n fields,\n query,\n}: SearchTokenMatchInput): Promise<string[] | null> {\n const trimmed = query.trim()\n if (!trimmed) return null\n\n const tokens = tokenizeText(trimmed, resolveSearchConfig())\n if (!tokens.hashes.length) return []\n\n const em = ctx.container.resolve('em') as EntityManager\n const db = em.getKysely<any>() as any\n let searchQuery = db\n .selectFrom('search_tokens')\n .select('entity_id')\n .where('entity_type', '=', entityType)\n .where('field', 'in', fields)\n .where('token_hash', 'in', tokens.hashes)\n .groupBy('entity_id')\n .having(sql<boolean>`count(distinct token_hash) >= ${tokens.hashes.length}`)\n\n if (ctx.auth?.tenantId !== undefined) {\n searchQuery = searchQuery.where(sql<boolean>`tenant_id is not distinct from ${ctx.auth?.tenantId ?? null}`)\n }\n if (ctx.selectedOrganizationId) {\n searchQuery = searchQuery.where('organization_id', '=', ctx.selectedOrganizationId)\n } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {\n searchQuery = searchQuery.where('organization_id', 'in', ctx.organizationIds)\n }\n\n const rows = await searchQuery.execute() as Array<{ entity_id?: unknown }>\n return rows\n .map((row) => (typeof row.entity_id === 'string' ? row.entity_id : null))\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\nasync function mapScopedEntityIds({\n ctx,\n ids,\n config,\n}: {\n ctx: CrudCtx\n ids: string[]\n config: NonNullable<SearchTokenSource['mapToEntityIds']>\n}): Promise<string[]> {\n if (!ids.length) return []\n\n const em = ctx.container.resolve('em') as EntityManager\n const db = em.getKysely<any>() as any\n const sourceColumn = config.sourceColumn ?? 'id'\n const tenantColumn = config.tenantColumn ?? 'tenant_id'\n const organizationColumn = config.organizationColumn ?? 'organization_id'\n\n let mapQuery = db\n .selectFrom(config.table)\n .select(config.targetColumn)\n .where(sourceColumn, 'in', ids)\n\n if (ctx.auth?.tenantId !== undefined) {\n mapQuery = mapQuery.where(sql<boolean>`${sql.ref(tenantColumn)} is not distinct from ${ctx.auth?.tenantId ?? null}`)\n }\n if (ctx.selectedOrganizationId) {\n mapQuery = mapQuery.where(organizationColumn, '=', ctx.selectedOrganizationId)\n } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {\n mapQuery = mapQuery.where(organizationColumn, 'in', ctx.organizationIds)\n }\n\n const rows = await mapQuery.execute() as Array<Record<string, unknown>>\n return rows\n .map((row) => {\n const value = row[config.targetColumn]\n return typeof value === 'string' ? value : null\n })\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\nexport async function findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx,\n sources,\n query,\n}: {\n ctx: CrudCtx\n sources: SearchTokenSource[]\n query: string\n}): Promise<string[] | null> {\n const trimmed = query.trim()\n if (!trimmed) return null\n\n const enrichedSources = await enrichSearchSourcesWithCustomFieldTokens(ctx, sources)\n const matchedIds = new Set<string>()\n for (const source of enrichedSources) {\n const rawIds = await findSearchTokenEntityIds({\n ctx,\n entityType: source.entityType,\n fields: source.fields,\n query: trimmed,\n })\n if (rawIds === null) return null\n const entityIds = source.mapToEntityIds\n ? await mapScopedEntityIds({ ctx, ids: rawIds, config: source.mapToEntityIds })\n : rawIds\n entityIds.forEach((id) => matchedIds.add(id))\n }\n\n return Array.from(matchedIds)\n}\n\nexport async function findMatchingEntityIdsBySearchTokens({\n ctx,\n entityType,\n fields,\n query,\n}: SearchTokenMatchInput): Promise<string[] | null> {\n return findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx,\n query,\n sources: [{ entityType, fields }],\n })\n}\n\nexport function applyEntityIdRestriction(\n filters: Record<string, unknown>,\n ids: string[] | null,\n): void {\n if (ids === null) return\n const currentIdFilter =\n filters.id && typeof filters.id === 'object' && !Array.isArray(filters.id)\n ? (filters.id as { $eq?: unknown; $in?: unknown })\n : null\n const currentEq = typeof currentIdFilter?.$eq === 'string' ? currentIdFilter.$eq : null\n\n if (currentEq) {\n filters.id = ids.includes(currentEq) ? { $eq: currentEq } : { $eq: NO_MATCH_ID }\n return\n }\n\n filters.id = ids.length > 0 ? { $in: ids } : { $eq: NO_MATCH_ID }\n}\n\nexport function applyEntityIdExclusion(\n filters: Record<string, unknown>,\n ids: string[],\n): void {\n const uniqueIds = Array.from(new Set(ids.filter((id) => typeof id === 'string' && id.length > 0)))\n if (!uniqueIds.length) return\n\n const currentIdFilter =\n filters.id && typeof filters.id === 'object' && !Array.isArray(filters.id)\n ? (filters.id as { $eq?: unknown; $in?: unknown; $nin?: unknown })\n : null\n const currentEq = typeof currentIdFilter?.$eq === 'string' ? currentIdFilter.$eq : null\n const currentIn = Array.isArray(currentIdFilter?.$in)\n ? currentIdFilter.$in.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : null\n const currentNotIn = Array.isArray(currentIdFilter?.$nin)\n ? currentIdFilter.$nin.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : []\n\n if (currentEq) {\n filters.id = uniqueIds.includes(currentEq) ? { $eq: NO_MATCH_ID } : { $eq: currentEq }\n return\n }\n\n if (currentIn) {\n const nextIds = currentIn.filter((id) => !uniqueIds.includes(id))\n filters.id = nextIds.length > 0 ? { $in: nextIds } : { $eq: NO_MATCH_ID }\n return\n }\n\n filters.id = {\n ...(currentIdFilter ?? {}),\n $nin: Array.from(new Set([...currentNotIn, ...uniqueIds])),\n }\n}\n\nexport async function findMatchingEntityIdsWithQueryEngine({\n ctx,\n entityId,\n filters,\n customFieldSources,\n joins,\n}: {\n ctx: CrudCtx\n entityId: EntityId\n filters: Record<string, unknown>\n customFieldSources?: QueryCustomFieldSource[]\n joins?: QueryJoinEdge[]\n}): Promise<string[]> {\n const qe = ctx.container.resolve('queryEngine') as QueryEngine\n const ids = new Set<string>()\n const pageSize = 100\n let page = 1\n let total = 0\n\n do {\n const result = await qe.query(entityId, {\n fields: ['id'],\n filters,\n page: { page, pageSize },\n sort: [{ field: 'id', dir: SortDir.Asc }],\n tenantId: ctx.auth?.tenantId ?? undefined,\n organizationId: ctx.selectedOrganizationId ?? undefined,\n organizationIds: ctx.organizationIds ?? undefined,\n customFieldSources,\n joins,\n })\n\n total = result.total ?? 0\n for (const item of result.items ?? []) {\n const id = item && typeof item === 'object' ? (item as Record<string, unknown>).id : null\n if (typeof id === 'string' && id.length > 0) {\n ids.add(id)\n }\n }\n if (!result.items?.length) break\n page += 1\n } while (ids.size < total)\n\n return Array.from(ids)\n}\n\nexport { withScopedPayload, parseScopedCommandInput }\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createScopedApiHelpers } from '@open-mercato/shared/lib/api/scoped'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { QueryCustomFieldSource, QueryJoinEdge, QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'\nimport { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'\nimport { SortDir } from '@open-mercato/shared/lib/query/types'\n\nconst { withScopedPayload, parseScopedCommandInput } = createScopedApiHelpers({\n messages: {\n tenantRequired: { key: 'customers.errors.tenant_required', fallback: 'Tenant context is required' },\n organizationRequired: { key: 'customers.errors.organization_required', fallback: 'Organization context is required' },\n },\n})\n\nconst NO_MATCH_ID = '00000000-0000-0000-0000-000000000000'\n\ntype SearchTokenMatchInput = {\n ctx: CrudCtx\n entityType: string\n fields: string[]\n query: string\n}\n\ntype SearchTokenSource = {\n entityType: string\n fields: string[]\n mapToEntityIds?: {\n table: string\n sourceColumn?: string\n targetColumn: string\n tenantColumn?: string\n organizationColumn?: string\n }\n}\n\nasync function enrichSearchSourcesWithCustomFieldTokens(\n ctx: CrudCtx,\n sources: SearchTokenSource[],\n): Promise<SearchTokenSource[]> {\n const entityTypes = Array.from(\n new Set(\n sources\n .map((source) => source.entityType)\n .filter((value): value is string => typeof value === 'string' && value.length > 0),\n ),\n )\n if (!entityTypes.length) return sources\n\n const em = ctx.container.resolve('em') as EntityManager\n const db = em.getKysely<any>() as any\n let defsQuery = db\n .selectFrom('custom_field_defs')\n .select(['entity_id', 'key', 'kind'])\n .where('entity_id', 'in', entityTypes)\n .where('is_active', '=', true)\n\n const tenantScope = ctx.auth?.tenantId ?? null\n defsQuery = defsQuery.where((eb: any) => eb.or([\n eb('tenant_id', '=', tenantScope),\n eb('tenant_id', 'is', null),\n ]))\n\n if (ctx.selectedOrganizationId) {\n defsQuery = defsQuery.where((eb: any) => eb.or([\n eb('organization_id', '=', ctx.selectedOrganizationId),\n eb('organization_id', 'is', null),\n ]))\n } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {\n defsQuery = defsQuery.where((eb: any) => eb.or([\n eb('organization_id', 'in', ctx.organizationIds),\n eb('organization_id', 'is', null),\n ]))\n }\n\n const customFieldKeysByEntity = new Map<string, Set<string>>()\n const rows = await defsQuery.execute()\n for (const row of rows as Array<{ entity_id?: unknown; key?: unknown; kind?: unknown }>) {\n if (row.kind === 'attachment') continue\n const entityType = typeof row.entity_id === 'string' ? row.entity_id : null\n const key = typeof row.key === 'string' ? row.key.trim() : ''\n if (!entityType || !key) continue\n const bucket = customFieldKeysByEntity.get(entityType) ?? new Set<string>()\n bucket.add(`cf:${key}`)\n customFieldKeysByEntity.set(entityType, bucket)\n }\n\n return sources.map((source) => {\n const customFieldKeys = customFieldKeysByEntity.get(source.entityType)\n return {\n ...source,\n fields: Array.from(new Set([\n 'search_text',\n ...source.fields,\n ...(customFieldKeys ? Array.from(customFieldKeys) : []),\n ])),\n }\n })\n}\n\nasync function findSearchTokenEntityIds({\n ctx,\n entityType,\n fields,\n query,\n}: SearchTokenMatchInput): Promise<string[] | null> {\n const trimmed = query.trim()\n if (!trimmed) return null\n\n const tokens = tokenizeText(trimmed, resolveSearchConfig())\n if (!tokens.hashes.length) return []\n\n const em = ctx.container.resolve('em') as EntityManager\n const db = em.getKysely<any>() as any\n let searchQuery = db\n .selectFrom('search_tokens')\n .select('entity_id')\n .where('entity_type', '=', entityType)\n .where('field', 'in', fields)\n .where('token_hash', 'in', tokens.hashes)\n .groupBy('entity_id')\n .having(sql<boolean>`count(distinct token_hash) >= ${tokens.hashes.length}`)\n\n if (ctx.auth?.tenantId !== undefined) {\n searchQuery = searchQuery.where(sql<boolean>`tenant_id is not distinct from ${ctx.auth?.tenantId ?? null}`)\n }\n if (ctx.selectedOrganizationId) {\n searchQuery = searchQuery.where('organization_id', '=', ctx.selectedOrganizationId)\n } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {\n searchQuery = searchQuery.where('organization_id', 'in', ctx.organizationIds)\n }\n\n const rows = await searchQuery.execute() as Array<{ entity_id?: unknown }>\n return rows\n .map((row) => (typeof row.entity_id === 'string' ? row.entity_id : null))\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\nasync function mapScopedEntityIds({\n ctx,\n ids,\n config,\n}: {\n ctx: CrudCtx\n ids: string[]\n config: NonNullable<SearchTokenSource['mapToEntityIds']>\n}): Promise<string[]> {\n if (!ids.length) return []\n\n const em = ctx.container.resolve('em') as EntityManager\n const db = em.getKysely<any>() as any\n const sourceColumn = config.sourceColumn ?? 'id'\n const tenantColumn = config.tenantColumn ?? 'tenant_id'\n const organizationColumn = config.organizationColumn ?? 'organization_id'\n\n let mapQuery = db\n .selectFrom(config.table)\n .select(config.targetColumn)\n .where(sourceColumn, 'in', ids)\n\n if (ctx.auth?.tenantId !== undefined) {\n mapQuery = mapQuery.where(sql<boolean>`${sql.ref(tenantColumn)} is not distinct from ${ctx.auth?.tenantId ?? null}`)\n }\n if (ctx.selectedOrganizationId) {\n mapQuery = mapQuery.where(organizationColumn, '=', ctx.selectedOrganizationId)\n } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {\n mapQuery = mapQuery.where(organizationColumn, 'in', ctx.organizationIds)\n }\n\n const rows = await mapQuery.execute() as Array<Record<string, unknown>>\n return rows\n .map((row) => {\n const value = row[config.targetColumn]\n return typeof value === 'string' ? value : null\n })\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\nexport async function findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx,\n sources,\n query,\n}: {\n ctx: CrudCtx\n sources: SearchTokenSource[]\n query: string\n}): Promise<string[] | null> {\n const trimmed = query.trim()\n if (!trimmed) return null\n\n const enrichedSources = await enrichSearchSourcesWithCustomFieldTokens(ctx, sources)\n const perSource = await Promise.all(\n enrichedSources.map(async (source) => {\n const rawIds = await findSearchTokenEntityIds({\n ctx,\n entityType: source.entityType,\n fields: source.fields,\n query: trimmed,\n })\n if (rawIds === null) return null\n return source.mapToEntityIds\n ? await mapScopedEntityIds({ ctx, ids: rawIds, config: source.mapToEntityIds })\n : rawIds\n }),\n )\n\n const matchedIds = new Set<string>()\n for (const entityIds of perSource) {\n if (entityIds === null) return null\n entityIds.forEach((id) => matchedIds.add(id))\n }\n\n return Array.from(matchedIds)\n}\n\nexport async function findMatchingEntityIdsBySearchTokens({\n ctx,\n entityType,\n fields,\n query,\n}: SearchTokenMatchInput): Promise<string[] | null> {\n return findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx,\n query,\n sources: [{ entityType, fields }],\n })\n}\n\nexport function applyEntityIdRestriction(\n filters: Record<string, unknown>,\n ids: string[] | null,\n): void {\n if (ids === null) return\n const currentIdFilter =\n filters.id && typeof filters.id === 'object' && !Array.isArray(filters.id)\n ? (filters.id as { $eq?: unknown; $in?: unknown })\n : null\n const currentEq = typeof currentIdFilter?.$eq === 'string' ? currentIdFilter.$eq : null\n\n if (currentEq) {\n filters.id = ids.includes(currentEq) ? { $eq: currentEq } : { $eq: NO_MATCH_ID }\n return\n }\n\n filters.id = ids.length > 0 ? { $in: ids } : { $eq: NO_MATCH_ID }\n}\n\nexport function applyEntityIdExclusion(\n filters: Record<string, unknown>,\n ids: string[],\n): void {\n const uniqueIds = Array.from(new Set(ids.filter((id) => typeof id === 'string' && id.length > 0)))\n if (!uniqueIds.length) return\n\n const currentIdFilter =\n filters.id && typeof filters.id === 'object' && !Array.isArray(filters.id)\n ? (filters.id as { $eq?: unknown; $in?: unknown; $nin?: unknown })\n : null\n const currentEq = typeof currentIdFilter?.$eq === 'string' ? currentIdFilter.$eq : null\n const currentIn = Array.isArray(currentIdFilter?.$in)\n ? currentIdFilter.$in.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : null\n const currentNotIn = Array.isArray(currentIdFilter?.$nin)\n ? currentIdFilter.$nin.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : []\n\n if (currentEq) {\n filters.id = uniqueIds.includes(currentEq) ? { $eq: NO_MATCH_ID } : { $eq: currentEq }\n return\n }\n\n if (currentIn) {\n const nextIds = currentIn.filter((id) => !uniqueIds.includes(id))\n filters.id = nextIds.length > 0 ? { $in: nextIds } : { $eq: NO_MATCH_ID }\n return\n }\n\n filters.id = {\n ...(currentIdFilter ?? {}),\n $nin: Array.from(new Set([...currentNotIn, ...uniqueIds])),\n }\n}\n\nexport async function findMatchingEntityIdsWithQueryEngine({\n ctx,\n entityId,\n filters,\n customFieldSources,\n joins,\n}: {\n ctx: CrudCtx\n entityId: EntityId\n filters: Record<string, unknown>\n customFieldSources?: QueryCustomFieldSource[]\n joins?: QueryJoinEdge[]\n}): Promise<string[]> {\n const qe = ctx.container.resolve('queryEngine') as QueryEngine\n const ids = new Set<string>()\n const pageSize = 100\n let page = 1\n let total = 0\n\n do {\n const result = await qe.query(entityId, {\n fields: ['id'],\n filters,\n page: { page, pageSize },\n sort: [{ field: 'id', dir: SortDir.Asc }],\n tenantId: ctx.auth?.tenantId ?? undefined,\n organizationId: ctx.selectedOrganizationId ?? undefined,\n organizationIds: ctx.organizationIds ?? undefined,\n customFieldSources,\n joins,\n })\n\n total = result.total ?? 0\n for (const item of result.items ?? []) {\n const id = item && typeof item === 'object' ? (item as Record<string, unknown>).id : null\n if (typeof id === 'string' && id.length > 0) {\n ids.add(id)\n }\n }\n if (!result.items?.length) break\n page += 1\n } while (ids.size < total)\n\n return Array.from(ids)\n}\n\nexport { withScopedPayload, parseScopedCommandInput }\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -81,29 +81,43 @@ function getSelectedTenantFromRequest(req) {
81
81
  const header = typeof headerContainer?.get === "function" ? headerContainer.get("cookie") : null;
82
82
  return parseSelectedTenantCookie(header);
83
83
  }
84
- async function collectWithDescendants(em, tenantId, ids) {
85
- if (!ids.length) return /* @__PURE__ */ new Set();
86
- const unique = Array.from(new Set(
84
+ function normalizeOrganizationIds(ids) {
85
+ return Array.from(new Set(
87
86
  ids.map((value) => normalizeOrganizationId(value)).filter((value) => {
88
87
  if (!value) return false;
89
88
  if (isAllOrganizationsSelection(value)) return false;
90
89
  return true;
91
90
  })
92
91
  ));
93
- if (!unique.length) return /* @__PURE__ */ new Set();
92
+ }
93
+ async function loadOrgDescendantMap(em, tenantId, ids) {
94
+ const unique = normalizeOrganizationIds(ids);
95
+ if (!unique.length) return /* @__PURE__ */ new Map();
94
96
  const filter = {
95
97
  tenant: tenantId,
96
98
  id: { $in: unique },
97
99
  deletedAt: null
98
100
  };
99
101
  const orgs = await em.find(Organization, filter);
100
- const set = /* @__PURE__ */ new Set();
102
+ const map = /* @__PURE__ */ new Map();
101
103
  for (const org of orgs) {
102
104
  const id = String(org.id);
103
- set.add(id);
105
+ const expansion = [id];
104
106
  if (Array.isArray(org.descendantIds)) {
105
- for (const desc of org.descendantIds) set.add(String(desc));
107
+ for (const desc of org.descendantIds) expansion.push(String(desc));
106
108
  }
109
+ map.set(id, expansion);
110
+ }
111
+ return map;
112
+ }
113
+ function expandWithDescendants(map, ids) {
114
+ const set = /* @__PURE__ */ new Set();
115
+ for (const value of ids) {
116
+ const id = normalizeOrganizationId(value);
117
+ if (!id || isAllOrganizationsSelection(id)) continue;
118
+ const expansion = map.get(id);
119
+ if (!expansion) continue;
120
+ for (const entry of expansion) set.add(entry);
107
121
  }
108
122
  return set;
109
123
  }
@@ -139,24 +153,23 @@ async function resolveOrganizationScope({
139
153
  const accessibleList = effectiveSuperAdmin ? null : normalizedAccessible && normalizedAccessible.some((value) => isAllOrganizationsSelection(value)) ? null : normalizedAccessible?.filter((value) => !isAllOrganizationsSelection(value)) ?? null;
140
154
  const accountOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null;
141
155
  const fallbackOrgId = accountOrgId ?? null;
142
- let fallbackSet = null;
143
- const loadFallbackSet = async () => {
144
- if (!fallbackOrgId) return null;
145
- if (!fallbackSet) {
146
- fallbackSet = await collectWithDescendants(em, tenantId, [fallbackOrgId]);
147
- }
148
- return fallbackSet;
149
- };
156
+ const candidateIds = [
157
+ ...accessibleList ?? [],
158
+ ...fallbackOrgId ? [fallbackOrgId] : [],
159
+ ...normalizedSelectedId ? [normalizedSelectedId] : []
160
+ ];
161
+ const orgDescendants = await loadOrgDescendantMap(em, tenantId, candidateIds);
162
+ const loadFallbackSet = () => fallbackOrgId ? expandWithDescendants(orgDescendants, [fallbackOrgId]) : null;
150
163
  let allowedSet = null;
151
164
  if (accessibleList === null) {
152
165
  allowedSet = null;
153
166
  } else if (accessibleList.length === 0) {
154
167
  allowedSet = /* @__PURE__ */ new Set();
155
168
  } else {
156
- allowedSet = await collectWithDescendants(em, tenantId, accessibleList);
169
+ allowedSet = expandWithDescendants(orgDescendants, accessibleList);
157
170
  }
158
171
  if (allowedSet && allowedSet.size === 0 && fallbackOrgId) {
159
- const computed = await loadFallbackSet();
172
+ const computed = loadFallbackSet();
160
173
  if (computed && computed.size > 0) {
161
174
  allowedSet = computed;
162
175
  }
@@ -173,16 +186,16 @@ async function resolveOrganizationScope({
173
186
  }
174
187
  let filterSet = null;
175
188
  if (effectiveSelected) {
176
- filterSet = await collectWithDescendants(em, tenantId, [effectiveSelected]);
189
+ filterSet = expandWithDescendants(orgDescendants, [effectiveSelected]);
177
190
  } else if (allowedSet !== null) {
178
191
  filterSet = allowedSet;
179
192
  } else if (widenToAllOrgs) {
180
193
  filterSet = null;
181
194
  } else if (auth.orgId) {
182
- filterSet = await loadFallbackSet();
195
+ filterSet = loadFallbackSet();
183
196
  }
184
197
  if ((!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {
185
- const computed = await loadFallbackSet();
198
+ const computed = loadFallbackSet();
186
199
  if (computed && computed.size > 0) {
187
200
  filterSet = computed;
188
201
  if (!effectiveSelected) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/directory/utils/organizationScope.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { parseSelectedOrganizationCookie, parseSelectedTenantCookie } from './scopeCookies'\n\nexport { parseSelectedOrganizationCookie, parseSelectedTenantCookie }\n\nexport type OrganizationScope = {\n selectedId: string | null\n filterIds: string[] | null\n allowedIds: string[] | null\n tenantId: string | null\n}\n\n// Phase 4 \u2014 short-TTL cache for resolveOrganizationScopeForRequest.\n// OrganizationScope is a pure function of (userId, tenantId, selectedOrgId,\n// requestedTenant) between membership changes; caching it bypasses 1\n// SELECT on `organizations` per CRUD request. TTL is short (60s default)\n// to keep staleness bounded for membership/visibility changes. Tag-based\n// invalidation kicks the cache when user_organizations or organizations\n// mutate (wired via invalidateOrganizationScopeCacheFor).\nconst ORG_SCOPE_CACHE_KEY_PREFIX = 'org-scope'\n// Phase 4 default-off until the same readiness probe (`GET /api/customers/people`)\n// stays green with the cache layer engaged. Set `OM_ORG_SCOPE_CACHE_TTL_MS=60000`\n// (or any positive integer) to opt in once cross-request safety is re-verified.\nconst ORG_SCOPE_DEFAULT_TTL_MS = 0\n\nfunction resolveOrgScopeTtlMs(): number {\n const raw = process.env.OM_ORG_SCOPE_CACHE_TTL_MS\n if (raw === undefined) return ORG_SCOPE_DEFAULT_TTL_MS\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed < 0) return ORG_SCOPE_DEFAULT_TTL_MS\n return parsed\n}\n\nfunction buildOrgScopeCacheKey(parts: {\n userId: string\n effectiveTenantId: string\n selectedOrgId: string | null\n requestedTenantId: string | null\n}): string {\n const selected = parts.selectedOrgId ?? 'none'\n const requested = parts.requestedTenantId ?? 'none'\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:${parts.userId}:${parts.effectiveTenantId}:${selected}:${requested}`\n}\n\nfunction buildOrgScopeCacheTags(parts: { userId: string; effectiveTenantId: string }): string[] {\n return [\n `${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${parts.userId}`,\n `${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${parts.effectiveTenantId}`,\n ]\n}\n\nfunction isValidCachedScope(value: unknown): value is OrganizationScope {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<OrganizationScope>\n const idOk = (v: unknown) => v === null || typeof v === 'string'\n const arrOk = (v: unknown) => v === null || (Array.isArray(v) && v.every((entry) => typeof entry === 'string'))\n return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds)\n}\n\nfunction resolveCacheFromContainer(container: AwilixContainer | null | undefined): CacheStrategy | null {\n if (!container) return null\n try {\n const c = container.resolve('cache') as CacheStrategy | undefined\n if (c && typeof c.get === 'function' && typeof c.set === 'function') return c\n } catch {\n return null\n }\n return null\n}\n\nexport async function invalidateOrganizationScopeCacheForUser(\n container: AwilixContainer,\n userId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await cache.deleteByTags([`${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${userId}`])\n } catch (err) {\n console.warn('[org-scope:cache] invalidate user failed', err)\n }\n}\n\nexport async function invalidateOrganizationScopeCacheForTenant(\n container: AwilixContainer,\n tenantId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await cache.deleteByTags([`${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${tenantId}`])\n } catch (err) {\n console.warn('[org-scope:cache] invalidate tenant failed', err)\n }\n}\n\nfunction normalizeOrganizationId(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport function getSelectedOrganizationFromRequest(req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_org')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedOrganizationCookie(header)\n}\n\nexport function getSelectedTenantFromRequest(\n req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } },\n): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_tenant')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedTenantCookie(header)\n}\n\nasync function collectWithDescendants(em: EntityManager, tenantId: string, ids: string[]): Promise<Set<string>> {\n if (!ids.length) return new Set()\n const unique = Array.from(new Set(\n ids.map((value) => normalizeOrganizationId(value)).filter((value): value is string => {\n if (!value) return false\n if (isAllOrganizationsSelection(value)) return false\n return true\n })\n ))\n if (!unique.length) return new Set()\n const filter: FilterQuery<Organization> = {\n tenant: tenantId,\n id: { $in: unique },\n deletedAt: null,\n }\n const orgs = await em.find(Organization, filter)\n const set = new Set<string>()\n for (const org of orgs) {\n const id = String(org.id)\n set.add(id)\n if (Array.isArray(org.descendantIds)) {\n for (const desc of org.descendantIds) set.add(String(desc))\n }\n }\n return set\n}\n\nexport async function resolveOrganizationScope({\n em,\n rbac,\n auth,\n selectedId,\n tenantId: tenantIdOverride,\n}: {\n em: EntityManager\n rbac: RbacService\n auth: AuthContext\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const actorTenantId = typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null\n const candidateTenantId = typeof tenantIdOverride === 'string' && tenantIdOverride.trim().length > 0\n ? tenantIdOverride.trim()\n : tenantIdOverride === null\n ? null\n : actorTenantId\n if (!candidateTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const usingOverride = candidateTenantId !== actorTenantId\n const isSuperAdminActor = auth.isSuperAdmin === true\n const tenantId = usingOverride && actorTenantId && !isSuperAdminActor ? actorTenantId : candidateTenantId\n if (!tenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const normalizedRequestedSelection = normalizeOrganizationId(selectedId)\n const explicitAllOrgsChoice =\n normalizedRequestedSelection !== null && isAllOrganizationsSelection(normalizedRequestedSelection)\n const normalizedSelectedId = explicitAllOrgsChoice\n ? null\n : normalizedRequestedSelection\n const contextOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const acl = await rbac.loadAcl(auth.sub, { tenantId, organizationId: contextOrgId })\n const aclIsSuperAdmin = acl?.isSuperAdmin === true\n const effectiveSuperAdmin = aclIsSuperAdmin || isSuperAdminActor\n const normalizedAccessible = effectiveSuperAdmin\n ? null\n : Array.isArray(acl?.organizations)\n ? acl.organizations\n .map((value) => normalizeOrganizationId(value))\n .filter((value): value is string => value !== null)\n : null\n const accessibleList = effectiveSuperAdmin\n ? null\n : normalizedAccessible && normalizedAccessible.some((value) => isAllOrganizationsSelection(value))\n ? null\n : normalizedAccessible?.filter((value) => !isAllOrganizationsSelection(value)) ?? null\n\n const accountOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const fallbackOrgId = accountOrgId ?? null\n let fallbackSet: Set<string> | null = null\n const loadFallbackSet = async (): Promise<Set<string> | null> => {\n if (!fallbackOrgId) return null\n if (!fallbackSet) {\n fallbackSet = await collectWithDescendants(em, tenantId, [fallbackOrgId])\n }\n return fallbackSet\n }\n\n let allowedSet: Set<string> | null = null\n if (accessibleList === null) {\n allowedSet = null\n } else if (accessibleList.length === 0) {\n allowedSet = new Set()\n } else {\n allowedSet = await collectWithDescendants(em, tenantId, accessibleList)\n }\n\n if (allowedSet && allowedSet.size === 0 && fallbackOrgId) {\n const computed = await loadFallbackSet()\n if (computed && computed.size > 0) {\n allowedSet = computed\n }\n }\n\n const hasUnrestrictedAccess = effectiveSuperAdmin || (accessibleList === null)\n const noOrgSelection = normalizedSelectedId === null && !explicitAllOrgsChoice\n const widenToAllOrgs =\n (explicitAllOrgsChoice && hasUnrestrictedAccess)\n || (effectiveSuperAdmin && noOrgSelection)\n const initialSelected =\n normalizedSelectedId\n ?? (widenToAllOrgs ? null : accountOrgId ?? null)\n let effectiveSelected: string | null = null\n if (initialSelected) {\n if (allowedSet === null || allowedSet.has(initialSelected)) {\n effectiveSelected = initialSelected\n }\n }\n\n let filterSet: Set<string> | null = null\n if (effectiveSelected) {\n filterSet = await collectWithDescendants(em, tenantId, [effectiveSelected])\n } else if (allowedSet !== null) {\n filterSet = allowedSet\n } else if (widenToAllOrgs) {\n filterSet = null\n } else if (auth.orgId) {\n filterSet = await loadFallbackSet()\n }\n\n if ((!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {\n const computed = await loadFallbackSet()\n if (computed && computed.size > 0) {\n filterSet = computed\n if (!effectiveSelected) {\n effectiveSelected = fallbackOrgId\n }\n }\n }\n\n return {\n selectedId: effectiveSelected,\n filterIds: filterSet ? Array.from(filterSet) : null,\n allowedIds: allowedSet ? Array.from(allowedSet) : null,\n tenantId,\n }\n}\n\nexport async function resolveOrganizationScopeForRequest({\n container,\n auth,\n request,\n selectedId,\n tenantId: tenantOverride,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n let em: EntityManager | null = null\n let rbac: RbacService | null = null\n try { em = container.resolve<EntityManager>('em') } catch { em = null }\n try { rbac = container.resolve<RbacService>('rbacService') } catch { rbac = null }\n const normalizeString = (value: unknown): string | null => {\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n return null\n }\n if (!em || !rbac) {\n const fallbackSelected = normalizeOrganizationId(selectedId ?? auth.orgId ?? null)\n return {\n selectedId: fallbackSelected,\n filterIds: fallbackSelected ? [fallbackSelected] : null,\n allowedIds: fallbackSelected ? [fallbackSelected] : null,\n tenantId: normalizeString(auth.tenantId),\n }\n }\n\n const actorTenantField = (auth as { actorTenantId?: string | null }).actorTenantId\n const actorTenant = actorTenantField === undefined\n ? normalizeString(auth.tenantId)\n : actorTenantField === null\n ? null\n : normalizeString(actorTenantField)\n const actorOrgField = (auth as { actorOrgId?: string | null }).actorOrgId\n const actorOrgId = actorOrgField === undefined\n ? normalizeString(auth.orgId)\n : actorOrgField === null\n ? null\n : normalizeString(actorOrgField)\n\n const cookieTenant = request ? getSelectedTenantFromRequest(request) : null\n const requestedTenant =\n tenantOverride !== undefined\n ? tenantOverride\n : cookieTenant !== undefined\n ? cookieTenant\n : undefined\n const requestedTenantId = typeof requestedTenant === 'string' && requestedTenant.trim().length > 0 ? requestedTenant.trim() : null\n const isSuperAdminActor = auth.isSuperAdmin === true\n let effectiveTenantId = requestedTenantId ?? actorTenant ?? null\n if (actorTenant && effectiveTenantId && effectiveTenantId !== actorTenant && !isSuperAdminActor) {\n effectiveTenantId = actorTenant\n }\n if (!effectiveTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n const scopedAuth = {\n ...auth,\n tenantId: effectiveTenantId,\n orgId: actorTenant && actorTenant === effectiveTenantId ? actorOrgId ?? null : null,\n }\n\n const rawSelected = selectedId !== undefined ? selectedId : (request ? getSelectedOrganizationFromRequest(request) : null)\n const normalizedSelectedId = typeof rawSelected === 'string' && rawSelected.trim().length > 0\n ? rawSelected.trim()\n : null\n\n const userId = typeof auth.sub === 'string' && auth.sub.length > 0 ? auth.sub : null\n const ttlMs = resolveOrgScopeTtlMs()\n const cache = ttlMs > 0 ? resolveCacheFromContainer(container) : null\n const cacheKey = userId\n ? buildOrgScopeCacheKey({\n userId,\n effectiveTenantId,\n selectedOrgId: normalizedSelectedId,\n requestedTenantId: requestedTenantId ?? null,\n })\n : null\n\n if (cache && cacheKey && typeof cache.get === 'function') {\n try {\n const cached = await cache.get(cacheKey)\n if (isValidCachedScope(cached)) return cached\n } catch (err) {\n console.warn('[org-scope:cache] read failed', err)\n }\n }\n\n const baseScope = await resolveOrganizationScope({\n em,\n rbac,\n auth: scopedAuth,\n selectedId: rawSelected,\n tenantId: effectiveTenantId,\n })\n\n if (cache && cacheKey && userId && typeof cache.set === 'function') {\n try {\n await cache.set(cacheKey, baseScope, {\n ttl: ttlMs,\n tags: buildOrgScopeCacheTags({ userId, effectiveTenantId }),\n })\n } catch (err) {\n console.warn('[org-scope:cache] write failed', err)\n }\n }\n\n return baseScope\n}\n\nexport type FeatureCheckContext = {\n organizationId: string | null\n scope: OrganizationScope\n allowedOrganizationIds: string[] | null\n}\n\nexport async function resolveFeatureCheckContext({\n container,\n auth,\n request,\n selectedId,\n tenantId,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<FeatureCheckContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request, selectedId, tenantId })\n const allowedOrganizationIds = scope.allowedIds ?? null\n const authOrgId = auth?.orgId ?? null\n const organizationId =\n scope.selectedId\n ?? (authOrgId && (!Array.isArray(allowedOrganizationIds) || allowedOrganizationIds.includes(authOrgId)) ? authOrgId : null)\n ?? (Array.isArray(allowedOrganizationIds) && allowedOrganizationIds.length ? allowedOrganizationIds[0] : null)\n\n return { organizationId, scope, allowedOrganizationIds }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { parseSelectedOrganizationCookie, parseSelectedTenantCookie } from './scopeCookies'\n\nexport { parseSelectedOrganizationCookie, parseSelectedTenantCookie }\n\nexport type OrganizationScope = {\n selectedId: string | null\n filterIds: string[] | null\n allowedIds: string[] | null\n tenantId: string | null\n}\n\n// Phase 4 \u2014 short-TTL cache for resolveOrganizationScopeForRequest.\n// OrganizationScope is a pure function of (userId, tenantId, selectedOrgId,\n// requestedTenant) between membership changes; caching it bypasses 1\n// SELECT on `organizations` per CRUD request. TTL is short (60s default)\n// to keep staleness bounded for membership/visibility changes. Tag-based\n// invalidation kicks the cache when user_organizations or organizations\n// mutate (wired via invalidateOrganizationScopeCacheFor).\nconst ORG_SCOPE_CACHE_KEY_PREFIX = 'org-scope'\n// Phase 4 default-off until the same readiness probe (`GET /api/customers/people`)\n// stays green with the cache layer engaged. Set `OM_ORG_SCOPE_CACHE_TTL_MS=60000`\n// (or any positive integer) to opt in once cross-request safety is re-verified.\nconst ORG_SCOPE_DEFAULT_TTL_MS = 0\n\nfunction resolveOrgScopeTtlMs(): number {\n const raw = process.env.OM_ORG_SCOPE_CACHE_TTL_MS\n if (raw === undefined) return ORG_SCOPE_DEFAULT_TTL_MS\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed < 0) return ORG_SCOPE_DEFAULT_TTL_MS\n return parsed\n}\n\nfunction buildOrgScopeCacheKey(parts: {\n userId: string\n effectiveTenantId: string\n selectedOrgId: string | null\n requestedTenantId: string | null\n}): string {\n const selected = parts.selectedOrgId ?? 'none'\n const requested = parts.requestedTenantId ?? 'none'\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:${parts.userId}:${parts.effectiveTenantId}:${selected}:${requested}`\n}\n\nfunction buildOrgScopeCacheTags(parts: { userId: string; effectiveTenantId: string }): string[] {\n return [\n `${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${parts.userId}`,\n `${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${parts.effectiveTenantId}`,\n ]\n}\n\nfunction isValidCachedScope(value: unknown): value is OrganizationScope {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<OrganizationScope>\n const idOk = (v: unknown) => v === null || typeof v === 'string'\n const arrOk = (v: unknown) => v === null || (Array.isArray(v) && v.every((entry) => typeof entry === 'string'))\n return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds)\n}\n\nfunction resolveCacheFromContainer(container: AwilixContainer | null | undefined): CacheStrategy | null {\n if (!container) return null\n try {\n const c = container.resolve('cache') as CacheStrategy | undefined\n if (c && typeof c.get === 'function' && typeof c.set === 'function') return c\n } catch {\n return null\n }\n return null\n}\n\nexport async function invalidateOrganizationScopeCacheForUser(\n container: AwilixContainer,\n userId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await cache.deleteByTags([`${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${userId}`])\n } catch (err) {\n console.warn('[org-scope:cache] invalidate user failed', err)\n }\n}\n\nexport async function invalidateOrganizationScopeCacheForTenant(\n container: AwilixContainer,\n tenantId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await cache.deleteByTags([`${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${tenantId}`])\n } catch (err) {\n console.warn('[org-scope:cache] invalidate tenant failed', err)\n }\n}\n\nfunction normalizeOrganizationId(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport function getSelectedOrganizationFromRequest(req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_org')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedOrganizationCookie(header)\n}\n\nexport function getSelectedTenantFromRequest(\n req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } },\n): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_tenant')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedTenantCookie(header)\n}\n\nfunction normalizeOrganizationIds(ids: string[]): string[] {\n return Array.from(new Set(\n ids.map((value) => normalizeOrganizationId(value)).filter((value): value is string => {\n if (!value) return false\n if (isAllOrganizationsSelection(value)) return false\n return true\n })\n ))\n}\n\n// Map each organization id to itself plus its persisted descendant ids. Only\n// orgs that exist for the tenant and are not soft-deleted are included, so an\n// unknown/inaccessible id simply has no entry (matching the per-id query that\n// returned an empty set for it).\ntype OrgDescendantMap = Map<string, string[]>\n\n// Issue #2228 \u2014 single round-trip for org-scope resolution. Instead of issuing\n// one `organizations` SELECT per `collectWithDescendants` call (up to 3-4\n// sequential queries per request: accessible set, fallback set, selected set),\n// gather every candidate id up front and fetch their descendant expansions in\n// one `em.find(Organization, { id: $in })`. Expansion then happens in-memory.\nasync function loadOrgDescendantMap(em: EntityManager, tenantId: string, ids: string[]): Promise<OrgDescendantMap> {\n const unique = normalizeOrganizationIds(ids)\n if (!unique.length) return new Map()\n const filter: FilterQuery<Organization> = {\n tenant: tenantId,\n id: { $in: unique },\n deletedAt: null,\n }\n const orgs = await em.find(Organization, filter)\n const map: OrgDescendantMap = new Map()\n for (const org of orgs) {\n const id = String(org.id)\n const expansion = [id]\n if (Array.isArray(org.descendantIds)) {\n for (const desc of org.descendantIds) expansion.push(String(desc))\n }\n map.set(id, expansion)\n }\n return map\n}\n\nfunction expandWithDescendants(map: OrgDescendantMap, ids: string[]): Set<string> {\n const set = new Set<string>()\n for (const value of ids) {\n const id = normalizeOrganizationId(value)\n if (!id || isAllOrganizationsSelection(id)) continue\n const expansion = map.get(id)\n if (!expansion) continue\n for (const entry of expansion) set.add(entry)\n }\n return set\n}\n\nexport async function resolveOrganizationScope({\n em,\n rbac,\n auth,\n selectedId,\n tenantId: tenantIdOverride,\n}: {\n em: EntityManager\n rbac: RbacService\n auth: AuthContext\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const actorTenantId = typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null\n const candidateTenantId = typeof tenantIdOverride === 'string' && tenantIdOverride.trim().length > 0\n ? tenantIdOverride.trim()\n : tenantIdOverride === null\n ? null\n : actorTenantId\n if (!candidateTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const usingOverride = candidateTenantId !== actorTenantId\n const isSuperAdminActor = auth.isSuperAdmin === true\n const tenantId = usingOverride && actorTenantId && !isSuperAdminActor ? actorTenantId : candidateTenantId\n if (!tenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const normalizedRequestedSelection = normalizeOrganizationId(selectedId)\n const explicitAllOrgsChoice =\n normalizedRequestedSelection !== null && isAllOrganizationsSelection(normalizedRequestedSelection)\n const normalizedSelectedId = explicitAllOrgsChoice\n ? null\n : normalizedRequestedSelection\n const contextOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const acl = await rbac.loadAcl(auth.sub, { tenantId, organizationId: contextOrgId })\n const aclIsSuperAdmin = acl?.isSuperAdmin === true\n const effectiveSuperAdmin = aclIsSuperAdmin || isSuperAdminActor\n const normalizedAccessible = effectiveSuperAdmin\n ? null\n : Array.isArray(acl?.organizations)\n ? acl.organizations\n .map((value) => normalizeOrganizationId(value))\n .filter((value): value is string => value !== null)\n : null\n const accessibleList = effectiveSuperAdmin\n ? null\n : normalizedAccessible && normalizedAccessible.some((value) => isAllOrganizationsSelection(value))\n ? null\n : normalizedAccessible?.filter((value) => !isAllOrganizationsSelection(value)) ?? null\n\n const accountOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const fallbackOrgId = accountOrgId ?? null\n\n // Every id that could be expanded below \u2014 accessible set, fallback (account)\n // org, and the requested selection \u2014 is known up front, so fetch them all in\n // a single `organizations` query and expand from the in-memory map.\n const candidateIds = [\n ...(accessibleList ?? []),\n ...(fallbackOrgId ? [fallbackOrgId] : []),\n ...(normalizedSelectedId ? [normalizedSelectedId] : []),\n ]\n const orgDescendants = await loadOrgDescendantMap(em, tenantId, candidateIds)\n const loadFallbackSet = (): Set<string> | null =>\n fallbackOrgId ? expandWithDescendants(orgDescendants, [fallbackOrgId]) : null\n\n let allowedSet: Set<string> | null = null\n if (accessibleList === null) {\n allowedSet = null\n } else if (accessibleList.length === 0) {\n allowedSet = new Set()\n } else {\n allowedSet = expandWithDescendants(orgDescendants, accessibleList)\n }\n\n if (allowedSet && allowedSet.size === 0 && fallbackOrgId) {\n const computed = loadFallbackSet()\n if (computed && computed.size > 0) {\n allowedSet = computed\n }\n }\n\n const hasUnrestrictedAccess = effectiveSuperAdmin || (accessibleList === null)\n const noOrgSelection = normalizedSelectedId === null && !explicitAllOrgsChoice\n const widenToAllOrgs =\n (explicitAllOrgsChoice && hasUnrestrictedAccess)\n || (effectiveSuperAdmin && noOrgSelection)\n const initialSelected =\n normalizedSelectedId\n ?? (widenToAllOrgs ? null : accountOrgId ?? null)\n let effectiveSelected: string | null = null\n if (initialSelected) {\n if (allowedSet === null || allowedSet.has(initialSelected)) {\n effectiveSelected = initialSelected\n }\n }\n\n let filterSet: Set<string> | null = null\n if (effectiveSelected) {\n filterSet = expandWithDescendants(orgDescendants, [effectiveSelected])\n } else if (allowedSet !== null) {\n filterSet = allowedSet\n } else if (widenToAllOrgs) {\n filterSet = null\n } else if (auth.orgId) {\n filterSet = loadFallbackSet()\n }\n\n if ((!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {\n const computed = loadFallbackSet()\n if (computed && computed.size > 0) {\n filterSet = computed\n if (!effectiveSelected) {\n effectiveSelected = fallbackOrgId\n }\n }\n }\n\n return {\n selectedId: effectiveSelected,\n filterIds: filterSet ? Array.from(filterSet) : null,\n allowedIds: allowedSet ? Array.from(allowedSet) : null,\n tenantId,\n }\n}\n\nexport async function resolveOrganizationScopeForRequest({\n container,\n auth,\n request,\n selectedId,\n tenantId: tenantOverride,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n let em: EntityManager | null = null\n let rbac: RbacService | null = null\n try { em = container.resolve<EntityManager>('em') } catch { em = null }\n try { rbac = container.resolve<RbacService>('rbacService') } catch { rbac = null }\n const normalizeString = (value: unknown): string | null => {\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n return null\n }\n if (!em || !rbac) {\n const fallbackSelected = normalizeOrganizationId(selectedId ?? auth.orgId ?? null)\n return {\n selectedId: fallbackSelected,\n filterIds: fallbackSelected ? [fallbackSelected] : null,\n allowedIds: fallbackSelected ? [fallbackSelected] : null,\n tenantId: normalizeString(auth.tenantId),\n }\n }\n\n const actorTenantField = (auth as { actorTenantId?: string | null }).actorTenantId\n const actorTenant = actorTenantField === undefined\n ? normalizeString(auth.tenantId)\n : actorTenantField === null\n ? null\n : normalizeString(actorTenantField)\n const actorOrgField = (auth as { actorOrgId?: string | null }).actorOrgId\n const actorOrgId = actorOrgField === undefined\n ? normalizeString(auth.orgId)\n : actorOrgField === null\n ? null\n : normalizeString(actorOrgField)\n\n const cookieTenant = request ? getSelectedTenantFromRequest(request) : null\n const requestedTenant =\n tenantOverride !== undefined\n ? tenantOverride\n : cookieTenant !== undefined\n ? cookieTenant\n : undefined\n const requestedTenantId = typeof requestedTenant === 'string' && requestedTenant.trim().length > 0 ? requestedTenant.trim() : null\n const isSuperAdminActor = auth.isSuperAdmin === true\n let effectiveTenantId = requestedTenantId ?? actorTenant ?? null\n if (actorTenant && effectiveTenantId && effectiveTenantId !== actorTenant && !isSuperAdminActor) {\n effectiveTenantId = actorTenant\n }\n if (!effectiveTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n const scopedAuth = {\n ...auth,\n tenantId: effectiveTenantId,\n orgId: actorTenant && actorTenant === effectiveTenantId ? actorOrgId ?? null : null,\n }\n\n const rawSelected = selectedId !== undefined ? selectedId : (request ? getSelectedOrganizationFromRequest(request) : null)\n const normalizedSelectedId = typeof rawSelected === 'string' && rawSelected.trim().length > 0\n ? rawSelected.trim()\n : null\n\n const userId = typeof auth.sub === 'string' && auth.sub.length > 0 ? auth.sub : null\n const ttlMs = resolveOrgScopeTtlMs()\n const cache = ttlMs > 0 ? resolveCacheFromContainer(container) : null\n const cacheKey = userId\n ? buildOrgScopeCacheKey({\n userId,\n effectiveTenantId,\n selectedOrgId: normalizedSelectedId,\n requestedTenantId: requestedTenantId ?? null,\n })\n : null\n\n if (cache && cacheKey && typeof cache.get === 'function') {\n try {\n const cached = await cache.get(cacheKey)\n if (isValidCachedScope(cached)) return cached\n } catch (err) {\n console.warn('[org-scope:cache] read failed', err)\n }\n }\n\n const baseScope = await resolveOrganizationScope({\n em,\n rbac,\n auth: scopedAuth,\n selectedId: rawSelected,\n tenantId: effectiveTenantId,\n })\n\n if (cache && cacheKey && userId && typeof cache.set === 'function') {\n try {\n await cache.set(cacheKey, baseScope, {\n ttl: ttlMs,\n tags: buildOrgScopeCacheTags({ userId, effectiveTenantId }),\n })\n } catch (err) {\n console.warn('[org-scope:cache] write failed', err)\n }\n }\n\n return baseScope\n}\n\nexport type FeatureCheckContext = {\n organizationId: string | null\n scope: OrganizationScope\n allowedOrganizationIds: string[] | null\n}\n\nexport async function resolveFeatureCheckContext({\n container,\n auth,\n request,\n selectedId,\n tenantId,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<FeatureCheckContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request, selectedId, tenantId })\n const allowedOrganizationIds = scope.allowedIds ?? null\n const authOrgId = auth?.orgId ?? null\n const organizationId =\n scope.selectedId\n ?? (authOrgId && (!Array.isArray(allowedOrganizationIds) || allowedOrganizationIds.includes(authOrgId)) ? authOrgId : null)\n ?? (Array.isArray(allowedOrganizationIds) && allowedOrganizationIds.length ? allowedOrganizationIds[0] : null)\n\n return { organizationId, scope, allowedOrganizationIds }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }