{"id":44132349,"url":"https://github.com/transcend-io/penumbra","last_synced_at":"2026-02-08T22:35:08.809Z","repository":{"id":37733333,"uuid":"185509110","full_name":"transcend-io/penumbra","owner":"transcend-io","description":"Encrypt/decrypt anything in the browser using streams on background threads.","archived":false,"fork":false,"pushed_at":"2025-12-18T23:43:25.000Z","size":49656,"stargazers_count":165,"open_issues_count":23,"forks_count":22,"subscribers_count":21,"default_branch":"main","last_synced_at":"2025-12-21T22:53:38.267Z","etag":null,"topics":["cryptography","decrypt-files","privacy","streaming","streams","whatwg-streams"],"latest_commit_sha":null,"homepage":"","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/transcend-io.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2019-05-08T02:01:45.000Z","updated_at":"2025-12-18T23:43:29.000Z","dependencies_parsed_at":"2023-12-05T16:00:43.337Z","dependency_job_id":"ea50ca74-b3ff-4856-8d1c-1e184dab6ec6","html_url":"https://github.com/transcend-io/penumbra","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/transcend-io/penumbra","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/transcend-io%2Fpenumbra","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/transcend-io%2Fpenumbra/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/transcend-io%2Fpenumbra/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/transcend-io%2Fpenumbra/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/transcend-io","download_url":"https://codeload.github.com/transcend-io/penumbra/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/transcend-io%2Fpenumbra/sbom","scorecard":{"id":896872,"data":{"date":"2025-08-11","repo":{"name":"github.com/transcend-io/penumbra","commit":"b8f256aa1702849454af955c0eb8e6d6113775c5"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":6.3,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Info: jobLevel 'contents' permission set to 'read': .github/workflows/ci.yml:72","Warn: jobLevel 'packages' permission set to 'write': .github/workflows/ci.yml:73","Warn: no topLevel permission defined: .github/workflows/ci.yml:1"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Code-Review","score":8,"reason":"Found 12/15 approved changesets -- score normalized to 8","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":10,"reason":"28 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":10,"reason":"SAST tool is run on all commits","details":["Info: all commits (27) are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:8: update your workflow using https://app.stepsecurity.io/secureworkflow/transcend-io/penumbra/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:21: update your workflow using https://app.stepsecurity.io/secureworkflow/transcend-io/penumbra/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:29: update your workflow using https://app.stepsecurity.io/secureworkflow/transcend-io/penumbra/ci.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci.yml:44: update your workflow using https://app.stepsecurity.io/secureworkflow/transcend-io/penumbra/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:51: update your workflow using https://app.stepsecurity.io/secureworkflow/transcend-io/penumbra/ci.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci.yml:58: update your workflow using https://app.stepsecurity.io/secureworkflow/transcend-io/penumbra/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:80: update your workflow using https://app.stepsecurity.io/secureworkflow/transcend-io/penumbra/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:84: update your workflow using https://app.stepsecurity.io/secureworkflow/transcend-io/penumbra/ci.yml/main?enable=pin","Info:   0 out of   6 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   2 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-24T14:04:00.766Z","repository_id":37733333,"created_at":"2025-08-24T14:04:00.766Z","updated_at":"2025-08-24T14:04:00.766Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29248081,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-08T21:42:34.334Z","status":"ssl_error","status_checked_at":"2026-02-08T21:41:38.468Z","response_time":57,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cryptography","decrypt-files","privacy","streaming","streams","whatwg-streams"],"created_at":"2026-02-08T22:35:08.134Z","updated_at":"2026-02-08T22:35:08.803Z","avatar_url":"https://github.com/transcend-io.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg alt=\"Penumbra by Transcend\" src=\"https://user-images.githubusercontent.com/7354176/61583246-43519500-aaea-11e9-82a2-e7470f3d4e00.png\"/\u003e\n\u003c/p\u003e\n\u003ch1 align=\"center\"\u003ePenumbra\u003c/h1\u003e\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eEncrypt/decrypt anything in the browser using streams on background threads.\u003c/strong\u003e\n  \u003cbr /\u003e\u003cbr /\u003e\n  \u003ci\u003eQuickly and efficiently decrypt remote resources in the browser. Display the files in the DOM, or download them with \u003ca href=\"https://github.com/transcend-io/conflux\"\u003econflux\u003c/a\u003e.\u003c/i\u003e\n  \u003cbr /\u003e\u003cbr /\u003e\n  \u003ca href=\"https://snyk.io//test/github/transcend-io/penumbra?targetFile=package.json\"\u003e\u003cimg src=\"https://snyk.io//test/github/transcend-io/penumbra/badge.svg?targetFile=package.json\" alt=\"Known Vulnerabilities\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://app.fossa.io/projects/git%2Bgithub.com%2Ftranscend-io%2Fpenumbra?ref=badge_shield\" alt=\"FOSSA Status\"\u003e\u003cimg src=\"https://app.fossa.io/api/projects/git%2Bgithub.com%2Ftranscend-io%2Fpenumbra.svg?type=shield\"/\u003e\u003c/a\u003e\n\u003c/p\u003e\n\u003cbr /\u003e\n\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n\n- [Compatibility](#compatibility)\n- [Usage](#usage)\n  - [Importing Penumbra](#importing-penumbra)\n  - [RemoteResource](#remoteresource)\n  - [.get](#get)\n  - [.encrypt](#encrypt)\n  - [.getDecryptionInfo](#getdecryptioninfo)\n  - [.decrypt](#decrypt)\n  - [.save](#save)\n  - [.getBlob](#getblob)\n  - [.getTextOrURI](#gettextoruri)\n  - [.saveZip](#savezip)\n- [Examples](#examples)\n  - [Display encrypted text](#display-encrypted-text)\n  - [Display encrypted image](#display-encrypted-image)\n  - [Download an encrypted file](#download-an-encrypted-file)\n  - [Download many encrypted files](#download-many-encrypted-files)\n- [Advanced](#advanced)\n  - [Prepare connections for file downloads in advance](#prepare-connections-for-file-downloads-in-advance)\n  - [Encrypt/Decrypt Job Completion Event Emitter](#encryptdecrypt-job-completion-event-emitter)\n  - [Progress Event Emitter](#progress-event-emitter)\n  - [Querying Penumbra browser support](#querying-penumbra-browser-support)\n- [Contributing](#contributing)\n- [License](#license)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n## Compatibility\n\n|              | .decrypt | .encrypt | .saveZip |\n| ------------ | -------: | -------: | -------: |\n| Chrome       |       ✅ |       ✅ |       ✅ |\n| Edge \u003e18     |       ✅ |       ✅ |       ✅ |\n| Safari ≥14.1 |       ✅ |       ✅ |       ✅ |\n| Firefox ≥102 |       ✅ |       ✅ |       ✅ |\n\n✅ = Full support with workers\n\n## Usage\n\n### Importing Penumbra\n\n```sh\nnpm install --save @transcend-io/penumbra\n```\n\n```js\nimport { penumbra } from '@transcend-io/penumbra';\n\npenumbra.get(...files).then(penumbra.save);\n```\n\n### RemoteResource\n\n`penumbra.get()` uses RemoteResource descriptors to specify where to request resources and their various decryption parameters.\n\n```ts\n/**\n * A file to download from a remote resource, that is optionally encrypted\n */\ntype RemoteResource = {\n  /** The URL to fetch the encrypted or unencrypted file from */\n  url: string;\n  /** The mimetype of the resulting file */\n  mimetype?: string;\n  /** The name of the underlying file without the extension */\n  filePrefix?: string;\n  /** If the file is encrypted, these are the required params */\n  decryptionOptions?: PenumbraDecryptionOptions;\n  /** Relative file path (needed for zipping) */\n  path?: string;\n  /** Fetch options */\n  requestInit?: RequestInit;\n  /** Last modified date */\n  lastModified?: Date;\n  /** Expected file size */\n  size?: number;\n};\n```\n\n### .get\n\nFetch and decrypt remote files.\n\n```ts\npenumbra.get(...resources: RemoteResource[]): Promise\u003cPenumbraFile[]\u003e\n```\n\n### .encrypt\n\nEncrypt files.\n\n```ts\npenumbra.encrypt(options: PenumbraEncryptionOptions, ...files: PenumbraFile[]): Promise\u003cPenumbraEncryptedFile[]\u003e\n\n/**\n * penumbra.encrypt() encryption options config (Uint8Array or base64-encoded string)\n */\ntype PenumbraEncryptionOptions = {\n  /** Encryption key */\n  key: string | Uint8Array;\n};\n```\n\n#### .encrypt() examples:\n\nEncrypt an empty stream:\n\n```ts\nsize = 4096 * 128;\naddEventListener('penumbra-progress', (e) =\u003e console.log(e.type, e.detail));\naddEventListener('penumbra-complete', (e) =\u003e console.log(e.type, e.detail));\nfile = penumbra.encrypt(null, {\n  stream: new Response(new Uint8Array(size)).body,\n  size,\n});\ndata = [];\nfile.then(async ([encrypted]) =\u003e {\n  console.log('encryption complete');\n  data.push(new Uint8Array(await new Response(encrypted.stream).arrayBuffer()));\n});\n```\n\nEncrypt and decrypt text:\n\n```ts\nconst te = new self.TextEncoder();\nconst td = new self.TextDecoder();\nconst input = '[test string]';\nconst buffer = te.encode(input);\nconst { byteLength: size } = buffer;\nconst stream = new Response(buffer).body;\nconst options = null;\nconst file = {\n  stream,\n  size,\n};\nconst [encrypted] = await penumbra.encrypt(options, file);\nconst decryptionInfo = await penumbra.getDecryptionInfo(encrypted);\nconst [decrypted] = await penumbra.decrypt(decryptionInfo, encrypted);\nconst decryptedData = await new Response(decrypted.stream).arrayBuffer();\nconst decryptedText = td.decode(decryptedData);\nconsole.log('decrypted text:', decryptedText);\n```\n\n### .getDecryptionInfo\n\nGet decryption info for a file, including the iv, authTag, and key. This may only be called on files that have finished being encrypted.\n\n```ts\npenumbra.getDecryptionInfo(file: PenumbraFile): Promise\u003cPenumbraDecryptionOptions\u003e\n```\n\n### .decrypt\n\nDecrypt files.\n\n```ts\npenumbra.decrypt(options: PenumbraDecryptionOptions, ...files: PenumbraEncryptedFile[]): Promise\u003cPenumbraFile[]\u003e\n```\n\n```ts\nconst te = new TextEncoder();\nconst td = new TextDecoder();\nconst data = te.encode('test');\nconst { byteLength: size } = data;\nconst [encrypted] = await penumbra.encrypt(null, {\n  stream: data,\n  size,\n});\nconst options = await penumbra.getDecryptionInfo(encrypted);\nconst [decrypted] = await penumbra.decrypt(options, encrypted);\nconst decryptedData = await new Response(decrypted.stream).arrayBuffer();\nreturn td.decode(decryptedData) === 'test';\n```\n\n### .save\n\nSave files retrieved by Penumbra. Downloads a .zip if there are multiple files. Returns an AbortController that can be used to cancel an in-progress save stream.\n\n```ts\npenumbra.save(data: PenumbraFile[], fileName?: string): AbortController\n```\n\n### .getBlob\n\nLoad files retrieved by Penumbra into memory as a Blob.\n\n```ts\npenumbra.getBlob(data: PenumbraFile[] | PenumbraFile | ReadableStream, type?: string): Promise\u003cBlob\u003e\n```\n\n### .getTextOrURI\n\nGet file text (if content is text) or [URI](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) (if content is not viewable).\n\n```ts\npenumbra.getTextOrURI(data: PenumbraFile[]): Promise\u003c{ type: 'text'|'uri', data: string, mimetype: string }[]\u003e\n```\n\n### .saveZip\n\nSave a zip containing files retrieved by Penumbra.\n\n```ts\ntype ZipOptions = {\n  /** Filename to save to (.zip is optional) */\n  name?: string;\n  /** Total size of archive in bytes (if known ahead of time, for 'store' compression level) */\n  size?: number;\n  /** PenumbraFile[] to add to zip archive */\n  files?: PenumbraFile[];\n  /** Abort controller for cancelling zip generation and saving */\n  controller?: AbortController;\n  /** Allow \u0026 auto-rename duplicate files sent to writer. Defaults to on */\n  allowDuplicates: boolean;\n  /** Zip archive compression level */\n  compressionLevel?: number;\n  /** Store a copy of the resultant zip file in-memory for inspection \u0026 testing */\n  saveBuffer?: boolean;\n  /**\n   * Auto-registered `'progress'` event listener. This is equivalent to calling\n   * `PenumbraZipWriter.addEventListener('progress', onProgress)`\n   */\n  onProgress?(event: CustomEvent\u003cZipProgressDetails\u003e): void;\n  /**\n   * Auto-registered `'complete'` event listener. This is equivalent to calling\n   * `PenumbraZipWriter.addEventListener('complete', onComplete)`\n   */\n  onComplete?(event: CustomEvent\u003c{}\u003e): void;\n};\n\npenumbra.saveZip(options?: ZipOptions): PenumbraZipWriter;\n\ninterface PenumbraZipWriter extends EventTarget {\n  /**\n   * Add decrypted PenumbraFiles to zip\n   *\n   * @param files - Decrypted PenumbraFile[] to add to zip\n   * @returns Total observed size of write call in bytes\n   */\n  write(...files: PenumbraFile[]): Promise\u003cnumber\u003e;\n  /**\n   * Enqueue closing of the Penumbra zip writer (after pending writes finish)\n   *\n   * @returns Total observed zip size in bytes after close completes\n   */\n  close(): Promise\u003cnumber\u003e;\n  /** Cancel Penumbra zip writer */\n  abort(): void;\n  /** Get buffered output (requires saveBuffer mode) */\n  getBuffer(): Promise\u003cArrayBuffer\u003e;\n  /** Get all written \u0026 pending file paths */\n  getFiles(): string[];\n  /**\n   * Get observed zip size after all pending writes are resolved\n   */\n  getSize(): Promise\u003cnumber\u003e;\n}\n\ntype ZipProgressDetails = {\n  /** Percentage completed. `null` indicates indetermination */\n  percent: number | null;\n  /** The number of bytes or items written so far */\n  written: number;\n  /** The total number of bytes or items to write. `null` indicates indetermination */\n  size: number | null;\n};\n```\n\nExample:\n\n```ts\nconst files = [\n  {\n    url: 'https://s3-us-west-2.amazonaws.com/your-bucket/tortoise.jpg.enc',\n    filePrefix: 'tortoise.jpg',\n    mimetype: 'image/jpeg',\n    decryptionOptions: {\n      key: 'vScyqmJKqGl73mJkuwm/zPBQk0wct9eQ5wPE8laGcWM=',\n      iv: '6lNU+2vxJw6SFgse',\n      authTag: 'ELry8dZ3djg8BRB+7TyXZA==',\n    },\n  },\n];\nconst writer = penumbra.saveZip();\nawait writer.write(...(await penumbra.get(...files)));\nawait writer.close();\n```\n\n## Examples\n\n### Display encrypted text\n\n```js\nconst decryptedText = await penumbra\n  .get({\n    url: 'https://s3-us-west-2.amazonaws.com/your-bucket/NYT.txt.enc',\n    mimetype: 'text/plain',\n    filePrefix: 'NYT',\n    decryptionOptions: {\n      key: 'vScyqmJKqGl73mJkuwm/zPBQk0wct9eQ5wPE8laGcWM=',\n      iv: '6lNU+2vxJw6SFgse',\n      authTag: 'gadZhS1QozjEmfmHLblzbg==',\n    },\n  })\n  .then((file) =\u003e penumbra.getTextOrURI(file)[0])\n  .then(({ data }) =\u003e {\n    document.getElementById('my-paragraph').innerText = data;\n  });\n```\n\n### Display encrypted image\n\n```js\nconst imageSrc = await penumbra\n  .get({\n    url: 'https://s3-us-west-2.amazonaws.com/your-bucket/tortoise.jpg.enc',\n    filePrefix: 'tortoise',\n    mimetype: 'image/jpeg',\n    decryptionOptions: {\n      key: 'vScyqmJKqGl73mJkuwm/zPBQk0wct9eQ5wPE8laGcWM=',\n      iv: '6lNU+2vxJw6SFgse',\n      authTag: 'ELry8dZ3djg8BRB+7TyXZA==',\n    },\n  })\n  .then((file) =\u003e penumbra.getTextOrURI(file)[0])\n  .then(({ data }) =\u003e {\n    document.getElementById('my-img').src = data;\n  });\n```\n\n### Download an encrypted file\n\n```js\npenumbra\n  .get({\n    url: 'https://s3-us-west-2.amazonaws.com/your-bucket/africa.topo.json.enc',\n    filePrefix: 'africa.topo.json',\n    mimetype: 'application/json',\n    decryptionOptions: {\n      key: 'vScyqmJKqGl73mJkuwm/zPBQk0wct9eQ5wPE8laGcWM=',\n      iv: '6lNU+2vxJw6SFgse',\n      authTag: 'ELry8dZ3djg8BRB+7TyXZA==',\n    },\n  })\n  .then((file) =\u003e penumbra.save(file));\n\n// saves africa.topo.json file to disk\n```\n\n### Download many encrypted files\n\n```js\npenumbra\n  .get(\n    {\n      url: 'https://s3-us-west-2.amazonaws.com/your-bucket/africa.topo.json.enc',\n      filePrefix: 'africa',\n      mimetype: 'image/jpeg',\n      decryptionOptions: {\n        key: 'vScyqmJKqGl73mJkuwm/zPBQk0wct9eQ5wPE8laGcWM=',\n        iv: '6lNU+2vxJw6SFgse',\n        authTag: 'ELry8dZ3djg8BRB+7TyXZA==',\n      },\n    },\n    {\n      url: 'https://s3-us-west-2.amazonaws.com/your-bucket/NYT.txt.enc',\n      mimetype: 'text/plain',\n      filePrefix: 'NYT',\n      decryptionOptions: {\n        key: 'vScyqmJKqGl73mJkuwm/zPBQk0wct9eQ5wPE8laGcWM=',\n        iv: '6lNU+2vxJw6SFgse',\n        authTag: 'gadZhS1QozjEmfmHLblzbg==',\n      },\n    },\n    {\n      url: 'https://s3-us-west-2.amazonaws.com/your-bucket/tortoise.jpg', // this is not encrypted\n      filePrefix: 'tortoise',\n      mimetype: 'image/jpeg',\n    },\n  )\n  .then((files) =\u003e penumbra.save(files, 'example'));\n\n// saves example.zip file to disk\n```\n\n## Advanced\n\n### Prepare connections for file downloads in advance\n\n```js\n// Resources to load\nconst resources = [\n  {\n    url: 'https://s3-us-west-2.amazonaws.com/your-bucket/NYT.txt.enc',\n    filePrefix: 'NYT',\n    mimetype: 'text/plain',\n    decryptionOptions: {\n      key: 'vScyqmJKqGl73mJkuwm/zPBQk0wct9eQ5wPE8laGcWM=',\n      iv: '6lNU+2vxJw6SFgse',\n      authTag: 'gadZhS1QozjEmfmHLblzbg==',\n    },\n  },\n  {\n    url: 'https://s3-us-west-2.amazonaws.com/your-bucket/tortoise.jpg.enc',\n    filePrefix: 'tortoise',\n    mimetype: 'image/jpeg',\n    decryptionOptions: {\n      key: 'vScyqmJKqGl73mJkuwm/zPBQk0wct9eQ5wPE8laGcWM=',\n      iv: '6lNU+2vxJw6SFgse',\n      authTag: 'ELry8dZ3djg8BRB+7TyXZA==',\n    },\n  },\n];\n\n// preconnect to the origins\npenumbra.preconnect(...resources);\n\n// or preload all of the URLS\npenumbra.preload(...resources);\n```\n\n### Encrypt/Decrypt Job Completion Event Emitter\n\nYou can listen to encrypt/decrypt job completion events through the `penumbra-complete` event.\n\n```js\nwindow.addEventListener(\n  'penumbra-complete',\n  ({ detail: { id, decryptionInfo } }) =\u003e {\n    console.log(\n      `finished encryption job #${id}%. decryption options:`,\n      decryptionInfo,\n    );\n  },\n);\n```\n\n### Progress Event Emitter\n\nYou can listen to download and encrypt/decrypt job progress events through the `penumbra-progress` event.\n\n```js\nwindow.addEventListener(\n  'penumbra-progress',\n  ({ detail: { percent, id, type } }) =\u003e {\n    console.log(`${type}% ${percent}% done for ${id}`);\n    // example output: decrypt 33% done for https://example.com/encrypted-data\n  },\n);\n```\n\nNote: this feature requires the `Content-Length` response header to be exposed. This works by adding `Access-Control-Expose-Headers: Content-Length` to the response header (read more [here](https://www.html5rocks.com/en/tutorials/cors/) and [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers))\n\nOn Amazon S3, this means adding the following line to your bucket policy, inside the `\u003cCORSRule\u003e` block:\n\n```xml\n\u003cExposeHeader\u003eContent-Length\u003c/ExposeHeader\u003e\n```\n\n### Querying Penumbra browser support\n\nYou can check if Penumbra is supported by the current browser by comparing `penumbra.supported(): PenumbraSupportLevel` with `penumbra.supported.levels`.\n\n```ts\nif (penumbra.supported() \u003e penumbra.supported.levels.none) {\n  // penumbra is supported\n}\n\n/** penumbra.supported.levels - Penumbra user agent support levels */\nenum PenumbraSupportLevel {\n  /** Old browser where Penumbra does not work at all */\n  none = -0,\n  /** Modern browser with full support */\n  full = 2,\n}\n```\n\nEverything Penumbra uses is widely supported by modern browsers, but depending on your browser target, you can load polyfills for:\n\n- `TransformStream`\n- `WritableStream`\n- `ReadableStream`\n- `CustomEvent`\n- `Proxy`\n- `BigInt` (if using `penumbra.saveZip`)\n\n## Contributing\n\n```bash\n# setup\npnpm install\npnpm build\n\n# run tests\npnpm test\n```\n\n## License\n\n[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Ftranscend-io%2Fpenumbra.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Ftranscend-io%2Fpenumbra?ref=badge_large)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftranscend-io%2Fpenumbra","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftranscend-io%2Fpenumbra","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftranscend-io%2Fpenumbra/lists"}