@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/LICENSE +202 -93
- package/dist/index.cjs +241 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +92 -5
- package/dist/index.d.ts +92 -5
- package/dist/index.js +241 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Logger, IServiceRegistry } from '@objectstack/spec/contracts';
|
|
2
|
-
export {
|
|
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
|
|
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,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 {
|
|
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
|
|
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,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
|
|
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,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,
|