@blamejs/core 0.8.76 → 0.8.77

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.
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.middleware.protectedResourceMetadata
4
+ * @nav Identity & access
5
+ * @title Protected Resource Metadata
6
+ * @order 210
7
+ * @slug protected-resource-metadata
8
+ *
9
+ * @intro
10
+ * draft-ietf-oauth-resource-metadata: serves the
11
+ * `/.well-known/oauth-protected-resource` document so RFC 9728
12
+ * clients can auto-discover which authorization servers issue
13
+ * tokens for this resource, what scopes the resource accepts,
14
+ * what dpop algorithms the resource verifies, and which bearer-
15
+ * method binding (DPoP / mTLS cnf claim) is required. Pairs with
16
+ * b.middleware.bearerAuth so a 401 from the protected resource
17
+ * includes `WWW-Authenticate: Bearer resource_metadata=<url>` and
18
+ * the client can self-rediscover.
19
+ *
20
+ * @card
21
+ * `.well-known/oauth-protected-resource` discovery endpoint per
22
+ * draft-ietf-oauth-resource-metadata. Closes the gap that previously
23
+ * forced operators to hand-write the JSON.
24
+ */
25
+
26
+ var framework_error = require("../framework-error");
27
+ var validateOpts = require("../validate-opts");
28
+ var requestHelpers = require("../request-helpers");
29
+
30
+ var H = requestHelpers.HTTP_STATUS;
31
+
32
+ var ProtectedResourceMetadataError = framework_error.defineClass(
33
+ "ProtectedResourceMetadataError",
34
+ "middleware/protected-resource-metadata"
35
+ );
36
+
37
+ var ALLOWED_BEARER_METHODS = ["header", "body", "query"];
38
+ var ALLOWED_DPOP_ALGS = ["ES256", "ES384", "RS256", "PS256", "EdDSA", "ML-DSA-65", "ML-DSA-87"];
39
+
40
+ /**
41
+ * @primitive b.middleware.protectedResourceMetadata
42
+ * @signature b.middleware.protectedResourceMetadata(opts)
43
+ * @since 0.8.77
44
+ * @related b.auth.oauth.introspectToken, b.middleware.bearerAuth
45
+ *
46
+ * Returns a request middleware that serves the protected-resource
47
+ * metadata JSON document at `/.well-known/oauth-protected-resource`
48
+ * (or operator-overridden path).
49
+ *
50
+ * @opts
51
+ * {
52
+ * resource: string, // canonical resource URI (required)
53
+ * authorizationServers: string[], // issuer URLs that mint tokens for this resource (required, ≥1)
54
+ * scopesSupported?: string[],
55
+ * bearerMethodsSupported?: ("header"|"body"|"query")[], // default ["header"]
56
+ * resourceSigningAlgValuesSupported?: string[], // for signed introspection / jwt-secured responses
57
+ * resourceDocumentation?: string, // URL to operator docs
58
+ * resourcePolicyUri?: string,
59
+ * resourceTosUri?: string,
60
+ * dpopSigningAlgValuesSupported?: string[],
61
+ * dpopBoundAccessTokensRequired?: boolean,
62
+ * mtlsBoundAccessTokensRequired?: boolean,
63
+ * path?: string, // default "/.well-known/oauth-protected-resource"
64
+ * }
65
+ *
66
+ * @example
67
+ * var mw = b.middleware.protectedResourceMetadata({
68
+ * resource: "https://api.example.com",
69
+ * authorizationServers: ["https://idp.example.com"],
70
+ * scopesSupported: ["read", "write"],
71
+ * dpopBoundAccessTokensRequired: true,
72
+ * });
73
+ * app.use(mw);
74
+ */
75
+ function create(opts) {
76
+ validateOpts.requireObject(opts, "middleware.protectedResourceMetadata",
77
+ ProtectedResourceMetadataError, "middleware/protected-resource-metadata/bad-opts");
78
+ validateOpts.requireNonEmptyString(opts.resource, "resource",
79
+ ProtectedResourceMetadataError, "middleware/protected-resource-metadata/no-resource");
80
+
81
+ if (!Array.isArray(opts.authorizationServers) || opts.authorizationServers.length === 0) {
82
+ throw new ProtectedResourceMetadataError(
83
+ "middleware/protected-resource-metadata/no-as",
84
+ "authorizationServers must be a non-empty array of issuer URLs");
85
+ }
86
+ opts.authorizationServers.forEach(function (u, i) {
87
+ if (typeof u !== "string" || u.length === 0) {
88
+ throw new ProtectedResourceMetadataError(
89
+ "middleware/protected-resource-metadata/bad-as",
90
+ "authorizationServers[" + i + "] must be a non-empty string");
91
+ }
92
+ });
93
+
94
+ var bearerMethods = opts.bearerMethodsSupported || ["header"];
95
+ bearerMethods.forEach(function (m, i) {
96
+ if (ALLOWED_BEARER_METHODS.indexOf(m) === -1) {
97
+ throw new ProtectedResourceMetadataError(
98
+ "middleware/protected-resource-metadata/bad-bearer-method",
99
+ "bearerMethodsSupported[" + i + "] must be one of: " + ALLOWED_BEARER_METHODS.join(", "));
100
+ }
101
+ });
102
+
103
+ if (opts.dpopSigningAlgValuesSupported) {
104
+ opts.dpopSigningAlgValuesSupported.forEach(function (a, i) {
105
+ if (ALLOWED_DPOP_ALGS.indexOf(a) === -1) {
106
+ throw new ProtectedResourceMetadataError(
107
+ "middleware/protected-resource-metadata/bad-dpop-alg",
108
+ "dpopSigningAlgValuesSupported[" + i + "] = '" + a +
109
+ "' not in allowlist: " + ALLOWED_DPOP_ALGS.join(", "));
110
+ }
111
+ });
112
+ }
113
+
114
+ var path = opts.path || "/.well-known/oauth-protected-resource";
115
+
116
+ var doc = {
117
+ resource: opts.resource,
118
+ authorization_servers: opts.authorizationServers,
119
+ bearer_methods_supported: bearerMethods,
120
+ };
121
+ if (opts.scopesSupported) doc.scopes_supported = opts.scopesSupported;
122
+ if (opts.resourceSigningAlgValuesSupported) doc.resource_signing_alg_values_supported = opts.resourceSigningAlgValuesSupported;
123
+ if (opts.resourceDocumentation) doc.resource_documentation = opts.resourceDocumentation;
124
+ if (opts.resourcePolicyUri) doc.resource_policy_uri = opts.resourcePolicyUri;
125
+ if (opts.resourceTosUri) doc.resource_tos_uri = opts.resourceTosUri;
126
+ if (opts.dpopSigningAlgValuesSupported) doc.dpop_signing_alg_values_supported = opts.dpopSigningAlgValuesSupported;
127
+ if (opts.dpopBoundAccessTokensRequired === true) doc.dpop_bound_access_tokens_required = true;
128
+ if (opts.mtlsBoundAccessTokensRequired === true) doc.tls_client_certificate_bound_access_tokens = true;
129
+
130
+ var bodyText = JSON.stringify(doc);
131
+ var bodyBytes = Buffer.byteLength(bodyText, "utf8");
132
+
133
+ function middleware(req, res, next) {
134
+ if (req.url !== path && req.url.split("?")[0] !== path) {
135
+ next();
136
+ return;
137
+ }
138
+ if (req.method !== "GET" && req.method !== "HEAD") {
139
+ res.writeHead(H.METHOD_NOT_ALLOWED, {
140
+ "Allow": "GET, HEAD",
141
+ "Cache-Control": "no-store",
142
+ });
143
+ res.end();
144
+ return;
145
+ }
146
+ res.writeHead(H.OK, {
147
+ "Content-Type": "application/json",
148
+ "Content-Length": String(bodyBytes),
149
+ "Cache-Control": "public, max-age=3600",
150
+ });
151
+ if (req.method === "HEAD") { res.end(); return; }
152
+ res.end(bodyText);
153
+ }
154
+
155
+ middleware.document = doc;
156
+ middleware.path = path;
157
+ return middleware;
158
+ }
159
+
160
+ module.exports = {
161
+ create: create,
162
+ ProtectedResourceMetadataError: ProtectedResourceMetadataError,
163
+ ALLOWED_BEARER_METHODS: ALLOWED_BEARER_METHODS,
164
+ ALLOWED_DPOP_ALGS: ALLOWED_DPOP_ALGS,
165
+ };
@@ -159,12 +159,16 @@ function _memoryTokenBucketBackend(opts) {
159
159
  buckets.delete(key);
160
160
  }
