@danielsimonjr/mathts-core 0.12.0 → 0.13.0

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.
@@ -1090,6 +1090,15 @@ declare function reduce<T, U>(mat: T[] | Matrix<T>, dim: number, callback: (acc:
1090
1090
  */
1091
1091
  declare function scatter<T>(a: SparseMatrix<T>, j: number, w: number[], x: T[] | null, u: number[], mark: number, cindex: number[], f?: (a: T, b: T) => T, inverse?: boolean, update?: boolean, value?: T): void;
1092
1092
 
1093
+ /**
1094
+ * Transpose a 2D array. Private helper used only by `core/src/collection.ts`'s
1095
+ * `_reduce` (Bucket B slice 2 canonical) — mirrors `expression/src/utils/switch.ts`
1096
+ * / `functions/src/utils/switch.ts`, which remain in place unchanged for now
1097
+ * (not part of this slice's named scope). Not exported from `internal.ts`.
1098
+ * @private
1099
+ */
1100
+ declare function _switch<T>(mat: T[][]): T[][];
1101
+
1093
1102
  /**
1094
1103
  * A map facade on a bare object.
1095
1104
  *
@@ -1237,4 +1246,100 @@ interface WasmManifest {
1237
1246
  [fileName: string]: string;
1238
1247
  }
1239
1248
 
1240
- export { type BigNumberValue, type ConfigOptions, DEFAULT_CONFIG, DimensionError, FactoryFunction, type FormatOptions, type GeneralFormatOptions, type IdentifiedValue, Index, IndexError, type LoadingMetrics, type MathJsConfig, MathjsError, Matrix$1 as Matrix, type MemoizedFunction, type NestedArray, type NormalizedFormatOptions, type NumberTypeConfig, ObjectWrappingMap, PartitionedMap, type SplitValue, UnitConstructor, UnitDependencies, type WasmManifest, acosh, arraySize, asinh, assign as assignMap, atanh, broadcastArrays, broadcastSizes, broadcastTo, canDefineProperty, cbrt, checkBroadcastingRules, clone$1 as clone, clone as cloneArray, compareText, concat, containsCollections, copysign, cosh, createEmptyMap, createIndexError, createMap, createUnitClass, deepExtend, deepFlatten, deepForEach, deepForEach$1 as deepForEachArray, deepMap, deepMap$1 as deepMapArray, deepStrictEqual, digits, endsWith, escape, expm1, extend, filter, filterRegExp, flatten, forEach, format$2 as format, format as formatBigNumber, format$1 as formatGeneric, generalize, get$1 as get, getArrayDataType, get as getArrayElement, hasOwnProperty, identify, initial, isEmptyIndex, isInteger, isLegacyFactory, isObjectWrappingMap, isPowZeroAtInfinity, join, last, lazy, log10, log1p, log2, map, mapObject, memoize, nearlyEqual, normalizeFormatOptions, pick, pickShallow, processSizesWildcard, reduce, reshape, resize, roundDigits, safeNumberType, scatter, set, sign, sinh, splitNumber, squeeze, stretch, stringify, tanh, toEngineering$1 as toEngineering, toEngineering as toEngineeringBigNumber, toExponential$1 as toExponential, toExponential as toExponentialBigNumber, toFixed$1 as toFixed, toFixed as toFixedBigNumber, toObject, toPrecision, traverse, unitDependencies, unsqueeze, validate, validateIndex, validateIndexSourceSize, warnOnce };
1249
+ /**
1250
+ * Shared WASM-loader runtime logic — SHA-384 integrity verification + packaged
1251
+ * artifact path resolution — consolidated here so `functions` and `matrix` build
1252
+ * on ONE copy instead of maintaining byte-identical forks.
1253
+ *
1254
+ * Why core: `matrix` cannot import from `functions` (functions depends on matrix,
1255
+ * which would invert the edge and create a cycle), so the historical solution was
1256
+ * to duplicate this logic in both packages. Both packages already depend on
1257
+ * `core`, so `@danielsimonjr/mathts-core/internal` is a home reachable from both
1258
+ * WITHOUT introducing a cycle. Exposed ONLY via the `/internal` subpath (never the
1259
+ * browser-facing `.` entry); every filesystem/crypto access uses a LAZY dynamic
1260
+ * `import()` so this module stays browser-safe (no static `node:` imports).
1261
+ *
1262
+ * SECURITY INVARIANT (CLAUDE.md "Security Invariants" #1): the SHA-384
1263
+ * hash-and-compare-before-instantiate is preserved BYTE-FOR-BYTE from the former
1264
+ * per-package `integrity.ts` copies — same algorithm, same fail-closed/soft-warn
1265
+ * behavior. Do NOT bypass, weaken to a non-cryptographic check, or skip on
1266
+ * streaming compile paths. Regression-covered by
1267
+ * `functions/tests/security/wasm-integrity.test.ts` and
1268
+ * `matrix/tests/security/wasm-integrity.test.ts`.
1269
+ *
1270
+ * The build pipeline writes a JSON manifest beside the .wasm artefact:
1271
+ *
1272
+ * {
1273
+ * "mathts-as.wasm": "sha384-<base64-of-binary-digest>"
1274
+ * }
1275
+ *
1276
+ * At load time the runtime hashes the freshly read buffer with SHA-384 and
1277
+ * compares against the manifest. A mismatch throws — preventing silent
1278
+ * code-injection if the .wasm artefact is tampered with on disk or in transit
1279
+ * (e.g. a CDN/MITM compromise).
1280
+ *
1281
+ * Browser path uses `crypto.subtle.digest('SHA-384', buf)`.
1282
+ * Node path uses `crypto.createHash('sha384').update(buf).digest()`.
1283
+ *
1284
+ * If the manifest is missing or has no entry for the given file, we emit a
1285
+ * console warning and fall through. This keeps existing unsigned builds usable
1286
+ * while making signed builds tamper-evident.
1287
+ *
1288
+ * @module @danielsimonjr/mathts-core/internal (wasm-loader)
1289
+ */
1290
+
1291
+ /**
1292
+ * Compute the SHA-384 digest of a buffer and return base64 encoding,
1293
+ * prefixed with "sha384-" to match SRI conventions.
1294
+ */
1295
+ declare function sha384OfBuffer(buffer: ArrayBuffer | Uint8Array): Promise<string>;
1296
+ /**
1297
+ * Load the manifest sitting next to the .wasm artefact.
1298
+ *
1299
+ * @param wasmPath Resolved path or URL to the .wasm file.
1300
+ * @returns parsed manifest, or `null` if the manifest cannot be located.
1301
+ */
1302
+ declare function loadWasmManifest(wasmPath: string): Promise<WasmManifest | null>;
1303
+ /**
1304
+ * Verify a freshly loaded WASM buffer against the manifest.
1305
+ *
1306
+ * @throws Error if the buffer's SHA-384 differs from the manifest entry.
1307
+ */
1308
+ declare function verifyWasmIntegrity(buffer: ArrayBuffer | Uint8Array, wasmPath: string, options?: {
1309
+ manifest?: WasmManifest | null;
1310
+ required?: boolean;
1311
+ }): Promise<void>;
1312
+ /**
1313
+ * Robustly locate a packaged `.wasm` artifact across both the monorepo-source
1314
+ * layout and the published-package layout (Node only).
1315
+ *
1316
+ * The historical loaders hard-coded `../../../lib/wasm/<file>` relative to
1317
+ * `import.meta.url`, which only resolved for the monorepo source layout and
1318
+ * pointed outside the package once bundled/published — so the wasm was never
1319
+ * found and every consumer silently fell back to JS. This walks up from the
1320
+ * calling module's directory probing `<dir>/wasm/<file>` and
1321
+ * `<dir>/dist/wasm/<file>` at each level, matching bundled dist, unbundled dist,
1322
+ * and monorepo source.
1323
+ *
1324
+ * The caller injects its OWN `metaUrl` (its `import.meta.url`) so resolution is
1325
+ * relative to the calling package, not to core.
1326
+ *
1327
+ * @returns absolute path to the artifact, or `null` if not found.
1328
+ */
1329
+ declare function resolvePackagedWasm(metaUrl: string, wasmFile: string): Promise<string | null>;
1330
+ /**
1331
+ * Canonical fallback location for the packaged wasm when {@link resolvePackagedWasm}
1332
+ * finds nothing: `<packageRoot>/dist/wasm/<wasmFile>`, with the package root found
1333
+ * by walking up from `metaUrl` to the nearest `package.json`. This replaces the
1334
+ * historical `../../../lib/wasm/<file>` fabrication, which was only correct for the
1335
+ * pre-bundling source layout. The returned path is where the binary SHOULD be, so
1336
+ * the missing-binary warning tells the user exactly what to build.
1337
+ *
1338
+ * In browser mode (no filesystem) it returns the bundle-relative `./wasm/<file>`
1339
+ * URL — correct for a served `dist/`, which is the only layout browsers see.
1340
+ */
1341
+ declare function defaultWasmLocation(metaUrl: string, wasmFile: string, opts?: {
1342
+ browser?: boolean;
1343
+ }): Promise<string>;
1344
+
1345
+ export { type BigNumberValue, type ConfigOptions, DEFAULT_CONFIG, DimensionError, FactoryFunction, type FormatOptions, type GeneralFormatOptions, type IdentifiedValue, Index, IndexError, type LoadingMetrics, type MathJsConfig, MathjsError, Matrix$1 as Matrix, type MemoizedFunction, type NestedArray, type NormalizedFormatOptions, type NumberTypeConfig, ObjectWrappingMap, PartitionedMap, type SplitValue, UnitConstructor, UnitDependencies, type WasmManifest, _switch, acosh, arraySize, asinh, assign as assignMap, atanh, broadcastArrays, broadcastSizes, broadcastTo, canDefineProperty, cbrt, checkBroadcastingRules, clone$1 as clone, clone as cloneArray, compareText, concat, containsCollections, copysign, cosh, createEmptyMap, createIndexError, createMap, createUnitClass, deepExtend, deepFlatten, deepForEach, deepForEach$1 as deepForEachArray, deepMap, deepMap$1 as deepMapArray, deepStrictEqual, defaultWasmLocation, digits, endsWith, escape, expm1, extend, filter, filterRegExp, flatten, forEach, format$2 as format, format as formatBigNumber, format$1 as formatGeneric, generalize, get$1 as get, getArrayDataType, get as getArrayElement, hasOwnProperty, identify, initial, isEmptyIndex, isInteger, isLegacyFactory, isObjectWrappingMap, isPowZeroAtInfinity, join, last, lazy, loadWasmManifest, log10, log1p, log2, map, mapObject, memoize, nearlyEqual, normalizeFormatOptions, pick, pickShallow, processSizesWildcard, reduce, reshape, resize, resolvePackagedWasm, roundDigits, safeNumberType, scatter, set, sha384OfBuffer, sign, sinh, splitNumber, squeeze, stretch, stringify, tanh, toEngineering$1 as toEngineering, toEngineering as toEngineeringBigNumber, toExponential$1 as toExponential, toExponential as toExponentialBigNumber, toFixed$1 as toFixed, toFixed as toFixedBigNumber, toObject, toPrecision, traverse, unitDependencies, unsqueeze, validate, validateIndex, validateIndexSourceSize, verifyWasmIntegrity, warnOnce };
package/dist/internal.js CHANGED
@@ -1352,6 +1352,114 @@ function isObjectWrappingMap(object) {
1352
1352
  const wrapper = object;
1353
1353
  return isObject(wrapper.wrappedObject);
1354
1354
  }
1355
+
1356
+ // src/wasm-loader.ts
1357
+ var ALGO = "sha384";
1358
+ async function sha384OfBuffer(buffer) {
1359
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
1360
+ const isNode = typeof process !== "undefined" && process.versions?.node !== void 0;
1361
+ let digest;
1362
+ if (isNode) {
1363
+ const { createHash } = await import("crypto");
1364
+ const hash = createHash(ALGO);
1365
+ hash.update(bytes);
1366
+ digest = new Uint8Array(hash.digest());
1367
+ } else {
1368
+ const copy = new ArrayBuffer(bytes.byteLength);
1369
+ new Uint8Array(copy).set(bytes);
1370
+ const out = await crypto.subtle.digest("SHA-384", copy);
1371
+ digest = new Uint8Array(out);
1372
+ }
1373
+ let binary = "";
1374
+ for (let i = 0; i < digest.length; i++) binary += String.fromCharCode(digest[i]);
1375
+ const b64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(digest).toString("base64");
1376
+ return `sha384-${b64}`;
1377
+ }
1378
+ async function loadWasmManifest(wasmPath) {
1379
+ const isNode = typeof process !== "undefined" && process.versions?.node !== void 0;
1380
+ const manifestPath = wasmPath.replace(/[^/\\]+$/, "wasm-manifest.json");
1381
+ try {
1382
+ if (isNode) {
1383
+ const fs = await import("fs");
1384
+ const { promisify } = await import("util");
1385
+ const readFile = promisify(fs.readFile);
1386
+ const text = await readFile(manifestPath, "utf8");
1387
+ return JSON.parse(text);
1388
+ } else {
1389
+ const res = await fetch(manifestPath);
1390
+ if (!res.ok) return null;
1391
+ return await res.json();
1392
+ }
1393
+ } catch {
1394
+ return null;
1395
+ }
1396
+ }
1397
+ async function verifyWasmIntegrity(buffer, wasmPath, options = {}) {
1398
+ const fileName = wasmPath.split(/[/\\]/).pop() || wasmPath;
1399
+ const manifest = options.manifest !== void 0 ? options.manifest : await loadWasmManifest(wasmPath);
1400
+ if (!manifest) {
1401
+ if (options.required) {
1402
+ throw new Error(
1403
+ `WASM integrity check failed: no manifest at sibling of "${wasmPath}" (required)`
1404
+ );
1405
+ }
1406
+ if (typeof console !== "undefined") {
1407
+ console.warn(
1408
+ `[wasm-integrity] no manifest found beside "${wasmPath}"; skipping SHA-384 verification (set options.required=true to fail closed)`
1409
+ );
1410
+ }
1411
+ return;
1412
+ }
1413
+ const expected = manifest[fileName];
1414
+ if (!expected) {
1415
+ if (options.required) {
1416
+ throw new Error(`WASM integrity check failed: manifest has no entry for "${fileName}"`);
1417
+ }
1418
+ return;
1419
+ }
1420
+ const actual = await sha384OfBuffer(buffer);
1421
+ if (actual !== expected) {
1422
+ throw new Error(
1423
+ `WASM integrity check failed for "${fileName}": expected ${expected}, got ${actual}`
1424
+ );
1425
+ }
1426
+ }
1427
+ async function resolvePackagedWasm(metaUrl, wasmFile) {
1428
+ const isNode = typeof process !== "undefined" && process.versions?.node !== void 0;
1429
+ if (!isNode) return null;
1430
+ const { fileURLToPath } = await import("url");
1431
+ const { dirname, join: join2 } = await import("path");
1432
+ const { existsSync } = await import("fs");
1433
+ let dir = dirname(fileURLToPath(metaUrl));
1434
+ for (let depth = 0; depth < 8; depth++) {
1435
+ const candidates = [join2(dir, "wasm", wasmFile), join2(dir, "dist", "wasm", wasmFile)];
1436
+ for (const candidate of candidates) {
1437
+ if (existsSync(candidate)) return candidate;
1438
+ }
1439
+ const parent = dirname(dir);
1440
+ if (parent === dir) break;
1441
+ dir = parent;
1442
+ }
1443
+ return null;
1444
+ }
1445
+ async function defaultWasmLocation(metaUrl, wasmFile, opts) {
1446
+ if (opts?.browser) {
1447
+ return new URL(`./wasm/${wasmFile}`, metaUrl).href;
1448
+ }
1449
+ const { fileURLToPath } = await import("url");
1450
+ const { dirname, join: join2 } = await import("path");
1451
+ const { existsSync } = await import("fs");
1452
+ let dir = dirname(fileURLToPath(metaUrl));
1453
+ for (let depth = 0; depth < 8; depth++) {
1454
+ if (existsSync(join2(dir, "package.json"))) {
1455
+ return join2(dir, "dist", "wasm", wasmFile);
1456
+ }
1457
+ const parent = dirname(dir);
1458
+ if (parent === dir) break;
1459
+ dir = parent;
1460
+ }
1461
+ return join2(dirname(fileURLToPath(metaUrl)), "wasm", wasmFile);
1462
+ }
1355
1463
  export {
1356
1464
  Unit as CoreUnit,
1357
1465
  DEFAULT_CONFIG,
@@ -1360,6 +1468,7 @@ export {
1360
1468
  MathjsError,
1361
1469
  ObjectWrappingMap,
1362
1470
  PartitionedMap,
1471
+ _switch,
1363
1472
  acosh,
1364
1473
  arraySize,
1365
1474
  asinh,
@@ -1391,6 +1500,7 @@ export {
1391
1500
  deepMap2 as deepMap,
1392
1501
  deepMap as deepMapArray,
1393
1502
  deepStrictEqual,
1503
+ defaultWasmLocation,
1394
1504
  digits,
1395
1505
  endsWith,
1396
1506
  escape,
@@ -1443,6 +1553,7 @@ export {
1443
1553
  join,
1444
1554
  last,
1445
1555
  lazy,
1556
+ loadWasmManifest,
1446
1557
  log10,
1447
1558
  log1p,
1448
1559
  log2,
@@ -1457,10 +1568,12 @@ export {
1457
1568
  reduce,
1458
1569
  reshape,
1459
1570
  resize,
1571
+ resolvePackagedWasm,
1460
1572
  roundDigits,
1461
1573
  safeNumberType,
1462
1574
  scatter,
1463
1575
  set,
1576
+ sha384OfBuffer,
1464
1577
  sign,
1465
1578
  sinh,
1466
1579
  sortFactories,
@@ -1485,5 +1598,6 @@ export {
1485
1598
  validate,
1486
1599
  validateIndex,
1487
1600
  validateIndexSourceSize,
1601
+ verifyWasmIntegrity,
1488
1602
  warnOnce
1489
1603
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danielsimonjr/mathts-core",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Core types, utilities, and configuration for MathTS",
5
5
  "author": "Daniel Simon Jr.",
6
6
  "license": "MIT",