@objectstack/objectql 7.1.0 → 7.2.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.js CHANGED
@@ -504,7 +504,9 @@ var SchemaRegistry = class {
504
504
  const direct = collection.get(name);
505
505
  if (direct) return direct;
506
506
  for (const [key, item] of collection) {
507
- if (key.endsWith(`:${name}`)) return item;
507
+ if (key.endsWith(`:${name}`)) {
508
+ return item;
509
+ }
508
510
  }
509
511
  return void 0;
510
512
  }
@@ -1356,6 +1358,7 @@ var import_data2 = require("@objectstack/spec/data");
1356
1358
  var import_shared3 = require("@objectstack/spec/shared");
1357
1359
  var import_system = require("@objectstack/spec/system");
1358
1360
  var import_kernel4 = require("@objectstack/spec/kernel");
1361
+ var import_kernel5 = require("@objectstack/spec/kernel");
1359
1362
  var import_zod = require("zod");
1360
1363
 
1361
1364
  // src/metadata-diagnostics.ts
@@ -1603,6 +1606,19 @@ function resolveOverlaySchema(type, _item) {
1603
1606
  const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
1604
1607
  return (0, import_kernel4.getMetadataTypeSchema)(singular) ?? null;
1605
1608
  }
1609
+ function mergeArtifactProtection(item, artifactItem) {
1610
+ if (item === void 0 || item === null) return item;
1611
+ if (artifactItem === void 0 || artifactItem === null) return item;
1612
+ const a = artifactItem;
1613
+ if (typeof a !== "object") return item;
1614
+ const out = { ...item };
1615
+ if (a._lock !== void 0) out._lock = a._lock;
1616
+ if (a._lockReason !== void 0) out._lockReason = a._lockReason;
1617
+ if (a._packageId !== void 0) out._packageId = a._packageId;
1618
+ if (a._packageVersion !== void 0) out._packageVersion = a._packageVersion;
1619
+ if (a._provenance !== void 0) out._provenance = a._provenance;
1620
+ return out;
1621
+ }
1606
1622
  function simpleHash(str) {
1607
1623
  let hash = 0;
1608
1624
  for (let i = 0; i < str.length; i++) {
@@ -2094,6 +2110,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2094
2110
  const includeWarnings = request.severity === "warning";
2095
2111
  const targetTypes = request.type ? [request.type] : import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => (0, import_kernel4.getMetadataTypeSchema)(e.type)).map((e) => e.type);
2096
2112
  const entries = [];
2113
+ const stats = {};
2097
2114
  let scannedItems = 0;
2098
2115
  for (const t of targetTypes) {
2099
2116
  let listed;
@@ -2107,8 +2124,11 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2107
2124
  continue;
2108
2125
  }
2109
2126
  const items = Array.isArray(listed?.items) ? listed.items : Array.isArray(listed) ? listed : [];
2127
+ const pkgSet = /* @__PURE__ */ new Set();
2110
2128
  for (const item of items) {
2111
2129
  scannedItems += 1;
2130
+ const pkg = item?._packageId ?? null;
2131
+ if (pkg) pkgSet.add(pkg);
2112
2132
  const diag = item?._diagnostics ?? computeMetadataDiagnostics(t, item);
2113
2133
  if (!diag) continue;
2114
2134
  if (diag.valid && !includeWarnings) continue;
@@ -2119,12 +2139,14 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2119
2139
  diagnostics: diag
2120
2140
  });
2121
2141
  }
2142
+ stats[t] = { count: items.length, packages: [...pkgSet].sort() };
2122
2143
  }
2123
2144
  return {
2124
2145
  entries,
2125
2146
  total: entries.length,
2126
2147
  scannedTypes: targetTypes.length,
2127
- scannedItems
2148
+ scannedItems,
2149
+ stats
2128
2150
  };
2129
2151
  }
2130
2152
  async getMetaItems(request) {
@@ -2221,7 +2243,16 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2221
2243
  }
2222
2244
  return {
2223
2245
  type: request.type,
2224
- items: decorateMetadataItems(request.type, items)
2246
+ items: decorateMetadataItems(
2247
+ request.type,
2248
+ items.map((it) => {
2249
+ const a = this.lookupArtifactItem(
2250
+ request.type,
2251
+ it?.name
2252
+ );
2253
+ return mergeArtifactProtection(it, a);
2254
+ })
2255
+ )
2225
2256
  };
2226
2257
  }
