feat(security,ui): self-host React deps, Tailwind, fonts; strict CSP; local QR; better selection state

Replace CDN React/ReactDOM/Babel with local libs; remove Babel and inline scripts
Build Tailwind locally, add safelist; switch to assets/tailwind.css
Self-host Font Awesome and Inter (CSS + woff2); remove external font CDNs
Implement strict CSP (no unsafe-inline/eval; scripts/styles/fonts from self)
Extract inline handlers; move PWA scripts to external files
Add local QR code generation (qrcode lib) and remove api.qrserver.com
Improve SessionTypeSelector visual selection (highlighted background and ring)
Keep PWA working with service worker and offline assets
Refs: CSP hardening, offline-first, no external dependencies
This commit is contained in:
lockbitchat
2025-09-08 16:04:58 -04:00
parent 3458270477
commit 0f8399ec88
352 changed files with 84907 additions and 4257 deletions

63
node_modules/qrcode/lib/renderer/canvas.js generated vendored Normal file
View File

@@ -0,0 +1,63 @@
const Utils = require('./utils')
function clearCanvas (ctx, canvas, size) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
if (!canvas.style) canvas.style = {}
canvas.height = size
canvas.width = size
canvas.style.height = size + 'px'
canvas.style.width = size + 'px'
}
function getCanvasElement () {
try {
return document.createElement('canvas')
} catch (e) {
throw new Error('You need to specify a canvas element')
}
}
exports.render = function render (qrData, canvas, options) {
let opts = options
let canvasEl = canvas
if (typeof opts === 'undefined' && (!canvas || !canvas.getContext)) {
opts = canvas
canvas = undefined
}
if (!canvas) {
canvasEl = getCanvasElement()
}
opts = Utils.getOptions(opts)
const size = Utils.getImageWidth(qrData.modules.size, opts)
const ctx = canvasEl.getContext('2d')
const image = ctx.createImageData(size, size)
Utils.qrToImageData(image.data, qrData, opts)
clearCanvas(ctx, canvasEl, size)
ctx.putImageData(image, 0, 0)
return canvasEl
}
exports.renderToDataURL = function renderToDataURL (qrData, canvas, options) {
let opts = options
if (typeof opts === 'undefined' && (!canvas || !canvas.getContext)) {
opts = canvas
canvas = undefined
}
if (!opts) opts = {}
const canvasEl = exports.render(qrData, canvas, opts)
const type = opts.type || 'image/png'
const rendererOpts = opts.rendererOpts || {}
return canvasEl.toDataURL(type, rendererOpts.quality)
}

78
node_modules/qrcode/lib/renderer/png.js generated vendored Normal file
View File

@@ -0,0 +1,78 @@
const fs = require('fs')
const PNG = require('pngjs').PNG
const Utils = require('./utils')
exports.render = function render (qrData, options) {
const opts = Utils.getOptions(options)
const pngOpts = opts.rendererOpts
const size = Utils.getImageWidth(qrData.modules.size, opts)
pngOpts.width = size
pngOpts.height = size
const pngImage = new PNG(pngOpts)
Utils.qrToImageData(pngImage.data, qrData, opts)
return pngImage
}
exports.renderToDataURL = function renderToDataURL (qrData, options, cb) {
if (typeof cb === 'undefined') {
cb = options
options = undefined
}
exports.renderToBuffer(qrData, options, function (err, output) {
if (err) cb(err)
let url = 'data:image/png;base64,'
url += output.toString('base64')
cb(null, url)
})
}
exports.renderToBuffer = function renderToBuffer (qrData, options, cb) {
if (typeof cb === 'undefined') {
cb = options
options = undefined
}
const png = exports.render(qrData, options)
const buffer = []
png.on('error', cb)
png.on('data', function (data) {
buffer.push(data)
})
png.on('end', function () {
cb(null, Buffer.concat(buffer))
})
png.pack()
}
exports.renderToFile = function renderToFile (path, qrData, options, cb) {
if (typeof cb === 'undefined') {
cb = options
options = undefined
}
let called = false
const done = (...args) => {
if (called) return
called = true
cb.apply(null, args)
}
const stream = fs.createWriteStream(path)
stream.on('error', done)
stream.on('close', done)
exports.renderToFileStream(stream, qrData, options)
}
exports.renderToFileStream = function renderToFileStream (stream, qrData, options) {
const png = exports.render(qrData, options)
png.pack().pipe(stream)
}

