@open-mercato/ai-assistant 0.6.7-develop.6775.1.c2313bb8a3 → 0.6.7-develop.6784.1.f80b9afce5

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.
@@ -123,11 +123,11 @@ async function PUT(req) {
123
123
  try {
124
124
  const container = await createRequestContainer();
125
125
  const rbacService = container.resolve("rbacService");
126
- const acl = await rbacService.loadAcl(auth.sub, {
127
- tenantId: auth.tenantId,
128
- organizationId: auth.orgId
129
- });
130
- const canManage = acl.isSuperAdmin || acl.features.includes("ai_assistant.settings.manage");
126
+ const canManage = await rbacService.userHasAllFeatures(
127
+ auth.sub,
128
+ ["ai_assistant.settings.manage"],
129
+ { tenantId: auth.tenantId, organizationId: auth.orgId }
130
+ );
131
131
  if (!canManage) {
132
132
  return NextResponse.json({ error: "Forbidden", code: "forbidden" }, { status: 403 });
133
133
  }
@@ -165,11 +165,11 @@ async function DELETE(req) {
165
165
  try {
166
166
  const container = await createRequestContainer();
167
167
  const rbacService = container.resolve("rbacService");
168
- const acl = await rbacService.loadAcl(auth.sub, {
169
- tenantId: auth.tenantId,
170
- organizationId: auth.orgId
171
- });
172
- const canManage = acl.isSuperAdmin || acl.features.includes("ai_assistant.settings.manage");
168
+ const canManage = await rbacService.userHasAllFeatures(
169
+ auth.sub,
170
+ ["ai_assistant.settings.manage"],
171
+ { tenantId: auth.tenantId, organizationId: auth.orgId }
172
+ );
173
173
  if (!canManage) {
174
174
  return NextResponse.json({ error: "Forbidden", code: "forbidden" }, { status: 403 });
175
175
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/ai_assistant/api/settings/allowlist/route.ts"],
4
- "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { AiTenantModelAllowlistRepository } from '../../../data/repositories/AiTenantModelAllowlistRepository'\nimport {\n canonicalProviderId,\n isModelAllowedForProvider,\n isProviderAllowed,\n modelAllowlistEnvVarName,\n} from '../../../lib/model-allowlist'\n\nconst logger = createLogger('ai_assistant')\n\nconst allowlistUpsertSchema = z.object({\n allowedProviders: z.array(z.string().min(1).max(64)).nullable().optional(),\n allowedModelsByProvider: z\n .record(z.string().min(1).max(64), z.array(z.string().min(1).max(256)))\n .optional(),\n})\n\nexport type AllowlistUpsertBody = z.infer<typeof allowlistUpsertSchema>\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI assistant tenant allowlist',\n methods: {\n PUT: {\n summary: 'Upsert per-tenant AI provider/model allowlist',\n description:\n 'Persists the per-tenant allowlist of providers and models. The runtime intersects this with the env allowlist (`OM_AI_AVAILABLE_*`) at resolution time. ' +\n 'Tenant values that fall outside the env allowlist are rejected with `provider_not_in_env_allowlist` / `model_not_in_env_allowlist` 400 codes. ' +\n 'Gated by `ai_assistant.settings.manage`.',\n requestBody: {\n contentType: 'application/json',\n description:\n 'Allowlist payload. `allowedProviders: null` clears tenant provider restriction (inherit env). Missing key in `allowedModelsByProvider` inherits env for that provider.',\n schema: allowlistUpsertSchema,\n },\n responses: [\n { status: 200, description: 'Allowlist saved. Returns the saved snapshot.' },\n ],\n errors: [\n { status: 400, description: 'Validation error or values outside env allowlist.' },\n { status: 401, description: 'Unauthenticated.' },\n { status: 403, description: 'Caller lacks ai_assistant.settings.manage.' },\n ],\n },\n DELETE: {\n summary: 'Clear per-tenant AI provider/model allowlist',\n description:\n 'Soft-deletes the tenant allowlist row. Tenant overrides revert to env-only enforcement. Idempotent \u2014 returns `{ cleared: false }` when no active row existed.',\n responses: [\n { status: 200, description: 'Returns `{ cleared: boolean }`.' },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated.' },\n { status: 403, description: 'Caller lacks ai_assistant.settings.manage.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n PUT: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n}\n\nexport async function PUT(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return NextResponse.json(\n { error: 'Request body must be valid JSON.', code: 'validation_error' },\n { status: 400 },\n )\n }\n\n const bodyResult = allowlistUpsertSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return NextResponse.json(\n { error: 'Invalid request body.', code: 'validation_error', issues: bodyResult.error.issues },\n { status: 400 },\n )\n }\n\n const env = process.env as Record<string, string | undefined>\n const knownProviderIds = llmProviderRegistry.list().map((provider) => provider.id)\n const canonicalize = (providerId: string) =>\n canonicalProviderId(providerId, knownProviderIds) ?? providerId\n const allowedProviders = bodyResult.data.allowedProviders === undefined\n ? null\n : bodyResult.data.allowedProviders?.map(canonicalize) ?? null\n const allowedModelsByProvider = Object.fromEntries(\n Object.entries(bodyResult.data.allowedModelsByProvider ?? {}).map(([providerId, models]) => [\n canonicalize(providerId),\n models,\n ]),\n )\n\n // Reject tenant entries that escape the env allowlist. The runtime would\n // intersect them away anyway; failing fast at write time keeps stored\n // tenant snapshots honest and visible in admin audits.\n if (Array.isArray(allowedProviders)) {\n for (const providerId of allowedProviders) {\n if (!isProviderAllowed(env, providerId)) {\n return NextResponse.json(\n {\n error: `Provider \"${providerId}\" is not in OM_AI_AVAILABLE_PROVIDERS; tenant allowlist may not widen the env allowlist.`,\n code: 'provider_not_in_env_allowlist',\n },\n { status: 400 },\n )\n }\n }\n }\n for (const providerId of Object.keys(allowedModelsByProvider)) {\n if (!isProviderAllowed(env, providerId)) {\n return NextResponse.json(\n {\n error: `Provider \"${providerId}\" is not in OM_AI_AVAILABLE_PROVIDERS; cannot save tenant model allowlist for it.`,\n code: 'provider_not_in_env_allowlist',\n },\n { status: 400 },\n )\n }\n for (const modelId of allowedModelsByProvider[providerId] ?? []) {\n if (!isModelAllowedForProvider(env, providerId, modelId)) {\n return NextResponse.json(\n {\n error: `Model \"${modelId}\" is not in ${modelAllowlistEnvVarName(providerId)}; tenant allowlist may not widen the env allowlist.`,\n code: 'model_not_in_env_allowlist',\n },\n { status: 400 },\n )\n }\n }\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const canManage =\n acl.isSuperAdmin || acl.features.includes('ai_assistant.settings.manage')\n if (!canManage) {\n return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiTenantModelAllowlistRepository(em)\n const row = await repo.upsert(\n { allowedProviders, allowedModelsByProvider },\n {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n },\n )\n return NextResponse.json({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n allowedProviders: row.allowedProviders ?? null,\n allowedModelsByProvider: row.allowedModelsByProvider ?? {},\n updatedAt: row.updatedAt,\n })\n } catch (error) {\n logger.error('AI Settings Allowlist \u2014 PUT error', { err: error })\n return NextResponse.json(\n { error: 'Failed to save tenant allowlist.' },\n { status: 500 },\n )\n }\n}\n\nexport async function DELETE(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const canManage =\n acl.isSuperAdmin || acl.features.includes('ai_assistant.settings.manage')\n if (!canManage) {\n return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiTenantModelAllowlistRepository(em)\n const cleared = await repo.clear({\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n })\n return NextResponse.json({ cleared })\n } catch (error) {\n logger.error('AI Settings Allowlist \u2014 DELETE error', { err: error })\n return NextResponse.json(\n { error: 'Failed to clear tenant allowlist.' },\n { status: 500 },\n )\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { AiTenantModelAllowlistRepository } from '../../../data/repositories/AiTenantModelAllowlistRepository'\nimport {\n canonicalProviderId,\n isModelAllowedForProvider,\n isProviderAllowed,\n modelAllowlistEnvVarName,\n} from '../../../lib/model-allowlist'\n\nconst logger = createLogger('ai_assistant')\n\nconst allowlistUpsertSchema = z.object({\n allowedProviders: z.array(z.string().min(1).max(64)).nullable().optional(),\n allowedModelsByProvider: z\n .record(z.string().min(1).max(64), z.array(z.string().min(1).max(256)))\n .optional(),\n})\n\nexport type AllowlistUpsertBody = z.infer<typeof allowlistUpsertSchema>\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI assistant tenant allowlist',\n methods: {\n PUT: {\n summary: 'Upsert per-tenant AI provider/model allowlist',\n description:\n 'Persists the per-tenant allowlist of providers and models. The runtime intersects this with the env allowlist (`OM_AI_AVAILABLE_*`) at resolution time. ' +\n 'Tenant values that fall outside the env allowlist are rejected with `provider_not_in_env_allowlist` / `model_not_in_env_allowlist` 400 codes. ' +\n 'Gated by `ai_assistant.settings.manage`.',\n requestBody: {\n contentType: 'application/json',\n description:\n 'Allowlist payload. `allowedProviders: null` clears tenant provider restriction (inherit env). Missing key in `allowedModelsByProvider` inherits env for that provider.',\n schema: allowlistUpsertSchema,\n },\n responses: [\n { status: 200, description: 'Allowlist saved. Returns the saved snapshot.' },\n ],\n errors: [\n { status: 400, description: 'Validation error or values outside env allowlist.' },\n { status: 401, description: 'Unauthenticated.' },\n { status: 403, description: 'Caller lacks ai_assistant.settings.manage.' },\n ],\n },\n DELETE: {\n summary: 'Clear per-tenant AI provider/model allowlist',\n description:\n 'Soft-deletes the tenant allowlist row. Tenant overrides revert to env-only enforcement. Idempotent \u2014 returns `{ cleared: false }` when no active row existed.',\n responses: [\n { status: 200, description: 'Returns `{ cleared: boolean }`.' },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated.' },\n { status: 403, description: 'Caller lacks ai_assistant.settings.manage.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n PUT: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n}\n\nexport async function PUT(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return NextResponse.json(\n { error: 'Request body must be valid JSON.', code: 'validation_error' },\n { status: 400 },\n )\n }\n\n const bodyResult = allowlistUpsertSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return NextResponse.json(\n { error: 'Invalid request body.', code: 'validation_error', issues: bodyResult.error.issues },\n { status: 400 },\n )\n }\n\n const env = process.env as Record<string, string | undefined>\n const knownProviderIds = llmProviderRegistry.list().map((provider) => provider.id)\n const canonicalize = (providerId: string) =>\n canonicalProviderId(providerId, knownProviderIds) ?? providerId\n const allowedProviders = bodyResult.data.allowedProviders === undefined\n ? null\n : bodyResult.data.allowedProviders?.map(canonicalize) ?? null\n const allowedModelsByProvider = Object.fromEntries(\n Object.entries(bodyResult.data.allowedModelsByProvider ?? {}).map(([providerId, models]) => [\n canonicalize(providerId),\n models,\n ]),\n )\n\n // Reject tenant entries that escape the env allowlist. The runtime would\n // intersect them away anyway; failing fast at write time keeps stored\n // tenant snapshots honest and visible in admin audits.\n if (Array.isArray(allowedProviders)) {\n for (const providerId of allowedProviders) {\n if (!isProviderAllowed(env, providerId)) {\n return NextResponse.json(\n {\n error: `Provider \"${providerId}\" is not in OM_AI_AVAILABLE_PROVIDERS; tenant allowlist may not widen the env allowlist.`,\n code: 'provider_not_in_env_allowlist',\n },\n { status: 400 },\n )\n }\n }\n }\n for (const providerId of Object.keys(allowedModelsByProvider)) {\n if (!isProviderAllowed(env, providerId)) {\n return NextResponse.json(\n {\n error: `Provider \"${providerId}\" is not in OM_AI_AVAILABLE_PROVIDERS; cannot save tenant model allowlist for it.`,\n code: 'provider_not_in_env_allowlist',\n },\n { status: 400 },\n )\n }\n for (const modelId of allowedModelsByProvider[providerId] ?? []) {\n if (!isModelAllowedForProvider(env, providerId, modelId)) {\n return NextResponse.json(\n {\n error: `Model \"${modelId}\" is not in ${modelAllowlistEnvVarName(providerId)}; tenant allowlist may not widen the env allowlist.`,\n code: 'model_not_in_env_allowlist',\n },\n { status: 400 },\n )\n }\n }\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const canManage = await rbacService.userHasAllFeatures(\n auth.sub,\n ['ai_assistant.settings.manage'],\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n if (!canManage) {\n return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiTenantModelAllowlistRepository(em)\n const row = await repo.upsert(\n { allowedProviders, allowedModelsByProvider },\n {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n },\n )\n return NextResponse.json({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n allowedProviders: row.allowedProviders ?? null,\n allowedModelsByProvider: row.allowedModelsByProvider ?? {},\n updatedAt: row.updatedAt,\n })\n } catch (error) {\n logger.error('AI Settings Allowlist \u2014 PUT error', { err: error })\n return NextResponse.json(\n { error: 'Failed to save tenant allowlist.' },\n { status: 500 },\n )\n }\n}\n\nexport async function DELETE(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const canManage = await rbacService.userHasAllFeatures(\n auth.sub,\n ['ai_assistant.settings.manage'],\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n if (!canManage) {\n return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiTenantModelAllowlistRepository(em)\n const cleared = await repo.clear({\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n })\n return NextResponse.json({ cleared })\n } catch (error) {\n logger.error('AI Settings Allowlist \u2014 DELETE error', { err: error })\n return NextResponse.json(\n { error: 'Failed to clear tenant allowlist.' },\n { status: 500 },\n )\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -505,11 +505,11 @@ async function PUT(req) {
505
505
  try {
506
506
  const container = await createRequestContainer();
507
507
  const rbacService = container.resolve("rbacService");
508
- const acl = await rbacService.loadAcl(auth.sub, {
509
- tenantId: auth.tenantId,
510
- organizationId: auth.orgId
511
- });
512
- const canManage = acl.isSuperAdmin || acl.features.includes("ai_assistant.settings.manage");
508
+ const canManage = await rbacService.userHasAllFeatures(
509
+ auth.sub,
510
+ ["ai_assistant.settings.manage"],
511
+ { tenantId: auth.tenantId, organizationId: auth.orgId }
512
+ );
513
513
  if (!canManage) {
514
514
  return NextResponse.json({ error: "Forbidden", code: "forbidden" }, { status: 403 });
515
515
  }
@@ -567,11 +567,11 @@ async function DELETE(req) {
567
567
  try {
568
568
  const container = await createRequestContainer();
569
569
  const rbacService = container.resolve("rbacService");
570
- const acl = await rbacService.loadAcl(auth.sub, {
571
- tenantId: auth.tenantId,
572
- organizationId: auth.orgId
573
- });
574
- const canManage = acl.isSuperAdmin || acl.features.includes("ai_assistant.settings.manage");
570
+ const canManage = await rbacService.userHasAllFeatures(
571
+ auth.sub,
572
+ ["ai_assistant.settings.manage"],
573
+ { tenantId: auth.tenantId, organizationId: auth.orgId }
574
+ );
575
575
  if (!canManage) {
576
576
  return NextResponse.json({ error: "Forbidden", code: "forbidden" }, { status: 403 });
577
577
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/ai_assistant/api/settings/route.ts"],
4
- "sourcesContent": ["import { existsSync, readFileSync } from 'node:fs'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { findApiKeyBySecret } from '@open-mercato/core/modules/api_keys/services/apiKeyService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { joinProviderModel } from '@open-mercato/shared/lib/ai/model-id'\nimport {\n OPEN_CODE_PROVIDER_IDS,\n OPEN_CODE_PROVIDERS,\n getOpenCodeProviderConfiguredEnvKey,\n isOpenCodeProviderConfigured,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport { AiAgentRuntimeOverrideRepository, AiAgentRuntimeOverrideValidationError } from '../../data/repositories/AiAgentRuntimeOverrideRepository'\nimport { AiTenantModelAllowlistRepository } from '../../data/repositories/AiTenantModelAllowlistRepository'\nimport { isBaseurlAllowlisted, readBaseurlAllowlist } from '../../lib/baseurl-allowlist'\nimport { loadAgentRegistry, listAgents } from '../../lib/agent-registry'\nimport { createModelFactory, resolveAllowRuntimeOverride } from '../../lib/model-factory'\nimport {\n agentOverrideModelAllowlistEnvVarName,\n agentOverrideProviderAllowlistEnvVarName,\n canonicalProviderId,\n hasAllowlistSnapshotRestrictions,\n intersectEffectiveAllowlistWithSnapshot,\n intersectAllowlists,\n isProviderAllowed,\n isProviderAllowedInEffective,\n isProviderModelAllowed,\n isProviderModelAllowedInEffective,\n modelAllowlistEnvVarName,\n readAgentRuntimeOverrideAllowlist,\n readAllowedModels,\n readAllowedProviders,\n readAllowlistConfig,\n type TenantAllowlistSnapshot,\n} from '../../lib/model-allowlist'\n\nconst logger = createLogger('ai_assistant')\n\nfunction modelCatalogWithAllowlistFallback(\n models: ReadonlyArray<{ id: string; name: string; contextWindow?: number | null; tags?: readonly string[] }>,\n allowlistModelIds: string[] | undefined,\n): ReadonlyArray<{ id: string; name: string; contextWindow?: number | null; tags?: readonly string[] }> {\n if (models.length > 0) return models\n return (allowlistModelIds ?? []).map((id) => ({ id, name: id }))\n}\n\nconst runtimeOverrideUpsertSchema = z.object({\n providerId: z.string().min(1).max(64).nullable().optional(),\n modelId: z.string().min(1).max(256).nullable().optional(),\n baseURL: z.string().url().max(2048).nullable().optional(),\n agentId: z.string().min(1).max(128).nullable().optional(),\n allowedOverrideProviders: z.array(z.string().min(1).max(64)).nullable().optional(),\n allowedOverrideModelsByProvider: z\n .record(z.string().min(1).max(64), z.array(z.string().min(1).max(256)))\n .optional(),\n})\n\nconst runtimeOverrideClearSchema = z.object({\n agentId: z.string().min(1).max(128).nullable().optional(),\n})\n\nexport type RuntimeOverrideUpsertBody = z.infer<typeof runtimeOverrideUpsertSchema>\nexport type RuntimeOverrideClearBody = z.infer<typeof runtimeOverrideClearSchema>\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI assistant settings',\n methods: {\n GET: { summary: 'Get AI provider configuration' },\n PUT: {\n summary: 'Upsert per-tenant AI runtime override',\n description:\n 'Creates or updates the per-tenant AI runtime override (provider, model, baseURL). ' +\n 'Optionally scoped to a specific agent via `agentId`. ' +\n 'Gated by `ai_assistant.settings.manage`. ' +\n 'baseURL must match AI_RUNTIME_BASEURL_ALLOWLIST when set.',\n requestBody: {\n contentType: 'application/json',\n description: 'Override payload. All fields nullable/optional; null explicitly clears the axis.',\n schema: runtimeOverrideUpsertSchema,\n },\n responses: [\n { status: 200, description: 'Override saved. Returns the saved row.' },\n ],\n errors: [\n { status: 400, description: 'Validation error: unknown provider, invalid URL, or baseURL not allowlisted.' },\n { status: 401, description: 'Unauthenticated.' },\n { status: 403, description: 'Caller lacks ai_assistant.settings.manage.' },\n ],\n },\n DELETE: {\n summary: 'Clear per-tenant AI runtime override',\n description:\n 'Soft-deletes the active per-tenant runtime override. ' +\n 'Pass `agentId` to clear only the agent-specific row; omit to clear the tenant-wide default. ' +\n 'Gated by `ai_assistant.settings.manage`. Idempotent \u2014 returns 200 with `cleared: false` when no active row existed.',\n requestBody: {\n contentType: 'application/json',\n description: 'Optional agentId to scope the delete.',\n schema: runtimeOverrideClearSchema,\n },\n responses: [\n { status: 200, description: 'Returns `{ cleared: boolean }` indicating whether a row was found and removed.' },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated.' },\n { status: 403, description: 'Caller lacks ai_assistant.settings.manage.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n PUT: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n}\n\n/**\n * GET /api/ai_assistant/settings\n *\n * Returns the current OpenCode provider configuration from environment variables\n * plus the Phase 4a additive fields: resolvedDefault, tenantOverride, agents[],\n * and availableProviders[].defaultModels.\n */\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const env = process.env as Record<string, string | undefined>\n const configuredProviderHint = env.OM_AI_PROVIDER?.trim() || env.OPENCODE_PROVIDER?.trim() || null\n const registryProviders = llmProviderRegistry.list()\n const knownProviderIdsForAllowlist: string[] = [\n ...OPEN_CODE_PROVIDER_IDS,\n ...registryProviders\n .map((p) => p.id)\n .filter((id) => !(OPEN_CODE_PROVIDER_IDS as readonly string[]).includes(id)),\n ]\n const registryProviderId = configuredProviderHint\n ? canonicalProviderId(configuredProviderHint, registryProviders.map((provider) => provider.id))\n : null\n const registryProvider = registryProviderId ? llmProviderRegistry.get(registryProviderId) : null\n const fallbackOpenCodeProviderId = (\n (configuredProviderHint\n ? canonicalProviderId(configuredProviderHint, OPEN_CODE_PROVIDER_IDS as readonly string[])\n : null) ?? 'openai'\n ) as keyof typeof OPEN_CODE_PROVIDERS\n const fallbackOpenCodeProvider = OPEN_CODE_PROVIDERS[fallbackOpenCodeProviderId]\n\n const providerId = registryProvider?.id ?? fallbackOpenCodeProviderId\n const providerName = registryProvider?.name ?? fallbackOpenCodeProvider?.name ?? providerId\n const defaultProviderModel = registryProvider?.defaultModel ?? fallbackOpenCodeProvider?.defaultModel ?? ''\n const configuredModelHint = env.OM_AI_MODEL?.trim() || env.OPENCODE_MODEL?.trim() || defaultProviderModel\n const fallbackModelWithProvider = joinProviderModel(providerId, configuredModelHint)\n const apiKeyConfigured = registryProvider\n ? registryProvider.isConfigured(env)\n : fallbackOpenCodeProvider\n ? isOpenCodeProviderConfigured(fallbackOpenCodeProviderId)\n : false\n const displayEnvKey = registryProvider\n ? registryProvider.getConfiguredEnvKey(env)\n : fallbackOpenCodeProvider\n ? getOpenCodeProviderConfiguredEnvKey(fallbackOpenCodeProviderId)\n : null\n\n // Check if the MCP API key is configured (required for MCP authentication):\n // either directly via MCP_SERVER_API_KEY or via the shared key file that\n // the containerized mcp service provisions (MCP_SERVER_API_KEY_FILE).\n // The file variant is validated against the api_keys table \u2014 the file\n // outlives DB resets and key deletion, so mere existence would report a\n // green badge while OpenCode -> MCP auth is actually broken.\n const mcpKeyFilePath = process.env.MCP_SERVER_API_KEY_FILE?.trim()\n let mcpKeyConfigured = !!process.env.MCP_SERVER_API_KEY?.trim()\n if (!mcpKeyConfigured && mcpKeyFilePath && existsSync(mcpKeyFilePath)) {\n try {\n const fileSecret = readFileSync(mcpKeyFilePath, 'utf8').trim()\n if (fileSecret.startsWith('omk_')) {\n const keyContainer = await createRequestContainer()\n const keyEm = keyContainer.resolve<EntityManager>('em')\n mcpKeyConfigured = !!(await findApiKeyBySecret(keyEm, fileSecret))\n }\n } catch {\n mcpKeyConfigured = false\n }\n }\n\n // Phase 4a: resolve tenant override row and per-agent resolution matrix\n let tenantOverride: {\n providerId: string | null\n modelId: string | null\n baseURL: string | null\n agentId: string | null\n updatedAt: string\n } | null = null\n\n let agentResolutions: Array<{\n agentId: string\n moduleId: string\n allowRuntimeOverride: boolean\n allowRuntimeModelOverride: boolean\n codeDefaultProviderId: string | null\n codeDefaultModelId: string | null\n override: {\n providerId: string | null\n modelId: string | null\n baseURL: string | null\n updatedAt: string\n } | null\n runtimeOverrideAllowlist: {\n env: TenantAllowlistSnapshot | null\n tenant: TenantAllowlistSnapshot | null\n effective: ReturnType<typeof intersectAllowlists>\n envVarNames: {\n providers: string\n modelsByProvider: Record<string, string>\n }\n }\n providerId: string\n modelId: string\n baseURL: string | null\n source: string\n }> = []\n\n let resolvedDefault: {\n providerId: string\n modelId: string\n baseURL: string | null\n source: string\n } | null = null\n\n let tenantAllowlistSnapshot: TenantAllowlistSnapshot | null = null\n\n try {\n const container = await createRequestContainer()\n const tenantId = auth.tenantId ?? null\n const organizationId = auth.orgId ?? null\n\n if (tenantId) {\n const em = container.resolve<EntityManager>('em')\n const repo = new AiAgentRuntimeOverrideRepository(em)\n const overrideRow = await repo.getDefault({ tenantId, organizationId, agentId: null })\n if (overrideRow) {\n tenantOverride = {\n providerId: overrideRow.providerId ?? null,\n modelId: overrideRow.modelId ?? null,\n baseURL: overrideRow.baseUrl ?? null,\n agentId: overrideRow.agentId ?? null,\n updatedAt: overrideRow.updatedAt.toISOString(),\n }\n }\n\n const allowlistRepo = new AiTenantModelAllowlistRepository(em)\n tenantAllowlistSnapshot = await allowlistRepo.getSnapshot({\n tenantId,\n organizationId,\n })\n\n const factory = createModelFactory(container)\n const defaultResolution = factory.resolveModel({\n tenantAllowlist: tenantAllowlistSnapshot,\n tenantOverride: tenantOverride\n ? { providerId: tenantOverride.providerId, modelId: tenantOverride.modelId, baseURL: tenantOverride.baseURL }\n : undefined,\n })\n resolvedDefault = {\n providerId: defaultResolution.providerId,\n modelId: defaultResolution.modelId,\n baseURL: defaultResolution.baseURL ?? null,\n source: defaultResolution.source,\n }\n\n await loadAgentRegistry()\n const agents = listAgents()\n const agentResolutionPromises = agents.map(async (agent) => {\n const agentOverrideRow = await repo.getExact({\n tenantId,\n organizationId,\n agentId: agent.id,\n })\n const agentTenantOverride = agentOverrideRow\n ? {\n providerId: agentOverrideRow.providerId ?? null,\n modelId: agentOverrideRow.modelId ?? null,\n baseURL: agentOverrideRow.baseUrl ?? null,\n }\n : (tenantOverride ?? undefined)\n const agentResolution = factory.resolveModel({\n moduleId: agent.moduleId,\n agentDefaultModel: agent.defaultModel,\n agentDefaultProvider: agent.defaultProvider,\n agentDefaultBaseUrl: agent.defaultBaseUrl,\n allowRuntimeOverride: resolveAllowRuntimeOverride(agent),\n tenantOverride: agentTenantOverride,\n tenantAllowlist: tenantAllowlistSnapshot,\n })\n const agentEnvAllowlist = readAgentRuntimeOverrideAllowlist(\n env,\n agent.id,\n knownProviderIdsForAllowlist,\n )\n const agentTenantAllowlist = agentOverrideRow\n ? {\n allowedProviders: agentOverrideRow.allowedOverrideProviders ?? null,\n allowedModelsByProvider: agentOverrideRow.allowedOverrideModelsByProvider ?? {},\n }\n : null\n const baseEffectiveAllowlist = intersectAllowlists(\n env,\n knownProviderIdsForAllowlist,\n tenantAllowlistSnapshot,\n )\n const agentEffectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(\n intersectEffectiveAllowlistWithSnapshot(\n baseEffectiveAllowlist,\n knownProviderIdsForAllowlist,\n agentEnvAllowlist,\n ),\n knownProviderIdsForAllowlist,\n agentTenantAllowlist,\n )\n const agentModelEnvVars = Object.fromEntries(\n knownProviderIdsForAllowlist.map((providerId) => [\n providerId,\n agentOverrideModelAllowlistEnvVarName(agent.id, providerId),\n ]),\n )\n return {\n agentId: agent.id,\n moduleId: agent.moduleId,\n allowRuntimeOverride: resolveAllowRuntimeOverride(agent),\n allowRuntimeModelOverride: resolveAllowRuntimeOverride(agent),\n codeDefaultProviderId: agent.defaultProvider ?? null,\n codeDefaultModelId: agent.defaultModel ?? null,\n override: agentOverrideRow\n ? {\n providerId: agentOverrideRow.providerId ?? null,\n modelId: agentOverrideRow.modelId ?? null,\n baseURL: agentOverrideRow.baseUrl ?? null,\n updatedAt: agentOverrideRow.updatedAt.toISOString(),\n }\n : null,\n runtimeOverrideAllowlist: {\n env: agentEnvAllowlist,\n tenant: hasAllowlistSnapshotRestrictions(agentTenantAllowlist)\n ? agentTenantAllowlist\n : null,\n effective: agentEffectiveAllowlist,\n envVarNames: {\n providers: agentOverrideProviderAllowlistEnvVarName(agent.id),\n modelsByProvider: agentModelEnvVars,\n },\n },\n providerId: agentResolution.providerId,\n modelId: agentResolution.modelId,\n baseURL: agentResolution.baseURL ?? null,\n source: agentResolution.source,\n }\n })\n agentResolutions = await Promise.all(agentResolutionPromises)\n }\n } catch (overrideError) {\n // Phase 4a fields are best-effort \u2014 log and continue returning the base response\n logger.warn('AI Settings \u2014 Failed to compute Phase 4a override fields', { err: overrideError })\n }\n\n // Build availableProviders with Phase 4a defaultModels, then clip to the\n // EFFECTIVE allowlist \u2014 env intersected with the per-tenant snapshot.\n // The env allowlist is the OUTER constraint; the tenant allowlist (Phase\n // 1780-6) narrows it further. The settings UI must never offer a value\n // the runtime would refuse to honor.\n const allowlistConfig = readAllowlistConfig(env, knownProviderIdsForAllowlist)\n const effectiveAllowlist = intersectAllowlists(\n env,\n knownProviderIdsForAllowlist,\n tenantAllowlistSnapshot,\n )\n\n const allRawProviders = [\n ...OPEN_CODE_PROVIDER_IDS.map((id) => {\n const info = OPEN_CODE_PROVIDERS[id]\n const registryProvider = llmProviderRegistry.get(id)\n return {\n id,\n name: info.name,\n defaultModel: info.defaultModel,\n envKey: getOpenCodeProviderConfiguredEnvKey(id),\n configured: isOpenCodeProviderConfigured(id),\n defaultModels: registryProvider?.defaultModels ?? [],\n }\n }),\n // Also surface any llmProviderRegistry providers not in OPEN_CODE_PROVIDER_IDS\n ...llmProviderRegistry.list()\n .filter((p) => !(OPEN_CODE_PROVIDER_IDS as readonly string[]).includes(p.id))\n .map((p) => ({\n id: p.id,\n name: p.name,\n defaultModel: p.defaultModels[0]?.id ?? '',\n envKey: null,\n configured: p.isConfigured(),\n defaultModels: p.defaultModels,\n })),\n ]\n\n const availableProviders = allRawProviders\n .filter((p) => isProviderAllowedInEffective(effectiveAllowlist, p.id))\n .map((p) => {\n const effectiveModelsList = effectiveAllowlist.modelsByProvider[p.id]\n const catalogModels = modelCatalogWithAllowlistFallback(\n p.defaultModels,\n effectiveModelsList,\n )\n const clippedDefaults = effectiveModelsList !== undefined\n ? catalogModels.filter((m) => effectiveModelsList.includes(m.id))\n : catalogModels\n return {\n ...p,\n defaultModel: effectiveModelsList && !effectiveModelsList.includes(p.defaultModel)\n ? (effectiveModelsList[0] ?? p.defaultModel)\n : p.defaultModel || clippedDefaults[0]?.id || '',\n defaultModels: clippedDefaults,\n }\n })\n\n const allowlistProviders = allRawProviders\n .filter((p) => isProviderAllowed(env, p.id))\n .map((p) => {\n const envModelsList = allowlistConfig.modelsByProvider[p.id]\n const catalogModels = modelCatalogWithAllowlistFallback(\n p.defaultModels,\n envModelsList,\n )\n const envClippedDefaults = envModelsList !== undefined\n ? catalogModels.filter((m) => envModelsList.includes(m.id))\n : catalogModels\n return {\n ...p,\n defaultModel: envModelsList && !envModelsList.includes(p.defaultModel)\n ? (envModelsList[0] ?? p.defaultModel)\n : p.defaultModel || envClippedDefaults[0]?.id || '',\n defaultModels: envClippedDefaults,\n }\n })\n\n return NextResponse.json({\n provider: {\n id: providerId,\n name: providerName,\n model: resolvedDefault\n ? joinProviderModel(resolvedDefault.providerId, resolvedDefault.modelId)\n : fallbackModelWithProvider,\n defaultModel: defaultProviderModel,\n envKey: displayEnvKey,\n configured: apiKeyConfigured,\n },\n availableProviders,\n // Editable universe for the tenant allowlist page. This is clipped only\n // by env so tenant-hidden models remain visible and can be re-enabled.\n allowlistProviders,\n // Snapshot of the env-driven allowlist so the UI can render hints like\n // \"limited to: openai, anthropic\" without re-implementing the parser.\n allowlist: allowlistConfig,\n // Per-tenant allowlist snapshot (Phase 1780-6). `null` when no row has\n // been persisted yet \u2014 the runtime then falls back to env-only\n // enforcement. The UI uses this to drive the editable MultiSelect.\n tenantAllowlist: tenantAllowlistSnapshot,\n // Effective allowlist after intersecting env with tenant. The UI uses\n // this to render the \"what the runtime will actually accept\" summary\n // and to clip pickers without re-implementing the intersection.\n effectiveAllowlist,\n mcpKeyConfigured,\n resolvedDefault,\n tenantOverride,\n agents: agentResolutions,\n })\n } catch (error) {\n logger.error('AI Settings \u2014 GET error', { err: error })\n return NextResponse.json({ error: 'Failed to fetch settings' }, { status: 500 })\n }\n}\n\n/**\n * PUT /api/ai_assistant/settings\n *\n * Upserts the per-tenant AI runtime override (Phase 4a). Requires\n * `ai_assistant.settings.manage`. The body is Zod-validated; a `baseURL`\n * must match `AI_RUNTIME_BASEURL_ALLOWLIST` when that env var is set.\n */\nexport async function PUT(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return NextResponse.json({ error: 'Request body must be valid JSON.', code: 'validation_error' }, { status: 400 })\n }\n\n const bodyResult = runtimeOverrideUpsertSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return NextResponse.json(\n { error: 'Invalid request body.', code: 'validation_error', issues: bodyResult.error.issues },\n { status: 400 },\n )\n }\n\n const { providerId: requestedProviderId, modelId, baseURL, agentId } = bodyResult.data\n const knownProviderIdsForRequest = llmProviderRegistry.list().map((p) => p.id)\n const providerId = requestedProviderId\n ? canonicalProviderId(requestedProviderId, knownProviderIdsForRequest) ?? requestedProviderId\n : requestedProviderId\n\n if (baseURL && baseURL.trim().length > 0) {\n const allowlist = readBaseurlAllowlist()\n if (!isBaseurlAllowlisted(baseURL.trim(), allowlist)) {\n return NextResponse.json(\n {\n error: `baseURL \"${baseURL}\" is not in AI_RUNTIME_BASEURL_ALLOWLIST.`,\n code: 'baseurl_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n\n const allowedOverrideProviders = bodyResult.data.allowedOverrideProviders === undefined\n ? undefined\n : bodyResult.data.allowedOverrideProviders?.map((id) =>\n canonicalProviderId(id, knownProviderIdsForRequest) ?? id,\n ) ?? null\n const allowedOverrideModelsByProvider = bodyResult.data.allowedOverrideModelsByProvider === undefined\n ? undefined\n : Object.fromEntries(\n Object.entries(bodyResult.data.allowedOverrideModelsByProvider ?? {}).map(([id, models]) => [\n canonicalProviderId(id, knownProviderIdsForRequest) ?? id,\n models,\n ]),\n )\n const hasRuntimeOverrideAllowlistWrite =\n allowedOverrideProviders !== undefined || allowedOverrideModelsByProvider !== undefined\n\n // Env-driven provider/model allowlist (Phase 1780-5) intersected with the\n // per-tenant allowlist (Phase 1780-6): the EFFECTIVE allowlist clips which\n // (provider, model) pairs the runtime accepts. Reject settings PUT requests\n // for pairs outside that effective set so the settings UI never persists a\n // value the runtime would later refuse. Tenant-allowlist enforcement is\n // best-effort here: if the snapshot lookup fails we fall back to env-only\n // checks (the runtime still re-clips at resolution time).\n let putEffectiveAllowlist: ReturnType<typeof intersectAllowlists> | null = null\n if (providerId || hasRuntimeOverrideAllowlistWrite) {\n try {\n const previewContainer = await createRequestContainer()\n const knownIdsForCheck = [\n ...OPEN_CODE_PROVIDER_IDS,\n ...llmProviderRegistry\n .list()\n .map((p) => p.id)\n .filter((id) => !(OPEN_CODE_PROVIDER_IDS as readonly string[]).includes(id)),\n ]\n let snapshot: TenantAllowlistSnapshot | null = null\n if (auth.tenantId) {\n try {\n const em = previewContainer.resolve<EntityManager>('em')\n const allowlistRepo = new AiTenantModelAllowlistRepository(em)\n snapshot = await allowlistRepo.getSnapshot({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n } catch {\n snapshot = null\n }\n }\n putEffectiveAllowlist = intersectAllowlists(\n process.env as Record<string, string | undefined>,\n knownIdsForCheck,\n snapshot,\n )\n } catch {\n putEffectiveAllowlist = null\n }\n\n if (providerId && putEffectiveAllowlist) {\n if (!isProviderAllowedInEffective(putEffectiveAllowlist, providerId)) {\n const source = putEffectiveAllowlist.tenantOverridesActive\n ? 'the effective allowlist (env \u2229 tenant)'\n : 'OM_AI_AVAILABLE_PROVIDERS'\n return NextResponse.json(\n {\n error: `Provider \"${providerId}\" is not in ${source}.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n if (modelId && !isProviderModelAllowedInEffective(putEffectiveAllowlist, providerId, modelId)) {\n const source = putEffectiveAllowlist.tenantOverridesActive\n ? `the effective allowlist (env \u2229 tenant) for \"${providerId}\"`\n : modelAllowlistEnvVarName(providerId)\n return NextResponse.json(\n {\n error: `Model \"${modelId}\" is not in ${source}.`,\n code: 'model_not_allowlisted',\n },\n { status: 400 },\n )\n }\n } else if (providerId) {\n if (!isProviderAllowed(process.env, providerId)) {\n return NextResponse.json(\n {\n error: `Provider \"${requestedProviderId}\" is not in OM_AI_AVAILABLE_PROVIDERS.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n if (modelId && !isProviderModelAllowed(process.env, providerId, modelId)) {\n return NextResponse.json(\n {\n error: `Model \"${modelId}\" is not in ${modelAllowlistEnvVarName(providerId)}.`,\n code: 'model_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n }\n\n if (hasRuntimeOverrideAllowlistWrite && !agentId) {\n return NextResponse.json(\n {\n error: 'agentId is required when saving chat override allowlist settings.',\n code: 'agent_required',\n },\n { status: 400 },\n )\n }\n\n if (Array.isArray(allowedOverrideProviders)) {\n for (const id of allowedOverrideProviders) {\n if (putEffectiveAllowlist && !isProviderAllowedInEffective(putEffectiveAllowlist, id)) {\n return NextResponse.json(\n {\n error: `Provider \"${id}\" is not in the effective tenant allowlist; per-agent chat override choices may not widen it.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n }\n if (allowedOverrideModelsByProvider) {\n for (const [id, models] of Object.entries(allowedOverrideModelsByProvider)) {\n if (putEffectiveAllowlist && !isProviderAllowedInEffective(putEffectiveAllowlist, id)) {\n return NextResponse.json(\n {\n error: `Provider \"${id}\" is not in the effective tenant allowlist; cannot save per-agent model choices for it.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n for (const allowedModelId of models) {\n if (\n putEffectiveAllowlist &&\n !isProviderModelAllowedInEffective(putEffectiveAllowlist, id, allowedModelId)\n ) {\n return NextResponse.json(\n {\n error: `Model \"${allowedModelId}\" is not in the effective tenant allowlist for \"${id}\".`,\n code: 'model_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n }\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const canManage = acl.isSuperAdmin || acl.features.includes('ai_assistant.settings.manage')\n if (!canManage) {\n return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiAgentRuntimeOverrideRepository(em)\n const upsertInput = {\n agentId: agentId ?? null,\n ...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'providerId')\n ? { providerId: providerId ?? null }\n : {}),\n ...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'modelId')\n ? { modelId: modelId ?? null }\n : {}),\n ...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'baseURL')\n ? { baseURL: baseURL ?? null }\n : {}),\n ...(allowedOverrideProviders !== undefined\n ? { allowedOverrideProviders }\n : {}),\n ...(allowedOverrideModelsByProvider !== undefined\n ? { allowedOverrideModelsByProvider }\n : {}),\n }\n const row = await repo.upsertDefault(\n upsertInput,\n { tenantId: auth.tenantId ?? '', organizationId: auth.orgId ?? null, userId: auth.sub },\n )\n return NextResponse.json({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n agentId: row.agentId,\n providerId: row.providerId,\n modelId: row.modelId,\n baseURL: row.baseUrl,\n allowedOverrideProviders: row.allowedOverrideProviders ?? null,\n allowedOverrideModelsByProvider: row.allowedOverrideModelsByProvider ?? {},\n updatedAt: row.updatedAt,\n })\n } catch (error) {\n if (error instanceof AiAgentRuntimeOverrideValidationError) {\n return NextResponse.json({ error: error.message, code: 'provider_unknown' }, { status: 400 })\n }\n logger.error('AI Settings \u2014 PUT error', { err: error })\n return NextResponse.json({ error: 'Failed to save runtime override.' }, { status: 500 })\n }\n}\n\n/**\n * DELETE /api/ai_assistant/settings\n *\n * Soft-deletes the active per-tenant AI runtime override (Phase 4a). Requires\n * `ai_assistant.settings.manage`. Pass `agentId` in the body to clear only\n * the agent-specific row; omit (or null) to clear the tenant-wide default.\n * Idempotent \u2014 returns `{ cleared: false }` when no active row was found.\n */\nexport async function DELETE(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let parsedBody: unknown = {}\n try {\n parsedBody = await req.json()\n } catch {\n // Body is optional for DELETE \u2014 empty body is fine\n }\n\n const bodyResult = runtimeOverrideClearSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return NextResponse.json(\n { error: 'Invalid request body.', code: 'validation_error', issues: bodyResult.error.issues },\n { status: 400 },\n )\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const canManage = acl.isSuperAdmin || acl.features.includes('ai_assistant.settings.manage')\n if (!canManage) {\n return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiAgentRuntimeOverrideRepository(em)\n const cleared = await repo.clearDefault({\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n agentId: bodyResult.data.agentId ?? null,\n })\n return NextResponse.json({ cleared })\n } catch (error) {\n logger.error('AI Settings \u2014 DELETE error', { err: error })\n return NextResponse.json({ error: 'Failed to clear runtime override.' }, { status: 500 })\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { existsSync, readFileSync } from 'node:fs'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { findApiKeyBySecret } from '@open-mercato/core/modules/api_keys/services/apiKeyService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { joinProviderModel } from '@open-mercato/shared/lib/ai/model-id'\nimport {\n OPEN_CODE_PROVIDER_IDS,\n OPEN_CODE_PROVIDERS,\n getOpenCodeProviderConfiguredEnvKey,\n isOpenCodeProviderConfigured,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport { AiAgentRuntimeOverrideRepository, AiAgentRuntimeOverrideValidationError } from '../../data/repositories/AiAgentRuntimeOverrideRepository'\nimport { AiTenantModelAllowlistRepository } from '../../data/repositories/AiTenantModelAllowlistRepository'\nimport { isBaseurlAllowlisted, readBaseurlAllowlist } from '../../lib/baseurl-allowlist'\nimport { loadAgentRegistry, listAgents } from '../../lib/agent-registry'\nimport { createModelFactory, resolveAllowRuntimeOverride } from '../../lib/model-factory'\nimport {\n agentOverrideModelAllowlistEnvVarName,\n agentOverrideProviderAllowlistEnvVarName,\n canonicalProviderId,\n hasAllowlistSnapshotRestrictions,\n intersectEffectiveAllowlistWithSnapshot,\n intersectAllowlists,\n isProviderAllowed,\n isProviderAllowedInEffective,\n isProviderModelAllowed,\n isProviderModelAllowedInEffective,\n modelAllowlistEnvVarName,\n readAgentRuntimeOverrideAllowlist,\n readAllowedModels,\n readAllowedProviders,\n readAllowlistConfig,\n type TenantAllowlistSnapshot,\n} from '../../lib/model-allowlist'\n\nconst logger = createLogger('ai_assistant')\n\nfunction modelCatalogWithAllowlistFallback(\n models: ReadonlyArray<{ id: string; name: string; contextWindow?: number | null; tags?: readonly string[] }>,\n allowlistModelIds: string[] | undefined,\n): ReadonlyArray<{ id: string; name: string; contextWindow?: number | null; tags?: readonly string[] }> {\n if (models.length > 0) return models\n return (allowlistModelIds ?? []).map((id) => ({ id, name: id }))\n}\n\nconst runtimeOverrideUpsertSchema = z.object({\n providerId: z.string().min(1).max(64).nullable().optional(),\n modelId: z.string().min(1).max(256).nullable().optional(),\n baseURL: z.string().url().max(2048).nullable().optional(),\n agentId: z.string().min(1).max(128).nullable().optional(),\n allowedOverrideProviders: z.array(z.string().min(1).max(64)).nullable().optional(),\n allowedOverrideModelsByProvider: z\n .record(z.string().min(1).max(64), z.array(z.string().min(1).max(256)))\n .optional(),\n})\n\nconst runtimeOverrideClearSchema = z.object({\n agentId: z.string().min(1).max(128).nullable().optional(),\n})\n\nexport type RuntimeOverrideUpsertBody = z.infer<typeof runtimeOverrideUpsertSchema>\nexport type RuntimeOverrideClearBody = z.infer<typeof runtimeOverrideClearSchema>\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI assistant settings',\n methods: {\n GET: { summary: 'Get AI provider configuration' },\n PUT: {\n summary: 'Upsert per-tenant AI runtime override',\n description:\n 'Creates or updates the per-tenant AI runtime override (provider, model, baseURL). ' +\n 'Optionally scoped to a specific agent via `agentId`. ' +\n 'Gated by `ai_assistant.settings.manage`. ' +\n 'baseURL must match AI_RUNTIME_BASEURL_ALLOWLIST when set.',\n requestBody: {\n contentType: 'application/json',\n description: 'Override payload. All fields nullable/optional; null explicitly clears the axis.',\n schema: runtimeOverrideUpsertSchema,\n },\n responses: [\n { status: 200, description: 'Override saved. Returns the saved row.' },\n ],\n errors: [\n { status: 400, description: 'Validation error: unknown provider, invalid URL, or baseURL not allowlisted.' },\n { status: 401, description: 'Unauthenticated.' },\n { status: 403, description: 'Caller lacks ai_assistant.settings.manage.' },\n ],\n },\n DELETE: {\n summary: 'Clear per-tenant AI runtime override',\n description:\n 'Soft-deletes the active per-tenant runtime override. ' +\n 'Pass `agentId` to clear only the agent-specific row; omit to clear the tenant-wide default. ' +\n 'Gated by `ai_assistant.settings.manage`. Idempotent \u2014 returns 200 with `cleared: false` when no active row existed.',\n requestBody: {\n contentType: 'application/json',\n description: 'Optional agentId to scope the delete.',\n schema: runtimeOverrideClearSchema,\n },\n responses: [\n { status: 200, description: 'Returns `{ cleared: boolean }` indicating whether a row was found and removed.' },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated.' },\n { status: 403, description: 'Caller lacks ai_assistant.settings.manage.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n PUT: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n}\n\n/**\n * GET /api/ai_assistant/settings\n *\n * Returns the current OpenCode provider configuration from environment variables\n * plus the Phase 4a additive fields: resolvedDefault, tenantOverride, agents[],\n * and availableProviders[].defaultModels.\n */\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const env = process.env as Record<string, string | undefined>\n const configuredProviderHint = env.OM_AI_PROVIDER?.trim() || env.OPENCODE_PROVIDER?.trim() || null\n const registryProviders = llmProviderRegistry.list()\n const knownProviderIdsForAllowlist: string[] = [\n ...OPEN_CODE_PROVIDER_IDS,\n ...registryProviders\n .map((p) => p.id)\n .filter((id) => !(OPEN_CODE_PROVIDER_IDS as readonly string[]).includes(id)),\n ]\n const registryProviderId = configuredProviderHint\n ? canonicalProviderId(configuredProviderHint, registryProviders.map((provider) => provider.id))\n : null\n const registryProvider = registryProviderId ? llmProviderRegistry.get(registryProviderId) : null\n const fallbackOpenCodeProviderId = (\n (configuredProviderHint\n ? canonicalProviderId(configuredProviderHint, OPEN_CODE_PROVIDER_IDS as readonly string[])\n : null) ?? 'openai'\n ) as keyof typeof OPEN_CODE_PROVIDERS\n const fallbackOpenCodeProvider = OPEN_CODE_PROVIDERS[fallbackOpenCodeProviderId]\n\n const providerId = registryProvider?.id ?? fallbackOpenCodeProviderId\n const providerName = registryProvider?.name ?? fallbackOpenCodeProvider?.name ?? providerId\n const defaultProviderModel = registryProvider?.defaultModel ?? fallbackOpenCodeProvider?.defaultModel ?? ''\n const configuredModelHint = env.OM_AI_MODEL?.trim() || env.OPENCODE_MODEL?.trim() || defaultProviderModel\n const fallbackModelWithProvider = joinProviderModel(providerId, configuredModelHint)\n const apiKeyConfigured = registryProvider\n ? registryProvider.isConfigured(env)\n : fallbackOpenCodeProvider\n ? isOpenCodeProviderConfigured(fallbackOpenCodeProviderId)\n : false\n const displayEnvKey = registryProvider\n ? registryProvider.getConfiguredEnvKey(env)\n : fallbackOpenCodeProvider\n ? getOpenCodeProviderConfiguredEnvKey(fallbackOpenCodeProviderId)\n : null\n\n // Check if the MCP API key is configured (required for MCP authentication):\n // either directly via MCP_SERVER_API_KEY or via the shared key file that\n // the containerized mcp service provisions (MCP_SERVER_API_KEY_FILE).\n // The file variant is validated against the api_keys table \u2014 the file\n // outlives DB resets and key deletion, so mere existence would report a\n // green badge while OpenCode -> MCP auth is actually broken.\n const mcpKeyFilePath = process.env.MCP_SERVER_API_KEY_FILE?.trim()\n let mcpKeyConfigured = !!process.env.MCP_SERVER_API_KEY?.trim()\n if (!mcpKeyConfigured && mcpKeyFilePath && existsSync(mcpKeyFilePath)) {\n try {\n const fileSecret = readFileSync(mcpKeyFilePath, 'utf8').trim()\n if (fileSecret.startsWith('omk_')) {\n const keyContainer = await createRequestContainer()\n const keyEm = keyContainer.resolve<EntityManager>('em')\n mcpKeyConfigured = !!(await findApiKeyBySecret(keyEm, fileSecret))\n }\n } catch {\n mcpKeyConfigured = false\n }\n }\n\n // Phase 4a: resolve tenant override row and per-agent resolution matrix\n let tenantOverride: {\n providerId: string | null\n modelId: string | null\n baseURL: string | null\n agentId: string | null\n updatedAt: string\n } | null = null\n\n let agentResolutions: Array<{\n agentId: string\n moduleId: string\n allowRuntimeOverride: boolean\n allowRuntimeModelOverride: boolean\n codeDefaultProviderId: string | null\n codeDefaultModelId: string | null\n override: {\n providerId: string | null\n modelId: string | null\n baseURL: string | null\n updatedAt: string\n } | null\n runtimeOverrideAllowlist: {\n env: TenantAllowlistSnapshot | null\n tenant: TenantAllowlistSnapshot | null\n effective: ReturnType<typeof intersectAllowlists>\n envVarNames: {\n providers: string\n modelsByProvider: Record<string, string>\n }\n }\n providerId: string\n modelId: string\n baseURL: string | null\n source: string\n }> = []\n\n let resolvedDefault: {\n providerId: string\n modelId: string\n baseURL: string | null\n source: string\n } | null = null\n\n let tenantAllowlistSnapshot: TenantAllowlistSnapshot | null = null\n\n try {\n const container = await createRequestContainer()\n const tenantId = auth.tenantId ?? null\n const organizationId = auth.orgId ?? null\n\n if (tenantId) {\n const em = container.resolve<EntityManager>('em')\n const repo = new AiAgentRuntimeOverrideRepository(em)\n const overrideRow = await repo.getDefault({ tenantId, organizationId, agentId: null })\n if (overrideRow) {\n tenantOverride = {\n providerId: overrideRow.providerId ?? null,\n modelId: overrideRow.modelId ?? null,\n baseURL: overrideRow.baseUrl ?? null,\n agentId: overrideRow.agentId ?? null,\n updatedAt: overrideRow.updatedAt.toISOString(),\n }\n }\n\n const allowlistRepo = new AiTenantModelAllowlistRepository(em)\n tenantAllowlistSnapshot = await allowlistRepo.getSnapshot({\n tenantId,\n organizationId,\n })\n\n const factory = createModelFactory(container)\n const defaultResolution = factory.resolveModel({\n tenantAllowlist: tenantAllowlistSnapshot,\n tenantOverride: tenantOverride\n ? { providerId: tenantOverride.providerId, modelId: tenantOverride.modelId, baseURL: tenantOverride.baseURL }\n : undefined,\n })\n resolvedDefault = {\n providerId: defaultResolution.providerId,\n modelId: defaultResolution.modelId,\n baseURL: defaultResolution.baseURL ?? null,\n source: defaultResolution.source,\n }\n\n await loadAgentRegistry()\n const agents = listAgents()\n const agentResolutionPromises = agents.map(async (agent) => {\n const agentOverrideRow = await repo.getExact({\n tenantId,\n organizationId,\n agentId: agent.id,\n })\n const agentTenantOverride = agentOverrideRow\n ? {\n providerId: agentOverrideRow.providerId ?? null,\n modelId: agentOverrideRow.modelId ?? null,\n baseURL: agentOverrideRow.baseUrl ?? null,\n }\n : (tenantOverride ?? undefined)\n const agentResolution = factory.resolveModel({\n moduleId: agent.moduleId,\n agentDefaultModel: agent.defaultModel,\n agentDefaultProvider: agent.defaultProvider,\n agentDefaultBaseUrl: agent.defaultBaseUrl,\n allowRuntimeOverride: resolveAllowRuntimeOverride(agent),\n tenantOverride: agentTenantOverride,\n tenantAllowlist: tenantAllowlistSnapshot,\n })\n const agentEnvAllowlist = readAgentRuntimeOverrideAllowlist(\n env,\n agent.id,\n knownProviderIdsForAllowlist,\n )\n const agentTenantAllowlist = agentOverrideRow\n ? {\n allowedProviders: agentOverrideRow.allowedOverrideProviders ?? null,\n allowedModelsByProvider: agentOverrideRow.allowedOverrideModelsByProvider ?? {},\n }\n : null\n const baseEffectiveAllowlist = intersectAllowlists(\n env,\n knownProviderIdsForAllowlist,\n tenantAllowlistSnapshot,\n )\n const agentEffectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(\n intersectEffectiveAllowlistWithSnapshot(\n baseEffectiveAllowlist,\n knownProviderIdsForAllowlist,\n agentEnvAllowlist,\n ),\n knownProviderIdsForAllowlist,\n agentTenantAllowlist,\n )\n const agentModelEnvVars = Object.fromEntries(\n knownProviderIdsForAllowlist.map((providerId) => [\n providerId,\n agentOverrideModelAllowlistEnvVarName(agent.id, providerId),\n ]),\n )\n return {\n agentId: agent.id,\n moduleId: agent.moduleId,\n allowRuntimeOverride: resolveAllowRuntimeOverride(agent),\n allowRuntimeModelOverride: resolveAllowRuntimeOverride(agent),\n codeDefaultProviderId: agent.defaultProvider ?? null,\n codeDefaultModelId: agent.defaultModel ?? null,\n override: agentOverrideRow\n ? {\n providerId: agentOverrideRow.providerId ?? null,\n modelId: agentOverrideRow.modelId ?? null,\n baseURL: agentOverrideRow.baseUrl ?? null,\n updatedAt: agentOverrideRow.updatedAt.toISOString(),\n }\n : null,\n runtimeOverrideAllowlist: {\n env: agentEnvAllowlist,\n tenant: hasAllowlistSnapshotRestrictions(agentTenantAllowlist)\n ? agentTenantAllowlist\n : null,\n effective: agentEffectiveAllowlist,\n envVarNames: {\n providers: agentOverrideProviderAllowlistEnvVarName(agent.id),\n modelsByProvider: agentModelEnvVars,\n },\n },\n providerId: agentResolution.providerId,\n modelId: agentResolution.modelId,\n baseURL: agentResolution.baseURL ?? null,\n source: agentResolution.source,\n }\n })\n agentResolutions = await Promise.all(agentResolutionPromises)\n }\n } catch (overrideError) {\n // Phase 4a fields are best-effort \u2014 log and continue returning the base response\n logger.warn('AI Settings \u2014 Failed to compute Phase 4a override fields', { err: overrideError })\n }\n\n // Build availableProviders with Phase 4a defaultModels, then clip to the\n // EFFECTIVE allowlist \u2014 env intersected with the per-tenant snapshot.\n // The env allowlist is the OUTER constraint; the tenant allowlist (Phase\n // 1780-6) narrows it further. The settings UI must never offer a value\n // the runtime would refuse to honor.\n const allowlistConfig = readAllowlistConfig(env, knownProviderIdsForAllowlist)\n const effectiveAllowlist = intersectAllowlists(\n env,\n knownProviderIdsForAllowlist,\n tenantAllowlistSnapshot,\n )\n\n const allRawProviders = [\n ...OPEN_CODE_PROVIDER_IDS.map((id) => {\n const info = OPEN_CODE_PROVIDERS[id]\n const registryProvider = llmProviderRegistry.get(id)\n return {\n id,\n name: info.name,\n defaultModel: info.defaultModel,\n envKey: getOpenCodeProviderConfiguredEnvKey(id),\n configured: isOpenCodeProviderConfigured(id),\n defaultModels: registryProvider?.defaultModels ?? [],\n }\n }),\n // Also surface any llmProviderRegistry providers not in OPEN_CODE_PROVIDER_IDS\n ...llmProviderRegistry.list()\n .filter((p) => !(OPEN_CODE_PROVIDER_IDS as readonly string[]).includes(p.id))\n .map((p) => ({\n id: p.id,\n name: p.name,\n defaultModel: p.defaultModels[0]?.id ?? '',\n envKey: null,\n configured: p.isConfigured(),\n defaultModels: p.defaultModels,\n })),\n ]\n\n const availableProviders = allRawProviders\n .filter((p) => isProviderAllowedInEffective(effectiveAllowlist, p.id))\n .map((p) => {\n const effectiveModelsList = effectiveAllowlist.modelsByProvider[p.id]\n const catalogModels = modelCatalogWithAllowlistFallback(\n p.defaultModels,\n effectiveModelsList,\n )\n const clippedDefaults = effectiveModelsList !== undefined\n ? catalogModels.filter((m) => effectiveModelsList.includes(m.id))\n : catalogModels\n return {\n ...p,\n defaultModel: effectiveModelsList && !effectiveModelsList.includes(p.defaultModel)\n ? (effectiveModelsList[0] ?? p.defaultModel)\n : p.defaultModel || clippedDefaults[0]?.id || '',\n defaultModels: clippedDefaults,\n }\n })\n\n const allowlistProviders = allRawProviders\n .filter((p) => isProviderAllowed(env, p.id))\n .map((p) => {\n const envModelsList = allowlistConfig.modelsByProvider[p.id]\n const catalogModels = modelCatalogWithAllowlistFallback(\n p.defaultModels,\n envModelsList,\n )\n const envClippedDefaults = envModelsList !== undefined\n ? catalogModels.filter((m) => envModelsList.includes(m.id))\n : catalogModels\n return {\n ...p,\n defaultModel: envModelsList && !envModelsList.includes(p.defaultModel)\n ? (envModelsList[0] ?? p.defaultModel)\n : p.defaultModel || envClippedDefaults[0]?.id || '',\n defaultModels: envClippedDefaults,\n }\n })\n\n return NextResponse.json({\n provider: {\n id: providerId,\n name: providerName,\n model: resolvedDefault\n ? joinProviderModel(resolvedDefault.providerId, resolvedDefault.modelId)\n : fallbackModelWithProvider,\n defaultModel: defaultProviderModel,\n envKey: displayEnvKey,\n configured: apiKeyConfigured,\n },\n availableProviders,\n // Editable universe for the tenant allowlist page. This is clipped only\n // by env so tenant-hidden models remain visible and can be re-enabled.\n allowlistProviders,\n // Snapshot of the env-driven allowlist so the UI can render hints like\n // \"limited to: openai, anthropic\" without re-implementing the parser.\n allowlist: allowlistConfig,\n // Per-tenant allowlist snapshot (Phase 1780-6). `null` when no row has\n // been persisted yet \u2014 the runtime then falls back to env-only\n // enforcement. The UI uses this to drive the editable MultiSelect.\n tenantAllowlist: tenantAllowlistSnapshot,\n // Effective allowlist after intersecting env with tenant. The UI uses\n // this to render the \"what the runtime will actually accept\" summary\n // and to clip pickers without re-implementing the intersection.\n effectiveAllowlist,\n mcpKeyConfigured,\n resolvedDefault,\n tenantOverride,\n agents: agentResolutions,\n })\n } catch (error) {\n logger.error('AI Settings \u2014 GET error', { err: error })\n return NextResponse.json({ error: 'Failed to fetch settings' }, { status: 500 })\n }\n}\n\n/**\n * PUT /api/ai_assistant/settings\n *\n * Upserts the per-tenant AI runtime override (Phase 4a). Requires\n * `ai_assistant.settings.manage`. The body is Zod-validated; a `baseURL`\n * must match `AI_RUNTIME_BASEURL_ALLOWLIST` when that env var is set.\n */\nexport async function PUT(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return NextResponse.json({ error: 'Request body must be valid JSON.', code: 'validation_error' }, { status: 400 })\n }\n\n const bodyResult = runtimeOverrideUpsertSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return NextResponse.json(\n { error: 'Invalid request body.', code: 'validation_error', issues: bodyResult.error.issues },\n { status: 400 },\n )\n }\n\n const { providerId: requestedProviderId, modelId, baseURL, agentId } = bodyResult.data\n const knownProviderIdsForRequest = llmProviderRegistry.list().map((p) => p.id)\n const providerId = requestedProviderId\n ? canonicalProviderId(requestedProviderId, knownProviderIdsForRequest) ?? requestedProviderId\n : requestedProviderId\n\n if (baseURL && baseURL.trim().length > 0) {\n const allowlist = readBaseurlAllowlist()\n if (!isBaseurlAllowlisted(baseURL.trim(), allowlist)) {\n return NextResponse.json(\n {\n error: `baseURL \"${baseURL}\" is not in AI_RUNTIME_BASEURL_ALLOWLIST.`,\n code: 'baseurl_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n\n const allowedOverrideProviders = bodyResult.data.allowedOverrideProviders === undefined\n ? undefined\n : bodyResult.data.allowedOverrideProviders?.map((id) =>\n canonicalProviderId(id, knownProviderIdsForRequest) ?? id,\n ) ?? null\n const allowedOverrideModelsByProvider = bodyResult.data.allowedOverrideModelsByProvider === undefined\n ? undefined\n : Object.fromEntries(\n Object.entries(bodyResult.data.allowedOverrideModelsByProvider ?? {}).map(([id, models]) => [\n canonicalProviderId(id, knownProviderIdsForRequest) ?? id,\n models,\n ]),\n )\n const hasRuntimeOverrideAllowlistWrite =\n allowedOverrideProviders !== undefined || allowedOverrideModelsByProvider !== undefined\n\n // Env-driven provider/model allowlist (Phase 1780-5) intersected with the\n // per-tenant allowlist (Phase 1780-6): the EFFECTIVE allowlist clips which\n // (provider, model) pairs the runtime accepts. Reject settings PUT requests\n // for pairs outside that effective set so the settings UI never persists a\n // value the runtime would later refuse. Tenant-allowlist enforcement is\n // best-effort here: if the snapshot lookup fails we fall back to env-only\n // checks (the runtime still re-clips at resolution time).\n let putEffectiveAllowlist: ReturnType<typeof intersectAllowlists> | null = null\n if (providerId || hasRuntimeOverrideAllowlistWrite) {\n try {\n const previewContainer = await createRequestContainer()\n const knownIdsForCheck = [\n ...OPEN_CODE_PROVIDER_IDS,\n ...llmProviderRegistry\n .list()\n .map((p) => p.id)\n .filter((id) => !(OPEN_CODE_PROVIDER_IDS as readonly string[]).includes(id)),\n ]\n let snapshot: TenantAllowlistSnapshot | null = null\n if (auth.tenantId) {\n try {\n const em = previewContainer.resolve<EntityManager>('em')\n const allowlistRepo = new AiTenantModelAllowlistRepository(em)\n snapshot = await allowlistRepo.getSnapshot({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n } catch {\n snapshot = null\n }\n }\n putEffectiveAllowlist = intersectAllowlists(\n process.env as Record<string, string | undefined>,\n knownIdsForCheck,\n snapshot,\n )\n } catch {\n putEffectiveAllowlist = null\n }\n\n if (providerId && putEffectiveAllowlist) {\n if (!isProviderAllowedInEffective(putEffectiveAllowlist, providerId)) {\n const source = putEffectiveAllowlist.tenantOverridesActive\n ? 'the effective allowlist (env \u2229 tenant)'\n : 'OM_AI_AVAILABLE_PROVIDERS'\n return NextResponse.json(\n {\n error: `Provider \"${providerId}\" is not in ${source}.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n if (modelId && !isProviderModelAllowedInEffective(putEffectiveAllowlist, providerId, modelId)) {\n const source = putEffectiveAllowlist.tenantOverridesActive\n ? `the effective allowlist (env \u2229 tenant) for \"${providerId}\"`\n : modelAllowlistEnvVarName(providerId)\n return NextResponse.json(\n {\n error: `Model \"${modelId}\" is not in ${source}.`,\n code: 'model_not_allowlisted',\n },\n { status: 400 },\n )\n }\n } else if (providerId) {\n if (!isProviderAllowed(process.env, providerId)) {\n return NextResponse.json(\n {\n error: `Provider \"${requestedProviderId}\" is not in OM_AI_AVAILABLE_PROVIDERS.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n if (modelId && !isProviderModelAllowed(process.env, providerId, modelId)) {\n return NextResponse.json(\n {\n error: `Model \"${modelId}\" is not in ${modelAllowlistEnvVarName(providerId)}.`,\n code: 'model_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n }\n\n if (hasRuntimeOverrideAllowlistWrite && !agentId) {\n return NextResponse.json(\n {\n error: 'agentId is required when saving chat override allowlist settings.',\n code: 'agent_required',\n },\n { status: 400 },\n )\n }\n\n if (Array.isArray(allowedOverrideProviders)) {\n for (const id of allowedOverrideProviders) {\n if (putEffectiveAllowlist && !isProviderAllowedInEffective(putEffectiveAllowlist, id)) {\n return NextResponse.json(\n {\n error: `Provider \"${id}\" is not in the effective tenant allowlist; per-agent chat override choices may not widen it.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n }\n if (allowedOverrideModelsByProvider) {\n for (const [id, models] of Object.entries(allowedOverrideModelsByProvider)) {\n if (putEffectiveAllowlist && !isProviderAllowedInEffective(putEffectiveAllowlist, id)) {\n return NextResponse.json(\n {\n error: `Provider \"${id}\" is not in the effective tenant allowlist; cannot save per-agent model choices for it.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n for (const allowedModelId of models) {\n if (\n putEffectiveAllowlist &&\n !isProviderModelAllowedInEffective(putEffectiveAllowlist, id, allowedModelId)\n ) {\n return NextResponse.json(\n {\n error: `Model \"${allowedModelId}\" is not in the effective tenant allowlist for \"${id}\".`,\n code: 'model_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n }\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const canManage = await rbacService.userHasAllFeatures(\n auth.sub,\n ['ai_assistant.settings.manage'],\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n if (!canManage) {\n return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiAgentRuntimeOverrideRepository(em)\n const upsertInput = {\n agentId: agentId ?? null,\n ...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'providerId')\n ? { providerId: providerId ?? null }\n : {}),\n ...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'modelId')\n ? { modelId: modelId ?? null }\n : {}),\n ...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'baseURL')\n ? { baseURL: baseURL ?? null }\n : {}),\n ...(allowedOverrideProviders !== undefined\n ? { allowedOverrideProviders }\n : {}),\n ...(allowedOverrideModelsByProvider !== undefined\n ? { allowedOverrideModelsByProvider }\n : {}),\n }\n const row = await repo.upsertDefault(\n upsertInput,\n { tenantId: auth.tenantId ?? '', organizationId: auth.orgId ?? null, userId: auth.sub },\n )\n return NextResponse.json({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n agentId: row.agentId,\n providerId: row.providerId,\n modelId: row.modelId,\n baseURL: row.baseUrl,\n allowedOverrideProviders: row.allowedOverrideProviders ?? null,\n allowedOverrideModelsByProvider: row.allowedOverrideModelsByProvider ?? {},\n updatedAt: row.updatedAt,\n })\n } catch (error) {\n if (error instanceof AiAgentRuntimeOverrideValidationError) {\n return NextResponse.json({ error: error.message, code: 'provider_unknown' }, { status: 400 })\n }\n logger.error('AI Settings \u2014 PUT error', { err: error })\n return NextResponse.json({ error: 'Failed to save runtime override.' }, { status: 500 })\n }\n}\n\n/**\n * DELETE /api/ai_assistant/settings\n *\n * Soft-deletes the active per-tenant AI runtime override (Phase 4a). Requires\n * `ai_assistant.settings.manage`. Pass `agentId` in the body to clear only\n * the agent-specific row; omit (or null) to clear the tenant-wide default.\n * Idempotent \u2014 returns `{ cleared: false }` when no active row was found.\n */\nexport async function DELETE(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let parsedBody: unknown = {}\n try {\n parsedBody = await req.json()\n } catch {\n // Body is optional for DELETE \u2014 empty body is fine\n }\n\n const bodyResult = runtimeOverrideClearSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return NextResponse.json(\n { error: 'Invalid request body.', code: 'validation_error', issues: bodyResult.error.issues },\n { status: 400 },\n )\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const canManage = await rbacService.userHasAllFeatures(\n auth.sub,\n ['ai_assistant.settings.manage'],\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n if (!canManage) {\n return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiAgentRuntimeOverrideRepository(em)\n const cleared = await repo.clearDefault({\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n agentId: bodyResult.data.agentId ?? null,\n })\n return NextResponse.json({ cleared })\n } catch (error) {\n logger.error('AI Settings \u2014 DELETE error', { err: error })\n return NextResponse.json({ error: 'Failed to clear runtime override.' }, { status: 500 })\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["providerId", "registryProvider"]
7
7
  }
@@ -1,5 +1,5 @@
1
1
  import { createLogger } from "@open-mercato/shared/lib/logger";
2
- import { hasAllFeatures } from "@open-mercato/shared/lib/auth/featureMatch";
2
+ import { authorizeFeatures } from "@open-mercato/shared/security/featurePolicy";
3
3
  const logger = createLogger("ai_assistant");
4
4
  async function authenticateMcpRequest(apiKeySecret, container) {
5
5
  if (!apiKeySecret || typeof apiKeySecret !== "string") {
@@ -47,12 +47,11 @@ async function authenticateMcpRequest(apiKeySecret, container) {
47
47
  }
48
48
  }
49
49
  function hasRequiredFeatures(requiredFeatures, userFeatures, isSuperAdmin, rbacService) {
50
- if (isSuperAdmin) return true;
51
- if (!requiredFeatures?.length) return true;
52
- if (rbacService) {
53
- return rbacService.hasAllFeatures(requiredFeatures, userFeatures);
54
- }
55
- return hasAllFeatures(requiredFeatures, userFeatures);
50
+ void rbacService;
51
+ return authorizeFeatures(requiredFeatures ?? [], {
52
+ grantedFeatures: userFeatures,
53
+ unrestricted: isSuperAdmin
54
+ });
56
55
  }
57
56
  function extractApiKeyFromHeaders(headers) {
58
57
  const getHeader = (name) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/auth.ts"],
4
- "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { hasAllFeatures } from '@open-mercato/shared/lib/auth/featureMatch'\n\nconst logger = createLogger('ai_assistant')\n\n/**\n * Successful authentication result.\n */\nexport type McpAuthSuccess = {\n success: true\n keyId: string\n keyName: string\n tenantId: string | null\n organizationId: string | null\n userId: string\n features: string[]\n isSuperAdmin: boolean\n}\n\n/**\n * Failed authentication result.\n */\nexport type McpAuthFailure = {\n success: false\n error: string\n}\n\n/**\n * Result from MCP authentication.\n */\nexport type McpAuthResult = McpAuthSuccess | McpAuthFailure\n\n/**\n * Authenticate an MCP request using an API key.\n *\n * This function validates the API key secret and loads the associated\n * ACL (features, organizations, super admin status) from the key's roles.\n *\n * @param apiKeySecret - The full API key secret (e.g., 'omk_xxxx.yyyy...')\n * @param container - Awilix DI container with 'em' and 'rbacService'\n * @returns Authentication result with user context or error\n */\nexport async function authenticateMcpRequest(\n apiKeySecret: string,\n container: AwilixContainer\n): Promise<McpAuthResult> {\n if (!apiKeySecret || typeof apiKeySecret !== 'string') {\n return { success: false, error: 'API key is required' }\n }\n\n const trimmedSecret = apiKeySecret.trim()\n if (!trimmedSecret) {\n return { success: false, error: 'API key is required' }\n }\n\n if (!trimmedSecret.startsWith('omk_')) {\n return { success: false, error: 'Invalid API key format' }\n }\n\n try {\n const em = container.resolve('em') as EntityManager\n\n const { findApiKeyBySecret } = await import(\n '@open-mercato/core/modules/api_keys/services/apiKeyService'\n )\n\n const apiKey = await findApiKeyBySecret(em, trimmedSecret)\n\n if (!apiKey) {\n return { success: false, error: 'Invalid or expired API key' }\n }\n\n const userId = `api_key:${apiKey.id}`\n\n const rbacService = container.resolve('rbacService') as {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null }\n ) => Promise<{\n isSuperAdmin: boolean\n features: string[]\n organizations: string[] | null\n }>\n }\n\n const acl = await rbacService.loadAcl(userId, {\n tenantId: apiKey.tenantId ?? null,\n organizationId: apiKey.organizationId ?? null,\n })\n\n try {\n apiKey.lastUsedAt = new Date()\n await em.persist(apiKey).flush()\n } catch {\n // Best-effort update; ignore write failures\n }\n\n return {\n success: true,\n keyId: apiKey.id,\n keyName: apiKey.name,\n tenantId: apiKey.tenantId ?? null,\n organizationId: apiKey.organizationId ?? null,\n userId,\n features: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n logger.error('MCP authentication failed', { err: error })\n return { success: false, error: 'Authentication failed' }\n }\n}\n\n/**\n * Check if user has the required features for a resource.\n *\n * Supports:\n * - Super admin bypass (always returns true)\n * - Direct feature match (e.g., 'customers.view')\n * - Global wildcard ('*' grants all features)\n * - Prefix wildcard (e.g., 'customers.*' grants 'customers.people.view' and the\n * bare 'customers' segment itself)\n *\n * @param requiredFeatures - List of features required for access\n * @param userFeatures - List of features the user has\n * @param isSuperAdmin - Whether the user is a super admin\n * @param rbacService - Optional RbacService to delegate feature matching\n * @returns True if user has access\n */\nexport function hasRequiredFeatures(\n requiredFeatures: string[] | undefined,\n userFeatures: string[],\n isSuperAdmin: boolean,\n rbacService?: RbacService\n): boolean {\n if (isSuperAdmin) return true\n if (!requiredFeatures?.length) return true\n\n // Delegate to RbacService if provided\n if (rbacService) {\n return rbacService.hasAllFeatures(requiredFeatures, userFeatures)\n }\n\n // Fallback for cases without rbacService: delegate to the canonical\n // wildcard-aware matcher so this path stays consistent with\n // RbacService.hasAllFeatures (which uses the same helper). The previous\n // bespoke loop rejected a bare-segment requirement (e.g. 'entities')\n // against an 'entities.*' grant, diverging from the canonical matcher.\n return hasAllFeatures(requiredFeatures, userFeatures)\n}\n\n/**\n * Extract API key from HTTP request headers.\n *\n * Supports two header formats:\n * - x-api-key: <secret>\n * - Authorization: ApiKey <secret>\n *\n * @param headers - Request headers (Map, Headers, or plain object)\n * @returns The API key secret or null if not found\n */\nexport function extractApiKeyFromHeaders(\n headers: Headers | Map<string, string> | Record<string, string | undefined>\n): string | null {\n const getHeader = (name: string): string | null => {\n if (headers instanceof Headers) {\n return headers.get(name)\n }\n if (headers instanceof Map) {\n return headers.get(name) ?? null\n }\n const value = headers[name] ?? headers[name.toLowerCase()]\n return typeof value === 'string' ? value : null\n }\n\n const xApiKey = getHeader('x-api-key')?.trim()\n if (xApiKey) {\n return xApiKey\n }\n\n const authHeader = getHeader('authorization')?.trim()\n if (authHeader && authHeader.toLowerCase().startsWith('apikey ')) {\n return authHeader.slice(7).trim()\n }\n\n return null\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAI7B,SAAS,sBAAsB;AAE/B,MAAM,SAAS,aAAa,cAAc;AAuC1C,eAAsB,uBACpB,cACA,WACwB;AACxB,MAAI,CAAC,gBAAgB,OAAO,iBAAiB,UAAU;AACrD,WAAO,EAAE,SAAS,OAAO,OAAO,sBAAsB;AAAA,EACxD;AAEA,QAAM,gBAAgB,aAAa,KAAK;AACxC,MAAI,CAAC,eAAe;AAClB,WAAO,EAAE,SAAS,OAAO,OAAO,sBAAsB;AAAA,EACxD;AAEA,MAAI,CAAC,cAAc,WAAW,MAAM,GAAG;AACrC,WAAO,EAAE,SAAS,OAAO,OAAO,yBAAyB;AAAA,EAC3D;AAEA,MAAI;AACF,UAAM,KAAK,UAAU,QAAQ,IAAI;AAEjC,UAAM,EAAE,mBAAmB,IAAI,MAAM,OACnC,4DACF;AAEA,UAAM,SAAS,MAAM,mBAAmB,IAAI,aAAa;AAEzD,QAAI,CAAC,QAAQ;AACX,aAAO,EAAE,SAAS,OAAO,OAAO,6BAA6B;AAAA,IAC/D;AAEA,UAAM,SAAS,WAAW,OAAO,EAAE;AAEnC,UAAM,cAAc,UAAU,QAAQ,aAAa;AAWnD,UAAM,MAAM,MAAM,YAAY,QAAQ,QAAQ;AAAA,MAC5C,UAAU,OAAO,YAAY;AAAA,MAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IAC3C,CAAC;AAED,QAAI;AACF,aAAO,aAAa,oBAAI,KAAK;AAC7B,YAAM,GAAG,QAAQ,MAAM,EAAE,MAAM;AAAA,IACjC,QAAQ;AAAA,IAER;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,OAAO;AAAA,MACd,SAAS,OAAO;AAAA,MAChB,UAAU,OAAO,YAAY;AAAA,MAC7B,gBAAgB,OAAO,kBAAkB;AAAA,MACzC;AAAA,MACA,UAAU,IAAI;AAAA,MACd,cAAc,IAAI;AAAA,IACpB;AAAA,EACF,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,WAAO,MAAM,6BAA6B,EAAE,KAAK,MAAM,CAAC;AACxD,WAAO,EAAE,SAAS,OAAO,OAAO,wBAAwB;AAAA,EAC1D;AACF;AAkBO,SAAS,oBACd,kBACA,cACA,cACA,aACS;AACT,MAAI,aAAc,QAAO;AACzB,MAAI,CAAC,kBAAkB,OAAQ,QAAO;AAGtC,MAAI,aAAa;AACf,WAAO,YAAY,eAAe,kBAAkB,YAAY;AAAA,EAClE;AAOA,SAAO,eAAe,kBAAkB,YAAY;AACtD;AAYO,SAAS,yBACd,SACe;AACf,QAAM,YAAY,CAAC,SAAgC;AACjD,QAAI,mBAAmB,SAAS;AAC9B,aAAO,QAAQ,IAAI,IAAI;AAAA,IACzB;AACA,QAAI,mBAAmB,KAAK;AAC1B,aAAO,QAAQ,IAAI,IAAI,KAAK;AAAA,IAC9B;AACA,UAAM,QAAQ,QAAQ,IAAI,KAAK,QAAQ,KAAK,YAAY,CAAC;AACzD,WAAO,OAAO,UAAU,WAAW,QAAQ;AAAA,EAC7C;AAEA,QAAM,UAAU,UAAU,WAAW,GAAG,KAAK;AAC7C,MAAI,SAAS;AACX,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,UAAU,eAAe,GAAG,KAAK;AACpD,MAAI,cAAc,WAAW,YAAY,EAAE,WAAW,SAAS,GAAG;AAChE,WAAO,WAAW,MAAM,CAAC,EAAE,KAAK;AAAA,EAClC;AAEA,SAAO;AACT;",
4
+ "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'\n\nconst logger = createLogger('ai_assistant')\n\n/**\n * Successful authentication result.\n */\nexport type McpAuthSuccess = {\n success: true\n keyId: string\n keyName: string\n tenantId: string | null\n organizationId: string | null\n userId: string\n features: string[]\n isSuperAdmin: boolean\n}\n\n/**\n * Failed authentication result.\n */\nexport type McpAuthFailure = {\n success: false\n error: string\n}\n\n/**\n * Result from MCP authentication.\n */\nexport type McpAuthResult = McpAuthSuccess | McpAuthFailure\n\n/**\n * Authenticate an MCP request using an API key.\n *\n * This function validates the API key secret and loads the associated\n * ACL (features, organizations, super admin status) from the key's roles.\n *\n * @param apiKeySecret - The full API key secret (e.g., 'omk_xxxx.yyyy...')\n * @param container - Awilix DI container with 'em' and 'rbacService'\n * @returns Authentication result with user context or error\n */\nexport async function authenticateMcpRequest(\n apiKeySecret: string,\n container: AwilixContainer\n): Promise<McpAuthResult> {\n if (!apiKeySecret || typeof apiKeySecret !== 'string') {\n return { success: false, error: 'API key is required' }\n }\n\n const trimmedSecret = apiKeySecret.trim()\n if (!trimmedSecret) {\n return { success: false, error: 'API key is required' }\n }\n\n if (!trimmedSecret.startsWith('omk_')) {\n return { success: false, error: 'Invalid API key format' }\n }\n\n try {\n const em = container.resolve('em') as EntityManager\n\n const { findApiKeyBySecret } = await import(\n '@open-mercato/core/modules/api_keys/services/apiKeyService'\n )\n\n const apiKey = await findApiKeyBySecret(em, trimmedSecret)\n\n if (!apiKey) {\n return { success: false, error: 'Invalid or expired API key' }\n }\n\n const userId = `api_key:${apiKey.id}`\n\n const rbacService = container.resolve('rbacService') as {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null }\n ) => Promise<{\n isSuperAdmin: boolean\n features: string[]\n organizations: string[] | null\n }>\n }\n\n const acl = await rbacService.loadAcl(userId, {\n tenantId: apiKey.tenantId ?? null,\n organizationId: apiKey.organizationId ?? null,\n })\n\n try {\n apiKey.lastUsedAt = new Date()\n await em.persist(apiKey).flush()\n } catch {\n // Best-effort update; ignore write failures\n }\n\n return {\n success: true,\n keyId: apiKey.id,\n keyName: apiKey.name,\n tenantId: apiKey.tenantId ?? null,\n organizationId: apiKey.organizationId ?? null,\n userId,\n features: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n logger.error('MCP authentication failed', { err: error })\n return { success: false, error: 'Authentication failed' }\n }\n}\n\n/**\n * Check if user has the required features for a resource.\n *\n * Supports:\n * - Super admin bypass (always returns true)\n * - Direct feature match (e.g., 'customers.view')\n * - Global wildcard ('*' grants all features)\n * - Prefix wildcard (e.g., 'customers.*' grants 'customers.people.view' and the\n * bare 'customers' segment itself)\n *\n * @param requiredFeatures - List of features required for access\n * @param userFeatures - List of features the user has\n * @param isSuperAdmin - Whether the user is a super admin\n * @param rbacService - Optional RbacService to delegate feature matching\n * @returns True if user has access\n */\nexport function hasRequiredFeatures(\n requiredFeatures: string[] | undefined,\n userFeatures: string[],\n isSuperAdmin: boolean,\n rbacService?: RbacService\n): boolean {\n void rbacService\n return authorizeFeatures(requiredFeatures ?? [], {\n grantedFeatures: userFeatures,\n unrestricted: isSuperAdmin,\n })\n}\n\n/**\n * Extract API key from HTTP request headers.\n *\n * Supports two header formats:\n * - x-api-key: <secret>\n * - Authorization: ApiKey <secret>\n *\n * @param headers - Request headers (Map, Headers, or plain object)\n * @returns The API key secret or null if not found\n */\nexport function extractApiKeyFromHeaders(\n headers: Headers | Map<string, string> | Record<string, string | undefined>\n): string | null {\n const getHeader = (name: string): string | null => {\n if (headers instanceof Headers) {\n return headers.get(name)\n }\n if (headers instanceof Map) {\n return headers.get(name) ?? null\n }\n const value = headers[name] ?? headers[name.toLowerCase()]\n return typeof value === 'string' ? value : null\n }\n\n const xApiKey = getHeader('x-api-key')?.trim()\n if (xApiKey) {\n return xApiKey\n }\n\n const authHeader = getHeader('authorization')?.trim()\n if (authHeader && authHeader.toLowerCase().startsWith('apikey ')) {\n return authHeader.slice(7).trim()\n }\n\n return null\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAI7B,SAAS,yBAAyB;AAElC,MAAM,SAAS,aAAa,cAAc;AAuC1C,eAAsB,uBACpB,cACA,WACwB;AACxB,MAAI,CAAC,gBAAgB,OAAO,iBAAiB,UAAU;AACrD,WAAO,EAAE,SAAS,OAAO,OAAO,sBAAsB;AAAA,EACxD;AAEA,QAAM,gBAAgB,aAAa,KAAK;AACxC,MAAI,CAAC,eAAe;AAClB,WAAO,EAAE,SAAS,OAAO,OAAO,sBAAsB;AAAA,EACxD;AAEA,MAAI,CAAC,cAAc,WAAW,MAAM,GAAG;AACrC,WAAO,EAAE,SAAS,OAAO,OAAO,yBAAyB;AAAA,EAC3D;AAEA,MAAI;AACF,UAAM,KAAK,UAAU,QAAQ,IAAI;AAEjC,UAAM,EAAE,mBAAmB,IAAI,MAAM,OACnC,4DACF;AAEA,UAAM,SAAS,MAAM,mBAAmB,IAAI,aAAa;AAEzD,QAAI,CAAC,QAAQ;AACX,aAAO,EAAE,SAAS,OAAO,OAAO,6BAA6B;AAAA,IAC/D;AAEA,UAAM,SAAS,WAAW,OAAO,EAAE;AAEnC,UAAM,cAAc,UAAU,QAAQ,aAAa;AAWnD,UAAM,MAAM,MAAM,YAAY,QAAQ,QAAQ;AAAA,MAC5C,UAAU,OAAO,YAAY;AAAA,MAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IAC3C,CAAC;AAED,QAAI;AACF,aAAO,aAAa,oBAAI,KAAK;AAC7B,YAAM,GAAG,QAAQ,MAAM,EAAE,MAAM;AAAA,IACjC,QAAQ;AAAA,IAER;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,OAAO;AAAA,MACd,SAAS,OAAO;AAAA,MAChB,UAAU,OAAO,YAAY;AAAA,MAC7B,gBAAgB,OAAO,kBAAkB;AAAA,MACzC;AAAA,MACA,UAAU,IAAI;AAAA,MACd,cAAc,IAAI;AAAA,IACpB;AAAA,EACF,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,WAAO,MAAM,6BAA6B,EAAE,KAAK,MAAM,CAAC;AACxD,WAAO,EAAE,SAAS,OAAO,OAAO,wBAAwB;AAAA,EAC1D;AACF;AAkBO,SAAS,oBACd,kBACA,cACA,cACA,aACS;AACT,OAAK;AACL,SAAO,kBAAkB,oBAAoB,CAAC,GAAG;AAAA,IAC/C,iBAAiB;AAAA,IACjB,cAAc;AAAA,EAChB,CAAC;AACH;AAYO,SAAS,yBACd,SACe;AACf,QAAM,YAAY,CAAC,SAAgC;AACjD,QAAI,mBAAmB,SAAS;AAC9B,aAAO,QAAQ,IAAI,IAAI;AAAA,IACzB;AACA,QAAI,mBAAmB,KAAK;AAC1B,aAAO,QAAQ,IAAI,IAAI,KAAK;AAAA,IAC9B;AACA,UAAM,QAAQ,QAAQ,IAAI,KAAK,QAAQ,KAAK,YAAY,CAAC;AACzD,WAAO,OAAO,UAAU,WAAW,QAAQ;AAAA,EAC7C;AAEA,QAAM,UAAU,UAAU,WAAW,GAAG,KAAK;AAC7C,MAAI,SAAS;AACX,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,UAAU,eAAe,GAAG,KAAK;AACpD,MAAI,cAAc,WAAW,YAAY,EAAE,WAAW,SAAS,GAAG;AAChE,WAAO,WAAW,MAAM,CAAC,EAAE,KAAK;AAAA,EAClC;AAEA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ import { authorizeFeatures } from "@open-mercato/shared/security/featurePolicy";
1
2
  import {
2
3
  TOOL_ENTITY_ID,
3
4
  GLOBAL_TENANT_ID,
@@ -177,18 +178,9 @@ class ToolSearchService {
177
178
  * Check if user has required features to access a tool.
178
179
  */
179
180
  hasFeatureAccess(requiredFeatures, userFeatures, isSuperAdmin) {
180
- if (isSuperAdmin) return true;
181
- if (!requiredFeatures?.length) return true;
182
- return requiredFeatures.every((required) => {
183
- if (userFeatures.includes(required)) return true;
184
- if (userFeatures.includes("*")) return true;
185
- return userFeatures.some((feature) => {
186
- if (feature.endsWith(".*")) {
187
- const prefix = feature.slice(0, -2);
188
- return required.startsWith(prefix + ".");
189
- }
190
- return false;
191
- });
181
+ return authorizeFeatures(requiredFeatures ?? [], {
182
+ grantedFeatures: userFeatures,
183
+ unrestricted: isSuperAdmin
192
184
  });
193
185
  }
194
186
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/tool-search.ts"],
4
- "sourcesContent": ["import type { SearchService } from '@open-mercato/search/service'\nimport type { SearchStrategyId, IndexableRecord } from '@open-mercato/search/types'\nimport type { McpToolRegistry, McpToolDefinition } from './types'\nimport {\n TOOL_ENTITY_ID,\n GLOBAL_TENANT_ID,\n TOOL_SEARCH_CONFIG,\n toolToIndexableRecord,\n computeToolsChecksum,\n} from './tool-index-config'\n\n/**\n * Result from tool search.\n */\nexport type ToolSearchResult = {\n /** Tool name (recordId) */\n toolName: string\n /** Relevance score */\n score: number\n /** Module that registered the tool */\n moduleId?: string\n /** Features required to use this tool */\n requiredFeatures?: string[]\n}\n\n/**\n * Status of available search strategies.\n */\nexport type StrategyStatus = {\n fulltext: boolean\n vector: boolean\n tokens: boolean\n /** Ordered list of available strategies */\n available: SearchStrategyId[]\n /** Strategies that will be used by default */\n default: SearchStrategyId[]\n}\n\n/**\n * Result from tool indexing operation.\n */\nexport type IndexingResult = {\n indexed: number\n skipped: number\n strategies: SearchStrategyId[]\n checksum: string\n}\n\n/**\n * Options for searching tools.\n */\nexport type ToolSearchOptions = {\n /** Maximum results to return (default: 12) */\n limit?: number\n /** Override strategies to use */\n strategies?: SearchStrategyId[]\n /** User's features for ACL filtering */\n userFeatures?: string[]\n /** Is user a super admin (bypasses ACL) */\n isSuperAdmin?: boolean\n /** Minimum score threshold (default: 0.2) */\n minScore?: number\n}\n\n/**\n * Service for searching and indexing MCP tools using hybrid search strategies.\n *\n * Uses the existing search module infrastructure to provide:\n * - Fulltext search (Meilisearch) when configured\n * - Vector/semantic search (PgVector) when configured\n * - Token-based search (PostgreSQL hash) as fallback\n *\n * Results are merged using Reciprocal Rank Fusion (RRF) with weights:\n * - fulltext: 1.2\n * - vector: 1.0\n * - tokens: 0.8\n */\nexport class ToolSearchService {\n private lastIndexChecksum: string | null = null\n\n constructor(\n private readonly searchService: SearchService,\n private readonly toolRegistry: McpToolRegistry\n ) {}\n\n /**\n * Get status of available search strategies.\n * This helps the AI understand which integrations are available.\n */\n async getStrategyStatus(): Promise<StrategyStatus> {\n const [fulltext, vector, tokens] = await Promise.all([\n this.searchService.isStrategyAvailable('fulltext'),\n this.searchService.isStrategyAvailable('vector'),\n this.searchService.isStrategyAvailable('tokens'),\n ])\n\n const available: SearchStrategyId[] = []\n if (fulltext) available.push('fulltext')\n if (vector) available.push('vector')\n if (tokens) available.push('tokens')\n\n const defaultStrategies = this.searchService.getDefaultStrategies()\n\n return {\n fulltext,\n vector,\n tokens,\n available,\n default: defaultStrategies.filter((s) => available.includes(s)),\n }\n }\n\n /**\n * Search for relevant tools using hybrid strategies.\n *\n * @param query - User's search query or message\n * @param options - Search options\n * @returns Array of matching tools sorted by relevance\n */\n async searchTools(\n query: string,\n options: ToolSearchOptions = {}\n ): Promise<ToolSearchResult[]> {\n const {\n limit = TOOL_SEARCH_CONFIG.defaultLimit,\n strategies,\n userFeatures = [],\n isSuperAdmin = false,\n minScore = TOOL_SEARCH_CONFIG.minScore,\n } = options\n\n // Use hybrid search with all available strategies\n const searchResults = await this.searchService.search(query, {\n tenantId: GLOBAL_TENANT_ID,\n organizationId: null,\n entityTypes: [TOOL_ENTITY_ID],\n strategies,\n limit: limit * 2, // Get extra results to account for ACL filtering\n })\n\n // Map to tool results and filter by ACL\n const toolResults: ToolSearchResult[] = []\n\n for (const result of searchResults) {\n // Skip results below minimum score\n if (result.score < minScore) continue\n\n const metadata = result.metadata as Record<string, unknown> | undefined\n const requiredFeatures = (metadata?.requiredFeatures as string[]) ?? []\n const moduleId = metadata?.moduleId as string | undefined\n\n // Filter by user's feature access\n if (!this.hasFeatureAccess(requiredFeatures, userFeatures, isSuperAdmin)) {\n continue\n }\n\n toolResults.push({\n toolName: result.recordId,\n score: result.score,\n moduleId,\n requiredFeatures,\n })\n\n // Stop when we have enough results\n if (toolResults.length >= limit) break\n }\n\n return toolResults\n }\n\n /**\n * Index all tools from the registry using available strategies.\n * Uses checksum-based change detection to avoid unnecessary re-indexing.\n *\n * @param force - Force re-indexing even if checksum hasn't changed\n * @returns Indexing result with statistics\n */\n async indexTools(force = false): Promise<IndexingResult> {\n const tools = Array.from(this.toolRegistry.getTools().values())\n const modules = this.getToolModules()\n\n // Compute checksum to detect changes\n const checksum = computeToolsChecksum(\n tools.map((t) => ({ name: t.name, description: t.description }))\n )\n\n // Skip if checksum matches and not forced\n if (!force && this.lastIndexChecksum === checksum) {\n const status = await this.getStrategyStatus()\n return {\n indexed: 0,\n skipped: tools.length,\n strategies: status.available,\n checksum,\n }\n }\n\n // Build indexable records for all tools\n const records: IndexableRecord[] = []\n for (const tool of tools) {\n const moduleId = modules.get(tool.name)\n records.push(toolToIndexableRecord(tool, moduleId))\n }\n\n // Bulk index using available strategies\n if (records.length > 0) {\n try {\n await this.searchService.bulkIndex(records)\n } catch (error) {\n throw new Error(\n `Failed to index ${records.length} tools: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\n }\n\n // Update checksum\n this.lastIndexChecksum = checksum\n\n const status = await this.getStrategyStatus()\n return {\n indexed: records.length,\n skipped: 0,\n strategies: status.available,\n checksum,\n }\n }\n\n /**\n * Purge all tool records from the search index.\n * Useful for cleanup or complete re-indexing.\n */\n async purgeIndex(): Promise<void> {\n await this.searchService.purge(TOOL_ENTITY_ID, GLOBAL_TENANT_ID)\n this.lastIndexChecksum = null\n }\n\n /**\n * Get tools by module ID from the registry.\n */\n getToolsByModule(moduleId: string): string[] {\n return this.toolRegistry.listToolsByModule(moduleId)\n }\n\n /**\n * Build a map of tool name to module ID.\n */\n private getToolModules(): Map<string, string> {\n const modules = new Map<string, string>()\n\n // Common module prefixes that indicate tool ownership\n const modulePatterns = [\n { pattern: /^customers_/, moduleId: 'customers' },\n { pattern: /^sales_/, moduleId: 'sales' },\n { pattern: /^catalog_/, moduleId: 'catalog' },\n { pattern: /^staff_/, moduleId: 'staff' },\n { pattern: /^resources_/, moduleId: 'resources' },\n { pattern: /^planner/, moduleId: 'planner' },\n { pattern: /^search_/, moduleId: 'search' },\n { pattern: /^context_/, moduleId: 'context' },\n { pattern: /^auth_/, moduleId: 'auth' },\n { pattern: /^directory_/, moduleId: 'directory' },\n { pattern: /^dashboards_/, moduleId: 'dashboards' },\n { pattern: /^entities_/, moduleId: 'entities' },\n { pattern: /^dictionaries_/, moduleId: 'dictionaries' },\n { pattern: /^workflows_/, moduleId: 'workflows' },\n { pattern: /^business_rules_/, moduleId: 'business_rules' },\n { pattern: /^audit_logs_/, moduleId: 'audit_logs' },\n { pattern: /^attachments_/, moduleId: 'attachments' },\n { pattern: /^feature_toggles_/, moduleId: 'feature_toggles' },\n { pattern: /^currencies_/, moduleId: 'currencies' },\n { pattern: /^example_/, moduleId: 'example' },\n { pattern: /^cli_/, moduleId: 'cli' },\n ]\n\n for (const toolName of this.toolRegistry.listToolNames()) {\n for (const { pattern, moduleId } of modulePatterns) {\n if (pattern.test(toolName)) {\n modules.set(toolName, moduleId)\n break\n }\n }\n }\n\n return modules\n }\n\n /**\n * Check if user has required features to access a tool.\n */\n private hasFeatureAccess(\n requiredFeatures: string[] | undefined,\n userFeatures: string[],\n isSuperAdmin: boolean\n ): boolean {\n if (isSuperAdmin) return true\n if (!requiredFeatures?.length) return true\n\n return requiredFeatures.every((required) => {\n // Direct match\n if (userFeatures.includes(required)) return true\n // Wildcard match\n if (userFeatures.includes('*')) return true\n\n // Prefix wildcard match (e.g., 'customers.*' matches 'customers.people.view')\n return userFeatures.some((feature) => {\n if (feature.endsWith('.*')) {\n const prefix = feature.slice(0, -2)\n return required.startsWith(prefix + '.')\n }\n return false\n })\n })\n }\n}\n"],
5
- "mappings": "AAGA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAoEA,MAAM,kBAAkB;AAAA,EAG7B,YACmB,eACA,cACjB;AAFiB;AACA;AAJnB,SAAQ,oBAAmC;AAAA,EAKxC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMH,MAAM,oBAA6C;AACjD,UAAM,CAAC,UAAU,QAAQ,MAAM,IAAI,MAAM,QAAQ,IAAI;AAAA,MACnD,KAAK,cAAc,oBAAoB,UAAU;AAAA,MACjD,KAAK,cAAc,oBAAoB,QAAQ;AAAA,MAC/C,KAAK,cAAc,oBAAoB,QAAQ;AAAA,IACjD,CAAC;AAED,UAAM,YAAgC,CAAC;AACvC,QAAI,SAAU,WAAU,KAAK,UAAU;AACvC,QAAI,OAAQ,WAAU,KAAK,QAAQ;AACnC,QAAI,OAAQ,WAAU,KAAK,QAAQ;AAEnC,UAAM,oBAAoB,KAAK,cAAc,qBAAqB;AAElE,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,kBAAkB,OAAO,CAAC,MAAM,UAAU,SAAS,CAAC,CAAC;AAAA,IAChE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,YACJ,OACA,UAA6B,CAAC,GACD;AAC7B,UAAM;AAAA,MACJ,QAAQ,mBAAmB;AAAA,MAC3B;AAAA,MACA,eAAe,CAAC;AAAA,MAChB,eAAe;AAAA,MACf,WAAW,mBAAmB;AAAA,IAChC,IAAI;AAGJ,UAAM,gBAAgB,MAAM,KAAK,cAAc,OAAO,OAAO;AAAA,MAC3D,UAAU;AAAA,MACV,gBAAgB;AAAA,MAChB,aAAa,CAAC,cAAc;AAAA,MAC5B;AAAA,MACA,OAAO,QAAQ;AAAA;AAAA,IACjB,CAAC;AAGD,UAAM,cAAkC,CAAC;AAEzC,eAAW,UAAU,eAAe;AAElC,UAAI,OAAO,QAAQ,SAAU;AAE7B,YAAM,WAAW,OAAO;AACxB,YAAM,mBAAoB,UAAU,oBAAiC,CAAC;AACtE,YAAM,WAAW,UAAU;AAG3B,UAAI,CAAC,KAAK,iBAAiB,kBAAkB,cAAc,YAAY,GAAG;AACxE;AAAA,MACF;AAEA,kBAAY,KAAK;AAAA,QACf,UAAU,OAAO;AAAA,QACjB,OAAO,OAAO;AAAA,QACd;AAAA,QACA;AAAA,MACF,CAAC;AAGD,UAAI,YAAY,UAAU,MAAO;AAAA,IACnC;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,WAAW,QAAQ,OAAgC;AACvD,UAAM,QAAQ,MAAM,KAAK,KAAK,aAAa,SAAS,EAAE,OAAO,CAAC;AAC9D,UAAM,UAAU,KAAK,eAAe;AAGpC,UAAM,WAAW;AAAA,MACf,MAAM,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,EAAE,YAAY,EAAE;AAAA,IACjE;AAGA,QAAI,CAAC,SAAS,KAAK,sBAAsB,UAAU;AACjD,YAAMA,UAAS,MAAM,KAAK,kBAAkB;AAC5C,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,MAAM;AAAA,QACf,YAAYA,QAAO;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAGA,UAAM,UAA6B,CAAC;AACpC,eAAW,QAAQ,OAAO;AACxB,YAAM,WAAW,QAAQ,IAAI,KAAK,IAAI;AACtC,cAAQ,KAAK,sBAAsB,MAAM,QAAQ,CAAC;AAAA,IACpD;AAGA,QAAI,QAAQ,SAAS,GAAG;AACtB,UAAI;AACF,cAAM,KAAK,cAAc,UAAU,OAAO;AAAA,MAC5C,SAAS,OAAO;AACd,cAAM,IAAI;AAAA,UACR,mBAAmB,QAAQ,MAAM,WAC/B,iBAAiB,QAAQ,MAAM,UAAU,KAC3C;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,SAAK,oBAAoB;AAEzB,UAAM,SAAS,MAAM,KAAK,kBAAkB;AAC5C,WAAO;AAAA,MACL,SAAS,QAAQ;AAAA,MACjB,SAAS;AAAA,MACT,YAAY,OAAO;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,aAA4B;AAChC,UAAM,KAAK,cAAc,MAAM,gBAAgB,gBAAgB;AAC/D,SAAK,oBAAoB;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAiB,UAA4B;AAC3C,WAAO,KAAK,aAAa,kBAAkB,QAAQ;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA,EAKQ,iBAAsC;AAC5C,UAAM,UAAU,oBAAI,IAAoB;AAGxC,UAAM,iBAAiB;AAAA,MACrB,EAAE,SAAS,eAAe,UAAU,YAAY;AAAA,MAChD,EAAE,SAAS,WAAW,UAAU,QAAQ;AAAA,MACxC,EAAE,SAAS,aAAa,UAAU,UAAU;AAAA,MAC5C,EAAE,SAAS,WAAW,UAAU,QAAQ;AAAA,MACxC,EAAE,SAAS,eAAe,UAAU,YAAY;AAAA,MAChD,EAAE,SAAS,YAAY,UAAU,UAAU;AAAA,MAC3C,EAAE,SAAS,YAAY,UAAU,SAAS;AAAA,MAC1C,EAAE,SAAS,aAAa,UAAU,UAAU;AAAA,MAC5C,EAAE,SAAS,UAAU,UAAU,OAAO;AAAA,MACtC,EAAE,SAAS,eAAe,UAAU,YAAY;AAAA,MAChD,EAAE,SAAS,gBAAgB,UAAU,aAAa;AAAA,MAClD,EAAE,SAAS,cAAc,UAAU,WAAW;AAAA,MAC9C,EAAE,SAAS,kBAAkB,UAAU,eAAe;AAAA,MACtD,EAAE,SAAS,eAAe,UAAU,YAAY;AAAA,MAChD,EAAE,SAAS,oBAAoB,UAAU,iBAAiB;AAAA,MAC1D,EAAE,SAAS,gBAAgB,UAAU,aAAa;AAAA,MAClD,EAAE,SAAS,iBAAiB,UAAU,cAAc;AAAA,MACpD,EAAE,SAAS,qBAAqB,UAAU,kBAAkB;AAAA,MAC5D,EAAE,SAAS,gBAAgB,UAAU,aAAa;AAAA,MAClD,EAAE,SAAS,aAAa,UAAU,UAAU;AAAA,MAC5C,EAAE,SAAS,SAAS,UAAU,MAAM;AAAA,IACtC;AAEA,eAAW,YAAY,KAAK,aAAa,cAAc,GAAG;AACxD,iBAAW,EAAE,SAAS,SAAS,KAAK,gBAAgB;AAClD,YAAI,QAAQ,KAAK,QAAQ,GAAG;AAC1B,kBAAQ,IAAI,UAAU,QAAQ;AAC9B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,iBACN,kBACA,cACA,cACS;AACT,QAAI,aAAc,QAAO;AACzB,QAAI,CAAC,kBAAkB,OAAQ,QAAO;AAEtC,WAAO,iBAAiB,MAAM,CAAC,aAAa;AAE1C,UAAI,aAAa,SAAS,QAAQ,EAAG,QAAO;AAE5C,UAAI,aAAa,SAAS,GAAG,EAAG,QAAO;AAGvC,aAAO,aAAa,KAAK,CAAC,YAAY;AACpC,YAAI,QAAQ,SAAS,IAAI,GAAG;AAC1B,gBAAM,SAAS,QAAQ,MAAM,GAAG,EAAE;AAClC,iBAAO,SAAS,WAAW,SAAS,GAAG;AAAA,QACzC;AACA,eAAO;AAAA,MACT,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACF;",
4
+ "sourcesContent": ["import type { SearchService } from '@open-mercato/search/service'\nimport type { SearchStrategyId, IndexableRecord } from '@open-mercato/search/types'\nimport { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'\nimport type { McpToolRegistry, McpToolDefinition } from './types'\nimport {\n TOOL_ENTITY_ID,\n GLOBAL_TENANT_ID,\n TOOL_SEARCH_CONFIG,\n toolToIndexableRecord,\n computeToolsChecksum,\n} from './tool-index-config'\n\n/**\n * Result from tool search.\n */\nexport type ToolSearchResult = {\n /** Tool name (recordId) */\n toolName: string\n /** Relevance score */\n score: number\n /** Module that registered the tool */\n moduleId?: string\n /** Features required to use this tool */\n requiredFeatures?: string[]\n}\n\n/**\n * Status of available search strategies.\n */\nexport type StrategyStatus = {\n fulltext: boolean\n vector: boolean\n tokens: boolean\n /** Ordered list of available strategies */\n available: SearchStrategyId[]\n /** Strategies that will be used by default */\n default: SearchStrategyId[]\n}\n\n/**\n * Result from tool indexing operation.\n */\nexport type IndexingResult = {\n indexed: number\n skipped: number\n strategies: SearchStrategyId[]\n checksum: string\n}\n\n/**\n * Options for searching tools.\n */\nexport type ToolSearchOptions = {\n /** Maximum results to return (default: 12) */\n limit?: number\n /** Override strategies to use */\n strategies?: SearchStrategyId[]\n /** User's features for ACL filtering */\n userFeatures?: string[]\n /** Is user a super admin (bypasses ACL) */\n isSuperAdmin?: boolean\n /** Minimum score threshold (default: 0.2) */\n minScore?: number\n}\n\n/**\n * Service for searching and indexing MCP tools using hybrid search strategies.\n *\n * Uses the existing search module infrastructure to provide:\n * - Fulltext search (Meilisearch) when configured\n * - Vector/semantic search (PgVector) when configured\n * - Token-based search (PostgreSQL hash) as fallback\n *\n * Results are merged using Reciprocal Rank Fusion (RRF) with weights:\n * - fulltext: 1.2\n * - vector: 1.0\n * - tokens: 0.8\n */\nexport class ToolSearchService {\n private lastIndexChecksum: string | null = null\n\n constructor(\n private readonly searchService: SearchService,\n private readonly toolRegistry: McpToolRegistry\n ) {}\n\n /**\n * Get status of available search strategies.\n * This helps the AI understand which integrations are available.\n */\n async getStrategyStatus(): Promise<StrategyStatus> {\n const [fulltext, vector, tokens] = await Promise.all([\n this.searchService.isStrategyAvailable('fulltext'),\n this.searchService.isStrategyAvailable('vector'),\n this.searchService.isStrategyAvailable('tokens'),\n ])\n\n const available: SearchStrategyId[] = []\n if (fulltext) available.push('fulltext')\n if (vector) available.push('vector')\n if (tokens) available.push('tokens')\n\n const defaultStrategies = this.searchService.getDefaultStrategies()\n\n return {\n fulltext,\n vector,\n tokens,\n available,\n default: defaultStrategies.filter((s) => available.includes(s)),\n }\n }\n\n /**\n * Search for relevant tools using hybrid strategies.\n *\n * @param query - User's search query or message\n * @param options - Search options\n * @returns Array of matching tools sorted by relevance\n */\n async searchTools(\n query: string,\n options: ToolSearchOptions = {}\n ): Promise<ToolSearchResult[]> {\n const {\n limit = TOOL_SEARCH_CONFIG.defaultLimit,\n strategies,\n userFeatures = [],\n isSuperAdmin = false,\n minScore = TOOL_SEARCH_CONFIG.minScore,\n } = options\n\n // Use hybrid search with all available strategies\n const searchResults = await this.searchService.search(query, {\n tenantId: GLOBAL_TENANT_ID,\n organizationId: null,\n entityTypes: [TOOL_ENTITY_ID],\n strategies,\n limit: limit * 2, // Get extra results to account for ACL filtering\n })\n\n // Map to tool results and filter by ACL\n const toolResults: ToolSearchResult[] = []\n\n for (const result of searchResults) {\n // Skip results below minimum score\n if (result.score < minScore) continue\n\n const metadata = result.metadata as Record<string, unknown> | undefined\n const requiredFeatures = (metadata?.requiredFeatures as string[]) ?? []\n const moduleId = metadata?.moduleId as string | undefined\n\n // Filter by user's feature access\n if (!this.hasFeatureAccess(requiredFeatures, userFeatures, isSuperAdmin)) {\n continue\n }\n\n toolResults.push({\n toolName: result.recordId,\n score: result.score,\n moduleId,\n requiredFeatures,\n })\n\n // Stop when we have enough results\n if (toolResults.length >= limit) break\n }\n\n return toolResults\n }\n\n /**\n * Index all tools from the registry using available strategies.\n * Uses checksum-based change detection to avoid unnecessary re-indexing.\n *\n * @param force - Force re-indexing even if checksum hasn't changed\n * @returns Indexing result with statistics\n */\n async indexTools(force = false): Promise<IndexingResult> {\n const tools = Array.from(this.toolRegistry.getTools().values())\n const modules = this.getToolModules()\n\n // Compute checksum to detect changes\n const checksum = computeToolsChecksum(\n tools.map((t) => ({ name: t.name, description: t.description }))\n )\n\n // Skip if checksum matches and not forced\n if (!force && this.lastIndexChecksum === checksum) {\n const status = await this.getStrategyStatus()\n return {\n indexed: 0,\n skipped: tools.length,\n strategies: status.available,\n checksum,\n }\n }\n\n // Build indexable records for all tools\n const records: IndexableRecord[] = []\n for (const tool of tools) {\n const moduleId = modules.get(tool.name)\n records.push(toolToIndexableRecord(tool, moduleId))\n }\n\n // Bulk index using available strategies\n if (records.length > 0) {\n try {\n await this.searchService.bulkIndex(records)\n } catch (error) {\n throw new Error(\n `Failed to index ${records.length} tools: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\n }\n\n // Update checksum\n this.lastIndexChecksum = checksum\n\n const status = await this.getStrategyStatus()\n return {\n indexed: records.length,\n skipped: 0,\n strategies: status.available,\n checksum,\n }\n }\n\n /**\n * Purge all tool records from the search index.\n * Useful for cleanup or complete re-indexing.\n */\n async purgeIndex(): Promise<void> {\n await this.searchService.purge(TOOL_ENTITY_ID, GLOBAL_TENANT_ID)\n this.lastIndexChecksum = null\n }\n\n /**\n * Get tools by module ID from the registry.\n */\n getToolsByModule(moduleId: string): string[] {\n return this.toolRegistry.listToolsByModule(moduleId)\n }\n\n /**\n * Build a map of tool name to module ID.\n */\n private getToolModules(): Map<string, string> {\n const modules = new Map<string, string>()\n\n // Common module prefixes that indicate tool ownership\n const modulePatterns = [\n { pattern: /^customers_/, moduleId: 'customers' },\n { pattern: /^sales_/, moduleId: 'sales' },\n { pattern: /^catalog_/, moduleId: 'catalog' },\n { pattern: /^staff_/, moduleId: 'staff' },\n { pattern: /^resources_/, moduleId: 'resources' },\n { pattern: /^planner/, moduleId: 'planner' },\n { pattern: /^search_/, moduleId: 'search' },\n { pattern: /^context_/, moduleId: 'context' },\n { pattern: /^auth_/, moduleId: 'auth' },\n { pattern: /^directory_/, moduleId: 'directory' },\n { pattern: /^dashboards_/, moduleId: 'dashboards' },\n { pattern: /^entities_/, moduleId: 'entities' },\n { pattern: /^dictionaries_/, moduleId: 'dictionaries' },\n { pattern: /^workflows_/, moduleId: 'workflows' },\n { pattern: /^business_rules_/, moduleId: 'business_rules' },\n { pattern: /^audit_logs_/, moduleId: 'audit_logs' },\n { pattern: /^attachments_/, moduleId: 'attachments' },\n { pattern: /^feature_toggles_/, moduleId: 'feature_toggles' },\n { pattern: /^currencies_/, moduleId: 'currencies' },\n { pattern: /^example_/, moduleId: 'example' },\n { pattern: /^cli_/, moduleId: 'cli' },\n ]\n\n for (const toolName of this.toolRegistry.listToolNames()) {\n for (const { pattern, moduleId } of modulePatterns) {\n if (pattern.test(toolName)) {\n modules.set(toolName, moduleId)\n break\n }\n }\n }\n\n return modules\n }\n\n /**\n * Check if user has required features to access a tool.\n */\n private hasFeatureAccess(\n requiredFeatures: string[] | undefined,\n userFeatures: string[],\n isSuperAdmin: boolean\n ): boolean {\n return authorizeFeatures(requiredFeatures ?? [], {\n grantedFeatures: userFeatures,\n unrestricted: isSuperAdmin,\n })\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["status"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ai-assistant",
3
- "version": "0.6.7-develop.6775.1.c2313bb8a3",
3
+ "version": "0.6.7-develop.6784.1.f80b9afce5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -99,16 +99,16 @@
99
99
  "zod-to-json-schema": "^3.25.2"
100
100
  },
101
101
  "peerDependencies": {
102
- "@open-mercato/shared": "0.6.7-develop.6775.1.c2313bb8a3",
103
- "@open-mercato/ui": "0.6.7-develop.6775.1.c2313bb8a3",
102
+ "@open-mercato/shared": "0.6.7-develop.6784.1.f80b9afce5",
103
+ "@open-mercato/ui": "0.6.7-develop.6784.1.f80b9afce5",
104
104
  "react": "^19.0.0",
105
105
  "react-dom": "^19.0.0",
106
106
  "zod": ">=3.23.0"
107
107
  },
108
108
  "devDependencies": {
109
- "@open-mercato/cli": "0.6.7-develop.6775.1.c2313bb8a3",
110
- "@open-mercato/shared": "0.6.7-develop.6775.1.c2313bb8a3",
111
- "@open-mercato/ui": "0.6.7-develop.6775.1.c2313bb8a3",
109
+ "@open-mercato/cli": "0.6.7-develop.6784.1.f80b9afce5",
110
+ "@open-mercato/shared": "0.6.7-develop.6784.1.f80b9afce5",
111
+ "@open-mercato/ui": "0.6.7-develop.6784.1.f80b9afce5",
112
112
  "@types/react": "^19.2.17",
113
113
  "@types/react-dom": "^19.2.3",
114
114
  "react": "19.2.7",
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  const authMock = jest.fn()
11
- const loadAclMock = jest.fn()
11
+ const userHasAllFeaturesMock = jest.fn()
12
12
  const createRequestContainerMock = jest.fn()
13
13
  const upsertDefaultMock = jest.fn()
14
14
  const clearDefaultMock = jest.fn()
@@ -96,10 +96,10 @@ describe('PUT /api/ai_assistant/settings', () => {
96
96
  jest.clearAllMocks()
97
97
  consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
98
98
  authMock.mockResolvedValue({ sub: 'user-1', tenantId: 'tenant-1', orgId: 'org-1' })
99
- loadAclMock.mockResolvedValue({ features: ['ai_assistant.settings.manage'], isSuperAdmin: false })
99
+ userHasAllFeaturesMock.mockResolvedValue(true)
100
100
  createRequestContainerMock.mockResolvedValue({
101
101
  resolve: (name: string) => {
102
- if (name === 'rbacService') return { loadAcl: loadAclMock }
102
+ if (name === 'rbacService') return { userHasAllFeatures: userHasAllFeaturesMock }
103
103
  if (name === 'em') return {}
104
104
  return null
105
105
  },
@@ -131,7 +131,7 @@ describe('PUT /api/ai_assistant/settings', () => {
131
131
  })
132
132
 
133
133
  it('returns 403 when caller lacks ai_assistant.settings.manage', async () => {
134
- loadAclMock.mockResolvedValueOnce({ features: ['ai_assistant.view'], isSuperAdmin: false })
134
+ userHasAllFeaturesMock.mockResolvedValueOnce(false)
135
135
 
136
136
  const response = await PUT(buildRequest('PUT', { providerId: 'openai' }) as any)
137
137
 
@@ -268,7 +268,7 @@ describe('PUT /api/ai_assistant/settings', () => {
268
268
  })
269
269
 
270
270
  it('allows superAdmin even without the manage feature', async () => {
271
- loadAclMock.mockResolvedValueOnce({ features: [], isSuperAdmin: true })
271
+ userHasAllFeaturesMock.mockResolvedValueOnce(true)
272
272
 
273
273
  const response = await PUT(buildRequest('PUT', { providerId: 'openai' }) as any)
274
274
 
@@ -337,10 +337,10 @@ describe('DELETE /api/ai_assistant/settings', () => {
337
337
  jest.clearAllMocks()
338
338
  consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
339
339
  authMock.mockResolvedValue({ sub: 'user-1', tenantId: 'tenant-1', orgId: 'org-1' })
340
- loadAclMock.mockResolvedValue({ features: ['ai_assistant.settings.manage'], isSuperAdmin: false })
340
+ userHasAllFeaturesMock.mockResolvedValue(true)
341
341
  createRequestContainerMock.mockResolvedValue({
342
342
  resolve: (name: string) => {
343
- if (name === 'rbacService') return { loadAcl: loadAclMock }
343
+ if (name === 'rbacService') return { userHasAllFeatures: userHasAllFeaturesMock }
344
344
  if (name === 'em') return {}
345
345
  return null
346
346
  },
@@ -361,7 +361,7 @@ describe('DELETE /api/ai_assistant/settings', () => {
361
361
  })
362
362
 
363
363
  it('returns 403 when caller lacks ai_assistant.settings.manage', async () => {
364
- loadAclMock.mockResolvedValueOnce({ features: ['ai_assistant.view'], isSuperAdmin: false })
364
+ userHasAllFeaturesMock.mockResolvedValueOnce(false)
365
365
 
366
366
  const response = await DELETE(buildRequest('DELETE', {}) as any)
367
367
 
@@ -151,12 +151,11 @@ export async function PUT(req: NextRequest) {
151
151
  try {
152
152
  const container = await createRequestContainer()
153
153
  const rbacService = container.resolve<RbacService>('rbacService')
154
- const acl = await rbacService.loadAcl(auth.sub, {
155
- tenantId: auth.tenantId,
156
- organizationId: auth.orgId,
157
- })
158
- const canManage =
159
- acl.isSuperAdmin || acl.features.includes('ai_assistant.settings.manage')
154
+ const canManage = await rbacService.userHasAllFeatures(
155
+ auth.sub,
156
+ ['ai_assistant.settings.manage'],
157
+ { tenantId: auth.tenantId, organizationId: auth.orgId },
158
+ )
160
159
  if (!canManage) {
161
160
  return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })
162
161
  }
@@ -197,12 +196,11 @@ export async function DELETE(req: NextRequest) {
197
196
  try {
198
197
  const container = await createRequestContainer()
199
198
  const rbacService = container.resolve<RbacService>('rbacService')
200
- const acl = await rbacService.loadAcl(auth.sub, {
201
- tenantId: auth.tenantId,
202
- organizationId: auth.orgId,
203
- })
204
- const canManage =
205
- acl.isSuperAdmin || acl.features.includes('ai_assistant.settings.manage')
199
+ const canManage = await rbacService.userHasAllFeatures(
200
+ auth.sub,
201
+ ['ai_assistant.settings.manage'],
202
+ { tenantId: auth.tenantId, organizationId: auth.orgId },
203
+ )
206
204
  if (!canManage) {
207
205
  return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })
208
206
  }
@@ -691,11 +691,11 @@ export async function PUT(req: NextRequest) {
691
691
  try {
692
692
  const container = await createRequestContainer()
693
693
  const rbacService = container.resolve<RbacService>('rbacService')
694
- const acl = await rbacService.loadAcl(auth.sub, {
695
- tenantId: auth.tenantId,
696
- organizationId: auth.orgId,
697
- })
698
- const canManage = acl.isSuperAdmin || acl.features.includes('ai_assistant.settings.manage')
694
+ const canManage = await rbacService.userHasAllFeatures(
695
+ auth.sub,
696
+ ['ai_assistant.settings.manage'],
697
+ { tenantId: auth.tenantId, organizationId: auth.orgId },
698
+ )
699
699
  if (!canManage) {
700
700
  return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })
701
701
  }
@@ -777,11 +777,11 @@ export async function DELETE(req: NextRequest) {
777
777
  try {
778
778
  const container = await createRequestContainer()
779
779
  const rbacService = container.resolve<RbacService>('rbacService')
780
- const acl = await rbacService.loadAcl(auth.sub, {
781
- tenantId: auth.tenantId,
782
- organizationId: auth.orgId,
783
- })
784
- const canManage = acl.isSuperAdmin || acl.features.includes('ai_assistant.settings.manage')
780
+ const canManage = await rbacService.userHasAllFeatures(
781
+ auth.sub,
782
+ ['ai_assistant.settings.manage'],
783
+ { tenantId: auth.tenantId, organizationId: auth.orgId },
784
+ )
785
785
  if (!canManage) {
786
786
  return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })
787
787
  }
@@ -137,7 +137,7 @@ describe('hasRequiredFeatures', () => {
137
137
  ).toBe(true)
138
138
  })
139
139
 
140
- it('delegates to rbacService when provided', () => {
140
+ it('uses the shared policy even when a legacy rbacService argument is provided', () => {
141
141
  const rbacService = {
142
142
  hasAllFeatures: jest.fn().mockReturnValue(true),
143
143
  }
@@ -148,9 +148,6 @@ describe('hasRequiredFeatures', () => {
148
148
  rbacService as any
149
149
  )
150
150
  expect(result).toBe(true)
151
- expect(rbacService.hasAllFeatures).toHaveBeenCalledWith(
152
- ['customers.view'],
153
- ['customers.view']
154
- )
151
+ expect(rbacService.hasAllFeatures).not.toHaveBeenCalled()
155
152
  })
156
153
  })
@@ -2,7 +2,7 @@ import { createLogger } from '@open-mercato/shared/lib/logger'
2
2
  import type { AwilixContainer } from 'awilix'
3
3
  import type { EntityManager } from '@mikro-orm/postgresql'
4
4
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
5
- import { hasAllFeatures } from '@open-mercato/shared/lib/auth/featureMatch'
5
+ import { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'
6
6
 
7
7
  const logger = createLogger('ai_assistant')
8
8
 
@@ -137,20 +137,11 @@ export function hasRequiredFeatures(
137
137
  isSuperAdmin: boolean,
138
138
  rbacService?: RbacService
139
139
  ): boolean {
140
- if (isSuperAdmin) return true
141
- if (!requiredFeatures?.length) return true
142
-
143
- // Delegate to RbacService if provided
144
- if (rbacService) {
145
- return rbacService.hasAllFeatures(requiredFeatures, userFeatures)
146
- }
147
-
148
- // Fallback for cases without rbacService: delegate to the canonical
149
- // wildcard-aware matcher so this path stays consistent with
150
- // RbacService.hasAllFeatures (which uses the same helper). The previous
151
- // bespoke loop rejected a bare-segment requirement (e.g. 'entities')
152
- // against an 'entities.*' grant, diverging from the canonical matcher.
153
- return hasAllFeatures(requiredFeatures, userFeatures)
140
+ void rbacService
141
+ return authorizeFeatures(requiredFeatures ?? [], {
142
+ grantedFeatures: userFeatures,
143
+ unrestricted: isSuperAdmin,
144
+ })
154
145
  }
155
146
 
156
147
  /**
@@ -1,5 +1,6 @@
1
1
  import type { SearchService } from '@open-mercato/search/service'
2
2
  import type { SearchStrategyId, IndexableRecord } from '@open-mercato/search/types'
3
+ import { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'
3
4
  import type { McpToolRegistry, McpToolDefinition } from './types'
4
5
  import {
5
6
  TOOL_ENTITY_ID,
@@ -294,23 +295,9 @@ export class ToolSearchService {
294
295
  userFeatures: string[],
295
296
  isSuperAdmin: boolean
296
297
  ): boolean {
297
- if (isSuperAdmin) return true
298
- if (!requiredFeatures?.length) return true
299
-
300
- return requiredFeatures.every((required) => {
301
- // Direct match
302
- if (userFeatures.includes(required)) return true
303
- // Wildcard match
304
- if (userFeatures.includes('*')) return true
305
-
306
- // Prefix wildcard match (e.g., 'customers.*' matches 'customers.people.view')
307
- return userFeatures.some((feature) => {
308
- if (feature.endsWith('.*')) {
309
- const prefix = feature.slice(0, -2)
310
- return required.startsWith(prefix + '.')
311
- }
312
- return false
313
- })
298
+ return authorizeFeatures(requiredFeatures ?? [], {
299
+ grantedFeatures: userFeatures,
300
+ unrestricted: isSuperAdmin,
314
301
  })
315
302
  }
316
303
  }