@objectstack/rest 13.0.0 → 14.5.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
@@ -33,7 +33,14 @@ __export(index_exports, {
33
33
  RestServer: () => RestServer,
34
34
  RouteGroupBuilder: () => RouteGroupBuilder,
35
35
  RouteManager: () => RouteManager,
36
- createRestApiPlugin: () => createRestApiPlugin
36
+ buildFieldMetaMap: () => buildFieldMetaMap,
37
+ coerceRow: () => coerceRow,
38
+ createRestApiPlugin: () => createRestApiPlugin,
39
+ isMetaEnvelope: () => isMetaEnvelope,
40
+ parseCsvToRows: () => parseCsvToRows,
41
+ parseXlsxToRows: () => parseXlsxToRows,
42
+ prepareImportRequest: () => prepareImportRequest,
43
+ runImport: () => runImport
37
44
  });
38
45
  module.exports = __toCommonJS(index_exports);
39
46
 
@@ -289,7 +296,14 @@ function buildFieldMetaMap(schema) {
289
296
  label: typeof f.label === "string" ? f.label : void 0,
290
297
  options: Array.isArray(f.options) ? f.options : void 0,
291
298
  reference: typeof f.reference === "string" ? f.reference : void 0,
292
- displayField: typeof f.displayField === "string" ? f.displayField : void 0
299
+ displayField: typeof f.displayField === "string" ? f.displayField : void 0,
300
+ required: f.required === true,
301
+ system: f.system === true,
302
+ readonly: f.readonly === true,
303
+ // Mirror the engine's `applyFieldDefaults` gate (`f.defaultValue == null`
304
+ // ⇒ no default): any non-null default — literal, expression object, or the
305
+ // `current_user` token — counts as satisfying a required field.
306
+ hasDefault: f.defaultValue != null
293
307
  });
294
308
  }
295
309
  return map;
@@ -327,6 +341,22 @@ function optionLabel(value, options) {
327
341
  const hit = options.find((o) => o && o.value === value);
328
342
  return hit?.label ?? value;
329
343
  }
