@objectstack/core 10.3.0 → 11.0.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
@@ -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,57 @@ 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
+
1724
1781
  /**
1725
1782
  * Environment utilities for universal (Node/Browser) compatibility.
1726
1783
  */
@@ -2168,4 +2225,4 @@ declare class NamespaceResolver {
2168
2225
  private suggestAlternative;
2169
2226
  }
2170
2227
 
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 };
2228
+ 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 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, extractApiKey, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, isExpired, isNode, parseScopes, parseSignature, resolveApiKeyPrincipal, resolveAuthzContext, resolveLocale, resolveLocalizationContext, safeExit, signPayload, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature };
package/dist/index.d.ts CHANGED
@@ -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,57 @@ 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
+
1724
1781
  /**
1725
1782
  * Environment utilities for universal (Node/Browser) compatibility.
1726
1783
  */
@@ -2168,4 +2225,4 @@ declare class NamespaceResolver {
2168
2225
  private suggestAlternative;
2169
2226
  }
2170
2227
 
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 };
2228
+ 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 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, extractApiKey, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, 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,197 @@ 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
+ if (!ctx.email) {
4061
+ const userRows = await tryFind(ql, "sys_user", { id: userId }, 1);
4062
+ if (userRows[0]?.email) ctx.email = String(userRows[0].email);
4063
+ }
4064
+ const memberWhere = tenantId ? { user_id: userId, organization_id: tenantId } : { user_id: userId };
4065
+ const members = await tryFind(ql, "sys_member", memberWhere, 50);
4066
+ for (const m of members) {
4067
+ if (m.role && typeof m.role === "string") {
4068
+ for (const raw of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
4069
+ const r = mapMembershipRole(raw);
4070
+ if (!ctx.roles.includes(r)) ctx.roles.push(r);
4071
+ }
4072
+ }
4073
+ }
4074
+ const userRoleRows = await tryFind(ql, "sys_user_role", { user_id: userId }, 200);
4075
+ for (const ur of userRoleRows) {
4076
+ const org = ur.organization_id ?? null;
4077
+ if (org && tenantId && org !== tenantId) continue;
4078
+ const r = ur.role;
4079
+ if (typeof r === "string" && r && !ctx.roles.includes(r)) ctx.roles.push(r);
4080
+ }
4081
+ if (tenantId) {
4082
+ const orgMembers = await tryFind(ql, "sys_member", { organization_id: tenantId }, 1e3);
4083
+ const ids = new Set(
4084
+ orgMembers.map((m) => m.user_id ?? m.userId).filter((v) => typeof v === "string" && v.length > 0)
4085
+ );
4086
+ ids.add(userId);
4087
+ ctx.org_user_ids = Array.from(ids);
4088
+ } else {
4089
+ ctx.org_user_ids = [userId];
4090
+ }
4091
+ const upsRows = await tryFind(ql, "sys_user_permission_set", { user_id: userId }, 100);
4092
+ const psIds = new Set(
4093
+ upsRows.filter((r) => {
4094
+ const org = r.organization_id ?? r.organizationId ?? null;
4095
+ return !(org && tenantId && org !== tenantId);
4096
+ }).map((r) => r.permission_set_id ?? r.permissionSetId).filter(Boolean)
4097
+ );
4098
+ const unscopedUserPsIds = new Set(
4099
+ upsRows.filter((r) => (r.organization_id ?? r.organizationId ?? null) === null).map((r) => r.permission_set_id ?? r.permissionSetId).filter(Boolean)
4100
+ );
4101
+ let hasPlatformAdminGrant = false;
4102
+ if (ctx.roles.length > 0) {
4103
+ const roleRows = await tryFind(ql, "sys_role", { name: { $in: ctx.roles } }, 100);
4104
+ const roleIds = roleRows.map((r) => r.id).filter(Boolean);
4105
+ if (roleIds.length > 0) {
4106
+ const rpsRows = await tryFind(ql, "sys_role_permission_set", { role_id: { $in: roleIds } }, 500);
4107
+ for (const r of rpsRows) {
4108
+ const id = r.permission_set_id ?? r.permissionSetId;
4109
+ if (id) psIds.add(id);
4110
+ }
4111
+ }
4112
+ }
4113
+ if (psIds.size > 0) {
4114
+ const psRows = await tryFind(ql, "sys_permission_set", { id: { $in: Array.from(psIds) } }, 500);
4115
+ const tabRank = { hidden: 0, default_off: 1, default_on: 2, visible: 3 };
4116
+ const mergedTabs = {};
4117
+ for (const ps of psRows) {
4118
+ if (ps.name && !ctx.permissions.includes(ps.name)) ctx.permissions.push(ps.name);
4119
+ if (ps.name === ADMIN_FULL_ACCESS && unscopedUserPsIds.has(ps.id)) hasPlatformAdminGrant = true;
4120
+ const sysPerms = typeof ps.system_permissions === "string" ? safeJsonParse2(ps.system_permissions, []) : ps.system_permissions ?? ps.systemPermissions;
4121
+ if (Array.isArray(sysPerms)) {
4122
+ for (const p of sysPerms) {
4123
+ if (typeof p === "string" && !ctx.systemPermissions.includes(p)) ctx.systemPermissions.push(p);
4124
+ }
4125
+ }
4126
+ const tabs = typeof ps.tab_permissions === "string" ? safeJsonParse2(ps.tab_permissions, {}) : ps.tab_permissions ?? ps.tabPermissions;
4127
+ if (tabs && typeof tabs === "object") {
4128
+ for (const [app, val] of Object.entries(tabs)) {
4129
+ if (typeof val !== "string" || !(val in tabRank)) continue;
4130
+ const cur = mergedTabs[app];
4131
+ if (!cur || tabRank[val] > tabRank[cur]) {
4132
+ mergedTabs[app] = val;
4133
+ }
4134
+ }
4135
+ }
4136
+ }
4137
+ if (Object.keys(mergedTabs).length > 0) ctx.tabPermissions = mergedTabs;
4138
+ }
4139
+ if (hasPlatformAdminGrant && !ctx.roles.includes(BUILTIN_ROLE_PLATFORM_ADMIN)) {
4140
+ ctx.roles.unshift(BUILTIN_ROLE_PLATFORM_ADMIN);
4141
+ }
4142
+ if (!ctx.permissions.includes("ai_seat")) {
4143
+ const seatRows = await tryFind(ql, "sys_user", { id: userId }, 1);
4144
+ const aiAccess = seatRows?.[0]?.ai_access;
4145
+ if (aiAccess === true || aiAccess === 1 || aiAccess === "1") ctx.permissions.push("ai_seat");
4146
+ }
4147
+ return ctx;
4148
+ }
4149
+ function isValidTimeZone(tz) {
4150
+ try {
4151
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
4152
+ return true;
4153
+ } catch {
4154
+ return false;
4155
+ }
4156
+ }
4157
+ function coerceTimeZone(value) {
4158
+ const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
4159
+ return s && isValidTimeZone(s) ? s : void 0;
4160
+ }
4161
+ function coerceLocale(value) {
4162
+ const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
4163
+ return s || void 0;
4164
+ }
4165
+ function coerceCurrency(value) {
4166
+ const s = typeof value === "string" ? value.trim().toUpperCase() : "";
4167
+ return /^[A-Z]{3}$/.test(s) ? s : void 0;
4168
+ }
4169
+ async function resolveLocalizationContext(input) {
4170
+ const { ql, settings, tenantId, userId } = input;
4171
+ try {
4172
+ if (settings && typeof settings.get === "function") {
4173
+ const sctx = { tenantId, userId };
4174
+ const [tzRes, localeRes, currencyRes] = await Promise.all([
4175
+ settings.get("localization", "timezone", sctx).catch(() => void 0),
4176
+ settings.get("localization", "locale", sctx).catch(() => void 0),
4177
+ settings.get("localization", "currency", sctx).catch(() => void 0)
4178
+ ]);
4179
+ const tz = coerceTimeZone(tzRes?.value);
4180
+ const locale = coerceLocale(localeRes?.value);
4181
+ const currency = coerceCurrency(currencyRes?.value);
4182
+ if (tz || locale || currency) return { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency };
4183
+ }
4184
+ } catch {
4185
+ }
4186
+ const tzRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "timezone", scope: "tenant" }, 1);
4187
+ const localeRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "locale", scope: "tenant" }, 1);
4188
+ const currencyRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "currency", scope: "tenant" }, 1);
4189
+ return {
4190
+ timezone: coerceTimeZone(tzRows[0]?.value) ?? "UTC",
4191
+ locale: coerceLocale(localeRows[0]?.value) ?? "en-US",
4192
+ currency: coerceCurrency(currencyRows[0]?.value)
4193
+ };
4194
+ }
4195
+
4003
4196
  // src/utils/datetime.ts
4004
4197
  function calendarPartsInTz(d, tz) {
4005
4198
  const parts = new Intl.DateTimeFormat("en-US", {
@@ -4986,7 +5179,9 @@ export {
4986
5179
  parseScopes,
4987
5180
  parseSignature,
4988
5181
  resolveApiKeyPrincipal,
5182
+ resolveAuthzContext,
4989
5183
  resolveLocale,
5184
+ resolveLocalizationContext,
4990
5185
  safeExit,
4991
5186
  signPayload,
4992
5187
  verifyPayload,