@objectstack/rest 9.7.0 → 9.8.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
@@ -299,6 +299,25 @@ declare class RestServer {
299
299
  * requests (they're internal-only), so they cannot bypass this gate.
300
300
  */
301
301
  private enforceAuth;
302
+ /**
303
+ * Enforce object-level API exposure (ObjectSchema `enable.apiEnabled` /
304
+ * `enable.apiMethods`) on the REST data surface — the *external* API boundary
305
+ * only. Internal callers (hooks, flows, raw objectql) are unaffected, which is
306
+ * the point: `apiEnabled` controls automatic API exposure, not data access.
307
+ *
308
+ * - `enable.apiEnabled === false` → object hidden from the API (404, so its
309
+ * existence isn't revealed).
310
+ * - `enable.apiMethods` (non-empty whitelist) → unlisted operations rejected (405).
311
+ *
312
+ * Default-allow: objects with no `enable` block (or `apiEnabled` unset/true and
313
+ * no `apiMethods` whitelist) behave exactly as before — no regression. Unknown
314
+ * objects fall through to the normal 404 path. A metadata-read failure does not
315
+ * block (the data call itself needs the same metadata and will surface the
316
+ * error). Returns `true` when the request was blocked (response already sent).
317
+ *
318
+ * See ADR-0049 (#1889): shipping a non-enforcing `apiEnabled` is false security.
319
+ */
320
+ private enforceApiAccess;
302
321
  /**
303
322
  * Resolve the request's execution context (RBAC/RLS/FLS) by looking up
304
323
  * the better-auth session via the project's `auth` service. Returns
package/dist/index.d.ts CHANGED
@@ -299,6 +299,25 @@ declare class RestServer {
299
299
  * requests (they're internal-only), so they cannot bypass this gate.
300
300
  */
301
301
  private enforceAuth;
302
+ /**
303
+ * Enforce object-level API exposure (ObjectSchema `enable.apiEnabled` /
304
+ * `enable.apiMethods`) on the REST data surface — the *external* API boundary
305
+ * only. Internal callers (hooks, flows, raw objectql) are unaffected, which is
306
+ * the point: `apiEnabled` controls automatic API exposure, not data access.
307
+ *
308
+ * - `enable.apiEnabled === false` → object hidden from the API (404, so its
309
+ * existence isn't revealed).
310
+ * - `enable.apiMethods` (non-empty whitelist) → unlisted operations rejected (405).
311
+ *
312
+ * Default-allow: objects with no `enable` block (or `apiEnabled` unset/true and
313
+ * no `apiMethods` whitelist) behave exactly as before — no regression. Unknown
314
+ * objects fall through to the normal 404 path. A metadata-read failure does not
315
+ * block (the data call itself needs the same metadata and will surface the
316
+ * error). Returns `true` when the request was blocked (response already sent).
317
+ *
318
+ * See ADR-0049 (#1889): shipping a non-enforcing `apiEnabled` is false security.
319
+ */
320
+ private enforceApiAccess;
302
321
  /**
303
322
  * Resolve the request's execution context (RBAC/RLS/FLS) by looking up
304
323
  * the better-auth session via the project's `auth` service. Returns
package/dist/index.js CHANGED
@@ -627,6 +627,60 @@ var RestServer = class {
627
627
  });
628
628
  return true;
629
629
  }
630
+ /**
631
+ * Enforce object-level API exposure (ObjectSchema `enable.apiEnabled` /
632
+ * `enable.apiMethods`) on the REST data surface — the *external* API boundary
633
+ * only. Internal callers (hooks, flows, raw objectql) are unaffected, which is
634
+ * the point: `apiEnabled` controls automatic API exposure, not data access.
635
+ *
636
+ * - `enable.apiEnabled === false` → object hidden from the API (404, so its
637
+ * existence isn't revealed).
638
+ * - `enable.apiMethods` (non-empty whitelist) → unlisted operations rejected (405).
639
+ *
640
+ * Default-allow: objects with no `enable` block (or `apiEnabled` unset/true and
641
+ * no `apiMethods` whitelist) behave exactly as before — no regression. Unknown
642
+ * objects fall through to the normal 404 path. A metadata-read failure does not
643
+ * block (the data call itself needs the same metadata and will surface the
644
+ * error). Returns `true` when the request was blocked (response already sent).
645
+ *
646
+ * See ADR-0049 (#1889): shipping a non-enforcing `apiEnabled` is false security.
647
+ */
648
+ async enforceApiAccess(req, res, p, environmentId, operation) {
649
+ const objectName = req?.params?.object;
650
+ if (!objectName) return false;
651
+ let enable;
652
+ try {
653
+ const r = await p.getMetaItems?.({
654
+ type: "object",
655
+ ...environmentId ? { environmentId } : {}
656
+ });
657
+ const items = Array.isArray(r?.items) ? r.items : Array.isArray(r) ? r : [];
658
+ const obj = items.find((o) => o?.name === objectName);
659
+ if (!obj) return false;
660
+ enable = obj.enable;
661
+ } catch {
662
+ return false;
663
+ }
664
+ if (!enable) return false;
665
+ if (enable.apiEnabled === false) {
666
+ res.status(404).json({
667
+ error: `Object '${objectName}' is not exposed via the API`,
668
+ code: "OBJECT_API_DISABLED",
669
+ object: objectName
670
+ });
671
+ return true;
672
+ }
673
+ if (Array.isArray(enable.apiMethods) && enable.apiMethods.length > 0 && !enable.apiMethods.includes(operation)) {
674
+ res.status(405).json({
675
+ error: `API operation '${operation}' is not allowed on object '${objectName}'`,
676
+ code: "OBJECT_API_METHOD_NOT_ALLOWED",
677
+ object: objectName,
678
+ allowed: enable.apiMethods
679
+ });
680
+ return true;
681
+ }
682
+ return false;
683
+ }
630
684
  /**
631
685
  * Resolve the request's execution context (RBAC/RLS/FLS) by looking up
632
686
  * the better-auth session via the project's `auth` service. Returns
@@ -2078,6 +2132,7 @@ var RestServer = class {
2078
2132
  const p = await this.resolveProtocol(environmentId, req);
2079
2133
  const context = await this.resolveExecCtx(environmentId, req);
2080
2134
  if (this.enforceAuth(req, res, context)) return;
2135
+ if (await this.enforceApiAccess(req, res, p, environmentId, "list")) return;
2081
2136
  const result = await p.findData({
2082
2137
  object: req.params.object,
2083
2138
  query: req.query,
@@ -2112,6 +2167,7 @@ var RestServer = class {
2112
2167
  const { select, expand } = req.query || {};
2113
2168
  const context = await this.resolveExecCtx(environmentId, req);
2114
2169
  if (this.enforceAuth(req, res, context)) return;
2170
+ if (await this.enforceApiAccess(req, res, p, environmentId, "get")) return;
2115
2171
  const result = await p.getData({
2116
2172
  object: req.params.object,
2117
2173
  id: req.params.id,
@@ -2143,6 +2199,7 @@ var RestServer = class {
2143
2199
  const p = await this.resolveProtocol(environmentId, req);
2144
2200
  const context = await this.resolveExecCtx(environmentId, req);
2145
2201
  if (this.enforceAuth(req, res, context)) return;
2202
+ if (await this.enforceApiAccess(req, res, p, environmentId, "create")) return;
2146
2203
  const result = await p.createData({
2147
2204
  object: req.params.object,
2148
2205
  data: req.body,
@@ -2172,6 +2229,7 @@ var RestServer = class {
2172
2229
  const p = await this.resolveProtocol(environmentId, req);
2173
2230
  const context = await this.resolveExecCtx(environmentId, req);
2174
2231
  if (this.enforceAuth(req, res, context)) return;
2232
+ if (await this.enforceApiAccess(req, res, p, environmentId, "list")) return;
2175
2233
  const result = await p.findData({
2176
2234
  object: req.params.object,
2177
2235
  query: req.body || {},
@@ -2209,6 +2267,7 @@ var RestServer = class {
2209
2267
  const { expectedVersion: _drop, ...rest } = data;
2210
2268
  data = rest;
2211
2269
  }
2270
+ if (await this.enforceApiAccess(req, res, p, environmentId, "update")) return;
2212
2271
  const result = await p.updateData({
2213
2272
  object: req.params.object,
2214
2273
  id: req.params.id,
@@ -2243,6 +2302,7 @@ var RestServer = class {
2243
2302
  const ifMatchHeader = req.headers?.["if-match"] ?? req.headers?.["If-Match"];
2244
2303
  const queryVersion = req.query && typeof req.query === "object" ? req.query.expectedVersion : void 0;
2245
2304
  const expectedVersion = queryVersion ?? ifMatchHeader;
2305
+ if (await this.enforceApiAccess(req, res, p, environmentId, "delete")) return;
2246
2306
  const result = await p.deleteData({
2247
2307
  object: req.params.object,
2248
2308
  id: req.params.id,
@@ -2311,6 +2371,42 @@ var RestServer = class {
2311
2371
  tags: ["data", "lead"]
2312
2372
  }
2313
2373
  });
2374
+ this.routeManager.register({
2375
+ method: "POST",
2376
+ path: `${dataPath}/:object/:id/clone`,
2377
+ handler: async (req, res) => {
2378
+ try {
2379
+ const environmentId = isScoped ? req.params?.environmentId : void 0;
2380
+ const p = await this.resolveProtocol(environmentId, req);
2381
+ const context = await this.resolveExecCtx(environmentId, req);
2382
+ if (this.enforceAuth(req, res, context)) return;
2383
+ if (await this.enforceApiAccess(req, res, p, environmentId, "create")) return;
2384
+ const cloneData = p.cloneData;
2385
+ if (typeof cloneData !== "function") {
2386
+ res.status(501).json({ code: "NOT_IMPLEMENTED", error: "Clone not supported by this protocol" });
2387
+ return;
2388
+ }
2389
+ const body = req.body ?? {};
2390
+ const overrides = body && typeof body === "object" && "overrides" in body ? body.overrides : body;
2391
+ const result = await cloneData.call(p, {
2392
+ object: req.params.object,
2393
+ id: req.params.id,
2394
+ ...overrides && typeof overrides === "object" ? { overrides } : {},
2395
+ ...environmentId ? { environmentId } : {},
2396
+ ...context ? { context } : {}
2397
+ });
2398
+ res.status(201).json(result);
2399
+ } catch (error) {
2400
+ const status = typeof error?.status === "number" ? error.status : mapDataError(error, req.params?.object).status;
2401
+ if (!isExpectedDataStatus(status) && error?.code !== "VALIDATION_FAILED") logError("[REST] Unhandled error:", error);
2402
+ sendError(res, error, req.params?.object);
2403
+ }
2404
+ },
2405
+ metadata: {
2406
+ summary: "Clone a record (gated by enable.clone)",
2407
+ tags: ["data", "clone"]
2408
+ }
2409
+ });
2314
2410
  this.routeManager.register({
2315
2411
  method: "POST",
2316
2412
  path: `${dataPath}/:object/import`,
@@ -2325,6 +2421,7 @@ var RestServer = class {
2325
2421
  res.status(400).json({ code: "INVALID_REQUEST", error: "object is required" });
2326
2422
  return;
2327
2423
  }
2424
+ if (await this.enforceApiAccess(req, res, p, environmentId, "import")) return;
2328
2425
  const body = req.body ?? {};
2329
2426
  const dryRun = body.dryRun === true;
2330
2427
  const mapping = body.mapping ?? {};
@@ -2408,6 +2505,7 @@ var RestServer = class {
2408
2505
  res.status(400).json({ code: "INVALID_REQUEST", error: "object is required" });
2409
2506
  return;
2410
2507
  }
2508
+ if (await this.enforceApiAccess(req, res, p, environmentId, "export")) return;
2411
2509
  const q = req.query ?? {};
2412
2510
  const format = String(q.format ?? "csv").toLowerCase() === "json" ? "json" : "csv";
2413
2511
  const HARD_CAP = 5e4;
@@ -3929,6 +4027,7 @@ var RestServer = class {
3929
4027
  const p = await this.resolveProtocol(environmentId, req);
3930
4028
  const context = await this.resolveExecCtx(environmentId, req);
3931
4029
  if (this.enforceAuth(req, res, context)) return;
4030
+ if (await this.enforceApiAccess(req, res, p, environmentId, "bulk")) return;
3932
4031
  const result = await p.batchData({
3933
4032
  object: req.params.object,
3934
4033
  request: req.body,
@@ -3957,6 +4056,7 @@ var RestServer = class {
3957
4056
  const p = await this.resolveProtocol(environmentId, req);
3958
4057
  const context = await this.resolveExecCtx(environmentId, req);
3959
4058
  if (this.enforceAuth(req, res, context)) return;
4059
+ if (await this.enforceApiAccess(req, res, p, environmentId, "create")) return;
3960
4060
  const result = await p.createManyData({
3961
4061
  object: req.params.object,
3962
4062
  records: req.body || [],
@@ -3985,6 +4085,7 @@ var RestServer = class {
3985
4085
  const p = await this.resolveProtocol(environmentId, req);
3986
4086
  const context = await this.resolveExecCtx(environmentId, req);
3987
4087
  if (this.enforceAuth(req, res, context)) return;
4088
+ if (await this.enforceApiAccess(req, res, p, environmentId, "update")) return;
3988
4089
  const result = await p.updateManyData({
3989
4090
  object: req.params.object,
3990
4091
  ...req.body,
@@ -4013,6 +4114,7 @@ var RestServer = class {
4013
4114
  const p = await this.resolveProtocol(environmentId, req);
4014
4115
  const context = await this.resolveExecCtx(environmentId, req);
4015
4116
  if (this.enforceAuth(req, res, context)) return;
4117
+ if (await this.enforceApiAccess(req, res, p, environmentId, "delete")) return;
4016
4118
  const result = await p.deleteManyData({
4017
4119
  object: req.params.object,
4018
4120
  ...req.body,