344
+ function toArgb(color) {
345
+ if (typeof color !== "string") return void 0;
346
+ const hex = color.trim().replace(/^#/, "");
347
+ if (/^[0-9a-fA-F]{3}$/.test(hex)) {
348
+ const [r, g, b] = hex;
349
+ return `FF${r}${r}${g}${g}${b}${b}`.toUpperCase();
350
+ }
351
+ if (/^[0-9a-fA-F]{6}$/.test(hex)) return `FF${hex}`.toUpperCase();
352
+ return void 0;
353
+ }
354
+ function cellFontColor(value, meta) {
355
+ if (value === null || value === void 0) return void 0;
356
+ if (!meta || !meta.type || !OPTION_TYPES.has(meta.type) || !meta.options) return void 0;
357
+ const hit = meta.options.find((o) => o && o.value === value);
358
+ return toArgb(hit?.color);
359
+ }
330
360
  function displayFromRecord(rec, displayField) {
331
361
  if (displayField && rec[displayField] != null) return String(rec[displayField]);
332
362
  for (const k of NAME_KEY_FALLBACKS) {
@@ -543,6 +573,27 @@ async function coerceFieldValue(raw, meta, ctx) {
543
573
  }
544
574
  return { value: trim && typeof raw === "string" ? raw.trim() : raw };
545
575
  }
576
+ var REQUIRED_CHECK_SKIP = /* @__PURE__ */ new Set([
577
+ "id",
578
+ "created_at",
579
+ "created_by",
580
+ "updated_at",
581
+ "updated_by"
582
+ ]);
583
+ function isBlankValue(v) {
584
+ return v === void 0 || v === null || typeof v === "string" && v.trim() === "";
585
+ }
586
+ function firstMissingRequiredField(data, metaMap) {
587
+ for (const meta of metaMap.values()) {
588
+ if (!meta.required) continue;
589
+ if (meta.system || meta.readonly) continue;
590
+ if (meta.hasDefault) continue;
591
+ if (meta.type === "autonumber") continue;
592
+ if (REQUIRED_CHECK_SKIP.has(meta.name)) continue;
593
+ if (isBlankValue(data[meta.name])) return meta.name;
594
+ }
595
+ return null;
596
+ }
546
597
  async function coerceRow(rawRow, metaMap, ctx) {
547
598
  const data = {};
548
599
  const errors = [];
@@ -741,7 +792,11 @@ function runImport(opts) {
741
792
  if (!handled) {
742
793
  const willUpdate = existing && typeof existing === "object";
743
794
  const willCreate = !willUpdate && (writeMode === "insert" || writeMode === "upsert");
744
- if (!willUpdate && !willCreate) {
795
+ const requiredMiss = willCreate && !runAutomations ? firstMissingRequiredField(data, metaMap) : null;
796
+ if (requiredMiss) {
797
+ errCount++;
798
+ results[i] = { row: rowNo, ok: false, action: "failed", field: requiredMiss, code: "required", error: `${requiredMiss} is required` };
799
+ } else if (!willUpdate && !willCreate) {
745
800
  skipped++;
746
801
  results[i] = { row: rowNo, ok: true, action: "skipped", code: "NO_MATCH" };
747
802
  } else if (dryRun) {
@@ -913,155 +968,10 @@ function applyMappingToRows(rows, artifact) {
913
968
  return { ok: true, rows: out };
914
969
  }
915
970
 
916
- // src/rest-server.ts
917
- var import_meta = {};
918
- var logError = (...args) => globalThis.console?.error(...args);
919
- var TRANSLATABLE_META_TYPES = /* @__PURE__ */ new Set(["view", "action", "object", "app", "dashboard"]);
971
+ // src/import-prepare.ts
920
972
  function isMetaEnvelope(value) {
921
973
  return !!value && typeof value === "object" && typeof value.type === "string" && typeof value.name === "string" && value.item != null && typeof value.item === "object" && !Array.isArray(value.item);
922
974
  }
923
- function mapDataError(error, object) {
924
- if (error?.code === "DELETE_RESTRICTED") {
925
- return {
926
- status: 409,
927
- body: {
928
- error: error?.message ?? "Cannot delete: dependent records exist",
929
- code: "DELETE_RESTRICTED",
930
- ...error?.dependentObject ? { dependentObject: error.dependentObject } : {},
931
- ...typeof error?.dependentCount === "number" ? { dependentCount: error.dependentCount } : {},
932
- ...object ? { object } : {}
933
- }
934
- };
935
- }
936
- if (error?.code === "CONCURRENT_UPDATE" || error?.name === "ConcurrentUpdateError") {
937
- return {
938
- status: 409,
939
- body: {
940
- error: error?.message ?? "Record was modified by another user",
941
- code: "CONCURRENT_UPDATE",
942
- ...error?.currentVersion ? { currentVersion: error.currentVersion } : {},
943
- ...error?.currentRecord ? { currentRecord: error.currentRecord } : {},
944
- ...object ? { object } : {}
945
- }
946
- };
947
- }
948
- if (error?.code === "VALIDATION_FAILED" || error?.name === "ValidationError") {
949
- return {
950
- status: 400,
951
- body: {
952
- error: error?.message ?? "Validation failed",
953
- code: "VALIDATION_FAILED",
954
- fields: Array.isArray(error?.fields) ? error.fields : [],
955
- ...object ? { object } : {}
956
- }
957
- };
958
- }
959
- if (error?.code === "PERMISSION_DENIED" || error?.name === "PermissionDeniedError" || typeof error?.message === "string" && error.message.startsWith("[Security] Access denied")) {
960
- return {
961
- status: 403,
962
- body: {
963
- error: error?.message ?? "Permission denied",
964
- code: "PERMISSION_DENIED",
965
- ...object ? { object } : {}
966
- }
967
- };
968
- }
969
- const raw = String(error?.message ?? error ?? "");
970
- const lower = raw.toLowerCase();
971
- if (raw.includes("[EnvironmentKernelFactory]") && (lower.includes("missing database_url") || lower.includes("not found"))) {
972
- const isProvisioning = lower.includes("status='provisioning'") || lower.includes("status='pending'");
973
- const isFailed = lower.includes("status='failed'");
974
- return {
975
- status: isProvisioning ? 503 : isFailed ? 502 : 404,
976
- body: {
977
- error: raw,
978
- code: isProvisioning ? "PROJECT_PROVISIONING" : isFailed ? "PROJECT_PROVISIONING_FAILED" : "PROJECT_NOT_FOUND"
979
- }
980
- };
981
- }
982
- if (error?.code === "RECORD_NOT_FOUND" || /^Record\s+\S+\s+not found in\s+\S+/i.test(raw)) {
983
- return {
984
- status: 404,
985
- body: {
986
- error: raw,
987
- code: "RECORD_NOT_FOUND",
988
- ...object ? { object } : {}
989
- }
990
- };
991
- }
992
- const unknownColumn = /has no column named\s+["'`]?([a-z0-9_]+)/i.exec(raw) || /no such column:\s*["'`]?([a-z0-9_.]+)/i.exec(raw) || /unknown column\s+["'`]([a-z0-9_]+)["'`]/i.exec(raw) || /column\s+["'`]([a-z0-9_]+)["'`]\s+of relation\s+\S+\s+does not exist/i.exec(raw);
993
- if (unknownColumn) {
994
- const field = unknownColumn[1]?.split(".").pop();
995
- return {
996
- status: 400,
997
- body: {
998
- error: field ? `Unknown field '${field}'${object ? ` on object '${object}'` : ""}` : "Request references a field that does not exist",
999
- code: "INVALID_FIELD",
1000
- ...field ? { field } : {},
1001
- ...object ? { object } : {}
1002
- }
1003
- };
1004
- }
1005
- const notNull = /not null constraint failed:\s*\S*?\.([a-z0-9_]+)/i.exec(raw) || /null value in column\s+["'`]([a-z0-9_]+)["'`]/i.exec(raw) || /column\s+["'`]([a-z0-9_]+)["'`]\s+cannot be null/i.exec(raw);
1006
- if (notNull) {
1007
- const field = notNull[1];
1008
- return {
1009
- status: 400,
1010
- body: {
1011
- error: `${field} is required`,
1012
- code: "VALIDATION_FAILED",
1013
- fields: [{ field, code: "required", message: `${field} is required` }],
1014
- hint: `If '${field}' is optional in your object metadata, the database column is still NOT NULL \u2014 the physical schema has drifted from metadata. Run 'os migrate' to reconcile (or reset the dev database).`,
1015
- ...object ? { object } : {}
1016
- }
1017
- };
1018
- }
1019
- const looksLikeUnknownObject = lower.includes("no such table") || lower.includes("relation") && lower.includes("does not exist") || lower.includes("table not found") || lower.includes("unknown object") || lower.includes("object not found") || lower.includes("no driver available") || object !== void 0 && lower.includes(`'${object.toLowerCase()}'`) && lower.includes("not");
1020
- if (looksLikeUnknownObject) {
1021
- return {
1022
- status: 404,
1023
- body: {
1024
- error: object ? `Object '${object}' is not registered` : "Object not found",
1025
- code: "object_not_found",
1026
- object
1027
- }
1028
- };
1029
- }
1030
- const looksLikeSqlLeak = lower.includes("sqlite_") || lower.includes("sqlstate") || lower.startsWith("insert into ") || lower.startsWith("update ") || lower.startsWith("select ") || lower.startsWith("delete from ") || lower.includes("constraint failed") || lower.includes("unique constraint") || lower.includes("foreign key");
1031
- if (looksLikeSqlLeak) {
1032
- if (lower.includes("unique constraint") || lower.includes("unique violation")) {
1033
- return {
1034
- status: 409,
1035
- body: {
1036
- error: "A record with this value already exists",
1037
- code: "UNIQUE_VIOLATION",
1038
- ...object ? { object } : {}
1039
- }
1040
- };
1041
- }
1042
- return {
1043
- status: 500,
1044
- body: { error: "Internal data error", code: "DATABASE_ERROR" }
1045
- };
1046
- }
1047
- return { status: 400, body: { error: raw || "Bad request" } };
1048
- }
1049
- function sendError(res, error, object) {
1050
- if (typeof error?.status === "number" && error.status >= 400 && error.status < 600) {
1051
- const safeMsg = typeof error.message === "string" && error.message.length < 500 ? error.message : "Request failed";
1052
- res.status(error.status).json({
1053
- error: safeMsg,
1054
- ...error.code ? { code: error.code } : {},
1055
- ...Array.isArray(error.issues) ? { issues: error.issues } : {}
1056
- });
1057
- return;
1058
- }
1059
- const mapped = mapDataError(error, object);
1060
- res.status(mapped.status).json(mapped.body);
1061
- }
1062
- function isExpectedDataStatus(status) {
1063
- return status === 403 || status === 404 || status === 409 || status === 502 || status === 503;
1064
- }
1065
975
  function parseCsvToRows(csv, mapping = {}) {
1066
976
  const text = csv.replace(/^\uFEFF/, "");
1067
977
  const cells = [];
@@ -1280,6 +1190,163 @@ async function prepareImportRequest(body, opts) {
1280
1190
  }
1281
1191
  };
1282
1192
  }
1193
+
1194
+ // src/rest-server.ts
1195
+ var import_meta = {};
1196
+ var logError = (...args) => globalThis.console?.error(...args);
1197
+ var TRANSLATABLE_META_TYPES = /* @__PURE__ */ new Set(["view", "action", "object", "app", "dashboard"]);
1198
+ function mapDataError(error, object) {
1199
+ if (error?.code === "DELETE_RESTRICTED") {
1200
+ return {
1201
+ status: 409,
1202
+ body: {
1203
+ error: error?.message ?? "Cannot delete: dependent records exist",
1204
+ code: "DELETE_RESTRICTED",
1205
+ ...error?.dependentObject ? { dependentObject: error.dependentObject } : {},
1206
+ ...typeof error?.dependentCount === "number" ? { dependentCount: error.dependentCount } : {},
1207
+ ...object ? { object } : {}
1208
+ }
1209
+ };
1210
+ }
1211
+ if (error?.code === "CONCURRENT_UPDATE" || error?.name === "ConcurrentUpdateError") {
1212
+ return {
1213
+ status: 409,
1214
+ body: {
1215
+ error: error?.message ?? "Record was modified by another user",
1216
+ code: "CONCURRENT_UPDATE",
1217
+ ...error?.currentVersion ? { currentVersion: error.currentVersion } : {},
1218
+ ...error?.currentRecord ? { currentRecord: error.currentRecord } : {},
1219
+ ...object ? { object } : {}
1220
+ }
1221
+ };
1222
+ }
1223
+ if (error?.code === "VALIDATION_FAILED" || error?.name === "ValidationError") {
1224
+ return {
1225
+ status: 400,
1226
+ body: {
1227
+ error: error?.message ?? "Validation failed",
1228
+ code: "VALIDATION_FAILED",
1229
+ fields: Array.isArray(error?.fields) ? error.fields : [],
1230
+ ...object ? { object } : {}
1231
+ }
1232
+ };
1233
+ }
1234
+ if (error?.code === "FEEDS_DISABLED" || error?.code === "FILES_DISABLED") {
1235
+ return {
1236
+ status: 403,
1237
+ body: {
1238
+ error: error?.message ?? "This capability is disabled for the target object",
1239
+ code: error.code,
1240
+ ...error?.object || object ? { object: error?.object ?? object } : {}
1241
+ }
1242
+ };
1243
+ }
1244
+ if (error?.code === "PERMISSION_DENIED" || error?.name === "PermissionDeniedError" || typeof error?.message === "string" && error.message.startsWith("[Security] Access denied")) {
1245
+ return {
1246
+ status: 403,
1247
+ body: {
1248
+ error: error?.message ?? "Permission denied",
1249
+ code: "PERMISSION_DENIED",
1250
+ ...object ? { object } : {}
1251
+ }
1252
+ };
1253
+ }
1254
+ const raw = String(error?.message ?? error ?? "");
1255
+ const lower = raw.toLowerCase();
1256
+ if (raw.includes("[EnvironmentKernelFactory]") && (lower.includes("missing database_url") || lower.includes("not found"))) {
1257
+ const isProvisioning = lower.includes("status='provisioning'") || lower.includes("status='pending'");
1258
+ const isFailed = lower.includes("status='failed'");
1259
+ return {
1260
+ status: isProvisioning ? 503 : isFailed ? 502 : 404,
1261
+ body: {
1262
+ error: raw,
1263
+ code: isProvisioning ? "PROJECT_PROVISIONING" : isFailed ? "PROJECT_PROVISIONING_FAILED" : "PROJECT_NOT_FOUND"
1264
+ }
1265
+ };
1266
+ }
1267
+ if (error?.code === "RECORD_NOT_FOUND" || /^Record\s+\S+\s+not found in\s+\S+/i.test(raw)) {
1268
+ return {
1269
+ status: 404,
1270
+ body: {
1271
+ error: raw,
1272
+ code: "RECORD_NOT_FOUND",
1273
+ ...object ? { object } : {}
1274
+ }
1275
+ };
1276
+ }
1277
+ const unknownColumn = /has no column named\s+["'`]?([a-z0-9_]+)/i.exec(raw) || /no such column:\s*["'`]?([a-z0-9_.]+)/i.exec(raw) || /unknown column\s+["'`]([a-z0-9_]+)["'`]/i.exec(raw) || /column\s+["'`]([a-z0-9_]+)["'`]\s+of relation\s+\S+\s+does not exist/i.exec(raw);
1278
+ if (unknownColumn) {
1279
+ const field = unknownColumn[1]?.split(".").pop();
1280
+ return {
1281
+ status: 400,
1282
+ body: {
1283
+ error: field ? `Unknown field '${field}'${object ? ` on object '${object}'` : ""}` : "Request references a field that does not exist",
1284
+ code: "INVALID_FIELD",
1285
+ ...field ? { field } : {},
1286
+ ...object ? { object } : {}
1287
+ }
1288
+ };
1289
+ }
1290
+ const notNull = /not null constraint failed:\s*\S*?\.([a-z0-9_]+)/i.exec(raw) || /null value in column\s+["'`]([a-z0-9_]+)["'`]/i.exec(raw) || /column\s+["'`]([a-z0-9_]+)["'`]\s+cannot be null/i.exec(raw);
1291
+ if (notNull) {
1292
+ const field = notNull[1];
1293
+ return {
1294
+ status: 400,
1295
+ body: {
1296
+ error: `${field} is required`,
1297
+ code: "VALIDATION_FAILED",
1298
+ fields: [{ field, code: "required", message: `${field} is required` }],
1299
+ hint: `If '${field}' is optional in your object metadata, the database column is still NOT NULL \u2014 the physical schema has drifted from metadata. Run 'os migrate' to reconcile (or reset the dev database).`,
1300
+ ...object ? { object } : {}
1301
+ }
1302
+ };
1303
+ }
1304
+ const looksLikeUnknownObject = lower.includes("no such table") || lower.includes("relation") && lower.includes("does not exist") || lower.includes("table not found") || lower.includes("unknown object") || lower.includes("object not found") || lower.includes("no driver available") || object !== void 0 && lower.includes(`'${object.toLowerCase()}'`) && lower.includes("not");
1305
+ if (looksLikeUnknownObject) {
1306
+ return {
1307
+ status: 404,
1308
+ body: {
1309
+ error: object ? `Object '${object}' is not registered` : "Object not found",
1310
+ code: "object_not_found",
1311
+ object
1312
+ }
1313
+ };
1314
+ }
1315
+ const looksLikeSqlLeak = lower.includes("sqlite_") || lower.includes("sqlstate") || lower.startsWith("insert into ") || lower.startsWith("update ") || lower.startsWith("select ") || lower.startsWith("delete from ") || lower.includes("constraint failed") || lower.includes("unique constraint") || lower.includes("foreign key");
1316
+ if (looksLikeSqlLeak) {
1317
+ if (lower.includes("unique constraint") || lower.includes("unique violation")) {
1318
+ return {
1319
+ status: 409,
1320
+ body: {
1321
+ error: "A record with this value already exists",
1322
+ code: "UNIQUE_VIOLATION",
1323
+ ...object ? { object } : {}
1324
+ }
1325
+ };
1326
+ }
1327
+ return {
1328
+ status: 500,
1329
+ body: { error: "Internal data error", code: "DATABASE_ERROR" }
1330
+ };
1331
+ }
1332
+ return { status: 400, body: { error: raw || "Bad request" } };
1333
+ }
1334
+ function sendError(res, error, object) {
1335
+ if (typeof error?.status === "number" && error.status >= 400 && error.status < 600) {
1336
+ const safeMsg = typeof error.message === "string" && error.message.length < 500 ? error.message : "Request failed";
1337
+ res.status(error.status).json({
1338
+ error: safeMsg,
1339
+ ...error.code ? { code: error.code } : {},
1340
+ ...Array.isArray(error.issues) ? { issues: error.issues } : {}
1341
+ });
1342
+ return;
1343
+ }
1344
+ const mapped = mapDataError(error, object);
1345
+ res.status(mapped.status).json(mapped.body);
1346
+ }
1347
+ function isExpectedDataStatus(status) {
1348
+ return status === 403 || status === 404 || status === 409 || status === 502 || status === 503;
1349
+ }
1283
1350
  var IMPORT_JOB_OBJECT = "sys_import_job";
1284
1351
  var IMPORT_JOB_MAX_ROWS = 5e4;
1285
1352
  var IMPORT_JOB_RESULTS_CAP = 500;
@@ -1384,7 +1451,7 @@ function rowsToCsv(fields, rows, includeHeader, metaMap) {
1384
1451
  }
1385
1452
  return lines.join("\r\n") + (lines.length > 0 ? "\r\n" : "");
1386
1453
  }
1387
- async function createXlsxStream(res) {
1454
+ async function createXlsxStream(res, useStyles = false) {
1388
1455
  const { PassThrough } = await import("stream");
1389
1456
  const ExcelJS = (await import("exceljs")).default ?? await import("exceljs");
1390
1457
  const passthrough = new PassThrough();
@@ -1401,7 +1468,7 @@ async function createXlsxStream(res) {
1401
1468
  });
1402
1469
  passthrough.on("error", reject);
1403
1470
  });
1404
- const wb = new ExcelJS.stream.xlsx.WorkbookWriter({ stream: passthrough, useStyles: false });
1471
+ const wb = new ExcelJS.stream.xlsx.WorkbookWriter({ stream: passthrough, useStyles });
1405
1472
  const ws = wb.addWorksheet("Export");
1406
1473
  return {
1407
1474
  ws,
@@ -1412,8 +1479,8 @@ async function createXlsxStream(res) {
1412
1479
  }
1413
1480
  };
1414
1481
  }
1415
- var RestServer = class {
1416
- constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider) {
1482
+ var RestServer = class _RestServer {
1483
+ constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider, securityServiceProvider) {
1417
1484
  /**
1418
1485
  * Short-TTL cache for `hostname → environmentId` (P1-4). `resolveByHostname`
1419
1486
  * is a control-plane lookup (typically a DB query) that otherwise runs on
@@ -1466,6 +1533,7 @@ var RestServer = class {
1466
1533
  this.analyticsServiceProvider = analyticsServiceProvider;
1467
1534
  this.settingsServiceProvider = settingsServiceProvider;
1468
1535
  this.serviceExistsProvider = serviceExistsProvider;
1536
+ this.securityServiceProvider = securityServiceProvider;
1469
1537
  }
1470
1538
  /**
1471
1539
  * Resolve the protocol for a given request. When `environmentId` is present
@@ -1704,6 +1772,54 @@ var RestServer = class {
1704
1772
  perReq.set(key, pending);
1705
1773
  return pending;
1706
1774
  }
1775
+ /**
1776
+ * [ADR-0046 §6.7] The audience-evaluation view of the caller for book/doc
1777
+ * gating. `permissionSets` resolves through the security service's
1778
+ * `resolvePermissionSetNames` — the SAME resolution as data-plane
1779
+ * enforcement (positions expanded, additive baseline), so the docs gate
1780
+ * can never drift from it. `permissionSets` stays undefined when the
1781
+ * service is absent or resolution fails; `audienceAllows` then DENIES
1782
+ * permission-set-gated audiences (fail closed, ADR-0049). Resolution is
1783
+ * skipped unless `needPermissionSets` — callers pass true only when a
1784
+ * `{ permissionSet }` audience is actually in play.
1785
+ */
1786
+ async resolveAudienceCaller(environmentId, req, opts) {
1787
+ const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
1788
+ const authenticated = !!ctx?.userId;
1789
+ if (!authenticated || !opts.needPermissionSets || !this.securityServiceProvider) {
1790
+ return { authenticated };
1791
+ }
1792
+ try {
1793
+ const svc = await this.securityServiceProvider(environmentId);
1794
+ if (!svc || typeof svc.resolvePermissionSetNames !== "function") return { authenticated };
1795
+ const names = await svc.resolvePermissionSetNames(ctx);
1796
+ return { authenticated, permissionSets: Array.isArray(names) ? names : [] };
1797
+ } catch {
1798
+ return { authenticated };
1799
+ }
1800
+ }
1801
+ /** Whether any of these books carries a `{ permissionSet }` audience. */
1802
+ static anyPermissionSetAudience(books) {
1803
+ return books.some(
1804
+ (b) => b && typeof b === "object" && b.audience && typeof b.audience === "object" && typeof b.audience.permissionSet === "string"
1805
+ );
1806
+ }
1807
+ /** Coerce a getMetaItems result (array | {items}) into an array. */
1808
+ static metaItemsArray(raw) {
1809
+ if (Array.isArray(raw)) return raw;
1810
+ if (raw && typeof raw === "object" && Array.isArray(raw.items)) return raw.items;
1811
+ return [];
1812
+ }
1813
+ /** Fetch every book of the environment, shaped for the audience resolver. */
1814
+ async fetchAudienceBooks(p, environmentId) {
1815
+ const raw = await p.getMetaItems({
1816
+ type: "book",
1817
+ ...environmentId ? { environmentId } : {}
1818
+ }).catch(() => []);
1819
+ return _RestServer.metaItemsArray(raw).map(
1820
+ (b) => b && typeof b === "object" ? { ...b, packageId: b._packageId } : b
1821
+ );
1822
+ }
1707
1823
  /** Heavy path behind `resolveExecCtx` — resolve identity + RBAC/RLS + localization. */
1708
1824
  async computeExecCtx(environmentId, req) {
1709
1825
  try {
@@ -2212,6 +2328,8 @@ var RestServer = class {
2212
2328
  this.registerReportsEndpoints(bp);
2213
2329
  this.registerApprovalsEndpoints(bp);
2214
2330
  this.registerAnalyticsEndpoints(bp);
2331
+ this.registerSecurityEndpoints(bp);
2332
+ this.registerSecurityExplainEndpoints(bp);
2215
2333
  this.registerDataActionEndpoints(bp);
2216
2334
  if (this.config.api.enableCrud) {
2217
2335
  this.registerCrudEndpoints(bp);
@@ -2424,9 +2542,49 @@ var RestServer = class {
2424
2542
  }
2425
2543
  }
2426
2544
  /**
2427
- * Register metadata endpoints
2545
+ * Register the metadata routes behind the SAME `requireAuth` gate the
2546
+ * `/data` routes use.
2547
+ *
2548
+ * `registerMetadataEndpoints` builds ~17 `/meta/*` routes but — unlike the
2549
+ * `/data` handlers — never calls {@link enforceAuth}: its handlers assumed
2550
+ * the `requireAuth` gate rejected anonymous callers "upstream", yet nothing
2551
+ * upstream covers `/meta`, so on a `requireAuth` deployment an anonymous
2552
+ * caller could read object / field schemas. On a tenant-less runtime host
2553
+ * those are SYSTEM-object schemas and the host is publicly reachable — a
2554
+ * real leak.
2555
+ *
2556
+ * Rather than add the gate to every handler (and have the next new route
2557
+ * forget it — the exact failure mode that caused this), wrap the route
2558
+ * registrar for the duration of registration so every meta route, present
2559
+ * and future, inherits it. The check is a no-op when `requireAuth` is off
2560
+ * (demo / single-tenant), so the previously-public metadata surface there
2561
+ * is unchanged; an authenticated user passes exactly as on `/data`.
2428
2562
  */
2429
2563
  registerMetadataEndpoints(basePath) {
2564
+ const realRouteManager = this.routeManager;
2565
+ const guardedRouteManager = {
2566
+ register: (entry) => {
2567
+ const inner = entry.handler;
2568
+ if (typeof inner !== "function") return realRouteManager.register(entry);
2569
+ return realRouteManager.register({
2570
+ ...entry,
2571
+ handler: async (req, res) => {
2572
+ const environmentId = req?.params?.environmentId;
2573
+ const context = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
2574
+ if (this.enforceAuth(req, res, context)) return;
2575
+ return inner(req, res);
2576
+ }
2577
+ });
2578
+ }
2579
+ };
2580
+ this.routeManager = guardedRouteManager;
2581
+ try {
2582
+ this.registerMetadataEndpointsInner(basePath);
2583
+ } finally {
2584
+ this.routeManager = realRouteManager;
2585
+ }
2586
+ }
2587
+ registerMetadataEndpointsInner(basePath) {
2430
2588
  const { metadata } = this.config;
2431
2589
  const metaPath = `${basePath}${metadata.prefix}`;
2432
2590
  const isScoped = basePath.includes("/environments/:environmentId");
@@ -2573,6 +2731,48 @@ var RestServer = class {
2573
2731
  visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
2574
2732
  }
2575
2733
  }
2734
+ if (req.params.type === "book") {
2735
+ const raw = visible;
2736
+ const list = _RestServer.metaItemsArray(raw);
2737
+ if (list.length > 0) {
2738
+ const { audienceAllows } = await import("@objectstack/spec/system");
2739
+ const caller = await this.resolveAudienceCaller(environmentId, req, {
2740
+ needPermissionSets: _RestServer.anyPermissionSetAudience(list)
2741
+ });
2742
+ const filtered = list.filter((b) => b && typeof b === "object" && audienceAllows(b.audience, caller));
2743
+ visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
2744
+ }
2745
+ }
2746
+ if (req.params.type === "doc") {
2747
+ const raw = visible;
2748
+ const list = _RestServer.metaItemsArray(raw);
2749
+ if (list.length > 0) {
2750
+ const { audienceAllows, docAudienceAllows, resolveDocAudiences } = await import("@objectstack/spec/system");
2751
+ const books = await this.fetchAudienceBooks(p, environmentId);
2752
+ const caller = await this.resolveAudienceCaller(environmentId, req, {
2753
+ needPermissionSets: _RestServer.anyPermissionSetAudience(books)
2754
+ });
2755
+ let filtered;
2756
+ if (caller.authenticated && !_RestServer.anyPermissionSetAudience(books)) {
2757
+ filtered = list;
2758
+ } else {
2759
+ const corpus = list.filter((d) => d && typeof d === "object").map((d) => ({
2760
+ name: d.name,
2761
+ group: d.group,
2762
+ tags: d.tags,
2763
+ order: d.order,
2764
+ packageId: d._packageId
2765
+ }));
2766
+ const audiences = resolveDocAudiences(books, corpus);
2767
+ filtered = list.filter((d) => {
2768
+ if (!d || typeof d !== "object") return false;
2769
+ const eff = audiences.get(d.name);
2770
+ return eff ? docAudienceAllows(eff, caller) : audienceAllows("org", caller);
2771
+ });
2772
+ }
2773
+ visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
2774
+ }
2775
+ }
2576
2776
  if (req.params.type === "doc") {
2577
2777
  const locale = this.extractLocale(req);
2578
2778
  const { resolveDocLocale } = await import("@objectstack/spec/system");
@@ -2646,7 +2846,7 @@ var RestServer = class {
2646
2846
  const prot = await this.resolveProtocol(environmentId, req);
2647
2847
  const locale = this.extractLocale(req);
2648
2848
  const packageId = req.query?.package || void 0;
2649
- const { resolveBookTree, deriveImplicitPackageBook, isPublicAudience, resolveDocLocale } = await import("@objectstack/spec/system");
2849
+ const { resolveBookTree, deriveImplicitPackageBook, audienceAllows, resolveDocAudiences, docAudienceAllows, resolveDocLocale } = await import("@objectstack/spec/system");
2650
2850
  const norm = (raw) => Array.isArray(raw) ? raw : raw && Array.isArray(raw.items) ? raw.items : [];
2651
2851
  const books = norm(await prot.getMetaItems({
2652
2852
  type: "book",
@@ -2657,9 +2857,16 @@ var RestServer = class {
2657
2857
  if (!book) {
2658
2858
  book = deriveImplicitPackageBook(req.params.name, req.params.name);
2659
2859
  }
2660
- const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
2661
- if (!ctx?.userId && !isPublicAudience(book.audience)) {
2662
- sendError(res, { code: "unauthorized", message: "This documentation requires sign-in", status: 401 });
2860
+ const audienceBooks = books.map((b) => b && typeof b === "object" ? { ...b, packageId: b._packageId } : b);
2861
+ const caller = await this.resolveAudienceCaller(environmentId, req, {
2862
+ needPermissionSets: _RestServer.anyPermissionSetAudience([book, ...audienceBooks])
2863
+ });
2864
+ if (!audienceAllows(book.audience, caller)) {
2865
+ if (!caller.authenticated) {
2866
+ sendError(res, { code: "unauthorized", message: "This documentation requires sign-in", status: 401 });
2867
+ } else {
2868
+ sendError(res, { code: "PERMISSION_DENIED", message: "This documentation is limited to holders of a permission set you do not have", status: 403 });
2869
+ }
2663
2870
  return;
2664
2871
  }
2665
2872
  const docs = norm(await prot.getMetaItems({
@@ -2676,6 +2883,14 @@ var RestServer = class {
2676
2883
  packageId: d._packageId
2677
2884
  }));
2678
2885
  const tree = resolveBookTree(book, docs, book._packageId);
2886
+ const gatedTreePossible = !caller.authenticated || _RestServer.anyPermissionSetAudience(audienceBooks);
2887
+ if (gatedTreePossible) {
2888
+ const audiences = resolveDocAudiences(audienceBooks, docs);
2889
+ tree.groups = tree.groups.map((g) => ({
2890
+ ...g,
2891
+ entries: g.entries.filter((e) => !e.doc || docAudienceAllows(audiences.get(e.doc), caller))
2892
+ })).filter((g) => g.entries.some((e) => e.doc || e.href));
2893
+ }
2679
2894
  res.json(tree);
2680
2895
  } catch (error) {
2681
2896
  logError("[REST] Unhandled error:", error);
@@ -2710,7 +2925,7 @@ var RestServer = class {
2710
2925
  const isDraftRead = typeof req.query?.state === "string" && req.query.state.toLowerCase() === "draft";
2711
2926
  const previewDrafts = typeof req.query?.preview === "string" && req.query.preview.toLowerCase() === "draft";
2712
2927
  const packageScoped = typeof req.query?.package === "string" && req.query.package.length > 0;
2713
- if (metadata.enableCache && p.getMetaItemCached && !isAppType && !isDraftRead && !previewDrafts && !packageScoped && req.params.type !== "doc") {
2928
+ if (metadata.enableCache && p.getMetaItemCached && !isAppType && !isDraftRead && !previewDrafts && !packageScoped && req.params.type !== "doc" && req.params.type !== "book") {
2714
2929
  const cacheRequest = {
2715
2930
  ifNoneMatch: req.headers["if-none-match"],
2716
2931
  ifModifiedSince: req.headers["if-modified-since"]
@@ -2779,6 +2994,47 @@ var RestServer = class {
2779
2994
  const serviceGate = registered ? (n) => registered.has(n) : void 0;
2780
2995
  if (serviceGate) visible = this.filterDashboardForUser(visible, serviceGate);
2781
2996
  }
2997
+ if ((req.params.type === "book" || req.params.type === "doc") && visible) {
2998
+ const { audienceAllows, docAudienceAllows, resolveDocAudiences } = await import("@objectstack/spec/system");
2999
+ const target = isMetaEnvelope(visible) ? visible.item : visible;
3000
+ let caller;
3001
+ let allowed;
3002
+ if (req.params.type === "book") {
3003
+ caller = await this.resolveAudienceCaller(environmentId, req, {
3004
+ needPermissionSets: _RestServer.anyPermissionSetAudience([target])
3005
+ });
3006
+ allowed = audienceAllows(target?.audience, caller);
3007
+ } else {
3008
+ const books = await this.fetchAudienceBooks(p, environmentId);
3009
+ caller = await this.resolveAudienceCaller(environmentId, req, {
3010
+ needPermissionSets: _RestServer.anyPermissionSetAudience(books)
3011
+ });
3012
+ if (caller.authenticated && !_RestServer.anyPermissionSetAudience(books)) {
3013
+ allowed = true;
3014
+ } else {
3015
+ const corpus = _RestServer.metaItemsArray(await p.getMetaItems({
3016
+ type: "doc",
3017
+ ...environmentId ? { environmentId } : {}
3018
+ }).catch(() => [])).filter((d) => d && typeof d === "object").map((d) => ({
3019
+ name: d.name,
3020
+ group: d.group,
3021
+ tags: d.tags,
3022
+ order: d.order,
3023
+ packageId: d._packageId
3024
+ }));
3025
+ const audiences = resolveDocAudiences(books, corpus);
3026
+ allowed = docAudienceAllows(audiences.get(target?.name), caller);
3027
+ }
3028
+ }
3029
+ if (!allowed) {
3030
+ if (!caller.authenticated) {
3031
+ sendError(res, { code: "unauthorized", message: "This documentation requires sign-in", status: 401 });
3032
+ } else {
3033
+ sendError(res, { code: "PERMISSION_DENIED", message: "This documentation is limited to holders of a permission set you do not have", status: 403 });
3034
+ }
3035
+ return;
3036
+ }
3037
+ }
2782
3038
  if (req.params.type === "doc" && visible) {
2783
3039
  const locale = this.extractLocale(req);
2784
3040
  const { resolveDocLocale } = await import("@objectstack/spec/system");
@@ -3779,9 +4035,11 @@ var RestServer = class {
3779
4035
  const includeHeader = String(q.header ?? "true").toLowerCase() !== "false";
3780
4036
  const HARD_CAP = 5e4;
3781
4037
  const MAX_CHUNK = 5e3;
4038
+ const STYLE_ROW_CAP = 1e4;
3782
4039
  const requestedLimit = q.limit != null ? Math.max(1, Number(q.limit) || 0) : 1e4;
3783
4040
  const limit = Math.min(requestedLimit, HARD_CAP);
3784
4041
  const chunkSize = Math.min(MAX_CHUNK, Math.max(50, q.page != null ? Number(q.page) || 500 : 500));
4042
+ const styled = format === "xlsx" && limit <= STYLE_ROW_CAP;
3785
4043
  let filter = void 0;
3786
4044
  if (typeof q.filter === "string" && q.filter.length > 0) {
3787
4045
  try {
@@ -3848,12 +4106,13 @@ var RestServer = class {
3848
4106
  }
3849
4107
  res.header("X-Export-Format", format);
3850
4108
  res.header("X-Export-Limit", String(limit));
4109
+ if (format === "xlsx") res.header("X-Export-Styles", styled ? "applied" : "dropped");
3851
4110
  res.header("Cache-Control", "no-store");
3852
4111
  let exported = 0;
3853
4112
  let firstChunk = true;
3854
4113
  let skip = 0;
3855
4114
  if (format === "json") res.write("[");
3856
- const xlsx = format === "xlsx" ? await createXlsxStream(res) : null;
4115
+ const xlsx = format === "xlsx" ? await createXlsxStream(res, styled) : null;
3857
4116
  while (exported < limit) {
3858
4117
  const take = Math.min(chunkSize, limit - exported);
3859
4118
  const findArgs = {
@@ -3881,8 +4140,16 @@ var RestServer = class {
3881
4140
  if (firstChunk && includeHeader) {
3882
4141
  xlsx.ws.addRow((fields ?? []).map((f) => headerLabel(f, metaMap))).commit();
3883
4142
  }
4143
+ const cols = fields ?? [];
3884
4144
  for (const row of rows) {
3885
- xlsx.ws.addRow(formatRowCells(row, fields ?? [], metaMap)).commit();
4145
+ const r = xlsx.ws.addRow(formatRowCells(row, cols, metaMap));
4146
+ if (styled) {
4147
+ cols.forEach((f, i) => {
4148
+ const argb = cellFontColor(row?.[f], metaMap.get(f));
4149
+ if (argb) r.getCell(i + 1).font = { color: { argb } };
4150
+ });
4151
+ }
4152
+ r.commit();
3886
4153
  }
3887
4154
  } else {
3888
4155
  for (let i = 0; i < rows.length; i++) {
@@ -4511,6 +4778,100 @@ var RestServer = class {
4511
4778
  metadata: { summary: "Run a semantic-layer dataset (preview/query)", tags: ["analytics"] }
4512
4779
  });
4513
4780
  }
4781
+ /**
4782
+ * [ADR-0090 D6] Access-explanation endpoint — the REST face of the
4783
+ * explain engine (framework#2696).
4784
+ *
4785
+ * GET {basePath}/security/explain?object=…&operation=…&userId=…
4786
+ * POST {basePath}/security/explain body: { object, operation, userId? }
4787
+ *
4788
+ * Delegates to the security service's `explain(request, callerContext)`
4789
+ * (`SecurityPlugin.explainAccessForCaller`) — the same code paths the
4790
+ * enforcement middleware runs, so the report is explained by
4791
+ * construction. Caller authorization lives in the SERVICE, not here:
4792
+ * explaining ANOTHER user requires `manage_users` or a delegated
4793
+ * `adminScope` covering that user (D12); the service's
4794
+ * `PermissionDeniedError` maps to 403. The route itself only insists on
4795
+ * an authenticated caller (an access report is sensitive even about
4796
+ * oneself, and the anonymous `guest` posture is not this endpoint's
4797
+ * business) and returns 501 when no security service exposing `explain`
4798
+ * is mounted (a deployment without `@objectstack/plugin-security`).
4799
+ */
4800
+ registerSecurityExplainEndpoints(basePath) {
4801
+ const isScoped = basePath.includes("/environments/:environmentId");
4802
+ const resolveService = async (environmentId, req) => {
4803
+ try {
4804
+ const envId = await this.resolveRequestEnvironmentId(environmentId, req);
4805
+ if (envId && envId !== "platform" && this.kernelManager) {
4806
+ const kernel = await this.kernelManager.getOrCreate(envId);
4807
+ const svc = await kernel.getServiceAsync("security").catch(() => void 0);
4808
+ if (svc) return svc;
4809
+ }
4810
+ } catch {
4811
+ }
4812
+ if (!this.securityServiceProvider) return void 0;
4813
+ try {
4814
+ return await this.securityServiceProvider(environmentId);
4815
+ } catch {
4816
+ return void 0;
4817
+ }
4818
+ };
4819
+ const handler = async (req, res) => {
4820
+ try {
4821
+ const environmentId = isScoped ? req.params?.environmentId : void 0;
4822
+ const context = await this.resolveExecCtx(environmentId, req);
4823
+ if (this.enforceAuth(req, res, context)) return;
4824
+ if (!context?.userId) {
4825
+ return res.status(401).json({
4826
+ code: "UNAUTHORIZED",
4827
+ message: "The access-explanation endpoint requires an authenticated caller."
4828
+ });
4829
+ }
4830
+ const svc = await resolveService(environmentId, req);
4831
+ if (!svc || typeof svc.explain !== "function") {
4832
+ return res.status(501).json({
4833
+ code: "NOT_IMPLEMENTED",
4834
+ message: "Access explanation is not available on this deployment (no security service with explain)."
4835
+ });
4836
+ }
4837
+ const src = req.method === "GET" ? req.query ?? {} : req.body ?? {};
4838
+ const { ExplainRequestSchema } = await import("@objectstack/spec/security");
4839
+ const parsed = ExplainRequestSchema.safeParse({
4840
+ object: src.object,
4841
+ operation: src.operation ?? "read",
4842
+ ...src.userId != null && src.userId !== "" ? { userId: src.userId } : {}
4843
+ });
4844
+ if (!parsed.success) {
4845
+ return res.status(400).json({
4846
+ code: "VALIDATION_FAILED",
4847
+ message: "Invalid explain request \u2014 expected { object: string, operation: read|create|update|delete|transfer|restore|purge, userId?: string }.",
4848
+ detail: String(parsed.error?.message ?? "").slice(0, 1e3)
4849
+ });
4850
+ }
4851
+ const decision = await svc.explain(parsed.data, context);
4852
+ res.json(decision);
4853
+ } catch (error) {
4854
+ const msg = String(error?.message ?? error ?? "");
4855
+ if (error?.code === "PERMISSION_DENIED" || error?.name === "PermissionDeniedError" || msg.startsWith("[Security] Access denied")) {
4856
+ return res.status(403).json({ code: "PERMISSION_DENIED", message: msg.slice(0, 1e3) });
4857
+ }
4858
+ logError("[REST] Security explain error:", error);
4859
+ res.status(500).json({ code: "EXPLAIN_FAILED", error: msg.slice(0, 500) });
4860
+ }
4861
+ };
4862
+ this.routeManager.register({
4863
+ method: "GET",
4864
+ path: `${basePath}/security/explain`,
4865
+ handler,
4866
+ metadata: { summary: "Explain why a principal can (or cannot) perform an operation on an object (ADR-0090 D6)", tags: ["security"] }
4867
+ });
4868
+ this.routeManager.register({
4869
+ method: "POST",
4870
+ path: `${basePath}/security/explain`,
4871
+ handler,
4872
+ metadata: { summary: "Explain why a principal can (or cannot) perform an operation on an object (ADR-0090 D6)", tags: ["security"] }
4873
+ });
4874
+ }
4514
4875
  registerSharingEndpoints(basePath) {
4515
4876
  const { crud } = this.config;
4516
4877
  const dataPath = `${basePath}${crud.dataPrefix}`;
@@ -4753,6 +5114,106 @@ var RestServer = class {
4753
5114
  metadata: { summary: "Re-evaluate a sharing rule and reconcile grants", tags: ["sharing"] }
4754
5115
  });
4755
5116
  }
5117
+ /**
5118
+ * Register the security admin endpoints (ADR-0090 D5/D9) — suggested
5119
+ * audience bindings. A package permission set declaring `isDefault: true`
5120
+ * is an install-time SUGGESTION to bind it to the `everyone` position;
5121
+ * these routes surface pending suggestions and let a tenant admin resolve
5122
+ * them. The `security` service (plugin-security) does the real gating:
5123
+ * tenant-admin pre-check on all three, and confirm writes the binding
5124
+ * with the caller's execution context so the audience-anchor and
5125
+ * delegated-admin gates enforce it — never auto-bound, never system.
5126
+ *
5127
+ * GET {basePath}/security/suggested-bindings?status=&packageId=
5128
+ * POST {basePath}/security/suggested-bindings/:id/confirm
5129
+ * POST {basePath}/security/suggested-bindings/:id/dismiss
5130
+ *
5131
+ * Routes return 501 when the `security` service is not registered
5132
+ * (deployment without plugin-security). Typed service errors carry their
5133
+ * HTTP status (403 permission / 404 not found / 409 state).
5134
+ */
5135
+ registerSecurityEndpoints(basePath) {
5136
+ const dataPath = basePath;
5137
+ const isScoped = basePath.includes("/environments/:environmentId");
5138
+ const resolveService = async (environmentId) => {
5139
+ if (!this.securityServiceProvider) return void 0;
5140
+ try {
5141
+ const svc = await this.securityServiceProvider(environmentId);
5142
+ return svc && typeof svc.listAudienceBindingSuggestions === "function" ? svc : void 0;
5143
+ } catch {
5144
+ return void 0;
5145
+ }
5146
+ };
5147
+ const respond501 = (res) => res.status(501).json({
5148
+ code: "NOT_IMPLEMENTED",
5149
+ message: "Security service is not configured on this deployment"
5150
+ });
5151
+ const handleError = (err, res, defaultCode) => {
5152
+ const status = typeof err?.statusCode === "number" ? err.statusCode : 500;
5153
+ if (status !== 500) {
5154
+ return res.status(status).json({ code: err?.code ?? defaultCode, error: String(err?.message ?? err) });
5155
+ }
5156
+ logError(`[REST] suggested-bindings ${defaultCode}:`, err);
5157
+ return res.status(500).json({ code: defaultCode, error: String(err?.message ?? err).slice(0, 500) });
5158
+ };
5159
+ this.routeManager.register({
5160
+ method: "GET",
5161
+ path: `${dataPath}/security/suggested-bindings`,
5162
+ handler: async (req, res) => {
5163
+ try {
5164
+ const environmentId = isScoped ? req.params?.environmentId : void 0;
5165
+ const context = await this.resolveExecCtx(environmentId, req);
5166
+ if (this.enforceAuth(req, res, context)) return;
5167
+ const svc = await resolveService(environmentId);
5168
+ if (!svc) return respond501(res);
5169
+ const result = await svc.listAudienceBindingSuggestions(context ?? {}, {
5170
+ status: req.query?.status ? String(req.query.status) : void 0,
5171
+ packageId: req.query?.packageId ? String(req.query.packageId) : void 0
5172
+ });
5173
+ res.json({ data: result });
5174
+ } catch (err) {
5175
+ handleError(err, res, "SUGGESTION_LIST_FAILED");
5176
+ }
5177
+ },
5178
+ metadata: { summary: "List suggested audience bindings (ADR-0090 D5/D9)", tags: ["security"] }
5179
+ });
5180
+ this.routeManager.register({
5181
+ method: "POST",
5182
+ path: `${dataPath}/security/suggested-bindings/:id/confirm`,
5183
+ handler: async (req, res) => {
5184
+ try {
5185
+ const environmentId = isScoped ? req.params?.environmentId : void 0;
5186
+ const context = await this.resolveExecCtx(environmentId, req);
5187
+ if (this.enforceAuth(req, res, context)) return;
5188
+ const svc = await resolveService(environmentId);
5189
+ if (!svc) return respond501(res);
5190
+ const result = await svc.confirmAudienceBindingSuggestion(context ?? {}, String(req.params.id));
5191
+ res.json({ data: result });
5192
+ } catch (err) {
5193
+ handleError(err, res, "SUGGESTION_CONFIRM_FAILED");
5194
+ }
5195
+ },
5196
+ metadata: { summary: "Confirm a suggested audience binding (creates the everyone/guest binding)", tags: ["security"] }
5197
+ });
5198
+ this.routeManager.register({
5199
+ method: "POST",
5200
+ path: `${dataPath}/security/suggested-bindings/:id/dismiss`,
5201
+ handler: async (req, res) => {
5202
+ try {
5203
+ const environmentId = isScoped ? req.params?.environmentId : void 0;
5204
+ const context = await this.resolveExecCtx(environmentId, req);
5205
+ if (this.enforceAuth(req, res, context)) return;
5206
+ const svc = await resolveService(environmentId);
5207
+ if (!svc) return respond501(res);
5208
+ const result = await svc.dismissAudienceBindingSuggestion(context ?? {}, String(req.params.id));
5209
+ res.json({ data: result });
5210
+ } catch (err) {
5211
+ handleError(err, res, "SUGGESTION_DISMISS_FAILED");
5212
+ }
5213
+ },
5214
+ metadata: { summary: "Dismiss a suggested audience binding", tags: ["security"] }
5215
+ });
5216
+ }
4756
5217
  /**
4757
5218
  * Register saved-report + scheduled-digest endpoints (M11.C16).
4758
5219
  *
@@ -5550,6 +6011,20 @@ function registerPackageRoutes(server, packageService, basePath = "/api/v1", opt
5550
6011
  try {
5551
6012
  const packageId = req.params.id;
5552
6013
  const version = req.query?.version;
6014
+ if (!version && typeof options.protocol?.deletePackage === "function") {
6015
+ const result2 = await options.protocol.deletePackage({ packageId });
6016
+ if (result2.failedCount === 0) {
6017
+ res.json({
6018
+ success: true,
6019
+ message: `Deleted ${packageId}`,
6020
+ deletedCount: result2.deletedCount,
6021
+ cleanups: result2.cleanups
6022
+ });
6023
+ return;
6024
+ }
6025
+ res.status(400).json({ success: false, failed: result2.failed, cleanups: result2.cleanups });
6026
+ return;
6027
+ }
5553
6028
  const result = await packageService.delete(packageId, version);
5554
6029
  if (result.success) {
5555
6030
  res.json({
@@ -5749,6 +6224,13 @@ function createRestApiPlugin(config = {}) {
5749
6224
  return void 0;
5750
6225
  }
5751
6226
  };
6227
+ const securityServiceProvider = async (_environmentId) => {
6228
+ try {
6229
+ return ctx.getService("security");
6230
+ } catch {
6231
+ return void 0;
6232
+ }
6233
+ };
5752
6234
  if (!server) {
5753
6235
  ctx.logger.warn(`RestApiPlugin: HTTP Server service '${serverService}' not found. REST routes skipped.`);
5754
6236
  return;
@@ -5766,7 +6248,7 @@ function createRestApiPlugin(config = {}) {
5766
6248
  }
5767
6249
  };
5768
6250
  try {
5769
- const restServer = new RestServer(server, protocol, config.api, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider);
6251
+ const restServer = new RestServer(server, protocol, config.api, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider, securityServiceProvider);
5770
6252
  restServer.registerRoutes();
5771
6253
  ctx.logger.info("REST API successfully registered");
5772
6254
  if (config.api?.api?.requireAuth === false) {
@@ -5822,6 +6304,13 @@ function createRestApiPlugin(config = {}) {
5822
6304
  RestServer,
5823
6305
  RouteGroupBuilder,
5824
6306
  RouteManager,
5825
- createRestApiPlugin
6307
+ buildFieldMetaMap,
6308
+ coerceRow,
6309
+ createRestApiPlugin,
6310
+ isMetaEnvelope,
6311
+ parseCsvToRows,
6312
+ parseXlsxToRows,
6313
+ prepareImportRequest,
6314
+ runImport
5826
6315
  });
5827
6316
  //# sourceMappingURL=index.cjs.map