@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.cjs +102 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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,
|