@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.js CHANGED
@@ -250,7 +250,14 @@ function buildFieldMetaMap(schema) {
250
250
  label: typeof f.label === "string" ? f.label : void 0,
251
251
  options: Array.isArray(f.options) ? f.options : void 0,
252
252
  reference: typeof f.reference === "string" ? f.reference : void 0,
253
- displayField: typeof f.displayField === "string" ? f.displayField : void 0
253
+ displayField: typeof f.displayField === "string" ? f.displayField : void 0,
254
+ required: f.required === true,
255
+ system: f.system === true,
256
+ readonly: f.readonly === true,
257
+ // Mirror the engine's `applyFieldDefaults` gate (`f.defaultValue == null`
258
+ // ⇒ no default): any non-null default — literal, expression object, or the
259
+ // `current_user` token — counts as satisfying a required field.
260
+ hasDefault: f.defaultValue != null
254
261
  });
255
262
  }
256
263
  return map;
@@ -288,6 +295,22 @@ function optionLabel(value, options) {
288
295
  const hit = options.find((o) => o && o.value === value);
289
296
  return hit?.label ?? value;
290
297
  }
298
+ function toArgb(color) {
299
+ if (typeof color !== "string") return void 0;
300
+ const hex = color.trim().replace(/^#/, "");
301
+ if (/^[0-9a-fA-F]{3}$/.test(hex)) {
302
+ const [r, g, b] = hex;
303
+ return `FF${r}${r}${g}${g}${b}${b}`.toUpperCase();
304
+ }
305
+ if (/^[0-9a-fA-F]{6}$/.test(hex)) return `FF${hex}`.toUpperCase();
306
+ return void 0;
307
+ }
308
+ function cellFontColor(value, meta) {
309
+ if (value === null || value === void 0) return void 0;
310
+ if (!meta || !meta.type || !OPTION_TYPES.has(meta.type) || !meta.options) return void 0;
311
+ const hit = meta.options.find((o) => o && o.value === value);
312
+ return toArgb(hit?.color);
313
+ }
291
314
  function displayFromRecord(rec, displayField) {
292
315
  if (displayField && rec[displayField] != null) return String(rec[displayField]);
293
316
  for (const k of NAME_KEY_FALLBACKS) {
@@ -504,6 +527,27 @@ async function coerceFieldValue(raw, meta, ctx) {
504
527
  }
505
528
  return { value: trim && typeof raw === "string" ? raw.trim() : raw };
506
529
  }
530
+ var REQUIRED_CHECK_SKIP = /* @__PURE__ */ new Set([
531
+ "id",
532
+ "created_at",
533
+ "created_by",
534
+ "updated_at",
535
+ "updated_by"
536
+ ]);
537
+ function isBlankValue(v) {
538
+ return v === void 0 || v === null || typeof v === "string" && v.trim() === "";
539
+ }
540
+ function firstMissingRequiredField(data, metaMap) {
541
+ for (const meta of metaMap.values()) {
542
+ if (!meta.required) continue;
543
+ if (meta.system || meta.readonly) continue;
544
+ if (meta.hasDefault) continue;
545
+ if (meta.type === "autonumber") continue;
546
+ if (REQUIRED_CHECK_SKIP.has(meta.name)) continue;
547
+ if (isBlankValue(data[meta.name])) return meta.name;
548
+ }
549
+ return null;
550
+ }
507
551
  async function coerceRow(rawRow, metaMap, ctx) {
508
552
  const data = {};
509
553
  const errors = [];
@@ -702,7 +746,11 @@ function runImport(opts) {
702
746
  if (!handled) {
703
747
  const willUpdate = existing && typeof existing === "object";
704
748
  const willCreate = !willUpdate && (writeMode === "insert" || writeMode === "upsert");
705
- if (!willUpdate && !willCreate) {
749
+ const requiredMiss = willCreate && !runAutomations ? firstMissingRequiredField(data, metaMap) : null;
750
+ if (requiredMiss) {
751
+ errCount++;
752
+ results[i] = { row: rowNo, ok: false, action: "failed", field: requiredMiss, code: "required", error: `${requiredMiss} is required` };
753
+ } else if (!willUpdate && !willCreate) {
706
754
  skipped++;
707
755
  results[i] = { row: rowNo, ok: true, action: "skipped", code: "NO_MATCH" };
708
756
  } else if (dryRun) {
@@ -874,154 +922,10 @@ function applyMappingToRows(rows, artifact) {
874
922
  return { ok: true, rows: out };
875
923
  }
876
924
 
877
- // src/rest-server.ts
878
- var logError = (...args) => globalThis.console?.error(...args);
879
- var TRANSLATABLE_META_TYPES = /* @__PURE__ */ new Set(["view", "action", "object", "app", "dashboard"]);
925
+ // src/import-prepare.ts
880
926
  function isMetaEnvelope(value) {
881
927
  return !!value && typeof value === "object" && typeof value.type === "string" && typeof value.name === "string" && value.item != null && typeof value.item === "object" && !Array.isArray(value.item);
882
928
  }
883
- function mapDataError(error, object) {
884
- if (error?.code === "DELETE_RESTRICTED") {
885
- return {
886
- status: 409,
887
- body: {
888
- error: error?.message ?? "Cannot delete: dependent records exist",
889
- code: "DELETE_RESTRICTED",
890
- ...error?.dependentObject ? { dependentObject: error.dependentObject } : {},
891
- ...typeof error?.dependentCount === "number" ? { dependentCount: error.dependentCount } : {},
892
- ...object ? { object } : {}
893
- }
894
- };
895
- }
896
- if (error?.code === "CONCURRENT_UPDATE" || error?.name === "ConcurrentUpdateError") {
897
- return {
898
- status: 409,
899
- body: {
900
- error: error?.message ?? "Record was modified by another user",
901
- code: "CONCURRENT_UPDATE",
902
- ...error?.currentVersion ? { currentVersion: error.currentVersion } : {},
903
- ...error?.currentRecord ? { currentRecord: error.currentRecord } : {},
904
- ...object ? { object } : {}
905
- }
906
- };
907
- }
908
- if (error?.code === "VALIDATION_FAILED" || error?.name === "ValidationError") {
909
- return {
910
- status: 400,
911
- body: {
912
- error: error?.message ?? "Validation failed",
913
- code: "VALIDATION_FAILED",
914
- fields: Array.isArray(error?.fields) ? error.fields : [],
915
- ...object ? { object } : {}
916
- }
917
- };
918
- }
919
- if (error?.code === "PERMISSION_DENIED" || error?.name === "PermissionDeniedError" || typeof error?.message === "string" && error.message.startsWith("[Security] Access denied")) {
920
- return {
921
- status: 403,
922
- body: {
923
- error: error?.message ?? "Permission denied",
924
- code: "PERMISSION_DENIED",
925
- ...object ? { object } : {}
926
- }
927
- };
928
- }
929
- const raw = String(error?.message ?? error ?? "");
930
- const lower = raw.toLowerCase();
931
- if (raw.includes("[EnvironmentKernelFactory]") && (lower.includes("missing database_url") || lower.includes("not found"))) {
932
- const isProvisioning = lower.includes("status='provisioning'") || lower.includes("status='pending'");
933
- const isFailed = lower.includes("status='failed'");
934
- return {
935
- status: isProvisioning ? 503 : isFailed ? 502 : 404,
936
- body: {
937
- error: raw,
938
- code: isProvisioning ? "PROJECT_PROVISIONING" : isFailed ? "PROJECT_PROVISIONING_FAILED" : "PROJECT_NOT_FOUND"
939
- }
940
- };
941
- }
942
- if (error?.code === "RECORD_NOT_FOUND" || /^Record\s+\S+\s+not found in\s+\S+/i.test(raw)) {
943
- return {
944
- status: 404,
945
- body: {
946
- error: raw,
947
- code: "RECORD_NOT_FOUND",
948
- ...object ? { object } : {}
949
- }
950
- };
951
- }
952
- 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);
953
- if (unknownColumn) {
954
- const field = unknownColumn[1]?.split(".").pop();
955
- return {
956
- status: 400,
957
- body: {
958
- error: field ? `Unknown field '${field}'${object ? ` on object '${object}'` : ""}` : "Request references a field that does not exist",
959
- code: "INVALID_FIELD",
960
- ...field ? { field } : {},
961
- ...object ? { object } : {}
962
- }
963
- };
964
- }
965
- 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);
966
- if (notNull) {
967
- const field = notNull[1];
968
- return {
969
- status: 400,
970
- body: {
971
- error: `${field} is required`,
972
- code: "VALIDATION_FAILED",
973
- fields: [{ field, code: "required", message: `${field} is required` }],
974
- 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).`,
975
- ...object ? { object } : {}
976
- }
977
- };
978
- }
979
- 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");
980
- if (looksLikeUnknownObject) {
981
- return {
982
- status: 404,
983
- body: {
984
- error: object ? `Object '${object}' is not registered` : "Object not found",
985
- code: "object_not_found",
986
- object
987
- }
988
- };
989
- }
990
- 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");
991
- if (looksLikeSqlLeak) {
992
- if (lower.includes("unique constraint") || lower.includes("unique violation")) {
993
- return {
994
- status: 409,
995
- body: {
996
- error: "A record with this value already exists",
997
- code: "UNIQUE_VIOLATION",
998
- ...object ? { object } : {}
999
- }
1000
- };
1001
- }
1002
- return {
1003
- status: 500,
1004
- body: { error: "Internal data error", code: "DATABASE_ERROR" }
1005
- };
1006
- }
1007
- return { status: 400, body: { error: raw || "Bad request" } };
1008
- }
1009
- function sendError(res, error, object) {
1010
- if (typeof error?.status === "number" && error.status >= 400 && error.status < 600) {
1011
- const safeMsg = typeof error.message === "string" && error.message.length < 500 ? error.message : "Request failed";
1012
- res.status(error.status).json({
1013
- error: safeMsg,
1014
- ...error.code ? { code: error.code } : {},
1015
- ...Array.isArray(error.issues) ? { issues: error.issues } : {}
1016
- });
1017
- return;
1018
- }
1019
- const mapped = mapDataError(error, object);
1020
- res.status(mapped.status).json(mapped.body);
1021
- }
1022
- function isExpectedDataStatus(status) {
1023
- return status === 403 || status === 404 || status === 409 || status === 502 || status === 503;
1024
- }
1025
929
  function parseCsvToRows(csv, mapping = {}) {
1026
930
  const text = csv.replace(/^\uFEFF/, "");
1027
931
  const cells = [];
@@ -1240,6 +1144,162 @@ async function prepareImportRequest(body, opts) {
1240
1144
  }
1241
1145
  };
1242
1146
  }
1147
+
1148
+ // src/rest-server.ts
1149
+ var logError = (...args) => globalThis.console?.error(...args);
1150
+ var TRANSLATABLE_META_TYPES = /* @__PURE__ */ new Set(["view", "action", "object", "app", "dashboard"]);
1151
+ function mapDataError(error, object) {
1152
+ if (error?.code === "DELETE_RESTRICTED") {
1153
+ return {
1154
+ status: 409,
1155
+ body: {
1156
+ error: error?.message ?? "Cannot delete: dependent records exist",
1157
+ code: "DELETE_RESTRICTED",
1158
+ ...error?.dependentObject ? { dependentObject: error.dependentObject } : {},
1159
+ ...typeof error?.dependentCount === "number" ? { dependentCount: error.dependentCount } : {},
1160
+ ...object ? { object } : {}
1161
+ }
1162
+ };
1163
+ }
1164
+ if (error?.code === "CONCURRENT_UPDATE" || error?.name === "ConcurrentUpdateError") {
1165
+ return {
1166
+ status: 409,
1167
+ body: {
1168
+ error: error?.message ?? "Record was modified by another user",
1169
+ code: "CONCURRENT_UPDATE",
1170
+ ...error?.currentVersion ? { currentVersion: error.currentVersion } : {},
1171
+ ...error?.currentRecord ? { currentRecord: error.currentRecord } : {},
1172
+ ...object ? { object } : {}
1173
+ }
1174
+ };
1175
+ }
1176
+ if (error?.code === "VALIDATION_FAILED" || error?.name === "ValidationError") {
1177
+ return {
1178
+ status: 400,
1179
+ body: {
1180
+ error: error?.message ?? "Validation failed",
1181
+ code: "VALIDATION_FAILED",
1182
+ fields: Array.isArray(error?.fields) ? error.fields : [],
1183
+ ...object ? { object } : {}
1184
+ }
1185
+ };
1186
+ }
1187
+ if (error?.code === "FEEDS_DISABLED" || error?.code === "FILES_DISABLED") {
1188
+ return {
1189
+ status: 403,
1190
+ body: {
1191
+ error: error?.message ?? "This capability is disabled for the target object",
1192
+ code: error.code,
1193
+ ...error?.object || object ? { object: error?.object ?? object } : {}
1194
+ }
1195
+ };
1196
+ }
1197
+ if (error?.code === "PERMISSION_DENIED" || error?.name === "PermissionDeniedError" || typeof error?.message === "string" && error.message.startsWith("[Security] Access denied")) {
1198
+ return {
1199
+ status: 403,
1200
+ body: {
1201
+ error: error?.message ?? "Permission denied",
1202
+ code: "PERMISSION_DENIED",
1203
+ ...object ? { object } : {}
1204
+ }
1205
+ };
1206
+ }
1207
+ const raw = String(error?.message ?? error ?? "");
1208
+ const lower = raw.toLowerCase();
1209
+ if (raw.includes("[EnvironmentKernelFactory]") && (lower.includes("missing database_url") || lower.includes("not found"))) {
1210
+ const isProvisioning = lower.includes("status='provisioning'") || lower.includes("status='pending'");
1211
+ const isFailed = lower.includes("status='failed'");
1212
+ return {
1213
+ status: isProvisioning ? 503 : isFailed ? 502 : 404,
1214
+ body: {
1215
+ error: raw,
1216
+ code: isProvisioning ? "PROJECT_PROVISIONING" : isFailed ? "PROJECT_PROVISIONING_FAILED" : "PROJECT_NOT_FOUND"
1217
+ }
1218
+ };
1219
+ }
1220
+ if (error?.code === "RECORD_NOT_FOUND" || /^Record\s+\S+\s+not found in\s+\S+/i.test(raw)) {
1221
+ return {
1222
+ status: 404,
1223
+ body: {
1224
+ error: raw,
1225
+ code: "RECORD_NOT_FOUND",
1226
+ ...object ? { object } : {}
1227
+ }
1228
+ };
1229
+ }
1230
+ 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);
1231
+ if (unknownColumn) {
1232
+ const field = unknownColumn[1]?.split(".").pop();
1233
+ return {
1234
+ status: 400,
1235
+ body: {
1236
+ error: field ? `Unknown field '${field}'${object ? ` on object '${object}'` : ""}` : "Request references a field that does not exist",
1237
+ code: "INVALID_FIELD",
1238
+ ...field ? { field } : {},
1239
+ ...object ? { object } : {}
1240
+ }
1241
+ };
1242
+ }
1243
+ 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);
1244
+ if (notNull) {
1245
+ const field = notNull[1];
1246
+ return {
1247
+ status: 400,
1248
+ body: {
1249
+ error: `${field} is required`,
1250
+ code: "VALIDATION_FAILED",
1251
+ fields: [{ field, code: "required", message: `${field} is required` }],
1252
+ 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).`,
1253
+ ...object ? { object } : {}
1254
+ }
1255
+ };
1256
+ }
1257
+ 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");
1258
+ if (looksLikeUnknownObject) {
1259
+ return {
1260
+ status: 404,
1261
+ body: {
1262
+ error: object ? `Object '${object}' is not registered` : "Object not found",
1263
+ code: "object_not_found",
1264
+ object
1265
+ }
1266
+ };
1267
+ }
1268
+ 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");
1269
+ if (looksLikeSqlLeak) {
1270
+ if (lower.includes("unique constraint") || lower.includes("unique violation")) {
1271
+ return {
1272
+ status: 409,
1273
+ body: {
1274
+ error: "A record with this value already exists",
1275
+ code: "UNIQUE_VIOLATION",
1276
+ ...object ? { object } : {}
1277
+ }
1278
+ };
1279
+ }
1280
+ return {
1281
+ status: 500,
1282
+ body: { error: "Internal data error", code: "DATABASE_ERROR" }
1283
+ };
1284
+ }
1285
+ return { status: 400, body: { error: raw || "Bad request" } };
1286
+ }
1287
+ function sendError(res, error, object) {
1288
+ if (typeof error?.status === "number" && error.status >= 400 && error.status < 600) {
1289
+ const safeMsg = typeof error.message === "string" && error.message.length < 500 ? error.message : "Request failed";
1290
+ res.status(error.status).json({
1291
+ error: safeMsg,
1292
+ ...error.code ? { code: error.code } : {},
1293
+ ...Array.isArray(error.issues) ? { issues: error.issues } : {}
1294
+ });
1295
+ return;
1296
+ }
1297
+ const mapped = mapDataError(error, object);
1298
+ res.status(mapped.status).json(mapped.body);
1299
+ }
1300
+ function isExpectedDataStatus(status) {
1301
+ return status === 403 || status === 404 || status === 409 || status === 502 || status === 503;
1302
+ }
1243
1303
  var IMPORT_JOB_OBJECT = "sys_import_job";
1244
1304
  var IMPORT_JOB_MAX_ROWS = 5e4;
1245
1305
  var IMPORT_JOB_RESULTS_CAP = 500;
@@ -1344,7 +1404,7 @@ function rowsToCsv(fields, rows, includeHeader, metaMap) {
1344
1404
  }
1345
1405
  return lines.join("\r\n") + (lines.length > 0 ? "\r\n" : "");
1346
1406
  }
1347
- async function createXlsxStream(res) {
1407
+ async function createXlsxStream(res, useStyles = false) {
1348
1408
  const { PassThrough } = await import("stream");
1349
1409
  const ExcelJS = (await import("exceljs")).default ?? await import("exceljs");
1350
1410
  const passthrough = new PassThrough();
@@ -1361,7 +1421,7 @@ async function createXlsxStream(res) {
1361
1421
  });
1362
1422
  passthrough.on("error", reject);
1363
1423
  });
1364
- const wb = new ExcelJS.stream.xlsx.WorkbookWriter({ stream: passthrough, useStyles: false });
1424
+ const wb = new ExcelJS.stream.xlsx.WorkbookWriter({ stream: passthrough, useStyles });
1365
1425
  const ws = wb.addWorksheet("Export");
1366
1426
  return {
1367
1427
  ws,
@@ -1372,8 +1432,8 @@ async function createXlsxStream(res) {
1372
1432
  }
1373
1433
  };
1374
1434
  }
1375
- var RestServer = class {
1376
- constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider) {
1435
+ var RestServer = class _RestServer {
1436
+ constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider, securityServiceProvider) {
1377
1437
  /**
1378
1438
  * Short-TTL cache for `hostname → environmentId` (P1-4). `resolveByHostname`
1379
1439
  * is a control-plane lookup (typically a DB query) that otherwise runs on
@@ -1426,6 +1486,7 @@ var RestServer = class {
1426
1486
  this.analyticsServiceProvider = analyticsServiceProvider;
1427
1487
  this.settingsServiceProvider = settingsServiceProvider;
1428
1488
  this.serviceExistsProvider = serviceExistsProvider;
1489
+ this.securityServiceProvider = securityServiceProvider;
1429
1490
  }
1430
1491
  /**
1431
1492
  * Resolve the protocol for a given request. When `environmentId` is present
@@ -1664,6 +1725,54 @@ var RestServer = class {
1664
1725
  perReq.set(key, pending);
1665
1726
  return pending;
1666
1727
  }
1728
+ /**
1729
+ * [ADR-0046 §6.7] The audience-evaluation view of the caller for book/doc
1730
+ * gating. `permissionSets` resolves through the security service's
1731
+ * `resolvePermissionSetNames` — the SAME resolution as data-plane
1732
+ * enforcement (positions expanded, additive baseline), so the docs gate
1733
+ * can never drift from it. `permissionSets` stays undefined when the
1734
+ * service is absent or resolution fails; `audienceAllows` then DENIES
1735
+ * permission-set-gated audiences (fail closed, ADR-0049). Resolution is
1736
+ * skipped unless `needPermissionSets` — callers pass true only when a
1737
+ * `{ permissionSet }` audience is actually in play.
1738
+ */
1739
+ async resolveAudienceCaller(environmentId, req, opts) {
1740
+ const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
1741
+ const authenticated = !!ctx?.userId;
1742
+ if (!authenticated || !opts.needPermissionSets || !this.securityServiceProvider) {
1743
+ return { authenticated };
1744
+ }
1745
+ try {
1746
+ const svc = await this.securityServiceProvider(environmentId);
1747
+ if (!svc || typeof svc.resolvePermissionSetNames !== "function") return { authenticated };
1748
+ const names = await svc.resolvePermissionSetNames(ctx);
1749
+ return { authenticated, permissionSets: Array.isArray(names) ? names : [] };
1750
+ } catch {
1751
+ return { authenticated };
1752
+ }
1753
+ }
1754
+ /** Whether any of these books carries a `{ permissionSet }` audience. */
1755
+ static anyPermissionSetAudience(books) {
1756
+ return books.some(
1757
+ (b) => b && typeof b === "object" && b.audience && typeof b.audience === "object" && typeof b.audience.permissionSet === "string"
1758
+ );
1759
+ }
1760
+ /** Coerce a getMetaItems result (array | {items}) into an array. */
1761
+ static metaItemsArray(raw) {
1762
+ if (Array.isArray(raw)) return raw;
1763
+ if (raw && typeof raw === "object" && Array.isArray(raw.items)) return raw.items;
1764
+ return [];
1765
+ }
1766
+ /** Fetch every book of the environment, shaped for the audience resolver. */
1767
+ async fetchAudienceBooks(p, environmentId) {
1768
+ const raw = await p.getMetaItems({
1769
+ type: "book",
1770
+ ...environmentId ? { environmentId } : {}
1771
+ }).catch(() => []);
1772
+ return _RestServer.metaItemsArray(raw).map(
1773
+ (b) => b && typeof b === "object" ? { ...b, packageId: b._packageId } : b
1774
+ );
1775
+ }
1667
1776
  /** Heavy path behind `resolveExecCtx` — resolve identity + RBAC/RLS + localization. */
1668
1777
  async computeExecCtx(environmentId, req) {
1669
1778
  try {
@@ -2172,6 +2281,8 @@ var RestServer = class {
2172
2281
  this.registerReportsEndpoints(bp);
2173
2282
  this.registerApprovalsEndpoints(bp);
2174
2283
  this.registerAnalyticsEndpoints(bp);
2284
+ this.registerSecurityEndpoints(bp);
2285
+ this.registerSecurityExplainEndpoints(bp);
2175
2286
  this.registerDataActionEndpoints(bp);
2176
2287
  if (this.config.api.enableCrud) {
2177
2288
  this.registerCrudEndpoints(bp);
@@ -2384,9 +2495,49 @@ var RestServer = class {
2384
2495
  }
2385
2496
  }
2386
2497
  /**
2387
- * Register metadata endpoints
2498
+ * Register the metadata routes behind the SAME `requireAuth` gate the
2499
+ * `/data` routes use.
2500
+ *
2501
+ * `registerMetadataEndpoints` builds ~17 `/meta/*` routes but — unlike the
2502
+ * `/data` handlers — never calls {@link enforceAuth}: its handlers assumed
2503
+ * the `requireAuth` gate rejected anonymous callers "upstream", yet nothing
2504
+ * upstream covers `/meta`, so on a `requireAuth` deployment an anonymous
2505
+ * caller could read object / field schemas. On a tenant-less runtime host
2506
+ * those are SYSTEM-object schemas and the host is publicly reachable — a
2507
+ * real leak.
2508
+ *
2509
+ * Rather than add the gate to every handler (and have the next new route
2510
+ * forget it — the exact failure mode that caused this), wrap the route
2511
+ * registrar for the duration of registration so every meta route, present
2512
+ * and future, inherits it. The check is a no-op when `requireAuth` is off
2513
+ * (demo / single-tenant), so the previously-public metadata surface there
2514
+ * is unchanged; an authenticated user passes exactly as on `/data`.
2388
2515
  */
2389
2516
  registerMetadataEndpoints(basePath) {
2517
+ const realRouteManager = this.routeManager;
2518
+ const guardedRouteManager = {
2519
+ register: (entry) => {
2520
+ const inner = entry.handler;
2521
+ if (typeof inner !== "function") return realRouteManager.register(entry);
2522
+ return realRouteManager.register({
2523
+ ...entry,
2524
+ handler: async (req, res) => {
2525
+ const environmentId = req?.params?.environmentId;
2526
+ const context = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
2527
+ if (this.enforceAuth(req, res, context)) return;
2528
+ return inner(req, res);
2529
+ }
2530
+ });
2531
+ }
2532
+ };
2533
+ this.routeManager = guardedRouteManager;
2534
+ try {
2535
+ this.registerMetadataEndpointsInner(basePath);
2536
+ } finally {
2537
+ this.routeManager = realRouteManager;
2538
+ }
2539
+ }
2540
+ registerMetadataEndpointsInner(basePath) {
2390
2541
  const { metadata } = this.config;
2391
2542
  const metaPath = `${basePath}${metadata.prefix}`;
2392
2543
  const isScoped = basePath.includes("/environments/:environmentId");
@@ -2533,6 +2684,48 @@ var RestServer = class {
2533
2684
  visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
2534
2685
  }
2535
2686
  }
2687
+ if (req.params.type === "book") {
2688
+ const raw = visible;
2689
+ const list = _RestServer.metaItemsArray(raw);
2690
+ if (list.length > 0) {
2691
+ const { audienceAllows } = await import("@objectstack/spec/system");
2692
+ const caller = await this.resolveAudienceCaller(environmentId, req, {
2693
+ needPermissionSets: _RestServer.anyPermissionSetAudience(list)
2694
+ });
2695
+ const filtered = list.filter((b) => b && typeof b === "object" && audienceAllows(b.audience, caller));
2696
+ visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
2697
+ }
2698
+ }
2699
+ if (req.params.type === "doc") {
2700
+ const raw = visible;
2701
+ const list = _RestServer.metaItemsArray(raw);
2702
+ if (list.length > 0) {
2703
+ const { audienceAllows, docAudienceAllows, resolveDocAudiences } = await import("@objectstack/spec/system");
2704
+ const books = await this.fetchAudienceBooks(p, environmentId);
2705
+ const caller = await this.resolveAudienceCaller(environmentId, req, {
2706
+ needPermissionSets: _RestServer.anyPermissionSetAudience(books)
2707
+ });
2708
+ let filtered;
2709
+ if (caller.authenticated && !_RestServer.anyPermissionSetAudience(books)) {
2710
+ filtered = list;
2711
+ } else {
2712
+ const corpus = list.filter((d) => d && typeof d === "object").map((d) => ({
2713
+ name: d.name,
2714
+ group: d.group,
2715
+ tags: d.tags,
2716
+ order: d.order,
2717
+ packageId: d._packageId
2718
+ }));
2719
+ const audiences = resolveDocAudiences(books, corpus);
2720
+ filtered = list.filter((d) => {
2721
+ if (!d || typeof d !== "object") return false;
2722
+ const eff = audiences.get(d.name);
2723
+ return eff ? docAudienceAllows(eff, caller) : audienceAllows("org", caller);
2724
+ });
2725
+ }
2726
+ visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
2727
+ }
2728
+ }
2536
2729
  if (req.params.type === "doc") {
2537
2730
  const locale = this.extractLocale(req);
2538
2731
  const { resolveDocLocale } = await import("@objectstack/spec/system");
@@ -2606,7 +2799,7 @@ var RestServer = class {
2606
2799
  const prot = await this.resolveProtocol(environmentId, req);
2607
2800
  const locale = this.extractLocale(req);
2608
2801
  const packageId = req.query?.package || void 0;
2609
- const { resolveBookTree, deriveImplicitPackageBook, isPublicAudience, resolveDocLocale } = await import("@objectstack/spec/system");
2802
+ const { resolveBookTree, deriveImplicitPackageBook, audienceAllows, resolveDocAudiences, docAudienceAllows, resolveDocLocale } = await import("@objectstack/spec/system");
2610
2803
  const norm = (raw) => Array.isArray(raw) ? raw : raw && Array.isArray(raw.items) ? raw.items : [];
2611
2804
  const books = norm(await prot.getMetaItems({
2612
2805
  type: "book",
@@ -2617,9 +2810,16 @@ var RestServer = class {
2617
2810
  if (!book) {
2618
2811
  book = deriveImplicitPackageBook(req.params.name, req.params.name);
2619
2812
  }
2620
- const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
2621
- if (!ctx?.userId && !isPublicAudience(book.audience)) {
2622
- sendError(res, { code: "unauthorized", message: "This documentation requires sign-in", status: 401 });
2813
+ const audienceBooks = books.map((b) => b && typeof b === "object" ? { ...b, packageId: b._packageId } : b);
2814
+ const caller = await this.resolveAudienceCaller(environmentId, req, {
2815
+ needPermissionSets: _RestServer.anyPermissionSetAudience([book, ...audienceBooks])
2816
+ });
2817
+ if (!audienceAllows(book.audience, caller)) {
2818
+ if (!caller.authenticated) {
2819
+ sendError(res, { code: "unauthorized", message: "This documentation requires sign-in", status: 401 });
2820
+ } else {
2821
+ sendError(res, { code: "PERMISSION_DENIED", message: "This documentation is limited to holders of a permission set you do not have", status: 403 });
2822
+ }
2623
2823
  return;
2624
2824
  }
2625
2825
  const docs = norm(await prot.getMetaItems({
@@ -2636,6 +2836,14 @@ var RestServer = class {
2636
2836
  packageId: d._packageId
2637
2837
  }));
2638
2838
  const tree = resolveBookTree(book, docs, book._packageId);
2839
+ const gatedTreePossible = !caller.authenticated || _RestServer.anyPermissionSetAudience(audienceBooks);
2840
+ if (gatedTreePossible) {
2841
+ const audiences = resolveDocAudiences(audienceBooks, docs);
2842
+ tree.groups = tree.groups.map((g) => ({
2843
+ ...g,
2844
+ entries: g.entries.filter((e) => !e.doc || docAudienceAllows(audiences.get(e.doc), caller))
2845
+ })).filter((g) => g.entries.some((e) => e.doc || e.href));
2846
+ }
2639
2847
  res.json(tree);
2640
2848
  } catch (error) {
2641
2849
  logError("[REST] Unhandled error:", error);
@@ -2670,7 +2878,7 @@ var RestServer = class {
2670
2878
  const isDraftRead = typeof req.query?.state === "string" && req.query.state.toLowerCase() === "draft";
2671
2879
  const previewDrafts = typeof req.query?.preview === "string" && req.query.preview.toLowerCase() === "draft";
2672
2880
  const packageScoped = typeof req.query?.package === "string" && req.query.package.length > 0;
2673
- if (metadata.enableCache && p.getMetaItemCached && !isAppType && !isDraftRead && !previewDrafts && !packageScoped && req.params.type !== "doc") {
2881
+ if (metadata.enableCache && p.getMetaItemCached && !isAppType && !isDraftRead && !previewDrafts && !packageScoped && req.params.type !== "doc" && req.params.type !== "book") {
2674
2882
  const cacheRequest = {
2675
2883
  ifNoneMatch: req.headers["if-none-match"],
2676
2884
  ifModifiedSince: req.headers["if-modified-since"]
@@ -2739,6 +2947,47 @@ var RestServer = class {
2739
2947
  const serviceGate = registered ? (n) => registered.has(n) : void 0;
2740
2948
  if (serviceGate) visible = this.filterDashboardForUser(visible, serviceGate);
2741
2949
  }
2950
+ if ((req.params.type === "book" || req.params.type === "doc") && visible) {
2951
+ const { audienceAllows, docAudienceAllows, resolveDocAudiences } = await import("@objectstack/spec/system");
2952
+ const target = isMetaEnvelope(visible) ? visible.item : visible;
2953
+ let caller;
2954
+ let allowed;
2955
+ if (req.params.type === "book") {
2956
+ caller = await this.resolveAudienceCaller(environmentId, req, {
2957
+ needPermissionSets: _RestServer.anyPermissionSetAudience([target])
2958
+ });
2959
+ allowed = audienceAllows(target?.audience, caller);
2960
+ } else {
2961
+ const books = await this.fetchAudienceBooks(p, environmentId);
2962
+ caller = await this.resolveAudienceCaller(environmentId, req, {
2963
+ needPermissionSets: _RestServer.anyPermissionSetAudience(books)
2964
+ });
2965
+ if (caller.authenticated && !_RestServer.anyPermissionSetAudience(books)) {
2966
+ allowed = true;
2967
+ } else {
2968
+ const corpus = _RestServer.metaItemsArray(await p.getMetaItems({
2969
+ type: "doc",
2970
+ ...environmentId ? { environmentId } : {}
2971
+ }).catch(() => [])).filter((d) => d && typeof d === "object").map((d) => ({
2972
+ name: d.name,
2973
+ group: d.group,
2974
+ tags: d.tags,
2975
+ order: d.order,
2976
+ packageId: d._packageId
2977
+ }));
2978
+ const audiences = resolveDocAudiences(books, corpus);
2979
+ allowed = docAudienceAllows(audiences.get(target?.name), caller);
2980
+ }
2981
+ }
2982
+ if (!allowed) {
2983
+ if (!caller.authenticated) {
2984
+ sendError(res, { code: "unauthorized", message: "This documentation requires sign-in", status: 401 });
2985
+ } else {
2986
+ sendError(res, { code: "PERMISSION_DENIED", message: "This documentation is limited to holders of a permission set you do not have", status: 403 });
2987
+ }
2988
+ return;
2989
+ }
2990
+ }
2742
2991
  if (req.params.type === "doc" && visible) {
2743
2992
  const locale = this.extractLocale(req);
2744
2993
  const { resolveDocLocale } = await import("@objectstack/spec/system");
@@ -3739,9 +3988,11 @@ var RestServer = class {
3739
3988
  const includeHeader = String(q.header ?? "true").toLowerCase() !== "false";
3740
3989
  const HARD_CAP = 5e4;
3741
3990
  const MAX_CHUNK = 5e3;
3991
+ const STYLE_ROW_CAP = 1e4;
3742
3992
  const requestedLimit = q.limit != null ? Math.max(1, Number(q.limit) || 0) : 1e4;
3743
3993
  const limit = Math.min(requestedLimit, HARD_CAP);
3744
3994
  const chunkSize = Math.min(MAX_CHUNK, Math.max(50, q.page != null ? Number(q.page) || 500 : 500));
3995
+ const styled = format === "xlsx" && limit <= STYLE_ROW_CAP;
3745
3996
  let filter = void 0;
3746
3997
  if (typeof q.filter === "string" && q.filter.length > 0) {
3747
3998
  try {
@@ -3808,12 +4059,13 @@ var RestServer = class {
3808
4059
  }
3809
4060
  res.header("X-Export-Format", format);
3810
4061
  res.header("X-Export-Limit", String(limit));
4062
+ if (format === "xlsx") res.header("X-Export-Styles", styled ? "applied" : "dropped");
3811
4063
  res.header("Cache-Control", "no-store");
3812
4064
  let exported = 0;
3813
4065
  let firstChunk = true;
3814
4066
  let skip = 0;
3815
4067
  if (format === "json") res.write("[");
3816
- const xlsx = format === "xlsx" ? await createXlsxStream(res) : null;
4068
+ const xlsx = format === "xlsx" ? await createXlsxStream(res, styled) : null;
3817
4069
  while (exported < limit) {
3818
4070
  const take = Math.min(chunkSize, limit - exported);
3819
4071
  const findArgs = {
@@ -3841,8 +4093,16 @@ var RestServer = class {
3841
4093
  if (firstChunk && includeHeader) {
3842
4094
  xlsx.ws.addRow((fields ?? []).map((f) => headerLabel(f, metaMap))).commit();
3843
4095
  }
4096
+ const cols = fields ?? [];
3844
4097
  for (const row of rows) {
3845
- xlsx.ws.addRow(formatRowCells(row, fields ?? [], metaMap)).commit();
4098
+ const r = xlsx.ws.addRow(formatRowCells(row, cols, metaMap));
4099
+ if (styled) {
4100
+ cols.forEach((f, i) => {
4101
+ const argb = cellFontColor(row?.[f], metaMap.get(f));
4102
+ if (argb) r.getCell(i + 1).font = { color: { argb } };
4103
+ });
4104
+ }
4105
+ r.commit();
3846
4106
  }
3847
4107
  } else {
3848
4108
  for (let i = 0; i < rows.length; i++) {
@@ -4471,6 +4731,100 @@ var RestServer = class {
4471
4731
  metadata: { summary: "Run a semantic-layer dataset (preview/query)", tags: ["analytics"] }
4472
4732
  });
4473
4733
  }
4734
+ /**
4735
+ * [ADR-0090 D6] Access-explanation endpoint — the REST face of the
4736
+ * explain engine (framework#2696).
4737
+ *
4738
+ * GET {basePath}/security/explain?object=…&operation=…&userId=…
4739
+ * POST {basePath}/security/explain body: { object, operation, userId? }
4740
+ *
4741
+ * Delegates to the security service's `explain(request, callerContext)`
4742
+ * (`SecurityPlugin.explainAccessForCaller`) — the same code paths the
4743
+ * enforcement middleware runs, so the report is explained by
4744
+ * construction. Caller authorization lives in the SERVICE, not here:
4745
+ * explaining ANOTHER user requires `manage_users` or a delegated
4746
+ * `adminScope` covering that user (D12); the service's
4747
+ * `PermissionDeniedError` maps to 403. The route itself only insists on
4748
+ * an authenticated caller (an access report is sensitive even about
4749
+ * oneself, and the anonymous `guest` posture is not this endpoint's
4750
+ * business) and returns 501 when no security service exposing `explain`
4751
+ * is mounted (a deployment without `@objectstack/plugin-security`).
4752
+ */
4753
+ registerSecurityExplainEndpoints(basePath) {
4754
+ const isScoped = basePath.includes("/environments/:environmentId");
4755
+ const resolveService = async (environmentId, req) => {
4756
+ try {
4757
+ const envId = await this.resolveRequestEnvironmentId(environmentId, req);
4758
+ if (envId && envId !== "platform" && this.kernelManager) {
4759
+ const kernel = await this.kernelManager.getOrCreate(envId);
4760
+ const svc = await kernel.getServiceAsync("security").catch(() => void 0);
4761
+ if (svc) return svc;
4762
+ }
4763
+ } catch {
4764
+ }
4765
+ if (!this.securityServiceProvider) return void 0;
4766
+ try {
4767
+ return await this.securityServiceProvider(environmentId);
4768
+ } catch {
4769
+ return void 0;
4770
+ }
4771
+ };
4772
+ const handler = async (req, res) => {
4773
+ try {
4774
+ const environmentId = isScoped ? req.params?.environmentId : void 0;
4775
+ const context = await this.resolveExecCtx(environmentId, req);
4776
+ if (this.enforceAuth(req, res, context)) return;
4777
+ if (!context?.userId) {
4778
+ return res.status(401).json({
4779
+ code: "UNAUTHORIZED",
4780
+ message: "The access-explanation endpoint requires an authenticated caller."
4781
+ });
4782
+ }
4783
+ const svc = await resolveService(environmentId, req);
4784
+ if (!svc || typeof svc.explain !== "function") {
4785
+ return res.status(501).json({
4786
+ code: "NOT_IMPLEMENTED",
4787
+ message: "Access explanation is not available on this deployment (no security service with explain)."
4788
+ });
4789
+ }
4790
+ const src = req.method === "GET" ? req.query ?? {} : req.body ?? {};
4791
+ const { ExplainRequestSchema } = await import("@objectstack/spec/security");
4792
+ const parsed = ExplainRequestSchema.safeParse({
4793
+ object: src.object,
4794
+ operation: src.operation ?? "read",
4795
+ ...src.userId != null && src.userId !== "" ? { userId: src.userId } : {}
4796
+ });
4797
+ if (!parsed.success) {
4798
+ return res.status(400).json({
4799
+ code: "VALIDATION_FAILED",
4800
+ message: "Invalid explain request \u2014 expected { object: string, operation: read|create|update|delete|transfer|restore|purge, userId?: string }.",
4801
+ detail: String(parsed.error?.message ?? "").slice(0, 1e3)
4802
+ });
4803
+ }
4804
+ const decision = await svc.explain(parsed.data, context);
4805
+ res.json(decision);
4806
+ } catch (error) {
4807
+ const msg = String(error?.message ?? error ?? "");
4808
+ if (error?.code === "PERMISSION_DENIED" || error?.name === "PermissionDeniedError" || msg.startsWith("[Security] Access denied")) {
4809
+ return res.status(403).json({ code: "PERMISSION_DENIED", message: msg.slice(0, 1e3) });
4810
+ }
4811
+ logError("[REST] Security explain error:", error);
4812
+ res.status(500).json({ code: "EXPLAIN_FAILED", error: msg.slice(0, 500) });
4813
+ }
4814
+ };
4815
+ this.routeManager.register({
4816
+ method: "GET",
4817
+ path: `${basePath}/security/explain`,
4818
+ handler,
4819
+ metadata: { summary: "Explain why a principal can (or cannot) perform an operation on an object (ADR-0090 D6)", tags: ["security"] }
4820
+ });
4821
+ this.routeManager.register({
4822
+ method: "POST",
4823
+ path: `${basePath}/security/explain`,
4824
+ handler,
4825
+ metadata: { summary: "Explain why a principal can (or cannot) perform an operation on an object (ADR-0090 D6)", tags: ["security"] }
4826
+ });
4827
+ }
4474
4828
  registerSharingEndpoints(basePath) {
4475
4829
  const { crud } = this.config;
4476
4830
  const dataPath = `${basePath}${crud.dataPrefix}`;
@@ -4713,6 +5067,106 @@ var RestServer = class {
4713
5067
  metadata: { summary: "Re-evaluate a sharing rule and reconcile grants", tags: ["sharing"] }
4714
5068
  });
4715
5069
  }
5070
+ /**
5071
+ * Register the security admin endpoints (ADR-0090 D5/D9) — suggested
5072
+ * audience bindings. A package permission set declaring `isDefault: true`
5073
+ * is an install-time SUGGESTION to bind it to the `everyone` position;
5074
+ * these routes surface pending suggestions and let a tenant admin resolve
5075
+ * them. The `security` service (plugin-security) does the real gating:
5076
+ * tenant-admin pre-check on all three, and confirm writes the binding
5077
+ * with the caller's execution context so the audience-anchor and
5078
+ * delegated-admin gates enforce it — never auto-bound, never system.
5079
+ *
5080
+ * GET {basePath}/security/suggested-bindings?status=&packageId=
5081
+ * POST {basePath}/security/suggested-bindings/:id/confirm
5082
+ * POST {basePath}/security/suggested-bindings/:id/dismiss
5083
+ *
5084
+ * Routes return 501 when the `security` service is not registered
5085
+ * (deployment without plugin-security). Typed service errors carry their
5086
+ * HTTP status (403 permission / 404 not found / 409 state).
5087
+ */
5088
+ registerSecurityEndpoints(basePath) {
5089
+ const dataPath = basePath;
5090
+ const isScoped = basePath.includes("/environments/:environmentId");
5091
+ const resolveService = async (environmentId) => {
5092
+ if (!this.securityServiceProvider) return void 0;
5093
+ try {
5094
+ const svc = await this.securityServiceProvider(environmentId);
5095
+ return svc && typeof svc.listAudienceBindingSuggestions === "function" ? svc : void 0;
5096
+ } catch {
5097
+ return void 0;
5098
+ }
5099
+ };
5100
+ const respond501 = (res) => res.status(501).json({
5101
+ code: "NOT_IMPLEMENTED",
5102
+ message: "Security service is not configured on this deployment"
5103
+ });
5104
+ const handleError = (err, res, defaultCode) => {
5105
+ const status = typeof err?.statusCode === "number" ? err.statusCode : 500;
5106
+ if (status !== 500) {
5107
+ return res.status(status).json({ code: err?.code ?? defaultCode, error: String(err?.message ?? err) });
5108
+ }
5109
+ logError(`[REST] suggested-bindings ${defaultCode}:`, err);
5110
+ return res.status(500).json({ code: defaultCode, error: String(err?.message ?? err).slice(0, 500) });
5111
+ };
5112
+ this.routeManager.register({
5113
+ method: "GET",
5114
+ path: `${dataPath}/security/suggested-bindings`,
5115
+ handler: async (req, res) => {
5116
+ try {
5117
+ const environmentId = isScoped ? req.params?.environmentId : void 0;
5118
+ const context = await this.resolveExecCtx(environmentId, req);
5119
+ if (this.enforceAuth(req, res, context)) return;
5120
+ const svc = await resolveService(environmentId);
5121
+ if (!svc) return respond501(res);
5122
+ const result = await svc.listAudienceBindingSuggestions(context ?? {}, {
5123
+ status: req.query?.status ? String(req.query.status) : void 0,
5124
+ packageId: req.query?.packageId ? String(req.query.packageId) : void 0
5125
+ });
5126
+ res.json({ data: result });
5127
+ } catch (err) {
5128
+ handleError(err, res, "SUGGESTION_LIST_FAILED");
5129
+ }
5130
+ },
5131
+ metadata: { summary: "List suggested audience bindings (ADR-0090 D5/D9)", tags: ["security"] }
5132
+ });
5133
+ this.routeManager.register({
5134
+ method: "POST",
5135
+ path: `${dataPath}/security/suggested-bindings/:id/confirm`,
5136
+ handler: async (req, res) => {
5137
+ try {
5138
+ const environmentId = isScoped ? req.params?.environmentId : void 0;
5139
+ const context = await this.resolveExecCtx(environmentId, req);
5140
+ if (this.enforceAuth(req, res, context)) return;
5141
+ const svc = await resolveService(environmentId);
5142
+ if (!svc) return respond501(res);
5143
+ const result = await svc.confirmAudienceBindingSuggestion(context ?? {}, String(req.params.id));
5144
+ res.json({ data: result });
5145
+ } catch (err) {
5146
+ handleError(err, res, "SUGGESTION_CONFIRM_FAILED");
5147
+ }
5148
+ },
5149
+ metadata: { summary: "Confirm a suggested audience binding (creates the everyone/guest binding)", tags: ["security"] }
5150
+ });
5151
+ this.routeManager.register({
5152
+ method: "POST",
5153
+ path: `${dataPath}/security/suggested-bindings/:id/dismiss`,
5154
+ handler: async (req, res) => {
5155
+ try {
5156
+ const environmentId = isScoped ? req.params?.environmentId : void 0;
5157
+ const context = await this.resolveExecCtx(environmentId, req);
5158
+ if (this.enforceAuth(req, res, context)) return;
5159
+ const svc = await resolveService(environmentId);
5160
+ if (!svc) return respond501(res);
5161
+ const result = await svc.dismissAudienceBindingSuggestion(context ?? {}, String(req.params.id));
5162
+ res.json({ data: result });
5163
+ } catch (err) {
5164
+ handleError(err, res, "SUGGESTION_DISMISS_FAILED");
5165
+ }
5166
+ },
5167
+ metadata: { summary: "Dismiss a suggested audience binding", tags: ["security"] }
5168
+ });
5169
+ }
4716
5170
  /**
4717
5171
  * Register saved-report + scheduled-digest endpoints (M11.C16).
4718
5172
  *
@@ -5510,6 +5964,20 @@ function registerPackageRoutes(server, packageService, basePath = "/api/v1", opt
5510
5964
  try {
5511
5965
  const packageId = req.params.id;
5512
5966
  const version = req.query?.version;
5967
+ if (!version && typeof options.protocol?.deletePackage === "function") {
5968
+ const result2 = await options.protocol.deletePackage({ packageId });
5969
+ if (result2.failedCount === 0) {
5970
+ res.json({
5971
+ success: true,
5972
+ message: `Deleted ${packageId}`,
5973
+ deletedCount: result2.deletedCount,
5974
+ cleanups: result2.cleanups
5975
+ });
5976
+ return;
5977
+ }
5978
+ res.status(400).json({ success: false, failed: result2.failed, cleanups: result2.cleanups });
5979
+ return;
5980
+ }
5513
5981
  const result = await packageService.delete(packageId, version);
5514
5982
  if (result.success) {
5515
5983
  res.json({
@@ -5709,6 +6177,13 @@ function createRestApiPlugin(config = {}) {
5709
6177
  return void 0;
5710
6178
  }
5711
6179
  };
6180
+ const securityServiceProvider = async (_environmentId) => {
6181
+ try {
6182
+ return ctx.getService("security");
6183
+ } catch {
6184
+ return void 0;
6185
+ }
6186
+ };
5712
6187
  if (!server) {
5713
6188
  ctx.logger.warn(`RestApiPlugin: HTTP Server service '${serverService}' not found. REST routes skipped.`);
5714
6189
  return;
@@ -5726,7 +6201,7 @@ function createRestApiPlugin(config = {}) {
5726
6201
  }
5727
6202
  };
5728
6203
  try {
5729
- const restServer = new RestServer(server, protocol, config.api, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider);
6204
+ const restServer = new RestServer(server, protocol, config.api, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider, securityServiceProvider);
5730
6205
  restServer.registerRoutes();
5731
6206
  ctx.logger.info("REST API successfully registered");
5732
6207
  if (config.api?.api?.requireAuth === false) {
@@ -5781,6 +6256,13 @@ export {
5781
6256
  RestServer,
5782
6257
  RouteGroupBuilder,
5783
6258
  RouteManager,
5784
- createRestApiPlugin
6259
+ buildFieldMetaMap,
6260
+ coerceRow,
6261
+ createRestApiPlugin,
6262
+ isMetaEnvelope,
6263
+ parseCsvToRows,
6264
+ parseXlsxToRows,
6265
+ prepareImportRequest,
6266
+ runImport
5785
6267
  };
5786
6268
  //# sourceMappingURL=index.js.map