2227
2258
  async getMetaItem(request) {
@@ -2295,10 +2326,26 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2295
2326
  if (alt) item = this.engine.registry.getItem(alt, request.name);
2296
2327
  }
2297
2328
  }
2329
+ const artifactItem = this.lookupArtifactItem(request.type, request.name);
2330
+ const decorated = decorateMetadataItem(
2331
+ request.type,
2332
+ mergeArtifactProtection(item, artifactItem)
2333
+ );
2334
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
2335
+ const lockState = (0, import_kernel5.resolveLockState)(decorated, artifactBacked);
2298
2336
  return {
2299
2337
  type: request.type,
2300
2338
  name: request.name,
2301
- item: decorateMetadataItem(request.type, item)
2339
+ item: decorated,
2340
+ lock: lockState.lock,
2341
+ ...lockState.lockReason !== void 0 ? { lockReason: lockState.lockReason } : {},
2342
+ ...lockState.lockSource !== void 0 ? { lockSource: lockState.lockSource } : {},
2343
+ ...lockState.provenance !== void 0 ? { provenance: lockState.provenance } : {},
2344
+ ...lockState.packageId !== void 0 ? { packageId: lockState.packageId } : {},
2345
+ ...lockState.packageVersion !== void 0 ? { packageVersion: lockState.packageVersion } : {},
2346
+ editable: lockState.editable,
2347
+ deletable: lockState.deletable,
2348
+ resettable: lockState.resettable
2302
2349
  };
2303
2350
  }
2304
2351
  /**
@@ -2378,6 +2425,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2378
2425
  }
2379
2426
  const effective = overlay ?? code;
2380
2427
  const _diagnostics = effective !== null && effective !== void 0 ? computeMetadataDiagnostics(request.type, effective) : void 0;
2428
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
2429
+ const lockSource = code ?? overlay ?? {};
2430
+ const lockState = (0, import_kernel5.resolveLockState)(lockSource, artifactBacked);
2381
2431
  return {
2382
2432
  type: request.type,
2383
2433
  name: request.name,
@@ -2385,9 +2435,69 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2385
2435
  overlay,
2386
2436
  overlayScope,
2387
2437
  effective,
2388
- ..._diagnostics ? { _diagnostics } : {}
2438
+ ..._diagnostics ? { _diagnostics } : {},
2439
+ lock: lockState.lock,
2440
+ ...lockState.lockReason !== void 0 ? { lockReason: lockState.lockReason } : {},
2441
+ ...lockState.lockSource !== void 0 ? { lockSource: lockState.lockSource } : {},
2442
+ ...lockState.provenance !== void 0 ? { provenance: lockState.provenance } : {},
2443
+ ...lockState.packageId !== void 0 ? { packageId: lockState.packageId } : {},
2444
+ ...lockState.packageVersion !== void 0 ? { packageVersion: lockState.packageVersion } : {},
2445
+ editable: lockState.editable,
2446
+ deletable: lockState.deletable,
2447
+ resettable: lockState.resettable
2389
2448
  };
2390
2449
  }
2450
+ /**
2451
+ * ADR-0010 §3.6 / Phase 4.1 — read the metadata-protection audit log
2452
+ * for a single item. Returns the most-recent rows of
2453
+ * `sys_metadata_audit` for this (type, name) tuple, sorted newest
2454
+ * first. Refused (`denied`) and forced (`forced`) writes both appear
2455
+ * here — they never reach the `history` endpoint, which only tracks
2456
+ * successful body snapshots.
2457
+ *
2458
+ * The table is provisioned by `platform-objects` and is the
2459
+ * compliance surface for the lock-enforcement story. When the
2460
+ * environment has not yet provisioned the table (legacy install
2461
+ * prior to ADR-0010) the call returns `{ events: [] }` instead of
2462
+ * raising, keeping the Studio tab harmless.
2463
+ */
2464
+ async auditMetaItem(request) {
2465
+ const singular = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
2466
+ const limit = Math.min(
2467
+ Math.max(1, request.limit ?? 100),
2468
+ 500
2469
+ );
2470
+ try {
2471
+ const where = {
2472
+ type: singular,
2473
+ name: request.name
2474
+ };
2475
+ const rows = await this.engine.find("sys_metadata_audit", {
2476
+ where,
2477
+ orderBy: [{ field: "occurred_at", direction: "desc" }],
2478
+ limit
2479
+ });
2480
+ const events = (Array.isArray(rows) ? rows : []).map((r) => ({
2481
+ id: r.id,
2482
+ occurredAt: typeof r.occurred_at === "string" ? r.occurred_at : r.occurred_at instanceof Date ? r.occurred_at.toISOString() : String(r.occurred_at ?? ""),
2483
+ actor: String(r.actor ?? "system"),
2484
+ source: r.source ?? null,
2485
+ operation: r.operation,
2486
+ outcome: r.outcome,
2487
+ code: String(r.code ?? ""),
2488
+ lockState: r.lock_state ?? null,
2489
+ lockOverridden: Boolean(r.lock_overridden),
2490
+ requestId: r.request_id ?? null,
2491
+ note: r.note ?? null
2492
+ }));
2493
+ return { events };
2494
+ } catch (err) {
2495
+ console.warn(
2496
+ `[Protocol] auditMetaItem read failed for ${request.type}/${request.name}: ${err?.message ?? err}`
2497
+ );
2498
+ return { events: [] };
2499
+ }
2500
+ }
2391
2501
  async getUiView(request) {
2392
2502
  const schema = this.engine.registry.getObject(request.object);
2393
2503
  if (!schema) throw new Error(`Object ${request.object} not found`);
@@ -3323,6 +3433,158 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3323
3433
  if (!item || !item._packageId) return false;
3324
3434
  return item._packageId !== "sys_metadata";
3325
3435
  }
