@objectstack/rest 12.6.0 → 14.3.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.d.cts CHANGED
@@ -251,6 +251,10 @@ declare class RestServer {
251
251
  private i18nServiceProvider?;
252
252
  private analyticsServiceProvider?;
253
253
  private settingsServiceProvider?;
254
+ /** [ADR-0090] `security` service resolver — used by the
255
+ * /security/suggested-bindings (D5/D9) and /security/explain (D6)
256
+ * routes (plugin-security). */
257
+ private securityServiceProvider?;
254
258
  /** Sync probe: is a kernel service registered? Single-env path for nav
255
259
  * capability gates (ADR-0057 D10) — resolveExecCtx sets no kernel in
256
260
  * single-kernel deployments, so this prevents the gate failing open. */
@@ -262,7 +266,7 @@ declare class RestServer {
262
266
  * the durable source of truth a restarted/other node reads.
263
267
  */
264
268
  private readonly cancelledImportJobs;
265
- 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);
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>);
266
270
  /**
267
271
  * Resolve the protocol for a given request. When `environmentId` is present
268
272
  * and a KernelManager is wired, fetch the per-project kernel's
@@ -350,6 +354,24 @@ declare class RestServer {
350
354
  * to the protocol layer (the SecurityPlugin treats undefined as anon).
351
355
  */
352
356
  private resolveExecCtx;
357
+ /**
358
+ * [ADR-0046 §6.7] The audience-evaluation view of the caller for book/doc
359
+ * gating. `permissionSets` resolves through the security service's
360
+ * `resolvePermissionSetNames` — the SAME resolution as data-plane
361
+ * enforcement (positions expanded, additive baseline), so the docs gate
362
+ * can never drift from it. `permissionSets` stays undefined when the
363
+ * service is absent or resolution fails; `audienceAllows` then DENIES
364
+ * permission-set-gated audiences (fail closed, ADR-0049). Resolution is
365
+ * skipped unless `needPermissionSets` — callers pass true only when a
366
+ * `{ permissionSet }` audience is actually in play.
367
+ */
368
+ private resolveAudienceCaller;
369
+ /** Whether any of these books carries a `{ permissionSet }` audience. */
370
+ private static anyPermissionSetAudience;
371
+ /** Coerce a getMetaItems result (array | {items}) into an array. */
372
+ private static metaItemsArray;
373
+ /** Fetch every book of the environment, shaped for the audience resolver. */
374
+ private fetchAudienceBooks;
353
375
  /** Heavy path behind `resolveExecCtx` — resolve identity + RBAC/RLS + localization. */
354
376
  private computeExecCtx;
355
377
  /**
@@ -593,6 +615,26 @@ declare class RestServer {
593
615
  * `@objectstack/service-analytics` fails cleanly.
594
616
  */
595
617
  private registerAnalyticsEndpoints;
618
+ /**
619
+ * [ADR-0090 D6] Access-explanation endpoint — the REST face of the
620
+ * explain engine (framework#2696).
621
+ *
622
+ * GET {basePath}/security/explain?object=…&operation=…&userId=…
623
+ * POST {basePath}/security/explain body: { object, operation, userId? }
624
+ *
625
+ * Delegates to the security service's `explain(request, callerContext)`
626
+ * (`SecurityPlugin.explainAccessForCaller`) — the same code paths the
627
+ * enforcement middleware runs, so the report is explained by
628
+ * construction. Caller authorization lives in the SERVICE, not here:
629
+ * explaining ANOTHER user requires `manage_users` or a delegated
630
+ * `adminScope` covering that user (D12); the service's
631
+ * `PermissionDeniedError` maps to 403. The route itself only insists on
632
+ * an authenticated caller (an access report is sensitive even about
633
+ * oneself, and the anonymous `guest` posture is not this endpoint's
634
+ * business) and returns 501 when no security service exposing `explain`
635
+ * is mounted (a deployment without `@objectstack/plugin-security`).
636
+ */
637
+ private registerSecurityExplainEndpoints;
596
638
  private registerSharingEndpoints;
597
639
  /**
598
640
  * Register sharing-rule endpoints (M10.17). Mirrors the existing
@@ -607,6 +649,25 @@ declare class RestServer {
607
649
  * Returns 501 when no sharing-rule service is configured.
608
650
  */
609
651
  private registerSharingRuleEndpoints;
