@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.d.mts +109 -3
- package/dist/index.d.ts +109 -3
- package/dist/index.js +399 -56
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +363 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
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(
|
|
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
|
-
|
|
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}`))
|
|
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 =
|
|
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 (`
|
|
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
|
|
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(
|
|
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:
|
|
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 =
|
|
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
|
|
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 `
|
|
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
|