Compare commits

...

67 commits
v0.0.1 ... main

Author SHA1 Message Date
renovate[bot]
345017c52d
Update dependency @types/node to v22.13.17 ()
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-01 21:57:00 +00:00
renovate[bot]
529d6a3483
Update actions/setup-node digest to cdca736 ()
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-01 05:48:04 +00:00
renovate[bot]
09960e348d
Update dependency tsx to v4.19.3 ()
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 10:34:19 +00:00
renovate[bot]
2e9d3bc281
Update actions/setup-node digest to 1d0ff46 ()
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 06:05:23 +00:00
kou029w
7e43c94bfb Release 1.0.1 2025-02-19 08:21:04 +00:00
9a2e7ad45c
Update tests to assert expected output structure for IntegrityMetadataSet 2025-02-19 17:19:45 +09:00
d0415bbd04
Fixed "Maximum call stack size exceeded" error when using IntegrityMetadataSet#strongest 2025-02-19 17:14:01 +09:00
kou029w
22450619c4 Release 1.0.0 2025-01-06 04:47:32 +00:00
83860ef7f8
update changelog 2025-01-06 13:43:21 +09:00
93af3bce3c
change strongest property type from Array<IntegrityMetadata> to IntegrityMetadataSet 2025-01-06 13:41:24 +09:00
3d1a4acd2f
update changelog 2025-01-06 11:38:20 +09:00
52ff1f7e9a
add documentation for IntegrityMetadata class properties 2025-01-06 11:32:52 +09:00
renovate[bot]
c8e7319396
Update dependency @types/node to v22.10.3 ()
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-01 07:35:29 +00:00
renovate[bot]
5310a04b87
Update dependency pkgroll to v2.6.0 ()
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-01 04:06:42 +00:00
renovate[bot]
7d53588259
Update dependency release-it to v17.10.0 ()
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-01 07:01:59 +00:00
renovate[bot]
33044b912a
Update dependency @types/node to v22.10.1 ()
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-01 04:23:10 +00:00
f4c800793b
update examples 2024-11-06 11:31:38 +09:00
renovate[bot]
f2854638df Update dependency tsx to v4.19.2 2024-11-01 09:01:58 +00:00
renovate[bot]
17fce57d8b Update dependency pkgroll to v2.5.1 2024-11-01 06:44:31 +00:00
renovate[bot]
5f6ceea225 Update actions/setup-node digest to 39370e3 2024-11-01 04:32:09 +00:00
renovate[bot]
5e369ade90 Update actions/checkout digest to 11bd719 2024-11-01 02:40:51 +00:00
8f7b6bd105
specify code block syntax 2024-10-03 16:13:52 +09:00
kou029w
472df9a60f Release 0.1.0 2024-10-02 13:20:28 +00:00
39a1e06c8d
add examples 2024-10-02 22:17:54 +09:00
419d7668a9
add comments 2024-10-02 22:17:54 +09:00
02c253b45f
update changelog 2024-10-02 22:17:53 +09:00
c652fb46f4
fix test command for node 20 2024-10-02 22:17:53 +09:00
d7cf1bf4dc
replace tsup with pkgroll and tsx for bundling 2024-10-02 22:17:53 +09:00
d740f57970
updated createIntegrityMetadataSet to accept more flexible input types 2024-10-02 22:17:52 +09:00
90f6a0844a
add match 2024-10-02 22:17:52 +09:00
ab57344af3
add strongestHashAlgorithms getter 2024-10-02 19:06:13 +09:00
c085fcbe8f
add iterator and size getter 2024-10-02 19:06:09 +09:00
f7ac85b913
refactor IntegrityMetadataSet for improved structure and flexible input 2024-10-02 19:06:06 +09:00
f538f67971
add integrity-metadata-set tests 2024-10-02 19:06:02 +09:00
renovate[bot]
2f3d3318e2 Update dependency typescript to v5.6.2 2024-10-01 12:42:21 +00:00
renovate[bot]
d9731fb6d7 Update dependency tsup to v8.3.0 2024-10-01 10:43:13 +00:00
renovate[bot]
b3a0c978eb Update dependency @types/node to v22.7.4 2024-10-01 06:34:09 +00:00
renovate[bot]
d492ebd876 Update actions/setup-node digest to 0a44ba7 2024-10-01 03:37:36 +00:00
1924621a2b
update package manager commands 2024-09-18 16:28:15 +09:00
47228186a0
update match to return false for unsupported, null, empty, or invalid hash algorithms 2024-09-18 16:14:32 +09:00
f8aff38a6b
accepts an IntegrityMetadata like object as input 2024-09-18 15:05:26 +09:00
5ad019029b
trims leading and trailing whitespace 2024-09-18 14:48:24 +09:00
ddcb93da0c
accepts options 2024-09-18 14:28:56 +09:00
9b64464d81
update getPrioritizedHashAlgorithm to return supported hash algorithm if unsupported one is provided 2024-09-18 14:21:27 +09:00
d7ab327e56
reference W3C SRI spec 2024-09-17 19:10:43 +09:00
3bd7f5566e
add integrity-metadata tests 2024-09-17 19:08:56 +09:00
b6cb686c37
remove IntegrityMetadataSet.match() 2024-09-09 22:18:16 +09:00
b8f8c3d209
change IIntegrityMetadata interface to IntegrityMetadataLike type 2024-09-09 21:33:54 +09:00
beff4e49d5
update changelog 2024-09-09 19:28:19 +09:00
9952afecc2
change return type of createIntegrityMetadata() to Promise<IntegrityMetadata> 2024-09-09 19:28:12 +09:00
bd29bf4dc1
add options to createIntegrityMetadataSet 2024-09-09 19:19:39 +09:00
e5f17c3631
add IIntegrityMetadata interface 2024-09-09 19:19:36 +09:00
5c5d171791
organize hash algorithm names 2024-09-09 18:32:50 +09:00
9561fb23dc
rename PrioritizedHash to PrioritizedHashAlgorithm 2024-09-09 17:21:04 +09:00
7b57a930e1
set up monthly renovations 2024-09-06 10:11:57 +09:00
716d3e88d5
Rename Integrity-metadata.test.ts to integrity-metadata.test.ts 2024-09-05 20:32:40 +09:00
963d4e2960
add keywords to package.json 2024-09-05 19:37:01 +09:00
renovate[bot]
f90d76097f Update dependency @types/node to v22.5.4 2024-09-05 01:24:31 +00:00
renovate[bot]
445bde4987 Update dependency @types/node to v22.5.3 2024-09-04 23:12:48 +09:00
renovate[bot]
af9243c726 Pin dependencies 2024-09-04 23:11:20 +09:00
6ec0f55736
remove redundant toLowerCase call 2024-09-04 23:10:33 +09:00
kou029w
79ff27dbe4 Release 0.0.3 2024-09-03 00:58:03 +00:00
8d6c191978
test with deno and bun 2024-09-03 09:55:39 +09:00
628cc0bdee
rename SubresourceIntegrity to IntegrityMetadataSet 2024-09-03 09:55:38 +09:00
kou029w
be28d23470 Release 0.0.2 2024-09-02 23:30:33 +00:00
a154ad7f6f
update changelog 2024-09-03 08:29:09 +09:00
bfee60800d
add types field in package.json 2024-09-03 08:28:17 +09:00
34 changed files with 2682 additions and 1810 deletions

