@blamejs/core 0.14.18 → 0.14.19
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/CHANGELOG.md +2 -0
- package/README.md +1 -1
- package/lib/archive.js +206 -52
- package/lib/auth/oauth.js +58 -0
- package/lib/auth/oid4vci.js +84 -27
- package/lib/mail-auth.js +554 -55
- package/lib/middleware/scim-server.js +294 -10
- package/lib/openapi-paths-builder.js +105 -29
- package/lib/openapi.js +225 -100
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -19,8 +19,17 @@ var ScimServerError = framework_error.defineClass(
|
|
|
19
19
|
|
|
20
20
|
var SCIM_CORE_SCHEMA_USER = "urn:ietf:params:scim:schemas:core:2.0:User";
|
|
21
21
|
var SCIM_CORE_SCHEMA_GROUP = "urn:ietf:params:scim:schemas:core:2.0:Group";
|
|
22
|
-
var SCIM_MESSAGE_ERROR
|
|
23
|
-
var SCIM_MESSAGE_LIST
|
|
22
|
+
var SCIM_MESSAGE_ERROR = "urn:ietf:params:scim:api:messages:2.0:Error";
|
|
23
|
+
var SCIM_MESSAGE_LIST = "urn:ietf:params:scim:api:messages:2.0:ListResponse";
|
|
24
|
+
var SCIM_MESSAGE_BULK_REQUEST = "urn:ietf:params:scim:api:messages:2.0:BulkRequest";
|
|
25
|
+
var SCIM_MESSAGE_BULK_RESPONSE = "urn:ietf:params:scim:api:messages:2.0:BulkResponse";
|
|
26
|
+
|
|
27
|
+
// RFC 7644 §3.7.2 — a bulkId cross-reference is the literal token
|
|
28
|
+
// "bulkId:" followed by the client-assigned identifier. Bounded: the
|
|
29
|
+
// identifier is matched lazily-free (one or more non-quote chars) and
|
|
30
|
+
// only ever applied to operator-defined bulkId strings, never to free
|
|
31
|
+
// request text, so there is no super-linear backtracking exposure.
|
|
32
|
+
var BULK_ID_REF_RE = /^bulkId:(.+)$/;
|
|
24
33
|
|
|
25
34
|
var ALLOWED_FILTER_OPS = ["eq", "ne", "co", "sw", "ew", "pr", "gt", "ge", "lt", "le"];
|
|
26
35
|
|
|
@@ -37,6 +46,11 @@ var BEARER_RE = /^Bearer\s+(.+)$/i;
|
|
|
37
46
|
* Returns a request middleware that handles SCIM 2.0 requests
|
|
38
47
|
* (RFC 7642-7644). Operator supplies CRUD callbacks per resource.
|
|
39
48
|
*
|
|
49
|
+
* Bulk operations (RFC 7644 §3.7) are opt-in: pass `opts.bulk` to
|
|
50
|
+
* enable the `/Bulk` POST endpoint and advertise it in
|
|
51
|
+
* ServiceProviderConfig. When omitted, `bulk.supported` stays `false`
|
|
52
|
+
* and `/Bulk` is not routed (back-compatible default).
|
|
53
|
+
*
|
|
40
54
|
* @opts
|
|
41
55
|
* {
|
|
42
56
|
* basePath?: string,
|
|
@@ -44,6 +58,10 @@ var BEARER_RE = /^Bearer\s+(.+)$/i;
|
|
|
44
58
|
* groups?: ScimResourceImpl,
|
|
45
59
|
* bearer?: (token) => Promise<actor>,
|
|
46
60
|
* maxPageSize?: number,
|
|
61
|
+
* bulk?: {
|
|
62
|
+
* maxOperations?: number, // default: 1000 (config-time positive int)
|
|
63
|
+
* maxPayloadSize?: number, // default: 1 MiB (config-time positive int, bytes)
|
|
64
|
+
* },
|
|
47
65
|
* }
|
|
48
66
|
*
|
|
49
67
|
* @example
|
|
@@ -51,6 +69,7 @@ var BEARER_RE = /^Bearer\s+(.+)$/i;
|
|
|
51
69
|
* basePath: "/scim/v2",
|
|
52
70
|
* users: myUserAdapter,
|
|
53
71
|
* groups: myGroupAdapter,
|
|
72
|
+
* bulk: { maxOperations: 100 },
|
|
54
73
|
* });
|
|
55
74
|
* app.use(mw);
|
|
56
75
|
*/
|
|
@@ -63,11 +82,12 @@ function create(opts) {
|
|
|
63
82
|
var basePath = opts.basePath || "/scim/v2";
|
|
64
83
|
var maxPageSize = opts.maxPageSize || 200; // page-size count, not bytes
|
|
65
84
|
var bearer = opts.bearer || null;
|
|
85
|
+
var bulkCfg = _resolveBulkConfig(opts.bulk);
|
|
66
86
|
|
|
67
87
|
function middleware(req, res, next) {
|
|
68
88
|
var url = req.url.split("?")[0];
|
|
69
89
|
if (url.indexOf(basePath) !== 0) { next(); return; }
|
|
70
|
-
_dispatch(req, res, basePath, bearer, opts, maxPageSize)
|
|
90
|
+
_dispatch(req, res, basePath, bearer, opts, maxPageSize, bulkCfg)
|
|
71
91
|
.catch(function (err) {
|
|
72
92
|
_writeScimError(res, err.statusCode || 500, err.scimType || "internal",
|
|
73
93
|
(err.message || String(err)).slice(0, 500));
|
|
@@ -92,7 +112,26 @@ function _validateResourceImpl(impl, name) {
|
|
|
92
112
|
});
|
|
93
113
|
}
|
|
94
114
|
|
|
95
|
-
|
|
115
|
+
// Config-time / entry-point tier: bad bulk caps THROW so the operator
|
|
116
|
+
// catches a typo at boot rather than at request time. RFC 7644 §3.7.
|
|
117
|
+
// Returns null when bulk is not opted into (back-compat default off).
|
|
118
|
+
function _resolveBulkConfig(bulk) {
|
|
119
|
+
if (bulk === undefined || bulk === null) return null;
|
|
120
|
+
if (typeof bulk !== "object") {
|
|
121
|
+
throw new ScimServerError("middleware/scim-server/bad-bulk",
|
|
122
|
+
"middleware.scimServer: opts.bulk must be an object { maxOperations?, maxPayloadSize? }");
|
|
123
|
+
}
|
|
124
|
+
validateOpts.optionalPositiveInt(bulk.maxOperations, "middleware.scimServer: opts.bulk.maxOperations",
|
|
125
|
+
ScimServerError, "middleware/scim-server/bad-bulk-max-operations");
|
|
126
|
+
validateOpts.optionalPositiveInt(bulk.maxPayloadSize, "middleware.scimServer: opts.bulk.maxPayloadSize",
|
|
127
|
+
ScimServerError, "middleware/scim-server/bad-bulk-max-payload-size");
|
|
128
|
+
return {
|
|
129
|
+
maxOperations: bulk.maxOperations || 1000,
|
|
130
|
+
maxPayloadSize: bulk.maxPayloadSize || C.BYTES.mib(1),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function _dispatch(req, res, basePath, bearer, opts, maxPageSize, bulkCfg) {
|
|
96
135
|
var relUrl = req.url.slice(basePath.length) || "/";
|
|
97
136
|
var qIdx = relUrl.indexOf("?");
|
|
98
137
|
var path = qIdx === -1 ? relUrl : relUrl.slice(0, qIdx);
|
|
@@ -148,6 +187,19 @@ async function _dispatch(req, res, basePath, bearer, opts, maxPageSize) {
|
|
|
148
187
|
var users = opts.users;
|
|
149
188
|
var groups = opts.groups;
|
|
150
189
|
|
|
190
|
+
if (path === "/Bulk") {
|
|
191
|
+
if (req.method !== "POST") {
|
|
192
|
+
_writeScimError(res, H.METHOD_NOT_ALLOWED, "noTarget", req.method + " not allowed on /Bulk");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!bulkCfg) {
|
|
196
|
+
_writeScimError(res, 501, "notSupported", "bulk operations are not enabled");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
await _handleBulk(req, res, opts, bulkCfg, ctx);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
151
203
|
// RESOURCE_PATH_RE applies to a URL path; node http caps URL at 8 KiB.
|
|
152
204
|
var match = path.length < C.BYTES.kib(8) && RESOURCE_PATH_RE.test(path) ? path.match(RESOURCE_PATH_RE) : null; // allow:regex-no-length-cap URL bounded above
|
|
153
205
|
if (!match) {
|
|
@@ -244,6 +296,230 @@ async function _dispatch(req, res, basePath, bearer, opts, maxPageSize) {
|
|
|
244
296
|
_writeScimError(res, H.METHOD_NOT_ALLOWED, "noTarget", req.method + " not allowed on " + path);
|
|
245
297
|
}
|
|
246
298
|
|
|
299
|
+
// RFC 7644 §3.7 — /Bulk POST. Parses a BulkRequest, enforces the
|
|
300
|
+
// config-time maxOperations / maxPayloadSize caps, optionally
|
|
301
|
+
// short-circuits at failOnErrors, resolves bulkId cross-references
|
|
302
|
+
// (§3.7.2), dispatches each operation through the same per-resource
|
|
303
|
+
// create/update/delete logic the singleton endpoints use, and returns
|
|
304
|
+
// a BulkResponse carrying one result object per operation.
|
|
305
|
+
async function _handleBulk(req, res, opts, bulkCfg, ctx) {
|
|
306
|
+
var body;
|
|
307
|
+
try {
|
|
308
|
+
body = await _readBulkBody(req, bulkCfg.maxPayloadSize);
|
|
309
|
+
} catch (e) {
|
|
310
|
+
// collectStream rejects with the configured errorClass on overflow.
|
|
311
|
+
if (e && e.code === "middleware/scim-server/bulk-too-large") {
|
|
312
|
+
_writeScimError(res, H.PAYLOAD_TOO_LARGE, "tooLarge",
|
|
313
|
+
"bulk payload exceeds maxPayloadSize of " + bulkCfg.maxPayloadSize + " bytes");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
throw e;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!body || typeof body !== "object" ||
|
|
320
|
+
!Array.isArray(body.schemas) ||
|
|
321
|
+
body.schemas.indexOf(SCIM_MESSAGE_BULK_REQUEST) === -1) {
|
|
322
|
+
_writeScimError(res, H.BAD_REQUEST, "invalidValue",
|
|
323
|
+
"BulkRequest body.schemas must include '" + SCIM_MESSAGE_BULK_REQUEST + "'");
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!Array.isArray(body.Operations)) {
|
|
327
|
+
_writeScimError(res, H.BAD_REQUEST, "invalidValue", "BulkRequest must include Operations[]");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (body.Operations.length > bulkCfg.maxOperations) {
|
|
331
|
+
_writeScimError(res, H.PAYLOAD_TOO_LARGE, "tooMany",
|
|
332
|
+
"bulk request has " + body.Operations.length +
|
|
333
|
+
" operations, exceeding maxOperations of " + bulkCfg.maxOperations);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
var failOnErrors = _parseFailOnErrors(body.failOnErrors);
|
|
338
|
+
var bulkIdMap = Object.create(null); // client bulkId -> assigned resource id
|
|
339
|
+
var results = [];
|
|
340
|
+
var errorCount = 0;
|
|
341
|
+
|
|
342
|
+
for (var i = 0; i < body.Operations.length; i++) {
|
|
343
|
+
var result = await _runBulkOperation(body.Operations[i], i, opts, ctx, bulkIdMap);
|
|
344
|
+
results.push(result.entry);
|
|
345
|
+
if (result.isError) {
|
|
346
|
+
errorCount++;
|
|
347
|
+
// RFC 7644 §3.7 — once the error count reaches failOnErrors the
|
|
348
|
+
// service stops processing and returns the results so far.
|
|
349
|
+
if (failOnErrors !== null && errorCount >= failOnErrors) break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
_writeJson(res, H.OK, {
|
|
354
|
+
schemas: [SCIM_MESSAGE_BULK_RESPONSE],
|
|
355
|
+
Operations: results,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// RFC 7644 §3.7 — failOnErrors is an OPTIONAL integer >= 1. Absent /
|
|
360
|
+
// non-conforming values mean "process every operation" (null).
|
|
361
|
+
function _parseFailOnErrors(value) {
|
|
362
|
+
if (typeof value !== "number" || !isFinite(value) || Math.floor(value) !== value || value < 1) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
return value;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Dispatch one BulkOperation through the matching per-resource adapter.
|
|
369
|
+
// Returns { entry, isError }; entry is the BulkResponse Operation object
|
|
370
|
+
// (RFC 7644 §3.7). Adapter rejections become per-op error entries — one
|
|
371
|
+
// failing operation never aborts the whole batch (unless failOnErrors).
|
|
372
|
+
async function _runBulkOperation(op, index, opts, ctx, bulkIdMap) {
|
|
373
|
+
if (!op || typeof op !== "object" || typeof op.method !== "string") {
|
|
374
|
+
return _bulkErr(op, "400", "invalidSyntax",
|
|
375
|
+
"Operations[" + index + "] missing string method");
|
|
376
|
+
}
|
|
377
|
+
var method = op.method.toUpperCase();
|
|
378
|
+
if (method !== "POST" && method !== "PUT" && method !== "PATCH" && method !== "DELETE") {
|
|
379
|
+
return _bulkErr(op, "400", "invalidValue",
|
|
380
|
+
"Operations[" + index + "].method '" + op.method + "' not in POST/PUT/PATCH/DELETE");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
var parsed = _parseBulkPath(op.path);
|
|
384
|
+
if (!parsed) {
|
|
385
|
+
return _bulkErr(op, "400", "invalidValue",
|
|
386
|
+
"Operations[" + index + "].path '" + String(op.path) + "' is not a valid bulk path");
|
|
387
|
+
}
|
|
388
|
+
var impl = parsed.resourceType === "Users" ? opts.users : opts.groups;
|
|
389
|
+
if (!impl) {
|
|
390
|
+
return _bulkErr(op, "404", "invalidValue", "/" + parsed.resourceType + " not configured");
|
|
391
|
+
}
|
|
392
|
+
// POST defines a bulkId-assigned resource; PUT/PATCH/DELETE target an id.
|
|
393
|
+
if (method === "POST" && !op.bulkId) {
|
|
394
|
+
return _bulkErr(op, "400", "invalidValue",
|
|
395
|
+
"Operations[" + index + "] POST requires a bulkId");
|
|
396
|
+
}
|
|
397
|
+
if (method !== "POST" && !parsed.resourceId) {
|
|
398
|
+
return _bulkErr(op, "400", "invalidValue",
|
|
399
|
+
"Operations[" + index + "] " + method + " requires a resource id in path");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
var data = op.data;
|
|
403
|
+
if (method === "POST" || method === "PUT" || method === "PATCH") {
|
|
404
|
+
// RFC 7644 §3.7.2 — substitute any bulkId cross-references that
|
|
405
|
+
// earlier operations have resolved before handing data to the adapter.
|
|
406
|
+
data = _resolveBulkIdRefs(op.data, bulkIdMap);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
if (method === "POST" || method === "PUT") {
|
|
411
|
+
// The same SCIM schema gate the singleton POST / PUT routes apply,
|
|
412
|
+
// so a bulk op cannot persist a resource with a missing or wrong
|
|
413
|
+
// schema that the singleton endpoints would reject (RFC 7644
|
|
414
|
+
// §3.5.1). A throw here is caught below and returned as the op's
|
|
415
|
+
// own error, not aborting the batch.
|
|
416
|
+
_assertSchema(data, parsed.resourceType === "Users" ? SCIM_CORE_SCHEMA_USER : SCIM_CORE_SCHEMA_GROUP);
|
|
417
|
+
}
|
|
418
|
+
if (method === "POST") {
|
|
419
|
+
var created = await impl.create(data || {}, ctx);
|
|
420
|
+
var newId = created && created.id;
|
|
421
|
+
if (op.bulkId && newId !== undefined && newId !== null) {
|
|
422
|
+
bulkIdMap[String(op.bulkId)] = String(newId);
|
|
423
|
+
}
|
|
424
|
+
return _bulkOk(op, "201", _bulkLocation(parsed.resourceType, newId), created);
|
|
425
|
+
}
|
|
426
|
+
if (method === "PUT") {
|
|
427
|
+
var replaced = await impl.update(parsed.resourceId, data || {}, ctx);
|
|
428
|
+
return _bulkOk(op, "200", _bulkLocation(parsed.resourceType, parsed.resourceId), replaced);
|
|
429
|
+
}
|
|
430
|
+
if (method === "PATCH") {
|
|
431
|
+
var ops = data && Array.isArray(data.Operations) ? data.Operations : [];
|
|
432
|
+
var patched = await impl.patch(parsed.resourceId, ops, ctx);
|
|
433
|
+
return _bulkOk(op, "200", _bulkLocation(parsed.resourceType, parsed.resourceId), patched);
|
|
434
|
+
}
|
|
435
|
+
await impl.remove(parsed.resourceId, ctx); // DELETE
|
|
436
|
+
return _bulkOk(op, "204", _bulkLocation(parsed.resourceType, parsed.resourceId), null);
|
|
437
|
+
} catch (e) {
|
|
438
|
+
var status = e && e.statusCode ? String(e.statusCode) : "500";
|
|
439
|
+
var scimType = e && e.scimType ? e.scimType : null;
|
|
440
|
+
// Operator-adapter error detail is surfaced verbatim (it is the
|
|
441
|
+
// adapter's own message, not request-derived secret material).
|
|
442
|
+
return _bulkErr(op, status, scimType, (e && e.message) ? String(e.message) : "operation failed");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// A bulk path is "/Users", "/Users/<id>", "/Groups", or "/Groups/<id>".
|
|
447
|
+
function _parseBulkPath(path) {
|
|
448
|
+
if (typeof path !== "string" || path.length === 0) return null;
|
|
449
|
+
// Reuse the singleton resource-path grammar; node http caps URL at 8 KiB.
|
|
450
|
+
var m = path.length < C.BYTES.kib(8) && RESOURCE_PATH_RE.test(path) // allow:regex-no-length-cap path bounded above
|
|
451
|
+
? path.match(RESOURCE_PATH_RE) : null;
|
|
452
|
+
if (!m) return null;
|
|
453
|
+
return { resourceType: m[1], resourceId: m[2] || null };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// RFC 7644 §3.7.2 — walk operation data and replace any string of the
|
|
457
|
+
// form "bulkId:<clientId>" with the server-assigned id once known.
|
|
458
|
+
// Operates only on operator/client-supplied bulk data of bounded size
|
|
459
|
+
// (the whole payload is capped at maxPayloadSize before parse).
|
|
460
|
+
function _resolveBulkIdRefs(value, bulkIdMap) {
|
|
461
|
+
if (typeof value === "string") {
|
|
462
|
+
var ref = BULK_ID_REF_RE.exec(value);
|
|
463
|
+
if (ref) {
|
|
464
|
+
var resolved = bulkIdMap[ref[1]];
|
|
465
|
+
return resolved !== undefined ? resolved : value;
|
|
466
|
+
}
|
|
467
|
+
return value;
|
|
468
|
+
}
|
|
469
|
+
if (Array.isArray(value)) {
|
|
470
|
+
var arr = [];
|
|
471
|
+
for (var i = 0; i < value.length; i++) arr.push(_resolveBulkIdRefs(value[i], bulkIdMap));
|
|
472
|
+
return arr;
|
|
473
|
+
}
|
|
474
|
+
if (value && typeof value === "object") {
|
|
475
|
+
var out = {};
|
|
476
|
+
var keys = Object.keys(value);
|
|
477
|
+
for (var k = 0; k < keys.length; k++) {
|
|
478
|
+
if (keys[k] === "__proto__" || keys[k] === "constructor" || keys[k] === "prototype") continue;
|
|
479
|
+
out[keys[k]] = _resolveBulkIdRefs(value[keys[k]], bulkIdMap);
|
|
480
|
+
}
|
|
481
|
+
return out;
|
|
482
|
+
}
|
|
483
|
+
return value;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function _bulkLocation(resourceType, id) {
|
|
487
|
+
if (id === undefined || id === null) return undefined;
|
|
488
|
+
return "/" + resourceType + "/" + String(id);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function _bulkOk(op, status, location, response) {
|
|
492
|
+
var entry = { method: op && op.method, status: status };
|
|
493
|
+
if (op && op.bulkId) entry.bulkId = op.bulkId;
|
|
494
|
+
if (location) entry.location = location;
|
|
495
|
+
// Per §3.7 the response body is OPTIONAL; include it when the adapter
|
|
496
|
+
// returned a representation (omit on 204 No Content).
|
|
497
|
+
if (response !== null && response !== undefined) entry.response = response;
|
|
498
|
+
return { entry: entry, isError: false };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function _bulkErr(op, status, scimType, detail) {
|
|
502
|
+
var errBody = { schemas: [SCIM_MESSAGE_ERROR], status: status, detail: detail };
|
|
503
|
+
if (scimType) errBody.scimType = scimType;
|
|
504
|
+
var entry = { method: op && op.method, status: status, response: errBody };
|
|
505
|
+
if (op && op.bulkId) entry.bulkId = op.bulkId;
|
|
506
|
+
return { entry: entry, isError: true };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function _readBulkBody(req, maxBytes) {
|
|
510
|
+
if (req.body && Buffer.isBuffer(req.body)) {
|
|
511
|
+
return Promise.resolve(safeJson.parse(req.body.toString("utf8"), { maxBytes: maxBytes }));
|
|
512
|
+
}
|
|
513
|
+
if (req.body && typeof req.body === "object") return Promise.resolve(req.body);
|
|
514
|
+
return safeBuffer.collectStream(req, {
|
|
515
|
+
maxBytes: maxBytes,
|
|
516
|
+
errorClass: ScimServerError,
|
|
517
|
+
sizeCode: "middleware/scim-server/bulk-too-large",
|
|
518
|
+
}).then(function (buf) {
|
|
519
|
+
return safeJson.parse(buf.toString("utf8"), { maxBytes: maxBytes });
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
247
523
|
function _parseQuery(qs) {
|
|
248
524
|
var out = {};
|
|
249
525
|
if (!qs) return out;
|
|
@@ -321,11 +597,17 @@ function _writeScimError(res, status, scimType, detail) {
|
|
|
321
597
|
}
|
|
322
598
|
|
|
323
599
|
function _serviceProviderConfig(opts) {
|
|
600
|
+
// RFC 7644 §3.7 — advertise bulk only when the operator opted in via
|
|
601
|
+
// opts.bulk; otherwise report it unsupported (back-compat default).
|
|
602
|
+
var bulkCfg = _resolveBulkConfig(opts.bulk);
|
|
603
|
+
var bulk = bulkCfg
|
|
604
|
+
? { supported: true, maxOperations: bulkCfg.maxOperations, maxPayloadSize: bulkCfg.maxPayloadSize }
|
|
605
|
+
: { supported: false, maxOperations: 0, maxPayloadSize: 0 };
|
|
324
606
|
return {
|
|
325
607
|
schemas: ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
|
|
326
608
|
documentationUri: opts.documentationUri || "https://datatracker.ietf.org/doc/html/rfc7643",
|
|
327
609
|
patch: { supported: true },
|
|
328
|
-
bulk:
|
|
610
|
+
bulk: bulk,
|
|
329
611
|
filter: { supported: true, maxResults: opts.maxPageSize || 200 },
|
|
330
612
|
changePassword: { supported: false },
|
|
331
613
|
sort: { supported: true },
|
|
@@ -370,9 +652,11 @@ function _schemas() {
|
|
|
370
652
|
}
|
|
371
653
|
|
|
372
654
|
module.exports = {
|
|
373
|
-
create:
|
|
374
|
-
ScimServerError:
|
|
375
|
-
SCIM_CORE_SCHEMA_USER:
|
|
376
|
-
SCIM_CORE_SCHEMA_GROUP:
|
|
377
|
-
|
|
655
|
+
create: create,
|
|
656
|
+
ScimServerError: ScimServerError,
|
|
657
|
+
SCIM_CORE_SCHEMA_USER: SCIM_CORE_SCHEMA_USER,
|
|
658
|
+
SCIM_CORE_SCHEMA_GROUP: SCIM_CORE_SCHEMA_GROUP,
|
|
659
|
+
SCIM_MESSAGE_BULK_REQUEST: SCIM_MESSAGE_BULK_REQUEST,
|
|
660
|
+
SCIM_MESSAGE_BULK_RESPONSE: SCIM_MESSAGE_BULK_RESPONSE,
|
|
661
|
+
ALLOWED_FILTER_OPS: ALLOWED_FILTER_OPS,
|
|
378
662
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* OpenAPI 3.1 — paths / operations builder.
|
|
3
|
+
* OpenAPI 3.1 / 3.2 — paths / operations + webhooks builder.
|
|
4
4
|
*
|
|
5
5
|
* Internal to lib/openapi.js. Holds the per-path operation table
|
|
6
6
|
* (method to operationObject) and produces the final `paths` map used
|
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Operation methods accepted: get / put / post / delete / options /
|
|
15
15
|
* head / patch / trace (RFC 9110 + OpenAPI 3.1 §4.8.5).
|
|
16
|
+
*
|
|
17
|
+
* `WebhooksBuilder` shares the same Operation Object normalisation but
|
|
18
|
+
* keys by a free-form webhook NAME (not a URL pattern): the top-level
|
|
19
|
+
* `webhooks` field is a map of named Path Item Objects describing
|
|
20
|
+
* out-of-band requests the API initiates (OpenAPI 3.2 §4.8.2, "Fixed
|
|
21
|
+
* Fields" — `webhooks`; carried forward unchanged from 3.1.0 §4.1).
|
|
22
|
+
* Webhook keys are not URL templates, so the `/`-prefix and
|
|
23
|
+
* path-template-placeholder checks are intentionally not applied.
|
|
16
24
|
*/
|
|
17
25
|
|
|
18
26
|
var validateOpts = require("./validate-opts");
|
|
@@ -39,25 +47,23 @@ function PathsBuilder() {
|
|
|
39
47
|
this._paths = {};
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
// _buildOperation — normalise a single Operation Object from operator
|
|
51
|
+
// opts. Shared by PathsBuilder.add and WebhooksBuilder.add. `label` is
|
|
52
|
+
// the caller-facing prefix used in error messages. `declaredPathParams`
|
|
53
|
+
// (out-param object) records every in=path parameter so the caller can
|
|
54
|
+
// verify path-template placeholders against it; webhooks have no URL
|
|
55
|
+
// template so the caller simply ignores it.
|
|
56
|
+
function _buildOperation(method, opts, label, declaredPathParams) {
|
|
44
57
|
if (typeof method !== "string" || VALID_METHODS.indexOf(method.toLowerCase()) === -1) {
|
|
45
58
|
throw new OpenApiError("openapi/bad-method",
|
|
46
|
-
"
|
|
59
|
+
label + ": method must be one of " + VALID_METHODS.join(", ") +
|
|
47
60
|
" - got " + JSON.stringify(method));
|
|
48
61
|
}
|
|
49
|
-
validateOpts.requireNonEmptyString(urlPattern, "paths.add: urlPattern",
|
|
50
|
-
OpenApiError, "openapi/bad-path");
|
|
51
|
-
if (urlPattern.charAt(0) !== "/") {
|
|
52
|
-
throw new OpenApiError("openapi/bad-path",
|
|
53
|
-
"paths.add: urlPattern must start with '/' - got " +
|
|
54
|
-
JSON.stringify(urlPattern));
|
|
55
|
-
}
|
|
56
62
|
validateOpts(opts, [
|
|
57
63
|
"summary", "description", "operationId", "tags",
|
|
58
64
|
"parameters", "requestBody", "responses",
|
|
59
65
|
"security", "deprecated", "servers", "externalDocs",
|
|
60
|
-
],
|
|
66
|
+
], label);
|
|
61
67
|
|
|
62
68
|
var op = {};
|
|
63
69
|
if (typeof opts.summary === "string") op.summary = opts.summary;
|
|
@@ -67,50 +73,65 @@ PathsBuilder.prototype.add = function (method, urlPattern, opts) {
|
|
|
67
73
|
op.tags = opts.tags.map(function (t) {
|
|
68
74
|
if (typeof t !== "string" || t.length === 0) {
|
|
69
75
|
throw new OpenApiError("openapi/bad-tag",
|
|
70
|
-
"
|
|
76
|
+
label + ": tags must be non-empty strings");
|
|
71
77
|
}
|
|
72
78
|
return t;
|
|
73
79
|
});
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
// Parameters
|
|
77
|
-
var declaredPathParams = Object.create(null);
|
|
78
83
|
if (Array.isArray(opts.parameters)) {
|
|
79
84
|
op.parameters = [];
|
|
80
85
|
for (var i = 0; i < opts.parameters.length; i += 1) {
|
|
81
|
-
var p = _normaliseParameter(opts.parameters[i], "
|
|
86
|
+
var p = _normaliseParameter(opts.parameters[i], label + ": parameters[" + i + "]");
|
|
82
87
|
op.parameters.push(p);
|
|
83
88
|
if (p.in === "path") declaredPathParams[p.name] = true;
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
|
-
// Verify every {placeholder} in path is declared.
|
|
87
|
-
var placeholders = _extractPathParams(urlPattern);
|
|
88
|
-
for (var j = 0; j < placeholders.length; j += 1) {
|
|
89
|
-
if (!declaredPathParams[placeholders[j]]) {
|
|
90
|
-
throw new OpenApiError("openapi/missing-path-param",
|
|
91
|
-
"paths.add: path template " + JSON.stringify(urlPattern) +
|
|
92
|
-
" references {" + placeholders[j] +
|
|
93
|
-
"} but no parameter with in=path name=" + JSON.stringify(placeholders[j]) +
|
|
94
|
-
" was declared");
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
91
|
|
|
98
92
|
// Request body
|
|
99
93
|
if (opts.requestBody) {
|
|
100
|
-
op.requestBody = _normaliseRequestBody(opts.requestBody, "
|
|
94
|
+
op.requestBody = _normaliseRequestBody(opts.requestBody, label + ": requestBody");
|
|
101
95
|
}
|
|
102
96
|
|
|
103
97
|
// Responses (required)
|
|
104
98
|
if (!opts.responses || typeof opts.responses !== "object") {
|
|
105
99
|
throw new OpenApiError("openapi/missing-responses",
|
|
106
|
-
"
|
|
100
|
+
label + ": responses object is required (per OpenAPI 3.1 §4.8.5)");
|
|
107
101
|
}
|
|
108
|
-
op.responses = _normaliseResponses(opts.responses, "
|
|
102
|
+
op.responses = _normaliseResponses(opts.responses, label + ": responses");
|
|
109
103
|
|
|
110
104
|
if (Array.isArray(opts.security)) op.security = opts.security.slice();
|
|
111
105
|
if (opts.deprecated === true) op.deprecated = true;
|
|
112
106
|
if (Array.isArray(opts.servers)) op.servers = opts.servers.slice();
|
|
113
107
|
if (opts.externalDocs) op.externalDocs = opts.externalDocs;
|
|
108
|
+
return op;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
PathsBuilder.prototype.add = function (method, urlPattern, opts) {
|
|
112
|
+
opts = opts || {};
|
|
113
|
+
validateOpts.requireNonEmptyString(urlPattern, "paths.add: urlPattern",
|
|
114
|
+
OpenApiError, "openapi/bad-path");
|
|
115
|
+
if (urlPattern.charAt(0) !== "/") {
|
|
116
|
+
throw new OpenApiError("openapi/bad-path",
|
|
117
|
+
"paths.add: urlPattern must start with '/' - got " +
|
|
118
|
+
JSON.stringify(urlPattern));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
var declaredPathParams = Object.create(null);
|
|
122
|
+
var op = _buildOperation(method, opts, "paths.add", declaredPathParams);
|
|
123
|
+
|
|
124
|
+
// Verify every {placeholder} in path is declared.
|
|
125
|
+
var placeholders = _extractPathParams(urlPattern);
|
|
126
|
+
for (var j = 0; j < placeholders.length; j += 1) {
|
|
127
|
+
if (!declaredPathParams[placeholders[j]]) {
|
|
128
|
+
throw new OpenApiError("openapi/missing-path-param",
|
|
129
|
+
"paths.add: path template " + JSON.stringify(urlPattern) +
|
|
130
|
+
" references {" + placeholders[j] +
|
|
131
|
+
"} but no parameter with in=path name=" + JSON.stringify(placeholders[j]) +
|
|
132
|
+
" was declared");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
114
135
|
|
|
115
136
|
if (!this._paths[urlPattern]) this._paths[urlPattern] = {};
|
|
116
137
|
if (this._paths[urlPattern][method.toLowerCase()]) {
|
|
@@ -240,8 +261,63 @@ PathsBuilder.prototype.toMap = function () {
|
|
|
240
261
|
return out;
|
|
241
262
|
};
|
|
242
263
|
|
|
264
|
+
// WebhooksBuilder — the top-level `webhooks` field is a map of named
|
|
265
|
+
// Path Item Objects describing requests the API initiates out-of-band
|
|
266
|
+
// (OpenAPI 3.2 §4.8.2 Fixed Fields → `webhooks`; unchanged from
|
|
267
|
+
// 3.1.0 §4.1). Keys are free-form webhook names (e.g. "newPet"), NOT
|
|
268
|
+
// URL templates — no `/`-prefix and no path-template-placeholder
|
|
269
|
+
// validation. Each named entry holds the same Operation Objects as a
|
|
270
|
+
// regular path item.
|
|
271
|
+
function WebhooksBuilder() {
|
|
272
|
+
// Null-prototype map so a free-form webhook name that collides with an
|
|
273
|
+
// Object.prototype member (__proto__ / constructor / prototype) becomes
|
|
274
|
+
// an own property instead of mutating the prototype (CWE-1321).
|
|
275
|
+
this._webhooks = Object.create(null);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
WebhooksBuilder.prototype.add = function (name, method, opts) {
|
|
279
|
+
opts = opts || {};
|
|
280
|
+
validateOpts.requireNonEmptyString(name, "webhook.add: name",
|
|
281
|
+
OpenApiError, "openapi/bad-webhook");
|
|
282
|
+
if (name === "__proto__" || name === "constructor" || name === "prototype") {
|
|
283
|
+
throw new OpenApiError("openapi/bad-webhook",
|
|
284
|
+
"webhook.add: name must not be a reserved object key (" + JSON.stringify(name) + ")");
|
|
285
|
+
}
|
|
286
|
+
var declaredPathParams = Object.create(null);
|
|
287
|
+
var op = _buildOperation(method, opts, "webhook.add", declaredPathParams);
|
|
288
|
+
if (!this._webhooks[name]) this._webhooks[name] = {};
|
|
289
|
+
if (this._webhooks[name][method.toLowerCase()]) {
|
|
290
|
+
throw new OpenApiError("openapi/duplicate-operation",
|
|
291
|
+
"webhook.add: duplicate operation " + method.toUpperCase() +
|
|
292
|
+
" on webhook " + JSON.stringify(name));
|
|
293
|
+
}
|
|
294
|
+
this._webhooks[name][method.toLowerCase()] = op;
|
|
295
|
+
return op;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
WebhooksBuilder.prototype.count = function () {
|
|
299
|
+
return Object.keys(this._webhooks).length;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
WebhooksBuilder.prototype.toMap = function () {
|
|
303
|
+
var sorted = Object.keys(this._webhooks).sort();
|
|
304
|
+
var out = {};
|
|
305
|
+
for (var i = 0; i < sorted.length; i += 1) {
|
|
306
|
+
var name = sorted[i];
|
|
307
|
+
var item = this._webhooks[name];
|
|
308
|
+
var ordered = {};
|
|
309
|
+
for (var j = 0; j < VALID_METHODS.length; j += 1) {
|
|
310
|
+
var method = VALID_METHODS[j];
|
|
311
|
+
if (item[method]) ordered[method] = item[method];
|
|
312
|
+
}
|
|
313
|
+
out[name] = ordered;
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
};
|
|
317
|
+
|
|
243
318
|
module.exports = {
|
|
244
319
|
PathsBuilder: PathsBuilder,
|
|
320
|
+
WebhooksBuilder: WebhooksBuilder,
|
|
245
321
|
VALID_METHODS: VALID_METHODS,
|
|
246
322
|
_extractPathParams: _extractPathParams,
|
|
247
323
|
OpenApiError: OpenApiError,
|