@objectstack/core 10.3.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Logger, IServiceRegistry } from '@objectstack/spec/contracts';
2
- export { DriverInterface, IDataDriver, IDataEngine, IHttpRequest, IHttpResponse, IHttpServer, Logger, Middleware, RouteHandler } from '@objectstack/spec/contracts';
2
+ export { IDataDriver, IDataEngine, IHttpRequest, IHttpResponse, IHttpServer, Logger, Middleware, RouteHandler } from '@objectstack/spec/contracts';
3
3
  import { z } from 'zod';
4
4
  import { LoggerConfig } from '@objectstack/spec/system';
5
5
  import { ObjectLogger } from './logger.cjs';
@@ -1693,9 +1693,15 @@ interface GeneratedApiKey {
1693
1693
  */
1694
1694
  declare function generateApiKey(prefix?: string): GeneratedApiKey;
1695
1695
  /**
1696
- * Extract an API key from request headers. Accepts `X-API-Key: <token>` or
1697
- * `Authorization: ApiKey <token>` (case-insensitive scheme). Bearer tokens are
1698
- * deliberately NOT treated as API keys — those flow through the session path.
1696
+ * Extract an API key from request headers. Accepts, in order:
1697
+ * - `X-API-Key: <token>`
1698
+ * - `Authorization: ApiKey <token>` (case-insensitive scheme)
1699
+ * - `Authorization: Bearer <token>` ONLY when `<token>` carries the ObjectStack
1700
+ * api-key prefix (`osk_`). Remote MCP clients (Claude Desktop / Cursor /
1701
+ * Claude Code) authenticate to `/api/v1/mcp` with the key as a Bearer per the
1702
+ * MCP spec, so rejecting Bearer outright made every standard MCP client fail.
1703
+ * A better-auth *session* token never starts with `osk_`, so a session Bearer
1704
+ * still falls through to the session path — this can't shadow it.
1699
1705
  */
1700
1706
  declare function extractApiKey(headers: any): string | undefined;
1701
1707
  /** Parse a `scopes` value that may be a JSON-string textarea or a real array. */
@@ -1721,6 +1727,87 @@ interface ApiKeyPrincipal {
1721
1727
  */
1722
1728
  declare function resolveApiKeyPrincipal(ql: any, headers: any, nowMs?: number): Promise<ApiKeyPrincipal | undefined>;
1723
1729
 
1730
+ /** The transport-agnostic authorization envelope produced from a request. */
1731
+ interface ResolvedAuthzContext {
1732
+ userId?: string;
1733
+ tenantId?: string;
1734
+ email?: string;
1735
+ accessToken?: string;
1736
+ roles: string[];
1737
+ permissions: string[];
1738
+ systemPermissions: string[];
1739
+ tabPermissions?: Record<string, 'visible' | 'hidden' | 'default_on' | 'default_off'>;
1740
+ /** Fellow-org user IDs for RLS scoping of identity tables (`id IN (...)`). */
1741
+ org_user_ids: string[];
1742
+ }
1743
+ interface ResolveAuthzInput {
1744
+ /** Data engine (ObjectQL) exposing `find(object, { where, limit, context })`. */
1745
+ ql: any;
1746
+ /** Inbound request headers (Web `Headers` or a plain record). */
1747
+ headers: any;
1748
+ /**
1749
+ * Resolve a better-auth session from `headers`, returning `{ user?, session? }`
1750
+ * (or undefined). Optional — when omitted or throwing, only the API-key path
1751
+ * runs and anonymous requests resolve to an empty context.
1752
+ */
1753
+ getSession?: (headers: any) => Promise<any> | any;
1754
+ /** Clock injection for API-key expiry (tests). */
1755
+ nowMs?: number;
1756
+ }
1757
+ /**
1758
+ * Resolve the authorization context for an inbound request. Always resolves —
1759
+ * never throws. Anonymous requests yield `{ roles: [], permissions: [], ... }`.
1760
+ */
1761
+ declare function resolveAuthzContext(input: ResolveAuthzInput): Promise<ResolvedAuthzContext>;
1762
+ interface ResolveLocalizationInput {
1763
+ ql: any;
1764
+ /** Settings service exposing `get(namespace, key, { tenantId, userId })`. */
1765
+ settings?: any;
1766
+ tenantId?: string;
1767
+ userId?: string;
1768
+ }
1769
+ /**
1770
+ * Resolve workspace localization defaults (reference `timezone` / `locale` /
1771
+ * `currency`). Canonical path is the `localization` SettingsManifest (cascade:
1772
+ * platform default → global → tenant); falls back to direct tenant-scoped
1773
+ * `sys_setting` rows, then the built-ins `UTC` / `en-US`. Never throws.
1774
+ */
1775
+ declare function resolveLocalizationContext(input: ResolveLocalizationInput): Promise<{
1776
+ timezone: string;
1777
+ locale: string;
1778
+ currency?: string;
1779
+ }>;
1780
+
1781
+ /**
1782
+ * ADR-0069 — authentication-policy session gate.
1783
+ *
1784
+ * Some auth policies (password expiry, enforced MFA) must block an
1785
+ * authenticated user from PROTECTED RESOURCES until they remediate, while
1786
+ * still letting them reach the auth endpoints (change-password, two-factor
1787
+ * enrollment, sign-out) and a few UI-bootstrap reads.
1788
+ *
1789
+ * The posture is computed ONCE, in the auth `customSession` enrichment, and
1790
+ * attached to the session user as `user.authGate = { code, message }`. The
1791
+ * transport seams (REST middleware, dispatcher) then call
1792
+ * {@link evaluateAuthGate} to decide whether THIS request is blocked. Keeping
1793
+ * the allow-list + decision in one pure function means the seams can never
1794
+ * drift on what is blocked.
1795
+ */
1796
+ interface AuthGate {
1797
+ /** Stable machine code, e.g. `PASSWORD_EXPIRED` / `MFA_REQUIRED`. */
1798
+ code: string;
1799
+ /** Human-facing message. */
1800
+ message: string;
1801
+ }
1802
+ /** True when `path` is exempt from the auth gate (auth + remediation + health). */
1803
+ declare function isAuthGateAllowlisted(rawPath: string | undefined | null): boolean;
1804
+ /**
1805
+ * Returns the active gate when `sessionUser` carries an `authGate` AND `path`
1806
+ * is not allow-listed; otherwise null. Anonymous users (no `authGate`) and
1807
+ * allow-listed paths always pass.
1808
+ */
1809
+ declare function evaluateAuthGate(sessionUser: any, path: string): AuthGate | null;
1810
+
1724
1811
  /**
1725
1812
  * Environment utilities for universal (Node/Browser) compatibility.
1726
1813
  */
@@ -2168,4 +2255,4 @@ declare class NamespaceResolver {
2168
2255
  private suggestAlternative;
2169
2256
  }
2170
2257
 
2171
- export { API_KEY_PREFIX, type ApiKeyPrincipal, ApiRegistry, type ApiRegistryPluginConfig, CORE_FALLBACK_FACTORIES, type CalendarParts, DependencyResolver, type GeneratedApiKey, HotReloadManager, type KernelState, type KeyInput, LiteKernel, type NamespaceCheckResult, type NamespaceConflict, type NamespaceEntry, NamespaceResolver, ObjectKernel, ObjectKernelBase, type ObjectKernelConfig, ObjectLogger, type ParsedSignature, type PermissionCheckResult$1 as PermissionCheckResult, type PermissionGrant, type Plugin, type PluginArtifactVerifyResult, PluginConfigValidator, type PluginContext, PluginHealthMonitor, type PluginHealthStatus, type PluginLoadResult, PluginLoader, type PluginMetadata, type PermissionCheckResult as PluginPermissionCheckResult, PluginPermissionEnforcer, PluginPermissionManager, type PluginPermissions, PluginSandboxRuntime, PluginSecurityScanner, type PluginSignatureConfig, PluginSignatureVerifier, type PluginStartupResult, type PublisherVerifyResult, index as QA, type ResourceUsage, SIGNATURE_ALG, type SandboxContext, type ScanTarget, SecurePluginContext, type SecurityIssue, SemanticVersionManager, type ServiceFactory, ServiceLifecycle, type ServiceRegistration, type SignatureVerificationResult, type VersionCompatibility, buildPermissionsFromGrants, calendarPartsInTz, calendarPartsInTzOrUtc, counterSignPayload, createApiRegistryPlugin, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, extractApiKey, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, isExpired, isNode, parseScopes, parseSignature, resolveApiKeyPrincipal, resolveLocale, safeExit, signPayload, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature };
2258
+ export { API_KEY_PREFIX, type ApiKeyPrincipal, ApiRegistry, type ApiRegistryPluginConfig, type AuthGate, CORE_FALLBACK_FACTORIES, type CalendarParts, DependencyResolver, type GeneratedApiKey, HotReloadManager, type KernelState, type KeyInput, LiteKernel, type NamespaceCheckResult, type NamespaceConflict, type NamespaceEntry, NamespaceResolver, ObjectKernel, ObjectKernelBase, type ObjectKernelConfig, ObjectLogger, type ParsedSignature, type PermissionCheckResult$1 as PermissionCheckResult, type PermissionGrant, type Plugin, type PluginArtifactVerifyResult, PluginConfigValidator, type PluginContext, PluginHealthMonitor, type PluginHealthStatus, type PluginLoadResult, PluginLoader, type PluginMetadata, type PermissionCheckResult as PluginPermissionCheckResult, PluginPermissionEnforcer, PluginPermissionManager, type PluginPermissions, PluginSandboxRuntime, PluginSecurityScanner, type PluginSignatureConfig, PluginSignatureVerifier, type PluginStartupResult, type PublisherVerifyResult, index as QA, type ResolveAuthzInput, type ResolveLocalizationInput, type ResolvedAuthzContext, type ResourceUsage, SIGNATURE_ALG, type SandboxContext, type ScanTarget, SecurePluginContext, type SecurityIssue, SemanticVersionManager, type ServiceFactory, ServiceLifecycle, type ServiceRegistration, type SignatureVerificationResult, type VersionCompatibility, buildPermissionsFromGrants, calendarPartsInTz, calendarPartsInTzOrUtc, counterSignPayload, createApiRegistryPlugin, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, evaluateAuthGate, extractApiKey, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, isAuthGateAllowlisted, isExpired, isNode, parseScopes, parseSignature, resolveApiKeyPrincipal, resolveAuthzContext, resolveLocale, resolveLocalizationContext, safeExit, signPayload, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Logger, IServiceRegistry } from '@objectstack/spec/contracts';
2
- export { DriverInterface, IDataDriver, IDataEngine, IHttpRequest, IHttpResponse, IHttpServer, Logger, Middleware, RouteHandler } from '@objectstack/spec/contracts';
2
+ export { IDataDriver, IDataEngine, IHttpRequest, IHttpResponse, IHttpServer, Logger, Middleware, RouteHandler } from '@objectstack/spec/contracts';
3
3
  import { z } from 'zod';
4
4
  import { LoggerConfig } from '@objectstack/spec/system';
5
5
  import { ObjectLogger } from './logger.js';
@@ -1693,9 +1693,15 @@ interface GeneratedApiKey {
1693
1693
  */
1694
1694
  declare function generateApiKey(prefix?: string): GeneratedApiKey;
1695
1695
  /**
1696
- * Extract an API key from request headers. Accepts `X-API-Key: <token>` or
1697
- * `Authorization: ApiKey <token>` (case-insensitive scheme). Bearer tokens are
1698
- * deliberately NOT treated as API keys — those flow through the session path.
1696
+ * Extract an API key from request headers. Accepts, in order:
1697
+ * - `X-API-Key: <token>`
1698
+ * - `Authorization: ApiKey <token>` (case-insensitive scheme)
1699
+ * - `Authorization: Bearer <token>` ONLY when `<token>` carries the ObjectStack
1700
+ * api-key prefix (`osk_`). Remote MCP clients (Claude Desktop / Cursor /
1701
+ * Claude Code) authenticate to `/api/v1/mcp` with the key as a Bearer per the
1702
+ * MCP spec, so rejecting Bearer outright made every standard MCP client fail.
1703
+ * A better-auth *session* token never starts with `osk_`, so a session Bearer
1704
+ * still falls through to the session path — this can't shadow it.
1699
1705
  */
1700
1706
  declare function extractApiKey(headers: any): string | undefined;
1701
1707
  /** Parse a `scopes` value that may be a JSON-string textarea or a real array. */
@@ -1721,6 +1727,87 @@ interface ApiKeyPrincipal {
1721
1727
  */
1722
1728
  declare function resolveApiKeyPrincipal(ql: any, headers: any, nowMs?: number): Promise<ApiKeyPrincipal | undefined>;
1723
1729
 
1730
+ /** The transport-agnostic authorization envelope produced from a request. */
1731
+ interface ResolvedAuthzContext {
1732
+ userId?: string;
1733
+ tenantId?: string;
1734
+ email?: string;
1735
+ accessToken?: string;
1736
+ roles: string[];
1737
+ permissions: string[];
1738
+ systemPermissions: string[];
1739
+ tabPermissions?: Record<string, 'visible' | 'hidden' | 'default_on' | 'default_off'>;
1740
+ /** Fellow-org user IDs for RLS scoping of identity tables (`id IN (...)`). */
1741
+ org_user_ids: string[];
1742
+ }
1743
+ interface ResolveAuthzInput {
1744
+ /** Data engine (ObjectQL) exposing `find(object, { where, limit, context })`. */
1745
+ ql: any;
1746
+ /** Inbound request headers (Web `Headers` or a plain record). */
1747
+ headers: any;
1748
+ /**
1749
+ * Resolve a better-auth session from `headers`, returning `{ user?, session? }`
1750
+ * (or undefined). Optional — when omitted or throwing, only the API-key path
1751
+ * runs and anonymous requests resolve to an empty context.
1752
+ */
1753
+ getSession?: (headers: any) => Promise<any> | any;
1754
+ /** Clock injection for API-key expiry (tests). */
1755
+ nowMs?: number;
1756
+ }
1757
+ /**
1758
+ * Resolve the authorization context for an inbound request. Always resolves —
1759
+ * never throws. Anonymous requests yield `{ roles: [], permissions: [], ... }`.
1760
+ */
1761
+ declare function resolveAuthzContext(input: ResolveAuthzInput): Promise<ResolvedAuthzContext>;
1762
+ interface ResolveLocalizationInput {
1763
+ ql: any;
1764
+ /** Settings service exposing `get(namespace, key, { tenantId, userId })`. */
1765
+ settings?: any;
1766
+ tenantId?: string;
1767
+ userId?: string;
1768
+ }
1769
+ /**
1770
+ * Resolve workspace localization defaults (reference `timezone` / `locale` /
1771
+ * `currency`). Canonical path is the `localization` SettingsManifest (cascade:
1772
+ * platform default → global → tenant); falls back to direct tenant-scoped
1773
+ * `sys_setting` rows, then the built-ins `UTC` / `en-US`. Never throws.
1774
+ */
1775
+ declare function resolveLocalizationContext(input: ResolveLocalizationInput): Promise<{
1776
+ timezone: string;
1777
+ locale: string;
1778
+ currency?: string;
1779
+ }>;
1780
+
1781
+ /**
1782
+ * ADR-0069 — authentication-policy session gate.
1783
+ *
1784
+ * Some auth policies (password expiry, enforced MFA) must block an
1785
+ * authenticated user from PROTECTED RESOURCES until they remediate, while
1786
+ * still letting them reach the auth endpoints (change-password, two-factor
1787
+ * enrollment, sign-out) and a few UI-bootstrap reads.
1788
+ *
1789
+ * The posture is computed ONCE, in the auth `customSession` enrichment, and
1790
+ * attached to the session user as `user.authGate = { code, message }`. The
1791
+ * transport seams (REST middleware, dispatcher) then call
1792
+ * {@link evaluateAuthGate} to decide whether THIS request is blocked. Keeping
1793
+ * the allow-list + decision in one pure function means the seams can never
1794
+ * drift on what is blocked.
1795
+ */
1796
+ interface AuthGate {
1797
+ /** Stable machine code, e.g. `PASSWORD_EXPIRED` / `MFA_REQUIRED`. */
1798
+ code: string;
1799
+ /** Human-facing message. */
1800
+ message: string;
1801
+ }
1802
+ /** True when `path` is exempt from the auth gate (auth + remediation + health). */
1803
+ declare function isAuthGateAllowlisted(rawPath: string | undefined | null): boolean;
1804
+ /**
1805
+ * Returns the active gate when `sessionUser` carries an `authGate` AND `path`
1806
+ * is not allow-listed; otherwise null. Anonymous users (no `authGate`) and
1807
+ * allow-listed paths always pass.
1808
+ */
1809
+ declare function evaluateAuthGate(sessionUser: any, path: string): AuthGate | null;
1810
+
1724
1811
  /**
1725
1812
  * Environment utilities for universal (Node/Browser) compatibility.
1726
1813
  */
@@ -2168,4 +2255,4 @@ declare class NamespaceResolver {
2168
2255
  private suggestAlternative;
2169
2256
  }
2170
2257
 
2171
- export { API_KEY_PREFIX, type ApiKeyPrincipal, ApiRegistry, type ApiRegistryPluginConfig, CORE_FALLBACK_FACTORIES, type CalendarParts, DependencyResolver, type GeneratedApiKey, HotReloadManager, type KernelState, type KeyInput, LiteKernel, type NamespaceCheckResult, type NamespaceConflict, type NamespaceEntry, NamespaceResolver, ObjectKernel, ObjectKernelBase, type ObjectKernelConfig, ObjectLogger, type ParsedSignature, type PermissionCheckResult$1 as PermissionCheckResult, type PermissionGrant, type Plugin, type PluginArtifactVerifyResult, PluginConfigValidator, type PluginContext, PluginHealthMonitor, type PluginHealthStatus, type PluginLoadResult, PluginLoader, type PluginMetadata, type PermissionCheckResult as PluginPermissionCheckResult, PluginPermissionEnforcer, PluginPermissionManager, type PluginPermissions, PluginSandboxRuntime, PluginSecurityScanner, type PluginSignatureConfig, PluginSignatureVerifier, type PluginStartupResult, type PublisherVerifyResult, index as QA, type ResourceUsage, SIGNATURE_ALG, type SandboxContext, type ScanTarget, SecurePluginContext, type SecurityIssue, SemanticVersionManager, type ServiceFactory, ServiceLifecycle, type ServiceRegistration, type SignatureVerificationResult, type VersionCompatibility, buildPermissionsFromGrants, calendarPartsInTz, calendarPartsInTzOrUtc, counterSignPayload, createApiRegistryPlugin, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, extractApiKey, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, isExpired, isNode, parseScopes, parseSignature, resolveApiKeyPrincipal, resolveLocale, safeExit, signPayload, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature };
2258
+ export { API_KEY_PREFIX, type ApiKeyPrincipal, ApiRegistry, type ApiRegistryPluginConfig, type AuthGate, CORE_FALLBACK_FACTORIES, type CalendarParts, DependencyResolver, type GeneratedApiKey, HotReloadManager, type KernelState, type KeyInput, LiteKernel, type NamespaceCheckResult, type NamespaceConflict, type NamespaceEntry, NamespaceResolver, ObjectKernel, ObjectKernelBase, type ObjectKernelConfig, ObjectLogger, type ParsedSignature, type PermissionCheckResult$1 as PermissionCheckResult, type PermissionGrant, type Plugin, type PluginArtifactVerifyResult, PluginConfigValidator, type PluginContext, PluginHealthMonitor, type PluginHealthStatus, type PluginLoadResult, PluginLoader, type PluginMetadata, type PermissionCheckResult as PluginPermissionCheckResult, PluginPermissionEnforcer, PluginPermissionManager, type PluginPermissions, PluginSandboxRuntime, PluginSecurityScanner, type PluginSignatureConfig, PluginSignatureVerifier, type PluginStartupResult, type PublisherVerifyResult, index as QA, type ResolveAuthzInput, type ResolveLocalizationInput, type ResolvedAuthzContext, type ResourceUsage, SIGNATURE_ALG, type SandboxContext, type ScanTarget, SecurePluginContext, type SecurityIssue, SemanticVersionManager, type ServiceFactory, ServiceLifecycle, type ServiceRegistration, type SignatureVerificationResult, type VersionCompatibility, buildPermissionsFromGrants, calendarPartsInTz, calendarPartsInTzOrUtc, counterSignPayload, createApiRegistryPlugin, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, evaluateAuthGate, extractApiKey, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, isAuthGateAllowlisted, isExpired, isNode, parseScopes, parseSignature, resolveApiKeyPrincipal, resolveAuthzContext, resolveLocale, resolveLocalizationContext, safeExit, signPayload, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature };
package/dist/index.js CHANGED
@@ -3919,9 +3919,11 @@ function extractApiKey(headers) {
3919
3919
  if (x && x.trim()) return x.trim();
3920
3920
  const auth = readHeader(headers, "authorization");
3921
3921
  if (!auth) return void 0;
3922
- const m = auth.match(/^ApiKey\s+(.+)$/i);
3923
- const token = m?.[1]?.trim();
3924
- return token || void 0;
3922
+ const apiKeyScheme = auth.match(/^ApiKey\s+(\S.*)$/i);
3923
+ if (apiKeyScheme?.[1]?.trim()) return apiKeyScheme[1].trim();
3924
+ const bearer = auth.match(/^Bearer\s+(\S.*)$/i)?.[1]?.trim();
3925
+ if (bearer && bearer.startsWith(API_KEY_PREFIX)) return bearer;
3926
+ return void 0;
3925
3927
  }
3926
3928
  function parseScopes(value) {
3927
3929
  if (Array.isArray(value)) {
@@ -4000,6 +4002,238 @@ function safeJsonParse(s, fallback) {
4000
4002
  }
4001
4003
  }
4002
4004
 
4005
+ // src/security/resolve-authz-context.ts
4006
+ import {
4007
+ mapMembershipRole,
4008
+ BUILTIN_ROLE_PLATFORM_ADMIN,
4009
+ ADMIN_FULL_ACCESS
4010
+ } from "@objectstack/spec";
4011
+ function safeJsonParse2(s, fallback) {
4012
+ try {
4013
+ return JSON.parse(s);
4014
+ } catch {
4015
+ return fallback;
4016
+ }
4017
+ }
4018
+ async function tryFind(ql, object, where, limit = 100) {
4019
+ if (!ql || typeof ql.find !== "function") return [];
4020
+ try {
4021
+ let rows = await ql.find(object, { where, limit, context: { isSystem: true } });
4022
+ if (rows && rows.value) rows = rows.value;
4023
+ return Array.isArray(rows) ? rows : [];
4024
+ } catch {
4025
+ return [];
4026
+ }
4027
+ }
4028
+ async function resolveAuthzContext(input) {
4029
+ const { ql, headers } = input;
4030
+ const ctx = {
4031
+ roles: [],
4032
+ permissions: [],
4033
+ systemPermissions: [],
4034
+ org_user_ids: []
4035
+ };
4036
+ let userId;
4037
+ let tenantId;
4038
+ const keyPrincipal = await resolveApiKeyPrincipal(ql, headers, input.nowMs);
4039
+ if (keyPrincipal) {
4040
+ userId = keyPrincipal.userId;
4041
+ tenantId = keyPrincipal.tenantId;
4042
+ for (const scope of keyPrincipal.scopes) {
4043
+ if (!ctx.permissions.includes(scope)) ctx.permissions.push(scope);
4044
+ }
4045
+ }
4046
+ if (!userId && typeof input.getSession === "function") {
4047
+ try {
4048
+ const sessionData = await input.getSession(headers);
4049
+ userId = sessionData?.user?.id ?? sessionData?.session?.userId;
4050
+ tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
4051
+ ctx.accessToken = sessionData?.session?.token ?? ctx.accessToken;
4052
+ if (sessionData?.user?.email) ctx.email = String(sessionData.user.email);
4053
+ } catch {
4054
+ }
4055
+ }
4056
+ if (!userId) return ctx;
4057
+ ctx.userId = userId;
4058
+ if (tenantId) ctx.tenantId = tenantId;
4059
+ if (!ql || typeof ql.find !== "function") return ctx;
4060
+ let userRowLoaded = false;
4061
+ let userRow;
4062
+ const getUserRow = async () => {
4063
+ if (!userRowLoaded) {
4064
+ userRowLoaded = true;
4065
+ const rows = await tryFind(ql, "sys_user", { id: userId }, 1);
4066
+ userRow = rows[0];
4067
+ }
4068
+ return userRow;
4069
+ };
4070
+ if (!ctx.email) {
4071
+ const u = await getUserRow();
4072
+ if (u?.email) ctx.email = String(u.email);
4073
+ }
4074
+ const memberWhere = tenantId ? { user_id: userId, organization_id: tenantId } : { user_id: userId };
4075
+ const members = await tryFind(ql, "sys_member", memberWhere, 50);
4076
+ for (const m of members) {
4077
+ if (m.role && typeof m.role === "string") {
4078
+ for (const raw of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
4079
+ const r = mapMembershipRole(raw);
4080
+ if (!ctx.roles.includes(r)) ctx.roles.push(r);
4081
+ }
4082
+ }
4083
+ }
4084
+ const userRoleRows = await tryFind(ql, "sys_user_role", { user_id: userId }, 200);
4085
+ for (const ur of userRoleRows) {
4086
+ const org = ur.organization_id ?? null;
4087
+ if (org && tenantId && org !== tenantId) continue;
4088
+ const r = ur.role;
4089
+ if (typeof r === "string" && r && !ctx.roles.includes(r)) ctx.roles.push(r);
4090
+ }
4091
+ if (tenantId) {
4092
+ const orgMembers = await tryFind(ql, "sys_member", { organization_id: tenantId }, 1e3);
4093
+ const ids = new Set(
4094
+ orgMembers.map((m) => m.user_id ?? m.userId).filter((v) => typeof v === "string" && v.length > 0)
4095
+ );
4096
+ ids.add(userId);
4097
+ ctx.org_user_ids = Array.from(ids);
4098
+ } else {
4099
+ ctx.org_user_ids = [userId];
4100
+ }
4101
+ const upsRows = await tryFind(ql, "sys_user_permission_set", { user_id: userId }, 100);
4102
+ const psIds = new Set(
4103
+ upsRows.filter((r) => {
4104
+ const org = r.organization_id ?? r.organizationId ?? null;
4105
+ return !(org && tenantId && org !== tenantId);
4106
+ }).map((r) => r.permission_set_id ?? r.permissionSetId).filter(Boolean)
4107
+ );
4108
+ const unscopedUserPsIds = new Set(
4109
+ upsRows.filter((r) => (r.organization_id ?? r.organizationId ?? null) === null).map((r) => r.permission_set_id ?? r.permissionSetId).filter(Boolean)
4110
+ );
4111
+ let hasPlatformAdminGrant = false;
4112
+ if (ctx.roles.length > 0) {
4113
+ const roleRows = await tryFind(ql, "sys_role", { name: { $in: ctx.roles } }, 100);
4114
+ const roleIds = roleRows.map((r) => r.id).filter(Boolean);
4115
+ if (roleIds.length > 0) {
4116
+ const rpsRows = await tryFind(ql, "sys_role_permission_set", { role_id: { $in: roleIds } }, 500);
4117
+ for (const r of rpsRows) {
4118
+ const id = r.permission_set_id ?? r.permissionSetId;
4119
+ if (id) psIds.add(id);
4120
+ }
4121
+ }
4122
+ }
4123
+ if (psIds.size > 0) {
4124
+ const psRows = await tryFind(ql, "sys_permission_set", { id: { $in: Array.from(psIds) } }, 500);
4125
+ const tabRank = { hidden: 0, default_off: 1, default_on: 2, visible: 3 };
4126
+ const mergedTabs = {};
4127
+ for (const ps of psRows) {
4128
+ if (ps.name && !ctx.permissions.includes(ps.name)) ctx.permissions.push(ps.name);
4129
+ if (ps.name === ADMIN_FULL_ACCESS && unscopedUserPsIds.has(ps.id)) hasPlatformAdminGrant = true;
4130
+ const sysPerms = typeof ps.system_permissions === "string" ? safeJsonParse2(ps.system_permissions, []) : ps.system_permissions ?? ps.systemPermissions;
4131
+ if (Array.isArray(sysPerms)) {
4132
+ for (const p of sysPerms) {
4133
+ if (typeof p === "string" && !ctx.systemPermissions.includes(p)) ctx.systemPermissions.push(p);
4134
+ }
4135
+ }
4136
+ const tabs = typeof ps.tab_permissions === "string" ? safeJsonParse2(ps.tab_permissions, {}) : ps.tab_permissions ?? ps.tabPermissions;
4137
+ if (tabs && typeof tabs === "object") {
4138
+ for (const [app, val] of Object.entries(tabs)) {
4139
+ if (typeof val !== "string" || !(val in tabRank)) continue;
4140
+ const cur = mergedTabs[app];
4141
+ if (!cur || tabRank[val] > tabRank[cur]) {
4142
+ mergedTabs[app] = val;
4143
+ }
4144
+ }
4145
+ }
4146
+ }
4147
+ if (Object.keys(mergedTabs).length > 0) ctx.tabPermissions = mergedTabs;
4148
+ }
4149
+ if (hasPlatformAdminGrant && !ctx.roles.includes(BUILTIN_ROLE_PLATFORM_ADMIN)) {
4150
+ ctx.roles.unshift(BUILTIN_ROLE_PLATFORM_ADMIN);
4151
+ }
4152
+ if (!ctx.permissions.includes("ai_seat")) {
4153
+ const aiAccess = (await getUserRow())?.ai_access;
4154
+ if (aiAccess === true || aiAccess === 1 || aiAccess === "1") ctx.permissions.push("ai_seat");
4155
+ }
4156
+ return ctx;
4157
+ }
4158
+ function isValidTimeZone(tz) {
4159
+ try {
4160
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
4161
+ return true;
4162
+ } catch {
4163
+ return false;
4164
+ }
4165
+ }
4166
+ function coerceTimeZone(value) {
4167
+ const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
4168
+ return s && isValidTimeZone(s) ? s : void 0;
4169
+ }
4170
+ function coerceLocale(value) {
4171
+ const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
4172
+ return s || void 0;
4173
+ }
4174
+ function coerceCurrency(value) {
4175
+ const s = typeof value === "string" ? value.trim().toUpperCase() : "";
4176
+ return /^[A-Z]{3}$/.test(s) ? s : void 0;
4177
+ }
4178
+ async function resolveLocalizationContext(input) {
4179
+ const { ql, settings, tenantId, userId } = input;
4180
+ try {
4181
+ if (settings && typeof settings.get === "function") {
4182
+ const sctx = { tenantId, userId };
4183
+ const [tzRes, localeRes, currencyRes] = await Promise.all([
4184
+ settings.get("localization", "timezone", sctx).catch(() => void 0),
4185
+ settings.get("localization", "locale", sctx).catch(() => void 0),
4186
+ settings.get("localization", "currency", sctx).catch(() => void 0)
4187
+ ]);
4188
+ const tz = coerceTimeZone(tzRes?.value);
4189
+ const locale = coerceLocale(localeRes?.value);
4190
+ const currency = coerceCurrency(currencyRes?.value);
4191
+ if (tz || locale || currency) return { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency };
4192
+ }
4193
+ } catch {
4194
+ }
4195
+ const rows = await tryFind(
4196
+ ql,
4197
+ "sys_setting",
4198
+ { namespace: "localization", key: { $in: ["timezone", "locale", "currency"] }, scope: "tenant" },
4199
+ 10
4200
+ );
4201
+ const valueOf = (k) => rows.find((r) => r.key === k)?.value;
4202
+ return {
4203
+ timezone: coerceTimeZone(valueOf("timezone")) ?? "UTC",
4204
+ locale: coerceLocale(valueOf("locale")) ?? "en-US",
4205
+ currency: coerceCurrency(valueOf("currency"))
4206
+ };
4207
+ }
4208
+
4209
+ // src/security/auth-gate.ts
4210
+ var ALLOW_PREFIXES = ["/api/v1/auth/", "/api/auth/", "/auth/"];
4211
+ var ALLOW_SUFFIXES = ["/health", "/ready", "/discovery", "/me/apps", "/me/localization"];
4212
+ function isAuthGateAllowlisted(rawPath) {
4213
+ if (!rawPath) return true;
4214
+ let path = rawPath.split("?")[0] || "/";
4215
+ let end = path.length;
4216
+ while (end > 1 && path.charCodeAt(end - 1) === 47) end--;
4217
+ path = path.slice(0, end) || "/";
4218
+ if (path.includes("/auth/")) return true;
4219
+ for (const p of ALLOW_PREFIXES) {
4220
+ if (path.startsWith(p) || path === p.replace(/\/$/, "")) return true;
4221
+ }
4222
+ for (const s of ALLOW_SUFFIXES) {
4223
+ if (path.endsWith(s)) return true;
4224
+ }
4225
+ return false;
4226
+ }
4227
+ function evaluateAuthGate(sessionUser, path) {
4228
+ const gate = sessionUser?.authGate;
4229
+ if (!gate || typeof gate.code !== "string") return null;
4230
+ if (isAuthGateAllowlisted(path)) return null;
4231
+ return {
4232
+ code: gate.code,
4233
+ message: typeof gate.message === "string" && gate.message ? gate.message : "Access is blocked by an authentication policy."
4234
+ };
4235
+ }
4236
+
4003
4237
  // src/utils/datetime.ts
4004
4238
  function calendarPartsInTz(d, tz) {
4005
4239
  const parts = new Intl.DateTimeFormat("en-US", {
@@ -4975,18 +5209,22 @@ export {
4975
5209
  createMemoryQueue,
4976
5210
  createPluginConfigValidator,
4977
5211
  createPluginPermissionEnforcer,
5212
+ evaluateAuthGate,
4978
5213
  extractApiKey,
4979
5214
  generateApiKey,
4980
5215
  generateEd25519KeyPair,
4981
5216
  getEnv,
4982
5217
  getMemoryUsage,
4983
5218
  hashApiKey,
5219
+ isAuthGateAllowlisted,
4984
5220
  isExpired,
4985
5221
  isNode,
4986
5222
  parseScopes,
4987
5223
  parseSignature,
4988
5224
  resolveApiKeyPrincipal,
5225
+ resolveAuthzContext,
4989
5226
  resolveLocale,
5227
+ resolveLocalizationContext,
4990
5228
  safeExit,
4991
5229
  signPayload,
4992
5230
  verifyPayload,