@objectstack/objectql 7.1.0 → 7.2.1

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.mjs CHANGED
@@ -1,7 +1,9 @@
1
1
  // src/registry.ts
2
2
  import { ObjectSchema } from "@objectstack/spec/data";
3
+ import { readEnvWithDeprecation } from "@objectstack/types";
3
4
  import { ManifestSchema, InstalledPackageSchema } from "@objectstack/spec/kernel";
4
5
  import { AppSchema } from "@objectstack/spec/ui";
6
+ import { applyProtection } from "@objectstack/spec/shared";
5
7
  var RESERVED_NAMESPACES = /* @__PURE__ */ new Set(["base", "system"]);
6
8
  var DEFAULT_OWNER_PRIORITY = 100;
7
9
  var DEFAULT_EXTENDER_PRIORITY = 200;
@@ -129,7 +131,7 @@ var SchemaRegistry = class {
129
131
  if (options.multiTenant !== void 0) {
130
132
  this.multiTenant = options.multiTenant;
131
133
  } else {
132
- this.multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
134
+ this.multiTenant = String(readEnvWithDeprecation("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
133
135
  }
134
136
  }
135
137
  get logLevel() {
@@ -231,6 +233,7 @@ var SchemaRegistry = class {
231
233
  contributors.splice(idx, 1);
232
234
  }
233
235
  }
236
+ applyProtection(schema, { packageId });
234
237
  const contributor = {
235
238
  packageId,
236
239
  namespace: namespace || "",
@@ -378,9 +381,7 @@ var SchemaRegistry = class {
378
381
  }
379
382
  const collection = this.metadata.get(type);
380
383
  const baseName = String(item[keyField]);
381
- if (packageId) {
382
- item._packageId = packageId;
383
- }
384
+ applyProtection(item, { packageId });
384
385
  try {
385
386
  this.validate(type, item);
386
387
  } catch (e) {
@@ -454,7 +455,9 @@ var SchemaRegistry = class {
454
455
  const direct = collection.get(name);
455
456
  if (direct) return direct;
456
457
  for (const [key, item] of collection) {
457
- if (key.endsWith(`:${name}`)) return item;
458
+ if (key.endsWith(`:${name}`)) {
459
+ return item;
460
+ }
458
461
  }
459
462
  return void 0;
460
463
  }
@@ -627,8 +630,12 @@ var SchemaRegistry = class {
627
630
  }
628
631
  };
629
632
 
633
+ // src/protocol.ts
634
+ import { readEnvWithDeprecation as readEnvWithDeprecation3 } from "@objectstack/types";
635
+
630
636
  // src/sys-metadata-repository.ts
631
637
  import { hashSpec, ConflictError } from "@objectstack/metadata-core";
638
+ import { readEnvWithDeprecation as readEnvWithDeprecation2 } from "@objectstack/types";
632
639
  import { DEFAULT_METADATA_TYPE_REGISTRY } from "@objectstack/spec/kernel";
633
640
  import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL } from "@objectstack/spec/shared";
634
641
  var OVERLAY_ALLOWED_TYPES = new Set(
@@ -643,7 +650,7 @@ var RUNTIME_CREATE_ALLOWED_TYPES = new Set(
643
650
  var _envWritableMetadataTypes = null;
644
651
  function envWritableMetadataTypes() {
645
652
  if (_envWritableMetadataTypes !== null) return _envWritableMetadataTypes;
646
- const raw = typeof process !== "undefined" && process?.env?.OBJECTSTACK_METADATA_WRITABLE || "";
653
+ const raw = readEnvWithDeprecation2("OS_METADATA_WRITABLE", "OBJECTSTACK_METADATA_WRITABLE") || "";
647
654
  const set = /* @__PURE__ */ new Set();
648
655
  for (const tok of raw.split(",")) {
649
656
  const t = tok.trim();
@@ -1170,7 +1177,7 @@ var SysMetadataRepository = class {
1170
1177
  * at `(type, name)`. In that case we accept types with
1171
1178
  * `allowRuntimeCreate: true`, even when `allowOrgOverride` is false.
1172
1179
  *
1173
- * The env-var escape hatch (`OBJECTSTACK_METADATA_WRITABLE`) still
1180
+ * The env-var escape hatch (`OS_METADATA_WRITABLE`) still
1174
1181
  * applies to BOTH intents, so operators can opt into artifact
1175
1182
  * overrides at runtime for emergency fixes.
1176
1183
  */
@@ -1195,7 +1202,7 @@ var SysMetadataRepository = class {
1195
1202
  const code = intent === "runtime-only" ? "not_creatable" : "not_overridable";
1196
1203
  const detail = intent === "runtime-only" ? `'${type}' has neither allowOrgOverride nor allowRuntimeCreate in the registry. ` : `'${type}' is not allowOrgOverride in the registry. `;
1197
1204
  const err = new Error(
1198
- `[${code}] ${detail}Overlay-allowed: ${Array.from(new Set(allowed)).join(", ") || "(none)"}. Set OBJECTSTACK_METADATA_WRITABLE to enable additional types at runtime.`
1205
+ `[${code}] ${detail}Overlay-allowed: ${Array.from(new Set(allowed)).join(", ") || "(none)"}. Set OS_METADATA_WRITABLE to enable additional types at runtime.`
1199
1206
  );
1200
1207
  err.code = code;
1201
1208
  err.status = 403;
@@ -1306,6 +1313,12 @@ import { parseFilterAST, isFilterAST } from "@objectstack/spec/data";
1306
1313
  import { PLURAL_TO_SINGULAR as PLURAL_TO_SINGULAR3, SINGULAR_TO_PLURAL as SINGULAR_TO_PLURAL2 } from "@objectstack/spec/shared";
1307
1314
  import { METADATA_FORM_REGISTRY } from "@objectstack/spec/system";
1308
1315
  import { DEFAULT_METADATA_TYPE_REGISTRY as DEFAULT_METADATA_TYPE_REGISTRY2, getMetadataTypeSchema as getMetadataTypeSchema2 } from "@objectstack/spec/kernel";
1316
+ import {
1317
+ extractProtection,
1318
+ evaluateLockForWrite,
1319
+ evaluateLockForDelete,
1320
+ resolveLockState
1321
+ } from "@objectstack/spec/kernel";
1309
1322
  import { z } from "zod";
1310
1323
 
1311
1324
  // src/metadata-diagnostics.ts
@@ -1553,6 +1566,21 @@ function resolveOverlaySchema(type, _item) {
1553
1566
  const singular = PLURAL_TO_SINGULAR3[type] ?? type;
1554
1567
  return getMetadataTypeSchema2(singular) ?? null;
1555
1568
  }
1569
+ function mergeArtifactProtection(item, artifactItem) {
1570
+ if (item === void 0 || item === null) return item;
1571
+ if (artifactItem === void 0 || artifactItem === null) return item;
1572
+ const a = artifactItem;
1573
+ if (typeof a !== "object") return item;
1574
+ const out = { ...item };
1575
+ if (a._lock !== void 0) out._lock = a._lock;
1576
+ if (a._lockReason !== void 0) out._lockReason = a._lockReason;
1577
+ if (a._lockDocsUrl !== void 0) out._lockDocsUrl = a._lockDocsUrl;
1578
+ if (a._lockSource !== void 0) out._lockSource = a._lockSource;
1579
+ if (a._packageId !== void 0) out._packageId = a._packageId;
1580
+ if (a._packageVersion !== void 0) out._packageVersion = a._packageVersion;
1581
+ if (a._provenance !== void 0) out._provenance = a._provenance;
1582
+ return out;
1583
+ }
1556
1584
  function simpleHash(str) {
1557
1585
  let hash = 0;
1558
1586
  for (let i = 0; i < str.length; i++) {
@@ -2044,6 +2072,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2044
2072
  const includeWarnings = request.severity === "warning";
2045
2073
  const targetTypes = request.type ? [request.type] : DEFAULT_METADATA_TYPE_REGISTRY2.filter((e) => getMetadataTypeSchema2(e.type)).map((e) => e.type);
2046
2074
  const entries = [];
2075
+ const stats = {};
2047
2076
  let scannedItems = 0;
2048
2077
  for (const t of targetTypes) {
2049
2078
  let listed;
@@ -2057,8 +2086,14 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2057
2086
  continue;
2058
2087
  }
2059
2088
  const items = Array.isArray(listed?.items) ? listed.items : Array.isArray(listed) ? listed : [];
2089
+ const pkgSet = /* @__PURE__ */ new Set();
2090
+ let lockedCount = 0;
2060
2091
  for (const item of items) {
2061
2092
  scannedItems += 1;
2093
+ const pkg = item?._packageId ?? null;
2094
+ if (pkg) pkgSet.add(pkg);
2095
+ const lock = item?._lock;
2096
+ if (lock && lock !== "none") lockedCount += 1;
2062
2097
  const diag = item?._diagnostics ?? computeMetadataDiagnostics(t, item);
2063
2098
  if (!diag) continue;
2064
2099
  if (diag.valid && !includeWarnings) continue;
@@ -2069,12 +2104,14 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2069
2104
  diagnostics: diag
2070
2105
  });
2071
2106
  }
2107
+ stats[t] = { count: items.length, locked: lockedCount, packages: [...pkgSet].sort() };
2072
2108
  }
2073
2109
  return {
2074
2110
  entries,
2075
2111
  total: entries.length,
2076
2112
  scannedTypes: targetTypes.length,
2077
- scannedItems
2113
+ scannedItems,
2114
+ stats
2078
2115
  };
2079
2116
  }
2080
2117
  async getMetaItems(request) {
@@ -2171,7 +2208,16 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2171
2208
  }
2172
2209
  return {
2173
2210
  type: request.type,
2174
- items: decorateMetadataItems(request.type, items)
2211
+ items: decorateMetadataItems(
2212
+ request.type,
2213
+ items.map((it) => {
2214
+ const a = this.lookupArtifactItem(
2215
+ request.type,
2216
+ it?.name
2217
+ );
2218
+ return mergeArtifactProtection(it, a);
2219
+ })
2220
+ )
2175
2221
  };
2176
2222
  }
2177
2223
  async getMetaItem(request) {
@@ -2245,10 +2291,27 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2245
2291
  if (alt) item = this.engine.registry.getItem(alt, request.name);
2246
2292
  }
2247
2293
  }
2294
+ const artifactItem = this.lookupArtifactItem(request.type, request.name);
2295
+ const decorated = decorateMetadataItem(
2296
+ request.type,
2297
+ mergeArtifactProtection(item, artifactItem)
2298
+ );
2299
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
2300
+ const lockState = resolveLockState(decorated, artifactBacked);
2248
2301
  return {
2249
2302
  type: request.type,
2250
2303
  name: request.name,
2251
- item: decorateMetadataItem(request.type, item)
2304
+ item: decorated,
2305
+ lock: lockState.lock,
2306
+ ...lockState.lockReason !== void 0 ? { lockReason: lockState.lockReason } : {},
2307
+ ...lockState.lockSource !== void 0 ? { lockSource: lockState.lockSource } : {},
2308
+ ...lockState.lockDocsUrl !== void 0 ? { lockDocsUrl: lockState.lockDocsUrl } : {},
2309
+ ...lockState.provenance !== void 0 ? { provenance: lockState.provenance } : {},
2310
+ ...lockState.packageId !== void 0 ? { packageId: lockState.packageId } : {},
2311
+ ...lockState.packageVersion !== void 0 ? { packageVersion: lockState.packageVersion } : {},
2312
+ editable: lockState.editable,
2313
+ deletable: lockState.deletable,
2314
+ resettable: lockState.resettable
2252
2315
  };
2253
2316
  }
2254
2317
  /**
@@ -2328,6 +2391,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2328
2391
  }
2329
2392
  const effective = overlay ?? code;
2330
2393
  const _diagnostics = effective !== null && effective !== void 0 ? computeMetadataDiagnostics(request.type, effective) : void 0;
2394
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
2395
+ const lockSource = code ?? overlay ?? {};
2396
+ const lockState = resolveLockState(lockSource, artifactBacked);
2331
2397
  return {
2332
2398
  type: request.type,
2333
2399
  name: request.name,
@@ -2335,9 +2401,70 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2335
2401
  overlay,
2336
2402
  overlayScope,
2337
2403
  effective,
2338
- ..._diagnostics ? { _diagnostics } : {}
2404
+ ..._diagnostics ? { _diagnostics } : {},
2405
+ lock: lockState.lock,
2406
+ ...lockState.lockReason !== void 0 ? { lockReason: lockState.lockReason } : {},
2407
+ ...lockState.lockSource !== void 0 ? { lockSource: lockState.lockSource } : {},
2408
+ ...lockState.lockDocsUrl !== void 0 ? { lockDocsUrl: lockState.lockDocsUrl } : {},
2409
+ ...lockState.provenance !== void 0 ? { provenance: lockState.provenance } : {},
2410
+ ...lockState.packageId !== void 0 ? { packageId: lockState.packageId } : {},
2411
+ ...lockState.packageVersion !== void 0 ? { packageVersion: lockState.packageVersion } : {},
2412
+ editable: lockState.editable,
2413
+ deletable: lockState.deletable,
2414
+ resettable: lockState.resettable
2339
2415
  };
2340
2416
  }
2417
+ /**
2418
+ * ADR-0010 §3.6 / Phase 4.1 — read the metadata-protection audit log
2419
+ * for a single item. Returns the most-recent rows of
2420
+ * `sys_metadata_audit` for this (type, name) tuple, sorted newest
2421
+ * first. Refused (`denied`) and forced (`forced`) writes both appear
2422
+ * here — they never reach the `history` endpoint, which only tracks
2423
+ * successful body snapshots.
2424
+ *
2425
+ * The table is provisioned by `platform-objects` and is the
2426
+ * compliance surface for the lock-enforcement story. When the
2427
+ * environment has not yet provisioned the table (legacy install
2428
+ * prior to ADR-0010) the call returns `{ events: [] }` instead of
2429
+ * raising, keeping the Studio tab harmless.
2430
+ */
2431
+ async auditMetaItem(request) {
2432
+ const singular = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
2433
+ const limit = Math.min(
2434
+ Math.max(1, request.limit ?? 100),
2435
+ 500
2436
+ );
2437
+ try {
2438
+ const where = {
2439
+ type: singular,
2440
+ name: request.name
2441
+ };
2442
+ const rows = await this.engine.find("sys_metadata_audit", {
2443
+ where,
2444
+ orderBy: [{ field: "occurred_at", direction: "desc" }],
2445
+ limit
2446
+ });
2447
+ const events = (Array.isArray(rows) ? rows : []).map((r) => ({
2448
+ id: r.id,
2449
+ occurredAt: typeof r.occurred_at === "string" ? r.occurred_at : r.occurred_at instanceof Date ? r.occurred_at.toISOString() : String(r.occurred_at ?? ""),
2450
+ actor: String(r.actor ?? "system"),
2451
+ source: r.source ?? null,
2452
+ operation: r.operation,
2453
+ outcome: r.outcome,
2454
+ code: String(r.code ?? ""),
2455
+ lockState: r.lock_state ?? null,
2456
+ lockOverridden: Boolean(r.lock_overridden),
2457
+ requestId: r.request_id ?? null,
2458
+ note: r.note ?? null
2459
+ }));
2460
+ return { events };
2461
+ } catch (err) {
2462
+ console.warn(
2463
+ `[Protocol] auditMetaItem read failed for ${request.type}/${request.name}: ${err?.message ?? err}`
2464
+ );
2465
+ return { events: [] };
2466
+ }
2467
+ }
2341
2468
  async getUiView(request) {
2342
2469
  const schema = this.engine.registry.getObject(request.object);
2343
2470
  if (!schema) throw new Error(`Object ${request.object} not found`);
@@ -3211,7 +3338,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3211
3338
  }
3212
3339
  static envWritableTypes() {
3213
3340
  if (this._envWritableTypes !== null) return this._envWritableTypes;
3214
- const raw = typeof process !== "undefined" && process?.env?.OBJECTSTACK_METADATA_WRITABLE || "";
3341
+ const raw = readEnvWithDeprecation3("OS_METADATA_WRITABLE", "OBJECTSTACK_METADATA_WRITABLE") || "";
3215
3342
  const set = /* @__PURE__ */ new Set();
3216
3343
  for (const tok of raw.split(",")) {
3217
3344
  const t = tok.trim();
@@ -3273,6 +3400,158 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3273
3400
  if (!item || !item._packageId) return false;
3274
3401
  return item._packageId !== "sys_metadata";
3275
3402
  }
3403
+ // ───────────────────────────────────────────────────────────────────
3404
+ // ADR-0010 — metadata protection (Phase 1: L3 item-level lock)
3405
+ // ───────────────────────────────────────────────────────────────────
3406
+ /**
3407
+ * Look up an item from the artifact registry across both the requested
3408
+ * type and its singular/plural twin. Returns `undefined` when the
3409
+ * registry is unavailable or the item is not artifact-backed.
3410
+ */
3411
+ lookupArtifactItem(type, name) {
3412
+ const registry = this.engine?.registry;
3413
+ if (!registry || typeof registry.getItem !== "function") return void 0;
3414
+ const singular = PLURAL_TO_SINGULAR3[type] ?? type;
3415
+ return registry.getItem(singular, name) ?? registry.getItem(type, name);
3416
+ }
3417
+ /**
3418
+ * Resolve the effective `_lock` for an item by consulting the
3419
+ * artifact registry first, then the persisted overlay row. Artifact
3420
+ * always wins — by design, an overlay cannot loosen a packaged
3421
+ * lock (ADR-0010 §3.3).
3422
+ *
3423
+ * Returns `'none'` when nothing is locked, which is the common
3424
+ * case. Safe to call when `environmentId` is undefined (control-
3425
+ * plane bootstrap) — the lock check is only meaningful in tenant
3426
+ * scope and the caller is expected to also gate on `environmentId`.
3427
+ */
3428
+ async getEffectiveLock(type, name, organizationId) {
3429
+ const registry = this.engine?.registry;
3430
+ const singular = PLURAL_TO_SINGULAR3[type] ?? type;
3431
+ let artifactItem;
3432
+ if (registry && typeof registry.getItem === "function") {
3433
+ artifactItem = registry.getItem(singular, name) ?? registry.getItem(type, name);
3434
+ }
3435
+ if (artifactItem && artifactItem._packageId && artifactItem._packageId !== "sys_metadata") {
3436
+ const p = extractProtection(artifactItem);
3437
+ if (p.lock !== "none") {
3438
+ return { lock: p.lock, lockReason: p.lockReason, lockSource: "artifact" };
3439
+ }
3440
+ }
3441
+ try {
3442
+ const where = {
3443
+ type,
3444
+ name,
3445
+ state: "active",
3446
+ organization_id: organizationId ?? null
3447
+ };
3448
+ const row = await this.engine.findOne("sys_metadata", { where });
3449
+ if (row) {
3450
+ const body = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
3451
+ const p = extractProtection(body);
3452
+ if (p.lock !== "none") {
3453
+ return { lock: p.lock, lockReason: p.lockReason, lockSource: "overlay" };
3454
+ }
3455
+ }
3456
+ } catch {
3457
+ }
3458
+ return { lock: "none", lockReason: void 0, lockSource: void 0 };
3459
+ }
3460
+ /**
3461
+ * Best-effort audit-row writer (ADR-0010 §3.6). Failures here are
3462
+ * logged but never block the underlying decision: an environment
3463
+ * without the audit table provisioned (legacy installs before this
3464
+ * ADR landed) still answers normal API calls, just without the
3465
+ * compliance trail. Phase 2 will make the audit table a hard
3466
+ * dependency.
3467
+ */
3468
+ async recordMetadataAudit(entry) {
3469
+ try {
3470
+ await this.engine.insert("sys_metadata_audit", {
3471
+ occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
3472
+ actor: entry.actor ?? "system",
3473
+ source: entry.source ?? "protocol",
3474
+ type: PLURAL_TO_SINGULAR3[entry.type] ?? entry.type,
3475
+ name: entry.name,
3476
+ organization_id: entry.organizationId ?? null,
3477
+ operation: entry.operation,
3478
+ outcome: entry.outcome,
3479
+ code: entry.code,
3480
+ lock_state: entry.lockState ?? "none",
3481
+ lock_overridden: entry.lockOverridden ?? false,
3482
+ request_id: entry.requestId ?? null,
3483
+ note: entry.note ?? null
3484
+ });
3485
+ } catch (err) {
3486
+ console.warn(
3487
+ `[Protocol] sys_metadata_audit write failed for ${entry.type}/${entry.name}: ${err?.message ?? err}`
3488
+ );
3489
+ }
3490
+ }
3491
+ /**
3492
+ * Phase 1 L3 enforcement for write operations (save / publish /
3493
+ * rollback). Returns null on allow. Returns the structured `Error`
3494
+ * the caller should `throw` on deny — also records the denial in
3495
+ * the audit log so refused attempts are visible in compliance
3496
+ * reports (refused writes never reach sys_metadata_history).
3497
+ */
3498
+ async assertLockAllowsWrite(args) {
3499
+ if (this.environmentId === void 0) return null;
3500
+ const state = await this.getEffectiveLock(args.type, args.name, args.organizationId ?? null);
3501
+ const refusal = evaluateLockForWrite(state.lock);
3502
+ if (!refusal) return null;
3503
+ const reason = state.lockReason ?? refusal.reason;
3504
+ const err = new Error(
3505
+ `[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ""}). ${reason} \u2014 See ADR-0010 \xA73.3.`
3506
+ );
3507
+ err.code = "item_locked";
3508
+ err.status = 403;
3509
+ err.lock = state.lock;
3510
+ err.lockReason = reason;
3511
+ await this.recordMetadataAudit({
3512
+ type: args.type,
3513
+ name: args.name,
3514
+ organizationId: args.organizationId ?? null,
3515
+ operation: args.operation,
3516
+ outcome: "denied",
3517
+ code: "item_locked",
3518
+ lockState: state.lock,
3519
+ actor: args.actor,
3520
+ source: args.source ?? `protocol.${args.operation}MetaItem`,
3521
+ requestId: args.requestId,
3522
+ note: reason
3523
+ });
3524
+ return err;
3525
+ }
3526
+ /** Counterpart of {@link assertLockAllowsWrite} for delete. */
3527
+ async assertLockAllowsDelete(args) {
3528
+ if (this.environmentId === void 0) return null;
3529
+ const state = await this.getEffectiveLock(args.type, args.name, args.organizationId ?? null);
3530
+ const refusal = evaluateLockForDelete(state.lock);
3531
+ if (!refusal) return null;
3532
+ const reason = state.lockReason ?? refusal.reason;
3533
+ const err = new Error(
3534
+ `[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ""}). ${reason} \u2014 See ADR-0010 \xA73.3.`
3535
+ );
3536
+ err.code = "item_locked";
3537
+ err.status = 403;
3538
+ err.lock = state.lock;
3539
+ err.lockReason = reason;
3540
+ await this.recordMetadataAudit({
3541
+ type: args.type,
3542
+ name: args.name,
3543
+ organizationId: args.organizationId ?? null,
3544
+ operation: "delete",
3545
+ outcome: "denied",
3546
+ code: "item_locked",
3547
+ lockState: state.lock,
3548
+ actor: args.actor,
3549
+ source: args.source ?? "protocol.deleteMetaItem",
3550
+ requestId: args.requestId,
3551
+ note: reason
3552
+ });
3553
+ return err;
3554
+ }
3276
3555
  /**
3277
3556
  * Mirror an object-type overlay write into the in-memory engine
3278
3557
  * registry so subsequent CRUD finds the new schema. Idempotent and
@@ -3305,7 +3584,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3305
3584
  const artifactBacked = this.isArtifactBacked(request.type, request.name);
3306
3585
  if (artifactBacked && !overlayAllowed) {
3307
3586
  const err = new Error(
3308
- `[not_overridable] Metadata item '${request.type}/${request.name}' is provided by a code package and the type has not opted into per-org overlay writes (allowOrgOverride=false). Edit the source artifact and redeploy, or set OBJECTSTACK_METADATA_WRITABLE to grant a runtime escape hatch. See docs/adr/0005-metadata-customization-overlay.md.`
3587
+ `[not_overridable] Metadata item '${request.type}/${request.name}' is provided by a code package and the type has not opted into per-org overlay writes (allowOrgOverride=false). Edit the source artifact and redeploy, or set OS_METADATA_WRITABLE to grant a runtime escape hatch. See docs/adr/0005-metadata-customization-overlay.md.`
3309
3588
  );
3310
3589
  err.code = "not_overridable";
3311
3590
  err.status = 403;
@@ -3319,6 +3598,15 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3319
3598
  err.status = 403;
3320
3599
  throw err;
3321
3600
  }
3601
+ const lockErr = await this.assertLockAllowsWrite({
3602
+ type: request.type,
3603
+ name: request.name,
3604
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3605
+ operation: "save",
3606
+ ...request.actor ? { actor: request.actor } : {},
3607
+ source: "protocol.saveMetaItem"
3608
+ });
3609
+ if (lockErr) throw lockErr;
3322
3610
  }
3323
3611
  const singularType = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
3324
3612
  if (!request.force && (singularType === "object" || singularType === "field")) {
@@ -3346,6 +3634,18 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3346
3634
  if (err?.code === "destructive_change") throw err;
3347
3635
  }
3348
3636
  }
3637
+ {
3638
+ const it = request.item;
3639
+ const looksLikeLayeredEnvelope = it && typeof it === "object" && !Array.isArray(it) && "code" in it && "overlay" in it && "overlayScope" in it && "effective" in it;
3640
+ if (looksLikeLayeredEnvelope) {
3641
+ const err = new Error(
3642
+ `[invalid_metadata] ${request.type}/${request.name}: the request body is a layered read envelope ({ code, overlay, overlayScope, effective }), not a metadata body. Unwrap and send the effective/overlay document instead \u2014 the layered shape is read-only (GET ?layers=true) and must never be persisted.`
3643
+ );
3644
+ err.code = "invalid_metadata";
3645
+ err.status = 422;
3646
+ throw err;
3647
+ }
3648
+ }
3349
3649
  {
3350
3650
  const schema = resolveOverlaySchema(request.type, request.item);
3351
3651
  if (schema) {
@@ -3400,6 +3700,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3400
3700
  if (mode === "publish") {
3401
3701
  this.applyObjectRegistryMutation(request);
3402
3702
  }
3703
+ await this.recordMetadataAudit({
3704
+ type: request.type,
3705
+ name: request.name,
3706
+ organizationId: orgId,
3707
+ operation: "save",
3708
+ outcome: "allowed",
3709
+ code: "ok",
3710
+ ...request.actor ? { actor: request.actor } : {},
3711
+ source: "protocol.saveMetaItem",
3712
+ note: mode === "draft" ? "draft" : "active"
3713
+ });
3403
3714
  return {
3404
3715
  success: true,
3405
3716
  version: result.version,
@@ -3522,6 +3833,15 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3522
3833
  err.status = 403;
3523
3834
  throw err;
3524
3835
  }
3836
+ const _publishLockErr = await this.assertLockAllowsWrite({
3837
+ type: request.type,
3838
+ name: request.name,
3839
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3840
+ operation: "publish",
3841
+ ...request.actor ? { actor: request.actor } : {},
3842
+ source: "protocol.publishMetaItem"
3843
+ });
3844
+ if (_publishLockErr) throw _publishLockErr;
3525
3845
  await this.ensureOverlayIndex();
3526
3846
  const orgId = request.organizationId ?? null;
3527
3847
  const repo = this.getOverlayRepo(orgId);
@@ -3589,6 +3909,15 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3589
3909
  err.status = 403;
3590
3910
  throw err;
3591
3911
  }
3912
+ const _rollbackLockErr = await this.assertLockAllowsWrite({
3913
+ type: request.type,
3914
+ name: request.name,
3915
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3916
+ operation: "rollback",
3917
+ ...request.actor ? { actor: request.actor } : {},
3918
+ source: "protocol.rollbackMetaItem"
3919
+ });
3920
+ if (_rollbackLockErr) throw _rollbackLockErr;
3592
3921
  await this.ensureOverlayIndex();
3593
3922
  const orgId = request.organizationId ?? null;
3594
3923
  const repo = this.getOverlayRepo(orgId);
@@ -3735,6 +4064,14 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3735
4064
  err.status = 403;
3736
4065
  throw err;
3737
4066
  }
4067
+ const lockErr = await this.assertLockAllowsDelete({
4068
+ type: request.type,
4069
+ name: request.name,
4070
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
4071
+ ...request.actor ? { actor: request.actor } : {},
4072
+ source: "protocol.deleteMetaItem"
4073
+ });
4074
+ if (lockErr) throw lockErr;
3738
4075
  }
3739
4076
  const singularTypeForRepo = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
3740
4077
  const overlayAllowedForRepoDel = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
@@ -3779,6 +4116,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3779
4116
  } catch {
3780
4117
  }
3781
4118
  }
4119
+ await this.recordMetadataAudit({
4120
+ type: request.type,
4121
+ name: request.name,
4122
+ organizationId: orgId,
4123
+ operation: "delete",
4124
+ outcome: "allowed",
4125
+ code: "ok",
4126
+ ...request.actor ? { actor: request.actor } : {},
4127
+ source: "protocol.deleteMetaItem",
4128
+ note: targetState
4129
+ });
3782
4130
  return {
3783
4131
  success: true,
3784
4132
  reset: true,
@@ -4095,7 +4443,7 @@ _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
4095
4443
  return out;
4096
4444
  })();
4097
4445
  /**
4098
- * Phase 3a-env-writable: parse `OBJECTSTACK_METADATA_WRITABLE` once.
4446
+ * Phase 3a-env-writable: parse `OS_METADATA_WRITABLE` once.
4099
4447
  * Comma-separated singular type names. When the env var is set, the
4100
4448
  * listed types get treated as `allowOrgOverride: true` regardless of
4101
4449
  * their static registry entry. This is the runtime escape hatch admins