81
node_modules/qrcode/lib/renderer/svg-tag.js generated vendored Normal file
View File

@@ -0,0 +1,81 @@
const Utils = require('./utils')
function getColorAttrib (color, attrib) {
const alpha = color.a / 255
const str = attrib + '="' + color.hex + '"'
return alpha < 1
? str + ' ' + attrib + '-opacity="' + alpha.toFixed(2).slice(1) + '"'
: str
}
function svgCmd (cmd, x, y) {
let str = cmd + x
if (typeof y !== 'undefined') str += ' ' + y
return str
}
function qrToPath (data, size, margin) {
let path = ''
let moveBy = 0
let newRow = false
let lineLength = 0
for (let i = 0; i < data.length; i++) {
const col = Math.floor(i % size)
const row = Math.floor(i / size)
if (!col && !newRow) newRow = true
if (data[i]) {
lineLength++
if (!(i > 0 && col > 0 && data[i - 1])) {
path += newRow
? svgCmd('M', col + margin, 0.5 + row + margin)
: svgCmd('m', moveBy, 0)
moveBy = 0
newRow = false
}
if (!(col + 1 < size && data[i + 1])) {
path += svgCmd('h', lineLength)
lineLength = 0
}
} else {
moveBy++
}
}
return path
}
exports.render = function render (qrData, options, cb) {
const opts = Utils.getOptions(options)
const size = qrData.modules.size
const data = qrData.modules.data
const qrcodesize = size + opts.margin * 2
const bg = !opts.color.light.a
? ''
: '<path ' + getColorAttrib(opts.color.light, 'fill') +
' d="M0 0h' + qrcodesize + 'v' + qrcodesize + 'H0z"/>'
const path =
'<path ' + getColorAttrib(opts.color.dark, 'stroke') +
' d="' + qrToPath(data, size, opts.margin) + '"/>'
const viewBox = 'viewBox="' + '0 0 ' + qrcodesize + ' ' + qrcodesize + '"'
const width = !opts.width ? '' : 'width="' + opts.width + '" height="' + opts.width + '" '
const svgTag = '<svg xmlns="http://www.w3.org/2000/svg" ' + width + viewBox + ' shape-rendering="crispEdges">' + bg + path + '</svg>\n'
if (typeof cb === 'function') {
cb(null, svgTag)
}
return svgTag
}

19
node_modules/qrcode/lib/renderer/svg.js generated vendored Normal file
View File

@@ -0,0 +1,19 @@
const svgTagRenderer = require('./svg-tag')
exports.render = svgTagRenderer.render
exports.renderToFile = function renderToFile (path, qrData, options, cb) {
if (typeof cb === 'undefined') {
cb = options
options = undefined
}
const fs = require('fs')
const svgTag = exports.render(qrData, options)
const xmlStr = '<?xml version="1.0" encoding="utf-8"?>' +
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' +
svgTag
fs.writeFile(path, xmlStr, cb)
}

9
node_modules/qrcode/lib/renderer/terminal.js generated vendored Normal file
View File

@@ -0,0 +1,9 @@
const big = require('./terminal/terminal')
const small = require('./terminal/terminal-small')
exports.render = function (qrData, options, cb) {
if (options && options.small) {
return small.render(qrData, options, cb)
}
return big.render(qrData, options, cb)
}

View File