161
161
 
162
+ function resetAll() {
163
+ buckets.clear();
164
+ }
165
+
162
166
  function close() {
163
167
  try { gcInterval.stop(); } catch (_e) { /* timer already stopped */ }
164
168
  buckets.clear();
165
169
  }
166
170
 
167
- return { take: take, reset: reset, close: close };
171
+ return { take: take, reset: reset, resetAll: resetAll, close: close };
168
172
  }
169
173
 
170
174
  // Fixed-window in-memory algorithm — per-key counter that resets at the
@@ -224,12 +228,14 @@ function _memoryFixedWindowBackend(opts) {
224
228
 
225
229
  function reset(key) { counters.delete(key); }
226
230
 
231
+ function resetAll() { counters.clear(); }
232
+
227
233
  function close() {
228
234
  try { gcInterval.stop(); } catch (_e) { /* timer already stopped */ }
229
235
  counters.clear();
230
236
  }
231
237
 
232
- return { take: take, reset: reset, close: close };
238
+ return { take: take, reset: reset, resetAll: resetAll, close: close };
233
239
  }
234
240
 
235
241
  // ---- Cluster backend (fixed-window counter, SQL-backed) ----
@@ -485,13 +491,63 @@ function create(opts) {
485
491
 
486
492
  // Expose a couple of operator hooks on the middleware function.
487
493
  middleware.reset = function (key) { return backend.reset(key); };
488
- middleware.close = function () { return backend.close && backend.close(); };
494
+ // Global drop-all for the in-memory backend. Used by incident-
495
+ // response workflows ("operator confirmed false-positive lockout
496
+ // wave, drop the whole table") + by test suites that need a clean
497
+ // slate between cases without re-creating the middleware. For the
498
+ // cluster backend this is a no-op (cluster backends are
499
+ // multi-process and require operator-side coordination — flushing
500
+ // a shared row table from one replica races every other replica's
501
+ // in-flight take() calls).
502
+ middleware.resetAll = function () {
503
+ if (typeof backend.resetAll === "function") return backend.resetAll();
504
+ return null;
505
+ };
506
+ middleware.close = function () {
507
+ _instances.delete(middleware);
508
+ return backend.close && backend.close();
509
+ };
489
510
 
511
+ _instances.add(middleware);
490
512
  return middleware;
491
513
  }
