From 0aa6ba5391ad07c2a3b16bd5455ff7f99ad78db2 Mon Sep 17 00:00:00 2001 From: George Oastler Date: Fri, 26 Jun 2026 02:51:19 +0100 Subject: [PATCH 1/2] refactor(util): add arrayBufferToBase64, hasTouchSupport, prependProtocolToUrl; export hammingDistance --- packages/util/src/arrayBufferToBase64.ts | 24 ++++++++++++++++++++++++ packages/util/src/hasTouchSupport.ts | 23 +++++++++++++++++++++++ packages/util/src/index.ts | 3 +++ packages/util/src/url.ts | 8 ++++++++ 4 files changed, 58 insertions(+) create mode 100644 packages/util/src/arrayBufferToBase64.ts create mode 100644 packages/util/src/hasTouchSupport.ts diff --git a/packages/util/src/arrayBufferToBase64.ts b/packages/util/src/arrayBufferToBase64.ts new file mode 100644 index 0000000000..caaeb58e04 --- /dev/null +++ b/packages/util/src/arrayBufferToBase64.ts @@ -0,0 +1,24 @@ +// Copyright 2021-2026 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** Encodes an ArrayBuffer as a base64 string. */ +export function arrayBufferToBase64(arrayBuffer: ArrayBuffer): string { + let binary = ""; + const bytes = new Uint8Array(arrayBuffer); + const len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i] as number); + } + return btoa(binary); +} diff --git a/packages/util/src/hasTouchSupport.ts b/packages/util/src/hasTouchSupport.ts new file mode 100644 index 0000000000..e447603976 --- /dev/null +++ b/packages/util/src/hasTouchSupport.ts @@ -0,0 +1,23 @@ +// Copyright 2021-2026 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Classifies the current device as "mobile" or "desktop" based on touch support. + * Browser-only: relies on `window`/`navigator`. + */ +export const hasTouchSupport = (): "mobile" | "desktop" => { + return "ontouchstart" in window || navigator.maxTouchPoints > 0 + ? "mobile" + : "desktop"; +}; diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 29d0c895ff..fae6685035 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -25,6 +25,9 @@ export * from "./choice.js"; export * from "./permutations.js"; export * from "./version.js"; export * from "./hex.js"; +export * from "./hammingDistance.js"; +export * from "./arrayBufferToBase64.js"; +export * from "./hasTouchSupport.js"; export * from "./checks.js"; export * from "./ip.js"; export { verifyRecency } from "./verifyRecency.js"; diff --git a/packages/util/src/url.ts b/packages/util/src/url.ts index e55a229988..2c1e6b4a1d 100644 --- a/packages/util/src/url.ts +++ b/packages/util/src/url.ts @@ -27,6 +27,14 @@ const isIPAddress = (hostname: string): boolean => { } }; +/** Prepends `https://` to a URL that has no http(s) protocol. Empty input is returned unchanged. */ +export const prependProtocolToUrl = (url: string): string => { + if (url && url.length > 0 && !url.startsWith("http")) { + return `https://${url}`; + } + return url; +}; + export const getURLProtocol = (url: URL) => { if (!isIPAddress(url.hostname)) { return "https"; From bc6511f00268d107d75bd918518838c798893599 Mon Sep 17 00:00:00 2001 From: George Oastler Date: Fri, 26 Jun 2026 08:16:17 +0100 Subject: [PATCH 2/2] refactor: add getCurrentPeriod to util; add shared modifyExportsFiles vite plugin --- dev/config/src/vite/index.ts | 1 + .../vite/vite-plugin-modify-exports-files.ts | 35 +++++++++++++++++++ packages/util/src/date.ts | 16 +++++++++ 3 files changed, 52 insertions(+) create mode 100644 dev/config/src/vite/vite-plugin-modify-exports-files.ts diff --git a/dev/config/src/vite/index.ts b/dev/config/src/vite/index.ts index e3aebb1cbb..c2820d090e 100644 --- a/dev/config/src/vite/index.ts +++ b/dev/config/src/vite/index.ts @@ -23,3 +23,4 @@ export { default as ViteEsmConfig } from "./vite.esm.config.js"; export { default as VitePluginRemoveUnusedTranslations } from "./vite-plugin-remove-unused-translations.js"; export * from "./TsNoCheckPlugin.js"; export { default as VitePluginCopy } from "./vite-plugin-copy.js"; +export { default as VitePluginModifyExportsFiles } from "./vite-plugin-modify-exports-files.js"; diff --git a/dev/config/src/vite/vite-plugin-modify-exports-files.ts b/dev/config/src/vite/vite-plugin-modify-exports-files.ts new file mode 100644 index 0000000000..ac2a83e1e2 --- /dev/null +++ b/dev/config/src/vite/vite-plugin-modify-exports-files.ts @@ -0,0 +1,35 @@ +// Copyright 2021-2026 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import type { Plugin } from "vite"; + +/** + * Empties the generated `exports2.FILES` array in every emitted chunk so that + * bundled file listings are not leaked into the output. + */ +export default function modifyExportsFilesPlugin(): Plugin { + return { + name: "modify-exports-files", + generateBundle(_options, bundle) { + for (const fileName in bundle) { + const chunk = bundle[fileName]; + if (chunk && chunk.type === "chunk") { + chunk.code = chunk.code.replace( + /exports2\.FILES = \[[\s\S]*?\];/g, + "exports2.FILES = [];\n", + ); + } + } + }, + }; +} diff --git a/packages/util/src/date.ts b/packages/util/src/date.ts index 35bbce7f4a..83801a4c22 100644 --- a/packages/util/src/date.ts +++ b/packages/util/src/date.ts @@ -11,6 +11,22 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +/** + * Returns the start (first ms) and end (final ms) of the current calendar month, + * in local time. + */ +export const getCurrentPeriod = (): { start: Date; end: Date } => { + const now = new Date(); + const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); + // set to final millisecond of the day + lastDayOfMonth.setHours(23, 59, 59, 999); + return { + start: firstDayOfMonth, + end: lastDayOfMonth, + }; +}; + export const getUTCDate = (date: Date) => new Date( Date.UTC(