652
+ /**
653
+ * Register the security admin endpoints (ADR-0090 D5/D9) — suggested
654
+ * audience bindings. A package permission set declaring `isDefault: true`
655
+ * is an install-time SUGGESTION to bind it to the `everyone` position;
656
+ * these routes surface pending suggestions and let a tenant admin resolve
657
+ * them. The `security` service (plugin-security) does the real gating:
658
+ * tenant-admin pre-check on all three, and confirm writes the binding
659
+ * with the caller's execution context so the audience-anchor and
660
+ * delegated-admin gates enforce it — never auto-bound, never system.
661
+ *
662
+ * GET {basePath}/security/suggested-bindings?status=&packageId=
663
+ * POST {basePath}/security/suggested-bindings/:id/confirm
664
+ * POST {basePath}/security/suggested-bindings/:id/dismiss
665
+ *
666
+ * Routes return 501 when the `security` service is not registered
667
+ * (deployment without plugin-security). Typed service errors carry their
668
+ * HTTP status (403 permission / 404 not found / 409 state).
669
+ */
670
+ private registerSecurityEndpoints;
610
671
  /**
611
672
  * Register saved-report + scheduled-digest endpoints (M11.C16).
612
673
  *
@@ -682,4 +743,324 @@ interface RestApiPluginConfig {
682
743
  */
683
744
  declare function createRestApiPlugin(config?: RestApiPluginConfig): Plugin;
684
745
 