View file

@ -16,10 +16,10 @@ jobs:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
with:
node-version: lts/*
cache: npm

View file

@ -1,7 +1,7 @@
name: test
on: push
jobs:
nodejs:
node:
name: Node.js v${{ matrix.node-version }}
strategy:
matrix:
@ -11,10 +11,32 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm test
deno:
name: Deno
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: denoland/setup-deno@v1
with:
deno-version: latest
- run: deno task test
working-directory: runtime/deno
bun:
name: Bun
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2
with:
bun-version: latest
- run: bun test
working-directory: runtime/bun

View file

@ -7,14 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.0.1] - 2025-02-19
- Fixed "Maximum call stack size exceeded" error when using `IntegrityMetadataSet#strongest`
## [1.0.0] - 2025-01-06
- Change `strongest` property type from `Array<IntegrityMetadata>` to `IntegrityMetadataSet`
- Improve documentation
## [0.1.0] - 2024-10-02
- **IntegrityMetadataSet Enhancements**: Refactored `IntegrityMetadataSet` to improve structure and flexibility, including new methods (`match`, `strongestHashAlgorithms`, `iterator`, and `size`), support for more flexible input types, and enhanced validation logic.
- **Type Definitions & Code Readability**: Improved type definitions and enhanced overall code readability.
- **Build & Tooling**: Replaced `tsup` with `pkgroll` and `tsx` for bundling.
## [0.0.3] - 2024-09-03
- Rename `SubresourceIntegrity` to `IntegrityMetadataSet`
## [0.0.2] - 2024-09-02
- Add `types` field in package.json
## [0.0.1] - 2024-09-02
- First release
## [0.0.0]
[Unreleased]: https://github.com/kou029w/websri
[0.0.0]: https://github.com/kou029w/websri
[unreleased]: https://github.com/kou029w/usri/compare/v0.0.1...HEAD
[1.0.1]: https://github.com/kou029w/websri/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/kou029w/websri/compare/v0.1.0...v1.0.0
[0.1.0]: https://github.com/kou029w/websri/compare/v0.0.3...v0.1.0
[0.0.3]: https://github.com/kou029w/websri/compare/v0.0.2...v0.0.3
[0.0.2]: https://github.com/kou029w/websri/compare/v0.0.1...v0.0.2
[0.0.1]: https://github.com/kou029w/usri/releases/tag/v0.0.1
[unreleased]: https://github.com/kou029w/websri/compare/v1.0.1...HEAD

View file

@ -16,10 +16,13 @@ npm install websri
yarn add websri
# pnpm
pnpm install websri
pnpm add websri
# deno
deno add npm:websri
# bun
bun install websri
bun add websri
```
[Integrity Metadata](https://www.w3.org/TR/SRI/#integrity-metadata):

2961
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "websri",
"version": "0.0.1",
"version": "1.0.1",
"description": "A universal Subresource Integrity (SRI) utility for Node.js, browsers, Cloudflare Workers, Deno, Bun, and other web-compatible runtimes.",
"license": "MIT",
"author": "Kohei Watanabe <nebel@fogtype.com>",
@ -8,7 +8,22 @@
"type": "git",
"url": "https://github.com/kou029w/websri.git"
},
"keywords": [
"browser",
"bun",
"cloudflare",
"cloudflare-workers",
"deno",
"hashing",
"integrity",
"nodejs",
"security",
"sri",
"subresource-integrity",
"typescript"
],
"type": "module",
"types": "dist/index.d.ts",
"exports": {
"import": {
"types": "./dist/index.d.ts",
@ -23,16 +38,17 @@
"dist"
],
"scripts": {
"build": "tsup",
"prepublishOnly": "tsup",
"test": "tsup && node --test",
"build": "pkgroll",
"prepublishOnly": "pkgroll",
"test": "tsx --test $(find test -type f)",
"release": "release-it --"
},
"devDependencies": {
"@release-it/keep-a-changelog": "5.0.0",
"@types/node": "22.5.2",
"release-it": "17.6.0",
"tsup": "8.2.4",
"typescript": "5.5.4"
"@types/node": "22.13.17",
"pkgroll": "2.6.0",
"release-it": "17.10.0",
"tsx": "4.19.3",
"typescript": "5.6.2"
}
}

View file

@ -1,4 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:best-practices", ":automergeAll"]
"extends": ["config:best-practices", ":automergeAll", "schedule:monthly"]
}

BIN
runtime/bun/bun.lockb Executable file

Binary file not shown.

View file

@ -0,0 +1,12 @@
import { expect, test } from "bun:test";
import { createIntegrityMetadata } from "../../src/index.ts";
test("createIntegrityMetadata()", async () => {
const res = new Response("Hello, world!");
const data = await res.arrayBuffer();
const integrityMetadata = await createIntegrityMetadata("sha256", data);
expect(integrityMetadata.toString()).toBe(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
});

8
runtime/bun/package.json Normal file
View file

@ -0,0 +1,8 @@
{
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
}
}

27
runtime/bun/tsconfig.json Normal file
View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

8
runtime/deno/deno.json Normal file
View file

@ -0,0 +1,8 @@
{
"tasks": {
"test": "deno test --no-check"
},
"imports": {
"@std/assert": "jsr:@std/assert@^1.0.3"
}
}

26
runtime/deno/deno.lock generated Normal file
View file

@ -0,0 +1,26 @@
{
"version": "3",
"packages": {
"specifiers": {
"jsr:@std/assert@^1.0.3": "jsr:@std/assert@1.0.3",
"jsr:@std/internal@^1.0.2": "jsr:@std/internal@1.0.2"
},
"jsr": {
"@std/assert@1.0.3": {
"integrity": "b0d03ce1ced880df67132eea140623010d415848df66f6aa5df76507ca7c26d8",
"dependencies": [
"jsr:@std/internal@^1.0.2"
]
},
"@std/internal@1.0.2": {
"integrity": "f4cabe2021352e8bfc24e6569700df87bf070914fc38d4b23eddd20108ac4495"
}
}
},
"remote": {},
"workspace": {
"dependencies": [
"jsr:@std/assert@^1.0.3"
]
}
}

View file

@ -0,0 +1,13 @@
import { assertEquals } from "@std/assert";
import { createIntegrityMetadata } from "../../src/index.ts";
Deno.test("createIntegrityMetadata()", async () => {
const res = new Response("Hello, world!");
const data = await res.arrayBuffer();
const integrityMetadata = await createIntegrityMetadata("sha256", data);
assertEquals(
integrityMetadata.toString(),
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
});

View file

@ -1,47 +1,130 @@
/** Content Security Policy Level 2, section 4.2 */
/**
* Represents the available hash algorithms used for Subresource Integrity.
* @see {@link https://www.w3.org/TR/CSP2/#hash_algo}
*/
export type HashAlgorithm = "sha256" | "sha384" | "sha512";
export const supportedHashAlgorithm: ReadonlyArray<HashAlgorithm> = [
"sha512",
"sha384",
"sha256",
];
export const supportedHashAlgorithmName = {
/**
* A constant object defining the supported hash algorithms and their corresponding string
* representations for cryptographic operations. These algorithms are referenced by name when
* working with hashing functions in Web Crypto APIs.
*/
export const supportedHashAlgorithms = {
/** SHA-256 hash algorithm */
sha256: "SHA-256",
/** SHA-384 hash algorithm */
sha384: "SHA-384",
/** SHA-512 hash algorithm */
sha512: "SHA-512",
} satisfies Record<HashAlgorithm, string>;
} as const satisfies Record<HashAlgorithm, HashAlgorithmIdentifier>;
export type PrioritizedHash = "" | HashAlgorithm;
/**
* A union type representing either an empty string or a valid hash algorithm.
* The empty string is used when no hash algorithm is selected or is considered equal.
*/
export type PrioritizedHashAlgorithm = "" | HashAlgorithm;
export function getPrioritizedHash(
/**
* Function to prioritize two hash algorithms, returning the stronger or an empty string if both
* are unsupported or equal.
* @see {@link https://www.w3.org/TR/SRI/#dfn-getprioritizedhashfunction-a-b}
* @param a The first hash algorithm to compare.
* @param b The second hash algorithm to compare.
* @returns The hash algorithm or an empty string if both algorithms are unsupported or equal.
*/
export type GetPrioritizedHashAlgorithm = (
a: HashAlgorithm,
b: HashAlgorithm,
): PrioritizedHash {
) => PrioritizedHashAlgorithm;
/**
* Function to prioritize two hash algorithms, returning the stronger or an empty string if both
* are unsupported or equal.
* @see {@link https://www.w3.org/TR/SRI/#dfn-getprioritizedhashfunction-a-b}
* @param a The first hash algorithm to compare.
* @param b The second hash algorithm to compare.
* @returns The hash algorithm or an empty string if both algorithms are unsupported or equal.
*/
export function getPrioritizedHashAlgorithm(
a: HashAlgorithm,
b: HashAlgorithm,
): PrioritizedHashAlgorithm {
if (a === b) return "";
if (!supportedHashAlgorithm.includes(a)) return "";
if (!supportedHashAlgorithm.includes(b)) return "";
if (!(a in supportedHashAlgorithms)) {
return b in supportedHashAlgorithms ? b : "";
}
if (!(b in supportedHashAlgorithms)) {
return a in supportedHashAlgorithms ? a : "";
}
return a < b ? b : a;
}
/**
* Regular expression for matching integrity metadata format.
*/
export const IntegrityMetadataRegex =
/^(?<alg>sha256|sha384|sha512)-(?<val>(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)(?:[?](?<opts>[\x21-\x7e]*))?$/;
/^(?<alg>sha256|sha384|sha512)-(?<val>(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)(?:[?](?<opt>[\x21-\x7e]*))?$/;
/**
* Regular expression for separating integrity metadata.
*/
export const SeparatorRegex = /[^\x21-\x7e]+/;
/** Integrity Metadata */
export class IntegrityMetadata {
alg: PrioritizedHash;
/**
* Represents the structure of integrity metadata used for validating resources with Subresource
* Integrity.
*/
export type IntegrityMetadataLike = {
/** Hash algorithm */
alg: PrioritizedHashAlgorithm;
/** The base64-encoded hash value of the resource */
val: string;
/** Optional additional attributes */
opt?: string[];
};
/**
* Class representing integrity metadata, consisting of a hash algorithm and hash value.
*/
export class IntegrityMetadata implements IntegrityMetadataLike {
/** Hash algorithm */
alg: PrioritizedHashAlgorithm;
/** The base64-encoded hash value of the resource */
val: string;
/** Optional additional attributes */
opt: string[];
constructor(integrity: string) {
/**
* Creates an instance of `IntegrityMetadata` from a given object or string.
* @param integrity The integrity metadata input, which can be a string or object.
* @example
* ```js
* new IntegrityMetadata("sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=")
* ```
*
* or
*
* ```js
* new IntegrityMetadata({
* alg: "sha256",
* val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
* })
* ```
*/
constructor(integrity: IntegrityMetadataLike | string | null | undefined) {
const integrityString =
typeof integrity === "object" && integrity !== null
? IntegrityMetadata.stringify(integrity)
: String(integrity ?? "").trim();
const {
alg = "",
val = "",
opt,
} = IntegrityMetadataRegex.exec(integrity)?.groups ?? {};
} = IntegrityMetadataRegex.exec(integrityString)?.groups ?? {};
Object.assign(this, {
alg,
@ -50,118 +133,310 @@ export class IntegrityMetadata {
});
}
match(integrity: { alg: PrioritizedHash; val: string }): boolean {
return integrity.alg === this.alg && integrity.val === this.val;
/**
* Compares the current integrity metadata with another object or string.
* @param integrity The integrity metadata to compare with.
* @returns `true` if the integrity metadata matches, `false` otherwise.
* @example
* ```js
* integrityMetadata.match("sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=")
* ```
*
* or
*
* ```js
* integrityMetadata.match({
* alg: "sha256",
* val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
* })
* ```
*/
match(integrity: IntegrityMetadataLike | string | null | undefined): boolean {
const { alg, val } = new IntegrityMetadata(integrity);
if (!alg) return false;
if (!val) return false;
if (!(alg in supportedHashAlgorithms)) return false;
return alg === this.alg && val === this.val;
}
/**
* Converts the integrity metadata into a string representation.
* @returns The string representation of the integrity metadata.
*/
toString(): string {
return IntegrityMetadata.stringify(this);
}
/**
* Converts the integrity metadata into a JSON string.
* @returns The JSON string representation of the integrity metadata.
*/
toJSON(): string {
return this.toString();
}
static stringify(integrity: {
alg: PrioritizedHash;
val: string;
opt: string[];
}): string {
if (!integrity.alg) return "";
if (!integrity.val) return "";
if (!supportedHashAlgorithm.includes(integrity.alg)) return "";
return `${integrity.alg}-${[integrity.val, ...integrity.opt].join("?")}`;
/**
* Static method to stringify an integrity metadata object.
* @param integrity The integrity metadata object to stringify.
* @returns The stringified integrity metadata.
* @example
* ```js
* IntegrityMetadata.stringify({
* alg: "sha256",
* val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
* }) // "sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM="
* ```
*/
static stringify({ alg, val, opt = [] }: IntegrityMetadataLike): string {
if (!alg) return "";
if (!val) return "";
if (!(alg in supportedHashAlgorithms)) return "";
return `${alg}-${[val, ...opt].join("?")}`;
}
}
/**
* Asynchronously creates an `IntegrityMetadata` object from a hash algorithm and data.
* @param hashAlgorithm The hash algorithm to use (e.g., `sha256`).
* @param data The data to hash (in `ArrayBuffer` format).
* @param opt Optional additional attributes.
* @returns A promise that resolves to an `IntegrityMetadata` object.
* @example
* ```js
* const res = new Response("Hello, world!");
* const data = await res.arrayBuffer();
* const integrityMetadata = await createIntegrityMetadata("sha256", data);
* ```
*/
export async function createIntegrityMetadata(
hashAlgorithm: HashAlgorithm,
data: ArrayBuffer,
opt: string[] = [],
): Promise<string> {
): Promise<IntegrityMetadata> {
const alg = hashAlgorithm.toLowerCase() as HashAlgorithm;
if (!supportedHashAlgorithm.includes(alg)) return "";
const arrayBuffer = await crypto.subtle.digest(
supportedHashAlgorithmName[alg.toLowerCase()],
data,
);
if (!(alg in supportedHashAlgorithms)) {
return new IntegrityMetadata("");
}
const hashAlgorithmIdentifier = supportedHashAlgorithms[alg];
const arrayBuffer = await crypto.subtle.digest(hashAlgorithmIdentifier, data);
const val = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
const integrity = IntegrityMetadata.stringify({ alg, val, opt });
return IntegrityMetadata.stringify({
alg,
val,
opt,
});
return new IntegrityMetadata(integrity);
}
/** Subresource Integrity */
export class SubresourceIntegrity extends Map<
HashAlgorithm,
IntegrityMetadata
> {
getPrioritizedHash: (a: HashAlgorithm, b: HashAlgorithm) => PrioritizedHash;
/**
* Options for configuring an `IntegrityMetadataSet`.
*/
export type IntegrityMetadataSetOptions = {
/** A custom function to determine the prioritized hash algorithm. */
getPrioritizedHashAlgorithm?: GetPrioritizedHashAlgorithm;
};
constructor(integrity: string, options = { getPrioritizedHash }) {
super();
/**
* Class representing a set of integrity metadata, used for managing multiple hash algorithms and
* their associated metadata.
*/
export class IntegrityMetadataSet {
#set: ReadonlyArray<IntegrityMetadata>;
#getPrioritizedHashAlgorithm = getPrioritizedHashAlgorithm;
const integrityMetadata = integrity.split(SeparatorRegex);
/**
* Create an instance of `IntegrityMetadataSet` from integrity metadata or an array of integrity
* metadata.
* @param integrity The integrity metadata or an array of integrity metadata.
* @param options Optional configuration options for hash algorithm prioritization.
* @example
* ```js
* new IntegrityMetadataSet([
* "sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
* "sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r",
* "sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==",
* ])
* ```
*
* or
*
* ```js
* new IntegrityMetadataSet(`
* sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=
* sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r
* sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==
* `)
* ```
*/
constructor(
integrity:
| ReadonlyArray<IntegrityMetadataLike | string | null | undefined>
| IntegrityMetadataLike
| string
| null
| undefined,
{
getPrioritizedHashAlgorithm:
_getPrioritizedHashAlgorithm = getPrioritizedHashAlgorithm,
}: IntegrityMetadataSetOptions = {},
) {
this.#set = [integrity]
.flat()
.flatMap(
(
integrity: IntegrityMetadataLike | string | null | undefined,
): ReadonlyArray<IntegrityMetadataLike | string | null | undefined> => {
if (typeof integrity === "string") {
return integrity.split(SeparatorRegex);
}
for (const integrity of integrityMetadata.filter(Boolean)) {
const integrityMetadata = new IntegrityMetadata(integrity);
return [integrity];
},
)
.map((integrity) => new IntegrityMetadata(integrity))
.filter((integrityMetadata) => integrityMetadata.toString() !== "");
if (integrityMetadata.alg) {
this.set(integrityMetadata.alg, integrityMetadata);
this.#getPrioritizedHashAlgorithm = _getPrioritizedHashAlgorithm;
}
/**
* Enables iteration over the set of integrity metadata.
* @returns A generator that yields each IntegrityMetadata object.
* @example
* ```js
* [...integrityMetadataSet]
* ```
*/
*[Symbol.iterator](): Generator<IntegrityMetadata> {
for (const integrityMetadata of this.#set) {
yield new IntegrityMetadata(integrityMetadata);
}
}
/**
* The number of integrity metadata entries in the set.
*/
get size(): number {
return this.#set.length;
}
/**
* The strongest (most secure) integrity metadata from the set.
* @see {@link https://www.w3.org/TR/SRI/#get-the-strongest-metadata-from-set}
*/
get strongest(): IntegrityMetadataSet {
let strongest = new IntegrityMetadataSet([]);
for (const integrityMetadata of this.#set) {
const [{ alg } = new IntegrityMetadata("")] = strongest;
const prioritizedHashAlgorithm = this.#getPrioritizedHashAlgorithm(
alg as HashAlgorithm,
integrityMetadata.alg as HashAlgorithm,
);
switch (prioritizedHashAlgorithm) {
case "":
strongest = new IntegrityMetadataSet([
...strongest,
integrityMetadata,
]);
break;
case integrityMetadata.alg:
strongest = new IntegrityMetadataSet(integrityMetadata);
break;
case alg:
break;
}
}
this.getPrioritizedHash = options.getPrioritizedHash;
return strongest;
}
get strongest(): IntegrityMetadata {
const [hashAlgorithm = supportedHashAlgorithm[0]]: HashAlgorithm[] = [
...this.keys(),
].sort((a, b) => {
switch (this.getPrioritizedHash(a, b)) {
default:
case "":
return 0;
case a:
return -1;
case b:
return +1;
}
});
/**
* Returns an array of the strongest supported hash algorithms in the set.
*/
get strongestHashAlgorithms(): ReadonlyArray<HashAlgorithm> {
const strongestHashAlgorithms = [...this.strongest]
.map(({ alg }) => alg as HashAlgorithm)
.filter(Boolean);
return this.get(hashAlgorithm) ?? new IntegrityMetadata("");
return [...new Set(strongestHashAlgorithms)];
}
match(integrity: { alg: PrioritizedHash; val: string }): boolean {
return this.strongest.match(integrity);
/**
* Checks if a given integrity metadata object or string matches any in the set.
* @param integrity The integrity metadata to match.
* @returns `true` if the integrity metadata matches, `false` otherwise.
* @example
* ```js
* integrityMetadataSet.match("sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=")
* ```
*
* or
*
* ```js
* integrityMetadataSet.match({
* alg: "sha256",
* val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
* })
* ```
*/
match(integrity: IntegrityMetadataLike | string | null | undefined): boolean {
return this.#set.some((integrityMetadata) =>
integrityMetadata.match(integrity),
);
}
join(separator = " ") {
return [...this.values()].map(String).join(separator);
/**
* Joins the integrity metadata in the set into a single string, separated by the specified
* separator.
* @param separator The separator to use (default is a space).
* @returns The joined string representation of the set.
*/
join(separator = " "): string {
return this.#set.map(String).join(separator);
}
/**
* Converts the set of integrity metadata to a string representation.
* @returns The string representation of the set.
*/
toString(): string {
return this.join();
}
/**
* Converts the set of integrity metadata to a JSON string.
* @returns The JSON string representation of the set.
*/
toJSON(): string {
return this.toString();
}
}
export async function createSubresourceIntegrity(
hashAlgorithms: HashAlgorithm[],
/**
* Asynchronously creates an `IntegrityMetadataSet` from a set of hash algorithms and data.
* @param hashAlgorithms A single hash algorithm or an array of supported hash algorithms.
* @param data The data to hash (in `ArrayBuffer` format).
* @param options Optional configuration options for the metadata set.
* @returns A promise that resolves to an `IntegrityMetadataSet` object.
* @example
* ```js
* const res = new Response("Hello, world!");
* const data = await res.arrayBuffer();
* const set = await createIntegrityMetadataSet(["sha256", "sha384", "sha512"], data);
* ```
*/
export async function createIntegrityMetadataSet(
hashAlgorithms: ReadonlyArray<HashAlgorithm> | HashAlgorithm,
data: ArrayBuffer,
): Promise<SubresourceIntegrity> {
const integrityMetadata = await Promise.all(
hashAlgorithms.map((alg) => createIntegrityMetadata(alg, data)),
options: IntegrityMetadataSetOptions = {
getPrioritizedHashAlgorithm,
},
): Promise<IntegrityMetadataSet> {
const set = await Promise.all(
[hashAlgorithms].flat().map((alg) => createIntegrityMetadata(alg, data)),
);
return new SubresourceIntegrity(integrityMetadata.join(" "));
return new IntegrityMetadataSet(set, options);
}

View file

@ -0,0 +1,35 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import {
createIntegrityMetadataSet,
IntegrityMetadata,
IntegrityMetadataSet,
} from "../src/index.ts";
test("instantiate a new IntegrityMetadataSet", async function () {
const res = new Response("Hello, world!");
const data = await res.arrayBuffer();
const set = await createIntegrityMetadataSet("sha256", data);
assert(set instanceof IntegrityMetadataSet);
});
test("instantiate with the specified hash algorithms and ArrayBuffer", async function () {
const res = new Response("Hello, world!");
const data = await res.arrayBuffer();
const set = await createIntegrityMetadataSet(["sha384", "sha512"], data);
assert.deepEqual(
[...set],
[
new IntegrityMetadata({
alg: "sha384",
val: "VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r",
}),
new IntegrityMetadata({
alg: "sha512",
val: "wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==",
}),
],
);
});

View file

@ -0,0 +1,23 @@
import assert from "node:assert";
import { test } from "node:test";
import { createIntegrityMetadata, IntegrityMetadata } from "../src/index.ts";
test("instantiate a new IntegrityMetadata", async function () {
const res = new Response("Hello, world!");
const data = await res.arrayBuffer();
const integrityMetadata = await createIntegrityMetadata("sha256", data);
assert(integrityMetadata instanceof IntegrityMetadata);
});
test("instantiate with the specified hash algorithm and ArrayBuffer", async function () {
const res = new Response("Hello, world!");
const data = await res.arrayBuffer();
const integrityMetadata = await createIntegrityMetadata("sha256", data);
assert.deepEqual(integrityMetadata, {
alg: "sha256",
val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
opt: [],
});
});

View file

@ -0,0 +1,31 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { getPrioritizedHashAlgorithm } from "../src/index.ts";
test("return the most collision-resistant hash algorithm", function () {
assert.strictEqual(getPrioritizedHashAlgorithm("sha256", "sha512"), "sha512");
});
test("if the priority is equal, return the empty string", function () {
assert.strictEqual(getPrioritizedHashAlgorithm("sha256", "sha256"), "");
});
test("if both hash algorithms are not supported, return the empty string", function () {
// @ts-expect-error unsupported hash algorithms
assert.strictEqual(getPrioritizedHashAlgorithm("md5", "sha1"), "");
});
test("if one of the hash algorithms is unsupported, return the supported hash algorithm", function () {
// @ts-expect-error unsupported hash algorithms
assert.strictEqual(getPrioritizedHashAlgorithm("md5", "sha256"), "sha256");
});
test("if both strings are empty, return the empty string", function () {
// @ts-expect-error empty string
assert.strictEqual(getPrioritizedHashAlgorithm("", ""), "");
});
test("if either is the empty string, it return the supported hash algorithm", function () {
// @ts-expect-error empty string
assert.strictEqual(getPrioritizedHashAlgorithm("sha256", ""), "sha256");
});

View file

@ -1,39 +0,0 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import {
createIntegrityMetadata,
createSubresourceIntegrity,
SubresourceIntegrity,
} from "../dist/index.js";
test("createIntegrityMetadata()", async function () {
const res = new Response("Hello, world!");
const data = await res.arrayBuffer();
const integrityMetadata = await createIntegrityMetadata("sha256", data);
assert.strictEqual(
integrityMetadata.toString(),
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
});
test("createSubresourceIntegrity()", async function () {
const res = new Response("Hello, world!");
const data = await res.arrayBuffer();
const sri = await createSubresourceIntegrity(["sha384", "sha512"], data);
assert.strictEqual(
sri.toString(),
"sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==",
);
});
test("SubresourceIntegrity.strongest", async function () {
const { strongest } = new SubresourceIntegrity(`
sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=
sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r
sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==
`);
assert.strictEqual(strongest.alg, "sha512");
});

View file

@ -0,0 +1,196 @@
import assert from "node:assert";
import { test } from "node:test";
import { IntegrityMetadataSet } from "../../src/index.ts";
test("supports SHA-256", function () {
const set = new IntegrityMetadataSet(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
assert.deepEqual(
[...set],
[
{
alg: "sha256",
val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
opt: [],
},
],
);
});
test("supports SHA-384", function () {
const set = new IntegrityMetadataSet(
"sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r",
);
assert.deepEqual(
[...set],
[
{
alg: "sha384",
val: "VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r",
opt: [],
},
],
);
});
test("supports SHA-512", function () {
const set = new IntegrityMetadataSet(
"sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==",
);
assert.deepEqual(
[...set],
[
{
alg: "sha512",
val: "wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==",
opt: [],
},
],
);
});
test("accepts options", function () {
const set = new IntegrityMetadataSet(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=?foo?bar",
);
assert.deepEqual(
[...set],
[
{
alg: "sha256",
val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
opt: ["foo", "bar"],
},
],
);
});
test("accepts an IntegrityMetadata like object as input", function () {
const set = new IntegrityMetadataSet({
alg: "sha256",
val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
});
assert.deepEqual(
[...set],
[
{
alg: "sha256",
val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
opt: [],
},
],
);
});
test("multiple algorithms can be accepted", function () {
const set = new IntegrityMetadataSet([
{
alg: "sha256",
val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
},
`
sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r
sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==
`,
]);
assert.deepEqual(
[...set],
[
...new IntegrityMetadataSet([
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
"sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r",
"sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==",
]),
],
);
});
test("multiple overlapping algorithms can be accepted", function () {
const set = new IntegrityMetadataSet([
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
"sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=",
]);
assert.deepEqual(
[...set],
[
...new IntegrityMetadataSet([
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
"sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=",
]),
],
);
});
test("trims leading and trailing whitespace", function () {
const set = new IntegrityMetadataSet(
"\t sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=\u0020\u00a0\u1680\u2000\u2001\u2002\u3000",
);
assert.deepEqual(
[...set],
[
{
alg: "sha256",
val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
opt: [],
},
],
);
});
test("whitespace can be analyzed as entry separator", function () {
const set = new IntegrityMetadataSet(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=\t\u0020\u00a0\u1680sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r\u2000\u2001\u2002\u3000sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==",
);
assert.deepEqual(
[...set],
[
...new IntegrityMetadataSet([
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
"sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r",
"sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==",
]),
],
);
});
test("discards unsupported hash algorithm", function () {
const set = new IntegrityMetadataSet("sha1-lDpwLQbzRZmu4fjajvn3KWAx1pk=");
assert.deepEqual([...set], []);
});
test("discards null input", function () {
const set = new IntegrityMetadataSet(null);
assert.deepEqual([...set], []);
});
test("discards empty string input", function () {
const set = new IntegrityMetadataSet([]);
assert.deepEqual([...set], []);
});
test("discards invalid value", function () {
const set = new IntegrityMetadataSet("md5\0/..invalid-value");
assert.deepEqual([...set], []);
});
test("discards invalid values in a list of multiple inputs", function () {
const set = new IntegrityMetadataSet(
"sha1-lDpwLQbzRZmu4fjajvn3KWAx1pk= md5\0/..invalid-value",
);
assert.deepEqual([...set], []);
});

View file

@ -0,0 +1,32 @@
import assert from "node:assert";
import { test } from "node:test";
import { IntegrityMetadata, IntegrityMetadataSet } from "../../src/index.ts";
test("correctly iterate over the set", function () {
const integrityMetadataSet = new IntegrityMetadataSet(`
sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=
sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r
sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==
`);
assert.deepEqual(
[...integrityMetadataSet],
[
new IntegrityMetadata(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
),
new IntegrityMetadata(
"sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r",
),
new IntegrityMetadata(
"sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==",
),
],
);
});
test("if the empty set, return the empty set", function () {
const integrityMetadataSet = new IntegrityMetadataSet([]);
assert.deepEqual([...integrityMetadataSet], []);
});

View file

@ -0,0 +1,32 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { IntegrityMetadataSet } from "../../src/index.ts";
test("join() can be used", function () {
const integrityMetadataSet = new IntegrityMetadataSet(
`
sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r
sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==
`,
);
assert.strictEqual(
integrityMetadataSet.join(),
"sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==",
);
});
test("a separator can be specified", function () {
const integrityMetadataSet = new IntegrityMetadataSet(
`
sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r
sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==
`,
);
assert.strictEqual(
integrityMetadataSet.join("\n"),
`sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r
sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==`,
);
});

View file

@ -0,0 +1,102 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { IntegrityMetadataSet } from "../../src/index.ts";
test("if the hash values match, return true", function () {
const integrityMetadata = new IntegrityMetadataSet(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
assert.strictEqual(
integrityMetadata.match({
alg: "sha256",
val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
}),
true,
);
});
test("if the hash of one of the selected algorithms matches, return true", function () {
const integrityMetadata = new IntegrityMetadataSet([
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
"sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=",
]);
assert.strictEqual(
integrityMetadata.match({
alg: "sha256",
val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
}),
true,
);
});
test("a string can be specified", function () {
const integrityMetadata = new IntegrityMetadataSet(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
assert.strictEqual(
integrityMetadata.match(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
),
true,
);
});
test("if the hash algorithms are different, return false", function () {
const integrityMetadata = new IntegrityMetadataSet(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
assert.strictEqual(
integrityMetadata.match({
alg: "sha512",
val: "MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw==",
}),
false,
);
});
test("if the hash values are different, return false", function () {
const integrityMetadata = new IntegrityMetadataSet(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
assert.strictEqual(
integrityMetadata.match({
alg: "sha256",
val: "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=",
}),
false,
);
});
test("if the hash algorithm is unsupported, return false", function () {
const integrityMetadata = new IntegrityMetadataSet(
"sha1-lDpwLQbzRZmu4fjajvn3KWAx1pk=",
);
assert.strictEqual(
integrityMetadata.match("sha1-lDpwLQbzRZmu4fjajvn3KWAx1pk="),
false,
);
});
test("if null, return false", function () {
const integrityMetadata = new IntegrityMetadataSet(null);
assert.strictEqual(integrityMetadata.match(null), false);
});
test("if empty, return false", function () {
const integrityMetadata = new IntegrityMetadataSet([]);
assert.strictEqual(integrityMetadata.match(""), false);
});
test("if invalid value, return false", function () {
const integrityMetadata = new IntegrityMetadataSet("md5\0/..invalid-value");
assert.strictEqual(integrityMetadata.match("md5\0/..invalid-value"), false);
});

View file

@ -0,0 +1,19 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { IntegrityMetadataSet } from "../../src/index.ts";
test("return the correct size of the set", function () {
const integrityMetadataSet = new IntegrityMetadataSet(`
sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=
sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r
sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==
`);
assert.strictEqual(integrityMetadataSet.size, 3);
});
test("if the empty set, return 0", function () {
const integrityMetadataSet = new IntegrityMetadataSet([]);
assert.strictEqual(integrityMetadataSet.size, 0);
});

View file

@ -0,0 +1,39 @@
import assert from "node:assert";
import { test } from "node:test";
import { IntegrityMetadataSet } from "../../src/index.ts";
test("pick the strongest hash algorithms", function () {
const integrityMetadataSet = new IntegrityMetadataSet(`
sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=
sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r
sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==
`);
assert.deepEqual(integrityMetadataSet.strongestHashAlgorithms, ["sha512"]);
});
test("if there are no supported algorithms, return the empty set", function () {
const integrityMetadataSet = new IntegrityMetadataSet(`
sha1-lDpwLQbzRZmu4fjajvn3KWAx1pk=
md5-bNNVbesNpUvKBgtMOUeYOQ==
`);
assert.deepEqual(integrityMetadataSet.strongestHashAlgorithms, []);
});
test("custom getPrioritizedHashAlgorithm function can be used", function () {
const integrityMetadataSet = new IntegrityMetadataSet(
`
sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=
sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r
sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==
`,
{
getPrioritizedHashAlgorithm() {
return "sha384";
},
},
);
assert.deepEqual(integrityMetadataSet.strongestHashAlgorithms, ["sha384"]);
});

View file

@ -0,0 +1,63 @@
import assert from "node:assert";
import { test } from "node:test";
import { IntegrityMetadataSet } from "../../src/index.ts";
test("pick the strongest metadata from set", function () {
const integrityMetadataSet = new IntegrityMetadataSet(`
sha256-gxZXfeA3KCK+ZyBybEt6liVPg+FWGf/KLVU6rufBujE=
sha384-LDW1hUX1OX+VZsNmW+LELiky69a4xF+FfVsTlqZOhqPiPj5YYo20jP6C8H8uXMZf
sha512-aqnrVqlE3w/CWs51jb3FHCsFSBwfpecdXHaFFYZNkxfW2Z1qyJm4mA9iCPK11KeWwEa8rbMDq7l6IrnevQuOQw==
sha512-P8q/bH6NoZs5MnZKL9D/r/oEZOlyEAmSfuXuJchD2WeXnKbnfcO3fF0WvO6CiqZUGWsEREs9BWLrv1xr3NPOLg==
`);
assert.deepEqual(
[...integrityMetadataSet.strongest],
[
{
alg: "sha512",
val: "aqnrVqlE3w/CWs51jb3FHCsFSBwfpecdXHaFFYZNkxfW2Z1qyJm4mA9iCPK11KeWwEa8rbMDq7l6IrnevQuOQw==",
opt: [],
},
{
alg: "sha512",
val: "P8q/bH6NoZs5MnZKL9D/r/oEZOlyEAmSfuXuJchD2WeXnKbnfcO3fF0WvO6CiqZUGWsEREs9BWLrv1xr3NPOLg==",
opt: [],
},
],
);
});
test("if there are no supported algorithms, return the empty set", function () {
const integrityMetadataSet = new IntegrityMetadataSet(`
sha1-lDpwLQbzRZmu4fjajvn3KWAx1pk=
md5-bNNVbesNpUvKBgtMOUeYOQ==
`);
assert.deepEqual([...integrityMetadataSet.strongest], []);
});
test("custom getPrioritizedHashAlgorithm function can be used", function () {
const integrityMetadataSet = new IntegrityMetadataSet(
`
sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=
sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r
sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==
`,
{
getPrioritizedHashAlgorithm() {
return "sha384";
},
},
);
assert.deepEqual(
[...integrityMetadataSet.strongest],
[
{
alg: "sha384",
val: "VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r",
opt: [],
},
],
);
});

View file

@ -0,0 +1,17 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { IntegrityMetadataSet } from "../../src/index.ts";
test("toJSON() can be used", function () {
const integrityMetadataSet = new IntegrityMetadataSet(
`
sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r
sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==
`,
);
assert.strictEqual(
integrityMetadataSet.toJSON(),
"sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==",
);
});

View file

@ -0,0 +1,17 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { IntegrityMetadataSet } from "../../src/index.ts";
test("toString() can be used", function () {
const integrityMetadataSet = new IntegrityMetadataSet(
`
sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r
sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==
`,
);
assert.strictEqual(
integrityMetadataSet.toString(),
"sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==",
);
});

View file

@ -0,0 +1,112 @@
import assert from "node:assert";
import { test } from "node:test";
import { IntegrityMetadata } from "../../src/index.ts";
test("supports SHA-256", function () {
const integrityMetadata = new IntegrityMetadata(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
assert.deepEqual(integrityMetadata, {
alg: "sha256",
val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
opt: [],
});
});
test("supports SHA-384", function () {
const integrityMetadata = new IntegrityMetadata(
"sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r",
);
assert.deepEqual(integrityMetadata, {
alg: "sha384",
val: "VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r",
opt: [],
});
});
test("supports SHA-512", function () {
const integrityMetadata = new IntegrityMetadata(
"sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==",
);
assert.deepEqual(integrityMetadata, {
alg: "sha512",
val: "wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==",
opt: [],
});
});
test("accepts options", function () {
const integrityMetadata = new IntegrityMetadata(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=?foo?bar",
);
assert.deepEqual(integrityMetadata, {
alg: "sha256",
val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
opt: ["foo", "bar"],
});
});
test("accepts an IntegrityMetadata like object as input", function () {
const integrityMetadata = new IntegrityMetadata({
alg: "sha256",
val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
});
assert.deepEqual(
integrityMetadata,
new IntegrityMetadata(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
),
);
});
test("trims leading and trailing whitespace", function () {
const integrityMetadata = new IntegrityMetadata(
"\t sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=\u0020\u00a0\u1680\u2000\u2001\u2002\u3000",
);
assert.deepEqual(
integrityMetadata,
new IntegrityMetadata(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
),
);
});
test("discards unsupported hash algorithm", function () {
const integrityMetadata = new IntegrityMetadata(
"sha1-lDpwLQbzRZmu4fjajvn3KWAx1pk=",
);
assert.deepEqual(integrityMetadata, new IntegrityMetadata(""));
});
test("discards null input", function () {
const integrityMetadata = new IntegrityMetadata(null);
assert.deepEqual(integrityMetadata, new IntegrityMetadata(""));
});
test("discards empty string input", function () {
const integrityMetadata = new IntegrityMetadata("");
assert.deepEqual(integrityMetadata, new IntegrityMetadata(""));
});
test("discards invalid value", function () {
const integrityMetadata = new IntegrityMetadata("md5\0/..invalid-value");
assert.deepEqual(integrityMetadata, new IntegrityMetadata(""));
});
test("discards invalid values in a list of multiple inputs", function () {
const integrityMetadata = new IntegrityMetadata(
"sha1-lDpwLQbzRZmu4fjajvn3KWAx1pk= md5\0/..invalid-value",
);
assert.deepEqual(integrityMetadata, new IntegrityMetadata(""));
});

View file

@ -0,0 +1,82 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { IntegrityMetadata } from "../../src/index.ts";
test("if the hash values match, return true", function () {
const integrityMetadata = new IntegrityMetadata(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
assert.strictEqual(
integrityMetadata.match({
alg: "sha256",
val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
}),
true,
);
});
test("if the hash algorithms are different, return false", function () {
const integrityMetadata = new IntegrityMetadata(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
assert.strictEqual(
integrityMetadata.match({
alg: "sha512",
val: "MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw==",
}),
false,
);
});
test("if the hash values are different, return false", function () {
const integrityMetadata = new IntegrityMetadata(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
assert.strictEqual(
integrityMetadata.match({
alg: "sha256",
val: "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=",
}),
false,
);
});
test("if the hash algorithm is unsupported, return false", function () {
const integrityMetadata = new IntegrityMetadata(
"sha1-lDpwLQbzRZmu4fjajvn3KWAx1pk=",
);
assert.strictEqual(
integrityMetadata.match(
new IntegrityMetadata("sha1-lDpwLQbzRZmu4fjajvn3KWAx1pk="),
),
false,
);
});
test("if null, return false", function () {
const integrityMetadata = new IntegrityMetadata(null);
assert.strictEqual(
integrityMetadata.match(new IntegrityMetadata(null)),
false,
);
});
test("if empty, return false", function () {
const integrityMetadata = new IntegrityMetadata("");
assert.strictEqual(integrityMetadata.match(new IntegrityMetadata("")), false);
});
test("if invalid value, return false", function () {
const integrityMetadata = new IntegrityMetadata("md5\0/..invalid-value");
assert.strictEqual(
integrityMetadata.match(new IntegrityMetadata("md5\0/..invalid-value")),
false,
);
});

View file

@ -0,0 +1,13 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { IntegrityMetadata } from "../../src/index.ts";
test("IntegrityMetadata like object can be serialized", function () {
assert.strictEqual(
IntegrityMetadata.stringify({
alg: "sha256",
val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
}),
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
});

View file

@ -0,0 +1,14 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { IntegrityMetadata } from "../../src/index.ts";
test("toJSON() can be used", function () {
const integrityMetadata = new IntegrityMetadata(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
assert.strictEqual(
integrityMetadata.toJSON(),
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
});

View file

@ -0,0 +1,14 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { IntegrityMetadata } from "../../src/index.ts";
test("toString() can be used", function () {
const integrityMetadata = new IntegrityMetadata(
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
assert.strictEqual(
integrityMetadata.toString(),
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
);
});

View file

@ -1,10 +0,0 @@
import { defineConfig } from "tsup";
export default defineConfig(() => {
return {
clean: true,
dts: true,
entry: ["src"],
format: ["cjs", "esm"],
};
});