@oh-my-pi/pi-utils 18.0.3 → 18.0.5

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 CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.0.5] - 2026-08-25
6
+
7
+ ### Added
8
+
9
+ - Added `stableStringifyJson` for deterministic serialization of nested JSON-shaped data.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed managed Chrome-for-Testing installation failures when extracting the trusted browser download.
14
+
15
+ ## [18.0.4] - 2026-08-24
16
+
17
+ ### Added
18
+
19
+ - Exported `getAvatarCacheDir` to resolve the avatar cache directory path.
20
+
5
21
  ## [18.0.1] - 2026-08-23
6
22
 
7
23
  ### Fixed
@@ -189,6 +189,8 @@ export declare function getLegacyPiExtensionCacheDbPath(): string;
189
189
  * operators can isolate or relocate the cache file.
190
190
  */
191
191
  export declare function getAuthBrokerSnapshotCachePath(): string;
192
+ /** Get the commit-author avatar cache directory (~/.omp/cache/avatars). */
193
+ export declare function getAvatarCacheDir(): string;
192
194
  /** Get the local FastEmbed model cache directory (~/.omp/cache/fastembed). */
193
195
  export declare function getFastembedCacheDir(): string;
194
196
  /** Get the on-demand fastembed runtime install root (~/.omp/cache/fastembed-runtime). */
@@ -12,3 +12,9 @@ export declare function tryParseJson<T = unknown>(content: string): T | null;
12
12
  * only lossless JSON representation.
13
13
  */
14
14
  export declare function stringifyJson(value: unknown, space?: string | number): string | undefined;
15
+ /**
16
+ * Deterministically serialize JSON-shaped data by sorting object keys at every
17
+ * depth while preserving array order. Throws for values JSON cannot represent
18
+ * as a top-level value instead of returning an easy-to-misuse undefined.
19
+ */
20
+ export declare function stableStringifyJson(value: unknown): string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-utils",
4
- "version": "18.0.3",
4
+ "version": "18.0.5",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -31,7 +31,7 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@oh-my-pi/pi-natives": "18.0.3"
34
+ "@oh-my-pi/pi-natives": "18.0.5"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/bun": "^1.3.14"
package/src/browsers.ts CHANGED
@@ -4,11 +4,23 @@ import type * as fs from "node:fs";
4
4
  import * as fsp from "node:fs/promises";
5
5
  import * as os from "node:os";
6
6
  import * as path from "node:path";
7
- import { extractArchive } from "./ar";
7
+ import { type ArchiveLimits, extractArchive } from "./ar";
8
8
 
9
9
  const CHROME_FOR_TESTING_BASE_URL = "https://storage.googleapis.com/chrome-for-testing-public";
10
10
  const CHROME_METADATA_BASE_URL = "https://googlechromelabs.github.io/chrome-for-testing";
11
11
 
12
+ /**
13
+ * Archive ceilings for the managed browser download. The default archive
14
+ * limits (64 MiB per member / 256 MiB in memory) guard against attacker-
15
+ * controlled archives, but the Chrome-for-Testing binary is a trusted
16
+ * first-party download whose `chrome` executable already exceeds both
17
+ * (~269 MB and growing), so extraction gets a ceiling sized for it.
18
+ */
19
+ const BROWSER_ARCHIVE_LIMITS: Partial<ArchiveLimits> = {
20
+ maxMemberSize: 1024 * 1024 * 1024,
21
+ maxInMemorySize: 1024 * 1024 * 1024,
22
+ };
23
+
12
24
  /** Supported browser products. */
13
25
  export enum Browser {
14
26
  CHROME = "chrome",
@@ -242,7 +254,7 @@ export async function install(options: InstallOptions): Promise<InstalledBrowser
242
254
  archivePath,
243
255
  options.downloadProgressCallback,
244
256
  );
245
- await extractArchive(archivePath, stagingPath);
257
+ await extractArchive(archivePath, stagingPath, { limits: BROWSER_ARCHIVE_LIMITS });
246
258
  await fsp.mkdir(path.dirname(installPath), { recursive: true });
247
259
  await fsp.rm(installPath, { recursive: true, force: true });
248
260
  await fsp.rename(stagingPath, installPath);
package/src/dirs.ts CHANGED
@@ -694,6 +694,11 @@ export function getAuthBrokerSnapshotCachePath(): string {
694
694
  return dirs.rootSubdir(path.join("cache", "auth-broker-snapshot.enc"), "cache");
695
695
  }
696
696
 
697
+ /** Get the commit-author avatar cache directory (~/.omp/cache/avatars). */
698
+ export function getAvatarCacheDir(): string {
699
+ return dirs.rootSubdir(path.join("cache", "avatars"), "cache");
700
+ }
701
+
697
702
  /** Get the local FastEmbed model cache directory (~/.omp/cache/fastembed). */
698
703
  export function getFastembedCacheDir(): string {
699
704
  return dirs.rootSubdir(path.join("cache", "fastembed"), "cache");
package/src/json.ts CHANGED
@@ -21,3 +21,26 @@ export function tryParseJson<T = unknown>(content: string): T | null {
21
21
  export function stringifyJson(value: unknown, space?: string | number): string | undefined {
22
22
  return JSON.stringify(value, (_key, item) => (typeof item === "bigint" ? item.toString() : item), space);
23
23
  }
24
+
25
+ function stableJsonClone(value: unknown): unknown {
26
+ if (Array.isArray(value)) return value.map(stableJsonClone);
27
+ if (value !== null && typeof value === "object") {
28
+ const sorted = Object.create(null) as Record<string, unknown>;
29
+ for (const key of Object.keys(value).sort()) {
30
+ sorted[key] = stableJsonClone(Reflect.get(value, key));
31
+ }
32
+ return sorted;
33
+ }
34
+ return value;
35
+ }
36
+
37
+ /**
38
+ * Deterministically serialize JSON-shaped data by sorting object keys at every
39
+ * depth while preserving array order. Throws for values JSON cannot represent
40
+ * as a top-level value instead of returning an easy-to-misuse undefined.
41
+ */
42
+ export function stableStringifyJson(value: unknown): string {
43
+ const serialized = JSON.stringify(stableJsonClone(value));
44
+ if (serialized === undefined) throw new TypeError("Value is not JSON-serializable");
45
+ return serialized;
46
+ }