@@ -0,0 +1,85 @@
const backgroundWhite = '\x1b[47m'
const backgroundBlack = '\x1b[40m'
const foregroundWhite = '\x1b[37m'
const foregroundBlack = '\x1b[30m'
const reset = '\x1b[0m'
const lineSetupNormal = backgroundWhite + foregroundBlack // setup colors
const lineSetupInverse = backgroundBlack + foregroundWhite // setup colors
const createPalette = function (lineSetup, foregroundWhite, foregroundBlack) {
return {
// 1 ... white, 2 ... black, 0 ... transparent (default)
'00': reset + ' ' + lineSetup,
'01': reset + foregroundWhite + '▄' + lineSetup,
'02': reset + foregroundBlack + '▄' + lineSetup,
10: reset + foregroundWhite + '▀' + lineSetup,
11: ' ',
12: '▄',
20: reset + foregroundBlack + '▀' + lineSetup,
21: '▀',
22: '█'
}
}
/**
* Returns code for QR pixel
* @param {boolean[][]} modules
* @param {number} size
* @param {number} x
* @param {number} y
* @return {'0' | '1' | '2'}
*/
const mkCodePixel = function (modules, size, x, y) {
const sizePlus = size + 1
if ((x >= sizePlus) || (y >= sizePlus) || (y < -1) || (x < -1)) return '0'
if ((x >= size) || (y >= size) || (y < 0) || (x < 0)) return '1'
const idx = (y * size) + x
return modules[idx] ? '2' : '1'
}
/**
* Returns code for four QR pixels. Suitable as key in palette.
* @param {boolean[][]} modules
* @param {number} size
* @param {number} x
* @param {number} y
* @return {keyof palette}
*/
const mkCode = function (modules, size, x, y) {
return (
mkCodePixel(modules, size, x, y) +
mkCodePixel(modules, size, x, y + 1)
)
}
exports.render = function (qrData, options, cb) {
const size = qrData.modules.size
const data = qrData.modules.data
const inverse = !!(options && options.inverse)
const lineSetup = options && options.inverse ? lineSetupInverse : lineSetupNormal
const white = inverse ? foregroundBlack : foregroundWhite
const black = inverse ? foregroundWhite : foregroundBlack
const palette = createPalette(lineSetup, white, black)
const newLine = reset + '\n' + lineSetup
let output = lineSetup // setup colors
for (let y = -1; y < size + 1; y += 2) {
for (let x = -1; x < size; x++) {
output += palette[mkCode(data, size, x, y)]
}
output += palette[mkCode(data, size, size, y)] + newLine
}
output += reset
if (typeof cb === 'function') {
cb(null, output)
}
return output
}

49
node_modules/qrcode/lib/renderer/terminal/terminal.js generated vendored Normal file
View File

@@ -0,0 +1,49 @@
// let Utils = require('./utils')
exports.render = function (qrData, options, cb) {
const size = qrData.modules.size
const data = qrData.modules.data
// let opts = Utils.getOptions(options)
// use same scheme as https://github.com/gtanner/qrcode-terminal because it actually works! =)
const black = '\x1b[40m \x1b[0m'
const white = '\x1b[47m \x1b[0m'
let output = ''
const hMargin = Array(size + 3).join(white)
const vMargin = Array(2).join(white)
output += hMargin + '\n'
for (let i = 0; i < size; ++i) {
output += white
for (let j = 0; j < size; j++) {
// let topModule = data[i * size + j]
// let bottomModule = data[(i + 1) * size + j]
output += data[i * size + j] ? black : white// getBlockChar(topModule, bottomModule)
}
// output += white+'\n'
output += vMargin + '\n'
}
output += hMargin + '\n'
if (typeof cb === 'function') {
cb(null, output)
}
return output
}
/*
exports.renderToFile = function renderToFile (path, qrData, options, cb) {
if (typeof cb === 'undefined') {
cb = options
options = undefined
}
let fs = require('fs')
let utf8 = exports.render(qrData, options)
fs.writeFile(path, utf8, cb)
}
*/

71
node_modules/qrcode/lib/renderer/utf8.js generated vendored Normal file
View File

