@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.cjs CHANGED
@@ -60,6 +60,8 @@ __export(index_exports, {
60
60
  SecurePluginContext: () => SecurePluginContext,
61
61
  SemanticVersionManager: () => SemanticVersionManager,
62
62
  ServiceLifecycle: () => ServiceLifecycle,
63
+ UnknownFilterTokenError: () => UnknownFilterTokenError,
64
+ UnresolvedFilterTokenError: () => UnresolvedFilterTokenError,
63
65
  bucketKeyToCalendarRange: () => bucketKeyToCalendarRange,
64
66
  buildPermissionsFromGrants: () => buildPermissionsFromGrants,
65
67
  bulkWrite: () => bulkWrite,
@@ -80,6 +82,7 @@ __export(index_exports, {
80
82
  derivePosture: () => derivePosture,
81
83
  evaluateAuthGate: () => evaluateAuthGate,
82
84
  extractApiKey: () => extractApiKey,
85
+ filterTokenContextFrom: () => filterTokenContextFrom,
83
86
  generateApiKey: () => generateApiKey,
84
87
  generateEd25519KeyPair: () => generateEd25519KeyPair,
85
88
  getEnv: () => getEnv,
@@ -96,6 +99,8 @@ __export(index_exports, {
96
99
  readAuthoredTranslationLayer: () => readAuthoredTranslationLayer,
97
100
  resolveApiKeyPrincipal: () => resolveApiKeyPrincipal,
98
101
  resolveAuthzContext: () => resolveAuthzContext,
102
+ resolveFilterToken: () => resolveFilterToken,
103
+ resolveFilterTokens: () => resolveFilterTokens,
99
104
  resolveLocale: () => resolveLocale,
100
105
  resolveLocalizationContext: () => resolveLocalizationContext,
101
106
  resolveUserAuthzGrants: () => resolveUserAuthzGrants,
@@ -535,7 +540,7 @@ function createLogger(config) {
535
540
  }
536
541
 
537
542
  // src/kernel.ts
538
- var import_system = require("@objectstack/spec/system");
543
+ var import_system2 = require("@objectstack/spec/system");
539
544
 
540
545
  // src/security/plugin-config-validator.ts
541
546
  var import_zod = require("zod");
@@ -1348,6 +1353,7 @@ function createMemoryMetadata() {
1348
1353
  }
1349
1354
 
1350
1355
  // src/fallbacks/authored-translation-sync.ts
1356
+ var import_system = require("@objectstack/spec/system");
1351
1357
  var OWNER_PROP = "__authoredTranslationSyncOwner";
1352
1358
  var LOCALE_LIKE = /^[a-z]{2,3}([_-]([A-Za-z]{4}|[A-Za-z]{2}|[0-9]{3}))?$/;
1353
1359
  async function readAuthoredTranslationLayer(engine, logger) {
@@ -1377,14 +1383,32 @@ async function readAuthoredTranslationLayer(engine, logger) {
1377
1383
  continue;
1378
1384
  }
1379
1385
  if (!data || typeof data !== "object") continue;
1380
- 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;
1386
+ const legacyKeys = import_system.LEGACY_OBJECT_FIRST_KEYS.filter((key) => data[key] !== void 0);
1387
+ if (legacyKeys.length > 0) {
1388
+ logger?.warn?.(
1389
+ `[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`
1390
+ );
1391
+ continue;
1392
+ }
1393
+ const locale = typeof data?.locale === "string" && data.locale || (typeof row?.name === "string" && LOCALE_LIKE.test(row.name) ? row.name : void 0) || void 0;
1381
1394
  if (!locale) {
1382
1395
  logger?.warn?.(
1383
- `[i18n] authored translation '${row?.name}' has no resolvable locale (set _meta.locale, or name the item after its BCP-47 locale) \u2014 skipped`
1396
+ `[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`
1384
1397
  );
1385
1398
  continue;
1386
1399
  }
1387
- const { name: _n, locale: _l, _packageId: _p, _provenance: _pr, _lock: _lk, ...payload } = data;
1400
+ const {
1401
+ name: _n,
1402
+ locale: _l,
1403
+ _packageId: _p,
1404
+ _packageVersion: _pv,
1405
+ _provenance: _pr,
1406
+ _lock: _lk,
1407
+ _lockReason: _lr,
1408
+ _lockDocsUrl: _ld,
1409
+ _lockSource: _ls,
1410
+ ...payload
1411
+ } = data;
1388
1412
  byLocale[locale] = deepMerge(byLocale[locale] ?? {}, payload);
1389
1413
  }
1390
1414
  return byLocale;
@@ -1608,7 +1632,7 @@ var ObjectKernel = class {
1608
1632
  */
1609
1633
  preInjectCoreFallbacks() {
1610
1634
  if (this.config.skipSystemValidation) return;
1611
- for (const [serviceName, criticality] of Object.entries(import_system.ServiceRequirementDef)) {
1635
+ for (const [serviceName, criticality] of Object.entries(import_system2.ServiceRequirementDef)) {
1612
1636
  if (criticality !== "core") continue;
1613
1637
  const hasService = this.services.has(serviceName) || this.pluginLoader.hasService(serviceName);
1614
1638
  if (!hasService) {
@@ -1632,7 +1656,7 @@ var ObjectKernel = class {
1632
1656
  this.logger.debug("Validating system service requirements...");
1633
1657
  const missingServices = [];
1634
1658
  const missingCoreServices = [];
1635
- for (const [serviceName, criticality] of Object.entries(import_system.ServiceRequirementDef)) {
1659
+ for (const [serviceName, criticality] of Object.entries(import_system2.ServiceRequirementDef)) {
1636
1660
  const hasService = this.services.has(serviceName) || this.pluginLoader.hasService(serviceName);
1637
1661
  if (!hasService) {
1638
1662
  if (criticality === "required") {
@@ -4411,7 +4435,8 @@ async function resolveAuthzContext(input) {
4411
4435
  positions: [],
4412
4436
  permissions: [],
4413
4437
  systemPermissions: [],
4414
- org_user_ids: []
4438
+ org_user_ids: [],
4439
+ accessible_org_ids: []
4415
4440
  };
4416
4441
  let userId;
4417
4442
  let tenantId;
@@ -4447,6 +4472,7 @@ async function resolveAuthzContext(input) {
4447
4472
  ctx.permissions = grants.permissions;
4448
4473
  ctx.systemPermissions = grants.systemPermissions;
4449
4474
  ctx.org_user_ids = grants.org_user_ids;
4475
+ ctx.accessible_org_ids = grants.accessible_org_ids;
4450
4476
  if (grants.tabPermissions) ctx.tabPermissions = grants.tabPermissions;
4451
4477
  if (grants.posture) ctx.posture = grants.posture;
4452
4478
  if (grants.email && !ctx.email) ctx.email = grants.email;
@@ -4458,7 +4484,8 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4458
4484
  positions: [],
4459
4485
  permissions: Array.isArray(opts.seedPermissions) ? [...opts.seedPermissions] : [],
4460
4486
  systemPermissions: [],
4461
- org_user_ids: [userId]
4487
+ org_user_ids: [userId],
4488
+ accessible_org_ids: []
4462
4489
  };
4463
4490
  if (opts.seedEmail) grants.email = opts.seedEmail;
4464
4491
  if (!ql || typeof ql.find !== "function") return grants;
@@ -4476,9 +4503,17 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4476
4503
  const u = await getUserRow();
4477
4504
  if (u?.email) grants.email = String(u.email);
4478
4505
  }
4479
- const memberWhere = tenantId ? { user_id: userId, organization_id: tenantId } : { user_id: userId };
4480
- const members = await tryFind(ql, "sys_member", memberWhere, 50);
4506
+ const nowMs = opts.nowMs ?? Date.now();
4507
+ const members = await tryFind(ql, "sys_member", { user_id: userId }, 200);
4508
+ const accessibleOrgIds = /* @__PURE__ */ new Set();
4481
4509
  for (const m of members) {
4510
+ if (!isGrantActive(m, nowMs)) continue;
4511
+ const org = m.organization_id ?? m.organizationId;
4512
+ if (typeof org === "string" && org) accessibleOrgIds.add(org);
4513
+ }
4514
+ grants.accessible_org_ids = Array.from(accessibleOrgIds);
4515
+ const activeMembers = tenantId ? members.filter((m) => (m.organization_id ?? m.organizationId) === tenantId) : members;
4516
+ for (const m of activeMembers) {
4482
4517
  if (m.role && typeof m.role === "string") {
4483
4518
  for (const raw of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
4484
4519
  const r = (0, import_spec.mapMembershipRole)(raw);
@@ -4486,7 +4521,6 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4486
4521
  }
4487
4522
  }
4488
4523
  }
4489
- const nowMs = opts.nowMs ?? Date.now();
4490
4524
  const userPositionRows = await tryFind(ql, "sys_user_position", { user_id: userId }, 200);
4491
4525
  for (const ur of userPositionRows) {
4492
4526
  const org = ur.organization_id ?? null;
@@ -4558,7 +4592,9 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4558
4592
  }
4559
4593
  grants.posture = derivePosture({
4560
4594
  isPlatformAdmin: hasPlatformAdminGrant,
4561
- isTenantAdmin: grants.permissions.includes(import_spec.ORGANIZATION_ADMIN)
4595
+ // [ADR-0105 D4] Either org-admin capability set resolves the rung — the
4596
+ // wall-less variant differs only by withholding the superuser bits.
4597
+ isTenantAdmin: import_spec.ORGANIZATION_ADMIN_GRANTS.some((n) => grants.permissions.includes(n))
4562
4598
  });
4563
4599
  if (!grants.permissions.includes("ai_seat")) {
4564
4600
  const aiAccess = (await getUserRow())?.ai_access;
@@ -4689,8 +4725,8 @@ function calendarPartsInTzOrUtc(d, tz) {
4689
4725
  day: d.getUTCDate()
4690
4726
  };
4691
4727
  }
4692
- function zonedDateStartToUtcMs(ymd, tz) {
4693
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd);
4728
+ function zonedDateStartToUtcMs(ymd2, tz) {
4729
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd2);
4694
4730
  const wallAsUtc = m ? Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : NaN;
4695
4731
  if (!tz || tz === "UTC" || Number.isNaN(wallAsUtc)) return wallAsUtc;
4696
4732
  try {
@@ -4906,6 +4942,209 @@ async function bulkWrite(rows, opts) {
4906
4942
  return results;
4907
4943
  }
4908
4944
 
4945
+ // src/utils/filter-tokens.ts
4946
+ var import_data = require("@objectstack/spec/data");
4947
+ var UnknownFilterTokenError = class extends Error {
4948
+ constructor(token, suggestion) {
4949
+ super(
4950
+ `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."
4951
+ );
4952
+ this.status = 400;
4953
+ this.code = "FILTER_TOKEN_UNKNOWN";
4954
+ this.name = "UnknownFilterTokenError";
4955
+ this.token = token;
4956
+ this.suggestion = suggestion;
4957
+ }
4958
+ };
4959
+ var UnresolvedFilterTokenError = class extends Error {
4960
+ constructor(token, detail) {
4961
+ super(`Filter placeholder "{${token}}" cannot be resolved: ${detail}`);
4962
+ /** 400, not 500 — see {@link UnknownFilterTokenError}. */
4963
+ this.status = 400;
4964
+ this.code = "FILTER_TOKEN_UNRESOLVED";
4965
+ this.name = "UnresolvedFilterTokenError";
4966
+ this.token = token;
4967
+ }
4968
+ };
4969
+ function ymd(year, month, day) {
4970
+ const p = (n) => String(n).padStart(2, "0");
4971
+ return `${year}-${p(month)}-${p(day)}`;
4972
+ }
4973
+ function proxyDay(now, timezone) {
4974
+ const { year, month, day } = calendarPartsInTzOrUtc(now, timezone);
4975
+ return new Date(Date.UTC(year, month - 1, day));
4976
+ }
4977
+ var asYmd = (d) => ymd(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
4978
+ function startOfPeriod(kind, d) {
4979
+ const r = new Date(d.getTime());
4980
+ switch (kind) {
4981
+ case "week": {
4982
+ const dow = (r.getUTCDay() + 6) % 7;
4983
+ r.setUTCDate(r.getUTCDate() - dow);
4984
+ return r;
4985
+ }
4986
+ case "month":
4987
+ return new Date(Date.UTC(r.getUTCFullYear(), r.getUTCMonth(), 1));
4988
+ case "quarter":
4989
+ return new Date(Date.UTC(r.getUTCFullYear(), Math.floor(r.getUTCMonth() / 3) * 3, 1));
4990
+ case "year":
4991
+ return new Date(Date.UTC(r.getUTCFullYear(), 0, 1));
4992
+ }
4993
+ }
4994
+ function daysInMonth(year, month) {
4995
+ return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
4996
+ }
4997
+ function addMonthsClamped(d, n) {
4998
+ const year = d.getUTCFullYear();
4999
+ const month = d.getUTCMonth() + n;
5000
+ const targetYear = year + Math.floor(month / 12);
5001
+ const targetMonth = (month % 12 + 12) % 12;
5002
+ const day = Math.min(d.getUTCDate(), daysInMonth(targetYear, targetMonth));
5003
+ return new Date(Date.UTC(
5004
+ targetYear,
5005
+ targetMonth,
5006
+ day,
5007
+ d.getUTCHours(),
5008
+ d.getUTCMinutes(),
5009
+ d.getUTCSeconds(),
5010
+ d.getUTCMilliseconds()
5011
+ ));
5012
+ }
5013
+ function addPeriods(kind, d, n) {
5014
+ switch (kind) {
5015
+ case "week": {
5016
+ const r = new Date(d.getTime());
5017
+ r.setUTCDate(r.getUTCDate() + n * 7);
5018
+ return r;
5019
+ }
5020
+ case "month":
5021
+ return addMonthsClamped(d, n);
5022
+ case "quarter":
5023
+ return addMonthsClamped(d, n * 3);
5024
+ case "year":
5025
+ return addMonthsClamped(d, n * 12);
5026
+ }
5027
+ }
5028
+ function addUnits(unit, d, n) {
5029
+ const r = new Date(d.getTime());
5030
+ switch (unit) {
5031
+ case "minute":
5032
+ r.setUTCMinutes(r.getUTCMinutes() + n);
5033
+ return r;
5034
+ case "hour":
5035
+ r.setUTCHours(r.getUTCHours() + n);
5036
+ return r;
5037
+ case "day":
5038
+ r.setUTCDate(r.getUTCDate() + n);
5039
+ return r;
5040
+ case "week":
5041
+ r.setUTCDate(r.getUTCDate() + n * 7);
5042
+ return r;
5043
+ // Month/year steps clamp rather than overflow — see addMonthsClamped.
5044
+ case "month":
5045
+ return addMonthsClamped(d, n);
5046
+ case "year":
5047
+ return addMonthsClamped(d, n * 12);
5048
+ }
5049
+ }
5050
+ var PERIOD_RE = /^(?:(current|last|next)_)?(week|month|quarter|year)_(start|end)$/;
5051
+ function resolvePeriodToken(token, today) {
5052
+ const m = PERIOD_RE.exec(token);
5053
+ if (!m) return void 0;
5054
+ const rel = m[1] ?? "current";
5055
+ const kind = m[2];
5056
+ const bound = m[3];
5057
+ const offset = rel === "last" ? -1 : rel === "next" ? 1 : 0;
5058
+ const periodStart = startOfPeriod(kind, addPeriods(kind, startOfPeriod(kind, today), offset));
5059
+ if (bound === "start") return asYmd(periodStart);
5060
+ const next = addPeriods(kind, periodStart, 1);
5061
+ next.setUTCDate(next.getUTCDate() - 1);
5062
+ return asYmd(next);
5063
+ }
5064
+ function resolveFilterToken(token, ctx = {}) {
5065
+ const now = ctx.now ?? /* @__PURE__ */ new Date();
5066
+ if (token === "current_user_id") {
5067
+ if (!ctx.userId) {
5068
+ throw new UnresolvedFilterTokenError(
5069
+ token,
5070
+ "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."
5071
+ );
5072
+ }
5073
+ return ctx.userId;
5074
+ }
5075
+ if (token === "current_org_id") {
5076
+ if (!ctx.orgId) {
5077
+ throw new UnresolvedFilterTokenError(
5078
+ token,
5079
+ "the request carries no active organization (ExecutionContext.tenantId is unset). Set the active org on the request, or drop the token from the filter."
5080
+ );
5081
+ }
5082
+ return ctx.orgId;
5083
+ }
5084
+ const today = proxyDay(now, ctx.timezone);
5085
+ switch (token) {
5086
+ case "now":
5087
+ return now.toISOString();
5088
+ case "today":
5089
+ return asYmd(today);
5090
+ case "yesterday":
5091
+ return asYmd(addUnits("day", today, -1));
5092
+ case "tomorrow":
5093
+ return asYmd(addUnits("day", today, 1));
5094
+ }
5095
+ const period = resolvePeriodToken(token, today);
5096
+ if (period !== void 0) return period;
5097
+ const param = (0, import_data.parseDateMacroParam)(token);
5098
+ if (param) {
5099
+ const sign = param.direction === "ago" ? -1 : 1;
5100
+ if (param.unit === "minute" || param.unit === "hour") {
5101
+ return addUnits(param.unit, now, sign * param.n).toISOString();
5102
+ }
5103
+ return asYmd(addUnits(param.unit, today, sign * param.n));
5104
+ }
5105
+ return void 0;
5106
+ }
5107
+ function hasFilterToken(node) {
5108
+ if (typeof node === "string") return (0, import_data.classifyFilterToken)(node) !== null;
5109
+ if (Array.isArray(node)) return node.some(hasFilterToken);
5110
+ if (node && typeof node === "object" && !(node instanceof Date)) {
5111
+ return Object.values(node).some(hasFilterToken);
5112
+ }
5113
+ return false;
5114
+ }
5115
+ function resolveFilterTokens(filter, ctx = {}) {
5116
+ if (filter == null) return filter;
5117
+ if (!hasFilterToken(filter)) return filter;
5118
+ const pinned = { ...ctx, now: ctx.now ?? /* @__PURE__ */ new Date() };
5119
+ const walk = (node) => {
5120
+ if (typeof node === "string") {
5121
+ const cls = (0, import_data.classifyFilterToken)(node);
5122
+ if (!cls) return node;
5123
+ if (cls.kind === "unknown") throw new UnknownFilterTokenError(cls.token, cls.suggestion);
5124
+ const resolved = resolveFilterToken(cls.token, pinned);
5125
+ if (resolved === void 0) throw new UnknownFilterTokenError(cls.token);
5126
+ return resolved;
5127
+ }
5128
+ if (Array.isArray(node)) return node.map(walk);
5129
+ if (node && typeof node === "object") {
5130
+ if (node instanceof Date) return node;
5131
+ const out = {};
5132
+ for (const [k, v] of Object.entries(node)) out[k] = walk(v);
5133
+ return out;
5134
+ }
5135
+ return node;
5136
+ };
5137
+ return walk(filter);
5138
+ }
5139
+ function filterTokenContextFrom(execCtx, now) {
5140
+ return {
5141
+ now,
5142
+ timezone: execCtx?.timezone,
5143
+ userId: execCtx?.userId,
5144
+ orgId: execCtx?.tenantId
5145
+ };
5146
+ }
5147
+
4909
5148
  // src/health-monitor.ts
4910
5149
  var PluginHealthMonitor = class {
4911
5150
  constructor(logger) {
@@ -5851,6 +6090,8 @@ var NamespaceResolver = class {
5851
6090
  SecurePluginContext,
5852
6091
  SemanticVersionManager,
5853
6092
  ServiceLifecycle,
6093
+ UnknownFilterTokenError,
6094
+ UnresolvedFilterTokenError,
5854
6095
  bucketKeyToCalendarRange,
5855
6096
  buildPermissionsFromGrants,
5856
6097
  bulkWrite,
@@ -5871,6 +6112,7 @@ var NamespaceResolver = class {
5871
6112
  derivePosture,
5872
6113
  evaluateAuthGate,
5873
6114
  extractApiKey,
6115
+ filterTokenContextFrom,
5874
6116
  generateApiKey,
5875
6117
  generateEd25519KeyPair,
5876
6118
  getEnv,
@@ -5887,6 +6129,8 @@ var NamespaceResolver = class {
5887
6129
  readAuthoredTranslationLayer,
5888
6130
  resolveApiKeyPrincipal,
5889
6131
  resolveAuthzContext,
6132
+ resolveFilterToken,
6133
+ resolveFilterTokens,
5890
6134
  resolveLocale,
5891
6135
  resolveLocalizationContext,
5892
6136
  resolveUserAuthzGrants,