Use let or const to avoid scope issues and hoisting
var contentDisposition = require('content-disposition');
1/*!2 * express3 * Copyright(c) 2009-2013 TJ Holowaychuk4 * Copyright(c) 2014-2015 Douglas Christopher Wilson5 * MIT Licensed6 */78'use strict';910/**11 * Module dependencies.12 * @private13 */1415var contentDisposition = require('content-disposition');16var createError = require('http-errors')17var deprecate = require('depd')('express');18var encodeUrl = require('encodeurl');19var escapeHtml = require('escape-html');20var http = require('node:http');21var onFinished = require('on-finished');22var mime = require('mime-types')23var path = require('node:path');24var pathIsAbsolute = require('node:path').isAbsolute;25var statuses = require('statuses')26var sign = require('cookie-signature').sign;27var normalizeType = require('./utils').normalizeType;28var normalizeTypes = require('./utils').normalizeTypes;29var setCharset = require('./utils').setCharset;30var cookie = require('cookie');31var send = require('send');32var extname = path.extname;33var resolve = path.resolve;34var vary = require('vary');35const { Buffer } = require('node:buffer');3637/**38 * Response prototype.39 * @public40 */4142var res = Object.create(http.ServerResponse.prototype)4344/**45 * Module exports.46 * @public47 */4849module.exports = res5051/**52 * Set the HTTP status code for the response.53 *54 * Expects an integer value between 100 and 999 inclusive.55 * Throws an error if the provided status code is not an integer or if it's outside the allowable range.56 *57 * @param {number} code - The HTTP status code to set.58 * @return {ServerResponse} - Returns itself for chaining methods.59 * @throws {TypeError} If `code` is not an integer.60 * @throws {RangeError} If `code` is outside the range 100 to 999.61 * @public62 */6364res.status = function status(code) {65 // Check if the status code is not an integer66 if (!Number.isInteger(code)) {67 throw new TypeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`);68 }69 // Check if the status code is outside of Node's valid range70 if (code < 100 || code > 999) {71 throw new RangeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`);72 }7374 this.statusCode = code;75 return this;76};7778/**79 * Set Link header field with the given `links`.80 *81 * Examples:82 *83 * res.links({84 * next: 'http://api.example.com/users?page=2',85 * last: 'http://api.example.com/users?page=5',86 * pages: [87 * 'http://api.example.com/users?page=1',88 * 'http://api.example.com/users?page=2'89 * ]90 * });91 *92 * @param {Object} links93 * @return {ServerResponse}94 * @public95 */9697res.links = function(links) {98 var link = this.get('Link') || '';99 if (link) link += ', ';100 return this.set('Link', link + Object.keys(links).map(function(rel) {101 // Allow multiple links if links[rel] is an array102 if (Array.isArray(links[rel])) {103 return links[rel].map(function (singleLink) {104 return `<${singleLink}>; rel="${rel}"`;105 }).join(', ');106 } else {107 return `<${links[rel]}>; rel="${rel}"`;108 }109 }).join(', '));110};111112/**113 * Send a response.114 *115 * Examples:116 *117 * res.send(Buffer.from('wahoo'));118 * res.send({ some: 'json' });119 * res.send('<p>some html</p>');120 *121 * @param {string|number|boolean|object|Buffer} body122 * @public123 */124125res.send = function send(body) {126 var chunk = body;127 var encoding;128 var req = this.req;129130 // settings131 var app = this.app;132133 switch (typeof chunk) {134 // string defaulting to html135 case 'string':136 encoding = 'utf8';137 const type = this.get('Content-Type');138139 if (typeof type === 'string') {140 this.set('Content-Type', setCharset(type, 'utf-8'));141 } else {142 this.type('html');143 }144 break;145 case 'boolean':146 case 'number':147 case 'object':148 if (chunk === null) {149 chunk = '';150 } else if (ArrayBuffer.isView(chunk)) {151 if (!this.get('Content-Type')) {152 this.type('bin');153 }154 } else {155 return this.json(chunk);156 }157 break;158 }159160 // determine if ETag should be generated161 var etagFn = app.get('etag fn')162 var generateETag = !this.get('ETag') && typeof etagFn === 'function'163164 // populate Content-Length165 var len166 if (chunk !== undefined) {167 if (Buffer.isBuffer(chunk)) {168 // get length of Buffer169 len = chunk.length170 } else if (!generateETag && chunk.length < 1000) {171 // just calculate length when no ETag + small chunk172 len = Buffer.byteLength(chunk, encoding)173 } else {174 // convert chunk to Buffer and calculate175 chunk = Buffer.from(chunk, encoding)176 encoding = undefined;177 len = chunk.length178 }179180 this.set('Content-Length', len);181 }182183 // populate ETag184 var etag;185 if (generateETag && len !== undefined) {186 if ((etag = etagFn(chunk, encoding))) {187 this.set('ETag', etag);188 }189 }190191 // freshness192 if (req.fresh) this.status(304);193194 // strip irrelevant headers195 if (204 === this.statusCode || 304 === this.statusCode) {196 this.removeHeader('Content-Type');197 this.removeHeader('Content-Length');198 this.removeHeader('Transfer-Encoding');199 chunk = '';200 }201202 // alter headers for 205203 if (this.statusCode === 205) {204 this.set('Content-Length', '0')205 this.removeHeader('Transfer-Encoding')206 chunk = ''207 }208209 if (req.method === 'HEAD') {210 // skip body for HEAD211 this.end();212 } else {213 // respond214 this.end(chunk, encoding);215 }216217 return this;218};219220/**221 * Send JSON response.222 *223 * Examples:224 *225 * res.json(null);226 * res.json({ user: 'tj' });227 *228 * @param {string|number|boolean|object} obj229 * @public230 */231232res.json = function json(obj) {233 // settings234 var app = this.app;235 var escape = app.get('json escape')236 var replacer = app.get('json replacer');237 var spaces = app.get('json spaces');238 var body = stringify(obj, replacer, spaces, escape)239240 // content-type241 if (!this.get('Content-Type')) {242 this.set('Content-Type', 'application/json');243 }244245 return this.send(body);246};247248/**249 * Send JSON response with JSONP callback support.250 *251 * Examples:252 *253 * res.jsonp(null);254 * res.jsonp({ user: 'tj' });255 *256 * @param {string|number|boolean|object} obj257 * @public258 */259260res.jsonp = function jsonp(obj) {261 // settings262 var app = this.app;263 var escape = app.get('json escape')264 var replacer = app.get('json replacer');265 var spaces = app.get('json spaces');266 var body = stringify(obj, replacer, spaces, escape)267 var callback = this.req.query[app.get('jsonp callback name')];268269 // content-type270 if (!this.get('Content-Type')) {271 this.set('X-Content-Type-Options', 'nosniff');272 this.set('Content-Type', 'application/json');273 }274275 // fixup callback276 if (Array.isArray(callback)) {277 callback = callback[0];278 }279280 // jsonp281 if (typeof callback === 'string' && callback.length !== 0) {282 this.set('X-Content-Type-Options', 'nosniff');283 this.set('Content-Type', 'text/javascript');284285 // restrict callback charset286 callback = callback.replace(/[^\[\]\w$.]/g, '');287288 if (body === undefined) {289 // empty argument290 body = ''291 } else if (typeof body === 'string') {292 // replace chars not allowed in JavaScript that are in JSON293 body = body294 .replace(/\u2028/g, '\\u2028')295 .replace(/\u2029/g, '\\u2029')296 }297298 // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse"299 // the typeof check is just to reduce client error noise300 body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');';301 }302303 return this.send(body);304};305306/**307 * Send given HTTP status code.308 *309 * Sets the response status to `statusCode` and the body of the310 * response to the standard description from node's http.STATUS_CODES311 * or the statusCode number if no description.312 *313 * Examples:314 *315 * res.sendStatus(200);316 *317 * @param {number} statusCode318 * @public319 */320321res.sendStatus = function sendStatus(statusCode) {322 var body = statuses.message[statusCode] || String(statusCode)323324 this.status(statusCode);325 this.type('txt');326327 return this.send(body);328};329330/**331 * Transfer the file at the given `path`.332 *333 * Automatically sets the _Content-Type_ response header field.334 * The callback `callback(err)` is invoked when the transfer is complete335 * or when an error occurs. Be sure to check `res.headersSent`336 * if you wish to attempt responding, as the header and some data337 * may have already been transferred.338 *339 * Options:340 *341 * - `maxAge` defaulting to 0 (can be string converted by `ms`)342 * - `root` root directory for relative filenames343 * - `headers` object of headers to serve with file344 * - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them345 *346 * Other options are passed along to `send`.347 *348 * Examples:349 *350 * The following example illustrates how `res.sendFile()` may351 * be used as an alternative for the `static()` middleware for352 * dynamic situations. The code backing `res.sendFile()` is actually353 * the same code, so HTTP cache support etc is identical.354 *355 * app.get('/user/:uid/photos/:file', function(req, res){356 * var uid = req.params.uid357 * , file = req.params.file;358 *359 * req.user.mayViewFilesFrom(uid, function(yes){360 * if (yes) {361 * res.sendFile('/uploads/' + uid + '/' + file);362 * } else {363 * res.send(403, 'Sorry! you cant see that.');364 * }365 * });366 * });367 *368 * @public369 */370371res.sendFile = function sendFile(path, options, callback) {372 var done = callback;373 var req = this.req;374 var res = this;375 var next = req.next;376 var opts = options || {};377378 if (!path) {379 throw new TypeError('path argument is required to res.sendFile');380 }381382 if (typeof path !== 'string') {383 throw new TypeError('path must be a string to res.sendFile')384 }385386 // support function as second arg387 if (typeof options === 'function') {388 done = options;389 opts = {};390 }391392 if (!opts.root && !pathIsAbsolute(path)) {393 throw new TypeError('path must be absolute or specify root to res.sendFile');394 }395396 // create file stream397 var pathname = encodeURI(path);398399 // wire application etag option to send400 opts.etag = this.app.enabled('etag');401 var file = send(req, pathname, opts);402403 // transfer404 sendfile(res, file, opts, function (err) {405 if (done) return done(err);406 if (err && err.code === 'EISDIR') return next();407408 // next() all but write errors409 if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') {410 next(err);411 }412 });413};414415/**416 * Transfer the file at the given `path` as an attachment.417 *418 * Optionally providing an alternate attachment `filename`,419 * and optional callback `callback(err)`. The callback is invoked420 * when the data transfer is complete, or when an error has421 * occurred. Be sure to check `res.headersSent` if you plan to respond.422 *423 * Optionally providing an `options` object to use with `res.sendFile()`.424 * This function will set the `Content-Disposition` header, overriding425 * any `Content-Disposition` header passed as header options in order426 * to set the attachment and filename.427 *428 * This method uses `res.sendFile()`.429 *430 * @public431 */432433res.download = function download (path, filename, options, callback) {434 var done = callback;435 var name = filename;436 var opts = options || null437438 // support function as second or third arg439 if (typeof filename === 'function') {440 done = filename;441 name = null;442 opts = null443 } else if (typeof options === 'function') {444 done = options445 opts = null446 }447448 // support optional filename, where options may be in it's place449 if (typeof filename === 'object' &&450 (typeof options === 'function' || options === undefined)) {451 name = null452 opts = filename453 }454455 // set Content-Disposition when file is sent456 var headers = {457 'Content-Disposition': contentDisposition(name || path)458 };459460 // merge user-provided headers461 if (opts && opts.headers) {462 var keys = Object.keys(opts.headers)463 for (var i = 0; i < keys.length; i++) {464 var key = keys[i]465 if (key.toLowerCase() !== 'content-disposition') {466 headers[key] = opts.headers[key]467 }468 }469 }470471 // merge user-provided options472 opts = Object.create(opts)473 opts.headers = headers474475 // Resolve the full path for sendFile476 var fullPath = !opts.root477 ? resolve(path)478 : path479480 // send file481 return this.sendFile(fullPath, opts, done)482};483484/**485 * Set _Content-Type_ response header with `type` through `mime.contentType()`486 * when it does not contain "/", or set the Content-Type to `type` otherwise.487 * When no mapping is found though `mime.contentType()`, the type is set to488 * "application/octet-stream".489 *490 * Examples:491 *492 * res.type('.html');493 * res.type('html');494 * res.type('json');495 * res.type('application/json');496 * res.type('png');497 *498 * @param {String} type499 * @return {ServerResponse} for chaining500 * @public501 */502503res.contentType =504res.type = function contentType(type) {505 var ct = type.indexOf('/') === -1506 ? (mime.contentType(type) || 'application/octet-stream')507 : type;508509 return this.set('Content-Type', ct);510};511512/**513 * Respond to the Acceptable formats using an `obj`514 * of mime-type callbacks.515 *516 * This method uses `req.accepted`, an array of517 * acceptable types ordered by their quality values.518 * When "Accept" is not present the _first_ callback519 * is invoked, otherwise the first match is used. When520 * no match is performed the server responds with521 * 406 "Not Acceptable".522 *523 * Content-Type is set for you, however if you choose524 * you may alter this within the callback using `res.type()`525 * or `res.set('Content-Type', ...)`.526 *527 * res.format({528 * 'text/plain': function(){529 * res.send('hey');530 * },531 *532 * 'text/html': function(){533 * res.send('<p>hey</p>');534 * },535 *536 * 'application/json': function () {537 * res.send({ message: 'hey' });538 * }539 * });540 *541 * In addition to canonicalized MIME types you may542 * also use extnames mapped to these types:543 *544 * res.format({545 * text: function(){546 * res.send('hey');547 * },548 *549 * html: function(){550 * res.send('<p>hey</p>');551 * },552 *553 * json: function(){554 * res.send({ message: 'hey' });555 * }556 * });557 *558 * By default Express passes an `Error`559 * with a `.status` of 406 to `next(err)`560 * if a match is not made. If you provide561 * a `.default` callback it will be invoked562 * instead.563 *564 * @param {Object} obj565 * @return {ServerResponse} for chaining566 * @public567 */568569res.format = function(obj){570 var req = this.req;571 var next = req.next;572573 var keys = Object.keys(obj)574 .filter(function (v) { return v !== 'default' })575576 var key = keys.length > 0577 ? req.accepts(keys)578 : false;579580 this.vary("Accept");581582 if (key) {583 this.set('Content-Type', normalizeType(key).value);584 obj[key](req, this, next);585 } else if (obj.default) {586 obj.default(req, this, next)587 } else {588 next(createError(406, {589 types: normalizeTypes(keys).map(function (o) { return o.value })590 }))591 }592593 return this;594};595596/**597 * Set _Content-Disposition_ header to _attachment_ with optional `filename`.598 *599 * @param {String} filename600 * @return {ServerResponse}601 * @public602 */603604res.attachment = function attachment(filename) {605 if (filename) {606 this.type(extname(filename));607 }608609 this.set('Content-Disposition', contentDisposition(filename));610611 return this;612};613614/**615 * Append additional header `field` with value `val`.616 *617 * Example:618 *619 * res.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);620 * res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');621 * res.append('Warning', '199 Miscellaneous warning');622 *623 * @param {String} field624 * @param {String|Array} val625 * @return {ServerResponse} for chaining626 * @public627 */628629res.append = function append(field, val) {630 var prev = this.get(field);631 var value = val;632633 if (prev) {634 // concat the new and prev vals635 value = Array.isArray(prev) ? prev.concat(val)636 : Array.isArray(val) ? [prev].concat(val)637 : [prev, val]638 }639640 return this.set(field, value);641};642643/**644 * Set header `field` to `val`, or pass645 * an object of header fields.646 *647 * Examples:648 *649 * res.set('Foo', ['bar', 'baz']);650 * res.set('Accept', 'application/json');651 * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });652 *653 * Aliased as `res.header()`.654 *655 * When the set header is "Content-Type", the type is expanded to include656 * the charset if not present using `mime.contentType()`.657 *658 * @param {String|Object} field659 * @param {String|Array} val660 * @return {ServerResponse} for chaining661 * @public662 */663664res.set =665res.header = function header(field, val) {666 if (arguments.length === 2) {667 var value = Array.isArray(val)668 ? val.map(String)669 : String(val);670671 // add charset to content-type672 if (field.toLowerCase() === 'content-type') {673 if (Array.isArray(value)) {674 throw new TypeError('Content-Type cannot be set to an Array');675 }676 value = mime.contentType(value)677 }678679 this.setHeader(field, value);680 } else {681 for (var key in field) {682 this.set(key, field[key]);683 }684 }685 return this;686};687688/**689 * Get value for header `field`.690 *691 * @param {String} field692 * @return {String}693 * @public694 */695696res.get = function(field){697 return this.getHeader(field);698};699700/**701 * Clear cookie `name`.702 *703 * @param {String} name704 * @param {Object} [options]705 * @return {ServerResponse} for chaining706 * @public707 */708709res.clearCookie = function clearCookie(name, options) {710 // Force cookie expiration by setting expires to the past711 const opts = { path: '/', ...options, expires: new Date(1)};712 // ensure maxAge is not passed713 delete opts.maxAge714715 return this.cookie(name, '', opts);716};717718/**719 * Set cookie `name` to `value`, with the given `options`.720 *721 * Options:722 *723 * - `maxAge` max-age in milliseconds, converted to `expires`724 * - `signed` sign the cookie725 * - `path` defaults to "/"726 *727 * Examples:728 *729 * // "Remember Me" for 15 minutes730 * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });731 *732 * // same as above733 * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })734 *735 * @param {String} name736 * @param {String|Object} value737 * @param {Object} [options]738 * @return {ServerResponse} for chaining739 * @public740 */741742res.cookie = function (name, value, options) {743 var opts = { ...options };744 var secret = this.req.secret;745 var signed = opts.signed;746747 if (signed && !secret) {748 throw new Error('cookieParser("secret") required for signed cookies');749 }750751 var val = typeof value === 'object'752 ? 'j:' + JSON.stringify(value)753 : String(value);754755 if (signed) {756 val = 's:' + sign(val, secret);757 }758759 if (opts.maxAge != null) {760 var maxAge = opts.maxAge - 0761762 if (!isNaN(maxAge)) {763 opts.expires = new Date(Date.now() + maxAge)764 opts.maxAge = Math.floor(maxAge / 1000)765 }766 }767768 if (opts.path == null) {769 opts.path = '/';770 }771772 this.append('Set-Cookie', cookie.serialize(name, String(val), opts));773774 return this;775};776777/**778 * Set the location header to `url`.779 *780 * The given `url` can also be "back", which redirects781 * to the _Referrer_ or _Referer_ headers or "/".782 *783 * Examples:784 *785 * res.location('/foo/bar').;786 * res.location('http://example.com');787 * res.location('../login');788 *789 * @param {String} url790 * @return {ServerResponse} for chaining791 * @public792 */793794res.location = function location(url) {795 return this.set('Location', encodeUrl(url));796};797798/**799 * Redirect to the given `url` with optional response `status`800 * defaulting to 302.801 *802 * Examples:803 *804 * res.redirect('/foo/bar');805 * res.redirect('http://example.com');806 * res.redirect(301, 'http://example.com');807 * res.redirect('../login'); // /blog/post/1 -> /blog/login808 *809 * @public810 */811812res.redirect = function redirect(url) {813 var address = url;814 var body;815 var status = 302;816817 // allow status / url818 if (arguments.length === 2) {819 status = arguments[0]820 address = arguments[1]821 }822823 if (!address) {824 deprecate('Provide a url argument');825 }826827 if (typeof address !== 'string') {828 deprecate('Url must be a string');829 }830831 if (typeof status !== 'number') {832 deprecate('Status must be a number');833 }834835 // Set location header836 address = this.location(address).get('Location');837838 // Support text/{plain,html} by default839 this.format({840 text: function(){841 body = statuses.message[status] + '. Redirecting to ' + address842 },843844 html: function(){845 var u = escapeHtml(address);846 body = '<!DOCTYPE html><head><title>' + statuses.message[status] + '</title></head>'847 + '<body><p>' + statuses.message[status] + '. Redirecting to ' + u + '</p></body>'848 },849850 default: function(){851 body = '';852 }853 });854855 // Respond856 this.status(status);857 this.set('Content-Length', Buffer.byteLength(body));858859 if (this.req.method === 'HEAD') {860 this.end();861 } else {862 this.end(body);863 }864};865866/**867 * Add `field` to Vary. If already present in the Vary set, then868 * this call is simply ignored.869 *870 * @param {Array|String} field871 * @return {ServerResponse} for chaining872 * @public873 */874875res.vary = function(field){876 vary(this, field);877878 return this;879};880881/**882 * Render `view` with the given `options` and optional callback `fn`.883 * When a callback function is given a response will _not_ be made884 * automatically, otherwise a response of _200_ and _text/html_ is given.885 *886 * Options:887 *888 * - `cache` boolean hinting to the engine it should cache889 * - `filename` filename of the view being rendered890 *891 * @public892 */893894res.render = function render(view, options, callback) {895 var app = this.req.app;896 var done = callback;897 var opts = options || {};898 var req = this.req;899 var self = this;900901 // support callback function as second arg902 if (typeof options === 'function') {903 done = options;904 opts = {};905 }906907 // merge res.locals908 opts._locals = self.locals;909910 // default callback to respond911 done = done || function (err, str) {912 if (err) return req.next(err);913 self.send(str);914 };915916 // render917 app.render(view, opts, done);918};919920// pipe the send file stream921function sendfile(res, file, options, callback) {922 var done = false;923 var streaming;924925 // request aborted926 function onaborted() {927 if (done) return;928 done = true;929930 var err = new Error('Request aborted');931 err.code = 'ECONNABORTED';932 callback(err);933 }934935 // directory936 function ondirectory() {937 if (done) return;938 done = true;939940 var err = new Error('EISDIR, read');941 err.code = 'EISDIR';942 callback(err);943 }944945 // errors946 function onerror(err) {947 if (done) return;948 done = true;949 callback(err);950 }951952 // ended953 function onend() {954 if (done) return;955 done = true;956 callback();957 }958959 // file960 function onfile() {961 streaming = false;962 }963964 // finished965 function onfinish(err) {966 if (err && err.code === 'ECONNRESET') return onaborted();967 if (err) return onerror(err);968 if (done) return;969970 setImmediate(function () {971 if (streaming !== false && !done) {972 onaborted();973 return;974 }975976 if (done) return;977 done = true;978 callback();979 });980 }981982 // streaming983 function onstream() {984 streaming = true;985 }986987 file.on('directory', ondirectory);988 file.on('end', onend);989 file.on('error', onerror);990 file.on('file', onfile);991 file.on('stream', onstream);992 onFinished(res, onfinish);993994 if (options.headers) {995 // set headers on successful transfer996 file.on('headers', function headers(res) {997 var obj = options.headers;998 var keys = Object.keys(obj);9991000 for (var i = 0; i < keys.length; i++) {1001 var k = keys[i];1002 res.setHeader(k, obj[k]);1003 }1004 });1005 }10061007 // pipe1008 file.pipe(res);1009}10101011/**1012 * Stringify JSON, like JSON.stringify, but v8 optimized, with the1013 * ability to escape characters that can trigger HTML sniffing.1014 *1015 * @param {*} value1016 * @param {function} replacer1017 * @param {number} spaces1018 * @param {boolean} escape1019 * @returns {string}1020 * @private1021 */10221023function stringify (value, replacer, spaces, escape) {1024 // v8 checks arguments.length for optimizing simple call1025 // https://bugs.chromium.org/p/v8/issues/detail?id=47301026 var json = replacer || spaces1027 ? JSON.stringify(value, replacer, spaces)1028 : JSON.stringify(value);10291030 if (escape && typeof json === 'string') {1031 json = json.replace(/[<>&]/g, function (c) {1032 switch (c.charCodeAt(0)) {1033 case 0x3c:1034 return '\\u003c'1035 case 0x3e:1036 return '\\u003e'1037 case 0x26:1038 return '\\u0026'1039 /* istanbul ignore next: unreachable default */1040 default:1041 return c1042 }1043 })1044 }10451046 return json1047}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.