@@ -0,0 +1,71 @@
const Utils = require('./utils')
const BLOCK_CHAR = {
WW: ' ',
WB: '▄',
BB: '█',
BW: '▀'
}
const INVERTED_BLOCK_CHAR = {
BB: ' ',
BW: '▄',
WW: '█',
WB: '▀'
}
function getBlockChar (top, bottom, blocks) {
if (top && bottom) return blocks.BB
if (top && !bottom) return blocks.BW
if (!top && bottom) return blocks.WB
return blocks.WW
}
exports.render = function (qrData, options, cb) {
const opts = Utils.getOptions(options)
let blocks = BLOCK_CHAR
if (opts.color.dark.hex === '#ffffff' || opts.color.light.hex === '#000000') {
blocks = INVERTED_BLOCK_CHAR
}
const size = qrData.modules.size
const data = qrData.modules.data
let output = ''
let hMargin = Array(size + (opts.margin * 2) + 1).join(blocks.WW)
hMargin = Array((opts.margin / 2) + 1).join(hMargin + '\n')
const vMargin = Array(opts.margin + 1).join(blocks.WW)
output += hMargin
for (let i = 0; i < size; i += 2) {
output += vMargin
for (let j = 0; j < size; j++) {
const topModule = data[i * size + j]
const bottomModule = data[(i + 1) * size + j]
output += getBlockChar(topModule, bottomModule, blocks)
}
output += vMargin + '\n'
}
output += hMargin.slice(0, -1)
if (typeof cb === 'function') {
cb(null, output)
}
return output
}
exports.renderToFile = function renderToFile (path, qrData, options, cb) {
if (typeof cb === 'undefined') {
cb = options
options = undefined
}
const fs = require('fs')
const utf8 = exports.render(qrData, options)
fs.writeFile(path, utf8, cb)
}

99
node_modules/qrcode/lib/renderer/utils.js generated vendored Normal file
View File

@@ -0,0 +1,99 @@
function hex2rgba (hex) {
if (typeof hex === 'number') {
hex = hex.toString()
}
if (typeof hex !== 'string') {
throw new Error('Color should be defined as hex string')
}
let hexCode = hex.slice().replace('#', '').split('')
if (hexCode.length < 3 || hexCode.length === 5 || hexCode.length > 8) {
throw new Error('Invalid hex color: ' + hex)
}
// Convert from short to long form (fff -> ffffff)
if (hexCode.length === 3 || hexCode.length === 4) {
hexCode = Array.prototype.concat.apply([], hexCode.map(function (c) {
return [c, c]
}))
}
// Add default alpha value
if (hexCode.length === 6) hexCode.push('F', 'F')
const hexValue = parseInt(hexCode.join(''), 16)
return {
r: (hexValue >> 24) & 255,
g: (hexValue >> 16) & 255,
b: (hexValue >> 8) & 255,
a: hexValue & 255,
hex: '#' + hexCode.slice(0, 6).join('')
}
}
exports.getOptions = function getOptions (options) {
if (!options) options = {}
if (!options.color) options.color = {}
const margin = typeof options.margin === 'undefined' ||
options.margin === null ||
options.margin < 0
? 4
: options.margin
const width = options.width && options.width >= 21 ? options.width : undefined
const scale = options.scale || 4
return {
width: width,
scale: width ? 4 : scale,
margin: margin,
color: {
dark: hex2rgba(options.color.dark || '#000000ff'),
light: hex2rgba(options.color.light || '#ffffffff')
},
type: options.type,
rendererOpts: options.rendererOpts || {}
}
}
exports.getScale = function getScale (qrSize, opts) {
return opts.width && opts.width >= qrSize + opts.margin * 2
? opts.width / (qrSize + opts.margin * 2)
: opts.scale
}
exports.getImageWidth = function getImageWidth (qrSize, opts) {
const scale = exports.getScale(qrSize, opts)
return Math.floor((qrSize + opts.margin * 2) * scale)
}
exports.qrToImageData = function qrToImageData (imgData, qr, opts) {
const size = qr.modules.size
const data = qr.modules.data
const scale = exports.getScale(size, opts)
const symbolSize = Math.floor((size + opts.margin * 2) * scale)
const scaledMargin = opts.margin * scale
const palette = [opts.color.light, opts.color.dark]
for (let i = 0; i < symbolSize; i++) {
for (let j = 0; j < symbolSize; j++) {
let posDst = (i * symbolSize + j) * 4
let pxColor = opts.color.light
if (i >= scaledMargin && j >= scaledMargin &&
i < symbolSize - scaledMargin && j < symbolSize - scaledMargin) {
const iSrc = Math.floor((i - scaledMargin) / scale)
const jSrc = Math.floor((j - scaledMargin) / scale)
pxColor = palette[data[iSrc * size + jSrc] ? 1 : 0]
}
imgData[posDst++] = pxColor.r
imgData[posDst++] = pxColor.g
imgData[posDst++] = pxColor.b
imgData[posDst] = pxColor.a
}
}
}