@objectstack/rest 15.0.0 → 15.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -2,7 +2,7 @@ import { IHttpServer, RouteHandler, Plugin } from '@objectstack/core';
2
2
  import * as System from '@objectstack/spec/system';
3
3
  import * as Shared from '@objectstack/spec/shared';
4
4
  import { z } from 'zod';
5
- import { ObjectStackProtocol, RestServerConfig } from '@objectstack/spec/api';
5
+ import { DataProtocol, MetadataProtocol, RestServerConfig } from '@objectstack/spec/api';
6
6
 
7
7
  type RouteHandlerMetadata = System.RouteHandlerMetadata;
8
8
  type HttpMethod = z.infer<typeof Shared.HttpMethod>;
@@ -155,6 +155,14 @@ declare class RouteGroupBuilder {
155
155
  private resolvePath;
156
156
  }
157
157
 
158
+ /**
159
+ * The protocol slice the REST layer actually consumes (ADR-0076 D9 / #2462
160
+ * A1.5): wire-normalized data CRUD plus the metadata control plane — not the
161
+ * full `ObjectStackProtocol` union. Server-only extensions (drafts, history,
162
+ * diagnostics, clone, …) are feature-detected via runtime casts and so don't
163
+ * widen this contract.
164
+ */
165
+ type RestProtocol = DataProtocol & MetadataProtocol;
158
166
  /**
159
167
  * Structural subset of `KernelManager` that RestServer needs in order to
160
168
  * resolve a per-project protocol at request time. Typed locally to avoid
@@ -266,7 +274,7 @@ declare class RestServer {
266
274
  * the durable source of truth a restarted/other node reads.
267
275
  */
268
276
  private readonly cancelledImportJobs;
269
- constructor(server: IHttpServer, protocol: ObjectStackProtocol, config?: RestServerConfig, kernelManager?: RestKernelManager, envRegistry?: RestEnvRegistry, defaultEnvironmentIdProvider?: () => string | undefined, authServiceProvider?: (environmentId?: string) => Promise<any | undefined>, objectQLProvider?: (environmentId?: string) => Promise<any | undefined>, emailServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingServiceProvider?: (environmentId?: string) => Promise<any | undefined>, reportsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, approvalsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingRulesServiceProvider?: (environmentId?: string) => Promise<any | undefined>, i18nServiceProvider?: (environmentId?: string) => Promise<any | undefined>, analyticsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, settingsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, serviceExistsProvider?: (name: string) => boolean, securityServiceProvider?: (environmentId?: string) => Promise<any | undefined>);
277
+ constructor(server: IHttpServer, protocol: RestProtocol, config?: RestServerConfig, kernelManager?: RestKernelManager, envRegistry?: RestEnvRegistry, defaultEnvironmentIdProvider?: () => string | undefined, authServiceProvider?: (environmentId?: string) => Promise<any | undefined>, objectQLProvider?: (environmentId?: string) => Promise<any | undefined>, emailServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingServiceProvider?: (environmentId?: string) => Promise<any | undefined>, reportsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, approvalsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingRulesServiceProvider?: (environmentId?: string) => Promise<any | undefined>, i18nServiceProvider?: (environmentId?: string) => Promise<any | undefined>, analyticsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, settingsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, serviceExistsProvider?: (name: string) => boolean, securityServiceProvider?: (environmentId?: string) => Promise<any | undefined>);
270
278
  /**
271
279
  * Resolve the protocol for a given request. When `environmentId` is present
272
280
  * and a KernelManager is wired, fetch the per-project kernel's
@@ -755,7 +763,7 @@ interface RestApiPluginConfig {
755
763
  *
756
764
  * Responsibilities:
757
765
  * 1. Consumes 'http.server' (or configured service)
758
- * 2. Consumes 'protocol' (ObjectStackProtocol)
766
+ * 2. Consumes 'protocol' (the `RestProtocol` slice — DataProtocol + MetadataProtocol, ADR-0076 D9)
759
767
  * 3. Instantiates RestServer to auto-generate routes
760
768
  */
761
769
  declare function createRestApiPlugin(config?: RestApiPluginConfig): Plugin;
