@objectstack/core 10.2.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/LICENSE +202 -93
- package/dist/index.cjs +196 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -4
- package/dist/index.d.ts +61 -4
- package/dist/index.js +198 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
1697
|
-
* `
|
|
1698
|
-
*
|
|
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
|
|
1697
|
-
* `
|
|
1698
|
-
*
|
|
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
|
|
3923
|
-
|
|
3924
|
-
|
|
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,
|