3436
+ // ───────────────────────────────────────────────────────────────────
3437
+ // ADR-0010 — metadata protection (Phase 1: L3 item-level lock)
3438
+ // ───────────────────────────────────────────────────────────────────
3439
+ /**
3440
+ * Look up an item from the artifact registry across both the requested
3441
+ * type and its singular/plural twin. Returns `undefined` when the
3442
+ * registry is unavailable or the item is not artifact-backed.
3443
+ */
3444
+ lookupArtifactItem(type, name) {
3445
+ const registry = this.engine?.registry;
3446
+ if (!registry || typeof registry.getItem !== "function") return void 0;
3447
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
3448
+ return registry.getItem(singular, name) ?? registry.getItem(type, name);
3449
+ }
3450
+ /**
3451
+ * Resolve the effective `_lock` for an item by consulting the
3452
+ * artifact registry first, then the persisted overlay row. Artifact
3453
+ * always wins — by design, an overlay cannot loosen a packaged
3454
+ * lock (ADR-0010 §3.3).
3455
+ *
3456
+ * Returns `'none'` when nothing is locked, which is the common
3457
+ * case. Safe to call when `environmentId` is undefined (control-
3458
+ * plane bootstrap) — the lock check is only meaningful in tenant
3459
+ * scope and the caller is expected to also gate on `environmentId`.
3460
+ */
3461
+ async getEffectiveLock(type, name, organizationId) {
3462
+ const registry = this.engine?.registry;
3463
+ const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
3464
+ let artifactItem;
3465
+ if (registry && typeof registry.getItem === "function") {
3466
+ artifactItem = registry.getItem(singular, name) ?? registry.getItem(type, name);
3467
+ }
3468
+ if (artifactItem && artifactItem._packageId && artifactItem._packageId !== "sys_metadata") {
3469
+ const p = (0, import_kernel5.extractProtection)(artifactItem);
3470
+ if (p.lock !== "none") {
3471
+ return { lock: p.lock, lockReason: p.lockReason, lockSource: "artifact" };
3472
+ }
3473
+ }
3474
+ try {
3475
+ const where = {
3476
+ type,
3477
+ name,
3478
+ state: "active",
3479
+ organization_id: organizationId ?? null
3480
+ };
3481
+ const row = await this.engine.findOne("sys_metadata", { where });
3482
+ if (row) {
3483
+ const body = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
3484
+ const p = (0, import_kernel5.extractProtection)(body);
3485
+ if (p.lock !== "none") {
3486
+ return { lock: p.lock, lockReason: p.lockReason, lockSource: "overlay" };
3487
+ }
3488
+ }
3489
+ } catch {
3490
+ }
3491
+ return { lock: "none", lockReason: void 0, lockSource: void 0 };
3492
+ }
3493
+ /**
3494
+ * Best-effort audit-row writer (ADR-0010 §3.6). Failures here are
3495
+ * logged but never block the underlying decision: an environment
3496
+ * without the audit table provisioned (legacy installs before this
3497
+ * ADR landed) still answers normal API calls, just without the
3498
+ * compliance trail. Phase 2 will make the audit table a hard
3499
+ * dependency.
3500
+ */
3501
+ async recordMetadataAudit(entry) {
3502
+ try {
3503
+ await this.engine.insert("sys_metadata_audit", {
3504
+ occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
3505
+ actor: entry.actor ?? "system",
3506
+ source: entry.source ?? "protocol",
3507
+ type: import_shared3.PLURAL_TO_SINGULAR[entry.type] ?? entry.type,
3508
+ name: entry.name,
3509
+ organization_id: entry.organizationId ?? null,
3510
+ operation: entry.operation,
3511
+ outcome: entry.outcome,
3512
+ code: entry.code,
3513
+ lock_state: entry.lockState ?? "none",
3514
+ lock_overridden: entry.lockOverridden ?? false,
3515
+ request_id: entry.requestId ?? null,
3516
+ note: entry.note ?? null
3517
+ });
3518
+ } catch (err) {
3519
+ console.warn(
3520
+ `[Protocol] sys_metadata_audit write failed for ${entry.type}/${entry.name}: ${err?.message ?? err}`
3521
+ );
3522
+ }
3523
+ }
3524
+ /**
3525
+ * Phase 1 L3 enforcement for write operations (save / publish /
3526
+ * rollback). Returns null on allow. Returns the structured `Error`
3527
+ * the caller should `throw` on deny — also records the denial in
3528
+ * the audit log so refused attempts are visible in compliance
3529
+ * reports (refused writes never reach sys_metadata_history).
3530
+ */
3531
+ async assertLockAllowsWrite(args) {
3532
+ if (this.environmentId === void 0) return null;
3533
+ const state = await this.getEffectiveLock(args.type, args.name, args.organizationId ?? null);
3534
+ const refusal = (0, import_kernel5.evaluateLockForWrite)(state.lock);
3535
+ if (!refusal) return null;
3536
+ const reason = state.lockReason ?? refusal.reason;
3537
+ const err = new Error(
3538
+ `[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ""}). ${reason} \u2014 See ADR-0010 \xA73.3.`
3539
+ );
3540
+ err.code = "item_locked";
3541
+ err.status = 403;
3542
+ err.lock = state.lock;
3543
+ err.lockReason = reason;
3544
+ await this.recordMetadataAudit({
3545
+ type: args.type,
3546
+ name: args.name,
3547
+ organizationId: args.organizationId ?? null,
3548
+ operation: args.operation,
3549
+ outcome: "denied",
3550
+ code: "item_locked",
3551
+ lockState: state.lock,
3552
+ actor: args.actor,
3553
+ source: args.source ?? `protocol.${args.operation}MetaItem`,
3554
+ requestId: args.requestId,
3555
+ note: reason
3556
+ });
3557
+ return err;
3558
+ }
3559
+ /** Counterpart of {@link assertLockAllowsWrite} for delete. */
3560
+ async assertLockAllowsDelete(args) {
3561
+ if (this.environmentId === void 0) return null;
3562
+ const state = await this.getEffectiveLock(args.type, args.name, args.organizationId ?? null);
3563
+ const refusal = (0, import_kernel5.evaluateLockForDelete)(state.lock);
3564
+ if (!refusal) return null;
3565
+ const reason = state.lockReason ?? refusal.reason;
3566
+ const err = new Error(
3567
+ `[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ""}). ${reason} \u2014 See ADR-0010 \xA73.3.`
3568
+ );
3569
+ err.code = "item_locked";
3570
+ err.status = 403;
3571
+ err.lock = state.lock;
3572
+ err.lockReason = reason;
3573
+ await this.recordMetadataAudit({
3574
+ type: args.type,
3575
+ name: args.name,
3576
+ organizationId: args.organizationId ?? null,
3577
+ operation: "delete",
3578
+ outcome: "denied",
3579
+ code: "item_locked",
3580
+ lockState: state.lock,
3581
+ actor: args.actor,
3582
+ source: args.source ?? "protocol.deleteMetaItem",
3583
+ requestId: args.requestId,
3584
+ note: reason
3585
+ });
3586
+ return err;
3587
+ }
3326
3588
  /**
3327
3589
  * Mirror an object-type overlay write into the in-memory engine
3328
3590
  * registry so subsequent CRUD finds the new schema. Idempotent and
@@ -3369,6 +3631,15 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3369
3631
  err.status = 403;
3370
3632
  throw err;
3371
3633
  }
3634
+ const lockErr = await this.assertLockAllowsWrite({
3635
+ type: request.type,
3636
+ name: request.name,
3637
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3638
+ operation: "save",
3639
+ ...request.actor ? { actor: request.actor } : {},
3640
+ source: "protocol.saveMetaItem"
3641
+ });
3642
+ if (lockErr) throw lockErr;
3372
3643
  }
3373
3644
  const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3374
3645
  if (!request.force && (singularType === "object" || singularType === "field")) {
@@ -3450,6 +3721,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3450
3721
  if (mode === "publish") {
3451
3722
  this.applyObjectRegistryMutation(request);
3452
3723
  }
3724
+ await this.recordMetadataAudit({
3725
+ type: request.type,
3726
+ name: request.name,
3727
+ organizationId: orgId,
3728
+ operation: "save",
3729
+ outcome: "allowed",
3730
+ code: "ok",
3731
+ ...request.actor ? { actor: request.actor } : {},
3732
+ source: "protocol.saveMetaItem",
3733
+ note: mode === "draft" ? "draft" : "active"
3734
+ });
3453
3735
  return {
3454
3736
  success: true,
3455
3737
  version: result.version,
@@ -3572,6 +3854,15 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3572
3854
  err.status = 403;
3573
3855
  throw err;
3574
3856
  }
3857
+ const _publishLockErr = await this.assertLockAllowsWrite({
3858
+ type: request.type,
3859
+ name: request.name,
3860
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3861
+ operation: "publish",
3862
+ ...request.actor ? { actor: request.actor } : {},
3863
+ source: "protocol.publishMetaItem"
3864
+ });
3865
+ if (_publishLockErr) throw _publishLockErr;
3575
3866
  await this.ensureOverlayIndex();
3576
3867
  const orgId = request.organizationId ?? null;
3577
3868
  const repo = this.getOverlayRepo(orgId);
@@ -3639,6 +3930,15 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3639
3930
  err.status = 403;
3640
3931
  throw err;
3641
3932
  }
3933
+ const _rollbackLockErr = await this.assertLockAllowsWrite({
3934
+ type: request.type,
3935
+ name: request.name,
3936
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3937
+ operation: "rollback",
3938
+ ...request.actor ? { actor: request.actor } : {},
3939
+ source: "protocol.rollbackMetaItem"
3940
+ });
3941
+ if (_rollbackLockErr) throw _rollbackLockErr;
3642
3942
  await this.ensureOverlayIndex();
3643
3943
  const orgId = request.organizationId ?? null;
3644
3944
  const repo = this.getOverlayRepo(orgId);
@@ -3785,6 +4085,14 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3785
4085
  err.status = 403;
3786
4086
  throw err;
3787
4087
  }
4088
+ const lockErr = await this.assertLockAllowsDelete({
4089
+ type: request.type,
4090
+ name: request.name,
4091
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
4092
+ ...request.actor ? { actor: request.actor } : {},
4093
+ source: "protocol.deleteMetaItem"
4094
+ });
4095
+ if (lockErr) throw lockErr;
3788
4096
  }
3789
4097
  const singularTypeForRepo = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3790
4098
  const overlayAllowedForRepoDel = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
@@ -3829,6 +4137,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3829
4137
  } catch {
3830
4138
  }
3831
4139
  }
4140
+ await this.recordMetadataAudit({
4141
+ type: request.type,
4142
+ name: request.name,
4143
+ organizationId: orgId,
4144
+ operation: "delete",
4145
+ outcome: "allowed",
4146
+ code: "ok",
4147
+ ...request.actor ? { actor: request.actor } : {},
4148
+ source: "protocol.deleteMetaItem",
4149
+ note: targetState
4150
+ });
3832
4151
  return {
3833
4152
  success: true,
3834
4153
  reset: true,
@@ -4194,7 +4513,7 @@ _ObjectStackProtocolImplementation.RUNTIME_CREATE_ALLOWED_TYPES = (() => {
4194
4513
  var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
4195
4514
 
4196
4515
  // src/engine.ts
4197
- var import_kernel5 = require("@objectstack/spec/kernel");
4516
+ var import_kernel6 = require("@objectstack/spec/kernel");
4198
4517
  var import_core = require("@objectstack/core");
4199
4518
  var import_system2 = require("@objectstack/spec/system");
4200
4519
  var import_shared4 = require("@objectstack/spec/shared");
@@ -6483,7 +6802,7 @@ var _ObjectQL = class _ObjectQL {
6483
6802
  */
6484
6803
  createContext(ctx) {
6485
6804
  return new ScopedContext(
6486
- import_kernel5.ExecutionContextSchema.parse(ctx),
6805
+ import_kernel6.ExecutionContextSchema.parse(ctx),
6487
6806
  this
6488
6807
  );
6489
6808
  }