492
514
 
515
+ // Module-level registry of every rate-limit middleware in the running
516
+ // process. Operators reach for this during incident response: when a
517
+ // false-positive lockout wave hits, an oncall script can iterate
518
+ // `instances()` and call `.resetAll()` on each, without having to
519
+ // thread a reference to every rate-limit middleware through wherever
520
+ // the response code runs. Tests use it to assert a clean slate.
521
+ //
522
+ // Lifetime: a middleware joins on `create()` return and leaves on
523
+ // `middleware.close()`. Long-lived servers create rate-limiters once at
524
+ // boot; throwaway middlewares (tests, sandboxes) must close() to
525
+ // deregister. We deliberately don't use WeakRef here — operators want
526
+ // strong, observable membership ("did this rate-limiter actually get
527
+ // created?"), and the count is bounded by how many limiters an app
528
+ // configures, not how much traffic it sees.
529
+ var _instances = new Set();
530
+
531
+ function instances() {
532
+ return Array.from(_instances);
533
+ }
534
+
535
+ // Global drop-all across every middleware in the process. Returns the
536
+ // number of instances that responded to resetAll (cluster-backed
537
+ // middlewares no-op their own resetAll but still count toward the
538
+ // total so operators see all instances were addressed).
539
+ function resetAll() {
540
+ var n = 0;
541
+ _instances.forEach(function (m) {
542
+ try { m.resetAll(); n += 1; } catch (_e) { /* best-effort */ }
543
+ });
544
+ return n;
545
+ }
546
+
493
547
  module.exports = {
494
548
  create: create,
549
+ instances: instances,
550
+ resetAll: resetAll,
495
551
  // Backends exported for tests + advanced operator wiring.
496
552
  _memoryBackend: _memoryBackend,
497
553
  _memoryTokenBucketBackend: _memoryTokenBucketBackend,
@@ -0,0 +1,375 @@
1
+ "use strict";
2
+ // SCIM 2.0 server middleware (RFC 7642 / 7643 / 7644).
3
+ // Provides /Users + /Groups + /ServiceProviderConfig + /ResourceTypes
4
+ // + /Schemas surfaces backed by operator-supplied CRUD callbacks.
5
+
6
+ var framework_error = require("../framework-error");
7
+ var validateOpts = require("../validate-opts");
8
+ var safeJson = require("../safe-json");
9
+ var safeBuffer = require("../safe-buffer");
10
+ var requestHelpers = require("../request-helpers");
11
+ var C = require("../constants");
12
+
13
+ var H = requestHelpers.HTTP_STATUS;
14
+
15
+ var ScimServerError = framework_error.defineClass(
16
+ "ScimServerError",
17
+ "middleware/scim-server"
18
+ );
19
+
20
+ var SCIM_CORE_SCHEMA_USER = "urn:ietf:params:scim:schemas:core:2.0:User";
21
+ var SCIM_CORE_SCHEMA_GROUP = "urn:ietf:params:scim:schemas:core:2.0:Group";
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
+
25
+ var ALLOWED_FILTER_OPS = ["eq", "ne", "co", "sw", "ew", "pr", "gt", "ge", "lt", "le"];
26
+
27
+ var SCIM_FILTER_RE = /^\s*([a-zA-Z][a-zA-Z0-9._-]*)\s+(eq|ne|co|sw|ew|pr|gt|ge|lt|le)(?:\s+(.+))?\s*$/;
28
+ var RESOURCE_PATH_RE = /^\/(Users|Groups)(?:\/([^/]+))?$/;
29
+ var BEARER_RE = /^Bearer\s+(.+)$/i;
30
+
31
+ /**
32
+ * @primitive b.middleware.scimServer
33
+ * @signature b.middleware.scimServer(opts)
34
+ * @since 0.8.77
35
+ * @related b.auth.oauth.introspectToken
36
+ *
37
+ * Returns a request middleware that handles SCIM 2.0 requests
38
+ * (RFC 7642-7644). Operator supplies CRUD callbacks per resource.
39
+ *
40
+ * @opts
41
+ * {
42
+ * basePath?: string,
43
+ * users: ScimResourceImpl,
44
+ * groups?: ScimResourceImpl,
45
+ * bearer?: (token) => Promise<actor>,
46
+ * maxPageSize?: number,
47
+ * }
48
+ *
49
+ * @example
50
+ * var mw = b.middleware.scimServer({
51
+ * basePath: "/scim/v2",
52
+ * users: myUserAdapter,
53
+ * groups: myGroupAdapter,
54
+ * });
55
+ * app.use(mw);
56
+ */
57
+ function create(opts) {
58
+ validateOpts.requireObject(opts, "middleware.scimServer",
59
+ ScimServerError, "middleware/scim-server/bad-opts");
60
+ _validateResourceImpl(opts.users, "users");
61
+ if (opts.groups) _validateResourceImpl(opts.groups, "groups");
62
+
63
+ var basePath = opts.basePath || "/scim/v2";
64
+ var maxPageSize = opts.maxPageSize || 200; // allow:raw-byte-literal — page-size count, not bytes
65
+ var bearer = opts.bearer || null;
66
+
67
+ function middleware(req, res, next) {
68
+ var url = req.url.split("?")[0];
69
+ if (url.indexOf(basePath) !== 0) { next(); return; }
70
+ _dispatch(req, res, basePath, bearer, opts, maxPageSize)
71
+ .catch(function (err) {
72
+ _writeScimError(res, err.statusCode || 500, err.scimType || "internal",
73
+ (err.message || String(err)).slice(0, 500));
74
+ });
75
+ }
76
+
77
+ middleware.basePath = basePath;
78
+ middleware.serviceProviderDoc = _serviceProviderConfig(opts);
79
+ return middleware;
80
+ }
81
+
82
+ function _validateResourceImpl(impl, name) {
83
+ if (!impl || typeof impl !== "object") {
84
+ throw new ScimServerError("middleware/scim-server/no-" + name,
85
+ "middleware.scimServer: opts." + name + " must be an object implementing { create, read, update, patch, remove, list }");
86
+ }
87
+ ["create", "read", "update", "patch", "remove", "list"].forEach(function (m) {
88
+ if (typeof impl[m] !== "function") {
89
+ throw new ScimServerError("middleware/scim-server/bad-" + name + "-impl",
90
+ "middleware.scimServer: opts." + name + "." + m + " must be a function");
91
+ }
92
+ });
93
+ }
94
+
95
+ async function _dispatch(req, res, basePath, bearer, opts, maxPageSize) {
96
+ var relUrl = req.url.slice(basePath.length) || "/";
97
+ var qIdx = relUrl.indexOf("?");
98
+ var path = qIdx === -1 ? relUrl : relUrl.slice(0, qIdx);
99
+ var query = _parseQuery(qIdx === -1 ? "" : relUrl.slice(qIdx + 1));
100
+
101
+ if (path === "/ServiceProviderConfig" && req.method === "GET") {
102
+ _writeJson(res, H.OK, _serviceProviderConfig(opts)); return;
103
+ }
104
+ if (path === "/ResourceTypes" && req.method === "GET") {
105
+ _writeJson(res, H.OK, _resourceTypes(opts)); return;
106
+ }
107
+ if (path === "/Schemas" && req.method === "GET") {
108
+ _writeJson(res, H.OK, _schemas()); return;
109
+ }
110
+
111
+ var actor = null;
112
+ if (bearer) {
113
+ var authz = req.headers && req.headers.authorization;
114
+ // BEARER_RE applies to a single header line; node http caps header lines at 8 KiB.
115
+ var m = authz && typeof authz === "string" && authz.length < C.BYTES.kib(8) && BEARER_RE.test(authz) // allow:regex-no-length-cap header line bounded above
116
+ ? authz.match(BEARER_RE) : null;
117
+ if (!m) {
118
+ res.writeHead(H.UNAUTHORIZED, {
119
+ "Content-Type": "application/scim+json",
120
+ "WWW-Authenticate": "Bearer",
121
+ "Cache-Control": "no-store",
122
+ });
123
+ res.end(JSON.stringify({
124
+ schemas: [SCIM_MESSAGE_ERROR],
125
+ status: "401",
126
+ detail: "Missing Bearer token",
127
+ }));
128
+ return;
129
+ }
130
+ try { actor = await bearer(m[1]); }
131
+ catch (_e) { actor = null; }
132
+ if (!actor) {
133
+ res.writeHead(H.UNAUTHORIZED, {
134
+ "Content-Type": "application/scim+json",
135
+ "WWW-Authenticate": 'Bearer error="invalid_token"',
136
+ "Cache-Control": "no-store",
137
+ });
138
+ res.end(JSON.stringify({
139
+ schemas: [SCIM_MESSAGE_ERROR],
140
+ status: "401",
141
+ detail: "Bearer token rejected",
142
+ }));
143
+ return;
144
+ }
145
+ }
146
+
147
+ var ctx = { actor: actor, req: req };
148
+ var users = opts.users;
149
+ var groups = opts.groups;
150
+
151
+ // RESOURCE_PATH_RE applies to a URL path; node http caps URL at 8 KiB.
152
+ 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
+ if (!match) {
154
+ _writeScimError(res, H.NOT_FOUND, "notFound", "no SCIM resource at " + path);
155
+ return;
156
+ }
157
+ var resourceType = match[1];
158
+ var resourceId = match[2] || null;
159
+ var impl = resourceType === "Users" ? users : groups;
160
+ if (!impl) {
161
+ _writeScimError(res, 404, "notFound", "/" + resourceType + " not configured");
162
+ return;
163
+ }
164
+
165
+ var body = null;
166
+ if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
167
+ body = await _readJsonBody(req);
168
+ }
169
+
170
+ if (req.method === "GET" && !resourceId) {
171
+ var filter = _parseFilter(query.filter);
172
+ var pageSize = Math.min(maxPageSize, parseInt(query.count || "100", 10) || 100);
173
+ var startIndex = Math.max(1, parseInt(query.startIndex || "1", 10) || 1);
174
+ var listRv = await impl.list({
175
+ filter: filter,
176
+ startIndex: startIndex,
177
+ count: pageSize,
178
+ sortBy: query.sortBy || null,
179
+ sortOrder: query.sortOrder || null,
180
+ attributes: query.attributes ? query.attributes.split(",") : null,
181
+ excludedAttributes: query.excludedAttributes ? query.excludedAttributes.split(",") : null,
182
+ }, ctx);
183
+ _writeJson(res, H.OK, {
184
+ schemas: [SCIM_MESSAGE_LIST],
185
+ totalResults: listRv.totalResults,
186
+ startIndex: startIndex,
187
+ itemsPerPage: listRv.Resources.length,
188
+ Resources: listRv.Resources,
189
+ });
190
+ return;
191
+ }
192
+
193
+ if (req.method === "GET" && resourceId) {
194
+ var rec = await impl.read(resourceId, ctx);
195
+ if (!rec) { _writeScimError(res, H.NOT_FOUND, "notFound", "no resource with id " + resourceId); return; }
196
+ _writeJson(res, H.OK, rec);
197
+ return;
198
+ }
199
+
200
+ if (req.method === "POST" && !resourceId) {
201
+ _assertSchema(body, resourceType === "Users" ? SCIM_CORE_SCHEMA_USER : SCIM_CORE_SCHEMA_GROUP);
202
+ var created = await impl.create(body, ctx);
203
+ _writeJson(res, H.CREATED, created);
204
+ return;
205
+ }
206
+
207
+ if (req.method === "PUT" && resourceId) {
208
+ _assertSchema(body, resourceType === "Users" ? SCIM_CORE_SCHEMA_USER : SCIM_CORE_SCHEMA_GROUP);
209
+ var updated = await impl.update(resourceId, body, ctx);
210
+ _writeJson(res, H.OK, updated);
211
+ return;
212
+ }
213
+
214
+ if (req.method === "PATCH" && resourceId) {
215
+ if (!body || !Array.isArray(body.Operations) || body.Operations.length === 0) {
216
+ _writeScimError(res, H.BAD_REQUEST, "invalidValue", "PATCH body must include Operations[]");
217
+ return;
218
+ }
219
+ body.Operations.forEach(function (op, i) {
220
+ if (!op || typeof op !== "object" || typeof op.op !== "string") {
221
+ var e = new Error("Operations[" + i + "] missing op");
222
+ e.statusCode = H.BAD_REQUEST; e.scimType = "invalidValue";
223
+ throw e;
224
+ }
225
+ var verb = op.op.toLowerCase();
226
+ if (verb !== "add" && verb !== "remove" && verb !== "replace") {
227
+ var e2 = new Error("Operations[" + i + "].op = '" + op.op + "' not in add/remove/replace");
228
+ e2.statusCode = H.BAD_REQUEST; e2.scimType = "invalidValue";
229
+ throw e2;
230
+ }
231
+ });
232
+ var patched = await impl.patch(resourceId, body.Operations, ctx);
233
+ _writeJson(res, H.OK, patched);
234
+ return;
235
+ }
236
+
237
+ if (req.method === "DELETE" && resourceId) {
238
+ await impl.remove(resourceId, ctx);
239
+ res.writeHead(H.NO_CONTENT, { "Cache-Control": "no-store" });
240
+ res.end();
241
+ return;
242
+ }
243
+
244
+ _writeScimError(res, H.METHOD_NOT_ALLOWED, "noTarget", req.method + " not allowed on " + path);
245
+ }
246
+
247
+ function _parseQuery(qs) {
248
+ var out = {};
249
+ if (!qs) return out;
250
+ qs.split("&").forEach(function (pair) {
251
+ var eq = pair.indexOf("=");
252
+ var k = eq === -1 ? pair : pair.slice(0, eq);
253
+ var v = eq === -1 ? "" : pair.slice(eq + 1);
254
+ try { out[decodeURIComponent(k)] = decodeURIComponent(v.replace(/\+/g, " ")); }
255
+ catch (_e) { /* skip malformed */ }
256
+ });
257
+ return out;
258
+ }
259
+
260
+ function _parseFilter(filter) {
261
+ if (typeof filter !== "string" || filter.length === 0) return null;
262
+ if (!SCIM_FILTER_RE.test(filter)) return { raw: filter };
263
+ var m = filter.match(SCIM_FILTER_RE);
264
+ var op = m[2].toLowerCase();
265
+ if (ALLOWED_FILTER_OPS.indexOf(op) === -1) return { raw: filter };
266
+ var rv = { attribute: m[1], op: op, raw: filter };
267
+ if (op === "pr") return rv;
268
+ var v = (m[3] || "").trim();
269
+ if (v.charAt(0) === '"' && v.charAt(v.length - 1) === '"') {
270
+ v = v.slice(1, -1).replace(/\\"/g, '"');
271
+ }
272
+ rv.value = v;
273
+ return rv;
274
+ }
275
+
276
+ function _readJsonBody(req) {
277
+ var MAX = C.BYTES.mib(1);
278
+ if (req.body && Buffer.isBuffer(req.body)) {
279
+ return Promise.resolve(safeJson.parse(req.body.toString("utf8"), { maxBytes: MAX }));
280
+ }
281
+ if (req.body && typeof req.body === "object") return Promise.resolve(req.body);
282
+ return safeBuffer.boundedChunkCollector(req, MAX, ScimServerError, "middleware/scim-server/body-too-large")
283
+ .then(function (buf) {
284
+ return safeJson.parse(buf.toString("utf8"), { maxBytes: MAX });
285
+ });
286
+ }
287
+
288
+ function _assertSchema(body, expectedSchema) {
289
+ if (!body || typeof body !== "object") {
290
+ var e = new Error("request body must be a JSON object");
291
+ e.statusCode = H.BAD_REQUEST; e.scimType = "invalidValue"; throw e;
292
+ }
293
+ if (!Array.isArray(body.schemas) || body.schemas.indexOf(expectedSchema) === -1) {
294
+ var e2 = new Error("body.schemas must include '" + expectedSchema + "'");
295
+ e2.statusCode = H.BAD_REQUEST; e2.scimType = "invalidValue"; throw e2;
296
+ }
297
+ }
298
+
299
+ function _writeJson(res, status, body) {
300
+ res.writeHead(status, {
301
+ "Content-Type": "application/scim+json",
302
+ "Cache-Control": "no-store",
303
+ });
304
+ res.end(JSON.stringify(body));
305
+ }
306
+
307
+ function _writeScimError(res, status, scimType, detail) {
308
+ res.writeHead(status, {
309
+ "Content-Type": "application/scim+json",
310
+ "Cache-Control": "no-store",
311
+ });
312
+ res.end(JSON.stringify({
313
+ schemas: [SCIM_MESSAGE_ERROR],
314
+ status: String(status),
315
+ scimType: scimType,
316
+ detail: detail,
317
+ }));
318
+ }
319
+
320
+ function _serviceProviderConfig(opts) {
321
+ return {
322
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
323
+ documentationUri: opts.documentationUri || "https://datatracker.ietf.org/doc/html/rfc7643",
324
+ patch: { supported: true },
325
+ bulk: { supported: false, maxOperations: 0, maxPayloadSize: 0 },
326
+ filter: { supported: true, maxResults: opts.maxPageSize || 200 },
327
+ changePassword: { supported: false },
328
+ sort: { supported: true },
329
+ etag: { supported: false },
330
+ authenticationSchemes: [
331
+ {
332
+ type: "oauthbearertoken",
333
+ name: "OAuth 2.0 Bearer Token",
334
+ description: "Authentication scheme using the OAuth Bearer Token Standard",
335
+ specUri: "https://www.rfc-editor.org/info/rfc6750",
336
+ primary: true,
337
+ },
338
+ ],
339
+ };
340
+ }
341
+
342
+ function _resourceTypes(opts) {
343
+ var rv = [
344
+ {
345
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
346
+ id: "User", name: "User", endpoint: "/Users",
347
+ description: "User resource (RFC 7643 §4.1)",
348
+ schema: SCIM_CORE_SCHEMA_USER,
349
+ },
350
+ ];
351
+ if (opts.groups) {
352
+ rv.push({
353
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
354
+ id: "Group", name: "Group", endpoint: "/Groups",
355
+ description: "Group resource (RFC 7643 §4.2)",
356
+ schema: SCIM_CORE_SCHEMA_GROUP,
357
+ });
358
+ }
359
+ return rv;
360
+ }
361
+
362
+ function _schemas() {
363
+ return [
364
+ { id: SCIM_CORE_SCHEMA_USER, name: "User", description: "RFC 7643 §4.1 User resource" },
365
+ { id: SCIM_CORE_SCHEMA_GROUP, name: "Group", description: "RFC 7643 §4.2 Group resource" },
366
+ ];
367
+ }
368
+
369
+ module.exports = {
370
+ create: create,
371
+ ScimServerError: ScimServerError,
372
+ SCIM_CORE_SCHEMA_USER: SCIM_CORE_SCHEMA_USER,
373
+ SCIM_CORE_SCHEMA_GROUP: SCIM_CORE_SCHEMA_GROUP,
374
+ ALLOWED_FILTER_OPS: ALLOWED_FILTER_OPS,
375
+ };
@@ -71,6 +71,18 @@ var DEFAULT_PERMISSIONS = [
71
71
  // embedding APIs; deny-by-default.
72
72
  "storage-access=()", "browsing-topics=()",
73
73
  "private-aggregation=()", "controlled-frame=()", "captured-surface-control=()",
74
+ // v0.8.77 expansion — remaining Privacy Sandbox + Browser-API
75
+ // directives surfacing through Chrome 130+ / Firefox 132+ stable.
76
+ // Default-deny: FedCM (identity-credentials-get), cross-site
77
+ // attribution reporting, WebAuthn create flow (operators that need
78
+ // it opt in explicitly), FLEDGE/Topics auction APIs (join-ad-
79
+ // interest-group / run-ad-auction), Shared Storage API + selectURL,
80
+ // Smart Card API, all-screens capture, deferred-fetch (background
81
+ // resource sync).
82
+ "identity-credentials-get=()", "attribution-reporting-cross-site=()",
83
+ "publickey-credentials-create=()", "join-ad-interest-group=()",
84
+ "run-ad-auction=()", "shared-storage=()", "shared-storage-select-url=()",
85
+ "smartcard=()", "all-screens-capture=()", "deferred-fetch=()",
74
86
  ];
75
87
 
76
88
  // Strict CSP — no 'unsafe-inline' on script-src OR style-src.