@objectstack/rest 9.6.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.cjs
CHANGED
|
@@ -667,6 +667,60 @@ var RestServer = class {
|
|
|
667
667
|
});
|
|
668
668
|
return true;
|
|
669
669
|
}
|
|
670
|
+
/**
|
|
671
|
+
* Enforce object-level API exposure (ObjectSchema `enable.apiEnabled` /
|
|
672
|
+
* `enable.apiMethods`) on the REST data surface — the *external* API boundary
|
|
673
|
+
* only. Internal callers (hooks, flows, raw objectql) are unaffected, which is
|
|
674
|
+
* the point: `apiEnabled` controls automatic API exposure, not data access.
|
|
675
|
+
*
|
|
676
|
+
* - `enable.apiEnabled === false` → object hidden from the API (404, so its
|
|
677
|
+
* existence isn't revealed).
|
|
678
|
+
* - `enable.apiMethods` (non-empty whitelist) → unlisted operations rejected (405).
|
|
679
|
+
*
|
|
680
|
+
* Default-allow: objects with no `enable` block (or `apiEnabled` unset/true and
|
|
681
|
+
* no `apiMethods` whitelist) behave exactly as before — no regression. Unknown
|
|
682
|
+
* objects fall through to the normal 404 path. A metadata-read failure does not
|
|
683
|
+
* block (the data call itself needs the same metadata and will surface the
|
|
684
|
+
* error). Returns `true` when the request was blocked (response already sent).
|
|
685
|
+
*
|
|
686
|
+
* See ADR-0049 (#1889): shipping a non-enforcing `apiEnabled` is false security.
|
|
687
|
+
*/
|
|
688
|
+
async enforceApiAccess(req, res, p, environmentId, operation) {
|
|
689
|
+
const objectName = req?.params?.object;
|
|
690
|
+
if (!objectName) return false;
|
|
691
|
+
let enable;
|
|
692
|
+
try {
|
|
693
|
+
const r = await p.getMetaItems?.({
|
|
694
|
+
type: "object",
|
|
695
|
+
...environmentId ? { environmentId } : {}
|
|
696
|
+
});
|
|
697
|
+
const items = Array.isArray(r?.items) ? r.items : Array.isArray(r) ? r : [];
|
|
698
|
+
const obj = items.find((o) => o?.name === objectName);
|
|
699
|
+
if (!obj) return false;
|
|
700
|
+
enable = obj.enable;
|
|
701
|
+
} catch {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
if (!enable) return false;
|
|
705
|
+
if (enable.apiEnabled === false) {
|
|
706
|
+
res.status(404).json({
|
|
707
|
+
error: `Object '${objectName}' is not exposed via the API`,
|
|
708
|
+
code: "OBJECT_API_DISABLED",
|
|
709
|
+
object: objectName
|
|
710
|
+
});
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
if (Array.isArray(enable.apiMethods) && enable.apiMethods.length > 0 && !enable.apiMethods.includes(operation)) {
|
|
714
|
+
res.status(405).json({
|
|
715
|
+
error: `API operation '${operation}' is not allowed on object '${objectName}'`,
|
|
716
|
+
code: "OBJECT_API_METHOD_NOT_ALLOWED",
|
|
717
|
+
object: objectName,
|
|
718
|
+
allowed: enable.apiMethods
|
|
719
|
+
});
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
670
724
|
/**
|
|
671
725
|
* Resolve the request's execution context (RBAC/RLS/FLS) by looking up
|
|
672
726
|
* the better-auth session via the project's `auth` service. Returns
|
|
@@ -2118,6 +2172,7 @@ var RestServer = class {
|
|
|
2118
2172
|
const p = await this.resolveProtocol(environmentId, req);
|
|
2119
2173
|
const context = await this.resolveExecCtx(environmentId, req);
|
|
2120
2174
|
if (this.enforceAuth(req, res, context)) return;
|
|
2175
|
+
if (await this.enforceApiAccess(req, res, p, environmentId, "list")) return;
|
|
2121
2176
|
const result = await p.findData({
|
|
2122
2177
|
object: req.params.object,
|
|
2123
2178
|
query: req.query,
|
|
@@ -2152,6 +2207,7 @@ var RestServer = class {
|
|
|
2152
2207
|
const { select, expand } = req.query || {};
|
|
2153
2208
|
const context = await this.resolveExecCtx(environmentId, req);
|
|
2154
2209
|
if (this.enforceAuth(req, res, context)) return;
|
|
2210
|
+
if (await this.enforceApiAccess(req, res, p, environmentId, "get")) return;
|
|
2155
2211
|
const result = await p.getData({
|
|
2156
2212
|
object: req.params.object,
|
|
2157
2213
|
id: req.params.id,
|
|
@@ -2183,6 +2239,7 @@ var RestServer = class {
|
|
|
2183
2239
|
const p = await this.resolveProtocol(environmentId, req);
|
|
2184
2240
|
const context = await this.resolveExecCtx(environmentId, req);
|
|
2185
2241
|
if (this.enforceAuth(req, res, context)) return;
|
|
2242
|
+
if (await this.enforceApiAccess(req, res, p, environmentId, "create")) return;
|
|
2186
2243
|
const result = await p.createData({
|
|
2187
2244
|
object: req.params.object,
|
|
2188
2245
|
data: req.body,
|
|
@@ -2212,6 +2269,7 @@ var RestServer = class {
|
|
|
2212
2269
|
const p = await this.resolveProtocol(environmentId, req);
|
|
2213
2270
|
const context = await this.resolveExecCtx(environmentId, req);
|
|
2214
2271
|
if (this.enforceAuth(req, res, context)) return;
|
|
2272
|
+
if (await this.enforceApiAccess(req, res, p, environmentId, "list")) return;
|
|
2215
2273
|
const result = await p.findData({
|
|
2216
2274
|
object: req.params.object,
|
|
2217
2275
|
query: req.body || {},
|
|
@@ -2249,6 +2307,7 @@ var RestServer = class {
|
|
|
2249
2307
|
const { expectedVersion: _drop, ...rest } = data;
|
|
2250
2308
|
data = rest;
|
|
2251
2309
|
}
|
|
2310
|
+
if (await this.enforceApiAccess(req, res, p, environmentId, "update")) return;
|
|
2252
2311
|
const result = await p.updateData({
|
|
2253
2312
|
object: req.params.object,
|
|
2254
2313
|
id: req.params.id,
|
|
@@ -2283,6 +2342,7 @@ var RestServer = class {
|
|
|
2283
2342
|
const ifMatchHeader = req.headers?.["if-match"] ?? req.headers?.["If-Match"];
|
|
2284
2343
|
const queryVersion = req.query && typeof req.query === "object" ? req.query.expectedVersion : void 0;
|
|
2285
2344
|
const expectedVersion = queryVersion ?? ifMatchHeader;
|
|
2345
|
+
if (await this.enforceApiAccess(req, res, p, environmentId, "delete")) return;
|
|
2286
2346
|
const result = await p.deleteData({
|
|
2287
2347
|
object: req.params.object,
|
|
2288
2348
|
id: req.params.id,
|
|
@@ -2351,6 +2411,42 @@ var RestServer = class {
|
|
|
2351
2411
|
tags: ["data", "lead"]
|
|
2352
2412
|
}
|
|
2353
2413
|
});
|
|
2414
|
+
this.routeManager.register({
|
|
2415
|
+
method: "POST",
|
|
2416
|
+
path: `${dataPath}/:object/:id/clone`,
|
|
2417
|
+
handler: async (req, res) => {
|
|
2418
|
+
try {
|
|
2419
|
+
const environmentId = isScoped ? req.params?.environmentId : void 0;
|
|
2420
|
+
const p = await this.resolveProtocol(environmentId, req);
|
|
2421
|
+
const context = await this.resolveExecCtx(environmentId, req);
|
|
2422
|
+
if (this.enforceAuth(req, res, context)) return;
|
|
2423
|
+
if (await this.enforceApiAccess(req, res, p, environmentId, "create")) return;
|
|
2424
|
+
const cloneData = p.cloneData;
|
|
2425
|
+
if (typeof cloneData !== "function") {
|
|
2426
|
+
res.status(501).json({ code: "NOT_IMPLEMENTED", error: "Clone not supported by this protocol" });
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
const body = req.body ?? {};
|
|
2430
|
+
const overrides = body && typeof body === "object" && "overrides" in body ? body.overrides : body;
|
|
2431
|
+
const result = await cloneData.call(p, {
|
|
2432
|
+
object: req.params.object,
|
|
2433
|
+
id: req.params.id,
|
|
2434
|
+
...overrides && typeof overrides === "object" ? { overrides } : {},
|
|
2435
|
+
...environmentId ? { environmentId } : {},
|
|
2436
|
+
...context ? { context } : {}
|
|
2437
|
+
});
|
|
2438
|
+
res.status(201).json(result);
|
|
2439
|
+
} catch (error) {
|
|
2440
|
+
const status = typeof error?.status === "number" ? error.status : mapDataError(error, req.params?.object).status;
|
|
2441
|
+
if (!isExpectedDataStatus(status) && error?.code !== "VALIDATION_FAILED") logError("[REST] Unhandled error:", error);
|
|
2442
|
+
sendError(res, error, req.params?.object);
|
|
2443
|
+
}
|
|
2444
|
+
},
|
|
2445
|
+
metadata: {
|
|
2446
|
+
summary: "Clone a record (gated by enable.clone)",
|
|
2447
|
+
tags: ["data", "clone"]
|
|
2448
|
+
}
|
|
2449
|
+
});
|
|
2354
2450
|
this.routeManager.register({
|
|
2355
2451
|
method: "POST",
|
|
2356
2452
|
path: `${dataPath}/:object/import`,
|
|
@@ -2365,6 +2461,7 @@ var RestServer = class {
|
|
|
2365
2461
|
res.status(400).json({ code: "INVALID_REQUEST", error: "object is required" });
|
|
2366
2462
|
return;
|
|
2367
2463
|
}
|
|
2464
|
+
if (await this.enforceApiAccess(req, res, p, environmentId, "import")) return;
|
|
2368
2465
|
const body = req.body ?? {};
|
|
2369
2466
|
const dryRun = body.dryRun === true;
|
|
2370
2467
|
const mapping = body.mapping ?? {};
|
|
@@ -2448,6 +2545,7 @@ var RestServer = class {
|
|
|
2448
2545
|
res.status(400).json({ code: "INVALID_REQUEST", error: "object is required" });
|
|
2449
2546
|
return;
|
|
2450
2547
|
}
|
|
2548
|
+
if (await this.enforceApiAccess(req, res, p, environmentId, "export")) return;
|
|
2451
2549
|
const q = req.query ?? {};
|
|
2452
2550
|
const format = String(q.format ?? "csv").toLowerCase() === "json" ? "json" : "csv";
|
|
2453
2551
|
const HARD_CAP = 5e4;
|
|
@@ -3969,6 +4067,7 @@ var RestServer = class {
|
|
|
3969
4067
|
const p = await this.resolveProtocol(environmentId, req);
|
|
3970
4068
|
const context = await this.resolveExecCtx(environmentId, req);
|
|
3971
4069
|
if (this.enforceAuth(req, res, context)) return;
|
|
4070
|
+
if (await this.enforceApiAccess(req, res, p, environmentId, "bulk")) return;
|
|
3972
4071
|
const result = await p.batchData({
|
|
3973
4072
|
object: req.params.object,
|
|
3974
4073
|
request: req.body,
|
|
@@ -3997,6 +4096,7 @@ var RestServer = class {
|
|
|
3997
4096
|
const p = await this.resolveProtocol(environmentId, req);
|
|
3998
4097
|
const context = await this.resolveExecCtx(environmentId, req);
|
|
3999
4098
|
if (this.enforceAuth(req, res, context)) return;
|
|
4099
|
+
if (await this.enforceApiAccess(req, res, p, environmentId, "create")) return;
|
|
4000
4100
|
const result = await p.createManyData({
|
|
4001
4101
|
object: req.params.object,
|
|
4002
4102
|
records: req.body || [],
|
|
@@ -4025,6 +4125,7 @@ var RestServer = class {
|
|
|
4025
4125
|
const p = await this.resolveProtocol(environmentId, req);
|
|
4026
4126
|
const context = await this.resolveExecCtx(environmentId, req);
|
|
4027
4127
|
if (this.enforceAuth(req, res, context)) return;
|
|
4128
|
+
if (await this.enforceApiAccess(req, res, p, environmentId, "update")) return;
|
|
4028
4129
|
const result = await p.updateManyData({
|
|
4029
4130
|
object: req.params.object,
|
|
4030
4131
|
...req.body,
|
|
@@ -4053,6 +4154,7 @@ var RestServer = class {
|
|
|
4053
4154
|
const p = await this.resolveProtocol(environmentId, req);
|
|
4054
4155
|
const context = await this.resolveExecCtx(environmentId, req);
|
|
4055
4156
|
if (this.enforceAuth(req, res, context)) return;
|
|
4157
|
+
if (await this.enforceApiAccess(req, res, p, environmentId, "delete")) return;
|
|
4056
4158
|
const result = await p.deleteManyData({
|
|
4057
4159
|
object: req.params.object,
|
|
4058
4160
|
...req.body,
|