@objectstack/core 16.1.0 → 17.0.0-rc.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
@@ -1751,6 +1751,14 @@ interface ResolvedAuthzContext {
1751
1751
  tabPermissions?: Record<string, 'visible' | 'hidden' | 'default_on' | 'default_off'>;
1752
1752
  /** Fellow-org user IDs for RLS scoping of identity tables (`id IN (...)`). */
1753
1753
  org_user_ids: string[];
1754
+ /**
1755
+ * [ADR-0105 D2] Every organization this principal currently holds a VALID
1756
+ * membership in — the caller's org access set, and the read reach of the
1757
+ * `group` tenancy posture (Layer 0 becomes `organization_id IN (...)`).
1758
+ * Resolved here, once, so no surface re-derives it; empty for an anonymous or
1759
+ * membership-less principal, which fails the group wall closed.
1760
+ */
1761
+ accessible_org_ids: string[];
1754
1762
  /**
1755
1763
  * [ADR-0095 D2/D3] The monotonic posture rung this principal resolves to,
1756
1764
  * DERIVED once here from held capability grants (never a better-auth role):
@@ -1788,6 +1796,8 @@ interface UserAuthzGrants {
1788
1796
  systemPermissions: string[];
1789
1797
  /** Fellow-org user IDs for RLS scoping of identity tables (`id IN (...)`). */
1790
1798
  org_user_ids: string[];
1799
+ /** [ADR-0105 D2] Organizations this user holds a currently-valid membership in. */
1800
+ accessible_org_ids: string[];
1791
1801
  tabPermissions?: Record<string, 'visible' | 'hidden' | 'default_on' | 'default_off'>;
1792
1802
  posture?: AuthzPosture;
1793
1803
  /** The user's unique email (`sys_user`), for `current_user.email` owner RLS. */
@@ -2123,12 +2133,16 @@ type BucketGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year';
2123
2133
  * `datetime` field in a reference timezone layers that on top (and, per
2124
2134
  * ADR-0053, a `date` field compares against these `YYYY-MM-DD` bounds directly).
2125
2135
  *
2126
- * Returns `null` for the null/empty bucket, an unparseable key, or a key that
2127
- * is shape-valid but out of range (e.g. `2026-13`, a `-W53` in a 52-week year,
2136
+ * Returns `null` for the empty bucket, an unparseable key, or a key that is
2137
+ * shape-valid but out of range (e.g. `2026-13`, a `-W53` in a 52-week year,
2128
2138
  * `2026-02-30`). Callers drop the range and fall back to an unscoped (superset)
2129
2139
  * drill rather than emit a wrong bound.
2140
+ *
2141
+ * `key` admits `null` because that IS the empty bucket's key on both aggregation
2142
+ * paths (#3839); callers pass a grouped row's dimension value straight through
2143
+ * rather than casting a lie.
2130
2144
  */
2131
- declare function bucketKeyToCalendarRange(key: string, granularity: BucketGranularity): {
2145
+ declare function bucketKeyToCalendarRange(key: string | null | undefined, granularity: BucketGranularity): {
2132
2146
  start: string;
2133
2147
  end: string;
2134
2148
  } | null;
@@ -2251,6 +2265,92 @@ declare function withTransientRetry<T>(fn: (attempt: number) => Promise<T>, opts
2251
2265
  */
2252
2266
  declare function bulkWrite<TRow, TRecord = any>(rows: TRow[], opts: BulkWriteOptions<TRow, TRecord>): Promise<BulkWriteRowResult<TRecord>[]>;
2253
2267
 
2268
+ /**
2269
+ * The slice of an execution context the resolver reads. Structural on purpose —
2270
+ * see {@link filterTokenContextFrom}.
2271
+ */
2272
+ interface ExecutionContextLike {
2273
+ readonly userId?: string;
2274
+ readonly tenantId?: string;
2275
+ readonly timezone?: string;
2276
+ }
2277
+ /**
2278
+ * The request-scoped values a placeholder can resolve against.
2279
+ *
2280
+ * `now` is captured ONCE per resolve call so every token in one filter shares
2281
+ * an instant — otherwise a `$gte {current_month_start}` / `$lt
2282
+ * {next_month_start}` pair evaluated microseconds apart could straddle a
2283
+ * month boundary and silently drop a row.
2284
+ */
2285
+ interface FilterTokenResolutionContext {
2286
+ /** Reference instant. Defaults to `new Date()` at call time. */
2287
+ now?: Date;
2288
+ /** IANA reference timezone for calendar boundaries. Defaults to UTC. */
2289
+ timezone?: string;
2290
+ /** Resolves `{current_user_id}`. */
2291
+ userId?: string;
2292
+ /** Resolves `{current_org_id}`. */
2293
+ orgId?: string;
2294
+ }
2295
+ /**
2296
+ * Raised when a filter carries a placeholder outside the vocabulary.
2297
+ *
2298
+ * Carries `status`/`code` so the REST layer's generic 4xx passthrough maps it
2299
+ * to a **400 with a fixable message** rather than a 500: the caller's filter is
2300
+ * malformed, the server is fine. (Same convention plugin-sharing uses for its
2301
+ * record-scope denial — no runtime dependency in either direction.)
2302
+ */
2303
+ declare class UnknownFilterTokenError extends Error {
2304
+ readonly token: string;
2305
+ readonly suggestion?: string;
2306
+ readonly status = 400;
2307
+ readonly code = "FILTER_TOKEN_UNKNOWN";
2308
+ constructor(token: string, suggestion?: string);
2309
+ }
2310
+ /**
2311
+ * Raised when a token IS in the vocabulary but the request carries no value
2312
+ * for it — an unauthenticated caller filtering on `{current_user_id}`.
2313
+ *
2314
+ * Distinct from {@link UnknownFilterTokenError} because the fix is different:
2315
+ * the metadata is correct, the context is not. Never silently resolves to
2316
+ * `null`/`undefined`, which on most drivers degrades to `IS NULL` and would
2317
+ * quietly hand back rows the filter was written to exclude.
2318
+ */
2319
+ declare class UnresolvedFilterTokenError extends Error {
2320
+ readonly token: string;
2321
+ /** 400, not 500 — see {@link UnknownFilterTokenError}. */
2322
+ readonly status = 400;
2323
+ readonly code = "FILTER_TOKEN_UNRESOLVED";
2324
+ constructor(token: string, detail: string);
2325
+ }
2326
+ /**
2327
+ * Resolve one token NAME (the bit inside the braces) to its concrete value.
2328
+ * Throws {@link UnresolvedFilterTokenError} for a vocabulary token the request
2329
+ * carries no value for. Returns `undefined` only when the token is outside the
2330
+ * vocabulary — callers turn that into {@link UnknownFilterTokenError}.
2331
+ */
2332
+ declare function resolveFilterToken(token: string, ctx?: FilterTokenResolutionContext): unknown;
2333
+ /**
2334
+ * Deep-replace every fully-wrapped placeholder in `filter` with its resolved
2335
+ * value, returning a NEW tree (the caller's metadata is never mutated — a view
2336
+ * or dataset definition is shared across requests, so resolving in place would
2337
+ * bake one request's user id, and one day's dates, into every later render).
2338
+ *
2339
+ * Returns the input unchanged, by reference, when it holds no placeholders.
2340
+ */
2341
+ declare function resolveFilterTokens<T>(filter: T, ctx?: FilterTokenResolutionContext): T;
2342
+ /**
2343
+ * Convenience bridge from an execution context to the resolver's inputs.
2344
+ * `{current_org_id}` reads `tenantId` — the active organization IS the tenant
2345
+ * on the read path (same value the RLS compiler binds to
2346
+ * `current_user.organization_id`).
2347
+ *
2348
+ * Typed structurally, not as `ExecutionContext`, so both the parsed context
2349
+ * (defaults applied) and the pre-parse `ExecutionContextInput` a caller holds
2350
+ * mid-pipeline satisfy it. The three fields read here are optional in both.
2351
+ */
2352
+ declare function filterTokenContextFrom(execCtx: ExecutionContextLike | undefined, now?: Date): FilterTokenResolutionContext;
2353
+
2254
2354
  /**
2255
2355
  * In-memory Map-backed cache fallback.
2256
2356
  *
@@ -2687,4 +2787,4 @@ declare class NamespaceResolver {
2687
2787
  private suggestAlternative;
2688
2788
  }
2689
2789
 
2690
- export { ANONYMOUS_DENY_BODY, ANONYMOUS_DENY_CODE, ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, API_KEY_PREFIX, type AnonymousDenyInput, type ApiKeyPrincipal, ApiRegistry, type ApiRegistryPluginConfig, type AuthGate, type BucketGranularity, type BulkWriteOptions, type BulkWriteRowResult, CORE_FALLBACK_FACTORIES, type CalendarParts, DependencyResolver, type GeneratedApiKey, type GrantValidityWindow, HotReloadManager, type KernelState, type KeyInput, type LadderPrincipal, type LadderRow, LiteKernel, type NamespaceCheckResult, type NamespaceConflict, type NamespaceEntry, NamespaceResolver, ObjectKernel, ObjectKernelBase, type ObjectKernelConfig, ObjectLogger, POSTURE_INJECTION_RULE, POSTURE_LADDER, POSTURE_RANK, 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 PostureEvidence, type PublisherVerifyResult, index as QA, type ResolveAuthzInput, type ResolveLocalizationInput, type ResolveUserAuthzGrantsOptions, type ResolvedAuthzContext, type ResourceUsage, type RetryOptions, SIGNATURE_ALG, type SandboxContext, type ScanTarget, SecurePluginContext, type SecurityIssue, SemanticVersionManager, type ServiceFactory, ServiceLifecycle, type ServiceRegistration, type SignatureVerificationResult, type UserAuthzGrants, type VersionCompatibility, bucketKeyToCalendarRange, buildPermissionsFromGrants, bulkWrite, calendarPartsInTz, calendarPartsInTzOrUtc, counterSignPayload, createApiRegistryPlugin, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, deepMerge, defaultIsTransientError, derivePosture, evaluateAuthGate, extractApiKey, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, isAuthGateAllowlisted, isExpired, isGrantActive, isGrantExpired, isNode, parseScopes, parseSignature, postureVisibleRows, readAuthoredTranslationLayer, resolveApiKeyPrincipal, resolveAuthzContext, resolveLocale, resolveLocalizationContext, resolveUserAuthzGrants, safeExit, shouldDenyAnonymous, signPayload, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature, wireAuthoredTranslationSync, withTransientRetry, zonedDateStartToUtcMs };
2790
+ export { ANONYMOUS_DENY_BODY, ANONYMOUS_DENY_CODE, ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, API_KEY_PREFIX, type AnonymousDenyInput, type ApiKeyPrincipal, ApiRegistry, type ApiRegistryPluginConfig, type AuthGate, type BucketGranularity, type BulkWriteOptions, type BulkWriteRowResult, CORE_FALLBACK_FACTORIES, type CalendarParts, DependencyResolver, type ExecutionContextLike, type FilterTokenResolutionContext, type GeneratedApiKey, type GrantValidityWindow, HotReloadManager, type KernelState, type KeyInput, type LadderPrincipal, type LadderRow, LiteKernel, type NamespaceCheckResult, type NamespaceConflict, type NamespaceEntry, NamespaceResolver, ObjectKernel, ObjectKernelBase, type ObjectKernelConfig, ObjectLogger, POSTURE_INJECTION_RULE, POSTURE_LADDER, POSTURE_RANK, 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 PostureEvidence, type PublisherVerifyResult, index as QA, type ResolveAuthzInput, type ResolveLocalizationInput, type ResolveUserAuthzGrantsOptions, type ResolvedAuthzContext, type ResourceUsage, type RetryOptions, SIGNATURE_ALG, type SandboxContext, type ScanTarget, SecurePluginContext, type SecurityIssue, SemanticVersionManager, type ServiceFactory, ServiceLifecycle, type ServiceRegistration, type SignatureVerificationResult, UnknownFilterTokenError, UnresolvedFilterTokenError, type UserAuthzGrants, type VersionCompatibility, bucketKeyToCalendarRange, buildPermissionsFromGrants, bulkWrite, calendarPartsInTz, calendarPartsInTzOrUtc, counterSignPayload, createApiRegistryPlugin, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, deepMerge, defaultIsTransientError, derivePosture, evaluateAuthGate, extractApiKey, filterTokenContextFrom, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, isAuthGateAllowlisted, isExpired, isGrantActive, isGrantExpired, isNode, parseScopes, parseSignature, postureVisibleRows, readAuthoredTranslationLayer, resolveApiKeyPrincipal, resolveAuthzContext, resolveFilterToken, resolveFilterTokens, resolveLocale, resolveLocalizationContext, resolveUserAuthzGrants, safeExit, shouldDenyAnonymous, signPayload, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature, wireAuthoredTranslationSync, withTransientRetry, zonedDateStartToUtcMs };
package/dist/index.d.ts CHANGED
@@ -1751,6 +1751,14 @@ interface ResolvedAuthzContext {
1751
1751
  tabPermissions?: Record<string, 'visible' | 'hidden' | 'default_on' | 'default_off'>;
1752
1752
  /** Fellow-org user IDs for RLS scoping of identity tables (`id IN (...)`). */
1753
1753
  org_user_ids: string[];
1754
+ /**
1755
+ * [ADR-0105 D2] Every organization this principal currently holds a VALID
1756
+ * membership in — the caller's org access set, and the read reach of the
1757
+ * `group` tenancy posture (Layer 0 becomes `organization_id IN (...)`).
1758
+ * Resolved here, once, so no surface re-derives it; empty for an anonymous or
1759
+ * membership-less principal, which fails the group wall closed.
1760
+ */
1761
+ accessible_org_ids: string[];
1754
1762
  /**
1755
1763
  * [ADR-0095 D2/D3] The monotonic posture rung this principal resolves to,
1756
1764
  * DERIVED once here from held capability grants (never a better-auth role):
@@ -1788,6 +1796,8 @@ interface UserAuthzGrants {
1788
1796
  systemPermissions: string[];
1789
1797
  /** Fellow-org user IDs for RLS scoping of identity tables (`id IN (...)`). */
1790
1798
  org_user_ids: string[];
1799
+ /** [ADR-0105 D2] Organizations this user holds a currently-valid membership in. */
1800
+ accessible_org_ids: string[];
1791
1801
  tabPermissions?: Record<string, 'visible' | 'hidden' | 'default_on' | 'default_off'>;
1792
1802
  posture?: AuthzPosture;
1793
1803
  /** The user's unique email (`sys_user`), for `current_user.email` owner RLS. */
@@ -2123,12 +2133,16 @@ type BucketGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year';
2123
2133
  * `datetime` field in a reference timezone layers that on top (and, per
2124
2134
  * ADR-0053, a `date` field compares against these `YYYY-MM-DD` bounds directly).
2125
2135
  *
2126
- * Returns `null` for the null/empty bucket, an unparseable key, or a key that
2127
- * is shape-valid but out of range (e.g. `2026-13`, a `-W53` in a 52-week year,
2136
+ * Returns `null` for the empty bucket, an unparseable key, or a key that is
2137
+ * shape-valid but out of range (e.g. `2026-13`, a `-W53` in a 52-week year,
2128
2138
  * `2026-02-30`). Callers drop the range and fall back to an unscoped (superset)
2129
2139
  * drill rather than emit a wrong bound.
2140
+ *
2141
+ * `key` admits `null` because that IS the empty bucket's key on both aggregation
2142
+ * paths (#3839); callers pass a grouped row's dimension value straight through
2143
+ * rather than casting a lie.
2130
2144
  */
2131
- declare function bucketKeyToCalendarRange(key: string, granularity: BucketGranularity): {
2145
+ declare function bucketKeyToCalendarRange(key: string | null | undefined, granularity: BucketGranularity): {
2132
2146
  start: string;
2133
2147
  end: string;
2134
2148
  } | null;
@@ -2251,6 +2265,92 @@ declare function withTransientRetry<T>(fn: (attempt: number) => Promise<T>, opts
2251
2265
  */
2252
2266
  declare function bulkWrite<TRow, TRecord = any>(rows: TRow[], opts: BulkWriteOptions<TRow, TRecord>): Promise<BulkWriteRowResult<TRecord>[]>;
2253
2267
 
2268
+ /**
2269
+ * The slice of an execution context the resolver reads. Structural on purpose —
2270
+ * see {@link filterTokenContextFrom}.
2271
+ */
2272
+ interface ExecutionContextLike {
2273
+ readonly userId?: string;
2274
+ readonly tenantId?: string;
2275
+ readonly timezone?: string;
2276
+ }
2277
+ /**
2278
+ * The request-scoped values a placeholder can resolve against.
2279
+ *
2280
+ * `now` is captured ONCE per resolve call so every token in one filter shares
2281
+ * an instant — otherwise a `$gte {current_month_start}` / `$lt
2282
+ * {next_month_start}` pair evaluated microseconds apart could straddle a
2283
+ * month boundary and silently drop a row.
2284
+ */
2285
+ interface FilterTokenResolutionContext {
2286
+ /** Reference instant. Defaults to `new Date()` at call time. */
2287
+ now?: Date;
2288
+ /** IANA reference timezone for calendar boundaries. Defaults to UTC. */
2289
+ timezone?: string;
2290
+ /** Resolves `{current_user_id}`. */
2291
+ userId?: string;
2292
+ /** Resolves `{current_org_id}`. */
2293
+ orgId?: string;
2294
+ }
2295
+ /**
2296
+ * Raised when a filter carries a placeholder outside the vocabulary.
2297
+ *
2298
+ * Carries `status`/`code` so the REST layer's generic 4xx passthrough maps it
2299
+ * to a **400 with a fixable message** rather than a 500: the caller's filter is
2300
+ * malformed, the server is fine. (Same convention plugin-sharing uses for its
2301
+ * record-scope denial — no runtime dependency in either direction.)
2302
+ */
2303
+ declare class UnknownFilterTokenError extends Error {
2304
+ readonly token: string;
2305
+ readonly suggestion?: string;
2306
+ readonly status = 400;
2307
+ readonly code = "FILTER_TOKEN_UNKNOWN";
2308
+ constructor(token: string, suggestion?: string);
2309
+ }
2310
+ /**
2311
+ * Raised when a token IS in the vocabulary but the request carries no value
2312
+ * for it — an unauthenticated caller filtering on `{current_user_id}`.
2313
+ *
2314
+ * Distinct from {@link UnknownFilterTokenError} because the fix is different:
2315
+ * the metadata is correct, the context is not. Never silently resolves to
2316
+ * `null`/`undefined`, which on most drivers degrades to `IS NULL` and would
2317
+ * quietly hand back rows the filter was written to exclude.
2318
+ */
2319
+ declare class UnresolvedFilterTokenError extends Error {
2320
+ readonly token: string;
2321
+ /** 400, not 500 — see {@link UnknownFilterTokenError}. */
2322
+ readonly status = 400;
2323
+ readonly code = "FILTER_TOKEN_UNRESOLVED";
2324
+ constructor(token: string, detail: string);
2325
+ }
2326
+ /**
2327
+ * Resolve one token NAME (the bit inside the braces) to its concrete value.
2328
+ * Throws {@link UnresolvedFilterTokenError} for a vocabulary token the request
2329
+ * carries no value for. Returns `undefined` only when the token is outside the
2330
+ * vocabulary — callers turn that into {@link UnknownFilterTokenError}.
2331
+ */
2332
+ declare function resolveFilterToken(token: string, ctx?: FilterTokenResolutionContext): unknown;
2333
+ /**
2334
+ * Deep-replace every fully-wrapped placeholder in `filter` with its resolved
2335
+ * value, returning a NEW tree (the caller's metadata is never mutated — a view
2336
+ * or dataset definition is shared across requests, so resolving in place would
2337
+ * bake one request's user id, and one day's dates, into every later render).
2338
+ *
2339
+ * Returns the input unchanged, by reference, when it holds no placeholders.
2340
+ */
2341
+ declare function resolveFilterTokens<T>(filter: T, ctx?: FilterTokenResolutionContext): T;
2342
+ /**
2343
+ * Convenience bridge from an execution context to the resolver's inputs.
2344
+ * `{current_org_id}` reads `tenantId` — the active organization IS the tenant
2345
+ * on the read path (same value the RLS compiler binds to
2346
+ * `current_user.organization_id`).
2347
+ *
2348
+ * Typed structurally, not as `ExecutionContext`, so both the parsed context
2349
+ * (defaults applied) and the pre-parse `ExecutionContextInput` a caller holds
2350
+ * mid-pipeline satisfy it. The three fields read here are optional in both.
2351
+ */
2352
+ declare function filterTokenContextFrom(execCtx: ExecutionContextLike | undefined, now?: Date): FilterTokenResolutionContext;
2353
+
2254
2354
  /**
2255
2355
  * In-memory Map-backed cache fallback.
2256
2356
  *
@@ -2687,4 +2787,4 @@ declare class NamespaceResolver {
2687
2787
  private suggestAlternative;
2688
2788
  }
2689
2789
 
2690
- export { ANONYMOUS_DENY_BODY, ANONYMOUS_DENY_CODE, ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, API_KEY_PREFIX, type AnonymousDenyInput, type ApiKeyPrincipal, ApiRegistry, type ApiRegistryPluginConfig, type AuthGate, type BucketGranularity, type BulkWriteOptions, type BulkWriteRowResult, CORE_FALLBACK_FACTORIES, type CalendarParts, DependencyResolver, type GeneratedApiKey, type GrantValidityWindow, HotReloadManager, type KernelState, type KeyInput, type LadderPrincipal, type LadderRow, LiteKernel, type NamespaceCheckResult, type NamespaceConflict, type NamespaceEntry, NamespaceResolver, ObjectKernel, ObjectKernelBase, type ObjectKernelConfig, ObjectLogger, POSTURE_INJECTION_RULE, POSTURE_LADDER, POSTURE_RANK, 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 PostureEvidence, type PublisherVerifyResult, index as QA, type ResolveAuthzInput, type ResolveLocalizationInput, type ResolveUserAuthzGrantsOptions, type ResolvedAuthzContext, type ResourceUsage, type RetryOptions, SIGNATURE_ALG, type SandboxContext, type ScanTarget, SecurePluginContext, type SecurityIssue, SemanticVersionManager, type ServiceFactory, ServiceLifecycle, type ServiceRegistration, type SignatureVerificationResult, type UserAuthzGrants, type VersionCompatibility, bucketKeyToCalendarRange, buildPermissionsFromGrants, bulkWrite, calendarPartsInTz, calendarPartsInTzOrUtc, counterSignPayload, createApiRegistryPlugin, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, deepMerge, defaultIsTransientError, derivePosture, evaluateAuthGate, extractApiKey, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, isAuthGateAllowlisted, isExpired, isGrantActive, isGrantExpired, isNode, parseScopes, parseSignature, postureVisibleRows, readAuthoredTranslationLayer, resolveApiKeyPrincipal, resolveAuthzContext, resolveLocale, resolveLocalizationContext, resolveUserAuthzGrants, safeExit, shouldDenyAnonymous, signPayload, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature, wireAuthoredTranslationSync, withTransientRetry, zonedDateStartToUtcMs };
2790
+ export { ANONYMOUS_DENY_BODY, ANONYMOUS_DENY_CODE, ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, API_KEY_PREFIX, type AnonymousDenyInput, type ApiKeyPrincipal, ApiRegistry, type ApiRegistryPluginConfig, type AuthGate, type BucketGranularity, type BulkWriteOptions, type BulkWriteRowResult, CORE_FALLBACK_FACTORIES, type CalendarParts, DependencyResolver, type ExecutionContextLike, type FilterTokenResolutionContext, type GeneratedApiKey, type GrantValidityWindow, HotReloadManager, type KernelState, type KeyInput, type LadderPrincipal, type LadderRow, LiteKernel, type NamespaceCheckResult, type NamespaceConflict, type NamespaceEntry, NamespaceResolver, ObjectKernel, ObjectKernelBase, type ObjectKernelConfig, ObjectLogger, POSTURE_INJECTION_RULE, POSTURE_LADDER, POSTURE_RANK, 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 PostureEvidence, type PublisherVerifyResult, index as QA, type ResolveAuthzInput, type ResolveLocalizationInput, type ResolveUserAuthzGrantsOptions, type ResolvedAuthzContext, type ResourceUsage, type RetryOptions, SIGNATURE_ALG, type SandboxContext, type ScanTarget, SecurePluginContext, type SecurityIssue, SemanticVersionManager, type ServiceFactory, ServiceLifecycle, type ServiceRegistration, type SignatureVerificationResult, UnknownFilterTokenError, UnresolvedFilterTokenError, type UserAuthzGrants, type VersionCompatibility, bucketKeyToCalendarRange, buildPermissionsFromGrants, bulkWrite, calendarPartsInTz, calendarPartsInTzOrUtc, counterSignPayload, createApiRegistryPlugin, createMemoryCache, createMemoryI18n, createMemoryJob, createMemoryMetadata, createMemoryQueue, createPluginConfigValidator, createPluginPermissionEnforcer, deepMerge, defaultIsTransientError, derivePosture, evaluateAuthGate, extractApiKey, filterTokenContextFrom, generateApiKey, generateEd25519KeyPair, getEnv, getMemoryUsage, hashApiKey, isAuthGateAllowlisted, isExpired, isGrantActive, isGrantExpired, isNode, parseScopes, parseSignature, postureVisibleRows, readAuthoredTranslationLayer, resolveApiKeyPrincipal, resolveAuthzContext, resolveFilterToken, resolveFilterTokens, resolveLocale, resolveLocalizationContext, resolveUserAuthzGrants, safeExit, shouldDenyAnonymous, signPayload, verifyPayload, verifyPlatformSignature, verifyPluginArtifact, verifyPublisherSignature, wireAuthoredTranslationSync, withTransientRetry, zonedDateStartToUtcMs };
package/dist/index.js CHANGED
@@ -1252,6 +1252,7 @@ function createMemoryMetadata() {
1252
1252
  }
1253
1253
 
1254
1254
  // src/fallbacks/authored-translation-sync.ts
1255
+ import { LEGACY_OBJECT_FIRST_KEYS } from "@objectstack/spec/system";
1255
1256
  var OWNER_PROP = "__authoredTranslationSyncOwner";
1256
1257
  var LOCALE_LIKE = /^[a-z]{2,3}([_-]([A-Za-z]{4}|[A-Za-z]{2}|[0-9]{3}))?$/;
1257
1258
  async function readAuthoredTranslationLayer(engine, logger) {
@@ -1281,14 +1282,32 @@ async function readAuthoredTranslationLayer(engine, logger) {
1281
1282
  continue;
1282
1283
  }
1283
1284
  if (!data || typeof data !== "object") continue;
1284
- const locale = typeof data?._meta?.locale === "string" && data._meta.locale || typeof data?.locale === "string" && data.locale || (typeof row?.name === "string" && LOCALE_LIKE.test(row.name) ? row.name : void 0) || void 0;
1285
+ const legacyKeys = LEGACY_OBJECT_FIRST_KEYS.filter((key) => data[key] !== void 0);
1286
+ if (legacyKeys.length > 0) {
1287
+ logger?.warn?.(
1288
+ `[i18n] authored translation '${row?.name}' uses the retired object-first shape (${legacyKeys.join(", ")}) \u2014 nothing resolves from it; re-author it under 'objects.<object_name>' with a top-level 'locale' \u2014 skipped`
1289
+ );
1290
+ continue;
1291
+ }
1292
+ const locale = typeof data?.locale === "string" && data.locale || (typeof row?.name === "string" && LOCALE_LIKE.test(row.name) ? row.name : void 0) || void 0;
1285
1293
  if (!locale) {
1286
1294
  logger?.warn?.(
1287
- `[i18n] authored translation '${row?.name}' has no resolvable locale (set _meta.locale, or name the item after its BCP-47 locale) \u2014 skipped`
1295
+ `[i18n] authored translation '${row?.name}' has no resolvable locale (set the top-level 'locale', or name the item after its BCP-47 locale) \u2014 skipped`
1288
1296
  );
1289
1297
  continue;
1290
1298
  }
1291
- const { name: _n, locale: _l, _packageId: _p, _provenance: _pr, _lock: _lk, ...payload } = data;
1299
+ const {
1300
+ name: _n,
1301
+ locale: _l,
1302
+ _packageId: _p,
1303
+ _packageVersion: _pv,
1304
+ _provenance: _pr,
1305
+ _lock: _lk,
1306
+ _lockReason: _lr,
1307
+ _lockDocsUrl: _ld,
1308
+ _lockSource: _ls,
1309
+ ...payload
1310
+ } = data;
1292
1311
  byLocale[locale] = deepMerge(byLocale[locale] ?? {}, payload);
1293
1312
  }
1294
1313
  return byLocale;
@@ -4212,7 +4231,7 @@ import {
4212
4231
  mapMembershipRole,
4213
4232
  BUILTIN_IDENTITY_PLATFORM_ADMIN,
4214
4233
  ADMIN_FULL_ACCESS,
4215
- ORGANIZATION_ADMIN
4234
+ ORGANIZATION_ADMIN_GRANTS
4216
4235
  } from "@objectstack/spec";
4217
4236
 
4218
4237
  // src/security/grant-validity.ts
@@ -4320,7 +4339,8 @@ async function resolveAuthzContext(input) {
4320
4339
  positions: [],
4321
4340
  permissions: [],
4322
4341
  systemPermissions: [],
4323
- org_user_ids: []
4342
+ org_user_ids: [],
4343
+ accessible_org_ids: []
4324
4344
  };
4325
4345
  let userId;
4326
4346
  let tenantId;
@@ -4356,6 +4376,7 @@ async function resolveAuthzContext(input) {
4356
4376
  ctx.permissions = grants.permissions;
4357
4377
  ctx.systemPermissions = grants.systemPermissions;
4358
4378
  ctx.org_user_ids = grants.org_user_ids;
4379
+ ctx.accessible_org_ids = grants.accessible_org_ids;
4359
4380
  if (grants.tabPermissions) ctx.tabPermissions = grants.tabPermissions;
4360
4381
  if (grants.posture) ctx.posture = grants.posture;
4361
4382
  if (grants.email && !ctx.email) ctx.email = grants.email;
@@ -4367,7 +4388,8 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4367
4388
  positions: [],
4368
4389
  permissions: Array.isArray(opts.seedPermissions) ? [...opts.seedPermissions] : [],
4369
4390
  systemPermissions: [],
4370
- org_user_ids: [userId]
4391
+ org_user_ids: [userId],
4392
+ accessible_org_ids: []
4371
4393
  };
4372
4394
  if (opts.seedEmail) grants.email = opts.seedEmail;
4373
4395
  if (!ql || typeof ql.find !== "function") return grants;
@@ -4385,9 +4407,17 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4385
4407
  const u = await getUserRow();
4386
4408
  if (u?.email) grants.email = String(u.email);
4387
4409
  }
4388
- const memberWhere = tenantId ? { user_id: userId, organization_id: tenantId } : { user_id: userId };
4389
- const members = await tryFind(ql, "sys_member", memberWhere, 50);
4410
+ const nowMs = opts.nowMs ?? Date.now();
4411
+ const members = await tryFind(ql, "sys_member", { user_id: userId }, 200);
4412
+ const accessibleOrgIds = /* @__PURE__ */ new Set();
4390
4413
  for (const m of members) {
4414
+ if (!isGrantActive(m, nowMs)) continue;
4415
+ const org = m.organization_id ?? m.organizationId;
4416
+ if (typeof org === "string" && org) accessibleOrgIds.add(org);
4417
+ }
4418
+ grants.accessible_org_ids = Array.from(accessibleOrgIds);
4419
+ const activeMembers = tenantId ? members.filter((m) => (m.organization_id ?? m.organizationId) === tenantId) : members;
4420
+ for (const m of activeMembers) {
4391
4421
  if (m.role && typeof m.role === "string") {
4392
4422
  for (const raw of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
4393
4423
  const r = mapMembershipRole(raw);
@@ -4395,7 +4425,6 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4395
4425
  }
4396
4426
  }
4397
4427
  }
4398
- const nowMs = opts.nowMs ?? Date.now();
4399
4428
  const userPositionRows = await tryFind(ql, "sys_user_position", { user_id: userId }, 200);
4400
4429
  for (const ur of userPositionRows) {
4401
4430
  const org = ur.organization_id ?? null;
@@ -4467,7 +4496,9 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4467
4496
  }
4468
4497
  grants.posture = derivePosture({
4469
4498
  isPlatformAdmin: hasPlatformAdminGrant,
4470
- isTenantAdmin: grants.permissions.includes(ORGANIZATION_ADMIN)
4499
+ // [ADR-0105 D4] Either org-admin capability set resolves the rung — the
4500
+ // wall-less variant differs only by withholding the superuser bits.
4501
+ isTenantAdmin: ORGANIZATION_ADMIN_GRANTS.some((n) => grants.permissions.includes(n))
4471
4502
  });
4472
4503
  if (!grants.permissions.includes("ai_seat")) {
4473
4504
  const aiAccess = (await getUserRow())?.ai_access;
@@ -4598,8 +4629,8 @@ function calendarPartsInTzOrUtc(d, tz) {
4598
4629
  day: d.getUTCDate()
4599
4630
  };
4600
4631
  }
4601
- function zonedDateStartToUtcMs(ymd, tz) {
4602
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd);
4632
+ function zonedDateStartToUtcMs(ymd2, tz) {
4633
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd2);
4603
4634
  const wallAsUtc = m ? Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : NaN;
4604
4635
  if (!tz || tz === "UTC" || Number.isNaN(wallAsUtc)) return wallAsUtc;
4605
4636
  try {
@@ -4815,6 +4846,212 @@ async function bulkWrite(rows, opts) {
4815
4846
  return results;
4816
4847
  }
4817
4848
 
4849
+ // src/utils/filter-tokens.ts
4850
+ import {
4851
+ classifyFilterToken,
4852
+ parseDateMacroParam
4853
+ } from "@objectstack/spec/data";
4854
+ var UnknownFilterTokenError = class extends Error {
4855
+ constructor(token, suggestion) {
4856
+ super(
4857
+ `Unresolvable filter placeholder "{${token}}". ` + (suggestion ? `Did you mean "{${suggestion}}"? ` : "Resolvable placeholders are the context tokens ({current_user_id}, {current_org_id}) and the date macros ({today}, {current_quarter_start}, {30_days_ago}, \u2026). ") + "Sending it to the data engine verbatim would compare it as a literal string and match nothing, which is indistinguishable from an empty result."
4858
+ );
4859
+ this.status = 400;
4860
+ this.code = "FILTER_TOKEN_UNKNOWN";
4861
+ this.name = "UnknownFilterTokenError";
4862
+ this.token = token;
4863
+ this.suggestion = suggestion;
4864
+ }
4865
+ };
4866
+ var UnresolvedFilterTokenError = class extends Error {
4867
+ constructor(token, detail) {
4868
+ super(`Filter placeholder "{${token}}" cannot be resolved: ${detail}`);
4869
+ /** 400, not 500 — see {@link UnknownFilterTokenError}. */
4870
+ this.status = 400;
4871
+ this.code = "FILTER_TOKEN_UNRESOLVED";
4872
+ this.name = "UnresolvedFilterTokenError";
4873
+ this.token = token;
4874
+ }
4875
+ };
4876
+ function ymd(year, month, day) {
4877
+ const p = (n) => String(n).padStart(2, "0");
4878
+ return `${year}-${p(month)}-${p(day)}`;
4879
+ }
4880
+ function proxyDay(now, timezone) {
4881
+ const { year, month, day } = calendarPartsInTzOrUtc(now, timezone);
4882
+ return new Date(Date.UTC(year, month - 1, day));
4883
+ }
4884
+ var asYmd = (d) => ymd(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
4885
+ function startOfPeriod(kind, d) {
4886
+ const r = new Date(d.getTime());
4887
+ switch (kind) {
4888
+ case "week": {
4889
+ const dow = (r.getUTCDay() + 6) % 7;
4890
+ r.setUTCDate(r.getUTCDate() - dow);
4891
+ return r;
4892
+ }
4893
+ case "month":
4894
+ return new Date(Date.UTC(r.getUTCFullYear(), r.getUTCMonth(), 1));
4895
+ case "quarter":
4896
+ return new Date(Date.UTC(r.getUTCFullYear(), Math.floor(r.getUTCMonth() / 3) * 3, 1));
4897
+ case "year":
4898
+ return new Date(Date.UTC(r.getUTCFullYear(), 0, 1));
4899
+ }
4900
+ }
4901
+ function daysInMonth(year, month) {
4902
+ return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
4903
+ }
4904
+ function addMonthsClamped(d, n) {
4905
+ const year = d.getUTCFullYear();
4906
+ const month = d.getUTCMonth() + n;
4907
+ const targetYear = year + Math.floor(month / 12);
4908
+ const targetMonth = (month % 12 + 12) % 12;
4909
+ const day = Math.min(d.getUTCDate(), daysInMonth(targetYear, targetMonth));
4910
+ return new Date(Date.UTC(
4911
+ targetYear,
4912
+ targetMonth,
4913
+ day,
4914
+ d.getUTCHours(),
4915
+ d.getUTCMinutes(),
4916
+ d.getUTCSeconds(),
4917
+ d.getUTCMilliseconds()
4918
+ ));
4919
+ }
4920
+ function addPeriods(kind, d, n) {
4921
+ switch (kind) {
4922
+ case "week": {
4923
+ const r = new Date(d.getTime());
4924
+ r.setUTCDate(r.getUTCDate() + n * 7);
4925
+ return r;
4926
+ }
4927
+ case "month":
4928
+ return addMonthsClamped(d, n);
4929
+ case "quarter":
4930
+ return addMonthsClamped(d, n * 3);
4931
+ case "year":
4932
+ return addMonthsClamped(d, n * 12);
4933
+ }
4934
+ }
4935
+ function addUnits(unit, d, n) {
4936
+ const r = new Date(d.getTime());
4937
+ switch (unit) {
4938
+ case "minute":
4939
+ r.setUTCMinutes(r.getUTCMinutes() + n);
4940
+ return r;
4941
+ case "hour":
4942
+ r.setUTCHours(r.getUTCHours() + n);
4943
+ return r;
4944
+ case "day":
4945
+ r.setUTCDate(r.getUTCDate() + n);
4946
+ return r;
4947
+ case "week":
4948
+ r.setUTCDate(r.getUTCDate() + n * 7);
4949
+ return r;
4950
+ // Month/year steps clamp rather than overflow — see addMonthsClamped.
4951
+ case "month":
4952
+ return addMonthsClamped(d, n);
4953
+ case "year":
4954
+ return addMonthsClamped(d, n * 12);
4955
+ }
4956
+ }
4957
+ var PERIOD_RE = /^(?:(current|last|next)_)?(week|month|quarter|year)_(start|end)$/;
4958
+ function resolvePeriodToken(token, today) {
4959
+ const m = PERIOD_RE.exec(token);
4960
+ if (!m) return void 0;
4961
+ const rel = m[1] ?? "current";
4962
+ const kind = m[2];
4963
+ const bound = m[3];
4964
+ const offset = rel === "last" ? -1 : rel === "next" ? 1 : 0;
4965
+ const periodStart = startOfPeriod(kind, addPeriods(kind, startOfPeriod(kind, today), offset));
4966
+ if (bound === "start") return asYmd(periodStart);
4967
+ const next = addPeriods(kind, periodStart, 1);
4968
+ next.setUTCDate(next.getUTCDate() - 1);
4969
+ return asYmd(next);
4970
+ }
4971
+ function resolveFilterToken(token, ctx = {}) {
4972
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
4973
+ if (token === "current_user_id") {
4974
+ if (!ctx.userId) {
4975
+ throw new UnresolvedFilterTokenError(
4976
+ token,
4977
+ "the request has no authenticated user. A filter scoped to the signed-in user cannot run for an anonymous or system caller \u2014 gate the surface on authentication, or drop the token from the filter."
4978
+ );
4979
+ }
4980
+ return ctx.userId;
4981
+ }
4982
+ if (token === "current_org_id") {
4983
+ if (!ctx.orgId) {
4984
+ throw new UnresolvedFilterTokenError(
4985
+ token,
4986
+ "the request carries no active organization (ExecutionContext.tenantId is unset). Set the active org on the request, or drop the token from the filter."
4987
+ );
4988
+ }
4989
+ return ctx.orgId;
4990
+ }
4991
+ const today = proxyDay(now, ctx.timezone);
4992
+ switch (token) {
4993
+ case "now":
4994
+ return now.toISOString();
4995
+ case "today":
4996
+ return asYmd(today);
4997
+ case "yesterday":
4998
+ return asYmd(addUnits("day", today, -1));
4999
+ case "tomorrow":
5000
+ return asYmd(addUnits("day", today, 1));
5001
+ }
5002
+ const period = resolvePeriodToken(token, today);
5003
+ if (period !== void 0) return period;
5004
+ const param = parseDateMacroParam(token);
5005
+ if (param) {
5006
+ const sign = param.direction === "ago" ? -1 : 1;
5007
+ if (param.unit === "minute" || param.unit === "hour") {
5008
+ return addUnits(param.unit, now, sign * param.n).toISOString();
5009
+ }
5010
+ return asYmd(addUnits(param.unit, today, sign * param.n));
5011
+ }
5012
+ return void 0;
5013
+ }
5014
+ function hasFilterToken(node) {
5015
+ if (typeof node === "string") return classifyFilterToken(node) !== null;
5016
+ if (Array.isArray(node)) return node.some(hasFilterToken);
5017
+ if (node && typeof node === "object" && !(node instanceof Date)) {
5018
+ return Object.values(node).some(hasFilterToken);
5019
+ }
5020
+ return false;
5021
+ }
5022
+ function resolveFilterTokens(filter, ctx = {}) {
5023
+ if (filter == null) return filter;
5024
+ if (!hasFilterToken(filter)) return filter;
5025
+ const pinned = { ...ctx, now: ctx.now ?? /* @__PURE__ */ new Date() };
5026
+ const walk = (node) => {
5027
+ if (typeof node === "string") {
5028
+ const cls = classifyFilterToken(node);
5029
+ if (!cls) return node;
5030
+ if (cls.kind === "unknown") throw new UnknownFilterTokenError(cls.token, cls.suggestion);
5031
+ const resolved = resolveFilterToken(cls.token, pinned);
5032
+ if (resolved === void 0) throw new UnknownFilterTokenError(cls.token);
5033
+ return resolved;
5034
+ }
5035
+ if (Array.isArray(node)) return node.map(walk);
5036
+ if (node && typeof node === "object") {
5037
+ if (node instanceof Date) return node;
5038
+ const out = {};
5039
+ for (const [k, v] of Object.entries(node)) out[k] = walk(v);
5040
+ return out;
5041
+ }
5042
+ return node;
5043
+ };
5044
+ return walk(filter);
5045
+ }
5046
+ function filterTokenContextFrom(execCtx, now) {
5047
+ return {
5048
+ now,
5049
+ timezone: execCtx?.timezone,
5050
+ userId: execCtx?.userId,
5051
+ orgId: execCtx?.tenantId
5052
+ };
5053
+ }
5054
+
4818
5055
  // src/health-monitor.ts
4819
5056
  var PluginHealthMonitor = class {
4820
5057
  constructor(logger) {
@@ -5759,6 +5996,8 @@ export {
5759
5996
  SecurePluginContext,
5760
5997
  SemanticVersionManager,
5761
5998
  ServiceLifecycle,
5999
+ UnknownFilterTokenError,
6000
+ UnresolvedFilterTokenError,
5762
6001
  bucketKeyToCalendarRange,
5763
6002
  buildPermissionsFromGrants,
5764
6003
  bulkWrite,
@@ -5779,6 +6018,7 @@ export {
5779
6018
  derivePosture,
5780
6019
  evaluateAuthGate,
5781
6020
  extractApiKey,
6021
+ filterTokenContextFrom,
5782
6022
  generateApiKey,
5783
6023
  generateEd25519KeyPair,
5784
6024
  getEnv,
@@ -5795,6 +6035,8 @@ export {
5795
6035
  readAuthoredTranslationLayer,
5796
6036
  resolveApiKeyPrincipal,
5797
6037
  resolveAuthzContext,
6038
+ resolveFilterToken,
6039
+ resolveFilterTokens,
5798
6040
  resolveLocale,
5799
6041
  resolveLocalizationContext,
5800
6042
  resolveUserAuthzGrants,