685
- export { type RestApiPluginConfig, RestServer, type RouteEntry, RouteGroupBuilder, RouteManager, createRestApiPlugin };
746
+ /**
747
+ * Type-aware value formatting for the streaming data export route
748
+ * (`GET /data/:object/export`).
749
+ *
750
+ * The raw rows returned by `findData` carry *storage* values: lookup / user
751
+ * fields hold ids (or, when `$expand`-ed, nested records), select fields hold
752
+ * option codes, booleans hold true/false, dates hold ISO strings. None of those
753
+ * read well in a spreadsheet. These helpers turn each value into a human
754
+ * readable cell using the object's field metadata.
755
+ *
756
+ * Contract: when no field metadata is available (schema lookup failed or carried
757
+ * no fields) every helper is a pass-through, so the export stays byte-for-byte
758
+ * identical to the un-formatted behaviour.
759
+ */
760
+ interface ExportFieldMeta {
761
+ name: string;
762
+ type?: string;
763
+ label?: string;
764
+ options?: Array<{
765
+ label?: string;
766
+ value?: unknown;
767
+ color?: string;
768
+ }>;
769
+ /** Target object for lookup / master_detail / user fields. */
770
+ reference?: string;
771
+ /** Field on the referenced record to show as its label. */
772
+ displayField?: string;
773
+ }
774
+ /**
775
+ * Build a field-name → metadata map from an object schema (best-effort).
776
+ *
777
+ * Accepts both shapes `fields` appears in across the stack: the runtime
778
+ * `ObjectSchema.fields` is a `Record<fieldName, FieldDefinition>` object map
779
+ * (the form served by the engine registry / `getMetaItem`), while some callers
780
+ * and fixtures hand back a plain `FieldDefinition[]` array. A field's name is
781
+ * taken from its own `name`, falling back to the map key.
782
+ */
783
+ declare function buildFieldMetaMap(schema: unknown): Map<string, ExportFieldMeta>;
784
+
785
+ /**
786
+ * Bulk-import request parsing — extracted verbatim from rest-server.ts
787
+ * (#2766 V2) so consumers outside the generic `/data/:object/import` routes
788
+ * (the identity import endpoint in plugin-auth) can accept byte-identical
789
+ * payloads (rows[]/csv/xlsxBase64, mapping in either shape, writeMode +
790
+ * matchFields) without re-implementing the parsers.
791
+ */
792
+
793
+ /**
794
+ * Detect the `getMetaItem` response envelope (`{ type, name, item, lock, … }`)
795
+ * whose translatable metadata document is nested at `.item`. The cached read
796
+ * path and `getMetaItems` element shape hand back the already-unwrapped
797
+ * document instead, so translation helpers must distinguish the two: an
798
+ * envelope carries a nested `item` object alongside its own `type`/`name`,
799
+ * which a bare metadata document never does.
800
+ */
801
+ declare function isMetaEnvelope(value: any): boolean;
802
+ /**
803
+ * Minimal RFC-4180-style CSV parser used by the bulk-import endpoint
804
+ * (M10.9). Handles quoted fields (including embedded quotes via "" and
805
+ * embedded commas/newlines) and both CRLF and LF line endings.
806
+ *
807
+ * The first non-empty line is treated as the header row. Header names
808
+ * can be re-mapped to canonical field names via the optional `mapping`
809
+ * argument (e.g. `{ "First Name": "first_name" }`); unmapped headers
810
+ * pass through unchanged. Empty cells become empty strings.
811
+ *
812
+ * Kept dependency-free so REST stays runtime-portable (Hono / Express
813
+ * adapters both consume this without pulling a CSV lib transitively).
814
+ */
815
+ declare function parseCsvToRows(csv: string, mapping?: Record<string, string>): Array<Record<string, any>>;
816
+ /**
817
+ * Parse an .xlsx workbook (raw bytes) into row objects, mirroring
818
+ * {@link parseCsvToRows}: first non-empty row is the header, each subsequent row
819
+ * becomes `{ header→cell }` with the optional `mapping` renaming columns. Reads
820
+ * the named/indexed `sheet` when given, else the first worksheet. Dynamically
821
+ * imports ExcelJS (already a dependency of the export path) so CSV/JSON imports
822
+ * don't pay for it.
823
+ */
824
+ declare function parseXlsxToRows(buffer: Buffer | ArrayBuffer, mapping?: Record<string, string>, sheet?: string | number): Promise<Array<Record<string, any>>>;
825
+ /** Everything the import runner needs, parsed & validated from a request body. */
826
+ interface PreparedImport {
827
+ rows: Array<Record<string, any>>;
828
+ metaMap: Map<string, ExportFieldMeta>;
829
+ writeMode: 'insert' | 'update' | 'upsert';
830
+ matchFields: string[];
831
+ dryRun: boolean;
832
+ runAutomations: boolean;
833
+ trimWhitespace: boolean;
834
+ nullValues?: string[];
835
+ createMissingOptions: boolean;
836
+ skipBlankMatchKey: boolean;
837
+ }
838
+ type PrepareImportResult = {
839
+ ok: true;
840
+ prepared: PreparedImport;
841
+ } | {
842
+ ok: false;
843
+ status: number;
844
+ code: string;
845
+ error: string;
846
+ };
847
+ /**
848
+ * Parse & validate a bulk-import request body into a {@link PreparedImport}.
849
+ *
850
+ * Shared by the synchronous `POST /data/:object/import` route and the async
851
+ * import-job create route so both accept byte-identical payloads (writeMode +
852
+ * matchFields, mapping in either shape, rows[]/csv/xlsxBase64) and resolve the
853
+ * same field metadata. The only knob that differs is `maxRows` (5k sync vs
854
+ * 50k async). Returns a discriminated result; the caller maps `!ok` to an HTTP
855
+ * error using the returned status/code/error.
856
+ */
857
+ declare function prepareImportRequest(body: any, opts: {
858
+ p: any;
859
+ objectName: string;
860
+ environmentId?: string;
861
+ maxRows: number;
862
+ }): Promise<PrepareImportResult>;
863
+
864
+ /**
865
+ * import-runner — the shared row-processing core for bulk import.
866
+ *
867
+ * Both the synchronous `POST /data/:object/import` route and the asynchronous
868
+ * import-job worker feed rows through {@link runImport}. Extracting the loop
869
+ * keeps the two paths byte-for-byte identical in coercion, upsert matching, and
870
+ * per-row reporting — the async worker only adds progress persistence and
871
+ * cancellation on top.
872
+ *
873
+ * Rows resolved to a CREATE are batched through `p.createManyData` (the
874
+ * engine's array-form `insert()` — one round-trip per batch, with transient
875
+ * retry and per-row degradation on a logical/validation failure) instead of
876
+ * one `p.createData` call per row — see framework#2678. A protocol that
877
+ * doesn't implement `createManyData` falls back to the original per-row
878
+ * `createData` path unchanged.
879
+ */
880
+ type ImportAction = 'created' | 'updated' | 'skipped' | 'failed';
881
+ interface ImportRowResult {
882
+ row: number;
883
+ ok: boolean;
884
+ action: ImportAction;
885
+ id?: string;
886
+ field?: string;
887
+ error?: string;
888
+ code?: string;
889
+ }
890
+ /** Running tallies handed to {@link RunImportOptions.onProgress}. */
891
+ interface ImportProgress {
892
+ processed: number;
893
+ total: number;
894
+ created: number;
895
+ updated: number;
896
+ skipped: number;
897
+ errors: number;
898
+ }
899
+ /**
900
+ * Records exactly what a non-dry-run import changed, so the job can be undone:
901
+ * created records are deleted, and updated records have the touched fields
902
+ * restored to their pre-import values. Only the fields the import wrote are
903
+ * captured (keyed to `before`), keeping the log precise and bounded.
904
+ */
905
+ interface ImportUndoLog {
906
+ /** Ids of records this import created (delete to undo). */
907
+ created: string[];
908
+ /** Per updated record: the touched fields' values *before* the import. */
909
+ updated: Array<{
910
+ id: string;
911
+ before: Record<string, any>;
912
+ }>;
913
+ }
914
+ interface ImportRunSummary extends ImportProgress {
915
+ ok: number;
916
+ results: ImportRowResult[];
917
+ cancelled: boolean;
918
+ /** Present only when `captureUndo` was set — the reversal instructions. */
919
+ undoLog?: ImportUndoLog;
920
+ }
921
+ /** Minimal protocol surface the runner needs (find / create / update). */
922
+ interface ImportProtocolLike {
923
+ findData(args: any): Promise<any>;
924
+ createData(args: any): Promise<any>;
925
+ updateData(args: any): Promise<any>;
926
+ /**
927
+ * Optional bulk-create primitive. When present, `runImport` batches
928
+ * CREATE-resolved rows through it instead of one `createData` call per
929
+ * row — see framework#2678. Must resolve to `{ records: any[] }` with one
930
+ * record per input row, in the same order.
931
+ */
932
+ createManyData?(args: {
933
+ object: string;
934
+ records: any[];
935
+ context?: any;
936
+ environmentId?: string;
937
+ }): Promise<{
938
+ records: any[];
939
+ }>;
940
+ }
941
+ interface RunImportOptions {
942
+ /** Protocol/engine to read & write through. */
943
+ p: ImportProtocolLike;
944
+ objectName: string;
945
+ environmentId?: string;
946
+ /** Exec context threaded onto reads and (with automation toggle) writes. */
947
+ context?: any;
948
+ /** Already-mapped rows (source columns renamed to target fields). */
949
+ rows: Array<Record<string, any>>;
950
+ /** Field metadata for value coercion (name→id lookups, select codes, …). */
951
+ metaMap: Map<string, ExportFieldMeta>;
952
+ writeMode: 'insert' | 'update' | 'upsert';
953
+ matchFields: string[];
954
+ dryRun: boolean;
955
+ runAutomations: boolean;
956
+ trimWhitespace: boolean;
957
+ nullValues?: string[];
958
+ createMissingOptions: boolean;
959
+ skipBlankMatchKey: boolean;
960
+ /**
961
+ * Progress callback, invoked every {@link RunImportOptions.progressEvery}
962
+ * processed rows and once at the end. May be async; the runner awaits it so a
963
+ * DB write of progress completes before the next chunk.
964
+ */
965
+ onProgress?: (p: ImportProgress) => void | Promise<void>;
966
+ /**
967
+ * Rows between onProgress calls (default 200). Also the flush boundary for
968
+ * buffered creates — a batch never grows past this before being written,
969
+ * so progress numbers stay accurate at every reported checkpoint.
970
+ */
971
+ progressEvery?: number;
972
+ /**
973
+ * Cooperative cancellation. Checked at each progress boundary; when it returns
974
+ * truthy the runner stops and returns `cancelled: true` with partial results.
975
+ */
976
+ shouldCancel?: () => boolean | Promise<boolean>;
977
+ /**
978
+ * When true (and not a dry run), accumulate an {@link ImportUndoLog} so the
979
+ * import can be reverted later. Callers gate this on row count to bound the
980
+ * stored snapshot size.
981
+ */
982
+ captureUndo?: boolean;
983
+ }
984
+ declare function runImport(opts: RunImportOptions): Promise<ImportRunSummary>;
985
+
986
+ /**
987
+ * Type-aware value *coercion* for the bulk-import route
988
+ * (`POST /data/:object/import`).
989
+ *
990
+ * This is the inverse of `export-format.ts`. A spreadsheet / CSV cell arrives as
991
+ * a raw string (or, for JSON payloads, an arbitrary primitive); the storage
992
+ * layer, on the other hand, expects *storage* values — booleans as real
993
+ * booleans, numbers as numbers, dates as ISO strings, select fields as their
994
+ * option **code** (not the human label), and lookup / user fields as the
995
+ * referenced record **id** (not its name). The engine deliberately does not
996
+ * coerce for storage (see `record-validator.ts`, which coerces only to *check*
997
+ * a value and then discards the coerced form), so import has to do it here.
998
+ *
999
+ * The accepted storage shapes below are dictated by what
1000
+ * `validateFieldValue` in `packages/objectql` will accept:
1001
+ * - number / currency / percent / rating / slider → a finite `number`
1002
+ * - boolean / toggle → a real `boolean`
1003
+ * - date / datetime → an ISO-8601 string
1004
+ * - time → `HH:MM` / `HH:MM:SS`
1005
+ * - select / radio → an option *value*
1006
+ * - multiselect / checkboxes / tags → an array of option values
1007
+ * - lookup / master_detail / user / reference → a record id (resolved async)
1008
+ *
1009
+ * Contract: when a field carries no usable metadata the value passes through
1010
+ * untouched, so an import stays byte-identical to the pre-coercion behaviour.
1011
+ */
1012
+
1013
+ /**
1014
+ * Structured outcome of a reference lookup. `id` set → a single record matched.
1015
+ * `ambiguous` → the display value matched more than one record, so linking any
1016
+ * one of them would be a guess the importer refuses to make. `matchedField`
1017
+ * names the field the match came from (for diagnostics). An empty object means
1018
+ * nothing matched. A bare `string | undefined` is still accepted from legacy
1019
+ * resolvers and normalised to this shape.
1020
+ */
1021
+ interface RefMatch {
1022
+ id?: string;
1023
+ ambiguous?: boolean;
1024
+ matchedField?: string;
1025
+ }
1026
+ /**
1027
+ * Resolve a reference field's display value (a name / email / id typed by the
1028
+ * user) to the referenced record's id. Return `undefined` / `{}` when nothing
1029
+ * matches (caller surfaces "not found"), a bare id string / `{ id }` on a unique
1030
+ * hit, or `{ ambiguous: true }` when several records share the value. Legacy
1031
+ * resolvers that return `string | undefined` keep working. Implementations are
1032
+ * expected to cache — the same name shows up on many rows.
1033
+ */
1034
+ type RefResolver = (referenceObject: string, displayValue: string, meta: ExportFieldMeta) => Promise<string | undefined | RefMatch>;
1035
+ interface CoerceContext {
1036
+ /** Trim leading/trailing whitespace from string-ish cells (default true). */
1037
+ trimWhitespace?: boolean;
1038
+ /** Extra strings (besides `''`) treated as null, e.g. `['N/A', 'null']`. */
1039
+ nullValues?: string[];
1040
+ /**
1041
+ * When a select/multiselect cell matches no known option, keep the raw value
1042
+ * instead of failing. Note: the engine still validates option membership, so
1043
+ * this only helps when the option is (or will be) present in the schema.
1044
+ */
1045
+ createMissingOptions?: boolean;
1046
+ /** Async reference resolver (name/email/id → record id). Optional. */
1047
+ resolveRef?: RefResolver;
1048
+ }
1049
+ /** A per-field coercion failure, shaped like the engine's validation errors. */
1050
+ interface FieldCoerceError {
1051
+ field: string;
1052
+ code: string;
1053
+ message: string;
1054
+ }
1055
+ /**
1056
+ * Coerce a whole raw row into a storage-ready record. Unknown columns (no
1057
+ * matching field metadata) pass through untouched so ad-hoc / schemaless
1058
+ * objects still import. Collects every field error rather than stopping at the
1059
+ * first, so a UI can show all problems in a row at once.
1060
+ */
1061
+ declare function coerceRow(rawRow: Record<string, unknown>, metaMap: Map<string, ExportFieldMeta>, ctx: CoerceContext): Promise<{
1062
+ data: Record<string, unknown>;
1063
+ errors: FieldCoerceError[];
1064
+ }>;
1065
+
1066
+ 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 };