From 17adf2db6c4a11459c1c7fbcb51b10af08f813a9 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sun, 8 Feb 2026 10:05:23 +1100 Subject: [PATCH 1/3] fix(docs): move content into apps/docs to resolve nextjs turbopack issue --- apps/docs/bun.lock | 52 +++++-- .../docs/content}/docs/acknowledgements.mdx | 0 .../docs/advanced/library-authors.mdx | 0 .../docs/advanced/low-level-drawing.mdx | 144 +++++++++--------- .../docs/content}/docs/advanced/meta.json | 0 .../docs/content}/docs/api/annotations.mdx | 0 .../docs/content}/docs/api/errors.mdx | 0 .../docs/content}/docs/api/index.mdx | 0 .../docs/content}/docs/api/meta.json | 0 .../docs/content}/docs/api/pdf-form.mdx | 0 .../docs/content}/docs/api/pdf-page.mdx | 32 ++-- .../docs/content}/docs/api/pdf.mdx | 75 ++++----- .../docs/concepts/incremental-saves.mdx | 0 .../docs/content}/docs/concepts/meta.json | 0 .../content}/docs/concepts/object-model.mdx | 0 .../content}/docs/concepts/pdf-structure.mdx | 0 .../docs/getting-started/create-pdf.mdx | 0 .../docs/getting-started/installation.mdx | 0 .../content}/docs/getting-started/meta.json | 0 .../docs/getting-started/parse-pdf.mdx | 0 .../docs/content}/docs/guides/drawing.mdx | 4 +- .../docs/content}/docs/guides/encryption.mdx | 0 .../docs/content}/docs/guides/fonts.mdx | 0 .../docs/content}/docs/guides/forms.mdx | 0 .../docs/content}/docs/guides/meta.json | 0 .../docs/content}/docs/guides/pages.mdx | 0 .../docs/guides/signatures/google-kms.mdx | 0 .../content}/docs/guides/signatures/index.mdx | 0 .../content}/docs/guides/signatures/meta.json | 0 .../content}/docs/guides/text-extraction.mdx | 0 {content => apps/docs/content}/docs/index.mdx | 0 {content => apps/docs/content}/docs/meta.json | 0 .../content}/docs/migration/from-pdf-lib.mdx | 0 .../docs/content}/docs/migration/meta.json | 0 apps/docs/next.config.mjs | 4 + apps/docs/package.json | 12 +- apps/docs/source.config.ts | 2 +- 37 files changed, 166 insertions(+), 159 deletions(-) rename {content => apps/docs/content}/docs/acknowledgements.mdx (100%) rename {content => apps/docs/content}/docs/advanced/library-authors.mdx (100%) rename {content => apps/docs/content}/docs/advanced/low-level-drawing.mdx (71%) rename {content => apps/docs/content}/docs/advanced/meta.json (100%) rename {content => apps/docs/content}/docs/api/annotations.mdx (100%) rename {content => apps/docs/content}/docs/api/errors.mdx (100%) rename {content => apps/docs/content}/docs/api/index.mdx (100%) rename {content => apps/docs/content}/docs/api/meta.json (100%) rename {content => apps/docs/content}/docs/api/pdf-form.mdx (100%) rename {content => apps/docs/content}/docs/api/pdf-page.mdx (96%) rename {content => apps/docs/content}/docs/api/pdf.mdx (90%) rename {content => apps/docs/content}/docs/concepts/incremental-saves.mdx (100%) rename {content => apps/docs/content}/docs/concepts/meta.json (100%) rename {content => apps/docs/content}/docs/concepts/object-model.mdx (100%) rename {content => apps/docs/content}/docs/concepts/pdf-structure.mdx (100%) rename {content => apps/docs/content}/docs/getting-started/create-pdf.mdx (100%) rename {content => apps/docs/content}/docs/getting-started/installation.mdx (100%) rename {content => apps/docs/content}/docs/getting-started/meta.json (100%) rename {content => apps/docs/content}/docs/getting-started/parse-pdf.mdx (100%) rename {content => apps/docs/content}/docs/guides/drawing.mdx (99%) rename {content => apps/docs/content}/docs/guides/encryption.mdx (100%) rename {content => apps/docs/content}/docs/guides/fonts.mdx (100%) rename {content => apps/docs/content}/docs/guides/forms.mdx (100%) rename {content => apps/docs/content}/docs/guides/meta.json (100%) rename {content => apps/docs/content}/docs/guides/pages.mdx (100%) rename {content => apps/docs/content}/docs/guides/signatures/google-kms.mdx (100%) rename {content => apps/docs/content}/docs/guides/signatures/index.mdx (100%) rename {content => apps/docs/content}/docs/guides/signatures/meta.json (100%) rename {content => apps/docs/content}/docs/guides/text-extraction.mdx (100%) rename {content => apps/docs/content}/docs/index.mdx (100%) rename {content => apps/docs/content}/docs/meta.json (100%) rename {content => apps/docs/content}/docs/migration/from-pdf-lib.mdx (100%) rename {content => apps/docs/content}/docs/migration/meta.json (100%) diff --git a/apps/docs/bun.lock b/apps/docs/bun.lock index f3ed455..8ca8261 100644 --- a/apps/docs/bun.lock +++ b/apps/docs/bun.lock @@ -6,20 +6,20 @@ "name": "docs", "dependencies": { "@vercel/analytics": "^1.6.1", - "fumadocs-core": "16.4.6", - "fumadocs-mdx": "14.2.4", - "fumadocs-ui": "16.4.6", + "fumadocs-core": "16.5.1", + "fumadocs-mdx": "14.2.6", + "fumadocs-ui": "16.5.1", "lucide-react": "^0.562.0", "next": "16.1.6", - "react": "^19.2.3", - "react-dom": "^19.2.3", + "react": "^19.2.4", + "react-dom": "^19.2.4", "tailwind-merge": "^3.4.0", }, "devDependencies": { "@tailwindcss/postcss": "^4.1.18", "@types/mdx": "^2.0.13", "@types/node": "^25.0.3", - "@types/react": "^19.2.7", + "@types/react": "^19.2.13", "@types/react-dom": "^19.2.3", "postcss": "^8.5.6", "tailwindcss": "^4.1.18", @@ -92,11 +92,11 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - "@formatjs/fast-memoize": ["@formatjs/fast-memoize@3.0.3", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-CArYtQKGLAOruCMeq5/RxCg6vUXFx3OuKBdTm30Wn/+gCefehmZ8Y2xSMxMrO2iel7hRyE3HKfV56t3vAU6D4Q=="], + "@formatjs/fast-memoize": ["@formatjs/fast-memoize@3.1.0", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg=="], - "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.7.5", "", { "dependencies": { "@formatjs/fast-memoize": "3.0.3", "tslib": "^2.8.0" } }, "sha512-7/nd90cn5CT7SVF71/ybUKAcnvBlr9nZlJJp8O8xIZHXFgYOC4SXExZlSdgHv2l6utjw1byidL06QzChvQMHwA=="], + "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.8.1", "", { "dependencies": { "@formatjs/fast-memoize": "3.1.0", "tslib": "^2.8.1" } }, "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA=="], - "@fumadocs/ui": ["@fumadocs/ui@16.4.6", "", { "dependencies": { "next-themes": "^0.4.6", "postcss-selector-parser": "^7.1.1", "tailwind-merge": "^3.4.0" }, "peerDependencies": { "@types/react": "*", "fumadocs-core": "16.4.6", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "tailwindcss": "^4.0.0" }, "optionalPeers": ["@types/react", "next", "tailwindcss"] }, "sha512-/skIjySh9mVNMZBkc2kdGjajGQIorFUCTUp/QSSEtdMOVIdW7776YGLvV4CFxa8hf5FuRO4xrOi8vhQqnhqwVQ=="], + "@fumadocs/tailwind": ["@fumadocs/tailwind@0.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "tailwindcss": "^4.0.0" }, "optionalPeers": ["tailwindcss"] }, "sha512-ZUoDIIqfXibEV3rftBVcBxUfQsmjWzO9ZCMnB6CAHp9JmVLjE8LNaeUGeZfwnaHUEeX+h66HaDCRGtsZN0JuAA=="], "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], @@ -316,7 +316,7 @@ "@types/node": ["@types/node@25.0.6", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q=="], - "@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="], + "@types/react": ["@types/react@19.2.13", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], @@ -412,11 +412,13 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "fumadocs-core": ["fumadocs-core@16.4.6", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.7.5", "@orama/orama": "^3.1.18", "@shikijs/rehype": "^3.21.0", "@shikijs/transformers": "^3.21.0", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "path-to-regexp": "^8.3.0", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^3.21.0", "tinyglobby": "^0.2.15", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@mixedbread/sdk": "^0.46.0", "@orama/core": "1.x.x", "@oramacloud/client": "2.x.x", "@tanstack/react-router": "1.x.x", "@types/react": "*", "algoliasearch": "5.x.x", "lucide-react": "*", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "7.x.x", "waku": "^0.26.0 || ^0.27.0", "zod": "4.x.x" }, "optionalPeers": ["@mixedbread/sdk", "@orama/core", "@oramacloud/client", "@tanstack/react-router", "@types/react", "algoliasearch", "lucide-react", "next", "react", "react-dom", "react-router", "waku", "zod"] }, "sha512-cPmKu7HmzzAOXk4TbAfJhVQ12C36nu0A8sDPi664X35lOAMr+vBtjY6yIYrc8szPEFrBcmkVRGLZyEkNDZWE/Q=="], + "framer-motion": ["framer-motion@12.33.0", "", { "dependencies": { "motion-dom": "^12.33.0", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-ca8d+rRPcDP5iIF+MoT3WNc0KHJMjIyFAbtVLvM9eA7joGSpeqDfiNH/kCs1t4CHi04njYvWyj0jS4QlEK/rJQ=="], - "fumadocs-mdx": ["fumadocs-mdx@14.2.4", "", { "dependencies": { "@mdx-js/mdx": "^3.1.1", "@standard-schema/spec": "^1.1.0", "chokidar": "^5.0.0", "esbuild": "^0.27.2", "estree-util-value-to-estree": "^3.5.0", "js-yaml": "^4.1.1", "mdast-util-to-markdown": "^2.1.2", "picocolors": "^1.1.1", "picomatch": "^4.0.3", "remark-mdx": "^3.1.1", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3", "zod": "^4.2.1" }, "peerDependencies": { "@fumadocs/mdx-remote": "^1.4.0", "@types/react": "*", "fumadocs-core": "^15.0.0 || ^16.0.0", "next": "^15.3.0 || ^16.0.0", "react": "*", "vite": "6.x.x || 7.x.x" }, "optionalPeers": ["@fumadocs/mdx-remote", "@types/react", "next", "react", "vite"], "bin": { "fumadocs-mdx": "dist/bin.js" } }, "sha512-YuDgzTopMuOOQmOhvOUfmXn2RryZY5Ev+9uwAzTBEYcLIpxIBxZl0/jHaLoYdlOMBM65AO6OBngA2SucC2hkIQ=="], + "fumadocs-core": ["fumadocs-core@16.5.1", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.8.0", "@orama/orama": "^3.1.18", "@shikijs/rehype": "^3.21.0", "@shikijs/transformers": "^3.21.0", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "path-to-regexp": "^8.3.0", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^3.21.0", "tinyglobby": "^0.2.15", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "@mixedbread/sdk": "^0.46.0", "@orama/core": "1.x.x", "@oramacloud/client": "2.x.x", "@tanstack/react-router": "1.x.x", "@types/react": "*", "algoliasearch": "5.x.x", "lucide-react": "*", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "7.x.x", "waku": "^0.26.0 || ^0.27.0", "zod": "4.x.x" }, "optionalPeers": ["@mixedbread/sdk", "@orama/core", "@oramacloud/client", "@tanstack/react-router", "@types/react", "algoliasearch", "lucide-react", "next", "react", "react-dom", "react-router", "waku", "zod"] }, "sha512-6jvt7hBHh8H9uOKQ8uIzIkvpswPN8WxC+yGis9/vExJ8lSowINhN1zIbPgp0kMD5runU+2+l4qj5k7QwOTVe1g=="], - "fumadocs-ui": ["fumadocs-ui@16.4.6", "", { "dependencies": { "@fumadocs/ui": "16.4.6", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "lucide-react": "^0.562.0", "next-themes": "^0.4.6", "react-medium-image-zoom": "^5.4.0", "scroll-into-view-if-needed": "^3.1.0" }, "peerDependencies": { "@types/react": "*", "fumadocs-core": "16.4.6", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "tailwindcss": "^4.0.0" }, "optionalPeers": ["@types/react", "next", "tailwindcss"] }, "sha512-WQD+rj2AMd5umu8cB+Q2pMl/EnCXT1MAeSb4UoEZKuoiv6zOkUiBHFemNVliijuPD2dX+18HzJlgDEk4PMVDUQ=="], + "fumadocs-mdx": ["fumadocs-mdx@14.2.6", "", { "dependencies": { "@mdx-js/mdx": "^3.1.1", "@standard-schema/spec": "^1.1.0", "chokidar": "^5.0.0", "esbuild": "^0.27.2", "estree-util-value-to-estree": "^3.5.0", "js-yaml": "^4.1.1", "mdast-util-to-markdown": "^2.1.2", "picocolors": "^1.1.1", "picomatch": "^4.0.3", "remark-mdx": "^3.1.1", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3", "zod": "^4.3.5" }, "peerDependencies": { "@fumadocs/mdx-remote": "^1.4.0", "@types/react": "*", "fumadocs-core": "^15.0.0 || ^16.0.0", "next": "^15.3.0 || ^16.0.0", "react": "*", "vite": "6.x.x || 7.x.x" }, "optionalPeers": ["@fumadocs/mdx-remote", "@types/react", "next", "react", "vite"], "bin": { "fumadocs-mdx": "dist/bin.js" } }, "sha512-T8i5IllZ6OGaZ3/4Wwjl1zovvypSsr6Cco9ZACvoABLqpqTQ2TDfrW1nBt1o9YUKyfzkwDnjKdrnrq/nDexfcg=="], + + "fumadocs-ui": ["fumadocs-ui@16.5.1", "", { "dependencies": { "@fumadocs/tailwind": "0.0.1", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "lucide-react": "^0.563.0", "motion": "^12.30.1", "next-themes": "^0.4.6", "react-medium-image-zoom": "^5.4.0", "react-remove-scroll": "^2.7.2", "scroll-into-view-if-needed": "^3.1.0", "tailwind-merge": "^3.4.0" }, "peerDependencies": { "@types/react": "*", "fumadocs-core": "16.5.1", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "tailwindcss": "^4.0.0" }, "optionalPeers": ["@types/react", "next", "tailwindcss"] }, "sha512-c+9BrSZ9GSmhQ2vhL+aophnn+qoEBB7PavyiNoNAFKHQswRjDhqhEuocLJJZB8n5MTbC15ZQ6AECg9OB5hDPTA=="], "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], @@ -590,6 +592,12 @@ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + "motion": ["motion@12.33.0", "", { "dependencies": { "framer-motion": "^12.33.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-TcND7PijsrTeIA9SRVUB8TOJQ+6mJnJ5K4a9oAJZvyI0Zy47Gq5oofU+VkTxbLcvDoKXnHspQcII2mnk3TbFsQ=="], + + "motion-dom": ["motion-dom@12.33.0", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-XRPebVypsl0UM+7v0Hr8o9UAj0S2djsQWRdHBd5iVouVpMrQqAI0C/rDAT3QaYnXnHuC5hMcwDHCboNeyYjPoQ=="], + + "motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -620,9 +628,9 @@ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], - "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], "react-medium-image-zoom": ["react-medium-image-zoom@5.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-BsE+EnFVQzFIlyuuQrZ9iTwyKpKkqdFZV1ImEQN573QPqGrIUuNni7aF+sZwDcxlsuOMayCr6oO/PZR/yJnbRg=="], @@ -718,7 +726,7 @@ "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], - "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], @@ -736,6 +744,8 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@mdx-js/mdx/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -744,6 +754,8 @@ "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@shikijs/rehype/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], @@ -756,8 +768,16 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "fumadocs-ui/lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="], + + "mdast-util-to-hast/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + + "mdast-util-to-markdown/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "unist-util-remove-position/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], } } diff --git a/content/docs/acknowledgements.mdx b/apps/docs/content/docs/acknowledgements.mdx similarity index 100% rename from content/docs/acknowledgements.mdx rename to apps/docs/content/docs/acknowledgements.mdx diff --git a/content/docs/advanced/library-authors.mdx b/apps/docs/content/docs/advanced/library-authors.mdx similarity index 100% rename from content/docs/advanced/library-authors.mdx rename to apps/docs/content/docs/advanced/library-authors.mdx diff --git a/content/docs/advanced/low-level-drawing.mdx b/apps/docs/content/docs/advanced/low-level-drawing.mdx similarity index 71% rename from content/docs/advanced/low-level-drawing.mdx rename to apps/docs/content/docs/advanced/low-level-drawing.mdx index 5271d11..6c9928c 100644 --- a/content/docs/advanced/low-level-drawing.mdx +++ b/apps/docs/content/docs/advanced/low-level-drawing.mdx @@ -25,9 +25,8 @@ page.drawOperators([ ``` - The low-level API requires understanding of PDF content stream structure. - Invalid operator sequences may produce corrupted PDFs. Use the high-level - methods when they're sufficient. + The low-level API requires understanding of PDF content stream structure. Invalid operator + sequences may produce corrupted PDFs. Use the high-level methods when they're sufficient. --- @@ -36,15 +35,15 @@ page.drawOperators([ The high-level methods (`drawRectangle`, `drawText`, etc.) cover most needs. Reach for the low-level API when you need: -| Feature | Low-Level Approach | -| --- | --- | -| Matrix transforms | `ops.concatMatrix()` for arbitrary rotation/scale/skew | -| Gradients | `createAxialShading()` or `createRadialShading()` | -| Repeating patterns | `createTilingPattern()` or `createImagePattern()` | -| Blend modes | `createExtGState({ blendMode: "Multiply" })` | -| Clipping regions | `ops.clip()` with `ops.endPath()` | -| Reusable graphics | `createFormXObject()` for stamps/watermarks | -| Fine-grained control | Direct operator sequences | +| Feature | Low-Level Approach | +| -------------------- | ------------------------------------------------------ | +| Matrix transforms | `ops.concatMatrix()` for arbitrary rotation/scale/skew | +| Gradients | `createAxialShading()` or `createRadialShading()` | +| Repeating patterns | `createTilingPattern()` or `createImagePattern()` | +| Blend modes | `createExtGState({ blendMode: "Multiply" })` | +| Clipping regions | `ops.clip()` with `ops.endPath()` | +| Reusable graphics | `createFormXObject()` for stamps/watermarks | +| Fine-grained control | Direct operator sequences | --- @@ -59,80 +58,80 @@ import { ops } from "@libpdf/core"; ### Graphics State ```typescript -ops.pushGraphicsState() // Save current state (q) -ops.popGraphicsState() // Restore saved state (Q) -ops.setGraphicsState(name) // Apply ExtGState resource (gs) -ops.concatMatrix(a, b, c, d, e, f) // Transform CTM (cm) +ops.pushGraphicsState(); // Save current state (q) +ops.popGraphicsState(); // Restore saved state (Q) +ops.setGraphicsState(name); // Apply ExtGState resource (gs) +ops.concatMatrix(a, b, c, d, e, f); // Transform CTM (cm) ``` ### Path Construction ```typescript -ops.moveTo(x, y) // Begin subpath (m) -ops.lineTo(x, y) // Line to point (l) -ops.curveTo(x1, y1, x2, y2, x3, y3) // Cubic bezier (c) -ops.rectangle(x, y, w, h) // Rectangle shorthand (re) -ops.closePath() // Close subpath (h) +ops.moveTo(x, y); // Begin subpath (m) +ops.lineTo(x, y); // Line to point (l) +ops.curveTo(x1, y1, x2, y2, x3, y3); // Cubic bezier (c) +ops.rectangle(x, y, w, h); // Rectangle shorthand (re) +ops.closePath(); // Close subpath (h) ``` ### Path Painting ```typescript -ops.stroke() // Stroke path (S) -ops.fill() // Fill path, non-zero winding (f) -ops.fillEvenOdd() // Fill path, even-odd rule (f*) -ops.fillAndStroke() // Fill then stroke (B) -ops.endPath() // Discard path without painting (n) +ops.stroke(); // Stroke path (S) +ops.fill(); // Fill path, non-zero winding (f) +ops.fillEvenOdd(); // Fill path, even-odd rule (f*) +ops.fillAndStroke(); // Fill then stroke (B) +ops.endPath(); // Discard path without painting (n) ``` ### Clipping ```typescript -ops.clip() // Set clip region, non-zero (W) -ops.clipEvenOdd() // Set clip region, even-odd (W*) +ops.clip(); // Set clip region, non-zero (W) +ops.clipEvenOdd(); // Set clip region, even-odd (W*) ``` ### Color ```typescript -ops.setStrokingGray(g) // Stroke grayscale (G) -ops.setNonStrokingGray(g) // Fill grayscale (g) -ops.setStrokingRGB(r, g, b) // Stroke RGB (RG) -ops.setNonStrokingRGB(r, g, b) // Fill RGB (rg) -ops.setStrokingCMYK(c, m, y, k) // Stroke CMYK (K) -ops.setNonStrokingCMYK(c, m, y, k) // Fill CMYK (k) -ops.setStrokingColorSpace(cs) // Set stroke color space (CS) -ops.setNonStrokingColorSpace(cs) // Set fill color space (cs) -ops.setStrokingColorN(name) // Set stroke pattern (SCN) -ops.setNonStrokingColorN(name) // Set fill pattern (scn) +ops.setStrokingGray(g); // Stroke grayscale (G) +ops.setNonStrokingGray(g); // Fill grayscale (g) +ops.setStrokingRGB(r, g, b); // Stroke RGB (RG) +ops.setNonStrokingRGB(r, g, b); // Fill RGB (rg) +ops.setStrokingCMYK(c, m, y, k); // Stroke CMYK (K) +ops.setNonStrokingCMYK(c, m, y, k); // Fill CMYK (k) +ops.setStrokingColorSpace(cs); // Set stroke color space (CS) +ops.setNonStrokingColorSpace(cs); // Set fill color space (cs) +ops.setStrokingColorN(name); // Set stroke pattern (SCN) +ops.setNonStrokingColorN(name); // Set fill pattern (scn) ``` ### Line Style ```typescript -ops.setLineWidth(w) // Line width (w) -ops.setLineCap(cap) // 0=butt, 1=round, 2=square (J) -ops.setLineJoin(join) // 0=miter, 1=round, 2=bevel (j) -ops.setMiterLimit(limit) // Miter limit ratio (M) -ops.setDashPattern(array, phase) // Dash pattern (d) +ops.setLineWidth(w); // Line width (w) +ops.setLineCap(cap); // 0=butt, 1=round, 2=square (J) +ops.setLineJoin(join); // 0=miter, 1=round, 2=bevel (j) +ops.setMiterLimit(limit); // Miter limit ratio (M) +ops.setDashPattern(array, phase); // Dash pattern (d) ``` ### Text ```typescript -ops.beginText() // Begin text object (BT) -ops.endText() // End text object (ET) -ops.setFont(name, size) // Set font (Tf) -ops.moveText(tx, ty) // Position text (Td) -ops.setTextMatrix(a, b, c, d, e, f) // Text matrix (Tm) -ops.showText(string) // Show text (Tj) +ops.beginText(); // Begin text object (BT) +ops.endText(); // End text object (ET) +ops.setFont(name, size); // Set font (Tf) +ops.moveText(tx, ty); // Position text (Td) +ops.setTextMatrix(a, b, c, d, e, f); // Text matrix (Tm) +ops.showText(string); // Show text (Tj) ``` ### XObjects and Shading ```typescript -ops.paintXObject(name) // Draw XObject (Do) -ops.paintShading(name) // Paint shading (sh) +ops.paintXObject(name); // Draw XObject (Do) +ops.paintShading(name); // Paint shading (sh) ``` --- @@ -146,7 +145,7 @@ import { Matrix, ops } from "@libpdf/core"; const matrix = Matrix.identity() .translate(200, 300) - .rotate(45) // degrees + .rotate(45) // degrees .scale(2, 1.5); page.drawOperators([ @@ -162,14 +161,14 @@ Or use raw matrix components: ```typescript // Translation: move 100 points right, 200 points up -ops.concatMatrix(1, 0, 0, 1, 100, 200) +ops.concatMatrix(1, 0, 0, 1, 100, 200); // Scale: 2x horizontal, 0.5x vertical -ops.concatMatrix(2, 0, 0, 0.5, 0, 0) +ops.concatMatrix(2, 0, 0, 0.5, 0, 0); // Rotation: 45 degrees around origin -const angle = 45 * Math.PI / 180; -ops.concatMatrix(Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), 0, 0) +const angle = (45 * Math.PI) / 180; +ops.concatMatrix(Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), 0, 0); ``` --- @@ -183,7 +182,7 @@ Create linear or radial gradients with color stops: ```typescript // CSS-style: angle + length const gradient = pdf.createLinearGradient({ - angle: 90, // 0=up, 90=right, 180=down, 270=left + angle: 90, // 0=up, 90=right, 180=down, 270=left length: 200, stops: [ { offset: 0, color: rgb(1, 0, 0) }, @@ -194,7 +193,7 @@ const gradient = pdf.createLinearGradient({ // Or explicit coordinates const axial = pdf.createAxialShading({ - coords: [0, 0, 200, 0], // x0, y0, x1, y1 + coords: [0, 0, 200, 0], // x0, y0, x1, y1 stops: [ { offset: 0, color: rgb(0, 0, 1) }, { offset: 1, color: rgb(1, 0, 1) }, @@ -206,7 +205,7 @@ const axial = pdf.createAxialShading({ ```typescript const radial = pdf.createRadialShading({ - coords: [100, 100, 0, 100, 100, 80], // x0, y0, r0, x1, y1, r1 + coords: [100, 100, 0, 100, 100, 80], // x0, y0, r0, x1, y1, r1 stops: [ { offset: 0, color: rgb(1, 1, 1) }, { offset: 1, color: rgb(0, 0, 0) }, @@ -234,9 +233,7 @@ page.drawOperators([ // Or wrap in a pattern for PathBuilder const pattern = pdf.createShadingPattern({ shading: gradient }); -page.drawPath() - .rectangle(50, 200, 200, 100) - .fill({ pattern }); +page.drawPath().rectangle(50, 200, 200, 100).fill({ pattern }); ``` --- @@ -282,9 +279,7 @@ const pattern = pdf.createImagePattern({ height: 50, }); -page.drawPath() - .circle(200, 400, 80) - .fill({ pattern }); +page.drawPath().circle(200, 400, 80).fill({ pattern }); ``` ### Gradient Pattern @@ -387,23 +382,23 @@ Restrict drawing to a region: ```typescript page.drawOperators([ ops.pushGraphicsState(), - + // Define clip region (circle) ops.moveTo(200, 300), // ... circle path using bezier curves ops.clip(), - ops.endPath(), // Required after clip - + ops.endPath(), // Required after clip + // Everything here is clipped to the circle ops.paintShading(gradientName), - - ops.popGraphicsState(), // Clipping is restored + + ops.popGraphicsState(), // Clipping is restored ]); ``` - Always follow `ops.clip()` with a path-painting operator. Use `ops.endPath()` - to discard the path, or `ops.fill()` to both clip and fill. + Always follow `ops.clip()` with a path-painting operator. Use `ops.endPath()` to discard the path, + or `ops.fill()` to both clip and fill. --- @@ -444,7 +439,7 @@ const page = pdf.addPage(); // Create button gradient const gradient = pdf.createLinearGradient({ - angle: 180, // top to bottom + angle: 180, // top to bottom length: 40, stops: [ { offset: 0, color: rgb(0.4, 0.6, 1) }, @@ -475,7 +470,8 @@ page.drawOperators([ ]); // Rounded rectangle path -page.drawPath() +page + .drawPath() .moveTo(110, 700) .lineTo(250, 700) .curveTo(255, 700, 260, 705, 260, 710) diff --git a/content/docs/advanced/meta.json b/apps/docs/content/docs/advanced/meta.json similarity index 100% rename from content/docs/advanced/meta.json rename to apps/docs/content/docs/advanced/meta.json diff --git a/content/docs/api/annotations.mdx b/apps/docs/content/docs/api/annotations.mdx similarity index 100% rename from content/docs/api/annotations.mdx rename to apps/docs/content/docs/api/annotations.mdx diff --git a/content/docs/api/errors.mdx b/apps/docs/content/docs/api/errors.mdx similarity index 100% rename from content/docs/api/errors.mdx rename to apps/docs/content/docs/api/errors.mdx diff --git a/content/docs/api/index.mdx b/apps/docs/content/docs/api/index.mdx similarity index 100% rename from content/docs/api/index.mdx rename to apps/docs/content/docs/api/index.mdx diff --git a/content/docs/api/meta.json b/apps/docs/content/docs/api/meta.json similarity index 100% rename from content/docs/api/meta.json rename to apps/docs/content/docs/api/meta.json diff --git a/content/docs/api/pdf-form.mdx b/apps/docs/content/docs/api/pdf-form.mdx similarity index 100% rename from content/docs/api/pdf-form.mdx rename to apps/docs/content/docs/api/pdf-form.mdx diff --git a/content/docs/api/pdf-page.mdx b/apps/docs/content/docs/api/pdf-page.mdx similarity index 96% rename from content/docs/api/pdf-page.mdx rename to apps/docs/content/docs/api/pdf-page.mdx index 1b30d01..8d73ca4 100644 --- a/content/docs/api/pdf-page.mdx +++ b/apps/docs/content/docs/api/pdf-page.mdx @@ -570,9 +570,9 @@ For advanced graphics operations, PDFPage provides methods to emit raw operators Emit raw PDF operators to the page content stream. -| Param | Type | Description | -| ----------- | ------------ | --------------------------- | -| `operators` | `Operator[]` | Array of operators to emit | +| Param | Type | Description | +| ----------- | ------------ | -------------------------- | +| `operators` | `Operator[]` | Array of operators to emit | ```typescript import { ops } from "@libpdf/core"; @@ -588,8 +588,8 @@ page.drawOperators([ ``` - The caller is responsible for valid operator sequences. Invalid sequences - may produce corrupted PDFs. + The caller is responsible for valid operator sequences. Invalid sequences may produce corrupted + PDFs. --- @@ -598,8 +598,8 @@ page.drawOperators([ Register a font resource and return its operator name. -| Param | Type | Description | -| ------ | ------------------------------ | ------------------ | +| Param | Type | Description | +| ------ | ------------------------------------ | ---------------- | | `font` | `EmbeddedFont \| Standard14FontName` | Font to register | **Returns**: `string` - Resource name (e.g., `"F0"`) @@ -623,9 +623,9 @@ page.drawOperators([ Register an image resource and return its operator name. -| Param | Type | Description | -| ------- | ---------- | ------------------ | -| `image` | `PDFImage` | Embedded image | +| Param | Type | Description | +| ------- | ---------- | -------------- | +| `image` | `PDFImage` | Embedded image | **Returns**: `string` - Resource name (e.g., `"Im0"`) @@ -647,8 +647,8 @@ page.drawOperators([ Register a shading (gradient) resource and return its operator name. -| Param | Type | Description | -| --------- | ------------ | ------------ | +| Param | Type | Description | +| --------- | ------------ | ---------------- | | `shading` | `PDFShading` | Shading resource | **Returns**: `string` - Resource name (e.g., `"Sh0"`) @@ -699,8 +699,8 @@ page.drawOperators([ Register an extended graphics state and return its operator name. -| Param | Type | Description | -| ------- | ------------- | -------------- | +| Param | Type | Description | +| ------- | -------------- | -------------- | | `state` | `PDFExtGState` | Graphics state | **Returns**: `string` - Resource name (e.g., `"GS0"`) @@ -728,8 +728,8 @@ page.drawOperators([ Register a Form XObject or embedded page and return its operator name. -| Param | Type | Description | -| --------- | --------------------------------- | ---------------- | +| Param | Type | Description | +| --------- | ----------------------------------- | ------------------- | | `xobject` | `PDFFormXObject \| PDFEmbeddedPage` | XObject to register | **Returns**: `string` - Resource name (e.g., `"Fm0"`) diff --git a/content/docs/api/pdf.mdx b/apps/docs/content/docs/api/pdf.mdx similarity index 90% rename from content/docs/api/pdf.mdx rename to apps/docs/content/docs/api/pdf.mdx index 87248e5..d212c14 100644 --- a/content/docs/api/pdf.mdx +++ b/apps/docs/content/docs/api/pdf.mdx @@ -653,11 +653,11 @@ These methods create PDF resources for advanced drawing operations. See [Low-Lev Create a linear gradient using CSS-style angle and length. -| Param | Type | Default | Description | -| ---------------- | ------------- | -------- | ----------------------------------------- | -| `options.angle` | `number` | required | Angle in degrees (0=up, 90=right) | -| `options.length` | `number` | required | Gradient length in points | -| `options.stops` | `ColorStop[]` | required | Color stops with offset (0-1) and color | +| Param | Type | Default | Description | +| ---------------- | ------------- | -------- | --------------------------------------- | +| `options.angle` | `number` | required | Angle in degrees (0=up, 90=right) | +| `options.length` | `number` | required | Gradient length in points | +| `options.stops` | `ColorStop[]` | required | Color stops with offset (0-1) and color | **Returns**: `PDFShading` @@ -681,10 +681,10 @@ page.drawRectangle({ x: 50, y: 50, width: 200, height: 100, pattern }); Create an axial (linear) shading with explicit coordinates. -| Param | Type | Default | Description | -| --------------- | ------------------------------------ | -------- | ----------------------------- | -| `options.coords` | `[x0, y0, x1, y1]` | required | Start and end points | -| `options.stops` | `ColorStop[]` | required | Color stops | +| Param | Type | Default | Description | +| ---------------- | ------------------ | -------- | -------------------- | +| `options.coords` | `[x0, y0, x1, y1]` | required | Start and end points | +| `options.stops` | `ColorStop[]` | required | Color stops | **Returns**: `PDFShading` @@ -705,10 +705,10 @@ const gradient = pdf.createAxialShading({ Create a radial shading with explicit coordinates. -| Param | Type | Default | Description | -| ---------------- | ----------------------------------- | -------- | ------------------------------------ | -| `options.coords` | `[x0, y0, r0, x1, y1, r1]` | required | Center and radius for both circles | -| `options.stops` | `ColorStop[]` | required | Color stops | +| Param | Type | Default | Description | +| ---------------- | -------------------------- | -------- | ---------------------------------- | +| `options.coords` | `[x0, y0, r0, x1, y1, r1]` | required | Center and radius for both circles | +| `options.stops` | `ColorStop[]` | required | Color stops | **Returns**: `PDFShading` @@ -728,11 +728,11 @@ const radial = pdf.createRadialShading({ Create a repeating tiling pattern. -| Param | Type | Default | Description | -| ------------------ | ------------ | -------- | -------------------------------- | -| `options.bbox` | `BBox` | required | Pattern cell bounding box | -| `options.xStep` | `number` | required | Horizontal spacing between tiles | -| `options.yStep` | `number` | required | Vertical spacing between tiles | +| Param | Type | Default | Description | +| ------------------- | ------------ | -------- | --------------------------------- | +| `options.bbox` | `BBox` | required | Pattern cell bounding box | +| `options.xStep` | `number` | required | Horizontal spacing between tiles | +| `options.yStep` | `number` | required | Vertical spacing between tiles | | `options.operators` | `Operator[]` | required | Content operators for the pattern | **Returns**: `PDFTilingPattern` @@ -742,11 +742,7 @@ const pattern = pdf.createTilingPattern({ bbox: { x: 0, y: 0, width: 10, height: 10 }, xStep: 10, yStep: 10, - operators: [ - ops.setNonStrokingGray(0.8), - ops.rectangle(0, 0, 5, 5), - ops.fill(), - ], + operators: [ops.setNonStrokingGray(0.8), ops.rectangle(0, 0, 5, 5), ops.fill()], }); ``` @@ -781,10 +777,10 @@ page.drawCircle({ x: 200, y: 400, radius: 80, pattern }); Wrap a shading (gradient) as a pattern for use with drawing methods. -| Param | Type | Default | Description | -| ----------------- | ------------ | -------- | --------------------- | -| `options.shading` | `PDFShading` | required | Shading to wrap | -| `[options.matrix]` | `PatternMatrix` | | Transform matrix | +| Param | Type | Default | Description | +| ------------------ | --------------- | -------- | ---------------- | +| `options.shading` | `PDFShading` | required | Shading to wrap | +| `[options.matrix]` | `PatternMatrix` | | Transform matrix | **Returns**: `PDFShadingPattern` @@ -803,11 +799,11 @@ page.drawPath() Create an extended graphics state for opacity and blend modes. -| Param | Type | Default | Description | -| ------------------------ | ----------- | ------- | ---------------- | -| `[options.fillOpacity]` | `number` | | Fill opacity 0-1 | +| Param | Type | Default | Description | +| ------------------------- | ----------- | ------- | ------------------ | +| `[options.fillOpacity]` | `number` | | Fill opacity 0-1 | | `[options.strokeOpacity]` | `number` | | Stroke opacity 0-1 | -| `[options.blendMode]` | `BlendMode` | | Blend mode | +| `[options.blendMode]` | `BlendMode` | | Blend mode | **Returns**: `PDFExtGState` @@ -827,9 +823,9 @@ const gsName = page.registerExtGState(gs); Create a reusable Form XObject (content block). -| Param | Type | Default | Description | -| ------------------ | ------------ | -------- | ----------------- | -| `options.bbox` | `BBox` | required | Bounding box | +| Param | Type | Default | Description | +| ------------------- | ------------ | -------- | ----------------- | +| `options.bbox` | `BBox` | required | Bounding box | | `options.operators` | `Operator[]` | required | Content operators | **Returns**: `PDFFormXObject` @@ -837,18 +833,11 @@ Create a reusable Form XObject (content block). ```typescript const stamp = pdf.createFormXObject({ bbox: { x: 0, y: 0, width: 100, height: 50 }, - operators: [ - ops.setNonStrokingRGB(1, 0, 0), - ops.rectangle(0, 0, 100, 50), - ops.fill(), - ], + operators: [ops.setNonStrokingRGB(1, 0, 0), ops.rectangle(0, 0, 100, 50), ops.fill()], }); const xobjectName = page.registerXObject(stamp); -page.drawOperators([ - ops.concatMatrix(1, 0, 0, 1, 200, 700), - ops.paintXObject(xobjectName), -]); +page.drawOperators([ops.concatMatrix(1, 0, 0, 1, 200, 700), ops.paintXObject(xobjectName)]); ``` --- diff --git a/content/docs/concepts/incremental-saves.mdx b/apps/docs/content/docs/concepts/incremental-saves.mdx similarity index 100% rename from content/docs/concepts/incremental-saves.mdx rename to apps/docs/content/docs/concepts/incremental-saves.mdx diff --git a/content/docs/concepts/meta.json b/apps/docs/content/docs/concepts/meta.json similarity index 100% rename from content/docs/concepts/meta.json rename to apps/docs/content/docs/concepts/meta.json diff --git a/content/docs/concepts/object-model.mdx b/apps/docs/content/docs/concepts/object-model.mdx similarity index 100% rename from content/docs/concepts/object-model.mdx rename to apps/docs/content/docs/concepts/object-model.mdx diff --git a/content/docs/concepts/pdf-structure.mdx b/apps/docs/content/docs/concepts/pdf-structure.mdx similarity index 100% rename from content/docs/concepts/pdf-structure.mdx rename to apps/docs/content/docs/concepts/pdf-structure.mdx diff --git a/content/docs/getting-started/create-pdf.mdx b/apps/docs/content/docs/getting-started/create-pdf.mdx similarity index 100% rename from content/docs/getting-started/create-pdf.mdx rename to apps/docs/content/docs/getting-started/create-pdf.mdx diff --git a/content/docs/getting-started/installation.mdx b/apps/docs/content/docs/getting-started/installation.mdx similarity index 100% rename from content/docs/getting-started/installation.mdx rename to apps/docs/content/docs/getting-started/installation.mdx diff --git a/content/docs/getting-started/meta.json b/apps/docs/content/docs/getting-started/meta.json similarity index 100% rename from content/docs/getting-started/meta.json rename to apps/docs/content/docs/getting-started/meta.json diff --git a/content/docs/getting-started/parse-pdf.mdx b/apps/docs/content/docs/getting-started/parse-pdf.mdx similarity index 100% rename from content/docs/getting-started/parse-pdf.mdx rename to apps/docs/content/docs/getting-started/parse-pdf.mdx diff --git a/content/docs/guides/drawing.mdx b/apps/docs/content/docs/guides/drawing.mdx similarity index 99% rename from content/docs/guides/drawing.mdx rename to apps/docs/content/docs/guides/drawing.mdx index 84df72a..9922944 100644 --- a/content/docs/guides/drawing.mdx +++ b/apps/docs/content/docs/guides/drawing.mdx @@ -588,9 +588,7 @@ page.drawRectangle({ }); // Also works with PathBuilder -page.drawPath() - .circle(300, 550, 50) - .fill({ pattern }); +page.drawPath().circle(300, 550, 50).fill({ pattern }); ``` See [Low-Level Drawing](/docs/advanced/low-level-drawing) for gradients, tiling patterns, and more. diff --git a/content/docs/guides/encryption.mdx b/apps/docs/content/docs/guides/encryption.mdx similarity index 100% rename from content/docs/guides/encryption.mdx rename to apps/docs/content/docs/guides/encryption.mdx diff --git a/content/docs/guides/fonts.mdx b/apps/docs/content/docs/guides/fonts.mdx similarity index 100% rename from content/docs/guides/fonts.mdx rename to apps/docs/content/docs/guides/fonts.mdx diff --git a/content/docs/guides/forms.mdx b/apps/docs/content/docs/guides/forms.mdx similarity index 100% rename from content/docs/guides/forms.mdx rename to apps/docs/content/docs/guides/forms.mdx diff --git a/content/docs/guides/meta.json b/apps/docs/content/docs/guides/meta.json similarity index 100% rename from content/docs/guides/meta.json rename to apps/docs/content/docs/guides/meta.json diff --git a/content/docs/guides/pages.mdx b/apps/docs/content/docs/guides/pages.mdx similarity index 100% rename from content/docs/guides/pages.mdx rename to apps/docs/content/docs/guides/pages.mdx diff --git a/content/docs/guides/signatures/google-kms.mdx b/apps/docs/content/docs/guides/signatures/google-kms.mdx similarity index 100% rename from content/docs/guides/signatures/google-kms.mdx rename to apps/docs/content/docs/guides/signatures/google-kms.mdx diff --git a/content/docs/guides/signatures/index.mdx b/apps/docs/content/docs/guides/signatures/index.mdx similarity index 100% rename from content/docs/guides/signatures/index.mdx rename to apps/docs/content/docs/guides/signatures/index.mdx diff --git a/content/docs/guides/signatures/meta.json b/apps/docs/content/docs/guides/signatures/meta.json similarity index 100% rename from content/docs/guides/signatures/meta.json rename to apps/docs/content/docs/guides/signatures/meta.json diff --git a/content/docs/guides/text-extraction.mdx b/apps/docs/content/docs/guides/text-extraction.mdx similarity index 100% rename from content/docs/guides/text-extraction.mdx rename to apps/docs/content/docs/guides/text-extraction.mdx diff --git a/content/docs/index.mdx b/apps/docs/content/docs/index.mdx similarity index 100% rename from content/docs/index.mdx rename to apps/docs/content/docs/index.mdx diff --git a/content/docs/meta.json b/apps/docs/content/docs/meta.json similarity index 100% rename from content/docs/meta.json rename to apps/docs/content/docs/meta.json diff --git a/content/docs/migration/from-pdf-lib.mdx b/apps/docs/content/docs/migration/from-pdf-lib.mdx similarity index 100% rename from content/docs/migration/from-pdf-lib.mdx rename to apps/docs/content/docs/migration/from-pdf-lib.mdx diff --git a/content/docs/migration/meta.json b/apps/docs/content/docs/migration/meta.json similarity index 100% rename from content/docs/migration/meta.json rename to apps/docs/content/docs/migration/meta.json diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index 6ca47d9..3c1f95b 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -1,10 +1,14 @@ import { createMDX } from "fumadocs-mdx/next"; +import path from "node:path"; const withMDX = createMDX(); /** @type {import('next').NextConfig} */ const config = { reactStrictMode: true, + turbopack: { + root: path.resolve(import.meta.dirname), + }, async rewrites() { return [ { diff --git a/apps/docs/package.json b/apps/docs/package.json index 4bc0221..40f0d35 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -11,20 +11,20 @@ }, "dependencies": { "@vercel/analytics": "^1.6.1", - "fumadocs-core": "16.4.6", - "fumadocs-mdx": "14.2.4", - "fumadocs-ui": "16.4.6", + "fumadocs-core": "16.5.1", + "fumadocs-mdx": "14.2.6", + "fumadocs-ui": "16.5.1", "lucide-react": "^0.562.0", "next": "16.1.6", - "react": "^19.2.3", - "react-dom": "^19.2.3", + "react": "^19.2.4", + "react-dom": "^19.2.4", "tailwind-merge": "^3.4.0" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.18", "@types/mdx": "^2.0.13", "@types/node": "^25.0.3", - "@types/react": "^19.2.7", + "@types/react": "^19.2.13", "@types/react-dom": "^19.2.3", "postcss": "^8.5.6", "tailwindcss": "^4.1.18", diff --git a/apps/docs/source.config.ts b/apps/docs/source.config.ts index 7b6b8b4..d6e0a83 100644 --- a/apps/docs/source.config.ts +++ b/apps/docs/source.config.ts @@ -3,7 +3,7 @@ import { defineConfig, defineDocs, frontmatterSchema, metaSchema } from "fumadoc // You can customise Zod schemas for frontmatter and `meta.json` here // see https://fumadocs.dev/docs/mdx/collections export const docs = defineDocs({ - dir: "../../content/docs", + dir: "content/docs", docs: { schema: frontmatterSchema, postprocess: { From f750486418f1a1613f14ccba64c300201ef4cda0 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sun, 8 Feb 2026 10:05:46 +1100 Subject: [PATCH 2/3] feat(signatures): add Azure Key Vault signer --- ...arm-purple-cloud-azure-key-vault-signer.md | 225 ++++ .../guides/signatures/azure-key-vault.mdx | 450 +++++++ .../content/docs/guides/signatures/index.mdx | 23 +- .../content/docs/guides/signatures/meta.json | 2 +- bun.lock | 111 +- package.json | 15 + src/index.ts | 2 + src/signatures/index.ts | 9 +- .../signers/azure-key-vault.test.ts | 600 +++++++++ src/signatures/signers/azure-key-vault.ts | 1096 +++++++++++++++++ src/signatures/signers/index.ts | 3 +- src/signatures/types.ts | 10 + 12 files changed, 2523 insertions(+), 23 deletions(-) create mode 100644 .agents/plans/warm-purple-cloud-azure-key-vault-signer.md create mode 100644 apps/docs/content/docs/guides/signatures/azure-key-vault.mdx create mode 100644 src/signatures/signers/azure-key-vault.test.ts create mode 100644 src/signatures/signers/azure-key-vault.ts diff --git a/.agents/plans/warm-purple-cloud-azure-key-vault-signer.md b/.agents/plans/warm-purple-cloud-azure-key-vault-signer.md new file mode 100644 index 0000000..ce4d42e --- /dev/null +++ b/.agents/plans/warm-purple-cloud-azure-key-vault-signer.md @@ -0,0 +1,225 @@ +--- +date: 2026-02-07 +title: Azure Key Vault Signer +--- + +## Problem Statement + +Users with keys stored in Azure Key Vault (including HSM-backed keys) need to digitally sign PDFs without extracting private keys. We already have a `GoogleKmsSigner` for GCP; we need an equivalent `AzureKeyVaultSigner` for the Azure ecosystem. + +## Goals + +- Provide an `AzureKeyVaultSigner` that implements the `Signer` interface +- Support RSA (PKCS#1 v1.5, PSS) and ECDSA signing via Azure Key Vault +- Follow the same patterns established by `GoogleKmsSigner` (optional peer dependency, async factory, local hashing, remote signing) +- Support both Azure Key Vault (software-protected keys) and Azure Key Vault Managed HSM (HSM-protected keys) +- No PKCS#11 — use Azure's REST API via their official TypeScript SDK + +## Research Findings + +### Azure Key Vault supports what we need + +Azure Key Vault provides a `CryptographyClient` (from `@azure/keyvault-keys`) with a `sign(algorithm, digest)` method that: + +1. **Takes a pre-computed digest** and returns a signature — same pattern as Google KMS +2. **Supports all algorithms we need:** + - `RS256`, `RS384`, `RS512` — RSASSA-PKCS1-v1_5 with SHA-256/384/512 + - `PS256`, `PS384`, `PS512` — RSASSA-PSS with SHA-256/384/512 + - `ES256`, `ES384`, `ES512` — ECDSA with P-256/P-384/P-521 +3. **Private key never leaves the vault** — signing happens server-side +4. **Works with both Key Vault and Managed HSM** — same API, different vault URL +5. **Uses `@azure/identity` for authentication** — supports DefaultAzureCredential, managed identities, service principals, etc. + +### Certificate management + +Azure Key Vault has two separate services relevant here: + +- **`@azure/keyvault-keys`** — For key operations (create, sign, verify). The `CryptographyClient` lives here. +- **`@azure/keyvault-certificates`** — For certificate management. Can retrieve the X.509 certificate (DER) associated with a key via `CertificateClient.getCertificate()`. + +The certificate's `.cer` property contains the DER-encoded public certificate. This is the equivalent of Google's Secret Manager approach — we can offer a helper method `getCertificateFromKeyVault()` to load the certificate directly from the vault. + +**Important distinction from GCP:** In Azure, a "certificate" in Key Vault is actually a bundle — it contains the certificate, the private key, and optionally the chain. The key and certificate share the same name. So if a user has a certificate named `"my-signing-cert"` in Key Vault, they can: + +1. Use `CertificateClient` to get the certificate bytes +2. Use `CryptographyClient` with the same key name to sign + +This is actually _simpler_ than GCP where the key and certificate live in separate services. + +### Key identification + +Azure Key Vault keys are identified by URL: + +``` +https://{vault-name}.vault.azure.net/keys/{key-name}/{key-version} +``` + +Or for Managed HSM: + +``` +https://{hsm-name}.managedhsm.azure.net/keys/{key-name}/{key-version} +``` + +The `CryptographyClient` accepts either a key URL string or a `KeyVaultKey` object. + +### Authentication + +Azure uses `TokenCredential` from `@azure/identity`. The recommended approach is `DefaultAzureCredential` which tries multiple auth methods in order (env vars, managed identity, Azure CLI, etc.). This is the equivalent of GCP's Application Default Credentials (ADC). + +### Error handling + +Azure SDK throws `RestError` with HTTP status codes. We'll need to map common ones: + +- 401/403 — Authentication/authorization failures +- 404 — Key or vault not found +- 409 — Conflict (key disabled, etc.) + +## Scope + +### In scope + +- `AzureKeyVaultSigner` class implementing `Signer` +- Algorithm mapping from Azure's algorithm names to our types +- `getCertificateFromKeyVault()` static helper method +- Public key validation (certificate matches key in vault) +- Unit tests (algorithm mapping, error handling) +- Integration tests (skipped without Azure credentials) +- Error hierarchy (`AzureKeyVaultSignerError extends KmsSignerError`) + +### Out of scope + +- Azure Managed Identity setup/configuration guidance +- PKCS#11 / native HSM communication +- Certificate issuance or renewal +- Azure Key Vault key creation + +## Desired Usage + +```typescript +import { AzureKeyVaultSigner } from "@libpdf/core"; +import { DefaultAzureCredential } from "@azure/identity"; + +// Option 1: Provide certificate explicitly +const signer = await AzureKeyVaultSigner.create({ + keyId: "https://my-vault.vault.azure.net/keys/my-signing-key/abc123", + certificate: certificateDer, + credential: new DefaultAzureCredential(), +}); + +// Option 2: Shorthand with vault name + key name +const signer = await AzureKeyVaultSigner.create({ + vaultName: "my-vault", + keyName: "my-signing-key", + keyVersion: "abc123", // optional, defaults to latest + certificate: certificateDer, + credential: new DefaultAzureCredential(), +}); + +// Sign a PDF +const pdf = await PDF.load(pdfBytes); +const { bytes } = await pdf.sign({ signer }); +``` + +```typescript +// Load certificate from Azure Key Vault directly +const { cert, chain } = await AzureKeyVaultSigner.getCertificateFromKeyVault({ + vaultUrl: "https://my-vault.vault.azure.net", + certificateName: "my-signing-cert", + credential: new DefaultAzureCredential(), +}); + +const signer = await AzureKeyVaultSigner.create({ + vaultName: "my-vault", + keyName: "my-signing-cert", // same name as certificate + certificate: cert, + certificateChain: chain, + credential: new DefaultAzureCredential(), +}); +``` + +## High-Level Architecture + +The implementation mirrors `GoogleKmsSigner` with Azure-specific adaptations: + +``` +AzureKeyVaultSigner implements Signer +├── create() — async factory +│ ├── Dynamic import of @azure/keyvault-keys +│ ├── Resolve key ID (full URL or vaultName+keyName) +│ ├── Fetch key metadata via KeyClient.getKey() +│ ├── Validate key is enabled and supports signing +│ ├── Map Azure algorithm → our types (KeyType, SignatureAlgorithm, DigestAlgorithm) +│ ├── Validate certificate public key matches vault key +│ └── Optional AIA chain building +├── sign() — hash locally, send digest to Azure +│ ├── Hash with @noble/hashes (same as GCP signer) +│ └── Call CryptographyClient.sign(algorithm, digest) +└── getCertificateFromKeyVault() — static helper + ├── Dynamic import of @azure/keyvault-certificates + └── Fetch certificate + parse chain +``` + +### Azure Algorithm Mapping + +| Azure Algorithm | KeyType | SignatureAlgorithm | DigestAlgorithm | +| --------------- | ------- | ------------------ | --------------- | +| RS256 | RSA | RSASSA-PKCS1-v1_5 | SHA-256 | +| RS384 | RSA | RSASSA-PKCS1-v1_5 | SHA-384 | +| RS512 | RSA | RSASSA-PKCS1-v1_5 | SHA-512 | +| PS256 | RSA | RSA-PSS | SHA-256 | +| PS384 | RSA | RSA-PSS | SHA-384 | +| PS512 | RSA | RSA-PSS | SHA-512 | +| ES256 | EC | ECDSA | SHA-256 | +| ES384 | EC | ECDSA | SHA-384 | +| ES512 | EC | ECDSA | SHA-512 | + +### Dependencies + +- `@azure/keyvault-keys` — Optional peer dependency (dynamically imported) +- `@azure/keyvault-certificates` — Optional peer dependency (dynamically imported, only for `getCertificateFromKeyVault`) +- `@azure/identity` — Required by user (provides `TokenCredential`), not our dependency + +### Key Differences from GCP Signer + +1. **Authentication**: Azure uses `TokenCredential` (passed explicitly) vs GCP's ADC (implicit) +2. **Algorithm detection**: Azure keys expose `keyOps` and key type, but the specific signing algorithm must be inferred from the key type + user's digest choice (unlike GCP where the algorithm is fixed at key creation) +3. **Certificate retrieval**: Azure has a dedicated certificate service in the same vault (vs GCP needing Secret Manager) +4. **Error format**: REST errors with HTTP status codes (vs gRPC status codes) +5. **Key URL format**: `https://{vault}.vault.azure.net/keys/{name}/{version}` (vs GCP resource path) + +## Test Plan + +### Unit tests + +- Algorithm mapping: all 9 Azure algorithms → our types +- Key URL building from shorthand (vaultName + keyName + keyVersion) +- Key URL parsing (extract vault name, key name, version) +- Error class hierarchy +- Rejection of unsupported key types (oct/symmetric keys) +- Validation of key operations (must include "sign") + +### Integration tests (skip without Azure credentials) + +- RSA PKCS#1 v1.5 signing + PDF signing +- RSA-PSS signing +- ECDSA signing (P-256, P-384) +- Certificate retrieval from Key Vault +- Full PDF signing flow with Azure HSM key +- Error cases: key not found, permission denied, key disabled + +## Open Questions + +1. **Algorithm detection strategy**: Azure keys don't lock to a single algorithm like GCP. An RSA key in Azure can be used with RS256, RS384, RS512, PS256, PS384, PS512. Should we: + - (a) Require the user to specify the algorithm? (more flexible) + - (b) Infer from key type + key size? (more automatic, but less control) + - (c) Let the signing flow pick based on the digest algorithm? (matches how our CMS builder calls `sign(data, digestAlgorithm)`) + + **Recommendation**: Option (c) — derive the Azure algorithm at `sign()` time from the key type + requested digest algorithm. This matches how our `Signer.sign(data, algorithm)` interface works. Store the key type (RSA vs EC) and optionally a preferred signature scheme (PKCS1 vs PSS) at creation time. + +2. **RSA scheme preference**: Since Azure RSA keys support both PKCS#1 v1.5 and PSS, should we default to PKCS#1 v1.5 (maximum compatibility) or PSS (more secure)? + + **Recommendation**: Default to PKCS#1 v1.5 for compatibility (matching the GCP signer's warning about PSS), with an option to prefer PSS. + +3. **Managed HSM vs Key Vault**: The API is identical, but the vault URL differs (`.vault.azure.net` vs `.managedhsm.azure.net`). Should we handle both transparently or have separate options? + + **Recommendation**: Handle transparently — accept any vault URL. The `KeyClient` and `CryptographyClient` work with both. diff --git a/apps/docs/content/docs/guides/signatures/azure-key-vault.mdx b/apps/docs/content/docs/guides/signatures/azure-key-vault.mdx new file mode 100644 index 0000000..cca7a47 --- /dev/null +++ b/apps/docs/content/docs/guides/signatures/azure-key-vault.mdx @@ -0,0 +1,450 @@ +--- +title: Azure Key Vault +description: Sign PDFs with keys stored in Azure Key Vault or Managed HSM, including HSM-backed keys. +--- + +# Azure Key Vault + +Sign PDFs using keys stored in Azure Key Vault or Azure Managed HSM. This is ideal for enterprise environments where private keys must never leave a hardware security module (HSM) for compliance and security reasons. + + + The private key never leaves the vault — only the digest is sent for signing. + + +## Installation + +The Azure Key Vault client is an optional peer dependency: + +```bash +npm install @azure/keyvault-keys @azure/identity +``` + +For loading certificates from Key Vault: + +```bash +npm install @azure/keyvault-certificates +``` + +## Quick Start + +```typescript +import { PDF, AzureKeyVaultSigner } from "@libpdf/core"; +import { readFile, writeFile } from "fs/promises"; + +// Load your DER-encoded certificate (issued by your CA for the vault key) +const certificate = await readFile("certificate.der"); + +// Create signer — uses DefaultAzureCredential automatically +const signer = await AzureKeyVaultSigner.create({ + vaultName: "my-vault", + keyName: "my-signing-key", + certificate, +}); + +// Sign the PDF +const pdf = await PDF.load(await readFile("document.pdf")); +const { bytes } = await pdf.sign({ signer }); + +await writeFile("signed.pdf", bytes); +``` + +--- + +## Authentication + +`AzureKeyVaultSigner` uses [`DefaultAzureCredential`](https://learn.microsoft.com/en-us/javascript/api/@azure/identity/defaultazurecredential) by default when no `credential` option is provided. This tries multiple authentication methods in order: + +| Method | Environment | Setup | +| ------------------- | ----------------- | ---------------------------------------------------- | +| Environment vars | Any | Set `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, and secret | +| Managed Identity | Azure VMs/App Svc | Automatic (uses instance metadata) | +| Azure CLI | Local development | Run `az login` | +| Workload Identity | AKS | Configure workload identity for your pod | +| Azure PowerShell | Local development | Run `Connect-AzAccount` | + +You can also provide an explicit credential: + +```typescript +import { ClientSecretCredential } from "@azure/identity"; + +const credential = new ClientSecretCredential(tenantId, clientId, clientSecret); + +const signer = await AzureKeyVaultSigner.create({ + vaultName: "my-vault", + keyName: "my-key", + certificate: certificateDer, + credential, +}); +``` + +### Required Permissions + +The authenticating identity needs these Key Vault permissions: + +- **Key Get** — Read key metadata and public key +- **Key Sign** — Sign digests with the key + +If using Key Vault access policies, assign the **Key Sign** and **Key Get** permissions. If using Azure RBAC, assign the **Key Vault Crypto User** role. + +For loading certificates, you also need: + +- **Certificate Get** — Read the certificate + +--- + +## AzureKeyVaultSigner.create(options) + +Create a new Azure Key Vault signer instance. + +### Full Key ID URL + +| Param | Type | Default | Description | +| ------------------------------- | ------------------ | ---------- | ----------------------------------------------- | +| `options` | `object` | required | | +| `options.keyId` | `string` | required | Full key identifier URL | +| `options.certificate` | `Uint8Array` | required | DER-encoded X.509 certificate for this key | +| `[options.credential]` | `TokenCredential` | | Azure credential (default: DefaultAzureCredential) | +| `[options.certificateChain]` | `Uint8Array[]` | | Intermediate and root certificates | +| `[options.buildChain]` | `boolean` | `false` | Fetch chain via AIA extensions | +| `[options.chainTimeout]` | `number` | `15000` | Timeout for AIA fetching (ms) | +| `[options.rsaScheme]` | `"PKCS1" \| "PSS"` | `"PKCS1"` | RSA signature scheme (RSA keys only) | +| `[options.keyClient]` | `KeyClient` | | Pre-configured Key Vault client | +| `[options.cryptographyClient]` | `CryptographyClient` | | Pre-configured Cryptography client | + +```typescript +const signer = await AzureKeyVaultSigner.create({ + keyId: "https://my-vault.vault.azure.net/keys/my-key/abc123", + certificate: certificateDer, + buildChain: true, // Automatically fetch intermediate certificates +}); +``` + +### Shorthand Options + +Instead of a full key ID URL, you can use shorthand properties: + +| Param | Type | Default | Description | +| ------------------------ | -------- | -------------------- | ---------------------------- | +| `options.vaultName` | `string` | required | Vault name | +| `options.keyName` | `string` | required | Key name in the vault | +| `[options.keyVersion]` | `string` | | Key version (default: latest) | +| `[options.vaultSuffix]` | `string` | `"vault.azure.net"` | Vault domain suffix | + +```typescript +const signer = await AzureKeyVaultSigner.create({ + vaultName: "my-vault", + keyName: "my-key", + keyVersion: "abc123", + certificate: certificateDer, +}); +``` + +**Returns**: `Promise` + +**Throws**: `AzureKeyVaultSignerError` if: + +- Key is not found or not accessible +- Key is not enabled +- Key type is unsupported (only RSA and EC keys are supported) +- Key doesn't have the "sign" operation +- Certificate public key doesn't match the vault key + +--- + +## Signer Properties + +After creation, inspect the signer's detected configuration: + +```typescript +const signer = await AzureKeyVaultSigner.create({ + vaultName: "my-vault", + keyName: "my-key", + certificate: certificateDer, +}); + +signer.keyType; // "RSA" or "EC" +signer.signatureAlgorithm; // "RSASSA-PKCS1-v1_5", "RSA-PSS", or "ECDSA" +signer.rsaScheme; // "PKCS1" or "PSS" +signer.keyId; // Full key URL (includes version resolved by Azure) +signer.certificate; // DER-encoded certificate +signer.certificateChain; // Chain certificates (if provided/built) +``` + +--- + +## Supported Algorithms + +Azure Key Vault supports RSA and EC keys. Unlike Google Cloud KMS where the algorithm is locked at key creation, Azure RSA keys can be used with multiple algorithms. The signer resolves the algorithm at sign-time based on the key type and RSA scheme preference. + +### RSA Keys + +| RSA Scheme | Digest | Azure Algorithm | Signature Algorithm | +| ---------- | -------- | --------------- | ------------------- | +| PKCS1 | SHA-256 | RS256 | RSASSA-PKCS1-v1_5 | +| PKCS1 | SHA-384 | RS384 | RSASSA-PKCS1-v1_5 | +| PKCS1 | SHA-512 | RS512 | RSASSA-PKCS1-v1_5 | +| PSS | SHA-256 | PS256 | RSA-PSS | +| PSS | SHA-384 | PS384 | RSA-PSS | +| PSS | SHA-512 | PS512 | RSA-PSS | + +### EC Keys + +| Curve | Digest | Azure Algorithm | Signature Algorithm | +| ----- | -------- | --------------- | ------------------- | +| P-256 | SHA-256 | ES256 | ECDSA | +| P-384 | SHA-384 | ES384 | ECDSA | +| P-521 | SHA-512 | ES512 | ECDSA | + + + **RSA-PSS compatibility**: RSA-PSS signatures may not verify correctly in older PDF readers (Adobe + Acrobat before 2020). Use `rsaScheme: "PKCS1"` (the default) for maximum compatibility. + + +**Unsupported**: Symmetric keys (`oct`, `oct-HSM`) cannot be used for PDF signing. + +--- + +## Managed HSM + +Azure Managed HSM uses the same API as standard Key Vault, just with a different domain. Use the `vaultSuffix` option: + +```typescript +const signer = await AzureKeyVaultSigner.create({ + vaultName: "my-hsm", + keyName: "my-key", + vaultSuffix: "managedhsm.azure.net", + certificate: certificateDer, +}); +``` + +Or provide the full URL directly: + +```typescript +const signer = await AzureKeyVaultSigner.create({ + keyId: "https://my-hsm.managedhsm.azure.net/keys/my-key/abc123", + certificate: certificateDer, +}); +``` + +--- + +## Certificate from Key Vault + +If your certificate is stored in Azure Key Vault (alongside the key), load it directly: + +```typescript +const { cert } = await AzureKeyVaultSigner.getCertificateFromKeyVault({ + vaultUrl: "https://my-vault.vault.azure.net", + certificateName: "my-signing-cert", +}); + +const signer = await AzureKeyVaultSigner.create({ + vaultName: "my-vault", + keyName: "my-signing-cert", // Same name as the certificate + certificate: cert, + buildChain: true, +}); +``` + +In Azure Key Vault, a "certificate" is a bundle containing both the certificate and the private key, sharing the same name. This means you can use the same name for both the certificate retrieval and the key reference. + +### getCertificateFromKeyVault(options) + +| Param | Type | Default | Description | +| ------------------------------- | ------------------ | -------- | -------------------------------------------------- | +| `options` | `object` | required | | +| `options.vaultUrl` | `string` | required | Full vault URL | +| `options.certificateName` | `string` | required | Certificate name in the vault | +| `[options.certificateVersion]` | `string` | | Certificate version (default: latest) | +| `[options.credential]` | `TokenCredential` | | Azure credential (default: DefaultAzureCredential) | +| `[options.certificateClient]` | `CertificateClient` | | Pre-configured Certificate client | + +**Returns**: `Promise<{ cert: Uint8Array; chain?: Uint8Array[] }>` + +--- + +## Certificate Chain + +For trusted signatures, include the full certificate chain (intermediates and root). + +### Automatic Chain Building (AIA) + +If your certificate has Authority Information Access (AIA) extensions (most CA-issued certificates do), the chain can be built automatically: + +```typescript +const signer = await AzureKeyVaultSigner.create({ + vaultName: "my-vault", + keyName: "my-key", + certificate: certificateDer, + buildChain: true, // Fetch intermediates via AIA + chainTimeout: 20000, // Optional: extend timeout (default 15s) +}); + +console.log(`Chain has ${signer.certificateChain.length} certificates`); +``` + +### Manual Chain + +Provide the chain explicitly if AIA isn't available or you want to avoid network calls: + +```typescript +const signer = await AzureKeyVaultSigner.create({ + vaultName: "my-vault", + keyName: "my-key", + certificate: certificateDer, + certificateChain: [intermediateDer, rootDer], +}); +``` + +--- + +## Complete Example + +```typescript +import { readFile, writeFile } from "fs/promises"; +import { PDF, AzureKeyVaultSigner, HttpTimestampAuthority } from "@libpdf/core"; + +async function signWithAzure() { + // Load certificate from Key Vault + const { cert } = await AzureKeyVaultSigner.getCertificateFromKeyVault({ + vaultUrl: "https://my-vault.vault.azure.net", + certificateName: "document-signing-cert", + }); + + // Create signer with automatic chain building + const signer = await AzureKeyVaultSigner.create({ + vaultName: "my-vault", + keyName: "document-signing-cert", + certificate: cert, + buildChain: true, + }); + + // Load document + const pdf = await PDF.load(await readFile("contract.pdf")); + + // Create timestamp authority for long-term validation + const tsa = new HttpTimestampAuthority("http://timestamp.digicert.com"); + + // Sign with timestamp + const { bytes } = await pdf.sign({ + signer, + level: "B-LT", + timestampAuthority: tsa, + reason: "Contract approval", + location: "Cloud signing service", + }); + + await writeFile("contract-signed.pdf", bytes); + console.log("Document signed with Azure Key Vault"); +} + +signWithAzure().catch(console.error); +``` + +--- + +## Performance Considerations + +Each `sign()` call makes a network request to Azure Key Vault, typically adding 50-200ms latency. For bulk signing operations, consider: + +- **Batch processing**: Sign documents in parallel where possible +- **Regional vaults**: Use a vault in a region close to your application +- **Connection reuse**: Reuse the same `AzureKeyVaultSigner` instance for multiple documents + +--- + +## Error Handling + +`AzureKeyVaultSigner` throws `AzureKeyVaultSignerError` for Azure-specific issues: + +```typescript +import { AzureKeyVaultSigner, AzureKeyVaultSignerError } from "@libpdf/core"; + +try { + const signer = await AzureKeyVaultSigner.create({ + vaultName: "my-vault", + keyName: "my-key", + certificate: certificateDer, + }); +} catch (error) { + if (error instanceof AzureKeyVaultSignerError) { + console.error("Azure Key Vault error:", error.message); + // error.cause contains the original Azure SDK error if available + } +} +``` + +Common errors: + +| Error | Cause | Solution | +| -------------------- | ---------------------------------------- | -------------------------------------------- | +| Key not found | Invalid key name, version, or vault URL | Verify the key reference and vault URL | +| Permission denied | Missing Key Vault permissions | Grant Key Sign and Key Get permissions | +| Key is not enabled | Key disabled in the vault | Enable the key in Azure Portal | +| Certificate mismatch | Certificate wasn't issued for this key | Regenerate certificate from the vault's key | +| Unsupported key type | Symmetric key or unsupported curve | Use RSA or EC (P-256, P-384, P-521) keys | + +--- + +## Key Vault Setup + +### Create a Key Vault and Key + +```bash +# Create a resource group (if needed) +az group create --name my-rg --location eastus + +# Create a key vault +az keyvault create --name my-vault --resource-group my-rg --location eastus + +# Create an RSA signing key (HSM-backed) +az keyvault key create \ + --vault-name my-vault \ + --name my-signing-key \ + --kty RSA-HSM \ + --size 2048 \ + --ops sign verify + +# Or create an EC signing key +az keyvault key create \ + --vault-name my-vault \ + --name my-signing-key-ec \ + --kty EC-HSM \ + --curve P-256 \ + --ops sign verify +``` + +### Grant Permissions + +```bash +# Using Azure RBAC (recommended) +az role assignment create \ + --role "Key Vault Crypto User" \ + --assignee \ + --scope /subscriptions//resourceGroups/my-rg/providers/Microsoft.KeyVault/vaults/my-vault +``` + +### Import or Create a Certificate + +```bash +# Create a self-signed certificate (for testing) +az keyvault certificate create \ + --vault-name my-vault \ + --name my-signing-cert \ + --policy @cert-policy.json + +# Or import a CA-issued certificate +az keyvault certificate import \ + --vault-name my-vault \ + --name my-signing-cert \ + --file certificate.pfx \ + --password pfx-password +``` + +--- + +## See Also + +- [Digital Signatures](/docs/guides/signatures) — Overview of PDF signing +- [Google Cloud KMS](/docs/guides/signatures/google-kms) — Sign with Google Cloud KMS +- [Encryption](/docs/guides/encryption) — Password-protect signed documents diff --git a/apps/docs/content/docs/guides/signatures/index.mdx b/apps/docs/content/docs/guides/signatures/index.mdx index 7b2087a..27ab7a1 100644 --- a/apps/docs/content/docs/guides/signatures/index.mdx +++ b/apps/docs/content/docs/guides/signatures/index.mdx @@ -106,9 +106,11 @@ await pdf.sign({ Adds a document timestamp that covers the embedded validation data, enabling indefinite verification. -## Sign with Cloud KMS +## Sign with Cloud KMS / HSM -For enterprise environments requiring HSM-backed keys, use `GoogleKmsSigner`: +For enterprise environments requiring HSM-backed keys, use the cloud KMS signer for your provider. + +### Google Cloud KMS ```ts import { PDF, GoogleKmsSigner } from "@libpdf/core"; @@ -125,6 +127,23 @@ const signed = await pdf.sign({ signer }); See the [Google Cloud KMS guide](/docs/guides/signatures/google-kms) for complete setup instructions. +### Azure Key Vault + +```ts +import { PDF, AzureKeyVaultSigner } from "@libpdf/core"; + +const signer = await AzureKeyVaultSigner.create({ + vaultName: "my-vault", + keyName: "my-signing-key", + certificate: certificateDer, // DER-encoded certificate for this key + buildChain: true, // Auto-fetch intermediate certificates +}); + +const signed = await pdf.sign({ signer }); +``` + +See the [Azure Key Vault guide](/docs/guides/signatures/azure-key-vault) for complete setup instructions. + ## Sign with Web Crypto API For browser environments: diff --git a/apps/docs/content/docs/guides/signatures/meta.json b/apps/docs/content/docs/guides/signatures/meta.json index ae7c21e..efe60ad 100644 --- a/apps/docs/content/docs/guides/signatures/meta.json +++ b/apps/docs/content/docs/guides/signatures/meta.json @@ -1,4 +1,4 @@ { "title": "Digital Signatures", - "pages": ["index", "google-kms"] + "pages": ["index", "google-kms", "azure-key-vault"] } diff --git a/bun.lock b/bun.lock index 2d34aa2..51e09f2 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,9 @@ "pkijs": "^3.3.3", }, "devDependencies": { + "@azure/identity": "^4.13.0", + "@azure/keyvault-certificates": "^4.10.0", + "@azure/keyvault-keys": "^4.10.0", "@google-cloud/kms": "^5.0.0", "@google-cloud/secret-manager": "^6.0.0", "@types/bun": "^1.3.5", @@ -28,16 +31,58 @@ "vitest": "^4.0.16", }, "peerDependencies": { + "@azure/identity": "^4.0.0", + "@azure/keyvault-certificates": "^4.0.0", + "@azure/keyvault-keys": "^4.0.0", "@google-cloud/kms": "^5.0.0", "@google-cloud/secret-manager": "^6.0.0", }, "optionalPeers": [ + "@azure/identity", + "@azure/keyvault-certificates", + "@azure/keyvault-keys", "@google-cloud/kms", "@google-cloud/secret-manager", ], }, }, "packages": { + "@azure-rest/core-client": ["@azure-rest/core-client@2.5.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A=="], + + "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], + + "@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="], + + "@azure/core-client": ["@azure/core-client@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w=="], + + "@azure/core-http-compat": ["@azure/core-http-compat@2.3.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2" }, "peerDependencies": { "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw=="], + + "@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="], + + "@azure/core-paging": ["@azure/core-paging@1.6.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA=="], + + "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.22.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg=="], + + "@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="], + + "@azure/core-util": ["@azure/core-util@1.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A=="], + + "@azure/identity": ["@azure/identity@4.13.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^4.2.0", "@azure/msal-node": "^3.5.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw=="], + + "@azure/keyvault-certificates": ["@azure/keyvault-certificates@4.10.0", "", { "dependencies": { "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.7.2", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.0", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/keyvault-common": "^2.0.0", "@azure/logger": "^1.1.4", "tslib": "^2.8.1" } }, "sha512-45fqZ5nLYu2jBNjGLy7718kUb11wEMG1Kt32ycFFqBcfw6s+du1wl2CEuvBJyz/J4T0b7o7NibRyGKtY5MkuNw=="], + + "@azure/keyvault-common": ["@azure/keyvault-common@2.0.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-client": "^1.5.0", "@azure/core-rest-pipeline": "^1.8.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.10.0", "@azure/logger": "^1.1.4", "tslib": "^2.2.0" } }, "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w=="], + + "@azure/keyvault-keys": ["@azure/keyvault-keys@4.10.0", "", { "dependencies": { "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.7.2", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.0", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/keyvault-common": "^2.0.0", "@azure/logger": "^1.1.4", "tslib": "^2.8.1" } }, "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag=="], + + "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], + + "@azure/msal-browser": ["@azure/msal-browser@4.28.1", "", { "dependencies": { "@azure/msal-common": "15.14.1" } }, "sha512-al2u2fTchbClq3L4C1NlqLm+vwKfhYCPtZN2LR/9xJVaQ4Mnrwf5vANvuyPSJHcGvw50UBmhuVmYUAhTEetTpA=="], + + "@azure/msal-common": ["@azure/msal-common@15.14.1", "", {}, "sha512-IkzF7Pywt6QKTS0kwdCv/XV8x8JXknZDvSjj/IccooxnP373T5jaadO3FnOrbWo3S0UqkfIDyZNTaQ/oAgRdXw=="], + + "@azure/msal-node": ["@azure/msal-node@3.8.6", "", { "dependencies": { "@azure/msal-common": "15.14.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-XTmhdItcBckcVVTy65Xp+42xG4LX5GK+9AqAsXPXk4IqUNv+LyQo5TMwNjuFYBfAB2GTG9iSQGk+QLc03vhf3w=="], + "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], @@ -300,6 +345,8 @@ "@types/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="], + "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA=="], + "@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.16", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.16", "ast-v8-to-istanbul": "^0.3.8", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.16", "vitest": "4.0.16" }, "optionalPeers": ["@vitest/browser"] }, "sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A=="], "@vitest/expect": ["@vitest/expect@4.0.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA=="], @@ -350,6 +397,8 @@ "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + "bytestreamjs": ["bytestreamjs@2.0.1", "", {}, "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], @@ -376,6 +425,12 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], "dts-resolver": ["dts-resolver@2.1.3", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw=="], @@ -446,7 +501,7 @@ "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], - "http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], @@ -456,10 +511,16 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], @@ -478,6 +539,8 @@ "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], @@ -488,6 +551,20 @@ "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], + + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -526,6 +603,8 @@ "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "oxfmt": ["oxfmt@0.24.0", "", { "dependencies": { "tinypool": "2.0.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.24.0", "@oxfmt/darwin-x64": "0.24.0", "@oxfmt/linux-arm64-gnu": "0.24.0", "@oxfmt/linux-arm64-musl": "0.24.0", "@oxfmt/linux-x64-gnu": "0.24.0", "@oxfmt/linux-x64-musl": "0.24.0", "@oxfmt/win32-arm64": "0.24.0", "@oxfmt/win32-x64": "0.24.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-UjeM3Peez8Tl7IJ9s5UwAoZSiDRMww7BEc21gDYxLq3S3/KqJnM3mjNxsoSHgmBvSeX6RBhoVc2MfC/+96RdSw=="], "oxlint": ["oxlint@1.39.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.39.0", "@oxlint/darwin-x64": "1.39.0", "@oxlint/linux-arm64-gnu": "1.39.0", "@oxlint/linux-arm64-musl": "1.39.0", "@oxlint/linux-x64-gnu": "1.39.0", "@oxlint/linux-x64-musl": "1.39.0", "@oxlint/win32-arm64": "1.39.0", "@oxlint/win32-x64": "1.39.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.10.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-wSiLr0wjG+KTU6c1LpVoQk7JZ7l8HCKlAkVDVTJKWmCGazsNxexxnOXl7dsar92mQcRnzko5g077ggP3RINSjA=="], @@ -584,6 +663,8 @@ "rollup": ["rollup@4.53.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.5", "@rollup/rollup-android-arm64": "4.53.5", "@rollup/rollup-darwin-arm64": "4.53.5", "@rollup/rollup-darwin-x64": "4.53.5", "@rollup/rollup-freebsd-arm64": "4.53.5", "@rollup/rollup-freebsd-x64": "4.53.5", "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", "@rollup/rollup-linux-arm-musleabihf": "4.53.5", "@rollup/rollup-linux-arm64-gnu": "4.53.5", "@rollup/rollup-linux-arm64-musl": "4.53.5", "@rollup/rollup-linux-loong64-gnu": "4.53.5", "@rollup/rollup-linux-ppc64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-musl": "4.53.5", "@rollup/rollup-linux-s390x-gnu": "4.53.5", "@rollup/rollup-linux-x64-gnu": "4.53.5", "@rollup/rollup-linux-x64-musl": "4.53.5", "@rollup/rollup-openharmony-arm64": "4.53.5", "@rollup/rollup-win32-arm64-msvc": "4.53.5", "@rollup/rollup-win32-ia32-msvc": "4.53.5", "@rollup/rollup-win32-x64-gnu": "4.53.5", "@rollup/rollup-win32-x64-msvc": "4.53.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ=="], + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -642,7 +723,7 @@ "tsdown": ["tsdown@0.18.4", "", { "dependencies": { "ansis": "^4.2.0", "cac": "^6.7.14", "defu": "^6.1.4", "empathic": "^2.0.0", "hookable": "^6.0.1", "import-without-cache": "^0.2.5", "obug": "^2.1.1", "picomatch": "^4.0.3", "rolldown": "1.0.0-beta.57", "rolldown-plugin-dts": "^0.20.0", "semver": "^7.7.3", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tree-kill": "^1.2.2", "unconfig-core": "^7.4.2", "unrun": "^0.2.21" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@vitejs/devtools": "*", "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["@arethetypeswrong/core", "@vitejs/devtools", "publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.mjs" } }, "sha512-J/tRS6hsZTkvqmt4+xdELUCkQYDuUCXgBv0fw3ImV09WPGbEKfsPD65E+WUjSu3E7Z6tji9XZ1iWs8rbGqB/ZA=="], - "tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -654,6 +735,8 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], "vitest": ["vitest@4.0.16", "", { "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", "@vitest/pretty-format": "4.0.16", "@vitest/runner": "4.0.16", "@vitest/snapshot": "4.0.16", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.16", "@vitest/browser-preview": "4.0.16", "@vitest/browser-webdriverio": "4.0.16", "@vitest/ui": "4.0.16", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q=="], @@ -670,6 +753,8 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], @@ -678,12 +763,6 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "@emnapi/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], @@ -692,27 +771,19 @@ "@pdf-lib/upng/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], - "@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "asn1js/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "pdf-lib/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], - "pkijs/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], - - "pkijs/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "pdf-lib/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - "pvtsutils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "pkijs/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -722,6 +793,8 @@ "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "teeny-request/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], + "teeny-request/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "unrun/rolldown": ["rolldown@1.0.0-beta.59", "", { "dependencies": { "@oxc-project/types": "=0.107.0", "@rolldown/pluginutils": "1.0.0-beta.59" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.59", "@rolldown/binding-darwin-arm64": "1.0.0-beta.59", "@rolldown/binding-darwin-x64": "1.0.0-beta.59", "@rolldown/binding-freebsd-x64": "1.0.0-beta.59", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.59", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.59", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.59", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.59", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.59", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.59", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.59", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.59", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.59" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Slm000Gd8/AO9z4Kxl4r8mp/iakrbAuJ1L+7ddpkNxgQ+Vf37WPvY63l3oeyZcfuPD1DRrUYBsRPIXSOhvOsmw=="], @@ -748,6 +821,8 @@ "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "teeny-request/http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "teeny-request/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "unrun/rolldown/@oxc-project/types": ["@oxc-project/types@0.107.0", "", {}, "sha512-QFDRbYfV2LVx8tyqtyiah3jQPUj1mK2+RYwxyFWyGoys6XJnwTdlzO6rdNNHOPorHAu5Uo34oWRKcvNpbJarmQ=="], diff --git a/package.json b/package.json index bcd023d..ba47701 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,9 @@ "pkijs": "^3.3.3" }, "devDependencies": { + "@azure/identity": "^4.13.0", + "@azure/keyvault-certificates": "^4.10.0", + "@azure/keyvault-keys": "^4.10.0", "@google-cloud/kms": "^5.0.0", "@google-cloud/secret-manager": "^6.0.0", "@types/bun": "^1.3.5", @@ -86,10 +89,22 @@ "vitest": "^4.0.16" }, "peerDependencies": { + "@azure/identity": "^4.0.0", + "@azure/keyvault-certificates": "^4.0.0", + "@azure/keyvault-keys": "^4.0.0", "@google-cloud/kms": "^5.0.0", "@google-cloud/secret-manager": "^6.0.0" }, "peerDependenciesMeta": { + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-certificates": { + "optional": true + }, + "@azure/keyvault-keys": { + "optional": true + }, "@google-cloud/kms": { "optional": true }, diff --git a/src/index.ts b/src/index.ts index 497d5ca..28b27e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -118,6 +118,8 @@ export type { TimestampAuthority, } from "./signatures"; export { + AzureKeyVaultSigner, + AzureKeyVaultSignerError, CertificateChainError, CryptoKeySigner, GoogleKmsSigner, diff --git a/src/signatures/index.ts b/src/signatures/index.ts index e694e64..30f9406 100644 --- a/src/signatures/index.ts +++ b/src/signatures/index.ts @@ -37,7 +37,13 @@ export { extractOcspResponderCerts, } from "./revocation"; // Signers -export { CryptoKeySigner, GoogleKmsSigner, P12Signer, type P12SignerOptions } from "./signers"; +export { + AzureKeyVaultSigner, + CryptoKeySigner, + GoogleKmsSigner, + P12Signer, + type P12SignerOptions, +} from "./signers"; // Timestamp export { HttpTimestampAuthority, type HttpTimestampAuthorityOptions } from "./timestamp"; // Types @@ -57,6 +63,7 @@ export type { } from "./types"; // Errors export { + AzureKeyVaultSignerError, CertificateChainError, KmsSignerError, PlaceholderError, diff --git a/src/signatures/signers/azure-key-vault.test.ts b/src/signatures/signers/azure-key-vault.test.ts new file mode 100644 index 0000000..28f2161 --- /dev/null +++ b/src/signatures/signers/azure-key-vault.test.ts @@ -0,0 +1,600 @@ +/** + * Tests for AzureKeyVaultSigner. + * + * Unit tests: Pure logic (algorithm mapping, key URL building, key type detection, error handling) + * Integration tests: Real Azure Key Vault (skipped without Azure credentials) + */ + +import { beforeAll, describe, expect, it } from "vitest"; + +import { AzureKeyVaultSignerError, KmsSignerError, SignerError } from "../types"; +import { + buildKeyId, + buildVaultUrl, + detectKeyType, + ecCurveToDigestAlgorithm, + mapAzureAlgorithm, + parseVaultUrl, + resolveAzureAlgorithm, +} from "./azure-key-vault"; + +// ───────────────────────────────────────────────────────────────────────────── +// Unit Tests: Algorithm Mapping +// ───────────────────────────────────────────────────────────────────────────── + +describe("mapAzureAlgorithm", () => { + describe("RSA PKCS#1 v1.5 algorithms", () => { + it("maps RS256", () => { + const result = mapAzureAlgorithm("RS256"); + + expect(result).toEqual({ + keyType: "RSA", + signatureAlgorithm: "RSASSA-PKCS1-v1_5", + digestAlgorithm: "SHA-256", + azureAlgorithm: "RS256", + }); + }); + + it("maps RS384", () => { + const result = mapAzureAlgorithm("RS384"); + + expect(result).toEqual({ + keyType: "RSA", + signatureAlgorithm: "RSASSA-PKCS1-v1_5", + digestAlgorithm: "SHA-384", + azureAlgorithm: "RS384", + }); + }); + + it("maps RS512", () => { + const result = mapAzureAlgorithm("RS512"); + + expect(result).toEqual({ + keyType: "RSA", + signatureAlgorithm: "RSASSA-PKCS1-v1_5", + digestAlgorithm: "SHA-512", + azureAlgorithm: "RS512", + }); + }); + }); + + describe("RSA-PSS algorithms", () => { + it("maps PS256", () => { + const result = mapAzureAlgorithm("PS256"); + + expect(result).toEqual({ + keyType: "RSA", + signatureAlgorithm: "RSA-PSS", + digestAlgorithm: "SHA-256", + azureAlgorithm: "PS256", + }); + }); + + it("maps PS384", () => { + const result = mapAzureAlgorithm("PS384"); + + expect(result).toEqual({ + keyType: "RSA", + signatureAlgorithm: "RSA-PSS", + digestAlgorithm: "SHA-384", + azureAlgorithm: "PS384", + }); + }); + + it("maps PS512", () => { + const result = mapAzureAlgorithm("PS512"); + + expect(result).toEqual({ + keyType: "RSA", + signatureAlgorithm: "RSA-PSS", + digestAlgorithm: "SHA-512", + azureAlgorithm: "PS512", + }); + }); + }); + + describe("ECDSA algorithms", () => { + it("maps ES256", () => { + const result = mapAzureAlgorithm("ES256"); + + expect(result).toEqual({ + keyType: "EC", + signatureAlgorithm: "ECDSA", + digestAlgorithm: "SHA-256", + azureAlgorithm: "ES256", + }); + }); + + it("maps ES384", () => { + const result = mapAzureAlgorithm("ES384"); + + expect(result).toEqual({ + keyType: "EC", + signatureAlgorithm: "ECDSA", + digestAlgorithm: "SHA-384", + azureAlgorithm: "ES384", + }); + }); + + it("maps ES512", () => { + const result = mapAzureAlgorithm("ES512"); + + expect(result).toEqual({ + keyType: "EC", + signatureAlgorithm: "ECDSA", + digestAlgorithm: "SHA-512", + azureAlgorithm: "ES512", + }); + }); + }); + + describe("unsupported algorithms", () => { + it("throws for unknown algorithm", () => { + expect(() => mapAzureAlgorithm("UNKNOWN")).toThrow(AzureKeyVaultSignerError); + expect(() => mapAzureAlgorithm("UNKNOWN")).toThrow(/Unsupported Azure Key Vault algorithm/); + }); + + it("includes algorithm name in error message", () => { + expect(() => mapAzureAlgorithm("HS256")).toThrow("HS256"); + }); + + it("throws for HMAC algorithms", () => { + expect(() => mapAzureAlgorithm("HS256")).toThrow(AzureKeyVaultSignerError); + }); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// Unit Tests: Algorithm Resolution +// ───────────────────────────────────────────────────────────────────────────── + +describe("resolveAzureAlgorithm", () => { + describe("RSA with PKCS1 scheme", () => { + it("resolves SHA-256 to RS256", () => { + expect(resolveAzureAlgorithm("RSA", "SHA-256", "PKCS1")).toBe("RS256"); + }); + + it("resolves SHA-384 to RS384", () => { + expect(resolveAzureAlgorithm("RSA", "SHA-384", "PKCS1")).toBe("RS384"); + }); + + it("resolves SHA-512 to RS512", () => { + expect(resolveAzureAlgorithm("RSA", "SHA-512", "PKCS1")).toBe("RS512"); + }); + }); + + describe("RSA with PSS scheme", () => { + it("resolves SHA-256 to PS256", () => { + expect(resolveAzureAlgorithm("RSA", "SHA-256", "PSS")).toBe("PS256"); + }); + + it("resolves SHA-384 to PS384", () => { + expect(resolveAzureAlgorithm("RSA", "SHA-384", "PSS")).toBe("PS384"); + }); + + it("resolves SHA-512 to PS512", () => { + expect(resolveAzureAlgorithm("RSA", "SHA-512", "PSS")).toBe("PS512"); + }); + }); + + describe("EC (ignores RSA scheme)", () => { + it("resolves SHA-256 to ES256", () => { + expect(resolveAzureAlgorithm("EC", "SHA-256", "PKCS1")).toBe("ES256"); + }); + + it("resolves SHA-384 to ES384", () => { + expect(resolveAzureAlgorithm("EC", "SHA-384", "PKCS1")).toBe("ES384"); + }); + + it("resolves SHA-512 to ES512", () => { + expect(resolveAzureAlgorithm("EC", "SHA-512", "PKCS1")).toBe("ES512"); + }); + + it("ignores PSS scheme for EC keys", () => { + expect(resolveAzureAlgorithm("EC", "SHA-256", "PSS")).toBe("ES256"); + }); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// Unit Tests: Key URL Building +// ───────────────────────────────────────────────────────────────────────────── + +describe("buildKeyId", () => { + it("builds URL from vault name, key name, and version", () => { + const result = buildKeyId({ + vaultName: "my-vault", + keyName: "my-key", + keyVersion: "abc123", + certificate: new Uint8Array(), + credential: {} as any, + }); + + expect(result).toBe("https://my-vault.vault.azure.net/keys/my-key/abc123"); + }); + + it("omits version when not specified (latest)", () => { + const result = buildKeyId({ + vaultName: "my-vault", + keyName: "my-key", + certificate: new Uint8Array(), + credential: {} as any, + }); + + expect(result).toBe("https://my-vault.vault.azure.net/keys/my-key"); + }); + + it("uses custom vault suffix for Managed HSM", () => { + const result = buildKeyId({ + vaultName: "my-hsm", + keyName: "my-key", + keyVersion: "v1", + vaultSuffix: "managedhsm.azure.net", + certificate: new Uint8Array(), + credential: {} as any, + }); + + expect(result).toBe("https://my-hsm.managedhsm.azure.net/keys/my-key/v1"); + }); + + it("handles vault names with special characters", () => { + const result = buildKeyId({ + vaultName: "my-vault-123", + keyName: "signing-key", + certificate: new Uint8Array(), + credential: {} as any, + }); + + expect(result).toBe("https://my-vault-123.vault.azure.net/keys/signing-key"); + }); +}); + +describe("buildVaultUrl", () => { + it("builds vault URL from vault name", () => { + const result = buildVaultUrl({ + vaultName: "my-vault", + keyName: "my-key", + certificate: new Uint8Array(), + credential: {} as any, + }); + + expect(result).toBe("https://my-vault.vault.azure.net"); + }); + + it("uses custom suffix", () => { + const result = buildVaultUrl({ + vaultName: "my-hsm", + keyName: "my-key", + vaultSuffix: "managedhsm.azure.net", + certificate: new Uint8Array(), + credential: {} as any, + }); + + expect(result).toBe("https://my-hsm.managedhsm.azure.net"); + }); +}); + +describe("parseVaultUrl", () => { + it("extracts vault URL from key ID", () => { + const result = parseVaultUrl("https://my-vault.vault.azure.net/keys/my-key/abc123"); + + expect(result).toBe("https://my-vault.vault.azure.net"); + }); + + it("handles Managed HSM URLs", () => { + const result = parseVaultUrl("https://my-hsm.managedhsm.azure.net/keys/my-key/v1"); + + expect(result).toBe("https://my-hsm.managedhsm.azure.net"); + }); + + it("handles key ID without version", () => { + const result = parseVaultUrl("https://my-vault.vault.azure.net/keys/my-key"); + + expect(result).toBe("https://my-vault.vault.azure.net"); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// Unit Tests: Key Type Detection +// ───────────────────────────────────────────────────────────────────────────── + +describe("detectKeyType", () => { + it("detects RSA key type", () => { + expect(detectKeyType("RSA")).toBe("RSA"); + }); + + it("detects RSA-HSM key type as RSA", () => { + expect(detectKeyType("RSA-HSM")).toBe("RSA"); + }); + + it("detects EC key type", () => { + expect(detectKeyType("EC")).toBe("EC"); + }); + + it("detects EC-HSM key type as EC", () => { + expect(detectKeyType("EC-HSM")).toBe("EC"); + }); + + it("rejects symmetric (oct) key type", () => { + expect(() => detectKeyType("oct")).toThrow(AzureKeyVaultSignerError); + expect(() => detectKeyType("oct")).toThrow(/Unsupported key type.*oct/); + }); + + it("rejects oct-HSM key type", () => { + expect(() => detectKeyType("oct-HSM")).toThrow(AzureKeyVaultSignerError); + expect(() => detectKeyType("oct-HSM")).toThrow(/Only RSA and EC/); + }); + + it("rejects unknown key types", () => { + expect(() => detectKeyType("unknown")).toThrow(AzureKeyVaultSignerError); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// Unit Tests: EC Curve → Digest Algorithm +// ───────────────────────────────────────────────────────────────────────────── + +describe("ecCurveToDigestAlgorithm", () => { + it("maps P-256 to SHA-256", () => { + expect(ecCurveToDigestAlgorithm("P-256")).toBe("SHA-256"); + }); + + it("maps P-384 to SHA-384", () => { + expect(ecCurveToDigestAlgorithm("P-384")).toBe("SHA-384"); + }); + + it("maps P-521 to SHA-512", () => { + expect(ecCurveToDigestAlgorithm("P-521")).toBe("SHA-512"); + }); + + it("rejects unsupported curves", () => { + expect(() => ecCurveToDigestAlgorithm("secp256k1")).toThrow(AzureKeyVaultSignerError); + expect(() => ecCurveToDigestAlgorithm("secp256k1")).toThrow( + /Unsupported EC curve.*P-256.*P-384.*P-521/, + ); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// Unit Tests: AzureKeyVaultSignerError +// ───────────────────────────────────────────────────────────────────────────── + +describe("AzureKeyVaultSignerError", () => { + it("prefixes message with KMS: Azure Key Vault:", () => { + const error = new AzureKeyVaultSignerError("Something went wrong"); + + expect(error.message).toBe("KMS: Azure Key Vault: Something went wrong"); + }); + + it("has correct name", () => { + const error = new AzureKeyVaultSignerError("test"); + + expect(error.name).toBe("AzureKeyVaultSignerError"); + }); + + it("stores cause when provided", () => { + const cause = new Error("Original error"); + const error = new AzureKeyVaultSignerError("Wrapped error", cause); + + expect(error.cause).toBe(cause); + }); + + it("is an instance of Error", () => { + const error = new AzureKeyVaultSignerError("test"); + + expect(error).toBeInstanceOf(Error); + }); + + it("is an instance of KmsSignerError", () => { + const error = new AzureKeyVaultSignerError("test"); + + expect(error).toBeInstanceOf(KmsSignerError); + }); + + it("is an instance of SignerError", () => { + const error = new AzureKeyVaultSignerError("test"); + + expect(error).toBeInstanceOf(SignerError); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// Integration Tests (Skipped without Azure credentials) +// ───────────────────────────────────────────────────────────────────────────── + +// Integration tests require: +// - Azure credentials (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET or az login) +// - TEST_AZURE_KEY_VAULT_URL env var with vault URL (e.g., https://my-vault.vault.azure.net) +// - TEST_AZURE_RSA_KEY_NAME env var with name of test RSA key +// - TEST_AZURE_EC_KEY_NAME env var with name of test EC key +// - TEST_AZURE_RSA_CERT_PATH env var with path to DER certificate for RSA key +// - TEST_AZURE_EC_CERT_PATH env var with path to DER certificate for EC key + +import { PDF } from "#src/api/pdf.ts"; +import { loadFixture, saveTestOutput } from "#src/test-utils.ts"; +import * as fs from "fs"; + +import { AzureKeyVaultSigner } from "./azure-key-vault"; + +const vaultUrl = process.env.TEST_AZURE_KEY_VAULT_URL; +const rsaKeyName = process.env.TEST_AZURE_RSA_KEY_NAME; +const ecKeyName = process.env.TEST_AZURE_EC_KEY_NAME; +const rsaCertPath = process.env.TEST_AZURE_RSA_CERT_PATH; +const ecCertPath = process.env.TEST_AZURE_EC_CERT_PATH; +const certName = process.env.TEST_AZURE_CERT_NAME; + +// Check for Azure credentials +const hasAzureCredentials = + (!!process.env.AZURE_TENANT_ID && !!process.env.AZURE_CLIENT_ID) || + !!process.env.AZURE_CLI_TENANT_ID; + +const canRunIntegrationTests = hasAzureCredentials && vaultUrl && (rsaKeyName || ecKeyName); + +describe.skipIf(!canRunIntegrationTests)("AzureKeyVaultSigner integration", () => { + let rsaCertificate: Uint8Array; + let ecCertificate: Uint8Array; + + beforeAll(async () => { + if (rsaCertPath) { + const fsPromises = await import("fs/promises"); + rsaCertificate = new Uint8Array(await fsPromises.readFile(rsaCertPath)); + } + + if (ecCertPath) { + const fsPromises = await import("fs/promises"); + ecCertificate = new Uint8Array(await fsPromises.readFile(ecCertPath)); + } + }); + + describe.skipIf(!rsaKeyName || !rsaCertPath)("RSA signing", () => { + it("creates signer with RSA key", async () => { + const { DefaultAzureCredential } = await import("@azure/identity"); + + const signer = await AzureKeyVaultSigner.create({ + vaultName: new URL(vaultUrl!).hostname.split(".")[0], + keyName: rsaKeyName!, + certificate: rsaCertificate, + credential: new DefaultAzureCredential(), + }); + + expect(signer.keyType).toBe("RSA"); + expect(signer.signatureAlgorithm).toBe("RSASSA-PKCS1-v1_5"); + }); + + it("signs data with RSA key", async () => { + const { DefaultAzureCredential } = await import("@azure/identity"); + + const signer = await AzureKeyVaultSigner.create({ + vaultName: new URL(vaultUrl!).hostname.split(".")[0], + keyName: rsaKeyName!, + certificate: rsaCertificate, + credential: new DefaultAzureCredential(), + }); + + const testData = new TextEncoder().encode("Hello, World!"); + const signature = await signer.sign(testData, "SHA-256"); + + expect(signature).toBeInstanceOf(Uint8Array); + expect(signature.length).toBeGreaterThan(0); + }); + + it("signs a PDF document with Azure Key Vault RSA key", async () => { + const { DefaultAzureCredential } = await import("@azure/identity"); + + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + const signer = await AzureKeyVaultSigner.create({ + vaultName: new URL(vaultUrl!).hostname.split(".")[0], + keyName: rsaKeyName!, + certificate: rsaCertificate, + credential: new DefaultAzureCredential(), + }); + + const { bytes, warnings } = await pdf.sign({ + signer, + reason: "Signed with Azure Key Vault (RSA key)", + location: "Integration Test", + }); + + expect(bytes.length).toBeGreaterThan(pdfBytes.length); + expect(new TextDecoder().decode(bytes.slice(0, 5))).toBe("%PDF-"); + expect(warnings).toHaveLength(0); + + const pdfStr = new TextDecoder().decode(bytes); + + expect(pdfStr).toContain("/Type /Sig"); + expect(pdfStr).toContain("/Filter /Adobe.PPKLite"); + + await saveTestOutput("signatures/azure-kv-signed-rsa.pdf", bytes); + }); + }); + + describe.skipIf(!ecKeyName || !ecCertPath)("ECDSA signing", () => { + it("creates signer with ECDSA key", async () => { + const { DefaultAzureCredential } = await import("@azure/identity"); + + const signer = await AzureKeyVaultSigner.create({ + vaultName: new URL(vaultUrl!).hostname.split(".")[0], + keyName: ecKeyName!, + certificate: ecCertificate, + credential: new DefaultAzureCredential(), + }); + + expect(signer.keyType).toBe("EC"); + expect(signer.signatureAlgorithm).toBe("ECDSA"); + }); + + it("signs data with ECDSA key", async () => { + const { DefaultAzureCredential } = await import("@azure/identity"); + + const signer = await AzureKeyVaultSigner.create({ + vaultName: new URL(vaultUrl!).hostname.split(".")[0], + keyName: ecKeyName!, + certificate: ecCertificate, + credential: new DefaultAzureCredential(), + }); + + const testData = new TextEncoder().encode("Hello, World!"); + const signature = await signer.sign(testData, "SHA-256"); + + expect(signature).toBeInstanceOf(Uint8Array); + expect(signature.length).toBeGreaterThan(0); + }); + + it("signs a PDF document with Azure Key Vault ECDSA key", async () => { + const { DefaultAzureCredential } = await import("@azure/identity"); + + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + const signer = await AzureKeyVaultSigner.create({ + vaultName: new URL(vaultUrl!).hostname.split(".")[0], + keyName: ecKeyName!, + certificate: ecCertificate, + credential: new DefaultAzureCredential(), + }); + + const { bytes, warnings } = await pdf.sign({ + signer, + reason: "Signed with Azure Key Vault (ECDSA key)", + location: "Integration Test", + }); + + expect(bytes.length).toBeGreaterThan(pdfBytes.length); + expect(new TextDecoder().decode(bytes.slice(0, 5))).toBe("%PDF-"); + expect(warnings).toHaveLength(0); + + await saveTestOutput("signatures/azure-kv-signed-ecdsa.pdf", bytes); + }); + }); + + describe.skipIf(!certName)("getCertificateFromKeyVault", () => { + it("loads certificate from Key Vault", async () => { + const { DefaultAzureCredential } = await import("@azure/identity"); + + const { cert } = await AzureKeyVaultSigner.getCertificateFromKeyVault({ + vaultUrl: vaultUrl!, + certificateName: certName!, + credential: new DefaultAzureCredential(), + }); + + expect(cert).toBeInstanceOf(Uint8Array); + expect(cert.length).toBeGreaterThan(0); + }); + }); + + describe.skipIf(!rsaKeyName)("error handling", () => { + it("rejects non-existent key", async () => { + const { DefaultAzureCredential } = await import("@azure/identity"); + + await expect( + AzureKeyVaultSigner.create({ + keyId: `${vaultUrl}/keys/nonexistent-key-12345/v1`, + certificate: new Uint8Array([0x30, 0x82]), + credential: new DefaultAzureCredential(), + }), + ).rejects.toThrow(AzureKeyVaultSignerError); + }); + }); +}); diff --git a/src/signatures/signers/azure-key-vault.ts b/src/signatures/signers/azure-key-vault.ts new file mode 100644 index 0000000..6d037b3 --- /dev/null +++ b/src/signatures/signers/azure-key-vault.ts @@ -0,0 +1,1096 @@ +/** + * Azure Key Vault signer. + * + * Signs using keys stored in Azure Key Vault (including Managed HSM). + * The private key never leaves the vault — only the digest is sent for signing. + * + * Uses the `@azure/keyvault-keys` SDK (optional peer dependency) via the + * `CryptographyClient.sign()` method, which accepts a pre-computed digest + * and returns raw signature bytes. No PKCS#11 required. + */ + +import { toArrayBuffer } from "#src/helpers/buffer.ts"; +import { derToPem, isPem, normalizePem, parsePem } from "#src/helpers/pem.ts"; +import { sha256, sha384, sha512 } from "@noble/hashes/sha2.js"; +import { fromBER } from "asn1js"; +import * as pkijs from "pkijs"; + +import { buildCertificateChain } from "../aia"; +import { AzureKeyVaultSignerError, CertificateChainError } from "../types"; +import type { DigestAlgorithm, KeyType, SignatureAlgorithm, Signer } from "../types"; + +// ───────────────────────────────────────────────────────────────────────────── +// Azure SDK Types (dynamically imported) +// ───────────────────────────────────────────────────────────────────────────── + +/** TokenCredential from @azure/identity — user must provide this */ +type TokenCredential = import("@azure/core-auth").TokenCredential; + +/** Azure Key Vault KeyClient */ +type KeyClient = import("@azure/keyvault-keys").KeyClient; + +/** Azure Key Vault CryptographyClient */ +type CryptographyClient = import("@azure/keyvault-keys").CryptographyClient; + +/** Azure Key Vault CertificateClient */ +type CertificateClient = import("@azure/keyvault-certificates").CertificateClient; + +// ───────────────────────────────────────────────────────────────────────────── +// Types +// ───────────────────────────────────────────────────────────────────────────── + +/** RSA signature scheme preference */ +type RsaScheme = "PKCS1" | "PSS"; + +/** Base options shared by both key reference styles */ +interface AzureKeyVaultSignerBaseOptions { + /** DER-encoded X.509 certificate issued for this key */ + certificate: Uint8Array; + + /** Certificate chain [intermediate, ..., root] (optional) */ + certificateChain?: Uint8Array[]; + + /** Build certificate chain via AIA extensions (default: false) */ + buildChain?: boolean; + + /** Timeout for AIA chain building in ms (default: 15000) */ + chainTimeout?: number; + + /** + * Azure credential (e.g., DefaultAzureCredential from @azure/identity). + * + * If not provided, `DefaultAzureCredential` from `@azure/identity` is used + * automatically, which tries multiple authentication methods in order + * (environment variables, managed identity, Azure CLI, etc.). + */ + credential?: TokenCredential; + + /** + * RSA signature scheme preference (default: "PKCS1"). + * + * Only applies to RSA keys. ECDSA keys always use ECDSA. + * + * - "PKCS1" — RSASSA-PKCS1-v1.5 (maximum compatibility with PDF readers) + * - "PSS" — RSASSA-PSS (more secure, but may not verify in older PDF readers like Adobe Acrobat < 2020) + */ + rsaScheme?: RsaScheme; + + /** Pre-configured KeyClient (optional, created automatically if not provided) */ + keyClient?: KeyClient; + + /** Pre-configured CryptographyClient (optional, created automatically if not provided) */ + cryptographyClient?: CryptographyClient; +} + +/** Full key ID URL style */ +interface AzureKeyVaultSignerKeyIdOptions extends AzureKeyVaultSignerBaseOptions { + /** + * Full key identifier URL. + * + * @example "https://my-vault.vault.azure.net/keys/my-key/abc123" + * @example "https://my-hsm.managedhsm.azure.net/keys/my-key/abc123" + */ + keyId: string; +} + +/** Shorthand style with vault name + key name */ +interface AzureKeyVaultSignerShorthandOptions extends AzureKeyVaultSignerBaseOptions { + /** Vault name (the part before .vault.azure.net) */ + vaultName: string; + + /** Key name in the vault */ + keyName: string; + + /** Key version (optional, defaults to latest) */ + keyVersion?: string; + + /** + * Vault suffix (default: "vault.azure.net"). + * + * Use "managedhsm.azure.net" for Managed HSM. + */ + vaultSuffix?: string; +} + +/** Options for AzureKeyVaultSigner.create() */ +export type AzureKeyVaultSignerOptions = + | AzureKeyVaultSignerKeyIdOptions + | AzureKeyVaultSignerShorthandOptions; + +/** Options for AzureKeyVaultSigner.getCertificateFromKeyVault() */ +export interface GetCertificateFromKeyVaultOptions { + /** Full vault URL (e.g., "https://my-vault.vault.azure.net") */ + vaultUrl: string; + + /** Certificate name in the vault */ + certificateName: string; + + /** Certificate version (optional, defaults to latest) */ + certificateVersion?: string; + + /** + * Azure credential. + * + * If not provided, `DefaultAzureCredential` from `@azure/identity` is used automatically. + */ + credential?: TokenCredential; + + /** Pre-configured CertificateClient (optional) */ + certificateClient?: CertificateClient; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Algorithm Mapping +// ───────────────────────────────────────────────────────────────────────────── + +/** Mapped algorithm info */ +interface AlgorithmInfo { + keyType: KeyType; + signatureAlgorithm: SignatureAlgorithm; + digestAlgorithm: DigestAlgorithm; + /** Azure algorithm identifier for CryptographyClient.sign() */ + azureAlgorithm: string; +} + +/** + * Azure algorithm name → our types. + * + * Azure uses JWA (JSON Web Algorithms) names for signing: + * - RS256/384/512 — RSASSA-PKCS1-v1_5 + * - PS256/384/512 — RSASSA-PSS + * - ES256/384/512 — ECDSA + */ +const AZURE_ALGORITHM_MAP: Record = { + // RSA PKCS#1 v1.5 + RS256: { + keyType: "RSA", + signatureAlgorithm: "RSASSA-PKCS1-v1_5", + digestAlgorithm: "SHA-256", + azureAlgorithm: "RS256", + }, + RS384: { + keyType: "RSA", + signatureAlgorithm: "RSASSA-PKCS1-v1_5", + digestAlgorithm: "SHA-384", + azureAlgorithm: "RS384", + }, + RS512: { + keyType: "RSA", + signatureAlgorithm: "RSASSA-PKCS1-v1_5", + digestAlgorithm: "SHA-512", + azureAlgorithm: "RS512", + }, + // RSA-PSS + PS256: { + keyType: "RSA", + signatureAlgorithm: "RSA-PSS", + digestAlgorithm: "SHA-256", + azureAlgorithm: "PS256", + }, + PS384: { + keyType: "RSA", + signatureAlgorithm: "RSA-PSS", + digestAlgorithm: "SHA-384", + azureAlgorithm: "PS384", + }, + PS512: { + keyType: "RSA", + signatureAlgorithm: "RSA-PSS", + digestAlgorithm: "SHA-512", + azureAlgorithm: "PS512", + }, + // ECDSA + ES256: { + keyType: "EC", + signatureAlgorithm: "ECDSA", + digestAlgorithm: "SHA-256", + azureAlgorithm: "ES256", + }, + ES384: { + keyType: "EC", + signatureAlgorithm: "ECDSA", + digestAlgorithm: "SHA-384", + azureAlgorithm: "ES384", + }, + ES512: { + keyType: "EC", + signatureAlgorithm: "ECDSA", + digestAlgorithm: "SHA-512", + azureAlgorithm: "ES512", + }, +}; + +/** + * Map an Azure algorithm name to our internal types. + * + * @param algorithm - Azure JWA algorithm name (e.g., "RS256", "ES384") + * @returns Algorithm info + * @throws {AzureKeyVaultSignerError} if algorithm is unsupported + * + * @internal Exported for testing + */ +export function mapAzureAlgorithm(algorithm: string): AlgorithmInfo { + const info = AZURE_ALGORITHM_MAP[algorithm]; + + if (!info) { + throw new AzureKeyVaultSignerError( + `Unsupported Azure Key Vault algorithm for PDF signing: ${algorithm}`, + ); + } + + return info; +} + +/** + * Resolve the Azure algorithm name from a key type, digest algorithm, and RSA scheme. + * + * Azure keys don't lock to a single algorithm like GCP — an RSA key can be + * used with RS256, RS384, RS512, PS256, PS384, PS512. This function picks + * the right algorithm at sign-time based on the key type and the digest the + * CMS builder requests. + * + * @param keyType - "RSA" or "EC" + * @param digestAlgorithm - The digest algorithm requested by the signing flow + * @param rsaScheme - "PKCS1" or "PSS" (only for RSA keys) + * @returns The Azure JWA algorithm name + * + * @internal Exported for testing + */ +export function resolveAzureAlgorithm( + keyType: KeyType, + digestAlgorithm: DigestAlgorithm, + rsaScheme: RsaScheme, +): string { + if (keyType === "EC") { + switch (digestAlgorithm) { + case "SHA-256": + return "ES256"; + case "SHA-384": + return "ES384"; + case "SHA-512": + return "ES512"; + } + } + + // RSA + if (rsaScheme === "PSS") { + switch (digestAlgorithm) { + case "SHA-256": + return "PS256"; + case "SHA-384": + return "PS384"; + case "SHA-512": + return "PS512"; + } + } + + // PKCS1 (default) + switch (digestAlgorithm) { + case "SHA-256": + return "RS256"; + case "SHA-384": + return "RS384"; + case "SHA-512": + return "RS512"; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Key URL Utilities +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Build a full key ID URL from shorthand options. + * + * @internal Exported for testing + */ +export function buildKeyId(options: AzureKeyVaultSignerShorthandOptions): string { + const suffix = options.vaultSuffix ?? "vault.azure.net"; + const base = `https://${options.vaultName}.${suffix}/keys/${options.keyName}`; + + if (options.keyVersion) { + return `${base}/${options.keyVersion}`; + } + + return base; +} + +/** + * Build the vault URL from shorthand options. + * + * @internal Exported for testing + */ +export function buildVaultUrl(options: AzureKeyVaultSignerShorthandOptions): string { + const suffix = options.vaultSuffix ?? "vault.azure.net"; + + return `https://${options.vaultName}.${suffix}`; +} + +/** + * Extract the vault URL from a full key ID URL. + * + * @example + * ``` + * parseVaultUrl("https://my-vault.vault.azure.net/keys/my-key/abc123") + * // "https://my-vault.vault.azure.net" + * ``` + * + * @internal Exported for testing + */ +export function parseVaultUrl(keyId: string): string { + const url = new URL(keyId); + + return `${url.protocol}//${url.host}`; +} + +/** + * Check if options use full key ID style. + */ +function isKeyIdOptions( + options: AzureKeyVaultSignerOptions, +): options is AzureKeyVaultSignerKeyIdOptions { + return "keyId" in options; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Certificate Utilities +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Extract public key PEM from a DER-encoded certificate. + */ +function extractPublicKeyFromCertificate(certDer: Uint8Array): string { + const asn1 = fromBER(toArrayBuffer(certDer)); + + if (asn1.offset === -1) { + throw new AzureKeyVaultSignerError("Failed to parse certificate"); + } + + const cert = new pkijs.Certificate({ schema: asn1.result }); + const spki = cert.subjectPublicKeyInfo.toSchema().toBER(false); + + return derToPem(new Uint8Array(spki), "PUBLIC KEY"); +} + +/** + * Convert an Azure JWK key to PEM for comparison with the certificate. + * + * Azure's `KeyVaultKey.key` is a JsonWebKey. We need to convert it to + * SPKI PEM format to compare with the certificate's public key. + */ +function jwkToSpkiPem(jwk: { + kty?: string; + n?: Uint8Array; + e?: Uint8Array; + x?: Uint8Array; + y?: Uint8Array; + crv?: string; +}): string | null { + if (!jwk.kty) { + return null; + } + + // Use a WebCrypto-compatible JWK for conversion + // We'll build the SPKI manually using ASN.1 + if (jwk.kty === "RSA" || jwk.kty === "RSA-HSM") { + if (!jwk.n || !jwk.e) { + return null; + } + + return buildRsaSpkiPem(jwk.n, jwk.e); + } + + if (jwk.kty === "EC" || jwk.kty === "EC-HSM") { + if (!jwk.x || !jwk.y || !jwk.crv) { + return null; + } + + return buildEcSpkiPem(jwk.x, jwk.y, jwk.crv); + } + + return null; +} + +/** + * Build an RSA SPKI PEM from modulus (n) and exponent (e). + */ +function buildRsaSpkiPem(n: Uint8Array, e: Uint8Array): string { + // Build RSAPublicKey: SEQUENCE { INTEGER n, INTEGER e } + const nInteger = buildAsn1Integer(n); + const eInteger = buildAsn1Integer(e); + const rsaPublicKey = buildAsn1Sequence([nInteger, eInteger]); + + // OID for rsaEncryption: 1.2.840.113549.1.1.1 + const rsaOid = new Uint8Array([0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01]); + const nullParam = new Uint8Array([0x05, 0x00]); + const algorithmId = buildAsn1Sequence([rsaOid, nullParam]); + + // BIT STRING wrapping the RSAPublicKey + const bitString = buildAsn1BitString(rsaPublicKey); + + // SubjectPublicKeyInfo: SEQUENCE { AlgorithmIdentifier, BIT STRING } + const spki = buildAsn1Sequence([algorithmId, bitString]); + + return derToPem(spki, "PUBLIC KEY"); +} + +/** + * Build an EC SPKI PEM from x, y coordinates and curve name. + */ +function buildEcSpkiPem(x: Uint8Array, y: Uint8Array, crv: string): string { + // OID for ecPublicKey: 1.2.840.10045.2.1 + const ecOid = new Uint8Array([0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01]); + + // Curve OID + const curveOid = getCurveOid(crv); + + if (!curveOid) { + return ""; + } + + const algorithmId = buildAsn1Sequence([ecOid, curveOid]); + + // Uncompressed point: 0x04 || x || y + const pointData = new Uint8Array(1 + x.length + y.length); + pointData[0] = 0x04; + pointData.set(x, 1); + pointData.set(y, 1 + x.length); + + const bitString = buildAsn1BitString(pointData); + + const spki = buildAsn1Sequence([algorithmId, bitString]); + + return derToPem(spki, "PUBLIC KEY"); +} + +/** + * Get the OID for a named EC curve. + */ +function getCurveOid(crv: string): Uint8Array | null { + switch (crv) { + case "P-256": + // 1.2.840.10045.3.1.7 + return new Uint8Array([0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]); + case "P-384": + // 1.3.132.0.34 + return new Uint8Array([0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22]); + case "P-521": + // 1.3.132.0.35 + return new Uint8Array([0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x23]); + default: + return null; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// ASN.1 DER Encoding Helpers +// ───────────────────────────────────────────────────────────────────────────── + +function buildAsn1Length(length: number): Uint8Array { + if (length < 0x80) { + return new Uint8Array([length]); + } + + if (length < 0x100) { + return new Uint8Array([0x81, length]); + } + + if (length < 0x10000) { + return new Uint8Array([0x82, (length >> 8) & 0xff, length & 0xff]); + } + + return new Uint8Array([0x83, (length >> 16) & 0xff, (length >> 8) & 0xff, length & 0xff]); +} + +function buildAsn1Sequence(elements: Uint8Array[]): Uint8Array { + let totalLength = 0; + + for (const el of elements) { + totalLength += el.length; + } + + const lengthBytes = buildAsn1Length(totalLength); + const result = new Uint8Array(1 + lengthBytes.length + totalLength); + result[0] = 0x30; // SEQUENCE tag + result.set(lengthBytes, 1); + + let offset = 1 + lengthBytes.length; + + for (const el of elements) { + result.set(el, offset); + offset += el.length; + } + + return result; +} + +function buildAsn1Integer(value: Uint8Array): Uint8Array { + // Ensure positive: if high bit is set, prepend 0x00 + const needsPadding = value.length > 0 && (value[0] & 0x80) !== 0; + const intLength = needsPadding ? value.length + 1 : value.length; + const lengthBytes = buildAsn1Length(intLength); + const result = new Uint8Array(1 + lengthBytes.length + intLength); + result[0] = 0x02; // INTEGER tag + result.set(lengthBytes, 1); + + if (needsPadding) { + result[1 + lengthBytes.length] = 0x00; + result.set(value, 2 + lengthBytes.length); + } else { + result.set(value, 1 + lengthBytes.length); + } + + return result; +} + +function buildAsn1BitString(data: Uint8Array): Uint8Array { + // BIT STRING: tag(0x03) + length + unused-bits(0x00) + data + const contentLength = 1 + data.length; // 1 byte for unused bits + const lengthBytes = buildAsn1Length(contentLength); + const result = new Uint8Array(1 + lengthBytes.length + contentLength); + result[0] = 0x03; // BIT STRING tag + result.set(lengthBytes, 1); + result[1 + lengthBytes.length] = 0x00; // no unused bits + result.set(data, 2 + lengthBytes.length); + + return result; +} + +/** + * Check if two public keys match (PEM comparison). + */ +function publicKeysMatch(vaultPem: string, certPem: string): boolean { + return normalizePem(vaultPem) === normalizePem(certPem); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Dynamic Imports +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Dynamically import @azure/keyvault-keys. + */ +async function importKeyVaultKeys(): Promise { + try { + return await import("@azure/keyvault-keys"); + } catch (error) { + // oxlint-disable-next-line typescript/no-unsafe-type-assertion + const code = (error as NodeJS.ErrnoException).code; + + if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") { + throw new AzureKeyVaultSignerError( + "@azure/keyvault-keys is required. Install with: npm install @azure/keyvault-keys @azure/identity", + ); + } + + throw error; + } +} + +/** + * Dynamically import @azure/keyvault-certificates. + */ +async function importKeyVaultCertificates(): Promise< + typeof import("@azure/keyvault-certificates") +> { + try { + return await import("@azure/keyvault-certificates"); + } catch (error) { + // oxlint-disable-next-line typescript/no-unsafe-type-assertion + const code = (error as NodeJS.ErrnoException).code; + + if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") { + throw new AzureKeyVaultSignerError( + "@azure/keyvault-certificates is required. Install with: npm install @azure/keyvault-certificates @azure/identity", + ); + } + + throw error; + } +} + +/** + * Resolve the credential — use the provided one or create a DefaultAzureCredential. + * + * @azure/identity is dynamically imported only when no credential is provided. + */ +async function resolveCredential(credential?: TokenCredential): Promise { + if (credential) { + return credential; + } + + try { + const { DefaultAzureCredential } = await import("@azure/identity"); + + return new DefaultAzureCredential(); + } catch (error) { + // oxlint-disable-next-line typescript/no-unsafe-type-assertion + const code = (error as NodeJS.ErrnoException).code; + + if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") { + throw new AzureKeyVaultSignerError( + "@azure/identity is required when no credential is provided. " + + "Install with: npm install @azure/identity", + ); + } + + throw error; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// REST Error Handling +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Shape of errors thrown by Azure SDK (RestError). + */ +interface AzureRestError extends Error { + statusCode?: number; + code?: string; +} + +/** + * Type guard for Azure REST errors. + */ +function isAzureRestError(error: unknown): error is AzureRestError { + // oxlint-disable-next-line typescript/no-unsafe-type-assertion + const restError = error as AzureRestError; + + return restError instanceof Error && typeof restError.statusCode === "number"; +} + +/** + * Detect key type from Azure's key type string. + * + * Azure key types: "RSA", "RSA-HSM", "EC", "EC-HSM", "oct", "oct-HSM" + * + * @internal Exported for testing + */ +export function detectKeyType(azureKeyType: string): KeyType { + if (azureKeyType === "RSA" || azureKeyType === "RSA-HSM") { + return "RSA"; + } + + if (azureKeyType === "EC" || azureKeyType === "EC-HSM") { + return "EC"; + } + + throw new AzureKeyVaultSignerError( + `Unsupported key type for PDF signing: ${azureKeyType}. Only RSA and EC keys are supported.`, + ); +} + +/** + * Detect the EC curve's associated digest algorithm from the Azure curve name. + * + * EC keys in Azure are locked to a specific curve, and each curve implies + * a specific digest algorithm for signing. + * + * @internal Exported for testing + */ +export function ecCurveToDigestAlgorithm(crv: string): DigestAlgorithm { + switch (crv) { + case "P-256": + return "SHA-256"; + case "P-384": + return "SHA-384"; + case "P-521": + return "SHA-512"; + default: + throw new AzureKeyVaultSignerError( + `Unsupported EC curve for PDF signing: ${crv}. Use P-256, P-384, or P-521.`, + ); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// AzureKeyVaultSigner +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Signer that uses Azure Key Vault for signing operations. + * + * Supports RSA and ECDSA keys stored in Azure Key Vault, including + * HSM-backed keys (both standard Key Vault and Managed HSM). + * The private key never leaves the vault — only the digest is sent for signing. + * + * **Performance note:** Each `sign()` call makes a network request to Azure + * Key Vault (~50-200ms latency). For bulk signing, consider the performance + * implications. + * + * @example + * ```typescript + * // Uses DefaultAzureCredential automatically + * const signer = await AzureKeyVaultSigner.create({ + * keyId: "https://my-vault.vault.azure.net/keys/my-key/abc123", + * certificate: certificateDer, + * }); + * + * const pdf = await PDF.load(pdfBytes); + * const { bytes } = await pdf.sign({ signer }); + * ``` + */ +export class AzureKeyVaultSigner implements Signer { + readonly certificate: Uint8Array; + readonly certificateChain: Uint8Array[]; + readonly keyType: KeyType; + readonly signatureAlgorithm: SignatureAlgorithm; + + /** The full key ID URL (for logging/debugging) */ + readonly keyId: string; + + /** RSA signature scheme preference */ + readonly rsaScheme: RsaScheme; + + private readonly cryptoClient: CryptographyClient; + + private constructor( + cryptoClient: CryptographyClient, + keyId: string, + certificate: Uint8Array, + certificateChain: Uint8Array[], + keyType: KeyType, + signatureAlgorithm: SignatureAlgorithm, + rsaScheme: RsaScheme, + ) { + this.cryptoClient = cryptoClient; + this.keyId = keyId; + this.certificate = certificate; + this.certificateChain = certificateChain; + this.keyType = keyType; + this.signatureAlgorithm = signatureAlgorithm; + this.rsaScheme = rsaScheme; + } + + /** + * Create an AzureKeyVaultSigner from a key reference. + * + * @param options - Configuration options (key reference, certificate, credential, etc.) + * @returns A new AzureKeyVaultSigner instance + * @throws {AzureKeyVaultSignerError} if key is invalid, disabled, or certificate doesn't match + * + * @example + * ```typescript + * // Full key ID URL (uses DefaultAzureCredential automatically) + * const signer = await AzureKeyVaultSigner.create({ + * keyId: "https://my-vault.vault.azure.net/keys/my-key/abc123", + * certificate: certificateDer, + * }); + * + * // Shorthand with explicit credential + * const signer = await AzureKeyVaultSigner.create({ + * vaultName: "my-vault", + * keyName: "my-key", + * keyVersion: "abc123", + * certificate: certificateDer, + * credential: new DefaultAzureCredential(), + * }); + * ``` + */ + static async create(options: AzureKeyVaultSignerOptions): Promise { + // Dynamically import Azure SDK + const keyvaultKeys = await importKeyVaultKeys(); + + // Resolve key ID and vault URL + const keyId = isKeyIdOptions(options) ? options.keyId : buildKeyId(options); + const vaultUrl = isKeyIdOptions(options) + ? parseVaultUrl(options.keyId) + : buildVaultUrl(options); + const rsaScheme = options.rsaScheme ?? "PKCS1"; + + // Resolve credential (use DefaultAzureCredential if not provided) + const credential = await resolveCredential(options.credential); + + // Create or use provided clients + const keyClient = options.keyClient ?? new keyvaultKeys.KeyClient(vaultUrl, credential); + + try { + // Extract key name (and optionally version) from keyId + const url = new URL(keyId); + const pathParts = url.pathname.split("/").filter(Boolean); + // pathParts: ["keys", ""] or ["keys", "", ""] + const keyName = pathParts[1]; + const keyVersion = pathParts.length > 2 ? pathParts[2] : undefined; + + if (!keyName) { + throw new AzureKeyVaultSignerError( + `Invalid key ID URL: ${keyId}. Expected format: https://{vault}.vault.azure.net/keys/{name}/{version}`, + ); + } + + // Fetch key metadata + const keyVaultKey = keyVersion + ? await keyClient.getKey(keyName, { version: keyVersion }) + : await keyClient.getKey(keyName); + + // Validate key is enabled + if (keyVaultKey.properties.enabled === false) { + throw new AzureKeyVaultSignerError( + `Key is not enabled: ${keyId}. Enable the key in Azure Key Vault before signing.`, + ); + } + + // Validate key supports signing + const keyOps = keyVaultKey.keyOperations ?? []; + + if (!keyOps.includes("sign")) { + throw new AzureKeyVaultSignerError( + `Key does not support signing: ${keyId}. ` + + `Permitted operations: [${keyOps.join(", ")}]. The key must have the "sign" operation.`, + ); + } + + // Detect key type + const azureKeyType = keyVaultKey.key?.kty; + + if (!azureKeyType) { + throw new AzureKeyVaultSignerError(`Failed to get key type for: ${keyId}`); + } + + const keyType = detectKeyType(azureKeyType); + + // Determine the signature algorithm + let signatureAlgorithm: SignatureAlgorithm; + + if (keyType === "EC") { + signatureAlgorithm = "ECDSA"; + } else if (rsaScheme === "PSS") { + signatureAlgorithm = "RSA-PSS"; + + console.warn( + "Warning: RSA-PSS signatures may not verify correctly in older PDF readers " + + "(Adobe Acrobat < 2020). Consider using PKCS1 (default) for maximum compatibility.", + ); + } else { + signatureAlgorithm = "RSASSA-PKCS1-v1_5"; + } + + // Validate certificate matches vault key + if (keyVaultKey.key) { + const vaultKeyPem = jwkToSpkiPem(keyVaultKey.key); + const certPem = extractPublicKeyFromCertificate(options.certificate); + + if (vaultKeyPem && !publicKeysMatch(vaultKeyPem, certPem)) { + throw new AzureKeyVaultSignerError( + "Certificate public key does not match Azure Key Vault key. " + + "Ensure the certificate was issued for this key.", + ); + } + } + + // Build certificate chain if requested + let chainCertsDer: Uint8Array[] = options.certificateChain ?? []; + + if (options.buildChain) { + try { + chainCertsDer = await buildCertificateChain(options.certificate, { + existingChain: chainCertsDer, + timeout: options.chainTimeout, + }); + } catch (error) { + if (error instanceof CertificateChainError) { + console.warn(`Could not complete certificate chain via AIA: ${error.message}`); + } else { + throw error; + } + } + } + + // Create CryptographyClient + const cryptoClient = + options.cryptographyClient ?? new keyvaultKeys.CryptographyClient(keyVaultKey, credential); + + // Use the actual key ID returned by Azure (includes version) + const resolvedKeyId = keyVaultKey.id ?? keyId; + + return new AzureKeyVaultSigner( + cryptoClient, + resolvedKeyId, + options.certificate, + chainCertsDer, + keyType, + signatureAlgorithm, + rsaScheme, + ); + } catch (error) { + if (error instanceof AzureKeyVaultSignerError) { + throw error; + } + + if (isAzureRestError(error)) { + switch (error.statusCode) { + case 401: + case 403: + throw new AzureKeyVaultSignerError( + `Permission denied for key: ${keyId}. ` + + `Ensure the identity has 'Key Sign' and 'Key Get' permissions on the vault.`, + error, + ); + + case 404: + throw new AzureKeyVaultSignerError( + `Key not found: ${keyId}. Verify the key name, version, and vault URL.`, + error, + ); + } + } + + const message = error instanceof Error ? error.message : String(error); + + throw new AzureKeyVaultSignerError(`Failed to initialize Azure Key Vault signer: ${message}`); + } + } + + /** + * Load a signing certificate from Azure Key Vault. + * + * Azure Key Vault certificates contain the X.509 certificate (and optionally + * the chain). The certificate is stored alongside the key with the same name. + * + * @param options - Options specifying which certificate to load + * @returns The certificate DER bytes and optional chain + * @throws {AzureKeyVaultSignerError} if certificate retrieval fails + * + * @example + * ```typescript + * // Uses DefaultAzureCredential automatically + * const { cert, chain } = await AzureKeyVaultSigner.getCertificateFromKeyVault({ + * vaultUrl: "https://my-vault.vault.azure.net", + * certificateName: "my-signing-cert", + * }); + * + * const signer = await AzureKeyVaultSigner.create({ + * vaultName: "my-vault", + * keyName: "my-signing-cert", + * certificate: cert, + * certificateChain: chain, + * }); + * ``` + */ + static async getCertificateFromKeyVault(options: GetCertificateFromKeyVaultOptions): Promise<{ + cert: Uint8Array; + chain?: Uint8Array[]; + }> { + const keyvaultCerts = await importKeyVaultCertificates(); + + // Resolve credential (use DefaultAzureCredential if not provided) + const credential = await resolveCredential(options.credential); + + const client = + options.certificateClient ?? + new keyvaultCerts.CertificateClient(options.vaultUrl, credential); + + try { + const certificate = options.certificateVersion + ? await client.getCertificateVersion(options.certificateName, options.certificateVersion) + : await client.getCertificate(options.certificateName); + + if (!certificate.cer) { + throw new AzureKeyVaultSignerError( + `Certificate has no certificate data: ${options.certificateName}`, + ); + } + + // certificate.cer is the DER-encoded X.509 certificate + const cert = new Uint8Array(certificate.cer); + + // Azure Key Vault doesn't directly expose the chain via getCertificate(). + // The chain is available as part of the certificate's secret (PEM/PFX), + // but for our use case, we return just the leaf certificate. + // Users can provide the chain separately or use buildChain: true. + return { cert }; + } catch (error) { + if (error instanceof AzureKeyVaultSignerError) { + throw error; + } + + if (isAzureRestError(error)) { + switch (error.statusCode) { + case 401: + case 403: + throw new AzureKeyVaultSignerError( + `Permission denied for certificate: ${options.certificateName}. ` + + `Ensure the identity has 'Certificate Get' permission on the vault.`, + error, + ); + + case 404: + throw new AzureKeyVaultSignerError( + `Certificate not found: ${options.certificateName}. ` + + `Verify the certificate name and vault URL.`, + error, + ); + } + } + + const message = error instanceof Error ? error.message : String(error); + + throw new AzureKeyVaultSignerError( + `Failed to fetch certificate from Azure Key Vault: ${message}`, + ); + } + } + + /** + * Sign data using the Azure Key Vault key. + * + * The data is hashed locally using the requested digest algorithm, then the + * digest is sent to Azure Key Vault for signing. The Azure algorithm is + * resolved at sign-time based on the key type and RSA scheme preference. + * + * @param data - The data to sign + * @param algorithm - The digest algorithm to use + * @returns The signature bytes + * @throws {AzureKeyVaultSignerError} if signing fails + */ + async sign(data: Uint8Array, algorithm: DigestAlgorithm): Promise { + // Hash data locally + const digest = this.hashData(data, algorithm); + + // Resolve the Azure algorithm + const azureAlgorithm = resolveAzureAlgorithm(this.keyType, algorithm, this.rsaScheme); + + try { + const result = await this.cryptoClient.sign(azureAlgorithm, digest); + + if (!result.result) { + throw new AzureKeyVaultSignerError("Azure Key Vault did not return a signature"); + } + + return new Uint8Array(result.result); + } catch (error) { + if (error instanceof AzureKeyVaultSignerError) { + throw error; + } + + if (isAzureRestError(error) && (error.statusCode === 401 || error.statusCode === 403)) { + throw new AzureKeyVaultSignerError( + `Permission denied for signing with key: ${this.keyId}. ` + + `Ensure the identity has 'Key Sign' permission.`, + error, + ); + } + + const message = error instanceof Error ? error.message : String(error); + + throw new AzureKeyVaultSignerError(`Failed to sign with Azure Key Vault: ${message}`); + } + } + + /** + * Hash data using the specified algorithm. + * + * @returns The digest bytes + */ + private hashData(data: Uint8Array, algorithm: DigestAlgorithm): Uint8Array { + switch (algorithm) { + case "SHA-256": + return sha256(data); + case "SHA-384": + return sha384(data); + case "SHA-512": + return sha512(data); + } + } +} diff --git a/src/signatures/signers/index.ts b/src/signatures/signers/index.ts index cbe76f3..6450097 100644 --- a/src/signatures/signers/index.ts +++ b/src/signatures/signers/index.ts @@ -3,7 +3,8 @@ */ export type { DigestAlgorithm, KeyType, SignatureAlgorithm, Signer } from "../types"; -export { KmsSignerError, SignerError } from "../types"; +export { AzureKeyVaultSignerError, KmsSignerError, SignerError } from "../types"; +export { AzureKeyVaultSigner } from "./azure-key-vault"; export { CryptoKeySigner } from "./crypto-key"; export { GoogleKmsSigner } from "./google-kms"; export { P12Signer, type P12SignerOptions } from "./p12"; diff --git a/src/signatures/types.ts b/src/signatures/types.ts index 9f6393a..1719e4d 100644 --- a/src/signatures/types.ts +++ b/src/signatures/types.ts @@ -412,3 +412,13 @@ export class KmsSignerError extends SignerError { this.cause = cause; } } + +/** + * Error with Azure Key Vault signer (e.g., key issues, permission denied, unsupported algorithm). + */ +export class AzureKeyVaultSignerError extends KmsSignerError { + constructor(message: string, cause?: Error) { + super(`Azure Key Vault: ${message}`, cause); + this.name = "AzureKeyVaultSignerError"; + } +} From dda2b046007a8aa44ff1f962a8381a7f91141a46 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sun, 8 Feb 2026 10:07:43 +1100 Subject: [PATCH 3/3] chore: formatting --- .lintstagedrc.json | 2 +- .../guides/signatures/azure-key-vault.mdx | 106 +++++++++--------- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 44c0bfb..bc4fbac 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -1,5 +1,5 @@ { "*.{ts,tsx,js,jsx}": ["oxlint --fix", "oxfmt"], "*.json": ["oxfmt"], - "*.md": ["oxfmt"] + "*.{md,mdx}": ["oxfmt"] } diff --git a/apps/docs/content/docs/guides/signatures/azure-key-vault.mdx b/apps/docs/content/docs/guides/signatures/azure-key-vault.mdx index cca7a47..df73f12 100644 --- a/apps/docs/content/docs/guides/signatures/azure-key-vault.mdx +++ b/apps/docs/content/docs/guides/signatures/azure-key-vault.mdx @@ -54,13 +54,13 @@ await writeFile("signed.pdf", bytes); `AzureKeyVaultSigner` uses [`DefaultAzureCredential`](https://learn.microsoft.com/en-us/javascript/api/@azure/identity/defaultazurecredential) by default when no `credential` option is provided. This tries multiple authentication methods in order: -| Method | Environment | Setup | -| ------------------- | ----------------- | ---------------------------------------------------- | -| Environment vars | Any | Set `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, and secret | -| Managed Identity | Azure VMs/App Svc | Automatic (uses instance metadata) | -| Azure CLI | Local development | Run `az login` | -| Workload Identity | AKS | Configure workload identity for your pod | -| Azure PowerShell | Local development | Run `Connect-AzAccount` | +| Method | Environment | Setup | +| ----------------- | ----------------- | ---------------------------------------------------- | +| Environment vars | Any | Set `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, and secret | +| Managed Identity | Azure VMs/App Svc | Automatic (uses instance metadata) | +| Azure CLI | Local development | Run `az login` | +| Workload Identity | AKS | Configure workload identity for your pod | +| Azure PowerShell | Local development | Run `Connect-AzAccount` | You can also provide an explicit credential: @@ -98,18 +98,18 @@ Create a new Azure Key Vault signer instance. ### Full Key ID URL -| Param | Type | Default | Description | -| ------------------------------- | ------------------ | ---------- | ----------------------------------------------- | -| `options` | `object` | required | | -| `options.keyId` | `string` | required | Full key identifier URL | -| `options.certificate` | `Uint8Array` | required | DER-encoded X.509 certificate for this key | -| `[options.credential]` | `TokenCredential` | | Azure credential (default: DefaultAzureCredential) | -| `[options.certificateChain]` | `Uint8Array[]` | | Intermediate and root certificates | -| `[options.buildChain]` | `boolean` | `false` | Fetch chain via AIA extensions | -| `[options.chainTimeout]` | `number` | `15000` | Timeout for AIA fetching (ms) | -| `[options.rsaScheme]` | `"PKCS1" \| "PSS"` | `"PKCS1"` | RSA signature scheme (RSA keys only) | -| `[options.keyClient]` | `KeyClient` | | Pre-configured Key Vault client | -| `[options.cryptographyClient]` | `CryptographyClient` | | Pre-configured Cryptography client | +| Param | Type | Default | Description | +| ------------------------------ | -------------------- | --------- | -------------------------------------------------- | +| `options` | `object` | required | | +| `options.keyId` | `string` | required | Full key identifier URL | +| `options.certificate` | `Uint8Array` | required | DER-encoded X.509 certificate for this key | +| `[options.credential]` | `TokenCredential` | | Azure credential (default: DefaultAzureCredential) | +| `[options.certificateChain]` | `Uint8Array[]` | | Intermediate and root certificates | +| `[options.buildChain]` | `boolean` | `false` | Fetch chain via AIA extensions | +| `[options.chainTimeout]` | `number` | `15000` | Timeout for AIA fetching (ms) | +| `[options.rsaScheme]` | `"PKCS1" \| "PSS"` | `"PKCS1"` | RSA signature scheme (RSA keys only) | +| `[options.keyClient]` | `KeyClient` | | Pre-configured Key Vault client | +| `[options.cryptographyClient]` | `CryptographyClient` | | Pre-configured Cryptography client | ```typescript const signer = await AzureKeyVaultSigner.create({ @@ -123,12 +123,12 @@ const signer = await AzureKeyVaultSigner.create({ Instead of a full key ID URL, you can use shorthand properties: -| Param | Type | Default | Description | -| ------------------------ | -------- | -------------------- | ---------------------------- | -| `options.vaultName` | `string` | required | Vault name | -| `options.keyName` | `string` | required | Key name in the vault | -| `[options.keyVersion]` | `string` | | Key version (default: latest) | -| `[options.vaultSuffix]` | `string` | `"vault.azure.net"` | Vault domain suffix | +| Param | Type | Default | Description | +| ----------------------- | -------- | ------------------- | ----------------------------- | +| `options.vaultName` | `string` | required | Vault name | +| `options.keyName` | `string` | required | Key name in the vault | +| `[options.keyVersion]` | `string` | | Key version (default: latest) | +| `[options.vaultSuffix]` | `string` | `"vault.azure.net"` | Vault domain suffix | ```typescript const signer = await AzureKeyVaultSigner.create({ @@ -178,22 +178,22 @@ Azure Key Vault supports RSA and EC keys. Unlike Google Cloud KMS where the algo ### RSA Keys -| RSA Scheme | Digest | Azure Algorithm | Signature Algorithm | -| ---------- | -------- | --------------- | ------------------- | -| PKCS1 | SHA-256 | RS256 | RSASSA-PKCS1-v1_5 | -| PKCS1 | SHA-384 | RS384 | RSASSA-PKCS1-v1_5 | -| PKCS1 | SHA-512 | RS512 | RSASSA-PKCS1-v1_5 | -| PSS | SHA-256 | PS256 | RSA-PSS | -| PSS | SHA-384 | PS384 | RSA-PSS | -| PSS | SHA-512 | PS512 | RSA-PSS | +| RSA Scheme | Digest | Azure Algorithm | Signature Algorithm | +| ---------- | ------- | --------------- | ------------------- | +| PKCS1 | SHA-256 | RS256 | RSASSA-PKCS1-v1_5 | +| PKCS1 | SHA-384 | RS384 | RSASSA-PKCS1-v1_5 | +| PKCS1 | SHA-512 | RS512 | RSASSA-PKCS1-v1_5 | +| PSS | SHA-256 | PS256 | RSA-PSS | +| PSS | SHA-384 | PS384 | RSA-PSS | +| PSS | SHA-512 | PS512 | RSA-PSS | ### EC Keys -| Curve | Digest | Azure Algorithm | Signature Algorithm | -| ----- | -------- | --------------- | ------------------- | -| P-256 | SHA-256 | ES256 | ECDSA | -| P-384 | SHA-384 | ES384 | ECDSA | -| P-521 | SHA-512 | ES512 | ECDSA | +| Curve | Digest | Azure Algorithm | Signature Algorithm | +| ----- | ------- | --------------- | ------------------- | +| P-256 | SHA-256 | ES256 | ECDSA | +| P-384 | SHA-384 | ES384 | ECDSA | +| P-521 | SHA-512 | ES512 | ECDSA | **RSA-PSS compatibility**: RSA-PSS signatures may not verify correctly in older PDF readers (Adobe @@ -250,14 +250,14 @@ In Azure Key Vault, a "certificate" is a bundle containing both the certificate ### getCertificateFromKeyVault(options) -| Param | Type | Default | Description | -| ------------------------------- | ------------------ | -------- | -------------------------------------------------- | -| `options` | `object` | required | | -| `options.vaultUrl` | `string` | required | Full vault URL | -| `options.certificateName` | `string` | required | Certificate name in the vault | -| `[options.certificateVersion]` | `string` | | Certificate version (default: latest) | -| `[options.credential]` | `TokenCredential` | | Azure credential (default: DefaultAzureCredential) | -| `[options.certificateClient]` | `CertificateClient` | | Pre-configured Certificate client | +| Param | Type | Default | Description | +| ------------------------------ | ------------------- | -------- | -------------------------------------------------- | +| `options` | `object` | required | | +| `options.vaultUrl` | `string` | required | Full vault URL | +| `options.certificateName` | `string` | required | Certificate name in the vault | +| `[options.certificateVersion]` | `string` | | Certificate version (default: latest) | +| `[options.credential]` | `TokenCredential` | | Azure credential (default: DefaultAzureCredential) | +| `[options.certificateClient]` | `CertificateClient` | | Pre-configured Certificate client | **Returns**: `Promise<{ cert: Uint8Array; chain?: Uint8Array[] }>` @@ -376,13 +376,13 @@ try { Common errors: -| Error | Cause | Solution | -| -------------------- | ---------------------------------------- | -------------------------------------------- | -| Key not found | Invalid key name, version, or vault URL | Verify the key reference and vault URL | -| Permission denied | Missing Key Vault permissions | Grant Key Sign and Key Get permissions | -| Key is not enabled | Key disabled in the vault | Enable the key in Azure Portal | -| Certificate mismatch | Certificate wasn't issued for this key | Regenerate certificate from the vault's key | -| Unsupported key type | Symmetric key or unsupported curve | Use RSA or EC (P-256, P-384, P-521) keys | +| Error | Cause | Solution | +| -------------------- | --------------------------------------- | ------------------------------------------- | +| Key not found | Invalid key name, version, or vault URL | Verify the key reference and vault URL | +| Permission denied | Missing Key Vault permissions | Grant Key Sign and Key Get permissions | +| Key is not enabled | Key disabled in the vault | Enable the key in Azure Portal | +| Certificate mismatch | Certificate wasn't issued for this key | Regenerate certificate from the vault's key | +| Unsupported key type | Symmetric key or unsupported curve | Use RSA or EC (P-256, P-384, P-521) keys | ---