@@ -787,6 +795,8 @@ interface ExportFieldMeta {
787
795
  reference?: string;
788
796
  /** Field on the referenced record to show as its label. */
789
797
  displayField?: string;
798
+ /** Field holds multiple values (an array), e.g. a `multiple: true` lookup. */
799
+ multiple?: boolean;
790
800
  /** Field is required — a value (or default) must exist on insert. */
791
801
  required?: boolean;
792
802
  /** Engine-owned column the client never supplies (never required of import). */
@@ -1027,6 +1037,12 @@ declare function runImport(opts: RunImportOptions): Promise<ImportRunSummary>;
1027
1037
  * - select / radio → an option *value*
1028
1038
  * - multiselect / checkboxes / tags → an array of option values
1029
1039
  * - lookup / master_detail / user / reference → a record id (resolved async)
1040
+ * - file / image → a file id / url (as-is)
1041
+ *
1042
+ * Any of the last four whose field is flagged `multiple: true` (per the spec,
1043
+ * `multiple` applies to select / lookup / file / image; `radio`/`user` share
1044
+ * their branch) instead store an **array** — the cell is split on the export
1045
+ * separator and each token coerced individually. See `isMultiValueField`.
1030
1046
  *
1031
1047
  * Contract: when a field carries no usable metadata the value passes through
1032
1048
  * untouched, so an import stays byte-identical to the pre-coercion behaviour.
@@ -1085,4 +1101,4 @@ declare function coerceRow(rawRow: Record<string, unknown>, metaMap: Map<string,
1085
1101
  errors: FieldCoerceError[];
1086
1102
  }>;
1087
1103
 
1088
- export { type CoerceContext, type ExportFieldMeta, type ImportAction, type ImportProgress, type ImportProtocolLike, type ImportRowResult, type ImportRunSummary, type ImportUndoLog, type PrepareImportResult, type PreparedImport, type RefResolver, type RestApiPluginConfig, RestServer, type RouteEntry, RouteGroupBuilder, RouteManager, type RunImportOptions, buildFieldMetaMap, coerceRow, createRestApiPlugin, isMetaEnvelope, parseCsvToRows, parseXlsxToRows, prepareImportRequest, runImport };
1104
+ export { type CoerceContext, type ExportFieldMeta, type ImportAction, type ImportProgress, type ImportProtocolLike, type ImportRowResult, type ImportRunSummary, type ImportUndoLog, type PrepareImportResult, type PreparedImport, type RefResolver, type RestApiPluginConfig, type RestProtocol, RestServer, type RouteEntry, RouteGroupBuilder, RouteManager, type RunImportOptions, buildFieldMetaMap, coerceRow, createRestApiPlugin, isMetaEnvelope, parseCsvToRows, parseXlsxToRows, prepareImportRequest, runImport };
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { IHttpServer, RouteHandler, Plugin } from '@objectstack/core';
2
2
  import * as System from '@objectstack/spec/system';
3
3
  import * as Shared from '@objectstack/spec/shared';
4
4
  import { z } from 'zod';
5
- import { ObjectStackProtocol, RestServerConfig } from '@objectstack/spec/api';
5
+ import { DataProtocol, MetadataProtocol, RestServerConfig } from '@objectstack/spec/api';
6
6
 
7
7
  type RouteHandlerMetadata = System.RouteHandlerMetadata;
8
8
  type HttpMethod = z.infer<typeof Shared.HttpMethod>;
@@ -155,6 +155,14 @@ declare class RouteGroupBuilder {
155
155
  private resolvePath;
156
156
  }
157
157
 
158
+ /**
159
+ * The protocol slice the REST layer actually consumes (ADR-0076 D9 / #2462
160
+ * A1.5): wire-normalized data CRUD plus the metadata control plane — not the
161
+ * full `ObjectStackProtocol` union. Server-only extensions (drafts, history,
162
+ * diagnostics, clone, …) are feature-detected via runtime casts and so don't
163
+ * widen this contract.
164
+ */
165
+ type RestProtocol = DataProtocol & MetadataProtocol;
158
166
  /**
159
167
  * Structural subset of `KernelManager` that RestServer needs in order to
160
168
  * resolve a per-project protocol at request time. Typed locally to avoid
@@ -266,7 +274,7 @@ declare class RestServer {
266
274
  * the durable source of truth a restarted/other node reads.
267
275
  */
268
276
  private readonly cancelledImportJobs;
269
- constructor(server: IHttpServer, protocol: ObjectStackProtocol, config?: RestServerConfig, kernelManager?: RestKernelManager, envRegistry?: RestEnvRegistry, defaultEnvironmentIdProvider?: () => string | undefined, authServiceProvider?: (environmentId?: string) => Promise<any | undefined>, objectQLProvider?: (environmentId?: string) => Promise<any | undefined>, emailServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingServiceProvider?: (environmentId?: string) => Promise<any | undefined>, reportsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, approvalsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingRulesServiceProvider?: (environmentId?: string) => Promise<any | undefined>, i18nServiceProvider?: (environmentId?: string) => Promise<any | undefined>, analyticsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, settingsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, serviceExistsProvider?: (name: string) => boolean, securityServiceProvider?: (environmentId?: string) => Promise<any | undefined>);
277
+ constructor(server: IHttpServer, protocol: RestProtocol, config?: RestServerConfig, kernelManager?: RestKernelManager, envRegistry?: RestEnvRegistry, defaultEnvironmentIdProvider?: () => string | undefined, authServiceProvider?: (environmentId?: string) => Promise<any | undefined>, objectQLProvider?: (environmentId?: string) => Promise<any | undefined>, emailServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingServiceProvider?: (environmentId?: string) => Promise<any | undefined>, reportsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, approvalsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingRulesServiceProvider?: (environmentId?: string) => Promise<any | undefined>, i18nServiceProvider?: (environmentId?: string) => Promise<any | undefined>, analyticsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, settingsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, serviceExistsProvider?: (name: string) => boolean, securityServiceProvider?: (environmentId?: string) => Promise<any | undefined>);
270
278
  /**
271
279
  * Resolve the protocol for a given request. When `environmentId` is present
272
280
  * and a KernelManager is wired, fetch the per-project kernel's
@@ -755,7 +763,7 @@ interface RestApiPluginConfig {
755
763
  *
756
764
  * Responsibilities:
757
765
  * 1. Consumes 'http.server' (or configured service)
758
- * 2. Consumes 'protocol' (ObjectStackProtocol)
766
+ * 2. Consumes 'protocol' (the `RestProtocol` slice — DataProtocol + MetadataProtocol, ADR-0076 D9)
759
767
  * 3. Instantiates RestServer to auto-generate routes
760
768
  */
761
769
  declare function createRestApiPlugin(config?: RestApiPluginConfig): Plugin;
@@ -787,6 +795,8 @@ interface ExportFieldMeta {
787
795
  reference?: string;
788
796
  /** Field on the referenced record to show as its label. */
789
797
  displayField?: string;
798
+ /** Field holds multiple values (an array), e.g. a `multiple: true` lookup. */
799
+ multiple?: boolean;
790
800
  /** Field is required — a value (or default) must exist on insert. */
791
801
  required?: boolean;
792
802
  /** Engine-owned column the client never supplies (never required of import). */
@@ -1027,6 +1037,12 @@ declare function runImport(opts: RunImportOptions): Promise<ImportRunSummary>;
1027
1037
  * - select / radio → an option *value*
1028
1038
  * - multiselect / checkboxes / tags → an array of option values
1029
1039
  * - lookup / master_detail / user / reference → a record id (resolved async)
1040
+ * - file / image → a file id / url (as-is)
1041
+ *
1042
+ * Any of the last four whose field is flagged `multiple: true` (per the spec,
1043
+ * `multiple` applies to select / lookup / file / image; `radio`/`user` share
1044
+ * their branch) instead store an **array** — the cell is split on the export
1045
+ * separator and each token coerced individually. See `isMultiValueField`.
1030
1046
  *
1031
1047
  * Contract: when a field carries no usable metadata the value passes through
1032
1048
  * untouched, so an import stays byte-identical to the pre-coercion behaviour.
@@ -1085,4 +1101,4 @@ declare function coerceRow(rawRow: Record<string, unknown>, metaMap: Map<string,
1085
1101
  errors: FieldCoerceError[];
1086
1102
  }>;
1087
1103
 
1088
- export { type CoerceContext, type ExportFieldMeta, type ImportAction, type ImportProgress, type ImportProtocolLike, type ImportRowResult, type ImportRunSummary, type ImportUndoLog, type PrepareImportResult, type PreparedImport, type RefResolver, type RestApiPluginConfig, RestServer, type RouteEntry, RouteGroupBuilder, RouteManager, type RunImportOptions, buildFieldMetaMap, coerceRow, createRestApiPlugin, isMetaEnvelope, parseCsvToRows, parseXlsxToRows, prepareImportRequest, runImport };
1104
+ export { type CoerceContext, type ExportFieldMeta, type ImportAction, type ImportProgress, type ImportProtocolLike, type ImportRowResult, type ImportRunSummary, type ImportUndoLog, type PrepareImportResult, type PreparedImport, type RefResolver, type RestApiPluginConfig, type RestProtocol, RestServer, type RouteEntry, RouteGroupBuilder, RouteManager, type RunImportOptions, buildFieldMetaMap, coerceRow, createRestApiPlugin, isMetaEnvelope, parseCsvToRows, parseXlsxToRows, prepareImportRequest, runImport };
package/dist/index.js CHANGED
@@ -1,5 +1,12 @@
1
1
  // src/rest-server.ts
2
- import { resolveAuthzContext, resolveLocalizationContext, isAuthGateAllowlisted } from "@objectstack/core";
2
+ import {
3
+ resolveAuthzContext,
4
+ resolveLocalizationContext,
5
+ isAuthGateAllowlisted,
6
+ shouldDenyAnonymous,
7
+ ANONYMOUS_DENY_BODY,
8
+ ANONYMOUS_DENY_STATUS
9
+ } from "@objectstack/core";
3
10
  import { isMcpServerEnabled } from "@objectstack/types";
4
11
 
5
12
  // src/route-manager.ts
@@ -212,6 +219,9 @@ var RouteGroupBuilder = class {
212
219
  }
213
220
  };
214
221
 
222
+ // src/rest-server.ts
223
+ import { PUBLIC_FORM_SERVER_MANAGED_FIELDS } from "@objectstack/spec/security";
224
+
215
225
  // src/export-format.ts
216
226
  function exportContentDisposition(objectName, label, ext, now = /* @__PURE__ */ new Date()) {
217
227
  const pad = (n) => String(n).padStart(2, "0");
@@ -261,6 +271,7 @@ function buildFieldMetaMap(schema) {
261
271
  options: Array.isArray(f.options) ? f.options : void 0,
262
272
  reference: typeof f.reference === "string" ? f.reference : void 0,
263
273
  displayField: typeof f.displayField === "string" ? f.displayField : void 0,
274
+ multiple: f.multiple === true,
264
275
  required: f.required === true,
265
276
  system: f.system === true,
266
277
  readonly: f.readonly === true,
@@ -382,6 +393,14 @@ var OPTION_TYPES2 = /* @__PURE__ */ new Set(["select", "radio"]);
382
393
  var MULTI_OPTION_TYPES2 = /* @__PURE__ */ new Set(["multiselect", "checkboxes", "tags"]);
383
394
  var NUMBER_TYPES = /* @__PURE__ */ new Set(["number", "currency", "percent", "rating", "slider"]);
384
395
  var BOOL_TYPES = /* @__PURE__ */ new Set(["boolean", "toggle"]);
396
+ var FILE_TYPES = /* @__PURE__ */ new Set(["file", "image"]);
397
+ var MULTI_CAPABLE_TYPES = /* @__PURE__ */ new Set(["select", "radio", "lookup", "user", "file", "image"]);
398
+ function isMultiValueField(meta) {
399
+ const t = meta?.type;
400
+ if (!t) return false;
401
+ if (MULTI_OPTION_TYPES2.has(t)) return true;
402
+ return MULTI_CAPABLE_TYPES.has(t) && meta?.multiple === true;
403
+ }
385
404
  function normalizeRefMatch(result) {
386
405
  if (result == null) return {};
387
406
  if (typeof result === "string") return result ? { id: result } : {};
@@ -499,7 +518,23 @@ async function coerceFieldValue(raw, meta, ctx) {
499
518
  if (d === void 0) return { error: { field, code: "invalid_date", message: `${field}: "${String(raw)}" is not a valid ${t}` } };
500
519
  return { value: d };
501
520
  }
502
- if (OPTION_TYPES2.has(t)) {
521
+ if (OPTION_TYPES2.has(t) || MULTI_OPTION_TYPES2.has(t)) {
522
+ if (isMultiValueField(meta)) {
523
+ const parts = splitMulti(raw);
524
+ const out = [];
525
+ for (const part of parts) {
526
+ const v2 = matchOption(part, meta?.options);
527
+ if (v2 === void 0) {
528
+ if (ctx.createMissingOptions) {
529
+ out.push(part);
530
+ continue;
531
+ }
532
+ return { error: { field, code: "invalid_option", message: `${field}: "${part}" is not a known option` } };
533
+ }
534
+ out.push(v2);
535
+ }
536
+ return { value: out };
537
+ }
503
538
  const v = matchOption(raw, meta?.options);
504
539
  if (v === void 0) {
505
540
  if (ctx.createMissingOptions) return { value: String(raw).trim() };
@@ -507,23 +542,23 @@ async function coerceFieldValue(raw, meta, ctx) {
507
542
  }
508
543
  return { value: v };
509
544
  }
510
- if (MULTI_OPTION_TYPES2.has(t)) {
511
- const parts = splitMulti(raw);
512
- const out = [];
513
- for (const part of parts) {
514
- const v = matchOption(part, meta?.options);
515
- if (v === void 0) {
516
- if (ctx.createMissingOptions) {
517
- out.push(part);
518
- continue;
519
- }
520
- return { error: { field, code: "invalid_option", message: `${field}: "${part}" is not a known option` } };
545
+ if (REFERENCE_TYPES2.has(t)) {
546
+ if (isMultiValueField(meta)) {
547
+ const tokens = splitMulti(raw);
548
+ if (!ctx.resolveRef || !meta.reference) return { value: tokens };
549
+ const out = [];
550
+ for (const token of tokens) {
551
+ const m = normalizeRefMatch(await ctx.resolveRef(meta.reference, token, meta));
552
+ if (m.ambiguous) {
553
+ return { error: { field, code: "reference_ambiguous", message: `${field}: "${token}" matches more than one ${meta.reference} \u2014 use a unique value or the record id` } };
554
+ }
555
+ if (m.id === void 0) {
556
+ return { error: { field, code: "reference_not_found", message: `${field}: no ${meta.reference} matches "${token}"` } };
557
+ }
558
+ out.push(m.id);
521
559
  }
522
- out.push(v);
560
+ return { value: out };
523
561
  }
524
- return { value: out };
525
- }
526
- if (REFERENCE_TYPES2.has(t)) {
527
562
  const display = String(raw).trim();
528
563
  if (!ctx.resolveRef || !meta?.reference) return { value: display };
529
564
  const match = normalizeRefMatch(await ctx.resolveRef(meta.reference, display, meta));
@@ -535,6 +570,9 @@ async function coerceFieldValue(raw, meta, ctx) {
535
570
  }
536
571
  return { value: match.id };
537
572
  }
573
+ if (FILE_TYPES.has(t) && isMultiValueField(meta)) {
574
+ return { value: splitMulti(raw) };
575
+ }
538
576
  return { value: trim && typeof raw === "string" ? raw.trim() : raw };
539
577
  }
540
578
  var REQUIRED_CHECK_SKIP = /* @__PURE__ */ new Set([
@@ -585,6 +623,10 @@ function toFailedResult(rowNo, err) {
585
623
  return { row: rowNo, ok: false, action: "failed", error: message, code };
586
624
  }
587
625
  var MAX_CREATE_BATCH_SIZE = 200;
626
+ var yieldToEventLoop = () => new Promise((resolve) => {
627
+ if (typeof setImmediate === "function") setImmediate(resolve);
628
+ else setTimeout(resolve, 0);
629
+ });
588
630
  function runImport(opts) {
589
631
  const {
590
632
  p,
@@ -803,8 +845,9 @@ function runImport(opts) {
803
845
  await flushPendingCreates();
804
846
  if (onProgress) await onProgress(snapshot(processed));
805
847
  }
806
- if (shouldCancel && processed < rows.length && processed % progressEvery === 0) {
807
- if (await shouldCancel()) {
848
+ if (processed < rows.length && processed % progressEvery === 0) {
849
+ await yieldToEventLoop();
850
+ if (shouldCancel && await shouldCancel()) {
808
851
  cancelled = true;
809
852
  break;
810
853
  }
@@ -1228,6 +1271,16 @@ function mapDataError(error, object) {
1228
1271
  }
1229
1272
  };
1230
1273
  }
1274
+ if (error?.code === "ATTACHMENT_PARENT_ACCESS" || error?.code === "ATTACHMENT_DELETE_DENIED") {
1275
+ return {
1276
+ status: 403,
1277
+ body: {
1278
+ error: error?.message ?? "Attachment access denied",
1279
+ code: error.code,
1280
+ ...error?.object || object ? { object: error?.object ?? object } : {}
1281
+ }
1282
+ };
1283
+ }
1231
1284
  if (error?.code === "PERMISSION_DENIED" || error?.name === "PermissionDeniedError" || typeof error?.message === "string" && error.message.startsWith("[Security] Access denied")) {
1232
1285
  return {
1233
1286
  status: 403,
@@ -1247,6 +1300,17 @@ function mapDataError(error, object) {
1247
1300
  }
1248
1301
  };
1249
1302
  }
1303
+ if (typeof error?.status === "number" && error.status >= 400 && error.status < 500) {
1304
+ const msg = typeof error?.message === "string" && error.message.length > 0 && error.message.length < 500 ? error.message : "Request failed";
1305
+ return {
1306
+ status: error.status,
1307
+ body: {
1308
+ error: msg,
1309
+ ...typeof error?.code === "string" && error.code ? { code: error.code } : {},
1310
+ ...object ? { object } : {}
1311
+ }
1312
+ };
1313
+ }
1250
1314
  const raw = String(error?.message ?? error ?? "");
1251
1315
  const lower = raw.toLowerCase();
1252
1316
  const sandboxWrapper = /^(?:hook|action) '[^']*' threw:\s*(.+)$/s.exec(raw);
@@ -1696,14 +1760,16 @@ var RestServer = class _RestServer {
1696
1760
  res.status(403).json({ error: { code: gate.code, message: gate.message } });
1697
1761
  return true;
1698
1762
  }
1699
- if (!this.config.api.requireAuth) return false;
1700
- if (context?.userId) return false;
1701
- if (req?.method === "OPTIONS") return false;
1702
- res.status(401).json({
1703
- error: "unauthenticated",
1704
- message: "Authentication is required to access this endpoint."
1705
- });
1706
- return true;
1763
+ if (shouldDenyAnonymous({
1764
+ requireAuth: this.config.api.requireAuth,
1765
+ userId: context?.userId,
1766
+ isSystem: context?.isSystem,
1767
+ method: req?.method
1768
+ })) {
1769
+ res.status(ANONYMOUS_DENY_STATUS).json(ANONYMOUS_DENY_BODY);
1770
+ return true;
1771
+ }
1772
+ return false;
1707
1773
  }
1708
1774
  /**
1709
1775
  * Enforce object-level API exposure (ObjectSchema `enable.apiEnabled` /
@@ -1921,6 +1987,10 @@ var RestServer = class _RestServer {
1921
1987
  permissions: authz.permissions,
1922
1988
  systemPermissions: authz.systemPermissions,
1923
1989
  ...authz.tabPermissions ? { tabPermissions: authz.tabPermissions } : {},
1990
+ // [ADR-0095 D2 / #2947] Carry the derived posture rung so the
1991
+ // enforcement side reads the SAME value the resolver computed,
1992
+ // instead of dropping it here (the boundary this issue closes).
1993
+ ...authz.posture ? { posture: authz.posture } : {},
1924
1994
  isSystem: false,
1925
1995
  org_user_ids: authz.org_user_ids,
1926
1996
  ...authGate ? { authGate } : {},
@@ -3450,7 +3520,7 @@ var RestServer = class _RestServer {
3450
3520
  res.json(result);
3451
3521
  } catch (error) {
3452
3522
  const mapped = mapDataError(error, req.params?.object);
3453
- if (mapped.status === 404 || mapped.status === 503 || mapped.status === 502) {
3523
+ if (isExpectedDataStatus(mapped.status) || mapped.body?.code === "UNSUPPORTED_QUERY_PARAM") {
3454
3524
  res.status(mapped.status).json(mapped.body);
3455
3525
  } else {
3456
3526
  logError("[REST] Unhandled error:", error);
@@ -3742,6 +3812,16 @@ var RestServer = class _RestServer {
3742
3812
  tags: ["data", "import"]
3743
3813
  }
3744
3814
  });
3815
+ const loadImportJob = async (p, jobId, environmentId, context) => {
3816
+ const r = await p.findData({
3817
+ object: IMPORT_JOB_OBJECT,
3818
+ query: { $filter: { id: jobId }, $top: 1 },
3819
+ ...environmentId ? { environmentId } : {},
3820
+ ...context ? { context } : {}
3821
+ });
3822
+ const rows = Array.isArray(r?.records) ? r.records : Array.isArray(r?.data) ? r.data : Array.isArray(r?.rows) ? r.rows : Array.isArray(r) ? r : [];
3823
+ return rows[0];
3824
+ };
3745
3825
  this.routeManager.register({
3746
3826
  method: "POST",
3747
3827
  path: `${dataPath}/:object/import/jobs`,
@@ -3807,6 +3887,11 @@ var RestServer = class _RestServer {
3807
3887
  };
3808
3888
  const captureUndo = !prepared.dryRun && prepared.rows.length <= IMPORT_JOB_UNDO_MAX_ROWS;
3809
3889
  void (async () => {
3890
+ if (this.cancelledImportJobs.has(jobId)) {
3891
+ this.cancelledImportJobs.delete(jobId);
3892
+ await patch({ status: "cancelled", completed_at: (/* @__PURE__ */ new Date()).toISOString() });
3893
+ return;
3894
+ }
3810
3895
  await patch({ status: "running", started_at: (/* @__PURE__ */ new Date()).toISOString() });
3811
3896
  try {
3812
3897
  const summary = await runImport({
@@ -3824,10 +3909,27 @@ var RestServer = class _RestServer {
3824
3909
  skipped_count: pr.skipped,
3825
3910
  error_count: pr.errors
3826
3911
  }),
3827
- shouldCancel: () => this.cancelledImportJobs.has(jobId)
3912
+ shouldCancel: async () => {
3913
+ if (this.cancelledImportJobs.has(jobId)) return true;
3914
+ try {
3915
+ const row = await loadImportJob(p, jobId, environmentId, context);
3916
+ return String(row?.status ?? "") === "cancelled";
3917
+ } catch {
3918
+ return false;
3919
+ }
3920
+ }
3828
3921
  });
3922
+ let finalStatus = summary.cancelled ? "cancelled" : "succeeded";
3923
+ if (finalStatus === "succeeded" && this.cancelledImportJobs.has(jobId)) finalStatus = "cancelled";
3924
+ if (finalStatus === "succeeded") {
3925
+ try {
3926
+ const row = await loadImportJob(p, jobId, environmentId, context);
3927
+ if (String(row?.status ?? "") === "cancelled") finalStatus = "cancelled";
3928
+ } catch {
3929
+ }
3930
+ }
3829
3931
  await patch({
3830
- status: summary.cancelled ? "cancelled" : "succeeded",
3932
+ status: finalStatus,
3831
3933
  processed_rows: summary.processed,
3832
3934
  created_count: summary.created,
3833
3935
  updated_count: summary.updated,
@@ -3857,16 +3959,6 @@ var RestServer = class _RestServer {
3857
3959
  tags: ["data", "import"]
3858
3960
  }
3859
3961
  });
3860
- const loadImportJob = async (p, jobId, environmentId, context) => {
3861
- const r = await p.findData({
3862
- object: IMPORT_JOB_OBJECT,
3863
- query: { $filter: { id: jobId }, $top: 1 },
3864
- ...environmentId ? { environmentId } : {},
3865
- ...context ? { context } : {}
3866
- });
3867
- const rows = Array.isArray(r?.records) ? r.records : Array.isArray(r?.data) ? r.data : Array.isArray(r?.rows) ? r.rows : Array.isArray(r) ? r : [];
3868
- return rows[0];
3869
- };
3870
3962
  this.routeManager.register({
3871
3963
  method: "POST",
3872
3964
  path: `${dataPath}/import/jobs/:jobId/cancel`,
@@ -4450,6 +4542,7 @@ var RestServer = class _RestServer {
4450
4542
  }
4451
4543
  const fields = {};
4452
4544
  for (const [name, def] of Object.entries(obj.fields)) {
4545
+ if (PUBLIC_FORM_SERVER_MANAGED_FIELDS.has(name)) continue;
4453
4546
  if (allowed.size === 0 || allowed.has(name)) {
4454
4547
  fields[name] = def;
4455
4548
  }
@@ -4474,6 +4567,7 @@ var RestServer = class _RestServer {
4474
4567
  const safeForm = (() => {
4475
4568
  if (!match.form || !Array.isArray(match.form.sections)) return match.form;
4476
4569
  const allow = (name, cfg) => {
4570
+ if (PUBLIC_FORM_SERVER_MANAGED_FIELDS.has(name)) return false;
4477
4571
  const def = objectSchema?.fields?.[name];
4478
4572
  const t = def?.type;
4479
4573
  if (t !== "lookup" && t !== "master_detail" && t !== "user") return true;
@@ -4539,12 +4633,10 @@ var RestServer = class _RestServer {
4539
4633
  }
4540
4634
  const rawBody = req.body && typeof req.body === "object" ? req.body : {};
4541
4635
  const filteredData = {};
4542
- if (allowedFields.size > 0) {
4543
- for (const [k, v] of Object.entries(rawBody)) {
4544
- if (allowedFields.has(k)) filteredData[k] = v;
4545
- }
4546
- } else {
4547
- Object.assign(filteredData, rawBody);
4636
+ for (const [k, v] of Object.entries(rawBody)) {
4637
+ if (PUBLIC_FORM_SERVER_MANAGED_FIELDS.has(k)) continue;
4638
+ if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
4639
+ if (allowedFields.size === 0 || allowedFields.has(k)) filteredData[k] = v;
4548
4640
  }
4549
4641
  const context = {
4550
4642
  publicFormGrant: { object: match.object },
@@ -4593,15 +4685,17 @@ var RestServer = class _RestServer {
4593
4685
  return;
4594
4686
  }
4595
4687
  let fieldCfg = null;
4596
- for (const sec of match.form?.sections ?? []) {
4597
- for (const f of sec?.fields ?? []) {
4598
- const name = typeof f === "string" ? f : f?.field;
4599
- if (name === fieldName) {
4600
- fieldCfg = typeof f === "string" ? {} : f;
4601
- break;
4688
+ if (!PUBLIC_FORM_SERVER_MANAGED_FIELDS.has(fieldName)) {
4689
+ for (const sec of match.form?.sections ?? []) {
4690
+ for (const f of sec?.fields ?? []) {
4691
+ const name = typeof f === "string" ? f : f?.field;
4692
+ if (name === fieldName) {
4693
+ fieldCfg = typeof f === "string" ? {} : f;
4694
+ break;
4695
+ }
4602
4696
  }
4697
+ if (fieldCfg) break;
4603
4698
  }
4604
- if (fieldCfg) break;
4605
4699
  }
4606
4700
  const picker = fieldCfg?.publicPicker;
4607
4701
  if (!picker) {