diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index a59ab55e5..160224b68 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1 @@
-* @hashicorp-forge/labs
+* @hashicorp-forge/labs @hashicorp/team-scale-performance-eng
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 12b26e1d6..634fe10bb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,6 +5,7 @@ on:
branches: [main]
push:
branches: [main]
+ workflow_dispatch:
jobs:
build-and-test:
@@ -24,28 +25,26 @@ jobs:
run: |
corepack enable
cd web
- # Remove .yarnrc.yml to let corepack handle yarn version
- rm -f .yarnrc.yml
corepack prepare yarn@$(node -p "require('./package.json').packageManager.split('@')[1]") --activate
yarn --version
- name: Set up Go
uses: actions/setup-go@v4
with:
- go-version: "^1.18"
-
- - name: Verify Yarn version
- run: cd web && yarn --version
+ go-version: "^1.24"
- name: Build web
run: cd web && yarn install && yarn build
- - name: Run web test (continue on failure)
- run: cd web && yarn test:ember --timeout 600000 || true
- continue-on-error: true
+ - name: Build plugin
+ run: cd hermes-plugin && npm install && npm run build
- - name: Build
+ - name: Build Go binary
run: make bin/linux
- name: Run Go test
run: make go/test
+
+ - name: Run web test (continue on failure)
+ run: cd web && yarn test:ember --timeout 600000 || true
+ continue-on-error: true
diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml
new file mode 100644
index 000000000..bf81935e0
--- /dev/null
+++ b/.github/workflows/docker-build-push.yml
@@ -0,0 +1,48 @@
+name: Docker Build & Push
+
+on:
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+jobs:
+ build-and-push:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: "18"
+
+ - name: Enable Corepack
+ run: |
+ corepack enable
+ cd web
+ corepack prepare yarn@$(node -p "require('./package.json').packageManager.split('@')[1]") --activate
+
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: "^1.24"
+
+ - name: Build
+ run: make build/linux
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: true
+ tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
diff --git a/.gitignore b/.gitignore
index 231155b1e..90c4530c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,9 @@
.DS_Store
.env
+# TLS certs (local dev)
+/certs
+
# Web application
node_modules
/web/.pnp.*
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..ab475aeca
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,18 @@
+FROM alpine:3.21.5
+
+# Update the package repository and install dependencies
+RUN apk --no-cache update && \
+ apk --no-cache upgrade
+
+# Set the working directory
+WORKDIR /app
+
+# Copy the application binary and configuration
+COPY hermes /app/hermes
+COPY configs /app/configs
+
+# Set the entrypoint to run the hermes application
+ENTRYPOINT ["/app/hermes"]
+
+# Default command when container starts
+CMD ["server"]
diff --git a/Makefile b/Makefile
index 2a9920675..b4d3fc869 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@
default: help
.PHONY: build
-build: web/build
+build: web/build plugin/build
rm -f ./hermes
CGO_ENABLED=0 go build -o ./hermes ./cmd/hermes
@@ -14,6 +14,15 @@ bin:
bin/linux: # bin creates hermes binary for linux
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./hermes ./cmd/hermes
+.PHONY: build/linux
+build/linux: web/build plugin/build
+ rm -f ./hermes
+ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./hermes ./cmd/hermes
+
+.PHONY: plugin/build
+plugin/build:
+ cd hermes-plugin && npm install && rm -rf dist/ && npm run build
+
.PHONY: dev
dev: ## One command to start a dev environment
dev: docker/postgres/start
diff --git a/configs/config.hcl b/configs/config.hcl
index 87a72f8f2..5520bfcee 100644
--- a/configs/config.hcl
+++ b/configs/config.hcl
@@ -243,3 +243,56 @@ server {
// addr is the address to bind to for listening.
addr = "127.0.0.1:8000"
}
+
+// --------------------------------------------------------------------------
+// SharePoint Backend Configuration (alternative to google_workspace)
+// --------------------------------------------------------------------------
+// Uncomment and configure these blocks to use SharePoint instead of Google.
+
+// sharepoint {
+// client_id = "YOUR_SHAREPOINT_CLIENT_ID"
+// client_secret = "YOUR_SHAREPOINT_CLIENT_SECRET"
+// tenant_id = "YOUR_AZURE_AD_TENANT_ID"
+// site_id = "YOUR_SHAREPOINT_SITE_ID"
+// drive_id = "YOUR_SHAREPOINT_DRIVE_ID"
+// domain = "your-domain.com"
+//
+// // templates_folder_id is the ID of the folder containing document templates.
+// templates_folder_id = ""
+//
+// // docs_folder_id is the ID of the published documents folder.
+// docs_folder_id = ""
+//
+// // drafts_folder_id is the ID of the drafts folder.
+// drafts_folder_id = ""
+// }
+
+// oidc_alb {
+// // auth_server_url is the OIDC discovery URL.
+// auth_server_url = "https://your-oidc-provider.com"
+// // aws_region is the region of the AWS ALB.
+// aws_region = "us-east-1"
+// // client_id is the OIDC client ID.
+// client_id = ""
+// // disabled disables OIDC ALB authorization.
+// disabled = false
+// // jwt_signer is the ARN of the ALB for JWT verification.
+// jwt_signer = "arn:aws:elasticloadbalancing:..."
+// }
+
+// For SharePoint deployments, configure SMTP email:
+// email {
+// enabled = true
+// from_address = "hermes@your-domain.com"
+// smtp_host = "smtp.your-domain.com"
+// smtp_port = 587
+// smtp_user = ""
+// smtp_pass = ""
+// }
+
+// For TLS (required for SharePoint/Office Add-in):
+// server {
+// addr = "0.0.0.0:8443"
+// tls_cert = "/path/to/cert.pem"
+// tls_key = "/path/to/key.pem"
+// }
diff --git a/go.mod b/go.mod
index dc78bcfb0..b40f4d215 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/hashicorp-forge/hermes
-go 1.18
+go 1.24.0
require (
github.com/algolia/algoliasearch-client-go/v3 v3.23.0
@@ -8,7 +8,7 @@ require (
github.com/cenkalti/backoff/v4 v4.1.3
github.com/forPelevin/gomoji v1.1.3
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
- github.com/golang-jwt/jwt/v5 v5.0.0
+ github.com/golang-jwt/jwt/v5 v5.3.0
github.com/hashicorp/go-hclog v1.2.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/hcl/v2 v2.11.1
@@ -17,7 +17,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/stretchr/testify v1.8.1
- golang.org/x/oauth2 v0.8.0
+ golang.org/x/oauth2 v0.32.0
google.golang.org/api v0.126.0
gopkg.in/DataDog/dd-trace-go.v1 v1.49.1
gorm.io/datatypes v1.1.0
@@ -26,8 +26,7 @@ require (
)
require (
- cloud.google.com/go/compute v1.19.3 // indirect
- cloud.google.com/go/compute/metadata v0.2.3 // indirect
+ cloud.google.com/go/compute/metadata v0.7.0 // indirect
github.com/DataDog/datadog-agent/pkg/obfuscate v0.46.0 // indirect
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.0-devel // indirect
github.com/DataDog/datadog-go/v5 v5.3.0 // indirect
@@ -42,30 +41,30 @@ require (
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
- github.com/cespare/xxhash/v2 v2.2.0 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.5.0-alpha.1 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
- github.com/golang/protobuf v1.5.3 // indirect
- github.com/google/go-cmp v0.6.0 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/google/go-cmp v0.7.0 // indirect
github.com/google/s2a-go v0.1.4 // indirect
- github.com/google/uuid v1.3.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
github.com/googleapis/gax-go/v2 v2.11.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
- github.com/jackc/pgconn v1.13.0 // indirect
+ github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
- github.com/jackc/pgproto3/v2 v2.3.1 // indirect
+ github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
- github.com/jackc/pgtype v1.13.0 // indirect
- github.com/jackc/pgx/v4 v4.17.2 // indirect
+ github.com/jackc/pgtype v1.14.0 // indirect
+ github.com/jackc/pgx/v4 v4.18.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
@@ -86,20 +85,19 @@ require (
go.uber.org/atomic v1.11.0 // indirect
go4.org/intern v0.0.0-20230525184215-6c62f75575cb // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect
- golang.org/x/crypto v0.33.0 // indirect
- golang.org/x/mod v0.17.0 // indirect
- golang.org/x/net v0.25.0 // indirect
- golang.org/x/sync v0.11.0 // indirect
- golang.org/x/sys v0.30.0 // indirect
- golang.org/x/text v0.22.0 // indirect
+ golang.org/x/crypto v0.45.0 // indirect
+ golang.org/x/mod v0.29.0 // indirect
+ golang.org/x/net v0.47.0 // indirect
+ golang.org/x/sync v0.18.0 // indirect
+ golang.org/x/sys v0.38.0 // indirect
+ golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.3.0 // indirect
- golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
+ golang.org/x/tools v0.38.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20230920183334-c177e329c48b // indirect
- google.golang.org/grpc v1.57.0 // indirect
- google.golang.org/protobuf v1.31.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
+ google.golang.org/grpc v1.76.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/mysql v1.4.5 // indirect
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a // indirect
diff --git a/go.sum b/go.sum
index f48ccc866..2e75ed886 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go/compute v1.19.3 h1:DcTwsFgGev/wV5+q8o2fzgcHOaac+DKGC91ZlvpsQds=
-cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI=
-cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
-cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
+cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.46.0 h1:rUNnUcHC4AlxoImuXmZeOfi6H80BDBHzeagWXWCVhnA=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.46.0/go.mod h1:e933RWa4kAWuHi5jpzEuOiULlv21HcCFEVIYegmaB5c=
@@ -54,8 +52,8 @@ github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
-github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -101,6 +99,10 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
@@ -111,10 +113,12 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
-github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
-github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
+github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
@@ -137,8 +141,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -147,8 +151,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
@@ -156,8 +160,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM=
github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w=
github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4=
@@ -192,8 +196,9 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
-github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
+github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
+github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
@@ -209,8 +214,9 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
-github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
+github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
@@ -219,14 +225,15 @@ github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCM
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
-github.com/jackc/pgtype v1.13.0 h1:XkIc7A+1BmZD19bB2NxrtjJweHxQ9agqvM+9URc68Cg=
-github.com/jackc/pgtype v1.13.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
+github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
+github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
-github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
+github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA=
+github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
@@ -246,6 +253,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -270,7 +278,9 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
+github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
+github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
github.com/mitchellh/cli v1.1.2 h1:PvH+lL2B7IQ101xQL63Of8yFS2y+aDlsFcsqNc+u/Kw=
github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
@@ -296,6 +306,7 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
+github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/outcaste-io/ristretto v0.2.1/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac=
github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOvUOL9w0=
github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac=
@@ -368,6 +379,18 @@ github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRK
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -405,8 +428,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
-golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -418,8 +441,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
+golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -444,12 +467,12 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
-golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
+golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
-golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
+golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
+golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -458,8 +481,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
+golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -498,8 +521,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -513,8 +536,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -534,8 +557,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
+golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -544,6 +567,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o=
google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -555,11 +580,11 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb h1:XFBgcDwm7irdHTbz4Zk2h7Mh+eis4nfJEFQFYzJzuIA=
-google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13 h1:U7+wNaVuSTaUqNvK2+osJ9ejEZxbjHHk8F2b6Hpx0AE=
-google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:RdyHbowztCGQySiCvQPgWQWgWhGnouTdCflKoDBt32U=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230920183334-c177e329c48b h1:tdhlmiMZNpc5p2W5qqKgRrOubaMZ3c85uG/GJtGgL98=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230920183334-c177e329c48b/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
+google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao=
+google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
+google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
@@ -568,8 +593,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
-google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
-google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
+google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
+google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -582,8 +607,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
-google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/DataDog/dd-trace-go.v1 v1.49.1 h1:fif50dazXYzwPN9fo3HL9B5WortmUTHxPvzP4pdl68o=
gopkg.in/DataDog/dd-trace-go.v1 v1.49.1/go.mod h1:Yp02hgfGPr9RXeVx4BgQa8uGKm6QD3DG7PohX2pg7bA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -611,7 +636,9 @@ gorm.io/driver/mysql v1.4.5/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8o
gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc=
gorm.io/driver/postgres v1.4.5/go.mod h1:GKNQYSJ14qvWkvPwXljMGehpKrhlDNsqYRr5HnYGncg=
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
+gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
+gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.3 h1:WL2ifUmzR/SLp85CSURAfybcHnGZ+yLSGSxgYXlFBHg=
diff --git a/hermes-plugin/.eslintrc.json b/hermes-plugin/.eslintrc.json
new file mode 100644
index 000000000..80f810a30
--- /dev/null
+++ b/hermes-plugin/.eslintrc.json
@@ -0,0 +1,8 @@
+{
+ "plugins": [
+ "office-addins"
+ ],
+ "extends": [
+ "plugin:office-addins/react"
+ ]
+}
\ No newline at end of file
diff --git a/hermes-plugin/.gitignore b/hermes-plugin/.gitignore
new file mode 100644
index 000000000..6565676ce
--- /dev/null
+++ b/hermes-plugin/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+.vscode
+dist/
\ No newline at end of file
diff --git a/hermes-plugin/.hintrc b/hermes-plugin/.hintrc
new file mode 100644
index 000000000..12c4655b4
--- /dev/null
+++ b/hermes-plugin/.hintrc
@@ -0,0 +1,8 @@
+{
+ "extends": [
+ "development"
+ ],
+ "hints": {
+ "typescript-config/strict": "off"
+ }
+}
\ No newline at end of file
diff --git a/hermes-plugin/addin.go b/hermes-plugin/addin.go
new file mode 100644
index 000000000..fe604e6ba
--- /dev/null
+++ b/hermes-plugin/addin.go
@@ -0,0 +1,51 @@
+package addin
+
+import (
+ "embed"
+ "io/fs"
+ "net/http"
+
+ "github.com/hashicorp/go-hclog"
+)
+
+//go:embed dist
+var addinContent embed.FS
+
+func AddinHandler(logger hclog.Logger) http.Handler {
+ if logger == nil {
+ logger = hclog.NewNullLogger()
+ }
+
+ logger.Info("initializing Word Add-in handler")
+ return http.StripPrefix("/addin/", addinHandler(http.FileServer(httpFileSystem()), logger))
+}
+
+func httpFileSystem() http.FileSystem {
+ return http.FS(fileSystem())
+}
+
+func fileSystem() fs.FS {
+ f, err := fs.Sub(addinContent, "dist")
+ if err != nil {
+ panic(err)
+ }
+
+ return f
+}
+
+// addinHandler is middleware for serving our single-page application.
+func addinHandler(next http.Handler, logger hclog.Logger) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
+ // Redirect empty paths to taskpane.html for add-in
+ if r.URL.Path == "" || r.URL.Path == "/" {
+ r.URL.Path = "/taskpane.html"
+ }
+
+ next.ServeHTTP(w, r)
+ })
+}
diff --git a/hermes-plugin/assets/document.png b/hermes-plugin/assets/document.png
new file mode 100644
index 000000000..5618a5023
Binary files /dev/null and b/hermes-plugin/assets/document.png differ
diff --git a/hermes-plugin/assets/hermes.png b/hermes-plugin/assets/hermes.png
new file mode 100644
index 000000000..4d6fb1aa6
Binary files /dev/null and b/hermes-plugin/assets/hermes.png differ
diff --git a/hermes-plugin/assets/icon-128.png b/hermes-plugin/assets/icon-128.png
new file mode 100644
index 000000000..37dfcd770
Binary files /dev/null and b/hermes-plugin/assets/icon-128.png differ
diff --git a/hermes-plugin/assets/icon-16.png b/hermes-plugin/assets/icon-16.png
new file mode 100644
index 000000000..b6509798a
Binary files /dev/null and b/hermes-plugin/assets/icon-16.png differ
diff --git a/hermes-plugin/assets/icon-32.png b/hermes-plugin/assets/icon-32.png
new file mode 100644
index 000000000..dcf56db70
Binary files /dev/null and b/hermes-plugin/assets/icon-32.png differ
diff --git a/hermes-plugin/assets/icon-64.png b/hermes-plugin/assets/icon-64.png
new file mode 100644
index 000000000..41051fce8
Binary files /dev/null and b/hermes-plugin/assets/icon-64.png differ
diff --git a/hermes-plugin/assets/icon-80.png b/hermes-plugin/assets/icon-80.png
new file mode 100644
index 000000000..5e63769d8
Binary files /dev/null and b/hermes-plugin/assets/icon-80.png differ
diff --git a/hermes-plugin/assets/logo-filled.png b/hermes-plugin/assets/logo-filled.png
new file mode 100644
index 000000000..5bf09cc24
Binary files /dev/null and b/hermes-plugin/assets/logo-filled.png differ
diff --git a/hermes-plugin/babel.config.json b/hermes-plugin/babel.config.json
new file mode 100644
index 000000000..ce63f5eca
--- /dev/null
+++ b/hermes-plugin/babel.config.json
@@ -0,0 +1,13 @@
+{
+ "presets": [
+ [
+ "@babel/preset-env",
+ {
+ "targets": {
+ "esmodules": false
+ }
+ }
+ ],
+ "@babel/preset-typescript"
+ ]
+}
diff --git a/hermes-plugin/manifest.json b/hermes-plugin/manifest.json
new file mode 100644
index 000000000..423a82aa7
--- /dev/null
+++ b/hermes-plugin/manifest.json
@@ -0,0 +1,143 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.17/MicrosoftTeams.schema.json",
+ "id": "f2b103f1-1ab1-4e1b-8f0b-072aa3d4e19d",
+ "manifestVersion": "1.17",
+ "version": "1.0.0",
+ "name": {
+ "short": "Contoso Task Pane Add-in",
+ "full": "Contoso Task Pane Add-in"
+ },
+ "description": {
+ "short": "A template to get started.",
+ "full": "This is the template to get started."
+ },
+ "developer": {
+ "name": "Contoso",
+ "websiteUrl": "https://www.contoso.com",
+ "privacyUrl": "https://www.contoso.com/privacy",
+ "termsOfUseUrl": "https://www.contoso.com/servicesagreement"
+ },
+ "icons": {
+ "outline": "assets/hermes.png",
+ "color": "assets/hermes.png"
+ },
+ "accentColor": "#230201",
+ "localizationInfo": {
+ "defaultLanguageTag": "en-us",
+ "additionalLanguages": []
+ },
+ "authorization": {
+ "permissions": {
+ "resourceSpecific": [
+ {
+ "name": "Mailbox.ReadWrite.User",
+ "type": "Delegated"
+ }
+ ]
+ }
+ },
+ "validDomains": ["contoso.com"],
+ "extensions": [
+ {
+ "requirements": {
+ "scopes": ["mail"],
+ "capabilities": [
+ { "name": "Mailbox", "minVersion": "1.3" }
+ ]
+ },
+ "runtimes": [
+ {
+ "requirements": {
+ "capabilities": [
+ { "name": "Mailbox", "minVersion": "1.3" }
+ ]
+ },
+ "id": "TaskPaneRuntime",
+ "type": "general",
+ "code": {
+ "page": "https://localhost:3000/taskpane.html"
+ },
+ "lifetime": "short",
+ "actions": [
+ {
+ "id": "TaskPaneRuntimeShow",
+ "type":"openPage",
+ "pinnable": false,
+ "view": "dashboard"
+ }
+ ]
+ },
+ {
+ "id": "CommandsRuntime",
+ "type": "general",
+ "code": {
+ "page": "https://localhost:3000/commands.html",
+ "script": "https://localhost:3000/commands.js"
+ },
+ "lifetime": "short",
+ "actions": [
+ {
+ "id": "action",
+ "type": "executeFunction"
+ }
+ ]
+ }
+ ],
+ "ribbons": [
+ {
+ "contexts": [
+ "mailCompose"
+ ],
+ "tabs": [
+ {
+ "builtInTabId": "TabDefault",
+ "groups": [
+ {
+ "id": "msgComposeGroup",
+ "label": "Contoso Add-in",
+ "icons": [
+ { "size": 16, "url": "https://localhost:3000/assets/icon-16.png" },
+ { "size": 32, "url": "https://localhost:3000/assets/icon-32.png" },
+ { "size": 80, "url": "https://localhost:3000/assets/icon-80.png" }
+ ],
+ "controls": [
+ {
+ "id": "msgComposeOpenPaneButton",
+ "type": "button",
+ "label": "Show Task Pane",
+ "icons": [
+ { "size": 16, "url": "https://localhost:3000/assets/icon-16.png" },
+ { "size": 32, "url": "https://localhost:3000/assets/icon-32.png" },
+ { "size": 80, "url": "https://localhost:3000/assets/icon-80.png" }
+ ],
+ "supertip": {
+ "title": "Show Task Pane",
+ "description": "Opens a task pane."
+ },
+ "actionId": "TaskPaneRuntimeShow"
+ },
+ {
+ "id": "ActionButton",
+ "type": "button",
+ "label": "Perform an action",
+ "icons": [
+ { "size": 16, "url": "https://localhost:3000/assets/icon-16.png" },
+ { "size": 32, "url": "https://localhost:3000/assets/icon-32.png" },
+ { "size": 80, "url": "https://localhost:3000/assets/icon-80.png" }
+ ],
+ "supertip": {
+ "title": "Perform an action",
+ "description": "Perform an action when clicked."
+ },
+ "actionId": "action"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
diff --git a/hermes-plugin/manifest.xml b/hermes-plugin/manifest.xml
new file mode 100644
index 000000000..5ed81230d
--- /dev/null
+++ b/hermes-plugin/manifest.xml
@@ -0,0 +1,87 @@
+
+
+ fbb6b523-1335-4cab-ab39-44116e88db77
+ 1.0.0.0
+ Hashicorp
+ en-US
+
+
+
+
+
+
+ https://www.hashicorp.com
+ https://localhost:8443
+
+
+
+
+
+
+
+
+ ReadWriteDocument
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ButtonId1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hermes-plugin/package-lock.json b/hermes-plugin/package-lock.json
new file mode 100644
index 000000000..35ba6bf15
--- /dev/null
+++ b/hermes-plugin/package-lock.json
@@ -0,0 +1,18800 @@
+{
+ "name": "office-addin-taskpane-react",
+ "version": "0.0.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "office-addin-taskpane-react",
+ "version": "0.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-components": "^9.72.4",
+ "@fluentui/react-icons": "^2.0.313",
+ "core-js": "^3.46.0",
+ "es6-promise": "^4.2.8",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "regenerator-runtime": "^0.14.1"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.28.5",
+ "@babel/preset-env": "^7.28.5",
+ "@babel/preset-typescript": "^7.28.5",
+ "@types/office-js": "^1.0.555",
+ "@types/office-runtime": "^1.0.35",
+ "@types/react": "^18.3.26",
+ "@types/react-dom": "^18.2.21",
+ "@types/webpack": "^5.28.5",
+ "acorn": "^8.11.3",
+ "babel-loader": "^9.1.3",
+ "copy-webpack-plugin": "^12.0.2",
+ "css-loader": "^7.1.2",
+ "eslint-plugin-office-addins": "^4.0.5",
+ "eslint-plugin-react": "^7.28.0",
+ "file-loader": "^6.2.0",
+ "html-loader": "^5.0.0",
+ "html-webpack-plugin": "^5.6.0",
+ "less": "^4.4.2",
+ "less-loader": "^12.2.0",
+ "mini-css-extract-plugin": "^2.9.4",
+ "office-addin-cli": "^2.0.5",
+ "office-addin-debugging": "^6.0.5",
+ "office-addin-dev-certs": "^2.0.5",
+ "office-addin-lint": "^3.0.5",
+ "office-addin-manifest": "^2.1.1",
+ "office-addin-prettier-config": "^2.0.1",
+ "os-browserify": "^0.3.0",
+ "process": "^0.11.10",
+ "source-map-loader": "^5.0.0",
+ "style-loader": "^4.0.0",
+ "ts-loader": "^9.5.1",
+ "typescript": "^5.9.3",
+ "webpack": "^5.102.1",
+ "webpack-cli": "^5.1.4",
+ "webpack-dev-server": "^5.2.2"
+ }
+ },
+ "node_modules/@apidevtools/json-schema-ref-parser": {
+ "version": "11.7.2",
+ "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz",
+ "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@jsdevtools/ono": "^7.1.3",
+ "@types/json-schema": "^7.0.15",
+ "js-yaml": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/philsturgeon"
+ }
+ },
+ "node_modules/@apidevtools/openapi-schemas": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
+ "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@apidevtools/swagger-methods": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
+ "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@apidevtools/swagger-parser": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.1.tgz",
+ "integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@apidevtools/json-schema-ref-parser": "11.7.2",
+ "@apidevtools/openapi-schemas": "^2.1.0",
+ "@apidevtools/swagger-methods": "^3.0.2",
+ "@jsdevtools/ono": "^7.1.3",
+ "ajv": "^8.17.1",
+ "ajv-draft-04": "^1.0.0",
+ "call-me-maybe": "^1.0.2"
+ },
+ "peerDependencies": {
+ "openapi-types": ">=7"
+ }
+ },
+ "node_modules/@apidevtools/swagger-parser/node_modules/ajv": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@apidevtools/swagger-parser/node_modules/ajv-draft-04": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
+ "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "peerDependencies": {
+ "ajv": "^8.5.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@apidevtools/swagger-parser/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@azure/abort-controller": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz",
+ "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/@azure/arm-appservice": {
+ "version": "13.0.3",
+ "resolved": "https://registry.npmjs.org/@azure/arm-appservice/-/arm-appservice-13.0.3.tgz",
+ "integrity": "sha512-Vu011o3/bikQNwtjouwmUJud+Z6Brcjij2D0omPWClRGg8i5gBfOYSpDkFGkHbhGlaky4fgvfkxD0uHGq34uYA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@azure/abort-controller": "^1.0.0",
+ "@azure/core-auth": "^1.3.0",
+ "@azure/core-client": "^1.6.1",
+ "@azure/core-lro": "^2.2.0",
+ "@azure/core-paging": "^1.2.0",
+ "@azure/core-rest-pipeline": "^1.8.0",
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@azure/arm-resources": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@azure/arm-resources/-/arm-resources-5.0.1.tgz",
+ "integrity": "sha512-JbZtIqfEulsIA0rC3zM7jfF4KkOnye9aKcaO/jJqxJRm/gM6lAjEv7sL4njW8D+35l50P1f+UuH5OqN+UKJqNA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@azure/abort-controller": "^1.0.0",
+ "@azure/core-auth": "^1.3.0",
+ "@azure/core-client": "^1.5.0",
+ "@azure/core-lro": "^2.2.0",
+ "@azure/core-paging": "^1.2.0",
+ "@azure/core-rest-pipeline": "^1.8.0",
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/@azure/arm-storage": {
+ "version": "17.2.1",
+ "resolved": "https://registry.npmjs.org/@azure/arm-storage/-/arm-storage-17.2.1.tgz",
+ "integrity": "sha512-J2jmTPv8ZraSHDTz9l2Bx8gNL3ktfDDWo2mxWfzarn64O9Fjhb+l85YWyubGy2xUdeGuZPKzvQLltGv8bSu8eQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@azure/abort-controller": "^1.0.0",
+ "@azure/core-auth": "^1.3.0",
+ "@azure/core-client": "^1.5.0",
+ "@azure/core-lro": "^2.2.0",
+ "@azure/core-paging": "^1.2.0",
+ "@azure/core-rest-pipeline": "^1.8.0",
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/@azure/arm-subscriptions": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@azure/arm-subscriptions/-/arm-subscriptions-5.1.0.tgz",
+ "integrity": "sha512-6BeOF2eQWNLq22ch7xP9RxYnPjtGev54OUCGggKOWoOvmesK7jUZbIyLk8JeXDT21PEl7iyYnxw78gxJ7zBxQw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@azure/abort-controller": "^1.0.0",
+ "@azure/core-auth": "^1.3.0",
+ "@azure/core-client": "^1.6.1",
+ "@azure/core-lro": "^2.2.0",
+ "@azure/core-paging": "^1.2.0",
+ "@azure/core-rest-pipeline": "^1.8.0",
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@azure/core-auth": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz",
+ "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@azure/abort-controller": "^2.1.2",
+ "@azure/core-util": "^1.13.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
+ "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-client": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.4.tgz",
+ "integrity": "sha512-f7IxTD15Qdux30s2qFARH+JxgwxWLG2Rlr4oSkPGuLWm+1p5y1+C04XGLA0vmX6EtqfutmjvpNmAfgwVIS5hpw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@azure/abort-controller": "^2.0.0",
+ "@azure/core-auth": "^1.4.0",
+ "@azure/core-rest-pipeline": "^1.20.0",
+ "@azure/core-tracing": "^1.0.0",
+ "@azure/core-util": "^1.6.1",
+ "@azure/logger": "^1.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-client/node_modules/@azure/abort-controller": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
+ "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-http-compat": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.0.tgz",
+ "integrity": "sha512-qLQujmUypBBG0gxHd0j6/Jdmul6ttl24c8WGiLXIk7IHXdBlfoBqW27hyz3Xn6xbfdyVSarl1Ttbk0AwnZBYCw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@azure/abort-controller": "^2.0.0",
+ "@azure/core-client": "^1.3.0",
+ "@azure/core-rest-pipeline": "^1.20.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-http-compat/node_modules/@azure/abort-controller": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
+ "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-lro": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz",
+ "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@azure/abort-controller": "^2.0.0",
+ "@azure/core-util": "^1.2.0",
+ "@azure/logger": "^1.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-lro/node_modules/@azure/abort-controller": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
+ "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-paging": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz",
+ "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-rest-pipeline": {
+ "version": "1.23.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz",
+ "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "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.4",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
+ "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-tracing": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz",
+ "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@azure/core-util": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz",
+ "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@azure/abort-controller": "^2.1.2",
+ "@typespec/ts-http-runtime": "^0.3.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@azure/core-util/node_modules/@azure/abort-controller": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
+ "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-xml": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.4.5.tgz",
+ "integrity": "sha512-gT4H8mTaSXRz7eGTuQyq1aIJnJqeXzpOe9Ay7Z3FrCouer14CbV3VzjnJrNrQfbBpGBLO9oy8BmrY75A0p53cA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "fast-xml-parser": "^5.0.7",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/identity": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.10.1.tgz",
+ "integrity": "sha512-YM/z6RxRtFlXUH2egAYF/FDPes+MUE6ZoknjEdaq7ebJMMNUzn9zCJ3bd2ZZZlkP0r1xKa88kolhFH/FGV7JnA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "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"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/identity/node_modules/@azure/abort-controller": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
+ "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/identity/node_modules/@azure/msal-node": {
+ "version": "3.8.8",
+ "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.8.tgz",
+ "integrity": "sha512-+f1VrJH1iI517t4zgmuhqORja0bL6LDQXfBqkjuMmfTYXTQQnh1EvwwxO3UbKLT05N0obF72SRHFrC1RBDv5Gg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@azure/msal-common": "15.15.0",
+ "jsonwebtoken": "^9.0.0",
+ "uuid": "^8.3.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@azure/identity/node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@azure/identity/node_modules/open": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
+ "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "default-browser": "^5.2.1",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "wsl-utils": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@azure/logger": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz",
+ "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@typespec/ts-http-runtime": "^0.3.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@azure/msal-browser": {
+ "version": "4.29.0",
+ "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.29.0.tgz",
+ "integrity": "sha512-/f3eHkSNUTl6DLQHm+bKecjBKcRQxbd/XLx8lvSYp8Nl/HRyPuIPOijt9Dt0sH50/SxOwQ62RnFCmFlGK+bR/w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@azure/msal-common": "15.15.0"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@azure/msal-common": {
+ "version": "15.15.0",
+ "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.15.0.tgz",
+ "integrity": "sha512-/n+bN0AKlVa+AOcETkJSKj38+bvFs78BaP4rNtv3MJCmPH0YrHiskMRe74OhyZ5DZjGISlFyxqvf9/4QVEi2tw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@azure/msal-node": {
+ "version": "2.16.3",
+ "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.3.tgz",
+ "integrity": "sha512-CO+SE4weOsfJf+C5LM8argzvotrXw252/ZU6SM2Tz63fEblhH1uuVaaO4ISYFuN4Q6BhTo7I3qIdi8ydUQCqhw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@azure/msal-common": "14.16.1",
+ "jsonwebtoken": "^9.0.0",
+ "uuid": "^8.3.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@azure/msal-node/node_modules/@azure/msal-common": {
+ "version": "14.16.1",
+ "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.1.tgz",
+ "integrity": "sha512-nyxsA6NA4SVKh5YyRpbSXiMr7oQbwark7JU9LMeg6tJYTSPyAGkdx61wPT4gyxZfxlSxMMEyAsWaubBlNyIa1w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@azure/storage-blob": {
+ "version": "12.27.0",
+ "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.27.0.tgz",
+ "integrity": "sha512-IQjj9RIzAKatmNca3D6bT0qJ+Pkox1WZGOg2esJF2YLHb45pQKOwGPIAV+w3rfgkj7zV3RMxpn/c6iftzSOZJQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@azure/abort-controller": "^2.1.2",
+ "@azure/core-auth": "^1.4.0",
+ "@azure/core-client": "^1.6.2",
+ "@azure/core-http-compat": "^2.0.0",
+ "@azure/core-lro": "^2.2.0",
+ "@azure/core-paging": "^1.1.1",
+ "@azure/core-rest-pipeline": "^1.10.1",
+ "@azure/core-tracing": "^1.1.2",
+ "@azure/core-util": "^1.6.1",
+ "@azure/core-xml": "^1.4.3",
+ "@azure/logger": "^1.0.0",
+ "events": "^3.0.0",
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/storage-blob/node_modules/@azure/abort-controller": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
+ "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
+ "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz",
+ "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-member-expression-to-functions": "^7.28.5",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.28.6",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/traverse": "^7.28.6",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz",
+ "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "regexpu-core": "^6.3.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-define-polyfill-provider": {
+ "version": "0.6.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.7.tgz",
+ "integrity": "sha512-6Fqi8MtQ/PweQ9xvux65emkLQ83uB+qAVtfHkC9UodyHMIZdxNI01HjLCLUtybElp2KY2XNE0nOgyP1E1vXw9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "debug": "^4.4.3",
+ "lodash.debounce": "^4.0.8",
+ "resolve": "^1.22.11"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz",
+ "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
+ "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-remap-async-to-generator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz",
+ "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-wrap-function": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz",
+ "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^7.28.5",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
+ "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-wrap-function": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz",
+ "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz",
+ "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz",
+ "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz",
+ "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz",
+ "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/plugin-transform-optional-chaining": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.13.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz",
+ "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-private-property-in-object": {
+ "version": "7.21.0-placeholder-for-preset-env.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
+ "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-assertions": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz",
+ "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-attributes": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz",
+ "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz",
+ "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz",
+ "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-unicode-sets-regex": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
+ "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-arrow-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz",
+ "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-generator-functions": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz",
+ "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-remap-async-to-generator": "^7.27.1",
+ "@babel/traverse": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-to-generator": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz",
+ "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-remap-async-to-generator": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoped-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz",
+ "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoping": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz",
+ "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-properties": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz",
+ "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-static-block": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz",
+ "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.12.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-classes": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz",
+ "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-replace-supers": "^7.28.6",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-computed-properties": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz",
+ "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/template": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-destructuring": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz",
+ "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dotall-regex": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz",
+ "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.28.5",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-keys": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz",
+ "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz",
+ "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.28.5",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dynamic-import": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz",
+ "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-explicit-resource-management": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz",
+ "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/plugin-transform-destructuring": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-exponentiation-operator": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz",
+ "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-export-namespace-from": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz",
+ "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-for-of": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz",
+ "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-function-name": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz",
+ "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-json-strings": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz",
+ "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz",
+ "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-logical-assignment-operators": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz",
+ "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-member-expression-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz",
+ "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-amd": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz",
+ "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz",
+ "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-systemjs": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz",
+ "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-umd": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz",
+ "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz",
+ "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.28.5",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-new-target": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz",
+ "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz",
+ "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-numeric-separator": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz",
+ "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-rest-spread": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz",
+ "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/plugin-transform-destructuring": "^7.28.5",
+ "@babel/plugin-transform-parameters": "^7.27.7",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-super": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz",
+ "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-catch-binding": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz",
+ "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-chaining": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz",
+ "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-parameters": {
+ "version": "7.27.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz",
+ "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-methods": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz",
+ "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-property-in-object": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz",
+ "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-create-class-features-plugin": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-property-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz",
+ "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-regenerator": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz",
+ "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-regexp-modifiers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz",
+ "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.28.5",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-reserved-words": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz",
+ "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-shorthand-properties": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz",
+ "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-spread": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz",
+ "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-sticky-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz",
+ "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-template-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz",
+ "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typeof-symbol": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz",
+ "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typescript": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz",
+ "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-create-class-features-plugin": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/plugin-syntax-typescript": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-escapes": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz",
+ "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-property-regex": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz",
+ "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.28.5",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz",
+ "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-sets-regex": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz",
+ "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.28.5",
+ "@babel/helper-plugin-utils": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/preset-env": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz",
+ "integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5",
+ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1",
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1",
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1",
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6",
+ "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
+ "@babel/plugin-syntax-import-assertions": "^7.28.6",
+ "@babel/plugin-syntax-import-attributes": "^7.28.6",
+ "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
+ "@babel/plugin-transform-arrow-functions": "^7.27.1",
+ "@babel/plugin-transform-async-generator-functions": "^7.29.0",
+ "@babel/plugin-transform-async-to-generator": "^7.28.6",
+ "@babel/plugin-transform-block-scoped-functions": "^7.27.1",
+ "@babel/plugin-transform-block-scoping": "^7.28.6",
+ "@babel/plugin-transform-class-properties": "^7.28.6",
+ "@babel/plugin-transform-class-static-block": "^7.28.6",
+ "@babel/plugin-transform-classes": "^7.28.6",
+ "@babel/plugin-transform-computed-properties": "^7.28.6",
+ "@babel/plugin-transform-destructuring": "^7.28.5",
+ "@babel/plugin-transform-dotall-regex": "^7.28.6",
+ "@babel/plugin-transform-duplicate-keys": "^7.27.1",
+ "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0",
+ "@babel/plugin-transform-dynamic-import": "^7.27.1",
+ "@babel/plugin-transform-explicit-resource-management": "^7.28.6",
+ "@babel/plugin-transform-exponentiation-operator": "^7.28.6",
+ "@babel/plugin-transform-export-namespace-from": "^7.27.1",
+ "@babel/plugin-transform-for-of": "^7.27.1",
+ "@babel/plugin-transform-function-name": "^7.27.1",
+ "@babel/plugin-transform-json-strings": "^7.28.6",
+ "@babel/plugin-transform-literals": "^7.27.1",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.28.6",
+ "@babel/plugin-transform-member-expression-literals": "^7.27.1",
+ "@babel/plugin-transform-modules-amd": "^7.27.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.28.6",
+ "@babel/plugin-transform-modules-systemjs": "^7.29.0",
+ "@babel/plugin-transform-modules-umd": "^7.27.1",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0",
+ "@babel/plugin-transform-new-target": "^7.27.1",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6",
+ "@babel/plugin-transform-numeric-separator": "^7.28.6",
+ "@babel/plugin-transform-object-rest-spread": "^7.28.6",
+ "@babel/plugin-transform-object-super": "^7.27.1",
+ "@babel/plugin-transform-optional-catch-binding": "^7.28.6",
+ "@babel/plugin-transform-optional-chaining": "^7.28.6",
+ "@babel/plugin-transform-parameters": "^7.27.7",
+ "@babel/plugin-transform-private-methods": "^7.28.6",
+ "@babel/plugin-transform-private-property-in-object": "^7.28.6",
+ "@babel/plugin-transform-property-literals": "^7.27.1",
+ "@babel/plugin-transform-regenerator": "^7.29.0",
+ "@babel/plugin-transform-regexp-modifiers": "^7.28.6",
+ "@babel/plugin-transform-reserved-words": "^7.27.1",
+ "@babel/plugin-transform-shorthand-properties": "^7.27.1",
+ "@babel/plugin-transform-spread": "^7.28.6",
+ "@babel/plugin-transform-sticky-regex": "^7.27.1",
+ "@babel/plugin-transform-template-literals": "^7.27.1",
+ "@babel/plugin-transform-typeof-symbol": "^7.27.1",
+ "@babel/plugin-transform-unicode-escapes": "^7.27.1",
+ "@babel/plugin-transform-unicode-property-regex": "^7.28.6",
+ "@babel/plugin-transform-unicode-regex": "^7.27.1",
+ "@babel/plugin-transform-unicode-sets-regex": "^7.28.6",
+ "@babel/preset-modules": "0.1.6-no-external-plugins",
+ "babel-plugin-polyfill-corejs2": "^0.4.15",
+ "babel-plugin-polyfill-corejs3": "^0.14.0",
+ "babel-plugin-polyfill-regenerator": "^0.6.6",
+ "core-js-compat": "^3.48.0",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-modules": {
+ "version": "0.1.6-no-external-plugins",
+ "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
+ "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/types": "^7.4.4",
+ "esutils": "^2.0.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/preset-typescript": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz",
+ "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-syntax-jsx": "^7.27.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.27.1",
+ "@babel/plugin-transform-typescript": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
+ "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@colors/colors": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
+ "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/@ctrl/tinycolor": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+ "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@discoveryjs/json-ext": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
+ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+ "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+ "license": "MIT"
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@exodus/schemasafe": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz",
+ "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@feathersjs/hooks": {
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/@feathersjs/hooks/-/hooks-0.6.5.tgz",
+ "integrity": "sha512-WtcEoG/imdHRvC3vofGi/OcgH+cjHHhO0AfEeTlsnrKLjVKKBXV6aoIrB2nHZPpE7iW5sA7AZMR6bPD8ytxN+w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/devtools": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/devtools/-/devtools-0.2.3.tgz",
+ "integrity": "sha512-ZTcxTvgo9CRlP7vJV62yCxdqmahHTGpSTi5QaTDgGoyQq0OyjaVZhUhXv/qdkQFOI3Sxlfmz0XGG4HaZMsDf8Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@floating-ui/dom": "^1.0.0"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT"
+ },
+ "node_modules/@fluentui/keyboard-keys": {
+ "version": "9.0.8",
+ "resolved": "https://registry.npmjs.org/@fluentui/keyboard-keys/-/keyboard-keys-9.0.8.tgz",
+ "integrity": "sha512-iUSJUUHAyTosnXK8O2Ilbfxma+ZyZPMua5vB028Ys96z80v+LFwntoehlFsdH3rMuPsA8GaC1RE7LMezwPBPdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@swc/helpers": "^0.5.1"
+ }
+ },
+ "node_modules/@fluentui/priority-overflow": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/@fluentui/priority-overflow/-/priority-overflow-9.3.0.tgz",
+ "integrity": "sha512-yaBC0R4e+4ZlCWDulB5S+xBrlnLwfzdg68GaarCqQO8OHjLg7Ah05xTj7PsAYcoHeEg/9vYeBwGXBpRO8+Tjqw==",
+ "license": "MIT",
+ "dependencies": {
+ "@swc/helpers": "^0.5.1"
+ }
+ },
+ "node_modules/@fluentui/react-accordion": {
+ "version": "9.9.2",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-accordion/-/react-accordion-9.9.2.tgz",
+ "integrity": "sha512-Mmi5nVKfQrBiBiD1JPVtCmIMrR1CpCy8hsWZLwv/pHt+uHHyW9HyrPXwiOitj3ookA5ec1kXyl34BN8RUi7DGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-motion": "^9.13.0",
+ "@fluentui/react-motion-components-preview": "^0.15.2",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-alert": {
+ "version": "9.0.0-beta.135",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-alert/-/react-alert-9.0.0-beta.135.tgz",
+ "integrity": "sha512-Qkr89e6tl4q0fhzfx9Wzb3ltiqbFtZj7AhT+CHZdW0I6KtpfGmJnvzaqvz0KXMdrKROTgvkA1Ny3Epf9ortc0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-avatar": "^9.10.2",
+ "@fluentui/react-button": "^9.8.2",
+ "@fluentui/react-icons": "^2.0.239",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-aria": {
+ "version": "9.17.10",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-aria/-/react-aria-9.17.10.tgz",
+ "integrity": "sha512-KqS2XcdN84XsgVG4fAESyOBfixN7zbObWfQVLNZ2gZrp2b1hPGVYfQ6J4WOO0vXMKYp0rre/QMOgDm6/srL0XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-avatar": {
+ "version": "9.10.2",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-avatar/-/react-avatar-9.10.2.tgz",
+ "integrity": "sha512-0qy3U1S80c2Z0A8O/3Ko8XmG4d/NCof1XZ1jclbneKLDT0PeoX3BUlDDgCalOEwb0s1x6TjLabam5FtY4E30cg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-badge": "^9.4.15",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-popover": "^9.14.0",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-tooltip": "^9.9.3",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-badge": {
+ "version": "9.4.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-badge/-/react-badge-9.4.15.tgz",
+ "integrity": "sha512-KgFUJHBHP76vE3EDuPg/ml7lGqxs9zJ634e+vtxn8D7ghCZ6h9P6A0WbmgsPcN6MZoBZYLzzYT3OJ6Vmu3BM8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-breadcrumb": {
+ "version": "9.3.17",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-breadcrumb/-/react-breadcrumb-9.3.17.tgz",
+ "integrity": "sha512-POnwCFyvXabq7lNtJRslASNkrm0iRoXpnrWwh0LyBTFZRDiGDKaV18Bpk0UiuQNTUurVQiH513164XKHIP+d7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-button": "^9.8.2",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-link": "^9.7.4",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-button": {
+ "version": "9.8.2",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-button/-/react-button-9.8.2.tgz",
+ "integrity": "sha512-T2xBn6s6DRNH17Y+kLO+uEOaRe89Q20WP1Rs6OzC45cSpOGc+q9ogbPbYBqU7Tr1fur+Xd8LRHxdQJ3j5ufbdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-card": {
+ "version": "9.5.11",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-card/-/react-card-9.5.11.tgz",
+ "integrity": "sha512-0W3BmDER/aKx+7+ttGy+M6LO09DW7DkJlO8F0x13L1ssOVxJ0OhyhSGiCF0cJliOK1tiGPveYf6+X2xMq2MT6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-text": "^9.6.15",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-carousel": {
+ "version": "9.9.4",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-carousel/-/react-carousel-9.9.4.tgz",
+ "integrity": "sha512-mzGZUOe3tB+86/WPsQTgppYRoqeM1vl8LswISl7FVrxk7PREnzZLW4BEZnFOKuP29dThcjJNzF0mM/5kq1lKug==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-button": "^9.8.2",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-tooltip": "^9.9.3",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1",
+ "embla-carousel": "^8.5.1",
+ "embla-carousel-autoplay": "^8.5.1",
+ "embla-carousel-fade": "^8.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-checkbox": {
+ "version": "9.5.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-checkbox/-/react-checkbox-9.5.15.tgz",
+ "integrity": "sha512-ZXvuZo8HvBLvsd74foI/p/YkxKRmruQLhleeQRMqyNKMbytFcYZ8rHmAN492tNMjmWxGIfZHv5Oh7Ds6poNmJg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-label": "^9.3.15",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-color-picker": {
+ "version": "9.2.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-color-picker/-/react-color-picker-9.2.15.tgz",
+ "integrity": "sha512-RMmawl7g4gUYLuTQG2QwCcR9fGC+vDD+snsBlXtObpj/cKpeDmYif46g88pYv86jeIXY1zsjINmLpELmz+uFmw==",
+ "license": "MIT",
+ "dependencies": {
+ "@ctrl/tinycolor": "^3.3.4",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-combobox": {
+ "version": "9.16.17",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-combobox/-/react-combobox-9.16.17.tgz",
+ "integrity": "sha512-/Q2incmVrKF4sKqtrkEntGvjkuddr5mHfV9K5ziM+aR9ZczMwFuFVUFbBTcJlmtnsYf8CLm4E+r7oBWgXy2TVA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-field": "^9.4.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-portal": "^9.8.11",
+ "@fluentui/react-positioning": "^9.22.0",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-components": {
+ "version": "9.73.2",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-components/-/react-components-9.73.2.tgz",
+ "integrity": "sha512-PZ9y66NLgUowuaZs9U75WtaxPXUTvjSUf/PHYABSV1Hl4DPVRM3koCQCPPxQEPlPhzHnbNqAK//5WZjPlmxBdA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-accordion": "^9.9.2",
+ "@fluentui/react-alert": "9.0.0-beta.135",
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-avatar": "^9.10.2",
+ "@fluentui/react-badge": "^9.4.15",
+ "@fluentui/react-breadcrumb": "^9.3.17",
+ "@fluentui/react-button": "^9.8.2",
+ "@fluentui/react-card": "^9.5.11",
+ "@fluentui/react-carousel": "^9.9.4",
+ "@fluentui/react-checkbox": "^9.5.15",
+ "@fluentui/react-color-picker": "^9.2.15",
+ "@fluentui/react-combobox": "^9.16.17",
+ "@fluentui/react-dialog": "^9.17.2",
+ "@fluentui/react-divider": "^9.6.2",
+ "@fluentui/react-drawer": "^9.11.5",
+ "@fluentui/react-field": "^9.4.15",
+ "@fluentui/react-image": "^9.3.15",
+ "@fluentui/react-infobutton": "9.0.0-beta.112",
+ "@fluentui/react-infolabel": "^9.4.17",
+ "@fluentui/react-input": "^9.7.15",
+ "@fluentui/react-label": "^9.3.15",
+ "@fluentui/react-link": "^9.7.4",
+ "@fluentui/react-list": "^9.6.10",
+ "@fluentui/react-menu": "^9.22.0",
+ "@fluentui/react-message-bar": "^9.6.20",
+ "@fluentui/react-motion": "^9.13.0",
+ "@fluentui/react-nav": "^9.3.20",
+ "@fluentui/react-overflow": "^9.7.1",
+ "@fluentui/react-persona": "^9.6.2",
+ "@fluentui/react-popover": "^9.14.0",
+ "@fluentui/react-portal": "^9.8.11",
+ "@fluentui/react-positioning": "^9.22.0",
+ "@fluentui/react-progress": "^9.4.15",
+ "@fluentui/react-provider": "^9.22.15",
+ "@fluentui/react-radio": "^9.5.15",
+ "@fluentui/react-rating": "^9.3.15",
+ "@fluentui/react-search": "^9.3.15",
+ "@fluentui/react-select": "^9.4.15",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-skeleton": "^9.4.15",
+ "@fluentui/react-slider": "^9.5.15",
+ "@fluentui/react-spinbutton": "^9.5.15",
+ "@fluentui/react-spinner": "^9.7.15",
+ "@fluentui/react-swatch-picker": "^9.4.15",
+ "@fluentui/react-switch": "^9.6.0",
+ "@fluentui/react-table": "^9.19.10",
+ "@fluentui/react-tabs": "^9.11.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-tag-picker": "^9.8.1",
+ "@fluentui/react-tags": "^9.7.17",
+ "@fluentui/react-teaching-popover": "^9.6.18",
+ "@fluentui/react-text": "^9.6.15",
+ "@fluentui/react-textarea": "^9.6.15",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-toast": "^9.7.14",
+ "@fluentui/react-toolbar": "^9.7.3",
+ "@fluentui/react-tooltip": "^9.9.3",
+ "@fluentui/react-tree": "^9.15.12",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@fluentui/react-virtualizer": "9.0.0-alpha.111",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-context-selector": {
+ "version": "9.2.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-context-selector/-/react-context-selector-9.2.15.tgz",
+ "integrity": "sha512-QymBntFLJNZ9VfTOaBn2ApUSSSC5UuDW8ZcgPJPA+06XEFH+U9Zny2d9QAg1xYNYwIGWahWGQ+7ATOuLxtB8Jw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-utilities": "^9.26.2",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0",
+ "scheduler": ">=0.19.0"
+ }
+ },
+ "node_modules/@fluentui/react-dialog": {
+ "version": "9.17.2",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-dialog/-/react-dialog-9.17.2.tgz",
+ "integrity": "sha512-mZdKylSvh2fRf0e3wMX3ZNccb9DahsOE7A5Y9LG97ghYvndMBVG2YwScIzUFVvLS206ari6HMOl0lC5JRB1bKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-motion": "^9.13.0",
+ "@fluentui/react-motion-components-preview": "^0.15.2",
+ "@fluentui/react-portal": "^9.8.11",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-divider": {
+ "version": "9.6.2",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-divider/-/react-divider-9.6.2.tgz",
+ "integrity": "sha512-jfHlpSoJys78STe/SSjqdcn+W7QjEO1xCGiedWp/MdTBi3pH5vEeYbt2u8RU+zP32IF0Clta85KsUEEG0DYELQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-drawer": {
+ "version": "9.11.5",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-drawer/-/react-drawer-9.11.5.tgz",
+ "integrity": "sha512-eoZY+jKZwbJo1PUsb7Ico7u/8aObHL4BhPP6hd+HHNzB7seTpN7rLd0DpASLZsxJUy5yvch4QF2TrjOu6V8kRA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-dialog": "^9.17.2",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-motion": "^9.13.0",
+ "@fluentui/react-motion-components-preview": "^0.15.2",
+ "@fluentui/react-portal": "^9.8.11",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-field": {
+ "version": "9.4.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-field/-/react-field-9.4.15.tgz",
+ "integrity": "sha512-hKdl+ncnT1C3vX8zQ4LqNGUk6TiatDOAW49dr18RkONcScg2staAaDme977Iozj6+AW7AJsDfkNxq/lwHhe/pg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-label": "^9.3.15",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-icons": {
+ "version": "2.0.320",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.320.tgz",
+ "integrity": "sha512-NU4gErPeaTD/T6Z9g3Uvp898lIFS6fDLr3++vpT8pcI4Ds0fZqQdrwNi3dF0R/SVws8DXQaRYiGlPHxszo4J4g==",
+ "license": "MIT",
+ "dependencies": {
+ "@griffel/react": "^1.0.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-image": {
+ "version": "9.3.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-image/-/react-image-9.3.15.tgz",
+ "integrity": "sha512-k8ftGUc5G3Hj5W9nOFnWEKZ1oXmoZE3EvAEdyI6Cn9R8E6zW2PZ1+cug0p6rr01JCDG8kbry1LAITcObMrlPdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-infobutton": {
+ "version": "9.0.0-beta.112",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-infobutton/-/react-infobutton-9.0.0-beta.112.tgz",
+ "integrity": "sha512-Fhqoc6b1MQtHW+Mm5sBhfa5ZrRdOV4azuUa5WyBvwD4Ozq/z2pBOC/wi/A/WCjKMnGoMlQ2CggoLaMhQmenzAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-icons": "^2.0.237",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-label": "^9.3.15",
+ "@fluentui/react-popover": "^9.14.0",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-infolabel": {
+ "version": "9.4.17",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-infolabel/-/react-infolabel-9.4.17.tgz",
+ "integrity": "sha512-zLw52jn2wAuEKWFzaNj3aKhuB4BAEI8LqblryCg0LKPKHcv/z9d9RllCqcVz+ngdK1tQGtCIPH/wxNlZXx/I3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-label": "^9.3.15",
+ "@fluentui/react-popover": "^9.14.0",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0 <20.0.0",
+ "@types/react-dom": ">=16.8.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-input": {
+ "version": "9.7.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-input/-/react-input-9.7.15.tgz",
+ "integrity": "sha512-pzGF1mOenV03RhIy+km8GrqCfahDSLm6YG7wxpE1m2q2fY73cyLZPuMbK7Kz27oaoyUI37v4Pa4612zl12228A==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.15",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-jsx-runtime": {
+ "version": "9.4.1",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-jsx-runtime/-/react-jsx-runtime-9.4.1.tgz",
+ "integrity": "sha512-ZodSm7jRa4kaLKDi+emfHFMP/IDnYwFQQAI2BdtKbVrvfwvzPRprGcnTgivnqKBT1ROvKOCY2ddz7+yZzesnNw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-utilities": "^9.26.2",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-label": {
+ "version": "9.3.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-label/-/react-label-9.3.15.tgz",
+ "integrity": "sha512-ycmaQwC4tavA8WeDfgcay1Ywu/4goHq1NOeVxkyzWTPGA7rs+tdCgdZBQZLAsBK2XFaZiHs7l+KG9r1oIRKolA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-link": {
+ "version": "9.7.4",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-link/-/react-link-9.7.4.tgz",
+ "integrity": "sha512-ILKFpo/QH1SRsLN9gopAyZT/b/xsGcdO4JxthEeuTRvpLD6gImvRplum8ySIlbTskVVzog6038bHUSYLMdN7OA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-list": {
+ "version": "9.6.10",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-list/-/react-list-9.6.10.tgz",
+ "integrity": "sha512-NTAWYL8Z4h9N9N1b39H9xqfTyhfGkhlNTc3higpoIS/6jgEf6GMNF8iwvAyhB++hFdjBd27c+NbDl4MCwHhGiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-checkbox": "^9.5.15",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0 <20.0.0",
+ "@types/react-dom": ">=16.8.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-menu": {
+ "version": "9.22.0",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-menu/-/react-menu-9.22.0.tgz",
+ "integrity": "sha512-RPZvqHsxMDEArsz80mJabs1fVGPlCrhMntzM/wt3Bga+fyPv4yEuDdN5FB8JqUpIAjRZneiW0RLC0Mr3WqmatA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-motion": "^9.13.0",
+ "@fluentui/react-motion-components-preview": "^0.15.2",
+ "@fluentui/react-portal": "^9.8.11",
+ "@fluentui/react-positioning": "^9.22.0",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-message-bar": {
+ "version": "9.6.20",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-message-bar/-/react-message-bar-9.6.20.tgz",
+ "integrity": "sha512-d0u+ZPYhAvm+dQSyLECR0vk4Q5UwomI/3azNWduthqU9eQXrgaTDmJkJIeF/bu0jOci3AaMwImbmZqNMSQBmGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-button": "^9.8.2",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-link": "^9.7.4",
+ "@fluentui/react-motion": "^9.13.0",
+ "@fluentui/react-motion-components-preview": "^0.15.2",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0 <20.0.0",
+ "@types/react-dom": ">=16.8.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-motion": {
+ "version": "9.13.0",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-motion/-/react-motion-9.13.0.tgz",
+ "integrity": "sha512-YdOpW6e7qfvzoWKcqh8hReCqwYEoiEmNBcCprGaupKjWOi9jBbF/JESM1AHI9nOjPd8aY90WUG2+ahvrqfL9LA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0 <20.0.0",
+ "@types/react-dom": ">=16.8.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-motion-components-preview": {
+ "version": "0.15.2",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-motion-components-preview/-/react-motion-components-preview-0.15.2.tgz",
+ "integrity": "sha512-KqHRV8lLmVwOWiHBdpUFA+TwMbuYu9cyzNvmhbMFLVKzZyr3MPgN+97Tf+6QYPf22o99SMT0BPySDv/HiNYanA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-motion": "*",
+ "@fluentui/react-utilities": "*",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-nav": {
+ "version": "9.3.20",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-nav/-/react-nav-9.3.20.tgz",
+ "integrity": "sha512-YIObOcR92Nz4OUePrDhRdLQ5m9ph0y+U7U9NYgE/XFrLtWl+uqUS7u36m3NJl9QGgZVpUHO4nbNjizGLkncCCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-button": "^9.8.2",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-divider": "^9.6.2",
+ "@fluentui/react-drawer": "^9.11.5",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-motion": "^9.13.0",
+ "@fluentui/react-motion-components-preview": "^0.15.2",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-tooltip": "^9.9.3",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-overflow": {
+ "version": "9.7.1",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-overflow/-/react-overflow-9.7.1.tgz",
+ "integrity": "sha512-Ml1GlcLrAUv31d9WN15WGOZv32gzDtZD5Mp1MOQ3ichDfTtxrswIch7MDzZ8hLMGf/7Y2IzBpV8iFR1XdSrGBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/priority-overflow": "^9.3.0",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-persona": {
+ "version": "9.6.2",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-persona/-/react-persona-9.6.2.tgz",
+ "integrity": "sha512-60kOmljlYjUiySWDN1bZh1FB4C7jbJS2dobtBJQh5agnKg34p3egO+6MwsBHRcwaGhVMh4T8XcbE6t2hw+iqyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-avatar": "^9.10.2",
+ "@fluentui/react-badge": "^9.4.15",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-popover": {
+ "version": "9.14.0",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-popover/-/react-popover-9.14.0.tgz",
+ "integrity": "sha512-XrZlSfSYhA12j5bna4Sq8N/If2vul7gl8woVrN8U3iQUjdaHB6OAMZ/WMNUdMm35Z+4e4rHClAZxU2dUsbHrmw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-motion": "^9.13.0",
+ "@fluentui/react-motion-components-preview": "^0.15.2",
+ "@fluentui/react-portal": "^9.8.11",
+ "@fluentui/react-positioning": "^9.22.0",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-portal": {
+ "version": "9.8.11",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-portal/-/react-portal-9.8.11.tgz",
+ "integrity": "sha512-2eg4MdW7e2UGRYWPg05GCytAjWYNd55YOP9+iUDINoQwwto9oeFTtZRyn08HYw37cSNqoH24qGz/VBctzTkqDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-positioning": {
+ "version": "9.22.0",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-positioning/-/react-positioning-9.22.0.tgz",
+ "integrity": "sha512-i3DLC4jd4MoYSZMYLKQNUTpkjKAJ0snIcihvkrjt2jpvv34CifKJhqVtjFQ470pRW4XNx/pBBX07vdXpA3poxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/devtools": "^0.2.3",
+ "@floating-ui/dom": "^1.6.12",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1",
+ "use-sync-external-store": "^1.2.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-progress": {
+ "version": "9.4.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-progress/-/react-progress-9.4.15.tgz",
+ "integrity": "sha512-U2dqtEtov7FoeIGSAEqdFV2O2pjx3gFzbCWpPkpuLCshOSGjCPPeLV3iiTGP1WFrGCcpwFoz5O2YmsnA3wf4oQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.15",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-provider": {
+ "version": "9.22.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-provider/-/react-provider-9.22.15.tgz",
+ "integrity": "sha512-a+ImgL9DOlylDM4UYPnxQTA3yXxbVj+O0iNEyTZ6fMzdMsHzpALU4GAq6tOyW4L7RaQtRBmNpVfwTCEKpqaTJQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/core": "^1.16.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-radio": {
+ "version": "9.5.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-radio/-/react-radio-9.5.15.tgz",
+ "integrity": "sha512-47Zhe1Ec02QXczoPNLTFwcvCQFGoXInEiXhsQYF0tD+XAX6Q675j/z6gsIItc8V+avvD0IITsDPpqQ09wfNYkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.15",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-label": "^9.3.15",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-rating": {
+ "version": "9.3.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-rating/-/react-rating-9.3.15.tgz",
+ "integrity": "sha512-MH/Jgoco8p+haf1d5Gi+d5VCjwd0qE6y/uP0YJsB9m11+DFnDxgKhzJKIiIzs3yzB2M4bMM8z9SqEHzQGCQEPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0 <20.0.0",
+ "@types/react-dom": ">=16.8.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-search": {
+ "version": "9.3.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-search/-/react-search-9.3.15.tgz",
+ "integrity": "sha512-xm9YveJM4aXAn/XjG3GMHpXxLO53Nz2mmuJpc80WXaYqQwesGSS0YfMSTbjM04RkvMsjmQM/dwWcudV9JQ0//g==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-input": "^9.7.15",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-select": {
+ "version": "9.4.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-select/-/react-select-9.4.15.tgz",
+ "integrity": "sha512-NWoDzf3H7mu8fXBCR3YIlumMb7lDElsbmcCSIlUz70n2cPTNXcNEQm4ERWiGAmxf8xoAfgfDWc5rYnRWAFi2fA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-shared-contexts": {
+ "version": "9.26.2",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-shared-contexts/-/react-shared-contexts-9.26.2.tgz",
+ "integrity": "sha512-upKXkwlIp5oIhELr4clAZXQkuCd4GDXM6GZEz8BOmRO+PnxyqmycCXvxDxsmi6XN+0vkGM4joiIgkB14o/FctQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-theme": "^9.2.1",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-skeleton": {
+ "version": "9.4.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-skeleton/-/react-skeleton-9.4.15.tgz",
+ "integrity": "sha512-QUVxZ5pYbIprCY1G5sJYDGvuvM1TNFl3vPkME8r/nD7pKXwxaZYJoob2L0DQ9OdnOeHgO8yTOgOgZEU+Km89dA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.15",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-slider": {
+ "version": "9.5.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-slider/-/react-slider-9.5.15.tgz",
+ "integrity": "sha512-lFDkyYYAUUGwbg1UJqjsuQ2tQUBFjxzv2Bpyr1StyAoS91q8skTUDyZxamJTJ0K6Ox/nhkfg+Wzz2aVg9kkF4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.15",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-spinbutton": {
+ "version": "9.5.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-spinbutton/-/react-spinbutton-9.5.15.tgz",
+ "integrity": "sha512-0NNfaXm8TJWHlillg6FPgJ1Ph7iO9ez+Gz4TSFYm1u+zF8RNsSGoplCf40U6gcKX8GkAHBwQ5vBZUbBK7syDng==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-field": "^9.4.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-spinner": {
+ "version": "9.7.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-spinner/-/react-spinner-9.7.15.tgz",
+ "integrity": "sha512-ZMJ7y08yvVXL9HuiMLLCy1cRn8plR9A4mL57CM2/otaXVWQbOwRaFD0/+Dx3u9A8sEtdYLo6O9gJIjU8fZGaYw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-label": "^9.3.15",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-swatch-picker": {
+ "version": "9.4.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-swatch-picker/-/react-swatch-picker-9.4.15.tgz",
+ "integrity": "sha512-jeYSEDwLbQAW/UoTP15EZpVm2Z+UpPSjkgJaKk73UxX1+rD/JIzpxrN3FfEfkn3/uTZUQkd/SE4NQrilu1OMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-field": "^9.4.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0 <20.0.0",
+ "@types/react-dom": ">=16.8.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-switch": {
+ "version": "9.6.0",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-switch/-/react-switch-9.6.0.tgz",
+ "integrity": "sha512-fqFj7PPSeGKIKI6OZ8JTwGKf5TSDZDhoUmXig03kUloX1w+rsGih92oUanZgnucEreIbkNwcgAKijRNbb1P0JQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-label": "^9.3.15",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-table": {
+ "version": "9.19.10",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-table/-/react-table-9.19.10.tgz",
+ "integrity": "sha512-FFMSgUlUsicVZxCoLoNvOMdpANIKa0Ys4bhiNhlObsayLPFLwKrM9aL1eOg5RZPE+NUIQ8DJSrFcws1zzo6Jpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-avatar": "^9.10.2",
+ "@fluentui/react-checkbox": "^9.5.15",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-radio": "^9.5.15",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-tabs": {
+ "version": "9.11.2",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-tabs/-/react-tabs-9.11.2.tgz",
+ "integrity": "sha512-zmWzySlPM9EwHJNW0/JhyxBCqBvmfZIj1OZLdRDpbPDsKjhO0aGZV6WjLHFYJmq58kbN0wHKUbxc7LfafHHUwA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-tabster": {
+ "version": "9.26.13",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-tabster/-/react-tabster-9.26.13.tgz",
+ "integrity": "sha512-uOuJj7jn1ME52Vc685/Ielf6srK/sfFQA5zBIbXIvy2Eisfp7R1RmJe2sXWoszz/Fu/XDkPwdM/GLv23N3vrvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1",
+ "keyborg": "^2.6.0",
+ "tabster": "^8.5.5"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-tag-picker": {
+ "version": "9.8.1",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-tag-picker/-/react-tag-picker-9.8.1.tgz",
+ "integrity": "sha512-DDCh4rrY6wcIjOCsSBCtC3d1zX9KgCLAIP7kGpd+LNYfaIc9AU/nUZIRSF1L/zTDqaODf0n60ba/lB5RufxdNA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-combobox": "^9.16.17",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-field": "^9.4.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-portal": "^9.8.11",
+ "@fluentui/react-positioning": "^9.22.0",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-tags": "^9.7.17",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-tags": {
+ "version": "9.7.17",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-tags/-/react-tags-9.7.17.tgz",
+ "integrity": "sha512-LCJJqoXIiN+aNqFHC/5nddsQJqh56xzrywwpMbMrQYI/dbIk5UYlmZ6arIPhQ9HVKat3YzGKAvOGlhFhEHIwDg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-avatar": "^9.10.2",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-teaching-popover": {
+ "version": "9.6.18",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-teaching-popover/-/react-teaching-popover-9.6.18.tgz",
+ "integrity": "sha512-cf76vSRZs40geZEw/RChfQvu6ioMyFKR0qvPc52QstPDC/cgGkOg+45G7SZo11IpYwBdkpUVWasnWUWSxTMiHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-button": "^9.8.2",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-popover": "^9.14.0",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1",
+ "use-sync-external-store": "^1.2.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0 <20.0.0",
+ "@types/react-dom": ">=16.8.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-text": {
+ "version": "9.6.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-text/-/react-text-9.6.15.tgz",
+ "integrity": "sha512-YB1azhq8MGfnYTGlEAX1mzcFZ6CvqkkaxaCogU4TM9BtPgQ1YUAxE01RMenl8VVi8W9hNbJKkuc8R8GzYwzT4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-textarea": {
+ "version": "9.6.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-textarea/-/react-textarea-9.6.15.tgz",
+ "integrity": "sha512-yGYW3d+t21qJXlVsbAHz07RR/YxVw5b56483nFAbqGP3RpPG8ert8q9Ci2mldI9LpjYTG5deXUHqfcVGJ7qDAg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.15",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-theme": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-theme/-/react-theme-9.2.1.tgz",
+ "integrity": "sha512-lJxfz7LmmglFz+c9C41qmMqaRRZZUPtPPl9DWQ79vH+JwZd4dkN7eA78OTRwcGCOTPEKoLTX72R+EFaWEDlX+w==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/tokens": "1.0.0-alpha.23",
+ "@swc/helpers": "^0.5.1"
+ }
+ },
+ "node_modules/@fluentui/react-toast": {
+ "version": "9.7.14",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-toast/-/react-toast-9.7.14.tgz",
+ "integrity": "sha512-Hzdzq/3hBPSZUYAStDRQ1bP1fwCZnOOik4YyPFGsVvgS60SWgcgHtRlvYgmFVd29dOHOU6J8A9VPbCwiWqf56A==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-motion": "^9.13.0",
+ "@fluentui/react-motion-components-preview": "^0.15.2",
+ "@fluentui/react-portal": "^9.8.11",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-toolbar": {
+ "version": "9.7.3",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-toolbar/-/react-toolbar-9.7.3.tgz",
+ "integrity": "sha512-h9mXLrQ55SFd2YXJXQOtpC+MJ3SckyGB5lWqFkQxqExFZkkeCL1u1bRf2/YFjNj8gbivVMwKmozzWeccexPeyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-button": "^9.8.2",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-divider": "^9.6.2",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-radio": "^9.5.15",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-tooltip": {
+ "version": "9.9.3",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-tooltip/-/react-tooltip-9.9.3.tgz",
+ "integrity": "sha512-a351JFoaBAOn0SnQ76tzuNv2ieHzAS+VO8Ncy4m9/emrIs5lvBBfKX8fvA4/efVxY+683XEQdoL1LuApuJuTWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-portal": "^9.8.11",
+ "@fluentui/react-positioning": "^9.22.0",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-tree": {
+ "version": "9.15.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-tree/-/react-tree-9.15.12.tgz",
+ "integrity": "sha512-xppRZ5lljdlrBS/FrTgxM7JHsbyjJ6PNK7kQvkFLUa6cSNac2nzbLExIDs9TAZZe+wNkAiJiX5RZY/9Sb87NJQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.10",
+ "@fluentui/react-avatar": "^9.10.2",
+ "@fluentui/react-button": "^9.8.2",
+ "@fluentui/react-checkbox": "^9.5.15",
+ "@fluentui/react-context-selector": "^9.2.15",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-motion": "^9.13.0",
+ "@fluentui/react-motion-components-preview": "^0.15.2",
+ "@fluentui/react-radio": "^9.5.15",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-tabster": "^9.26.13",
+ "@fluentui/react-theme": "^9.2.1",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-utilities": {
+ "version": "9.26.2",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-utilities/-/react-utilities-9.26.2.tgz",
+ "integrity": "sha512-Yp2GGNoWifj8Z/VVir4HyRumRsqXnLJd4IP/Y70vEm9ruAvyqUvfn+1lQUuA+k/Reqw8GI+Ix7FTo3rogixZBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-virtualizer": {
+ "version": "9.0.0-alpha.111",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-virtualizer/-/react-virtualizer-9.0.0-alpha.111.tgz",
+ "integrity": "sha512-yku++0779Ve1RNz6y/HWjlXKd2x1wCSbWMydT2IdCICBVwolXjPYMpkqqZUSjbJ0N9gl6BfsCBpU9Dfe2bR8Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-jsx-runtime": "^9.4.1",
+ "@fluentui/react-shared-contexts": "^9.26.2",
+ "@fluentui/react-utilities": "^9.26.2",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/tokens": {
+ "version": "1.0.0-alpha.23",
+ "resolved": "https://registry.npmjs.org/@fluentui/tokens/-/tokens-1.0.0-alpha.23.tgz",
+ "integrity": "sha512-uxrzF9Z+J10naP0pGS7zPmzSkspSS+3OJDmYIK3o1nkntQrgBXq3dBob4xSlTDm5aOQ0kw6EvB9wQgtlyy4eKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@swc/helpers": "^0.5.1"
+ }
+ },
+ "node_modules/@griffel/core": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.20.1.tgz",
+ "integrity": "sha512-ld1mX04zpmeHn8agx4slSEh8kJ+8or3Y0x9gsJNKSKn6GdCkZBSiGUh+oBXCBn8RKzz8l60TA9IhVSStnyKekA==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/hash": "^0.9.0",
+ "@griffel/style-types": "^1.4.0",
+ "csstype": "^3.1.3",
+ "rtl-css-js": "^1.16.1",
+ "stylis": "^4.2.0",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@griffel/react": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.6.1.tgz",
+ "integrity": "sha512-mNM4/+dIXzqeHboWpVZ1/jiwTAYNc5/8y/V/HasnQ2QXnV6gSUYpeUk/0n6IFU3NJmVJly9JrLSfNo0hM/IFeA==",
+ "license": "MIT",
+ "dependencies": {
+ "@griffel/core": "^1.20.1",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@griffel/style-types": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.4.0.tgz",
+ "integrity": "sha512-vNDfOGV7RN/XkA7vxgf7Z5HgW8eiBm5cHT9wQPhsKB4pxWom5u6eQ9CkYE5mCCTSPl9H6Nd1NBai04d4P6BD7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.1.3"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@inquirer/ansi": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
+ "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/checkbox": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-3.0.1.tgz",
+ "integrity": "sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/core": "^9.2.1",
+ "@inquirer/figures": "^1.0.6",
+ "@inquirer/type": "^2.0.0",
+ "ansi-escapes": "^4.3.2",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/checkbox/node_modules/@inquirer/core": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz",
+ "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/figures": "^1.0.6",
+ "@inquirer/type": "^2.0.0",
+ "@types/mute-stream": "^0.0.4",
+ "@types/node": "^22.5.5",
+ "@types/wrap-ansi": "^3.0.0",
+ "ansi-escapes": "^4.3.2",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^1.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/checkbox/node_modules/@inquirer/type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz",
+ "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "mute-stream": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/checkbox/node_modules/@types/node": {
+ "version": "22.19.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@inquirer/checkbox/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@inquirer/confirm": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-4.0.1.tgz",
+ "integrity": "sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/core": "^9.2.1",
+ "@inquirer/type": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/confirm/node_modules/@inquirer/core": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz",
+ "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/figures": "^1.0.6",
+ "@inquirer/type": "^2.0.0",
+ "@types/mute-stream": "^0.0.4",
+ "@types/node": "^22.5.5",
+ "@types/wrap-ansi": "^3.0.0",
+ "ansi-escapes": "^4.3.2",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^1.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/confirm/node_modules/@inquirer/type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz",
+ "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "mute-stream": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/confirm/node_modules/@types/node": {
+ "version": "22.19.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@inquirer/confirm/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@inquirer/core": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-5.1.2.tgz",
+ "integrity": "sha512-w3PMZH5rahrukn8/I7P9Ihil+twgLTUHDZtJlJyBbUKyPaOSSQjLZkb0PpncVhin1gCaMgOFXy6iNPgcZUoo2w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/type": "^1.1.6",
+ "@types/mute-stream": "^0.0.4",
+ "@types/node": "^20.10.7",
+ "@types/wrap-ansi": "^3.0.0",
+ "ansi-escapes": "^4.3.2",
+ "chalk": "^4.1.2",
+ "cli-spinners": "^2.9.2",
+ "cli-width": "^4.1.0",
+ "figures": "^3.2.0",
+ "mute-stream": "^1.0.0",
+ "run-async": "^3.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^6.2.0"
+ },
+ "engines": {
+ "node": ">=14.18.0"
+ }
+ },
+ "node_modules/@inquirer/core/node_modules/@types/node": {
+ "version": "20.19.37",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
+ "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@inquirer/core/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@inquirer/editor": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-3.0.1.tgz",
+ "integrity": "sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/core": "^9.2.1",
+ "@inquirer/type": "^2.0.0",
+ "external-editor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/editor/node_modules/@inquirer/core": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz",
+ "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/figures": "^1.0.6",
+ "@inquirer/type": "^2.0.0",
+ "@types/mute-stream": "^0.0.4",
+ "@types/node": "^22.5.5",
+ "@types/wrap-ansi": "^3.0.0",
+ "ansi-escapes": "^4.3.2",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^1.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/editor/node_modules/@inquirer/type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz",
+ "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "mute-stream": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/editor/node_modules/@types/node": {
+ "version": "22.19.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@inquirer/editor/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@inquirer/expand": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-3.0.1.tgz",
+ "integrity": "sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/core": "^9.2.1",
+ "@inquirer/type": "^2.0.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/expand/node_modules/@inquirer/core": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz",
+ "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/figures": "^1.0.6",
+ "@inquirer/type": "^2.0.0",
+ "@types/mute-stream": "^0.0.4",
+ "@types/node": "^22.5.5",
+ "@types/wrap-ansi": "^3.0.0",
+ "ansi-escapes": "^4.3.2",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^1.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/expand/node_modules/@inquirer/type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz",
+ "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "mute-stream": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/expand/node_modules/@types/node": {
+ "version": "22.19.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@inquirer/expand/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@inquirer/external-editor": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
+ "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chardet": "^2.1.1",
+ "iconv-lite": "^0.7.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/external-editor/node_modules/chardet": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
+ "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@inquirer/external-editor/node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@inquirer/figures": {
+ "version": "1.0.15",
+ "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
+ "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/input": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-3.0.1.tgz",
+ "integrity": "sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/core": "^9.2.1",
+ "@inquirer/type": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/input/node_modules/@inquirer/core": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz",
+ "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/figures": "^1.0.6",
+ "@inquirer/type": "^2.0.0",
+ "@types/mute-stream": "^0.0.4",
+ "@types/node": "^22.5.5",
+ "@types/wrap-ansi": "^3.0.0",
+ "ansi-escapes": "^4.3.2",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^1.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/input/node_modules/@inquirer/type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz",
+ "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "mute-stream": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/input/node_modules/@types/node": {
+ "version": "22.19.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@inquirer/input/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@inquirer/number": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-2.0.1.tgz",
+ "integrity": "sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/core": "^9.2.1",
+ "@inquirer/type": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/number/node_modules/@inquirer/core": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz",
+ "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/figures": "^1.0.6",
+ "@inquirer/type": "^2.0.0",
+ "@types/mute-stream": "^0.0.4",
+ "@types/node": "^22.5.5",
+ "@types/wrap-ansi": "^3.0.0",
+ "ansi-escapes": "^4.3.2",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^1.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/number/node_modules/@inquirer/type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz",
+ "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "mute-stream": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/number/node_modules/@types/node": {
+ "version": "22.19.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@inquirer/number/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@inquirer/password": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-3.0.1.tgz",
+ "integrity": "sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/core": "^9.2.1",
+ "@inquirer/type": "^2.0.0",
+ "ansi-escapes": "^4.3.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/password/node_modules/@inquirer/core": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz",
+ "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/figures": "^1.0.6",
+ "@inquirer/type": "^2.0.0",
+ "@types/mute-stream": "^0.0.4",
+ "@types/node": "^22.5.5",
+ "@types/wrap-ansi": "^3.0.0",
+ "ansi-escapes": "^4.3.2",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^1.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/password/node_modules/@inquirer/type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz",
+ "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "mute-stream": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/password/node_modules/@types/node": {
+ "version": "22.19.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@inquirer/password/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@inquirer/prompts": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-6.0.1.tgz",
+ "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/checkbox": "^3.0.1",
+ "@inquirer/confirm": "^4.0.1",
+ "@inquirer/editor": "^3.0.1",
+ "@inquirer/expand": "^3.0.1",
+ "@inquirer/input": "^3.0.1",
+ "@inquirer/number": "^2.0.1",
+ "@inquirer/password": "^3.0.1",
+ "@inquirer/rawlist": "^3.0.1",
+ "@inquirer/search": "^2.0.1",
+ "@inquirer/select": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/rawlist": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-3.0.1.tgz",
+ "integrity": "sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/core": "^9.2.1",
+ "@inquirer/type": "^2.0.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/rawlist/node_modules/@inquirer/core": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz",
+ "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/figures": "^1.0.6",
+ "@inquirer/type": "^2.0.0",
+ "@types/mute-stream": "^0.0.4",
+ "@types/node": "^22.5.5",
+ "@types/wrap-ansi": "^3.0.0",
+ "ansi-escapes": "^4.3.2",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^1.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/rawlist/node_modules/@inquirer/type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz",
+ "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "mute-stream": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/rawlist/node_modules/@types/node": {
+ "version": "22.19.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@inquirer/rawlist/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@inquirer/search": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-2.0.1.tgz",
+ "integrity": "sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/core": "^9.2.1",
+ "@inquirer/figures": "^1.0.6",
+ "@inquirer/type": "^2.0.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/search/node_modules/@inquirer/core": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz",
+ "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/figures": "^1.0.6",
+ "@inquirer/type": "^2.0.0",
+ "@types/mute-stream": "^0.0.4",
+ "@types/node": "^22.5.5",
+ "@types/wrap-ansi": "^3.0.0",
+ "ansi-escapes": "^4.3.2",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^1.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/search/node_modules/@inquirer/type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz",
+ "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "mute-stream": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/search/node_modules/@types/node": {
+ "version": "22.19.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@inquirer/search/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@inquirer/select": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-3.0.1.tgz",
+ "integrity": "sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/core": "^9.2.1",
+ "@inquirer/figures": "^1.0.6",
+ "@inquirer/type": "^2.0.0",
+ "ansi-escapes": "^4.3.2",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/select/node_modules/@inquirer/core": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz",
+ "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@inquirer/figures": "^1.0.6",
+ "@inquirer/type": "^2.0.0",
+ "@types/mute-stream": "^0.0.4",
+ "@types/node": "^22.5.5",
+ "@types/wrap-ansi": "^3.0.0",
+ "ansi-escapes": "^4.3.2",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^1.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/select/node_modules/@inquirer/type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz",
+ "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "mute-stream": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/select/node_modules/@types/node": {
+ "version": "22.19.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@inquirer/select/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@inquirer/type": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz",
+ "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "mute-stream": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@jsdevtools/ono": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
+ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@jsonjoy.com/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/buffers": {
+ "version": "17.67.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz",
+ "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/codegen": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz",
+ "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-core": {
+ "version": "4.56.11",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.56.11.tgz",
+ "integrity": "sha512-wThHjzUp01ImIjfCwhs+UnFkeGPFAymwLEkOtenHewaKe2pTP12p6r1UuwikA9NEvNf9Vlck92r8fb8n/MWM5w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/fs-node-builtins": "4.56.11",
+ "@jsonjoy.com/fs-node-utils": "4.56.11",
+ "thingies": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-fsa": {
+ "version": "4.56.11",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.56.11.tgz",
+ "integrity": "sha512-ZYlF3XbMayyp97xEN8ZvYutU99PCHjM64mMZvnCseXkCJXJDVLAwlF8Q/7q/xiWQRsv3pQBj1WXHd9eEyYcaCQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/fs-core": "4.56.11",
+ "@jsonjoy.com/fs-node-builtins": "4.56.11",
+ "@jsonjoy.com/fs-node-utils": "4.56.11",
+ "thingies": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-node": {
+ "version": "4.56.11",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.56.11.tgz",
+ "integrity": "sha512-D65YrnP6wRuZyEWoSFnBJSr5zARVpVBGctnhie4rCsMuGXNzX7IHKaOt85/Aj7SSoG1N2+/xlNjWmkLvZ2H3Tg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/fs-core": "4.56.11",
+ "@jsonjoy.com/fs-node-builtins": "4.56.11",
+ "@jsonjoy.com/fs-node-utils": "4.56.11",
+ "@jsonjoy.com/fs-print": "4.56.11",
+ "@jsonjoy.com/fs-snapshot": "4.56.11",
+ "glob-to-regex.js": "^1.0.0",
+ "thingies": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-node-builtins": {
+ "version": "4.56.11",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.56.11.tgz",
+ "integrity": "sha512-CNmt3a0zMCIhniFLXtzPWuUxXFU+U+2VyQiIrgt/rRVeEJNrMQUABaRbVxR0Ouw1LyR9RjaEkPM6nYpED+y43A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-node-to-fsa": {
+ "version": "4.56.11",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.56.11.tgz",
+ "integrity": "sha512-5OzGdvJDgZVo+xXWEYo72u81zpOWlxlbG4d4nL+hSiW+LKlua/dldNgPrpWxtvhgyntmdFQad2UTxFyGjJAGhA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/fs-fsa": "4.56.11",
+ "@jsonjoy.com/fs-node-builtins": "4.56.11",
+ "@jsonjoy.com/fs-node-utils": "4.56.11"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-node-utils": {
+ "version": "4.56.11",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.56.11.tgz",
+ "integrity": "sha512-JADOZFDA3wRfsuxkT0+MYc4F9hJO2PYDaY66kRTG6NqGX3+bqmKu66YFYAbII/tEmQWPZeHoClUB23rtQM9UPg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/fs-node-builtins": "4.56.11"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-print": {
+ "version": "4.56.11",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.56.11.tgz",
+ "integrity": "sha512-rnaKRgCRIn8JGTjxhS0JPE38YM3Pj/H7SW4/tglhIPbfKEkky7dpPayNKV2qy25SZSL15oFVgH/62dMZ/z7cyA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/fs-node-utils": "4.56.11",
+ "tree-dump": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-snapshot": {
+ "version": "4.56.11",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.56.11.tgz",
+ "integrity": "sha512-IIldPX+cIRQuUol9fQzSS3hqyECxVpYMJQMqdU3dCKZFRzEl1rkIkw4P6y7Oh493sI7YdxZlKr/yWdzEWZ1wGQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/buffers": "^17.65.0",
+ "@jsonjoy.com/fs-node-utils": "4.56.11",
+ "@jsonjoy.com/json-pack": "^17.65.0",
+ "@jsonjoy.com/util": "^17.65.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": {
+ "version": "17.67.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz",
+ "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": {
+ "version": "17.67.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz",
+ "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": {
+ "version": "17.67.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz",
+ "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/base64": "17.67.0",
+ "@jsonjoy.com/buffers": "17.67.0",
+ "@jsonjoy.com/codegen": "17.67.0",
+ "@jsonjoy.com/json-pointer": "17.67.0",
+ "@jsonjoy.com/util": "17.67.0",
+ "hyperdyperid": "^1.2.0",
+ "thingies": "^2.5.0",
+ "tree-dump": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": {
+ "version": "17.67.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz",
+ "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/util": "17.67.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": {
+ "version": "17.67.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz",
+ "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/buffers": "17.67.0",
+ "@jsonjoy.com/codegen": "17.67.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/json-pack": {
+ "version": "1.21.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz",
+ "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/base64": "^1.1.2",
+ "@jsonjoy.com/buffers": "^1.2.0",
+ "@jsonjoy.com/codegen": "^1.0.0",
+ "@jsonjoy.com/json-pointer": "^1.0.2",
+ "@jsonjoy.com/util": "^1.9.0",
+ "hyperdyperid": "^1.2.0",
+ "thingies": "^2.5.0",
+ "tree-dump": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz",
+ "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/json-pointer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz",
+ "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/codegen": "^1.0.0",
+ "@jsonjoy.com/util": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/util": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz",
+ "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/buffers": "^1.0.0",
+ "@jsonjoy.com/codegen": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz",
+ "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@leichtgewicht/ip-codec": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
+ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@microsoft/app-manifest": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@microsoft/app-manifest/-/app-manifest-1.0.3.tgz",
+ "integrity": "sha512-51nJK5XkNGp+QsHsG5YBRK7XkJi2Tn7jfA4tffHN3fg0cfhgcjkbIAzFqbondYg7bj86aMJ/9ax4ndi40lHPmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/fs-extra": "^11.0.1",
+ "@types/node-fetch": "^2.6.9",
+ "ajv": "^8.5.0",
+ "ajv-draft-04": "^1.0.0",
+ "ajv-formats": "^3.0.1",
+ "node-fetch": "2.7.0"
+ }
+ },
+ "node_modules/@microsoft/app-manifest/node_modules/ajv": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@microsoft/app-manifest/node_modules/ajv-draft-04": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
+ "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "peerDependencies": {
+ "ajv": "^8.5.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@microsoft/app-manifest/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@microsoft/dev-tunnels-contracts": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@microsoft/dev-tunnels-contracts/-/dev-tunnels-contracts-1.1.9.tgz",
+ "integrity": "sha512-OayhehwI+CnO0Wr53e29ZJZWGsNA5yVG7r54qmZSLc5HxA5Cozk4hP7EbYDCXkxh4NbQoT1dhTzC8bkRo+wWXw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "buffer": "^5.2.1",
+ "debug": "^4.1.1",
+ "vscode-jsonrpc": "^4.0.0"
+ }
+ },
+ "node_modules/@microsoft/dev-tunnels-management": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@microsoft/dev-tunnels-management/-/dev-tunnels-management-1.1.9.tgz",
+ "integrity": "sha512-wGuFEzvRiWZmDxQMGKEjOKhEIVnLiG6vRUuM9Hwqxpe/kbiyA2WiUyEVpniNPaaw8gDHTf9zJHnPNNj0JiL5mA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@microsoft/dev-tunnels-contracts": ">1.1.8",
+ "axios": "^1.6.2",
+ "buffer": "^5.2.1",
+ "debug": "^4.1.1",
+ "vscode-jsonrpc": "^4.0.0"
+ }
+ },
+ "node_modules/@microsoft/kiota": {
+ "version": "1.29.0",
+ "resolved": "https://registry.npmjs.org/@microsoft/kiota/-/kiota-1.29.0.tgz",
+ "integrity": "sha512-qqIlTz48OJ5ZMRoTA/uQA70B7ltS4lPSs9atG5PUn+dKZcgXny3LzQPe12B1LsKoBJYbwhaU3fD8/C1DsLW6Cw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "adm-zip": "^0.5.16",
+ "original-fs": "^1.2.0",
+ "uuid": "^13.0.0",
+ "vscode-jsonrpc": "^8.2.1"
+ }
+ },
+ "node_modules/@microsoft/kiota/node_modules/adm-zip": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
+ "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12.0"
+ }
+ },
+ "node_modules/@microsoft/kiota/node_modules/uuid": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
+ "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "uuid": "dist-node/bin/uuid"
+ }
+ },
+ "node_modules/@microsoft/kiota/node_modules/vscode-jsonrpc": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz",
+ "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@microsoft/m365-spec-parser": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@microsoft/m365-spec-parser/-/m365-spec-parser-0.2.10.tgz",
+ "integrity": "sha512-sej/17vEA6O9kYC0Rx4f5aSKtRZAvmgpZima2sJNQDZKzdOz/Tf95vtlW5/TbAhJSrFKqPlcgjFb7kOzbPzQXw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@apidevtools/swagger-parser": "^10.1.1",
+ "@microsoft/app-manifest": "1.0.3",
+ "fs-extra": "^11.2.0",
+ "js-yaml": "^4.1.0",
+ "openapi-types": "^7.2.3",
+ "swagger2openapi": "^7.0.8"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/@microsoft/m365-spec-parser/node_modules/fs-extra": {
+ "version": "11.3.4",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz",
+ "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/@microsoft/m365agentstoolkit-cli": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@microsoft/m365agentstoolkit-cli/-/m365agentstoolkit-cli-1.1.4.tgz",
+ "integrity": "sha512-vyQ905C8FRvGdQSSeY6wynN0s57zYXZhGmY2mn50b2cnHwI5MiX4WlbRKT47WZ5lLRCHrNHoirzkwN7+AphBLQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@azure/arm-subscriptions": "^5.0.0",
+ "@azure/core-auth": "^1.4.0",
+ "@azure/identity": "^4.1.0",
+ "@azure/msal-node": "^2.6.6",
+ "@inquirer/core": "^5.1.2",
+ "@inquirer/prompts": "^6.0.0",
+ "@inquirer/type": "^1.1.5",
+ "@microsoft/teamsfx-api": "0.23.11",
+ "@microsoft/teamsfx-core": "3.0.9",
+ "ansi-escapes": "^4.3.2",
+ "applicationinsights": "^1.8.10",
+ "async-mutex": "^0.3.1",
+ "chalk": "^4.1.0",
+ "cli-table3": "^0.6.3",
+ "dotenv": "^8.2.0",
+ "express": "^4.21.2",
+ "figures": "^3.2.0",
+ "fs-extra": "^9.1.0",
+ "lodash": "^4.17.21",
+ "node-machine-id": "^1.1.12",
+ "open": "^8.2.1",
+ "semver": "^7.5.4",
+ "tree-kill": "^1.2.2",
+ "underscore": "^1.12.1"
+ },
+ "bin": {
+ "atk": "cli.js",
+ "teamsapp": "cliold.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "keytar": "^7.7.0"
+ }
+ },
+ "node_modules/@microsoft/m365agentstoolkit-cli/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@microsoft/teams-manifest": {
+ "version": "0.1.9",
+ "resolved": "https://registry.npmjs.org/@microsoft/teams-manifest/-/teams-manifest-0.1.9.tgz",
+ "integrity": "sha512-WMGdAYir9cdcE9tqfWgk5JRgIHc4f3Bk6cjDFScpQD3+DbzMkRKyFj9ZtfprM4Z0cbgwQmNfp6KaGH+qYSgk+Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/fs-extra": "^11.0.1",
+ "@types/node-fetch": "^2.6.9",
+ "ajv": "^8.5.0",
+ "ajv-draft-04": "^1.0.0",
+ "ajv-formats": "^3.0.1",
+ "fs-extra": "^9.1.0",
+ "node-fetch": "2.7.0"
+ }
+ },
+ "node_modules/@microsoft/teams-manifest/node_modules/ajv": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@microsoft/teams-manifest/node_modules/ajv-draft-04": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
+ "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "peerDependencies": {
+ "ajv": "^8.5.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@microsoft/teams-manifest/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@microsoft/teamsfx-api": {
+ "version": "0.23.11",
+ "resolved": "https://registry.npmjs.org/@microsoft/teamsfx-api/-/teamsfx-api-0.23.11.tgz",
+ "integrity": "sha512-v7OPqua9lmpuQlunoDrK4CgwfhI/eK1hpxhjIUpOc2ceuT2frmZ5c96dJtnohUIR5ZIRiHXFZPIsI+ESMEtcSg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@azure/core-auth": "^1.4.0",
+ "@microsoft/app-manifest": "1.0.3",
+ "chai": "^4.3.4",
+ "jsonschema": "^1.4.0",
+ "neverthrow": "^3.2.0",
+ "tslib": "^2.3.1"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@microsoft/teamsfx-core/-/teamsfx-core-3.0.9.tgz",
+ "integrity": "sha512-kQgLfIgllxDbOJIUcnqbUcv1NDju8bVFAWWfE+VdUkSorJ1WbmYWhKKEGCerBKhvKvK3lpQ80smA0yptKCpoRA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@apidevtools/swagger-parser": "^10.1.0",
+ "@azure/arm-appservice": "^13.0.0",
+ "@azure/arm-resources": "~5.0.1",
+ "@azure/arm-storage": "^17.2.1",
+ "@azure/arm-subscriptions": "^5.0.0",
+ "@azure/core-auth": "^1.4.0",
+ "@azure/core-rest-pipeline": "^1.22.1",
+ "@azure/identity": "^4.1.0",
+ "@azure/msal-node": "^2.6.6",
+ "@azure/storage-blob": "^12.25.0",
+ "@feathersjs/hooks": "^0.6.5",
+ "@microsoft/dev-tunnels-contracts": "1.1.9",
+ "@microsoft/dev-tunnels-management": "1.1.9",
+ "@microsoft/kiota": "1.29.0",
+ "@microsoft/m365-spec-parser": "^0.2.10",
+ "@microsoft/teamsfx-api": "0.23.11",
+ "adm-zip": "^0.5.10",
+ "ajv": "^8.5.0",
+ "axios": "^1.8.3",
+ "axios-retry": "^3.3.1",
+ "comment-json": "^4.2.3",
+ "cryptr": "^6.0.2",
+ "deep-diff": "^1.0.2",
+ "detect-port": "^1.3.0",
+ "dotenv": "^8.2.0",
+ "form-data": "^4.0.2",
+ "fs-extra": "^9.1.0",
+ "glob": "^7.1.6",
+ "handlebars": "^4.7.7",
+ "iconv-lite": "^0.6.3",
+ "ignore": "^5.1.8",
+ "js-base64": "^3.6.0",
+ "js-yaml": "^4.0.0",
+ "jsonschema": "^1.4.0",
+ "klaw": "^3.0.0",
+ "linkedom": "^0.18.9",
+ "md5": "^2.3.0",
+ "mime": "^2.5.2",
+ "mustache": "^4.1.0",
+ "neverthrow": "^3.2.0",
+ "node-fetch": "2.7.0",
+ "node-forge": "^1.3.1",
+ "office-addin-manifest": "^1.13.5",
+ "office-addin-project": "^1.0.0",
+ "openapi-types": "^7.2.3",
+ "proper-lockfile": "^4.1.2",
+ "read-package-json-fast": "^2.0.3",
+ "reflect-metadata": "^0.1.13",
+ "semver": "^7.5.2",
+ "shell-quote": "^1.8.1",
+ "strip-bom": "^4.0.0",
+ "swagger2openapi": "^7.0.8",
+ "tar": "^7.4.3",
+ "typedi": "^0.10.0",
+ "uuid": "^8.3.2",
+ "validator": "^13.7.0",
+ "xml2js": "^0.5.0",
+ "yaml": "^2.2.2"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/ajv": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/commander": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
+ "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/office-addin-manifest": {
+ "version": "1.13.6",
+ "resolved": "https://registry.npmjs.org/office-addin-manifest/-/office-addin-manifest-1.13.6.tgz",
+ "integrity": "sha512-w9JCzPTURwzknB965ejNu2CCf8q/ZfRAKyEvjZrnH0+iNJRnXFy7lcBwNJQgmqG6lYcZPmvvirR8479trHluSA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@microsoft/teams-manifest": "^0.1.3",
+ "adm-zip": "0.5.12",
+ "chalk": "^2.4.2",
+ "commander": "^6.2.0",
+ "fs-extra": "^7.0.1",
+ "node-fetch": "^2.6.1",
+ "office-addin-usage-data": "^1.6.14",
+ "path": "^0.12.7",
+ "uuid": "^8.3.2",
+ "xml2js": "^0.5.0"
+ },
+ "bin": {
+ "office-addin-manifest": "cli.js"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/office-addin-manifest/node_modules/fs-extra": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
+ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/office-addin-usage-data": {
+ "version": "1.6.14",
+ "resolved": "https://registry.npmjs.org/office-addin-usage-data/-/office-addin-usage-data-1.6.14.tgz",
+ "integrity": "sha512-V3TQoMR7McjJ62TWQeRnZqVp1mpjk1bbazvea/pqCJod5zggjlgDspjL5NJzTZc4+MwwJPR8k5Yr5Me+OcRLQw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "applicationinsights": "^1.7.3",
+ "commander": "^6.2.0",
+ "readline-sync": "^1.4.9",
+ "uuid": "8.3.2"
+ },
+ "bin": {
+ "office-addin-usage-data": "cli.js"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@microsoft/teamsfx-core/node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
+ "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@peculiar/asn1-cms": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz",
+ "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.1",
+ "@peculiar/asn1-x509-attr": "^2.6.1",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-csr": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz",
+ "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.1",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-ecc": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz",
+ "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.1",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-pfx": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz",
+ "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-cms": "^2.6.1",
+ "@peculiar/asn1-pkcs8": "^2.6.1",
+ "@peculiar/asn1-rsa": "^2.6.1",
+ "@peculiar/asn1-schema": "^2.6.0",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-pkcs8": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz",
+ "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.1",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-pkcs9": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz",
+ "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-cms": "^2.6.1",
+ "@peculiar/asn1-pfx": "^2.6.1",
+ "@peculiar/asn1-pkcs8": "^2.6.1",
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.1",
+ "@peculiar/asn1-x509-attr": "^2.6.1",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-rsa": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz",
+ "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.1",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-schema": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz",
+ "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asn1js": "^3.0.6",
+ "pvtsutils": "^1.3.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-x509": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz",
+ "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.6.0",
+ "asn1js": "^3.0.6",
+ "pvtsutils": "^1.3.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-x509-attr": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz",
+ "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.1",
+ "asn1js": "^3.0.6",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/x509": {
+ "version": "1.14.3",
+ "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz",
+ "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-cms": "^2.6.0",
+ "@peculiar/asn1-csr": "^2.6.0",
+ "@peculiar/asn1-ecc": "^2.6.0",
+ "@peculiar/asn1-pkcs9": "^2.6.0",
+ "@peculiar/asn1-rsa": "^2.6.0",
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.0",
+ "pvtsutils": "^1.3.6",
+ "reflect-metadata": "^0.2.2",
+ "tslib": "^2.8.1",
+ "tsyringe": "^4.10.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@peculiar/x509/node_modules/reflect-metadata": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
+ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@pkgr/core": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
+ "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/pkgr"
+ }
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@sindresorhus/merge-streams": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
+ "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
+ "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/bonjour": {
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz",
+ "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect-history-api-fallback": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz",
+ "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/express-serve-static-core": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/eslint": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
+ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/@types/eslint-scope": {
+ "version": "3.7.7",
+ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
+ "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/eslint": "*",
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.25",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
+ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "^1"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.19.8",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz",
+ "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/fs-extra": {
+ "version": "11.0.4",
+ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
+ "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/jsonfile": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/html-minifier-terser": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
+ "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/http-proxy": {
+ "version": "1.17.17",
+ "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz",
+ "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/jsonfile": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz",
+ "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mute-stream": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz",
+ "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "25.3.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz",
+ "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "node_modules/@types/node-fetch": {
+ "version": "2.6.13",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
+ "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "form-data": "^4.0.4"
+ }
+ },
+ "node_modules/@types/office-js": {
+ "version": "1.0.581",
+ "resolved": "https://registry.npmjs.org/@types/office-js/-/office-js-1.0.581.tgz",
+ "integrity": "sha512-/eisbn3zAOWC1e2LUu5Q5tKugfwCuxNnsqDE9KqQWsj6uIGqnjfRBCyG7MqsdtO93iRQvq5X0bxfUGq3kVWYGg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/office-runtime": {
+ "version": "1.0.36",
+ "resolved": "https://registry.npmjs.org/@types/office-runtime/-/office-runtime-1.0.36.tgz",
+ "integrity": "sha512-PiXgflBSd1Arcb5z64K4DuQL2t/g7rEN7x3McLLSnKcs8qzhLKHi1UpzfeXC3UzmJk+q9qfu+vOy7V7suBJOAA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/qs": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
+ "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.28",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/retry": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
+ "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-index": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz",
+ "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.10",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
+ "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "<1"
+ }
+ },
+ "node_modules/@types/serve-static/node_modules/@types/send": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
+ "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/sockjs": {
+ "version": "0.3.36",
+ "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz",
+ "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/webpack": {
+ "version": "5.28.5",
+ "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz",
+ "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "tapable": "^2.2.0",
+ "webpack": "^5"
+ }
+ },
+ "node_modules/@types/wrap-ansi": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz",
+ "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
+ "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.56.1",
+ "@typescript-eslint/type-utils": "8.56.1",
+ "@typescript-eslint/utils": "8.56.1",
+ "@typescript-eslint/visitor-keys": "8.56.1",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.56.1",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz",
+ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.56.1",
+ "@typescript-eslint/types": "8.56.1",
+ "@typescript-eslint/typescript-estree": "8.56.1",
+ "@typescript-eslint/visitor-keys": "8.56.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz",
+ "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.56.1",
+ "@typescript-eslint/types": "^8.56.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz",
+ "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.56.1",
+ "@typescript-eslint/visitor-keys": "8.56.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz",
+ "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz",
+ "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.56.1",
+ "@typescript-eslint/typescript-estree": "8.56.1",
+ "@typescript-eslint/utils": "8.56.1",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz",
+ "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz",
+ "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.56.1",
+ "@typescript-eslint/tsconfig-utils": "8.56.1",
+ "@typescript-eslint/types": "8.56.1",
+ "@typescript-eslint/visitor-keys": "8.56.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz",
+ "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.56.1",
+ "@typescript-eslint/types": "8.56.1",
+ "@typescript-eslint/typescript-estree": "8.56.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz",
+ "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.56.1",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@typespec/ts-http-runtime": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.4.tgz",
+ "integrity": "sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@webassemblyjs/ast": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
+ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/helper-numbers": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
+ "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-api-error": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
+ "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-buffer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
+ "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-numbers": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
+ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/floating-point-hex-parser": "1.13.2",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
+ "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-wasm-section": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
+ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/wasm-gen": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/ieee754": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
+ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@xtuc/ieee754": "^1.2.0"
+ }
+ },
+ "node_modules/@webassemblyjs/leb128": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
+ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/utf8": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
+ "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/wasm-edit": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
+ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/helper-wasm-section": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-opt": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1",
+ "@webassemblyjs/wast-printer": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-gen": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
+ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-opt": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
+ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-parser": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
+ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wast-printer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
+ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webpack-cli/configtest": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz",
+ "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x",
+ "webpack-cli": "5.x.x"
+ }
+ },
+ "node_modules/@webpack-cli/info": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz",
+ "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x",
+ "webpack-cli": "5.x.x"
+ }
+ },
+ "node_modules/@webpack-cli/serve": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz",
+ "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x",
+ "webpack-cli": "5.x.x"
+ },
+ "peerDependenciesMeta": {
+ "webpack-dev-server": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.11",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
+ "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/@xtuc/ieee754": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+ "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@xtuc/long": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-import-phases": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
+ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "peerDependencies": {
+ "acorn": "^8.14.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/address": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz",
+ "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/adm-zip": {
+ "version": "0.5.12",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz",
+ "integrity": "sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ajv-formats/node_modules/ajv": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ajv-keywords": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "ajv": "^6.9.1"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-html-community": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz",
+ "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==",
+ "dev": true,
+ "engines": [
+ "node >= 0.8.0"
+ ],
+ "license": "Apache-2.0",
+ "bin": {
+ "ansi-html": "bin/ansi-html"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/applicationinsights": {
+ "version": "1.8.10",
+ "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.8.10.tgz",
+ "integrity": "sha512-ZLDA7mShh4mP2Z/HlFolmvhBPX1LfnbIWXrselyYVA7EKjHhri1fZzpu2EiWAmfbRxNBY6fRjoPJWbx5giKy4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cls-hooked": "^4.2.2",
+ "continuation-local-storage": "^3.2.1",
+ "diagnostic-channel": "0.3.1",
+ "diagnostic-channel-publishers": "0.4.4"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
+ "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.0",
+ "es-object-atoms": "^1.1.1",
+ "get-intrinsic": "^1.3.0",
+ "is-string": "^1.1.1",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-timsort": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz",
+ "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/array.prototype.findlast": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
+ "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+ "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/asn1js": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz",
+ "integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "pvtsutils": "^1.3.6",
+ "pvutils": "^1.1.3",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/async-hook-jl": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz",
+ "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "stack-chain": "^1.3.7"
+ },
+ "engines": {
+ "node": "^4.7 || >=6.9 || >=7.3"
+ }
+ },
+ "node_modules/async-listener": {
+ "version": "0.6.10",
+ "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz",
+ "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "semver": "^5.3.0",
+ "shimmer": "^1.1.0"
+ },
+ "engines": {
+ "node": "<=0.11.8 || >0.11.10"
+ }
+ },
+ "node_modules/async-listener/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/async-mutex": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz",
+ "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.3.1"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.13.6",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
+ "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios-retry": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.9.1.tgz",
+ "integrity": "sha512-8PJDLJv7qTTMMwdnbMvrLYuvB47M81wRtxQmEdV5w4rgbTXTt+vtPkXwajOfOdSyv/wZICJOC+/UhXH4aQ/R+w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.15.4",
+ "is-retry-allowed": "^2.2.0"
+ }
+ },
+ "node_modules/babel-loader": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz",
+ "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-cache-dir": "^4.0.0",
+ "schema-utils": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14.15.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.12.0",
+ "webpack": ">=5"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2": {
+ "version": "0.4.16",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.16.tgz",
+ "integrity": "sha512-xaVwwSfebXf0ooE11BJovZYKhFjIvQo7TsyVpETuIeH2JHv0k/T6Y5j22pPTvqYqmpkxdlPAJlyJ0tfOJAoMxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-define-polyfill-provider": "^0.6.7",
+ "semver": "^6.3.1"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs3": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.1.tgz",
+ "integrity": "sha512-ENp89vM9Pw4kv/koBb5N2f9bDZsR0hpf3BdPMOg/pkS3pwO4dzNnQZVXtBbeyAadgm865DmQG2jMMLqmZXvuCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.7",
+ "core-js-compat": "^3.48.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-regenerator": {
+ "version": "0.6.7",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.7.tgz",
+ "integrity": "sha512-OTYbUlSwXhNgr4g6efMZgsO8//jA61P7ZbRX3iTT53VON8l+WQS8IAUEVo4a4cWknrg2W8Cj4gQhRYNCJ8GkAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.7"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
+ "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/batch": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
+ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/big.js": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
+ "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bonjour-service": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz",
+ "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "multicast-dns": "^7.2.5"
+ }
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "peer": true
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bundle-name": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
+ "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "run-applescript": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/bytestreamjs": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz",
+ "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-me-maybe": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
+ "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camel-case": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
+ "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pascal-case": "^3.1.2",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001777",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz",
+ "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chai": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
+ "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "assertion-error": "^1.1.0",
+ "check-error": "^1.0.3",
+ "deep-eql": "^4.1.3",
+ "get-func-name": "^2.0.2",
+ "loupe": "^2.3.6",
+ "pathval": "^1.1.1",
+ "type-detect": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/charenc": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
+ "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
+ "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "get-func-name": "^2.0.2"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/chrome-trace-event": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
+ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/clean-css": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
+ "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "source-map": "~0.6.0"
+ },
+ "engines": {
+ "node": ">= 10.0"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-table3": {
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz",
+ "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "string-width": "^4.2.0"
+ },
+ "engines": {
+ "node": "10.* || >= 12.*"
+ },
+ "optionalDependencies": {
+ "@colors/colors": "1.5.0"
+ }
+ },
+ "node_modules/cli-width": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
+ "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/clone-deep": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+ "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4",
+ "kind-of": "^6.0.2",
+ "shallow-clone": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cls-hooked": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz",
+ "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "async-hook-jl": "^1.7.6",
+ "emitter-listener": "^1.0.1",
+ "semver": "^5.4.1"
+ },
+ "engines": {
+ "node": "^4.7 || >=6.9 || >=7.3 || >=8.2.1"
+ }
+ },
+ "node_modules/cls-hooked/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/comment-json": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.6.2.tgz",
+ "integrity": "sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "array-timsort": "^1.0.3",
+ "esprima": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/common-path-prefix": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz",
+ "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "compressible": "~2.0.18",
+ "debug": "2.6.9",
+ "negotiator": "~0.6.4",
+ "on-headers": "~1.1.0",
+ "safe-buffer": "5.2.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/compression/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/compression/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/compression/node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/connect-history-api-fallback": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz",
+ "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/continuation-local-storage": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz",
+ "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "async-listener": "^0.6.0",
+ "emitter-listener": "^1.1.1"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/copy-anything": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz",
+ "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-what": "^3.14.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/copy-webpack-plugin": {
+ "version": "12.0.2",
+ "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz",
+ "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.1",
+ "globby": "^14.0.0",
+ "normalize-path": "^3.0.0",
+ "schema-utils": "^4.2.0",
+ "serialize-javascript": "^6.0.2"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ }
+ },
+ "node_modules/core-js": {
+ "version": "3.48.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
+ "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/core-js-compat": {
+ "version": "3.48.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz",
+ "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/crypt": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
+ "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/cryptr": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/cryptr/-/cryptr-6.4.0.tgz",
+ "integrity": "sha512-9jpMU9HMt1vhMUqNO+MPuGEpbh/f7HHZdxrd6L2DMwTuYGyt9pgUJfQyTS1Ei4/sn7qPM4FkjxUoiW79k0x8sA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/css-loader": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz",
+ "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "icss-utils": "^5.1.0",
+ "postcss": "^8.4.40",
+ "postcss-modules-extract-imports": "^3.1.0",
+ "postcss-modules-local-by-default": "^4.0.5",
+ "postcss-modules-scope": "^3.2.0",
+ "postcss-modules-values": "^4.0.0",
+ "postcss-value-parser": "^4.2.0",
+ "semver": "^7.6.3"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0",
+ "webpack": "^5.27.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/css-loader/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/css-select": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
+ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cssom": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
+ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-diff": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz",
+ "integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==",
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/deep-eql": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
+ "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "type-detect": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/default-browser": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
+ "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bundle-name": "^4.1.0",
+ "default-browser-id": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
+ "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
+ "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-node": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-port": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz",
+ "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "address": "^1.0.1",
+ "debug": "4"
+ },
+ "bin": {
+ "detect": "bin/detect-port.js",
+ "detect-port": "bin/detect-port.js"
+ },
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/diagnostic-channel": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-0.3.1.tgz",
+ "integrity": "sha512-6eb9YRrimz8oTr5+JDzGmSYnXy5V7YnK5y/hd8AUDK1MssHjQKm9LlD6NSrHx4vMDF3+e/spI2hmWTviElgWZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^5.3.0"
+ }
+ },
+ "node_modules/diagnostic-channel-publishers": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.4.4.tgz",
+ "integrity": "sha512-l126t01d2ZS9EreskvEtZPrcgstuvH3rbKy82oUhUrVmBaGx4hO9wECdl3cvZbKDYjMF3QJDB5z5dL9yWAjvZQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "diagnostic-channel": "*"
+ }
+ },
+ "node_modules/diagnostic-channel/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/dns-packet": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
+ "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@leichtgewicht/ip-codec": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/dom-converter": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
+ "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "utila": "~0.4"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/dot-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
+ "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz",
+ "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.307",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz",
+ "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/embla-carousel": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
+ "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
+ "license": "MIT"
+ },
+ "node_modules/embla-carousel-autoplay": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.6.0.tgz",
+ "integrity": "sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "embla-carousel": "8.6.0"
+ }
+ },
+ "node_modules/embla-carousel-fade": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/embla-carousel-fade/-/embla-carousel-fade-8.6.0.tgz",
+ "integrity": "sha512-qaYsx5mwCz72ZrjlsXgs1nKejSrW+UhkbOMwLgfRT7w2LtdEB03nPRI06GHuHv5ac2USvbEiX2/nAHctcDwvpg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "embla-carousel": "8.6.0"
+ }
+ },
+ "node_modules/emitter-listener": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz",
+ "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "shimmer": "^1.2.0"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/emojis-list": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
+ "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.20.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
+ "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/envinfo": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz",
+ "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "envinfo": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/errno": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
+ "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "prr": "~1.0.1"
+ },
+ "bin": {
+ "errno": "cli.js"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.24.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
+ "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.2.1",
+ "is-set": "^2.0.3",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.1",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.4",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.4",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "stop-iteration-iterator": "^1.1.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.19"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-iterator-helpers": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz",
+ "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.1",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.1.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.3.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "iterator.prototype": "^1.1.5",
+ "safe-array-concat": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
+ "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+ "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+ "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.0.5",
+ "is-symbol": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es6-promise": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+ "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
+ "license": "MIT"
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz",
+ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-office-addins": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-office-addins/-/eslint-plugin-office-addins-4.0.6.tgz",
+ "integrity": "sha512-9O0VxtlFerOa+uKpZA6+xBL9Gl6b114g3U6Mxr3ZQc7D+GR8iZvLzq3zEVzu2ZyOb6gurtAj+AuC2D4tpoaAkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-prettier": "^5.1.3",
+ "eslint-plugin-react": "^7.34.1",
+ "eslint-plugin-react-native": "^5.0.0",
+ "office-addin-prettier-config": "^2.0.1",
+ "prettier": "^3.2.5",
+ "requireindex": "~1.2.0",
+ "typescript": "^5.4.3",
+ "typescript-eslint": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "eslint": "^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-prettier": {
+ "version": "5.5.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz",
+ "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prettier-linter-helpers": "^1.0.1",
+ "synckit": "^0.11.12"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-plugin-prettier"
+ },
+ "peerDependencies": {
+ "@types/eslint": ">=8.0.0",
+ "eslint": ">=8.0.0",
+ "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
+ "prettier": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/eslint": {
+ "optional": true
+ },
+ "eslint-config-prettier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.37.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
+ "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.8",
+ "array.prototype.findlast": "^1.2.5",
+ "array.prototype.flatmap": "^1.3.3",
+ "array.prototype.tosorted": "^1.1.4",
+ "doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.2.1",
+ "estraverse": "^5.3.0",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.9",
+ "object.fromentries": "^2.0.8",
+ "object.values": "^1.2.1",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.5",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.12",
+ "string.prototype.repeat": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
+ }
+ },
+ "node_modules/eslint-plugin-react-native": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-native/-/eslint-plugin-react-native-5.0.0.tgz",
+ "integrity": "sha512-VyWlyCC/7FC/aONibOwLkzmyKg4j9oI8fzrk9WYNs4I8/m436JuOTAFwLvEn1CVvc7La4cPfbCyspP4OYpP52Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-plugin-react-native-globals": "^0.1.1"
+ },
+ "peerDependencies": {
+ "eslint": "^3.17.0 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-react-native-globals": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz",
+ "integrity": "sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.6",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz",
+ "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "node-exports-info": "^1.6.0",
+ "object-keys": "^1.1.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "dev": true,
+ "license": "(MIT OR WTFPL)",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/external-editor/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-diff": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-safe-stringify": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
+ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fast-xml-builder": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz",
+ "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "5.4.2",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.2.tgz",
+ "integrity": "sha512-pw/6pIl4k0CSpElPEJhDppLzaixDEuWui2CUQQBH/ECDf7+y6YwA4Gf7Tyb0Rfe4DIMuZipYj4AEL0nACKglvQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "fast-xml-builder": "^1.0.0",
+ "strnum": "^2.1.2"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
+ "node_modules/fastest-levenshtein": {
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.9.1"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/faye-websocket": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
+ "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "websocket-driver": ">=0.5.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/figures": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "escape-string-regexp": "^1.0.5"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/figures/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/file-loader": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
+ "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^4.0.0 || ^5.0.0"
+ }
+ },
+ "node_modules/file-loader/node_modules/schema-utils": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
+ "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.8",
+ "ajv": "^6.12.5",
+ "ajv-keywords": "^3.5.2"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/find-cache-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz",
+ "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "common-path-prefix": "^3.0.0",
+ "pkg-dir": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
+ "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/fs-minipass/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fs-minipass/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+ "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "functions-have-names": "^1.2.3",
+ "hasown": "^2.0.2",
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-func-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
+ "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob-to-regex.js": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz",
+ "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/globby": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz",
+ "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/merge-streams": "^2.1.0",
+ "fast-glob": "^3.3.3",
+ "ignore": "^7.0.3",
+ "path-type": "^6.0.0",
+ "slash": "^5.1.0",
+ "unicorn-magic": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globby/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/handle-thing": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
+ "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/handlebars": {
+ "version": "4.7.8",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
+ "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "minimist": "^1.2.5",
+ "neo-async": "^2.6.2",
+ "source-map": "^0.6.1",
+ "wordwrap": "^1.0.0"
+ },
+ "bin": {
+ "handlebars": "bin/handlebars"
+ },
+ "engines": {
+ "node": ">=0.4.7"
+ },
+ "optionalDependencies": {
+ "uglify-js": "^3.1.4"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/hpack.js": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
+ "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "obuf": "^1.0.0",
+ "readable-stream": "^2.0.1",
+ "wbuf": "^1.1.0"
+ }
+ },
+ "node_modules/hpack.js/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hpack.js/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/hpack.js/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hpack.js/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
+ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/html-loader": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-5.1.0.tgz",
+ "integrity": "sha512-Jb3xwDbsm0W3qlXrCZwcYqYGnYz55hb6aoKQTlzyZPXsPpi6tHXzAfqalecglMQgNvtEfxrCQPaKT90Irt5XDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "html-minifier-terser": "^7.2.0",
+ "parse5": "^7.1.2"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/html-minifier-terser": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz",
+ "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "camel-case": "^4.1.2",
+ "clean-css": "~5.3.2",
+ "commander": "^10.0.0",
+ "entities": "^4.4.0",
+ "param-case": "^3.0.4",
+ "relateurl": "^0.2.7",
+ "terser": "^5.15.1"
+ },
+ "bin": {
+ "html-minifier-terser": "cli.js"
+ },
+ "engines": {
+ "node": "^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/html-webpack-plugin": {
+ "version": "5.6.6",
+ "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.6.tgz",
+ "integrity": "sha512-bLjW01UTrvoWTJQL5LsMRo1SypHW80FTm12OJRSnr3v6YHNhfe+1r0MYUZJMACxnCHURVnBWRwAsWs2yPU9Ezw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/html-minifier-terser": "^6.0.0",
+ "html-minifier-terser": "^6.0.2",
+ "lodash": "^4.17.21",
+ "pretty-error": "^4.0.0",
+ "tapable": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/html-webpack-plugin"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || 1.x",
+ "webpack": "^5.20.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/html-webpack-plugin/node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/html-webpack-plugin/node_modules/html-minifier-terser": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
+ "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "camel-case": "^4.1.2",
+ "clean-css": "^5.2.2",
+ "commander": "^8.3.0",
+ "he": "^1.2.0",
+ "param-case": "^3.0.4",
+ "relateurl": "^0.2.7",
+ "terser": "^5.10.0"
+ },
+ "bin": {
+ "html-minifier-terser": "cli.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/htmlparser2": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
+ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.2.2",
+ "entities": "^7.0.1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/http-deceiver": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
+ "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/http-parser-js": {
+ "version": "0.5.10",
+ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
+ "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-proxy": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/http-proxy-middleware": {
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
+ "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-proxy": "^1.17.8",
+ "http-proxy": "^1.18.1",
+ "is-glob": "^4.0.1",
+ "is-plain-obj": "^3.0.0",
+ "micromatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "@types/express": "^4.17.13"
+ },
+ "peerDependenciesMeta": {
+ "@types/express": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/http2-client": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz",
+ "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/hyperdyperid": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz",
+ "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.18"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/icss-utils": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
+ "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause",
+ "peer": true
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/image-size": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
+ "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "image-size": "bin/image-size.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-local": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
+ "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-local/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/import-local/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/import-local/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-local/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/import-local/node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/inquirer": {
+ "version": "12.11.1",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.11.1.tgz",
+ "integrity": "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^1.0.2",
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/prompts": "^7.10.1",
+ "@inquirer/type": "^3.0.10",
+ "mute-stream": "^2.0.0",
+ "run-async": "^4.0.6",
+ "rxjs": "^7.8.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/inquirer/node_modules/@inquirer/checkbox": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz",
+ "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^1.0.2",
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/figures": "^1.0.15",
+ "@inquirer/type": "^3.0.10",
+ "yoctocolors-cjs": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/inquirer/node_modules/@inquirer/confirm": {
+ "version": "5.1.21",
+ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz",
+ "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/type": "^3.0.10"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/inquirer/node_modules/@inquirer/core": {
+ "version": "10.3.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz",
+ "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^1.0.2",
+ "@inquirer/figures": "^1.0.15",
+ "@inquirer/type": "^3.0.10",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^2.0.0",
+ "signal-exit": "^4.1.0",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/inquirer/node_modules/@inquirer/editor": {
+ "version": "4.2.23",
+ "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz",
+ "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/external-editor": "^1.0.3",
+ "@inquirer/type": "^3.0.10"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/inquirer/node_modules/@inquirer/expand": {
+ "version": "4.0.23",
+ "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz",
+ "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/type": "^3.0.10",
+ "yoctocolors-cjs": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/inquirer/node_modules/@inquirer/input": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz",
+ "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/type": "^3.0.10"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/inquirer/node_modules/@inquirer/number": {
+ "version": "3.0.23",
+ "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz",
+ "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/type": "^3.0.10"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/inquirer/node_modules/@inquirer/password": {
+ "version": "4.0.23",
+ "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz",
+ "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^1.0.2",
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/type": "^3.0.10"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/inquirer/node_modules/@inquirer/prompts": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz",
+ "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/checkbox": "^4.3.2",
+ "@inquirer/confirm": "^5.1.21",
+ "@inquirer/editor": "^4.2.23",
+ "@inquirer/expand": "^4.0.23",
+ "@inquirer/input": "^4.3.1",
+ "@inquirer/number": "^3.0.23",
+ "@inquirer/password": "^4.0.23",
+ "@inquirer/rawlist": "^4.1.11",
+ "@inquirer/search": "^3.2.2",
+ "@inquirer/select": "^4.4.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/inquirer/node_modules/@inquirer/rawlist": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz",
+ "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/type": "^3.0.10",
+ "yoctocolors-cjs": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/inquirer/node_modules/@inquirer/search": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz",
+ "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/figures": "^1.0.15",
+ "@inquirer/type": "^3.0.10",
+ "yoctocolors-cjs": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/inquirer/node_modules/@inquirer/select": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz",
+ "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^1.0.2",
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/figures": "^1.0.15",
+ "@inquirer/type": "^3.0.10",
+ "yoctocolors-cjs": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/inquirer/node_modules/@inquirer/type": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz",
+ "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/inquirer/node_modules/mute-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
+ "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/inquirer/node_modules/run-async": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz",
+ "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/interpret": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
+ "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-inside-container/node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-network-error": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz",
+ "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
+ "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-retry-allowed": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz",
+ "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-what": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz",
+ "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/iterator.prototype": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
+ "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "get-proto": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/js-base64": {
+ "version": "3.7.8",
+ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz",
+ "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "peer": true
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsonschema": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz",
+ "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "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"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/junk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz",
+ "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/keyborg": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/keyborg/-/keyborg-2.6.0.tgz",
+ "integrity": "sha512-o5kvLbuTF+o326CMVYpjlaykxqYP9DphFQZ2ZpgrvBouyvOxyEB7oqe8nOLFpiV5VCtz0D3pt8gXQYWpLpBnmA==",
+ "license": "MIT"
+ },
+ "node_modules/keytar": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz",
+ "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "node-addon-api": "^4.3.0",
+ "prebuild-install": "^7.0.1"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/klaw": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz",
+ "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.9"
+ }
+ },
+ "node_modules/launch-editor": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.1.tgz",
+ "integrity": "sha512-lPSddlAAluRKJ7/cjRFoXUFzaX7q/YKI7yPHuEvSJVqoXvFnJov1/Ud87Aa4zULIbA9Nja4mSPK8l0z/7eV2wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^1.1.1",
+ "shell-quote": "^1.8.3"
+ }
+ },
+ "node_modules/less": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/less/-/less-4.5.1.tgz",
+ "integrity": "sha512-UKgI3/KON4u6ngSsnDADsUERqhZknsVZbnuzlRZXLQCmfC/MDld42fTydUE9B+Mla1AL6SJ/Pp6SlEFi/AVGfw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "copy-anything": "^2.0.1",
+ "parse-node-version": "^1.0.1",
+ "tslib": "^2.3.0"
+ },
+ "bin": {
+ "lessc": "bin/lessc"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "optionalDependencies": {
+ "errno": "^0.1.1",
+ "graceful-fs": "^4.1.2",
+ "image-size": "~0.5.0",
+ "make-dir": "^2.1.0",
+ "mime": "^1.4.1",
+ "needle": "^3.1.0",
+ "source-map": "~0.6.0"
+ }
+ },
+ "node_modules/less-loader": {
+ "version": "12.3.1",
+ "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.3.1.tgz",
+ "integrity": "sha512-JZZmG7gMzoDP3VGeEG8Sh6FW5wygB5jYL7Wp29FFihuRTsIBacqO3LbRPr2yStYD11riVf13selLm/CPFRDBRQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0",
+ "less": "^3.5.0 || ^4.0.0",
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/linkedom": {
+ "version": "0.18.12",
+ "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.12.tgz",
+ "integrity": "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "css-select": "^5.1.0",
+ "cssom": "^0.5.0",
+ "html-escaper": "^3.0.3",
+ "htmlparser2": "^10.0.0",
+ "uhyphen": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "canvas": ">= 2"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/loader-runner": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
+ "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.11.5"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/loader-utils": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
+ "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
+ "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "get-func-name": "^2.0.1"
+ }
+ },
+ "node_modules/lower-case": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
+ "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+ "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "pify": "^4.0.1",
+ "semver": "^5.6.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/md5": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
+ "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "dependencies": {
+ "charenc": "0.0.2",
+ "crypt": "0.0.2",
+ "is-buffer": "~1.1.6"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/memfs": {
+ "version": "4.56.11",
+ "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.56.11.tgz",
+ "integrity": "sha512-/GodtwVeKVIHZKLUSr2ZdOxKBC5hHki4JNCU22DoCGPEHr5o2PD5U721zvESKyWwCfTfavFl9WZYgA13OAYK0g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/fs-core": "4.56.11",
+ "@jsonjoy.com/fs-fsa": "4.56.11",
+ "@jsonjoy.com/fs-node": "4.56.11",
+ "@jsonjoy.com/fs-node-builtins": "4.56.11",
+ "@jsonjoy.com/fs-node-to-fsa": "4.56.11",
+ "@jsonjoy.com/fs-node-utils": "4.56.11",
+ "@jsonjoy.com/fs-print": "4.56.11",
+ "@jsonjoy.com/fs-snapshot": "4.56.11",
+ "@jsonjoy.com/json-pack": "^1.11.0",
+ "@jsonjoy.com/util": "^1.9.0",
+ "glob-to-regex.js": "^1.0.1",
+ "thingies": "^2.5.0",
+ "tree-dump": "^1.0.3",
+ "tslib": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mini-css-extract-plugin": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.0.tgz",
+ "integrity": "sha512-540P2c5dYnJlyJxTaSloliZexv8rji6rY8FhQN+WF/82iHQfA23j/xtJx97L+mXOML27EqksSek/g4eK7jaL3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "schema-utils": "^4.0.0",
+ "tapable": "^2.2.1"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minizlib/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true
+ },
+ "node_modules/mkcert": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mkcert/-/mkcert-3.2.0.tgz",
+ "integrity": "sha512-026Eivq9RoOjOuLJGzbhGwXUAjBxRX11Z7Jbm4/7lqT/Av+XNy9SPrJte6+UpEt7i+W3e/HZYxQqlQcqXZWSzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^11.0.0",
+ "node-forge": "^1.3.1"
+ },
+ "bin": {
+ "mkcert": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/mkcert/node_modules/commander": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/multicast-dns": {
+ "version": "7.2.5",
+ "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
+ "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dns-packet": "^5.2.2",
+ "thunky": "^1.0.2"
+ },
+ "bin": {
+ "multicast-dns": "cli.js"
+ }
+ },
+ "node_modules/mustache": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
+ "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "mustache": "bin/mustache"
+ }
+ },
+ "node_modules/mute-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
+ "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/needle": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz",
+ "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.3",
+ "sax": "^1.2.4"
+ },
+ "bin": {
+ "needle": "bin/needle"
+ },
+ "engines": {
+ "node": ">= 4.4.x"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/neverthrow": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-3.2.0.tgz",
+ "integrity": "sha512-AINA32QbYO83L+3CBI6I5lH4LpBSlLwWteJ+uI25s4AQy6g/xz3RZuedmuNo91lLw2rY+AbPEPQdxd7mg1rXoQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/no-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
+ "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lower-case": "^2.0.2",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/node-abi": {
+ "version": "3.87.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
+ "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-abi/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
+ "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/node-exports-info": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
+ "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array.prototype.flatmap": "^1.3.3",
+ "es-errors": "^1.3.0",
+ "object.entries": "^1.1.9",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/node-fetch-h2": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz",
+ "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "http2-client": "^1.2.5"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ }
+ },
+ "node_modules/node-forge": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
+ "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==",
+ "dev": true,
+ "license": "(BSD-3-Clause OR GPL-2.0)",
+ "engines": {
+ "node": ">= 6.13.0"
+ }
+ },
+ "node_modules/node-machine-id": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz",
+ "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/node-readfiles": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz",
+ "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "es6-promise": "^3.2.1"
+ }
+ },
+ "node_modules/node-readfiles/node_modules/es6-promise": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
+ "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.36",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
+ "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-normalize-package-bin": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
+ "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/oas-kit-common": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz",
+ "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "dependencies": {
+ "fast-safe-stringify": "^2.0.7"
+ }
+ },
+ "node_modules/oas-linter": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz",
+ "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "dependencies": {
+ "@exodus/schemasafe": "^1.0.0-rc.2",
+ "should": "^13.2.1",
+ "yaml": "^1.10.0"
+ },
+ "funding": {
+ "url": "https://github.com/Mermade/oas-kit?sponsor=1"
+ }
+ },
+ "node_modules/oas-linter/node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/oas-resolver": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz",
+ "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "dependencies": {
+ "node-fetch-h2": "^2.3.0",
+ "oas-kit-common": "^1.0.8",
+ "reftools": "^1.1.9",
+ "yaml": "^1.10.0",
+ "yargs": "^17.0.1"
+ },
+ "bin": {
+ "resolve": "resolve.js"
+ },
+ "funding": {
+ "url": "https://github.com/Mermade/oas-kit?sponsor=1"
+ }
+ },
+ "node_modules/oas-resolver/node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/oas-schema-walker": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz",
+ "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/Mermade/oas-kit?sponsor=1"
+ }
+ },
+ "node_modules/oas-validator": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz",
+ "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "dependencies": {
+ "call-me-maybe": "^1.0.1",
+ "oas-kit-common": "^1.0.8",
+ "oas-linter": "^3.2.2",
+ "oas-resolver": "^2.5.6",
+ "oas-schema-walker": "^1.1.5",
+ "reftools": "^1.1.9",
+ "should": "^13.2.1",
+ "yaml": "^1.10.0"
+ },
+ "funding": {
+ "url": "https://github.com/Mermade/oas-kit?sponsor=1"
+ }
+ },
+ "node_modules/oas-validator/node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz",
+ "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+ "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
+ "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/obuf": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
+ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/office-addin-cli": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/office-addin-cli/-/office-addin-cli-2.0.6.tgz",
+ "integrity": "sha512-TazLZNGqw8h2S5Hgzp/wiShe7X5oL8o2dkZRVpI/7Ond5RhwT4f/gb5ybvNkOHBG6fwNWicY4bnkXWVOQzYNOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^13.0.0",
+ "node-fetch": "^2.6.1",
+ "read-package-json-fast": "^2.0.2"
+ },
+ "bin": {
+ "office-addin-cli": "cli.js"
+ }
+ },
+ "node_modules/office-addin-cli/node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/office-addin-debugging": {
+ "version": "6.0.6",
+ "resolved": "https://registry.npmjs.org/office-addin-debugging/-/office-addin-debugging-6.0.6.tgz",
+ "integrity": "sha512-YFLOYZ+N7dneT7EVVm8ol4M9AWzD8PpJ6VbNY8cdkASmQbI0AecnOUFjWH3889oLPaIQrcTefzEE6w6sPX4Unw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "adm-zip": "0.5.12",
+ "commander": "^13.0.0",
+ "node-fetch": "^2.6.1",
+ "office-addin-cli": "^2.0.6",
+ "office-addin-dev-certs": "^2.0.6",
+ "office-addin-dev-settings": "^3.0.6",
+ "office-addin-manifest": "^2.1.2",
+ "office-addin-node-debugger": "^1.0.6",
+ "office-addin-usage-data": "^2.0.6"
+ },
+ "bin": {
+ "office-addin-debugging": "cli.js"
+ }
+ },
+ "node_modules/office-addin-debugging/node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/office-addin-dev-certs": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/office-addin-dev-certs/-/office-addin-dev-certs-2.0.6.tgz",
+ "integrity": "sha512-uCD0HA55lQh1Qj78aDz/AehR2NlR4Rs/EIi79RQzNVDDZpKoa4xTchiG6Pjc/zUxmQRUYHvzZA3FG8bE7fauBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^13.0.0",
+ "fs-extra": "^11.2.0",
+ "mkcert": "^3.2.0",
+ "office-addin-cli": "^2.0.6",
+ "office-addin-usage-data": "^2.0.6"
+ },
+ "bin": {
+ "office-addin-dev-certs": "cli.js"
+ }
+ },
+ "node_modules/office-addin-dev-certs/node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/office-addin-dev-certs/node_modules/fs-extra": {
+ "version": "11.3.4",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz",
+ "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/office-addin-dev-settings": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/office-addin-dev-settings/-/office-addin-dev-settings-3.0.6.tgz",
+ "integrity": "sha512-KWsANtxdoYHBo/hlPoE74Y9cCFb0u2J/+hJmLF4LXbugJwFv6btljtEyEAyD79nVSlU/2lor+pByzESMDSkT7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "adm-zip": "0.5.12",
+ "commander": "^13.0.0",
+ "fs-extra": "^11.2.0",
+ "inquirer": "^12.10.0",
+ "junk": "^3.1.0",
+ "office-addin-manifest": "^2.1.2",
+ "office-addin-usage-data": "^2.0.6",
+ "open": "^6.4.0",
+ "whatwg-url": "^14.0.0",
+ "winreg": "1.2.4"
+ },
+ "bin": {
+ "office-addin-dev-settings": "cli.js"
+ },
+ "peerDependencies": {
+ "@microsoft/m365agentstoolkit-cli": "^1.1.1"
+ }
+ },
+ "node_modules/office-addin-dev-settings/node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/office-addin-dev-settings/node_modules/fs-extra": {
+ "version": "11.3.4",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz",
+ "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/office-addin-dev-settings/node_modules/is-wsl": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
+ "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/office-addin-dev-settings/node_modules/open": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz",
+ "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-wsl": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/office-addin-dev-settings/node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/office-addin-dev-settings/node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/office-addin-dev-settings/node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/office-addin-lint": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/office-addin-lint/-/office-addin-lint-3.0.6.tgz",
+ "integrity": "sha512-TSu9spScz0FylNkKeNB3KETBIg/Xbn+8lrAy3C0FovBclgipmlZiKMmuX7DuCvM0IgPCvtMLztB136c4w4ujTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^13.0.0",
+ "eslint": "^9.0.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-office-addins": "^4.0.6",
+ "eslint-plugin-prettier": "^5.1.3",
+ "office-addin-prettier-config": "^2.0.1",
+ "office-addin-usage-data": "^2.0.6",
+ "prettier": "^3.2.5",
+ "typescript-eslint": "^8.4.0"
+ },
+ "bin": {
+ "office-addin-lint": "lib/cli.js"
+ }
+ },
+ "node_modules/office-addin-lint/node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/office-addin-manifest": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/office-addin-manifest/-/office-addin-manifest-2.1.2.tgz",
+ "integrity": "sha512-/6lQXYZM0n7jPYH+gZfXErqB9v5DLWmpg4UC5osSp8978HtHuO2BwEXj0iiuvfg+FuK8QiHc2MnQRAvE1L0u0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@microsoft/app-manifest": "1.0.1",
+ "adm-zip": "0.5.16",
+ "chalk": "^2.4.2",
+ "commander": "^13.0.0",
+ "fs-extra": "^7.0.1",
+ "node-fetch": "^2.6.1",
+ "office-addin-usage-data": "^2.0.6",
+ "uuid": "^8.3.2",
+ "xml2js": "^0.5.0"
+ },
+ "bin": {
+ "office-addin-manifest": "cli.js"
+ }
+ },
+ "node_modules/office-addin-manifest-converter": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/office-addin-manifest-converter/-/office-addin-manifest-converter-0.4.1.tgz",
+ "integrity": "sha512-2eOdCCYJ5bhCe2p9KKETdg1UNshsKaT0lDU/jNopAg3t7zC1WxwvofTSO/+4Log5L4Re+wUdV8MqrQikZBa7+Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@xmldom/xmldom": "^0.8.5",
+ "commander": "^9.0.0",
+ "terser": "^5.29.1"
+ },
+ "bin": {
+ "office-addin-manifest-converter": "cli.js"
+ }
+ },
+ "node_modules/office-addin-manifest-converter/node_modules/commander": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
+ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": "^12.20.0 || >=14"
+ }
+ },
+ "node_modules/office-addin-manifest/node_modules/@microsoft/app-manifest": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@microsoft/app-manifest/-/app-manifest-1.0.1.tgz",
+ "integrity": "sha512-W4fw8JX/9CPATwNAi9dc25rCK/b3qSnoClVDzGfbYuy6ewY9FHgkwk/C1NzC8k/YwZAsKwMhHOvXUCt3u9ak3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/fs-extra": "^11.0.1",
+ "@types/node-fetch": "^2.6.9",
+ "ajv": "^8.5.0",
+ "ajv-draft-04": "^1.0.0",
+ "ajv-formats": "^3.0.1",
+ "node-fetch": "2.7.0"
+ }
+ },
+ "node_modules/office-addin-manifest/node_modules/adm-zip": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
+ "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0"
+ }
+ },
+ "node_modules/office-addin-manifest/node_modules/ajv": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/office-addin-manifest/node_modules/ajv-draft-04": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
+ "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "ajv": "^8.5.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/office-addin-manifest/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/office-addin-manifest/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/office-addin-manifest/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/office-addin-manifest/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/office-addin-manifest/node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/office-addin-manifest/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/office-addin-manifest/node_modules/fs-extra": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
+ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/office-addin-manifest/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/office-addin-manifest/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/office-addin-manifest/node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "dev": true,
+ "license": "MIT",
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/office-addin-manifest/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/office-addin-manifest/node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/office-addin-node-debugger": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/office-addin-node-debugger/-/office-addin-node-debugger-1.0.6.tgz",
+ "integrity": "sha512-0zhNBlMatX0hNvhwBeIB2x2xFJ6cXjgB8FOksLLGF6wjfgY6hSMCrBsZsIbOXEDyl9x2mKmhBOuNlp5YJ22kHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^13.0.0",
+ "node-fetch": "^2.6.1",
+ "office-addin-usage-data": "^2.0.6",
+ "ws": "^7.4.6"
+ },
+ "bin": {
+ "office-addin-node-debugger": "cli.js"
+ }
+ },
+ "node_modules/office-addin-node-debugger/node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/office-addin-prettier-config": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/office-addin-prettier-config/-/office-addin-prettier-config-2.0.1.tgz",
+ "integrity": "sha512-3jvZMQ4iTiQ8KVCMEjEWaot3UB1bIqtGQbA03dp1KK3Z/I+seV08Yn+hW9hQm2Zy+cVlJjTa7EZECwdxlbWRbQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/office-addin-project": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/office-addin-project/-/office-addin-project-1.0.6.tgz",
+ "integrity": "sha512-gTpCgX2g+iBGMKdwcyErVeDd/1jC692NyBUV3uBDgOv0C4+4aDWAQsL9MoAtAh+ZfI2YKFYsJavB5bbNsEG40Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "adm-zip": "0.5.12",
+ "commander": "^13.0.0",
+ "fs-extra": "^7.0.1",
+ "inquirer": "^12.10.0",
+ "office-addin-manifest": "^2.1.2",
+ "office-addin-manifest-converter": "^0.4.1",
+ "office-addin-usage-data": "^2.0.6"
+ },
+ "bin": {
+ "office-addin-project": "cli.js"
+ }
+ },
+ "node_modules/office-addin-project/node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/office-addin-project/node_modules/fs-extra": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
+ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/office-addin-project/node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/office-addin-project/node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/office-addin-usage-data": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/office-addin-usage-data/-/office-addin-usage-data-2.0.6.tgz",
+ "integrity": "sha512-yOWCJKIpplFIVAFN9BAjwTYZqgMv2lGh2Dcd2BICRCoqQSfCvUsf6dRfIsMk5TcG0Ns6MLRWUUepWtX3aokBdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "applicationinsights": "^1.7.3",
+ "commander": "^13.0.0",
+ "readline-sync": "^1.4.9",
+ "uuid": "8.3.2"
+ },
+ "bin": {
+ "office-addin-usage-data": "cli.js"
+ }
+ },
+ "node_modules/office-addin-usage-data/node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/open": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
+ "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "define-lazy-prop": "^2.0.0",
+ "is-docker": "^2.1.1",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/openapi-types": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-7.2.3.tgz",
+ "integrity": "sha512-olbaNxz12R27+mTyJ/ZAFEfUruauHH27AkeQHDHRq5AF0LdNkK1SSV7EourXQDK+4aX7dv2HtyirAGK06WMAsA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/original-fs": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/original-fs/-/original-fs-1.2.0.tgz",
+ "integrity": "sha512-IGo+qFumpIV65oDchJrqL0BOk9kr82fObnTesNJt8t3YgP6vfqcmRs0ofPzg3D9PKMeBHt7lrg1k/6L+oFdS8g==",
+ "dev": true,
+ "license": "Unlicense",
+ "peer": true
+ },
+ "node_modules/os-browserify": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+ "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/own-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+ "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.6",
+ "object-keys": "^1.1.1",
+ "safe-push-apply": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-retry": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz",
+ "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/retry": "0.12.2",
+ "is-network-error": "^1.0.0",
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=16.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-retry/node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/param-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
+ "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dot-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-node-version": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
+ "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/pascal-case": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
+ "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/path": {
+ "version": "0.12.7",
+ "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
+ "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "process": "^0.11.1",
+ "util": "^0.10.3"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz",
+ "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pathval": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
+ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz",
+ "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/find-up": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz",
+ "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^7.1.0",
+ "path-exists": "^5.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/locate-path": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz",
+ "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^6.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-limit": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
+ "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-locate": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz",
+ "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/path-exists": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
+ "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/yocto-queue": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
+ "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkijs": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.3.3.tgz",
+ "integrity": "sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@noble/hashes": "1.4.0",
+ "asn1js": "^3.0.6",
+ "bytestreamjs": "^2.0.1",
+ "pvtsutils": "^1.3.6",
+ "pvutils": "^1.1.3",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-modules-extract-imports": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
+ "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-local-by-default": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz",
+ "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "icss-utils": "^5.0.0",
+ "postcss-selector-parser": "^7.0.0",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-scope": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz",
+ "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "postcss-selector-parser": "^7.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-values": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
+ "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "icss-utils": "^5.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
+ "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
+ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-linter-helpers": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz",
+ "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-diff": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/pretty-error": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
+ "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.20",
+ "renderkid": "^3.0.0"
+ }
+ },
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/proper-lockfile": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
+ "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "retry": "^0.12.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "node_modules/proper-lockfile/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/prr": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+ "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pump": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
+ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pvtsutils": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
+ "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/pvutils": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz",
+ "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "dev": true,
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/rc/node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-dom/node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/read-package-json-fast": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz",
+ "integrity": "sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "json-parse-even-better-errors": "^2.3.0",
+ "npm-normalize-package-bin": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/readline-sync": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
+ "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/rechoir": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
+ "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve": "^1.20.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/reflect-metadata": {
+ "version": "0.1.14",
+ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz",
+ "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true
+ },
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+ "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.7",
+ "get-proto": "^1.0.1",
+ "which-builtin-type": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/reftools": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz",
+ "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/Mermade/oas-kit?sponsor=1"
+ }
+ },
+ "node_modules/regenerate": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
+ "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regenerate-unicode-properties": {
+ "version": "10.2.2",
+ "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz",
+ "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+ "license": "MIT"
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexpu-core": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz",
+ "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2",
+ "regenerate-unicode-properties": "^10.2.2",
+ "regjsgen": "^0.8.0",
+ "regjsparser": "^0.13.0",
+ "unicode-match-property-ecmascript": "^2.0.0",
+ "unicode-match-property-value-ecmascript": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regjsgen": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz",
+ "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regjsparser": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz",
+ "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "jsesc": "~3.1.0"
+ },
+ "bin": {
+ "regjsparser": "bin/parser"
+ }
+ },
+ "node_modules/relateurl": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
+ "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/renderkid": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
+ "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-select": "^4.1.3",
+ "dom-converter": "^0.2.0",
+ "htmlparser2": "^6.1.0",
+ "lodash": "^4.17.21",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "node_modules/renderkid/node_modules/css-select": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
+ "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.0.1",
+ "domhandler": "^4.3.1",
+ "domutils": "^2.8.0",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/renderkid/node_modules/dom-serializer": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
+ "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "domhandler": "^4.2.0",
+ "entities": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/renderkid/node_modules/domhandler": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
+ "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.2.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/renderkid/node_modules/domutils": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
+ "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^1.0.1",
+ "domelementtype": "^2.2.0",
+ "domhandler": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/renderkid/node_modules/entities": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
+ "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/renderkid/node_modules/htmlparser2": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
+ "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
+ "dev": true,
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "domhandler": "^4.0.0",
+ "domutils": "^2.5.2",
+ "entities": "^2.0.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/requireindex": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
+ "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.5"
+ }
+ },
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-cwd/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rtl-css-js": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz",
+ "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.1.2"
+ }
+ },
+ "node_modules/run-applescript": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
+ "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/run-async": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
+ "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
+ "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "has-symbols": "^1.1.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safe-push-apply": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/sax": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz",
+ "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=11.0.0"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/schema-utils": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
+ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "ajv": "^8.9.0",
+ "ajv-formats": "^2.1.1",
+ "ajv-keywords": "^5.1.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/schema-utils/node_modules/ajv": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/schema-utils/node_modules/ajv-formats": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
+ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/schema-utils/node_modules/ajv-keywords": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3"
+ },
+ "peerDependencies": {
+ "ajv": "^8.8.2"
+ }
+ },
+ "node_modules/schema-utils/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/select-hose": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
+ "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/selfsigned": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz",
+ "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/x509": "^1.14.2",
+ "pkijs": "^3.3.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/serve-index": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz",
+ "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "batch": "0.6.1",
+ "debug": "2.6.9",
+ "escape-html": "~1.0.3",
+ "http-errors": "~1.8.0",
+ "mime-types": "~2.1.35",
+ "parseurl": "~1.3.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-index/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/serve-index/node_modules/depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-index/node_modules/http-errors": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+ "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-index/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/serve-index/node_modules/statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-proto": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
+ "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/shallow-clone": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
+ "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
+ "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/shimmer": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
+ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/should": {
+ "version": "13.2.3",
+ "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz",
+ "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "should-equal": "^2.0.0",
+ "should-format": "^3.0.3",
+ "should-type": "^1.4.0",
+ "should-type-adaptors": "^1.0.1",
+ "should-util": "^1.0.0"
+ }
+ },
+ "node_modules/should-equal": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz",
+ "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "should-type": "^1.4.0"
+ }
+ },
+ "node_modules/should-format": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz",
+ "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "should-type": "^1.3.0",
+ "should-type-adaptors": "^1.0.1"
+ }
+ },
+ "node_modules/should-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz",
+ "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/should-type-adaptors": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz",
+ "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "should-type": "^1.3.0",
+ "should-util": "^1.0.0"
+ }
+ },
+ "node_modules/should-util": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz",
+ "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/slash": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
+ "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/sockjs": {
+ "version": "0.3.24",
+ "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
+ "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "faye-websocket": "^0.11.3",
+ "uuid": "^8.3.2",
+ "websocket-driver": "^0.7.4"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-loader": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz",
+ "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "^0.6.3",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.72.1"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/spdy": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
+ "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.0",
+ "handle-thing": "^2.0.0",
+ "http-deceiver": "^1.2.7",
+ "select-hose": "^2.0.0",
+ "spdy-transport": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/spdy-transport": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz",
+ "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.0",
+ "detect-node": "^2.0.4",
+ "hpack.js": "^2.1.6",
+ "obuf": "^1.1.2",
+ "readable-stream": "^3.0.6",
+ "wbuf": "^1.7.3"
+ }
+ },
+ "node_modules/stack-chain": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz",
+ "integrity": "sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.12",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
+ "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "regexp.prototype.flags": "^1.5.3",
+ "set-function-name": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.repeat": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
+ "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
+ "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-data-property": "^1.1.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-object-atoms": "^1.0.0",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
+ "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
+ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strnum": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz",
+ "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/style-loader": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz",
+ "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.27.0"
+ }
+ },
+ "node_modules/stylis": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
+ "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
+ "license": "MIT"
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/swagger2openapi": {
+ "version": "7.0.8",
+ "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz",
+ "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "dependencies": {
+ "call-me-maybe": "^1.0.1",
+ "node-fetch": "^2.6.1",
+ "node-fetch-h2": "^2.3.0",
+ "node-readfiles": "^0.2.0",
+ "oas-kit-common": "^1.0.8",
+ "oas-resolver": "^2.5.6",
+ "oas-schema-walker": "^1.1.5",
+ "oas-validator": "^5.0.8",
+ "reftools": "^1.1.9",
+ "yaml": "^1.10.0",
+ "yargs": "^17.0.1"
+ },
+ "bin": {
+ "boast": "boast.js",
+ "oas-validate": "oas-validate.js",
+ "swagger2openapi": "swagger2openapi.js"
+ },
+ "funding": {
+ "url": "https://github.com/Mermade/oas-kit?sponsor=1"
+ }
+ },
+ "node_modules/swagger2openapi/node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/synckit": {
+ "version": "0.11.12",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz",
+ "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@pkgr/core": "^0.2.9"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/synckit"
+ }
+ },
+ "node_modules/tabster": {
+ "version": "8.7.0",
+ "resolved": "https://registry.npmjs.org/tabster/-/tabster-8.7.0.tgz",
+ "integrity": "sha512-AKYquti8AdWzuqJdQo4LUMQDZrHoYQy6V+8yUq2PmgLZV10EaB+8BD0nWOfC/3TBp4mPNg4fbHkz6SFtkr0PpA==",
+ "license": "MIT",
+ "dependencies": {
+ "keyborg": "2.6.0",
+ "tslib": "^2.8.1"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-linux-x64-gnu": "4.53.3"
+ }
+ },
+ "node_modules/tapable": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
+ "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tar": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+ "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-fs/node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true
+ },
+ "node_modules/terser": {
+ "version": "5.46.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
+ "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.15.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser-webpack-plugin": {
+ "version": "5.3.17",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz",
+ "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jest-worker": "^27.4.5",
+ "schema-utils": "^4.3.0",
+ "terser": "^5.31.1"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "uglify-js": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/terser/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/thingies": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz",
+ "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "^2"
+ }
+ },
+ "node_modules/thunky": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
+ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tmp": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+ "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tree-dump": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz",
+ "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
+ "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/ts-loader": {
+ "version": "9.5.4",
+ "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz",
+ "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "enhanced-resolve": "^5.0.0",
+ "micromatch": "^4.0.0",
+ "semver": "^7.3.4",
+ "source-map": "^0.7.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "typescript": "*",
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/ts-loader/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ts-loader/node_modules/source-map": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
+ "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/tsyringe": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz",
+ "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/tsyringe/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
+ "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "reflect.getprototypeof": "^1.0.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz",
+ "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "is-typed-array": "^1.1.13",
+ "possible-typed-array-names": "^1.0.0",
+ "reflect.getprototypeof": "^1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typedi": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/typedi/-/typedi-0.10.0.tgz",
+ "integrity": "sha512-v3UJF8xm68BBj6AF4oQML3ikrfK2c9EmZUyLOfShpJuItAqVBHWP/KtpGinkSsIiP6EZyyb6Z3NXyW9dgS9X1w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz",
+ "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.56.1",
+ "@typescript-eslint/parser": "8.56.1",
+ "@typescript-eslint/typescript-estree": "8.56.1",
+ "@typescript-eslint/utils": "8.56.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/uglify-js": {
+ "version": "3.19.3",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/uhyphen": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",
+ "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "which-boxed-primitive": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/underscore": {
+ "version": "1.13.8",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
+ "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unicode-canonical-property-names-ecmascript": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
+ "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-ecmascript": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
+ "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "unicode-canonical-property-names-ecmascript": "^2.0.0",
+ "unicode-property-aliases-ecmascript": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-value-ecmascript": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz",
+ "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-property-aliases-ecmascript": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz",
+ "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicorn-magic": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+ "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/util": {
+ "version": "0.10.4",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+ "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "inherits": "2.0.3"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/util/node_modules/inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true
+ },
+ "node_modules/utila": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
+ "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/validator": {
+ "version": "13.15.26",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
+ "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vscode-jsonrpc": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-4.0.0.tgz",
+ "integrity": "sha512-perEnXQdQOJMTDFNv+UF3h1Y0z4iSiaN9jIlb0OqIYgosPCZGYh/MCUlkFtV2668PL69lRDO32hmvL2yiidUYg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8.0.0 || >=10.0.0"
+ }
+ },
+ "node_modules/watchpack": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
+ "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/wbuf": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz",
+ "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/webpack": {
+ "version": "5.105.4",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz",
+ "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/eslint-scope": "^3.7.7",
+ "@types/estree": "^1.0.8",
+ "@types/json-schema": "^7.0.15",
+ "@webassemblyjs/ast": "^1.14.1",
+ "@webassemblyjs/wasm-edit": "^1.14.1",
+ "@webassemblyjs/wasm-parser": "^1.14.1",
+ "acorn": "^8.16.0",
+ "acorn-import-phases": "^1.0.3",
+ "browserslist": "^4.28.1",
+ "chrome-trace-event": "^1.0.2",
+ "enhanced-resolve": "^5.20.0",
+ "es-module-lexer": "^2.0.0",
+ "eslint-scope": "5.1.1",
+ "events": "^3.2.0",
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.2.11",
+ "json-parse-even-better-errors": "^2.3.1",
+ "loader-runner": "^4.3.1",
+ "mime-types": "^2.1.27",
+ "neo-async": "^2.6.2",
+ "schema-utils": "^4.3.3",
+ "tapable": "^2.3.0",
+ "terser-webpack-plugin": "^5.3.17",
+ "watchpack": "^2.5.1",
+ "webpack-sources": "^3.3.4"
+ },
+ "bin": {
+ "webpack": "bin/webpack.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependenciesMeta": {
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-cli": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",
+ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@discoveryjs/json-ext": "^0.5.0",
+ "@webpack-cli/configtest": "^2.1.1",
+ "@webpack-cli/info": "^2.0.2",
+ "@webpack-cli/serve": "^2.0.5",
+ "colorette": "^2.0.14",
+ "commander": "^10.0.1",
+ "cross-spawn": "^7.0.3",
+ "envinfo": "^7.7.3",
+ "fastest-levenshtein": "^1.0.12",
+ "import-local": "^3.0.2",
+ "interpret": "^3.1.1",
+ "rechoir": "^0.8.0",
+ "webpack-merge": "^5.7.3"
+ },
+ "bin": {
+ "webpack-cli": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x"
+ },
+ "peerDependenciesMeta": {
+ "@webpack-cli/generators": {
+ "optional": true
+ },
+ "webpack-bundle-analyzer": {
+ "optional": true
+ },
+ "webpack-dev-server": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-dev-middleware": {
+ "version": "7.4.5",
+ "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz",
+ "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "colorette": "^2.0.10",
+ "memfs": "^4.43.1",
+ "mime-types": "^3.0.1",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "schema-utils": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-dev-middleware/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/webpack-dev-middleware/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/webpack-dev-server": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.3.tgz",
+ "integrity": "sha512-9Gyu2F7+bg4Vv+pjbovuYDhHX+mqdqITykfzdM9UyKqKHlsE5aAjRhR+oOEfXW5vBeu8tarzlJFIZva4ZjAdrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/bonjour": "^3.5.13",
+ "@types/connect-history-api-fallback": "^1.5.4",
+ "@types/express": "^4.17.25",
+ "@types/express-serve-static-core": "^4.17.21",
+ "@types/serve-index": "^1.9.4",
+ "@types/serve-static": "^1.15.5",
+ "@types/sockjs": "^0.3.36",
+ "@types/ws": "^8.5.10",
+ "ansi-html-community": "^0.0.8",
+ "bonjour-service": "^1.2.1",
+ "chokidar": "^3.6.0",
+ "colorette": "^2.0.10",
+ "compression": "^1.8.1",
+ "connect-history-api-fallback": "^2.0.0",
+ "express": "^4.22.1",
+ "graceful-fs": "^4.2.6",
+ "http-proxy-middleware": "^2.0.9",
+ "ipaddr.js": "^2.1.0",
+ "launch-editor": "^2.6.1",
+ "open": "^10.0.3",
+ "p-retry": "^6.2.0",
+ "schema-utils": "^4.2.0",
+ "selfsigned": "^5.5.0",
+ "serve-index": "^1.9.1",
+ "sockjs": "^0.3.24",
+ "spdy": "^4.0.2",
+ "webpack-dev-middleware": "^7.4.2",
+ "ws": "^8.18.0"
+ },
+ "bin": {
+ "webpack-dev-server": "bin/webpack-dev-server.js"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "webpack": {
+ "optional": true
+ },
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/ipaddr.js": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
+ "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/open": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
+ "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "default-browser": "^5.2.1",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "wsl-utils": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/ws": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-merge": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz",
+ "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone-deep": "^4.0.1",
+ "flat": "^5.0.2",
+ "wildcard": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/webpack-sources": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz",
+ "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/webpack/node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/webpack/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/websocket-driver": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
+ "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "http-parser-js": ">=0.5.1",
+ "safe-buffer": ">=5.1.0",
+ "websocket-extensions": ">=0.1.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/websocket-extensions": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
+ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.1.0",
+ "is-finalizationregistry": "^1.1.0",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.2.1",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.1.0",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.20",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
+ "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/wildcard": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz",
+ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/winreg": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz",
+ "integrity": "sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true
+ },
+ "node_modules/ws": {
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/wsl-utils": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
+ "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-wsl": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/wsl-utils/node_modules/is-wsl": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
+ "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-inside-container": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/xml2js": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
+ "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yaml": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
+ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/eemeli"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yoctocolors-cjs": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
+ "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/hermes-plugin/package.json b/hermes-plugin/package.json
new file mode 100644
index 000000000..90b270397
--- /dev/null
+++ b/hermes-plugin/package.json
@@ -0,0 +1,84 @@
+{
+ "name": "office-addin-taskpane-react",
+ "version": "0.0.1",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/OfficeDev/Office-Addin-TaskPane-React.git"
+ },
+ "license": "MIT",
+ "config": {
+ "app_to_debug": "word",
+ "app_type_to_debug": "desktop",
+ "dev_server_port": 3000
+ },
+ "scripts": {
+ "build": "webpack --mode production",
+ "build:dev": "webpack --mode development",
+ "dev-server": "webpack serve --mode development",
+ "lint": "office-addin-lint check",
+ "lint:fix": "office-addin-lint fix",
+ "prettier": "office-addin-lint prettier",
+ "signin": "office-addin-dev-settings m365-account login",
+ "signout": "office-addin-dev-settings m365-account logout",
+ "start": "office-addin-debugging start manifest.xml",
+ "stop": "office-addin-debugging stop manifest.xml",
+ "validate": "office-addin-manifest validate manifest.xml",
+ "watch": "webpack --mode development --watch"
+ },
+ "dependencies": {
+ "@fluentui/react-components": "^9.72.4",
+ "@fluentui/react-icons": "^2.0.313",
+ "core-js": "^3.46.0",
+ "es6-promise": "^4.2.8",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "regenerator-runtime": "^0.14.1"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.28.5",
+ "@babel/preset-env": "^7.28.5",
+ "@babel/preset-typescript": "^7.28.5",
+ "@types/office-js": "^1.0.555",
+ "@types/office-runtime": "^1.0.35",
+ "@types/react": "^18.3.26",
+ "@types/react-dom": "^18.2.21",
+ "@types/webpack": "^5.28.5",
+ "acorn": "^8.11.3",
+ "babel-loader": "^9.1.3",
+ "copy-webpack-plugin": "^12.0.2",
+ "css-loader": "^7.1.2",
+ "eslint-plugin-office-addins": "^4.0.5",
+ "eslint-plugin-react": "^7.28.0",
+ "file-loader": "^6.2.0",
+ "html-loader": "^5.0.0",
+ "html-webpack-plugin": "^5.6.0",
+ "less": "^4.4.2",
+ "less-loader": "^12.2.0",
+ "mini-css-extract-plugin": "^2.9.4",
+ "office-addin-cli": "^2.0.5",
+ "office-addin-debugging": "^6.0.5",
+ "office-addin-dev-certs": "^2.0.5",
+ "office-addin-lint": "^3.0.5",
+ "office-addin-manifest": "^2.1.1",
+ "office-addin-prettier-config": "^2.0.1",
+ "os-browserify": "^0.3.0",
+ "process": "^0.11.10",
+ "source-map-loader": "^5.0.0",
+ "style-loader": "^4.0.0",
+ "ts-loader": "^9.5.1",
+ "typescript": "^5.9.3",
+ "webpack": "^5.102.1",
+ "webpack-cli": "^5.1.4",
+ "webpack-dev-server": "^5.2.2"
+ },
+ "overrides": {
+ "tmp": "^0.2.4",
+ "tar": "^6.2.1",
+ "markdown-it": "^14.1.0"
+ },
+ "prettier": "office-addin-prettier-config",
+ "browserslist": [
+ "last 2 versions",
+ "ie 11"
+ ]
+}
diff --git a/hermes-plugin/set_keywords_helper.js b/hermes-plugin/set_keywords_helper.js
new file mode 100644
index 000000000..50cc796bb
--- /dev/null
+++ b/hermes-plugin/set_keywords_helper.js
@@ -0,0 +1,29 @@
+/**
+ * Helper function to set document keywords using Word's JavaScript API
+ * This should be called from within the Word add-in context
+ */
+
+function setDocumentKeywords(keywords) {
+ return new Promise((resolve, reject) => {
+ Word.run(async (context) => {
+ try {
+ // Set keywords using Word's API
+ context.document.properties.keywords = keywords;
+
+ // Sync the changes
+ await context.sync();
+
+ console.log('[SUCCESS] Keywords set via Word API:', keywords);
+ resolve(true);
+ } catch (error) {
+ console.error('[ERROR] Failed to set keywords via Word API:', error);
+ reject(error);
+ }
+ });
+ });
+}
+
+// Export for use in other modules
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = { setDocumentKeywords };
+}
\ No newline at end of file
diff --git a/hermes-plugin/src/auth-callback.html b/hermes-plugin/src/auth-callback.html
new file mode 100644
index 000000000..19ad430de
--- /dev/null
+++ b/hermes-plugin/src/auth-callback.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+ Hermes - Authentication
+
+
+
+
+
+
🔐
+
Processing Authentication
+
Please wait...
+
+
+
+
+
diff --git a/hermes-plugin/src/auth-callback.ts b/hermes-plugin/src/auth-callback.ts
new file mode 100644
index 000000000..16a001d80
--- /dev/null
+++ b/hermes-plugin/src/auth-callback.ts
@@ -0,0 +1,244 @@
+/**
+ * OAuth callback handler script.
+ *
+ * This handles the OAuth callback in the popup window after ALB OIDC
+ * authentication completes and signals success to the parent add-in.
+ *
+ * The actual authentication session is stored in ALB OIDC cookies,
+ * which the add-in accesses via the Storage Access API.
+ */
+
+import { sendAuthResultToOpener } from './taskpane/utils/authPopup';
+
+const AUTH_INIT_DELAY = 300;
+
+interface AuthInfo {
+ success: boolean;
+ email?: string;
+ error?: string;
+}
+
+/**
+ * Gets a cookie value by name.
+ */
+function getCookie(name: string): string | null {
+ const value = `; ${document.cookie}`;
+ const parts = value.split(`; ${name}=`);
+ if (parts.length === 2) {
+ const cookieValue = parts.pop()?.split(';').shift();
+ return cookieValue || null;
+ }
+ return null;
+}
+
+/**
+ * Extracts authentication success information.
+ * We just need to confirm OIDC completed - the session cookie handles auth.
+ */
+function extractAuthInfo(): AuthInfo {
+ // Check for error in URL parameters
+ const urlParams = new URLSearchParams(window.location.search);
+ const error = urlParams.get('error');
+
+ if (error) {
+ return {
+ success: false,
+ error: urlParams.get('error_description') || error
+ };
+ }
+
+ // Check for user_email cookie (set by backend after successful auth)
+ const userEmail = getCookie('user_email');
+
+ if (userEmail) {
+ return {
+ success: true,
+ email: userEmail
+ };
+ }
+
+ // Check if ALB OIDC session cookie exists (indicates auth completed)
+ // The cookie name is set in Terraform: session_cookie_name = "AWSELBAuthSessionCookie"
+ const albCookie = getCookie('AWSELBAuthSessionCookie');
+ if (albCookie) {
+ return {
+ success: true,
+ email: 'authenticated' // We have a session but don't know email from cookie
+ };
+ }
+
+ return {
+ success: false,
+ error: 'Authentication not completed'
+ };
+}
+
+/**
+ * Creates a styled container element with flex layout.
+ */
+function createContainer(): HTMLDivElement {
+ const container = document.createElement('div');
+ Object.assign(container.style, {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: '100vh',
+ backgroundColor: '#1a1a1a',
+ color: '#ffffff',
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
+ textAlign: 'center',
+ padding: '20px'
+ });
+ return container;
+}
+
+/**
+ * Creates an icon element.
+ */
+function createIcon(icon: string, fontSize: string = '48px', marginBottom: string = '20px'): HTMLDivElement {
+ const iconDiv = document.createElement('div');
+ iconDiv.textContent = icon;
+ Object.assign(iconDiv.style, {
+ fontSize,
+ marginBottom
+ });
+ return iconDiv;
+}
+
+/**
+ * Creates a heading element.
+ */
+function createHeading(text: string): HTMLHeadingElement {
+ const heading = document.createElement('h1');
+ heading.textContent = text;
+ Object.assign(heading.style, {
+ fontSize: '24px',
+ fontWeight: '600',
+ marginBottom: '12px'
+ });
+ return heading;
+}
+
+/**
+ * Creates a paragraph element.
+ */
+function createParagraph(
+ text: string,
+ color: string = '#999',
+ fontSize: string = '14px',
+ marginBottom: string = '20px'
+): HTMLParagraphElement {
+ const para = document.createElement('p');
+ para.textContent = text;
+ Object.assign(para.style, {
+ fontSize,
+ color,
+ marginBottom
+ });
+ return para;
+}
+
+/**
+ * Creates a styled button element.
+ */
+function createButton(text: string, onClick: () => void): HTMLButtonElement {
+ const button = document.createElement('button');
+ button.textContent = text;
+ Object.assign(button.style, {
+ padding: '10px 20px',
+ backgroundColor: '#0078d4',
+ color: 'white',
+ border: 'none',
+ borderRadius: '4px',
+ cursor: 'pointer',
+ fontSize: '14px'
+ });
+ button.addEventListener('click', onClick);
+ return button;
+}
+
+/**
+ * Displays success UI using secure DOM manipulation.
+ */
+function showSuccess(email: string): void {
+ // Clear body and create container
+ document.body.textContent = '';
+ const container = createContainer();
+
+ // Add success icon
+ container.appendChild(createIcon('✓'));
+
+ // Add heading
+ container.appendChild(createHeading('Authentication Successful'));
+
+ // Add email message
+ const emailText = email !== 'authenticated' ? `Signed in as ${email}` : 'Signed in';
+ container.appendChild(createParagraph(emailText));
+
+ // Add closing message
+ const closingMsg = document.createElement('p');
+ closingMsg.textContent = 'This window will close automatically...';
+ Object.assign(closingMsg.style, {
+ fontSize: '12px',
+ color: '#666'
+ });
+ container.appendChild(closingMsg);
+
+ document.body.appendChild(container);
+}
+
+/**
+ * Displays error UI using secure DOM manipulation.
+ */
+function showError(error: string): void {
+ // Clear body and create container
+ document.body.textContent = '';
+ const container = createContainer();
+
+ // Add error icon
+ container.appendChild(createIcon('✗'));
+
+ // Add heading
+ container.appendChild(createHeading('Authentication Failed'));
+
+ // Add error message (textContent automatically prevents XSS)
+ container.appendChild(createParagraph(error));
+
+ // Add close button
+ const closeButton = createButton('Close Window', () => window.close());
+ container.appendChild(closeButton);
+
+ document.body.appendChild(container);
+}
+
+/**
+ * Main initialization function.
+ */
+function init(): void {
+ setTimeout(() => {
+ const authInfo = extractAuthInfo();
+
+ if (authInfo.success) {
+ sendAuthResultToOpener(authInfo);
+ showSuccess(authInfo.email || 'authenticated');
+
+ // Close popup after brief delay
+ setTimeout(() => {
+ window.close();
+ }, 1500);
+ } else {
+ console.error('Authentication failed:', authInfo.error);
+ sendAuthResultToOpener(authInfo);
+ showError(authInfo.error || 'Unknown error');
+ }
+ }, AUTH_INIT_DELAY);
+}
+
+// Run when DOM is ready
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+} else {
+ init();
+}
+
diff --git a/hermes-plugin/src/commands/commands.html b/hermes-plugin/src/commands/commands.html
new file mode 100644
index 000000000..9a54dd181
--- /dev/null
+++ b/hermes-plugin/src/commands/commands.html
@@ -0,0 +1,19 @@
+@@ -1,18 +0,0 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hermes-plugin/src/commands/commands.ts b/hermes-plugin/src/commands/commands.ts
new file mode 100644
index 000000000..11ca5f665
--- /dev/null
+++ b/hermes-plugin/src/commands/commands.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
+ * See LICENSE in the project root for license information.
+ */
+
+/* global Office */
+
+Office.onReady(() => {
+ // If needed, Office.js is ready to be called.
+});
+
+/**
+ * Shows a notification when the add-in command is executed.
+ * @param event
+ */
+function action(event: Office.AddinCommands.Event) {
+ const message: Office.NotificationMessageDetails = {
+ type: Office.MailboxEnums.ItemNotificationMessageType.InformationalMessage,
+ message: "Performed action.",
+ icon: "Icon.80x80",
+ persistent: true,
+ };
+
+ // Show a notification message.
+ Office.context.mailbox.item?.notificationMessages.replaceAsync(
+ "ActionPerformanceNotification",
+ message
+ );
+
+ // Be sure to indicate when the add-in command function is complete.
+ event.completed();
+}
+
+// Register the function with Office.
+Office.actions.associate("action", action);
diff --git a/hermes-plugin/src/config.json b/hermes-plugin/src/config.json
new file mode 100644
index 000000000..3a56f667f
--- /dev/null
+++ b/hermes-plugin/src/config.json
@@ -0,0 +1,4 @@
+{
+ "useHostUrl": true,
+ "baseUrl": "https://localhost:8443"
+}
\ No newline at end of file
diff --git a/hermes-plugin/src/safari-init/safari-init.css b/hermes-plugin/src/safari-init/safari-init.css
new file mode 100644
index 000000000..4971c979f
--- /dev/null
+++ b/hermes-plugin/src/safari-init/safari-init.css
@@ -0,0 +1,89 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background-color: #1a1a1a;
+ color: #ffffff;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100vh;
+}
+
+.container {
+ text-align: center;
+ padding: 40px;
+ max-width: 500px;
+}
+
+.icon {
+ font-size: 48px;
+ margin-bottom: 20px;
+}
+
+.spinner {
+ border: 3px solid rgba(255, 255, 255, 0.1);
+ border-top: 3px solid #0078d4;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ animation: spin 1s linear infinite;
+ margin: 0 auto 24px;
+}
+
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+h1 {
+ font-size: 20px;
+ font-weight: 600;
+ margin: 0 0 12px 0;
+}
+
+p {
+ font-size: 14px;
+ color: #999;
+ margin: 0;
+ line-height: 1.5;
+}
+
+.btn {
+ margin-top: 24px;
+ padding: 12px 32px;
+ background-color: #0078d4;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.btn:hover {
+ background-color: #106ebe;
+}
+
+.btn:active {
+ background-color: #005a9e;
+}
+
+.btn:focus {
+ outline: 2px solid #fff;
+ outline-offset: 2px;
+}
+
+.error {
+ color: #ff6b6b;
+}
diff --git a/hermes-plugin/src/safari-init/safari-init.html b/hermes-plugin/src/safari-init/safari-init.html
new file mode 100644
index 000000000..ccd5f5706
--- /dev/null
+++ b/hermes-plugin/src/safari-init/safari-init.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+ Sign In to Hermes
+
+
+
+
+
+
+
diff --git a/hermes-plugin/src/safari-init/safari-init.ts b/hermes-plugin/src/safari-init/safari-init.ts
new file mode 100644
index 000000000..12b021bdf
--- /dev/null
+++ b/hermes-plugin/src/safari-init/safari-init.ts
@@ -0,0 +1,139 @@
+/**
+ * Safari initialization script for authentication flow.
+ *
+ * This page is shown in Safari when initiating authentication,
+ * validates the redirect URL, and redirects to the auth endpoint.
+ */
+
+import './safari-init.css';
+
+// Configuration
+const TRUSTED_DOMAINS = ['login.microsoftonline.com'];
+const MAX_REDIRECT_LENGTH = 2048;
+
+/**
+ * Validates redirect URL to prevent open redirect vulnerability.
+ */
+function isValidRedirectUrl(url: string | null): boolean {
+ if (!url || typeof url !== 'string' || url.length > MAX_REDIRECT_LENGTH) {
+ return false;
+ }
+
+ try {
+ const decodedUrl = decodeURIComponent(url);
+ const parsedUrl = new URL(decodedUrl, window.location.origin);
+
+ // Only allow HTTPS (or same-origin HTTP for local development)
+ if (parsedUrl.protocol !== 'https:' && parsedUrl.origin !== window.location.origin) {
+ return false;
+ }
+
+ // Allow same-origin redirects
+ if (parsedUrl.origin === window.location.origin) {
+ return true;
+ }
+
+ // Allow HTTPS URLs from trusted domains only
+ return parsedUrl.protocol === 'https:' &&
+ TRUSTED_DOMAINS.some(domain =>
+ parsedUrl.hostname === domain ||
+ parsedUrl.hostname.endsWith('.' + domain)
+ );
+ } catch (e) {
+ console.error('URL validation error:', e);
+ return false;
+ }
+}
+
+/**
+ * Creates and returns an element with text content.
+ */
+function createElement(tag: string, text?: string, className?: string): HTMLElement {
+ const element = document.createElement(tag);
+ if (text) element.textContent = text;
+ if (className) element.className = className;
+ return element;
+}
+
+/**
+ * Displays error message using secure DOM manipulation.
+ */
+function showError(title: string, message: string): void {
+ const container = document.getElementById('container');
+ if (!container) return;
+
+ container.textContent = ''; // Clear existing content
+
+ container.appendChild(createElement('div', '⚠️', 'icon'));
+ container.appendChild(createElement('h1', title));
+ container.appendChild(createElement('p', message, 'error'));
+}
+
+/**
+ * Shows loading state with spinner.
+ */
+function showLoading(): void {
+ const container = document.getElementById('container');
+ if (!container) return;
+
+ container.textContent = ''; // Clear existing content
+
+ container.appendChild(createElement('div', '', 'spinner'));
+ container.appendChild(createElement('h1', 'Redirecting...'));
+ container.appendChild(createElement('p', 'Please wait...'));
+}
+
+/**
+ * Shows initial sign-in UI.
+ */
+function showSignIn(onContinue: () => void): void {
+ const container = document.getElementById('container');
+ if (!container) return;
+
+ container.textContent = ''; // Clear existing content
+
+ container.appendChild(createElement('div', '🔐', 'icon'));
+ container.appendChild(createElement('h1', 'Sign In to Hermes'));
+ container.appendChild(createElement('p', 'Click below to continue with authentication.'));
+
+ const button = createElement('button', 'Continue to Sign In', 'btn') as HTMLButtonElement;
+ button.addEventListener('click', onContinue);
+ container.appendChild(button);
+}
+
+/**
+ * Initializes the authentication flow.
+ */
+function init(): void {
+ const urlParams = new URLSearchParams(window.location.search);
+ const redirectTo = urlParams.get('redirect_to');
+
+ // Validate redirect parameter exists
+ if (!redirectTo) {
+ showError('Invalid Request', 'Missing redirect parameter.');
+ return;
+ }
+
+ // Validate redirect URL
+ if (!isValidRedirectUrl(redirectTo)) {
+ showError('Invalid Request', 'Redirect URL is not allowed for security reasons.');
+ return;
+ }
+
+ // Show sign-in UI
+ showSignIn(() => {
+ showLoading();
+
+ // Perform redirect after brief delay to show loading state
+ setTimeout(() => {
+ window.location.href = decodeURIComponent(redirectTo);
+ }, 300);
+ });
+}
+
+// Initialize when DOM is ready
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+} else {
+ init();
+}
diff --git a/hermes-plugin/src/taskpane/components/AddRelatedResourceModal.tsx b/hermes-plugin/src/taskpane/components/AddRelatedResourceModal.tsx
new file mode 100644
index 000000000..f5f649e3a
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/AddRelatedResourceModal.tsx
@@ -0,0 +1,246 @@
+import * as React from "react";
+import {
+ makeStyles,
+ Text,
+ Button,
+ Input,
+ Label,
+ Dialog,
+ DialogTrigger,
+ DialogSurface,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ DialogBody,
+ Field,
+} from "@fluentui/react-components";
+import { RelatedResource, RelatedExternalLink } from "../interfaces/relatedResources";
+import DarkTheme from "../utils/darkTheme";
+import LightTheme from "../utils/lightTheme";
+import { useTheme } from "../utils/themeContext";
+
+const useStyles = makeStyles({
+ dialogContent: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "16px",
+ minWidth: "400px",
+ backgroundColor: DarkTheme.background.elevated,
+ color: DarkTheme.text.primary,
+ },
+
+ fieldContainer: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "8px",
+ },
+
+ urlPreview: {
+ fontSize: "12px",
+ color: DarkTheme.text.tertiary,
+ fontStyle: "italic",
+ wordBreak: "break-all",
+ },
+
+ errorText: {
+ fontSize: "12px",
+ },
+
+ helpText: {
+ fontSize: "12px",
+ color: DarkTheme.text.tertiary,
+ marginTop: "4px",
+ },
+
+ fieldLabel: {
+ "& label": {
+ color: `${DarkTheme.text.primary} !important`,
+ fontSize: "14px !important",
+ fontWeight: "500 !important",
+ },
+ },
+
+ inputField: {
+ backgroundColor: `${DarkTheme.components.input.background} !important`,
+ border: `1px solid ${DarkTheme.border.primary} !important`,
+ borderRadius: "4px !important",
+ color: `${DarkTheme.text.primary} !important`,
+ "&::placeholder": {
+ color: `${DarkTheme.components.input.placeholder} !important`,
+ },
+ },
+
+ primaryButton: {
+ backgroundColor: `${DarkTheme.interactive.primary} !important`,
+ color: `${DarkTheme.text.primary} !important`,
+ border: "none !important",
+ borderRadius: "4px !important",
+ fontWeight: "600 !important",
+ },
+});
+
+interface AddRelatedResourceModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onAdd: (resource: RelatedResource) => Promise;
+}
+
+const AddRelatedResourceModal: React.FC = ({
+ isOpen,
+ onClose,
+ onAdd,
+}) => {
+ const styles = useStyles();
+ const { isDark } = useTheme();
+ const theme = isDark ? DarkTheme : LightTheme;
+ const [isCancelHovered, setIsCancelHovered] = React.useState(false);
+ const [url, setUrl] = React.useState("");
+ const [name, setName] = React.useState("");
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
+ const [urlError, setUrlError] = React.useState("");
+
+ const validateUrl = (urlString: string): boolean => {
+ try {
+ new URL(urlString);
+ return true;
+ } catch {
+ return false;
+ }
+ };
+
+ const handleUrlChange = (value: string) => {
+ setUrl(value);
+ setUrlError("");
+
+ // Auto-generate name from URL if name is empty
+ if (!name.trim() && value.trim()) {
+ try {
+ const urlObj = new URL(value);
+ const hostname = urlObj.hostname.replace(/^www\./, "");
+ setName(hostname);
+ } catch {
+ // Invalid URL, don't auto-generate name
+ }
+ }
+ };
+
+ const handleSubmit = async () => {
+ if (!url.trim()) {
+ setUrlError("URL is required");
+ return;
+ }
+
+ if (!validateUrl(url.trim())) {
+ setUrlError("Please enter a valid URL");
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ const resource: RelatedExternalLink = {
+ url: url.trim(),
+ name: name.trim() || url.trim(),
+ sortOrder: Date.now(), // Temporary sort order, will be adjusted by server
+ };
+
+ await onAdd(resource);
+
+ // Reset form
+ setUrl("");
+ setName("");
+ setUrlError("");
+ } catch (error) {
+ console.error("Failed to add resource:", error);
+ // Error handling is managed by parent component
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleClose = () => {
+ if (!isSubmitting) {
+ setUrl("");
+ setName("");
+ setUrlError("");
+ onClose();
+ }
+ };
+
+ return (
+ !data.open && handleClose()}>
+
+
+ Add Related Resource
+
+
+ handleUrlChange(data.value)}
+ placeholder="https://example.com"
+ disabled={isSubmitting}
+ />
+
+
+
+ setName(data.value)}
+ placeholder="Display name for the resource"
+ disabled={isSubmitting}
+ />
+
+ If left empty, the URL domain will be used as the name
+
+
+
+ {url && validateUrl(url) && (
+
+ Preview: {name.trim() || new URL(url).hostname.replace(/^www\./, "")}
+
+ )}
+
+
+ setIsCancelHovered(true)}
+ onMouseLeave={() => setIsCancelHovered(false)}
+ style={{
+ backgroundColor: isCancelHovered ? theme.background.tertiary : "transparent",
+ color: isCancelHovered ? theme.text.primary : theme.text.secondary,
+ border: `1px solid ${isCancelHovered ? theme.border.secondary : theme.border.primary}`,
+ borderRadius: "4px",
+ fontWeight: "500",
+ }}
+ >
+ Cancel
+
+
+ {isSubmitting ? "Adding..." : "Add Resource"}
+
+
+
+
+
+ );
+};
+
+export default AddRelatedResourceModal;
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/components/AddResourceForm.tsx b/hermes-plugin/src/taskpane/components/AddResourceForm.tsx
new file mode 100644
index 000000000..f57ea62b5
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/AddResourceForm.tsx
@@ -0,0 +1,447 @@
+import * as React from "react";
+import {
+ makeStyles,
+ mergeClasses,
+ Text,
+ Button,
+ Input,
+ Field,
+ ProgressBar,
+} from "@fluentui/react-components";
+import { Dismiss24Regular, Checkmark24Regular, Document24Regular, Link24Regular } from "@fluentui/react-icons";
+import { RelatedResource, RelatedExternalLink, RelatedHermesDocument } from "../interfaces/relatedResources";
+import IDocumentMetadata from "../interfaces/documentMetadata";
+import DocumentThumbnail from "./DocumentThumbnail";
+import DarkTheme from "../utils/darkTheme";
+import LightTheme from "../utils/lightTheme";
+import { useTheme } from "../utils/themeContext";
+
+const useStyles = makeStyles({
+ container: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "16px",
+ padding: "0",
+ },
+
+ formRow: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "8px",
+ },
+
+ buttonRow: {
+ display: "flex",
+ gap: "8px",
+ justifyContent: "flex-end",
+ paddingTop: "8px",
+ },
+
+ button: {
+ minWidth: "auto",
+ padding: "6px 12px",
+ borderRadius: "4px !important",
+ fontWeight: "600 !important",
+ },
+
+ cancelButton: {
+ minWidth: "auto",
+ padding: "6px 12px",
+ },
+
+ addButton: {
+ backgroundColor: `${DarkTheme.interactive.primary} !important`,
+ color: `${DarkTheme.text.primary} !important`,
+ border: "none !important",
+ },
+
+ helpText: {
+ fontSize: "12px",
+ color: DarkTheme.text.tertiary,
+ marginTop: "4px",
+ },
+
+ searchResult: {
+ padding: "12px 0",
+ cursor: "pointer",
+ display: "flex",
+ alignItems: "flex-start",
+ gap: "12px",
+ transition: "background-color 0.1s ease",
+ ":last-child": {
+ borderBottom: "none",
+ },
+ },
+
+ searchResultContent: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "4px",
+ flex: 1,
+ minWidth: 0, // Allow text to truncate
+ },
+
+ searchResults: {
+ display: "flex",
+ flexDirection: "column",
+ maxHeight: "240px",
+ overflowY: "auto",
+ marginTop: "8px",
+ },
+
+ loadingContainer: {
+ display: "flex",
+ justifyContent: "center",
+ padding: "16px 0",
+ },
+
+ documentTitle: {
+ fontSize: "14px",
+ fontWeight: 500,
+ lineHeight: "1.4",
+ },
+
+ documentSubtitle: {
+ fontSize: "13px",
+ lineHeight: "1.2",
+ },
+
+ errorContainer: {
+ display: "flex",
+ alignItems: "center",
+ padding: "8px 12px",
+ borderRadius: "6px",
+ marginTop: "4px",
+ },
+ errorText: {
+ fontSize: "14px",
+ fontWeight: 400,
+ },
+
+ fieldLabel: {
+ "& label": {
+ color: `${DarkTheme.text.primary} !important`,
+ fontSize: "14px !important",
+ fontWeight: "500 !important",
+ },
+ },
+
+ inputField: {
+ backgroundColor: `${DarkTheme.components.input.background} !important`,
+ border: `1px solid ${DarkTheme.border.primary} !important`,
+ borderRadius: "4px !important",
+ color: `${DarkTheme.text.primary} !important`,
+ "&::placeholder": {
+ color: `${DarkTheme.components.input.placeholder} !important`,
+ },
+ },
+});
+
+
+
+interface AddResourceFormProps {
+ onAdd: (resource: RelatedResource) => Promise;
+ onCancel: () => void;
+ onSearchDocuments?: (query: string) => Promise;
+}
+
+const AddResourceForm: React.FC = ({
+ onAdd,
+ onCancel,
+ onSearchDocuments,
+}) => {
+ const styles = useStyles();
+ const { isDark } = useTheme();
+ const theme = isDark ? DarkTheme : LightTheme;
+ const [isCancelHovered, setIsCancelHovered] = React.useState(false);
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
+
+ // Single input state that handles both URLs and search
+ const [inputValue, setInputValue] = React.useState("");
+ const [isUrl, setIsUrl] = React.useState(false);
+ const [urlName, setUrlName] = React.useState("");
+ const [showUrlNameInput, setShowUrlNameInput] = React.useState(false);
+
+ // Search state
+ const [searchResults, setSearchResults] = React.useState([]);
+ const [isSearching, setIsSearching] = React.useState(false);
+
+ // Error states
+ const [error, setError] = React.useState("");
+ const [searchTimeout, setSearchTimeout] = React.useState(null);
+
+ const validateUrl = (urlString: string): boolean => {
+ try {
+ new URL(urlString);
+ return true;
+ } catch {
+ return false;
+ }
+ };
+
+ const getOwnerDisplayName = (email: string): string => {
+ if (!email) return 'Unknown Owner';
+
+ // Extract name part from email and format it
+ const namePart = email.split('@')[0];
+
+ // Convert formats like "john.doe" or "john_doe" to "John Doe"
+ return namePart
+ .split(/[._-]/)
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
+ .join(' ');
+ };
+
+ const handleInputChange = (value: string) => {
+ setInputValue(value);
+ setError("");
+
+ const isValidUrl = validateUrl(value.trim());
+ setIsUrl(isValidUrl);
+
+ if (isValidUrl) {
+ // Auto-generate name from URL
+ try {
+ const urlObj = new URL(value);
+ const hostname = urlObj.hostname.replace(/^www\./, "");
+ setUrlName(hostname);
+ setShowUrlNameInput(true);
+ setSearchResults([]); // Clear search results when URL is detected
+ } catch {
+ // Should not happen since validateUrl passed
+ }
+ } else {
+ setShowUrlNameInput(false);
+
+ // Handle document search
+ if (onSearchDocuments && value.trim().length >= 2) {
+ // Clear previous timeout
+ if (searchTimeout) {
+ clearTimeout(searchTimeout);
+ }
+
+ // Debounce search
+ const timeout = setTimeout(async () => {
+ setIsSearching(true);
+ try {
+ const results = await onSearchDocuments(value.trim());
+ setSearchResults(results || []);
+ } catch (error) {
+ console.error("Document search failed:", error);
+ const errorMessage = error instanceof Error ? error.message : "Search failed - please try again";
+ setError(errorMessage);
+ setSearchResults([]);
+ } finally {
+ setIsSearching(false);
+ }
+ }, 300);
+
+ setSearchTimeout(timeout);
+ } else {
+ setSearchResults([]);
+ }
+ }
+ };
+
+ const handleDocumentClick = async (doc: IDocumentMetadata) => {
+ setIsSubmitting(true);
+ setError("");
+
+ try {
+ const resource: RelatedHermesDocument = {
+ FileID: doc.objectID,
+ title: doc.title,
+ documentType: doc.docType,
+ documentNumber: doc.docNumber,
+ sortOrder: Date.now(),
+ createdTime: doc.createdTime,
+ modifiedTime: doc.modifiedTime,
+ product: doc.product,
+ status: doc.status,
+ owners: doc.owners || [],
+ summary: doc.summary,
+ };
+
+ await onAdd(resource);
+
+ // Form will be closed by parent component after successful add
+ } catch (error) {
+ console.error("Failed to add document:", error);
+ const errorMessage = error instanceof Error ? error.message : "Failed to add document - please try again";
+ setError(errorMessage);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleUrlSubmit = async () => {
+ if (!inputValue.trim() || !validateUrl(inputValue.trim())) {
+ setError("Please enter a valid URL");
+ return;
+ }
+
+ setIsSubmitting(true);
+ setError("");
+
+ try {
+ const resource: RelatedExternalLink = {
+ url: inputValue.trim(),
+ name: urlName.trim() || inputValue.trim(),
+ sortOrder: Date.now(),
+ };
+
+ await onAdd(resource);
+
+ // Form will be closed by parent component after successful add
+ } catch (error) {
+ console.error("Failed to add URL:", error);
+ const errorMessage = error instanceof Error ? error.message : "Failed to add URL - please try again";
+ setError(errorMessage);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ React.useEffect(() => {
+ return () => {
+ if (searchTimeout) {
+ clearTimeout(searchTimeout);
+ }
+ };
+ }, [searchTimeout]);
+
+ return (
+
+
+ Add Related Resource
+
+
+
+
+ handleInputChange(data.value)}
+ placeholder="Type document name or paste URL..."
+ disabled={isSubmitting}
+ size="small"
+ className={styles.inputField}
+ />
+
+
+
+ {showUrlNameInput && (
+
+
+ setUrlName(data.value)}
+ placeholder="Name for this resource"
+ disabled={isSubmitting}
+ size="small"
+ className={styles.inputField}
+ />
+
+
+ }
+ onClick={onCancel}
+ disabled={isSubmitting}
+ size="small"
+ onMouseEnter={() => setIsCancelHovered(true)}
+ onMouseLeave={() => setIsCancelHovered(false)}
+ style={{
+ backgroundColor: isCancelHovered ? theme.background.tertiary : "transparent",
+ color: isCancelHovered ? theme.text.primary : theme.text.secondary,
+ border: `1px solid ${isCancelHovered ? theme.border.secondary : theme.border.primary}`,
+ }}
+ >
+ Cancel
+
+ }
+ onClick={handleUrlSubmit}
+ disabled={isSubmitting}
+ size="small"
+ >
+ {isSubmitting ? "Adding..." : "Add URL"}
+
+
+
+ )}
+
+ {isSearching && !isUrl && (
+
+ )}
+
+ {searchResults.length > 0 && !isUrl && (
+
+ {searchResults.map((doc) => (
+
handleDocumentClick(doc)}
+ style={{ cursor: isSubmitting ? 'not-allowed' : 'pointer' }}
+ >
+
+
+
+ {doc.title}
+
+
+ {doc.docType} · {doc.docNumber} · {doc.owners && doc.owners.length > 0 ? getOwnerDisplayName(doc.owners[0]) : 'Unknown Owner'}
+
+
+
+ ))}
+
+ )}
+
+ {inputValue.length >= 2 && !isSearching && searchResults.length === 0 && !isUrl && (
+
+ No documents found matching "{inputValue}"
+
+ )}
+
+ {error && (
+
+
+ {error}
+
+
+ )}
+
+ {!showUrlNameInput && (
+
+ }
+ onClick={onCancel}
+ disabled={isSubmitting}
+ size="small"
+ onMouseEnter={() => setIsCancelHovered(true)}
+ onMouseLeave={() => setIsCancelHovered(false)}
+ style={{
+ backgroundColor: isCancelHovered ? theme.background.tertiary : "transparent",
+ color: isCancelHovered ? theme.text.primary : theme.text.secondary,
+ border: `1px solid ${isCancelHovered ? theme.border.secondary : theme.border.primary}`,
+ }}
+ >
+ Cancel
+
+
+ )}
+
+ );
+};
+
+export default AddResourceForm;
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/components/App.tsx b/hermes-plugin/src/taskpane/components/App.tsx
new file mode 100644
index 000000000..b9c99cee7
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/App.tsx
@@ -0,0 +1,647 @@
+import * as React from "react";
+import { makeStyles, ProgressBar, Button, Text, FluentProvider, webDarkTheme, webLightTheme } from "@fluentui/react-components";
+import Sidebar from "./Sidebar";
+import WordPluginController, { DocumentManageStatus } from "../utils/wordPluginController";
+import DarkTheme from "../utils/darkTheme";
+import LightTheme from "../utils/lightTheme";
+import { ThemeProvider, useTheme } from "../utils/themeContext";
+import { authenticateWithPopup, checkAuthStatus, grantStorageAccess, initializeStorageAccess } from "../utils/authPopup";
+import { HERMES_AUTH_REQUIRED_EVENT } from "../utils/hermesClient";
+
+interface AppProps {
+ controller: WordPluginController;
+}
+
+const useStyles = makeStyles({
+ root: {
+ minHeight: "100vh",
+ backgroundColor: DarkTheme.background.primary,
+ color: DarkTheme.text.primary,
+ },
+
+ loadingDiv: {
+ display: "flex",
+ justifyItems: "center",
+ alignContent: "center",
+ alignItems: "center",
+ paddingTop: "40vh",
+ backgroundColor: DarkTheme.background.primary,
+ },
+
+ authContainer: {
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ padding: "32px 16px",
+ textAlign: "center",
+ gap: "16px",
+ backgroundColor: DarkTheme.background.primary,
+ minHeight: "100vh",
+ },
+
+ authTitle: {
+ fontSize: "18px",
+ fontWeight: "bold",
+ color: DarkTheme.text.primary,
+ },
+
+ authDescription: {
+ fontSize: "14px",
+ color: DarkTheme.text.secondary,
+ lineHeight: "1.4",
+ },
+
+ retryContainer: {
+ display: "flex",
+ alignItems: "center",
+ gap: "8px",
+ marginTop: "8px",
+ },
+
+ notManagedContainer: {
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ padding: "32px 16px",
+ textAlign: "center",
+ gap: "16px",
+ backgroundColor: DarkTheme.background.primary,
+ minHeight: "100vh",
+ },
+
+ notManagedIcon: {
+ fontSize: "48px",
+ color: DarkTheme.text.tertiary,
+ marginBottom: "8px",
+ },
+
+ notManagedTitle: {
+ fontSize: "20px",
+ fontWeight: "bold",
+ color: DarkTheme.text.primary,
+ margin: "0",
+ },
+
+ notManagedDescription: {
+ fontSize: "14px",
+ color: DarkTheme.text.secondary,
+ lineHeight: "1.5",
+ maxWidth: "300px",
+ },
+
+ notManagedActions: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "12px",
+ marginTop: "16px",
+ },
+});
+
+enum DocFetchStatus {
+ Loading,
+ NotManaged,
+ Managed,
+ InternalError,
+}
+
+const AppCore: React.FC = ({ controller }: AppProps) => {
+ const styles = useStyles();
+ const { isDark } = useTheme();
+ const theme = isDark ? DarkTheme : LightTheme;
+
+ const [docStatus, setDocStatus] = React.useState(DocumentManageStatus.Loading);
+ const [isAuthenticating, setIsAuthenticating] = React.useState(false);
+ const [authError, setAuthError] = React.useState(null);
+ const [popupAuthComplete, setPopupAuthComplete] = React.useState(false);
+ const [isGrantingAccess, setIsGrantingAccess] = React.useState(false);
+ const [isInitializing, setIsInitializing] = React.useState(true);
+
+ // Apply data-addon-theme attribute for CSS selectors used by child components
+ React.useEffect(() => {
+ document.body.dataset.addonTheme = isDark ? "dark" : "light";
+ return () => {
+ delete document.body.dataset.addonTheme;
+ };
+ }, [isDark]);
+
+ // Add global styles for Fluent UI components — re-runs when theme changes
+ React.useEffect(() => {
+ const style = document.createElement('style');
+ style.dataset.hermesTheme = "global";
+ style.textContent = `
+ /* Fluent UI Input/Textarea Theme Overrides */
+ .fui-Input,
+ .fui-Textarea,
+ input[class*="fui"],
+ textarea[class*="fui"] {
+ background-color: ${theme.components.input.background} !important;
+ border-color: ${theme.border.primary} !important;
+ color: ${theme.text.primary} !important;
+ }
+
+ .fui-Input:hover,
+ .fui-Textarea:hover,
+ input[class*="fui"]:hover,
+ textarea[class*="fui"]:hover {
+ border-color: ${theme.border.secondary} !important;
+ }
+
+ .fui-Input:focus,
+ .fui-Textarea:focus,
+ input[class*="fui"]:focus,
+ textarea[class*="fui"]:focus,
+ .fui-Input:focus-within,
+ .fui-Textarea:focus-within {
+ border-color: ${theme.border.focus} !important;
+ }
+
+ .fui-Input::placeholder,
+ .fui-Textarea::placeholder,
+ input[class*="fui"]::placeholder,
+ textarea[class*="fui"]::placeholder {
+ color: ${theme.components.input.placeholder} !important;
+ }
+
+ /* Field Labels */
+ .fui-Field label,
+ .fui-Label,
+ label[class*="fui"] {
+ color: ${theme.text.primary} !important;
+ }
+
+ /* Dialog/Modal Theme */
+ .fui-DialogSurface,
+ [class*="DialogSurface"] {
+ background-color: ${theme.background.elevated} !important;
+ color: ${theme.text.primary} !important;
+ }
+
+ .fui-DialogTitle,
+ [class*="DialogTitle"] {
+ color: ${theme.text.primary} !important;
+ }
+
+ /* Button Theme - Primary */
+ .fui-Button[data-fui-appearance="primary"],
+ button[class*="Button--primary"] {
+ background-color: ${theme.interactive.primary} !important;
+ color: ${theme.text.inverse} !important;
+ border: none !important;
+ }
+
+ .fui-Button[data-fui-appearance="primary"]:hover,
+ button[class*="Button--primary"]:hover {
+ background-color: ${theme.interactive.primaryHover} !important;
+ }
+
+ /* Button Theme - Secondary */
+ .fui-Button[data-fui-appearance="secondary"],
+ button[class*="Button--secondary"] {
+ background-color: transparent !important;
+ color: ${theme.text.secondary} !important;
+ border: 1px solid ${theme.border.primary} !important;
+ }
+
+ .fui-Button[data-fui-appearance="secondary"]:hover,
+ button[class*="Button--secondary"]:hover {
+ background-color: ${theme.background.tertiary} !important;
+ color: ${theme.text.primary} !important;
+ border-color: ${theme.border.secondary} !important;
+ }
+
+ /* Error text */
+ .fui-Field__validationMessage[data-state="error"],
+ [class*="validationMessage--error"] {
+ color: ${theme.interactive.danger} !important;
+ }
+
+ /* Container backgrounds — override makeStyles baked-in values */
+ body[data-addon-theme] #container > div,
+ body[data-addon-theme] .fui-FluentProvider {
+ background-color: ${theme.background.primary} !important;
+ color: ${theme.text.primary} !important;
+ }
+
+ /* ── TEXT: override makeStyles-baked dark colors ── */
+
+ /* Fluent UI Text component (renders as ) */
+ .fui-Text {
+ color: ${theme.text.primary} !important;
+ }
+
+ /* Headings */
+ h1, h2, h3, h4, h5, h6 {
+ color: ${theme.text.primary} !important;
+ }
+
+ /* Anchors */
+ a {
+ color: ${theme.text.link} !important;
+ }
+ a:hover {
+ color: ${theme.interactive.primaryHover} !important;
+ }
+
+ /* Force correct colors scoped to light mode only */
+ body[data-addon-theme="light"] {
+ color: ${theme.text.primary};
+ background-color: ${theme.background.primary};
+ }
+ body[data-addon-theme="light"] span,
+ body[data-addon-theme="light"] p,
+ body[data-addon-theme="light"] label,
+ body[data-addon-theme="light"] div:not([class*="fui-Portal"]):not([class*="Badge"]):not([class*="badge"]) {
+ color: ${theme.text.primary};
+ }
+ /* Let explicit !important overrides from status badges/buttons win */
+ body[data-addon-theme="light"] .fui-Text {
+ color: ${theme.text.primary} !important;
+ }
+
+ /* Dropdown / listbox backgrounds in light mode */
+ body[data-addon-theme="light"] .fui-Portal .fui-Dropdown__listbox,
+ body[data-addon-theme="light"] [role="listbox"],
+ body[data-addon-theme="light"] .fui-Listbox {
+ background-color: ${theme.background.elevated} !important;
+ border-color: ${theme.border.primary} !important;
+ color: ${theme.text.primary} !important;
+ }
+ body[data-addon-theme="light"] [role="option"],
+ body[data-addon-theme="light"] .fui-Option {
+ background-color: ${theme.background.elevated} !important;
+ color: ${theme.text.primary} !important;
+ }
+ body[data-addon-theme="light"] [role="option"]:hover,
+ body[data-addon-theme="light"] .fui-Option:hover {
+ background-color: ${theme.background.tertiary} !important;
+ }
+
+ /* Tooltip in light mode */
+ body[data-addon-theme="light"] .fui-Tooltip__content {
+ background-color: ${theme.components.tooltip.background} !important;
+ color: ${theme.components.tooltip.text} !important;
+ border-color: ${theme.components.tooltip.border} !important;
+ }
+
+ /* ── Override makeStyles-baked dark theme colors across all components ── */
+
+ /* Input fields (EditableText, AddResourceForm, EditResourceForm) */
+ .fui-Input,
+ .fui-Textarea,
+ [class*="input"],
+ [class*="textarea"] {
+ background-color: ${theme.components.input.background} !important;
+ border-color: ${theme.border.primary} !important;
+ color: ${theme.text.primary} !important;
+ }
+ .fui-Input input,
+ .fui-Textarea textarea {
+ color: ${theme.text.primary} !important;
+ }
+
+ /* Labels in sections */
+ [class*="label"] {
+ color: ${theme.text.tertiary};
+ }
+
+ /* Value text */
+ [class*="value"] {
+ color: ${theme.text.primary};
+ }
+
+ /* Edit icon */
+ [class*="editIcon"],
+ [class*="closeIcon"] {
+ color: ${theme.text.tertiary} !important;
+ }
+
+ /* Primary buttons */
+ [class*="primaryButton"] {
+ background-color: ${theme.interactive.primary} !important;
+ color: ${theme.text.inverse} !important;
+ }
+ [class*="primaryButton"]:hover {
+ background-color: ${theme.interactive.primaryHover} !important;
+ }
+
+ /* Secondary buttons */
+ [class*="secondaryButton"] {
+ color: ${theme.text.secondary} !important;
+ border-color: ${theme.border.primary} !important;
+ }
+
+ /* Clickable field hover override */
+ [class*="clickableField"]:hover {
+ background-color: ${theme.background.tertiary} !important;
+ }
+
+ /* Add resource form - addButton and helpText */
+ [class*="addButton"] {
+ background-color: ${theme.interactive.primary} !important;
+ color: ${theme.text.inverse} !important;
+ }
+
+ /* Help text */
+ [class*="helpText"] {
+ color: ${theme.text.tertiary} !important;
+ }
+
+ /* Field labels in forms */
+ .fui-Field label,
+ .fui-Label,
+ [class*="fieldLabel"] label {
+ color: ${theme.text.primary} !important;
+ }
+
+ /* Document action icons */
+ [class*="documentIcon"] {
+ color: ${theme.interactive.primary} !important;
+ }
+ [class*="linkIcon"],
+ [class*="openButton"] {
+ color: ${theme.text.tertiary} !important;
+ }
+ [class*="editButton"] {
+ color: ${theme.interactive.primary} !important;
+ }
+ [class*="removeButton"] {
+ color: ${theme.interactive.danger} !important;
+ }
+
+ /* Header link */
+ [class*="navHermesLink"] {
+ color: ${theme.components.header.linkColor} !important;
+ }
+ [class*="navHermesLink"]:hover {
+ color: ${theme.components.header.linkHover} !important;
+ }
+
+ /* Share button */
+ [class*="shareButton"] {
+ color: ${theme.text.secondary} !important;
+ }
+
+ /* Error text */
+ [class*="errorText"] {
+ color: ${theme.interactive.danger} !important;
+ }
+ `;
+ document.head.appendChild(style);
+
+ return () => {
+ document.head.removeChild(style);
+ };
+ }, [isDark]);
+
+ React.useEffect(() => {
+ (async () => {
+ try {
+ const baseUrl = controller.getHermesBaseUrl();
+ await initializeStorageAccess(baseUrl);
+ const sts = await controller.checkDocumentManageStatus();
+ setDocStatus(sts);
+ } catch (error) {
+ console.error("Failed to initialize storage access or fetch document manage status:", error);
+ setDocStatus(DocumentManageStatus.Error);
+ } finally {
+ setIsInitializing(false);
+ }
+ })()
+ }, []);
+
+ React.useEffect(() => {
+ const handleAuthRequired = () => {
+ setAuthError(null);
+ setIsAuthenticating(false);
+ setIsGrantingAccess(false);
+ setPopupAuthComplete(false);
+ setDocStatus(DocumentManageStatus.AuthenticationRequired);
+ };
+
+ window.addEventListener(HERMES_AUTH_REQUIRED_EVENT, handleAuthRequired);
+
+ return () => {
+ window.removeEventListener(HERMES_AUTH_REQUIRED_EVENT, handleAuthRequired);
+ };
+ }, []);
+
+ /**
+ * Step 1: Open popup for ALB OIDC authentication.
+ * After popup closes, we need a separate user click for Storage Access API.
+ */
+ const handleSignInWithPopup = async () => {
+ setIsAuthenticating(true);
+ setAuthError(null);
+ setPopupAuthComplete(false);
+
+ try {
+ const baseUrl = controller.getHermesBaseUrl();
+ const authUrl = `${baseUrl}/authenticate?init=true&popup=true`;
+
+ const result = await authenticateWithPopup(authUrl);
+
+ if (result.success) {
+ setIsAuthenticating(false);
+
+ const storageAccessGranted = await grantStorageAccess(baseUrl);
+
+ if (storageAccessGranted) {
+ const newStatus = await controller.checkDocumentManageStatus();
+ setDocStatus(newStatus);
+ setPopupAuthComplete(false);
+ } else {
+ setPopupAuthComplete(true);
+ }
+ } else {
+ setAuthError(result.error || 'Authentication failed');
+ setIsAuthenticating(false);
+ }
+ } catch (error) {
+ setAuthError(error instanceof Error ? error.message : 'An unexpected error occurred');
+ setIsAuthenticating(false);
+ }
+ };
+
+ /**
+ * Step 2: Grant storage access and verify auth.
+ * This MUST be called directly from a button click (user gesture).
+ * Safari requires this for requestStorageAccess() to work.
+ */
+ const handleGrantStorageAccess = async () => {
+ setIsGrantingAccess(true);
+ setAuthError(null);
+
+ try {
+ const baseUrl = controller.getHermesBaseUrl();
+ const storageAccessGranted = await grantStorageAccess(baseUrl);
+
+ if (!storageAccessGranted) {
+ setAuthError('Cookie access was not granted. Please click the button again and allow access when prompted by the browser.');
+ return;
+ }
+
+ const newStatus = await controller.checkDocumentManageStatus();
+ setDocStatus(newStatus);
+ setPopupAuthComplete(false);
+ } catch (error) {
+ setAuthError(error instanceof Error ? error.message : 'An unexpected error occurred');
+ } finally {
+ setIsGrantingAccess(false);
+ }
+ };
+
+ const renderBody = () => {
+ if (isInitializing) {
+ return (
+
+ );
+ }
+
+ switch (docStatus) {
+ case DocumentManageStatus.Loading:
+ return (
+
+ );
+ case DocumentManageStatus.Managed:
+ return ;
+ case DocumentManageStatus.Error:
+ return Something went wrong ;
+ case DocumentManageStatus.NotManaged:
+ return (
+
+
📄
+
+ Document Not Managed
+
+
+ This document is not managed by Hermes. To enable document
+ management features like metadata tracking, approvals, and collaboration,
+ please create document through Hermes.
+
+
+ window.open(controller.getHermesBaseUrl(), '_blank')}
+ >
+ Open Hermes
+
+
+
+ );
+ case DocumentManageStatus.AuthenticationRequired:
+ return (
+
+
Authentication Required
+
+ {!popupAuthComplete ? (
+ // Step 1: Sign In
+ <>
+
+ You need to sign in to Hermes to manage this document.
+ A secure popup window will open for authentication.
+
+
+ {isAuthenticating ? "Authenticating..." : "Sign In to Hermes"}
+
+ {isAuthenticating && (
+
+
+
+ Please complete sign-in in the popup window, then close it...
+
+
+ )}
+ >
+ ) : (
+ // Step 2: Grant Storage Access
+ <>
+
+ ✓ Sign-in complete! Now click below to allow cookie access.
+ This is required for the add-in to access your session.
+
+
+ {isGrantingAccess ? "Granting Access..." : "Grant Cookie Access"}
+
+ {isGrantingAccess && (
+
+
+
+ Verifying access...
+
+
+ )}
+ >
+ )}
+
+ {authError && (
+
+ {authError}
+
+ )}
+
+ );
+ default:
+ return (
+
+ );
+ }
+ };
+
+ // The list items are static and won't change at runtime,
+ // so this should be an ordinary const, not a part of state.
+
+ return (
+
+ {renderBody()}
+
+ );
+};
+
+/**
+ * App is the top-level component that provides the ThemeContext and FluentProvider.
+ * AppCore handles all business logic and rendering.
+ */
+const App: React.FC = ({ controller }) => {
+ return (
+
+
+
+ );
+};
+
+/**
+ * AppInner reads the theme from context to configure FluentProvider,
+ * then renders AppCore.
+ */
+const AppInner: React.FC = ({ controller }) => {
+ const { isDark } = useTheme();
+ return (
+
+
+
+ );
+};
+
+export default App;
diff --git a/hermes-plugin/src/taskpane/components/DocumentThumbnail.tsx b/hermes-plugin/src/taskpane/components/DocumentThumbnail.tsx
new file mode 100644
index 000000000..d65cee1a7
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/DocumentThumbnail.tsx
@@ -0,0 +1,119 @@
+import * as React from "react";
+import { makeStyles, mergeClasses } from "@fluentui/react-components";
+import ProductIcon from "./ProductIcon";
+import DarkTheme from "../utils/darkTheme";
+import LightTheme from "../utils/lightTheme";
+import { useTheme } from "../utils/themeContext";
+
+const useStyles = makeStyles({
+ thumbnail: {
+ position: "relative",
+ width: "48px",
+ height: "48px",
+ borderRadius: "4px",
+ backgroundColor: DarkTheme.background.secondary,
+ border: `1px solid ${DarkTheme.border.primary}`,
+ flexShrink: 0,
+ overflow: "hidden",
+ },
+ documentImage: {
+ width: "100%",
+ height: "100%",
+ objectFit: "contain",
+ },
+ overlay: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ width: "100%",
+ height: "100%",
+ pointerEvents: "none",
+ },
+ productBadge: {
+ position: "absolute",
+ bottom: "2px",
+ left: "2px",
+ },
+ statusIcon: {
+ position: "absolute",
+ top: "2px",
+ right: "2px",
+ width: "16px",
+ height: "16px",
+ borderRadius: "50%",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ fontSize: "12px",
+ },
+ approved: {
+ backgroundColor: DarkTheme.interactive.success,
+ color: DarkTheme.text.primary,
+ },
+ obsolete: {
+ backgroundColor: DarkTheme.status.obsolete.background,
+ color: DarkTheme.status.obsolete.text,
+ },
+});
+
+interface DocumentThumbnailProps {
+ product?: string;
+ status?: string;
+ className?: string;
+}
+
+const DocumentThumbnail: React.FC = ({
+ product,
+ status,
+ className
+}) => {
+ const styles = useStyles();
+ const { isDark } = useTheme();
+ const theme = isDark ? DarkTheme : LightTheme;
+
+ const isApproved = status?.toLowerCase() === "approved";
+ const isObsolete = status?.toLowerCase() === "obsolete";
+
+ return (
+
+ {/* Base document image */}
+
{
+ // Fallback if image doesn't load
+ const target = e.target as HTMLImageElement;
+ target.style.display = "none";
+ target.parentElement!.style.backgroundColor = theme.background.tertiary;
+ target.parentElement!.innerHTML += `
📄
`;
+ }}
+ />
+
+ {/* Overlay for badges and status */}
+
+ {/* Status icon */}
+ {(isApproved || isObsolete) && (
+
+ {isApproved ? "✓" : "🗃"}
+
+ )}
+
+ {/* Product badge */}
+ {product && (
+
+ )}
+
+
+ );
+};
+
+export default DocumentThumbnail;
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/components/EditRelatedResourceModal.tsx b/hermes-plugin/src/taskpane/components/EditRelatedResourceModal.tsx
new file mode 100644
index 000000000..8c535d756
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/EditRelatedResourceModal.tsx
@@ -0,0 +1,252 @@
+import * as React from "react";
+import {
+ makeStyles,
+ Button,
+ Input,
+ Dialog,
+ DialogSurface,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ DialogBody,
+ Field,
+} from "@fluentui/react-components";
+import { Delete24Regular } from "@fluentui/react-icons";
+import { RelatedExternalLink } from "../interfaces/relatedResources";
+import DarkTheme, { commonStyles } from "../utils/darkTheme";
+import LightTheme from "../utils/lightTheme";
+import { useTheme } from "../utils/themeContext";
+
+const useStyles = makeStyles({
+ dialogContent: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "16px",
+ minWidth: "400px",
+ backgroundColor: DarkTheme.background.elevated,
+ color: DarkTheme.text.primary,
+ },
+
+ urlPreview: {
+ fontSize: "12px",
+ color: DarkTheme.text.tertiary,
+ fontStyle: "italic",
+ wordBreak: "break-all",
+ },
+
+ errorText: {
+ fontSize: "12px",
+ },
+
+ deleteSection: {
+ borderTop: `1px solid ${DarkTheme.border.primary}`,
+ paddingTop: "16px",
+ marginTop: "8px",
+ },
+
+ deleteButton: {
+ color: `${DarkTheme.interactive.danger} !important`,
+ backgroundColor: "transparent !important",
+ border: `1px solid ${DarkTheme.interactive.danger} !important`,
+ borderRadius: "4px !important",
+ fontWeight: "500 !important",
+ },
+
+ deleteText: {
+ fontSize: "14px",
+ color: DarkTheme.text.secondary,
+ marginBottom: "12px",
+ },
+
+ fieldLabel: {
+ "& label": {
+ ...commonStyles.fieldLabel,
+ },
+ },
+
+ inputField: commonStyles.inputField,
+ primaryButton: commonStyles.primaryButton,
+});
+
+interface EditRelatedResourceModalProps {
+ isOpen: boolean;
+ resource: RelatedExternalLink;
+ onClose: () => void;
+ onSave: (resource: RelatedExternalLink) => Promise;
+ onRemove: (resource: RelatedExternalLink) => Promise;
+}
+
+const EditRelatedResourceModal: React.FC = ({
+ isOpen,
+ resource,
+ onClose,
+ onSave,
+ onRemove,
+}) => {
+ const styles = useStyles();
+ const { isDark } = useTheme();
+ const theme = isDark ? DarkTheme : LightTheme;
+ const [isCancelHovered, setIsCancelHovered] = React.useState(false);
+ const [url, setUrl] = React.useState(resource.url || "");
+ const [name, setName] = React.useState(resource.name || "");
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
+ const [urlError, setUrlError] = React.useState("");
+
+ React.useEffect(() => {
+ setUrl(resource.url || "");
+ setName(resource.name || "");
+ setUrlError("");
+ }, [resource]);
+
+ const validateUrl = (urlString: string): boolean => {
+ try {
+ new URL(urlString);
+ return true;
+ } catch {
+ return false;
+ }
+ };
+
+ const handleUrlChange = (value: string) => {
+ setUrl(value);
+ setUrlError("");
+ };
+
+ const handleSave = async () => {
+ if (!url.trim()) {
+ setUrlError("URL is required");
+ return;
+ }
+
+ if (!validateUrl(url.trim())) {
+ setUrlError("Please enter a valid URL");
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ const updatedResource: RelatedExternalLink = {
+ ...resource,
+ url: url.trim(),
+ name: name.trim() || url.trim(),
+ };
+
+ await onSave(updatedResource);
+ } catch (error) {
+ console.error("Failed to save resource:", error);
+ // Error handling is managed by parent component
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleRemove = async () => {
+ if (window.confirm("Are you sure you want to remove this resource?")) {
+ setIsSubmitting(true);
+
+ try {
+ await onRemove(resource);
+ } catch (error) {
+ console.error("Failed to remove resource:", error);
+ // Error handling is managed by parent component
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+ };
+
+ const handleClose = () => {
+ if (!isSubmitting) {
+ onClose();
+ }
+ };
+
+ const hasChanges = url !== resource.url || name !== resource.name;
+
+ return (
+ !data.open && handleClose()}>
+
+
+ Edit Related Resource
+
+
+ handleUrlChange(data.value)}
+ placeholder="https://example.com"
+ disabled={isSubmitting}
+ />
+
+
+
+ setName(data.value)}
+ placeholder="Display name for the resource"
+ disabled={isSubmitting}
+ />
+
+
+ {url && validateUrl(url) && (
+
+ Preview: {name.trim() || new URL(url).hostname.replace(/^www\./, "")}
+
+ )}
+
+
+
+ Remove this resource permanently
+
+
}
+ onClick={handleRemove}
+ disabled={isSubmitting}
+ >
+ Remove Resource
+
+
+
+
+ setIsCancelHovered(true)}
+ onMouseLeave={() => setIsCancelHovered(false)}
+ style={{
+ backgroundColor: isCancelHovered ? theme.background.tertiary : "transparent",
+ color: isCancelHovered ? theme.text.primary : theme.text.secondary,
+ border: `1px solid ${isCancelHovered ? theme.border.secondary : theme.border.primary}`,
+ borderRadius: "4px",
+ fontWeight: "500",
+ }}
+ >
+ Cancel
+
+
+ {isSubmitting ? "Saving..." : "Save Changes"}
+
+
+
+
+
+ );
+};
+
+export default EditRelatedResourceModal;
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/components/EditResourceForm.tsx b/hermes-plugin/src/taskpane/components/EditResourceForm.tsx
new file mode 100644
index 000000000..42c9d8994
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/EditResourceForm.tsx
@@ -0,0 +1,301 @@
+import * as React from "react";
+import {
+ makeStyles,
+ mergeClasses,
+ Text,
+ Button,
+ Input,
+ Field,
+} from "@fluentui/react-components";
+import {
+ Dismiss24Regular,
+ Checkmark24Regular,
+ Delete24Regular
+} from "@fluentui/react-icons";
+import { RelatedExternalLink } from "../interfaces/relatedResources";
+import DarkTheme, { commonStyles } from "../utils/darkTheme";
+import LightTheme from "../utils/lightTheme";
+import { useTheme } from "../utils/themeContext";
+
+const useStyles = makeStyles({
+ container: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "12px",
+ padding: "12px",
+ backgroundColor: DarkTheme.background.secondary,
+ border: `1px solid ${DarkTheme.border.primary}`,
+ borderRadius: "6px",
+ marginTop: "4px",
+ },
+
+ formRow: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "6px",
+ },
+
+ buttonRow: {
+ display: "flex",
+ gap: "8px",
+ justifyContent: "space-between",
+ alignItems: "center",
+ },
+
+ primaryButtons: {
+ display: "flex",
+ gap: "8px",
+ },
+
+ button: {
+ minWidth: "auto",
+ padding: "6px 12px",
+ borderRadius: "4px !important",
+ fontWeight: "600 !important",
+ },
+
+ cancelButton: {
+ minWidth: "auto",
+ padding: "6px 12px",
+ },
+
+ saveButton: {
+ ...commonStyles.primaryButton,
+ minWidth: "auto",
+ padding: "6px 12px",
+ },
+
+ deleteButton: commonStyles.dangerButton,
+
+ urlPreview: {
+ fontSize: "12px",
+ color: DarkTheme.text.tertiary,
+ fontStyle: "italic",
+ wordBreak: "break-all",
+ marginTop: "4px",
+ },
+
+ errorText: {
+ fontSize: "12px",
+ marginTop: "4px",
+ },
+
+ errorContainer: {
+ display: "flex",
+ alignItems: "center",
+ padding: "8px 12px",
+ borderRadius: "6px",
+ marginTop: "4px",
+ },
+
+ fieldLabel: {
+ "& label": {
+ ...commonStyles.fieldLabel,
+ },
+ },
+
+ inputField: commonStyles.inputField,
+});
+
+interface EditResourceFormProps {
+ resource: RelatedExternalLink;
+ onSave: (resource: RelatedExternalLink) => Promise;
+ onRemove: (resource: RelatedExternalLink) => Promise;
+ onCancel: () => void;
+}
+
+const EditResourceForm: React.FC = ({
+ resource,
+ onSave,
+ onRemove,
+ onCancel,
+}) => {
+ const styles = useStyles();
+ const { isDark } = useTheme();
+ const theme = isDark ? DarkTheme : LightTheme;
+ const [isCancelHovered, setIsCancelHovered] = React.useState(false);
+ const [url, setUrl] = React.useState(resource.url || "");
+ const [name, setName] = React.useState(resource.name || "");
+ const [urlError, setUrlError] = React.useState("");
+ const [saveError, setSaveError] = React.useState("");
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
+
+ React.useEffect(() => {
+ setUrl(resource.url || "");
+ setName(resource.name || "");
+ setUrlError("");
+ }, [resource]);
+
+ const validateUrl = (urlString: string): boolean => {
+ try {
+ new URL(urlString);
+ return true;
+ } catch {
+ return false;
+ }
+ };
+
+ const handleUrlChange = (value: string) => {
+ setUrl(value);
+ setUrlError("");
+ };
+
+ const handleSave = async () => {
+ if (!url.trim()) {
+ setUrlError("URL is required");
+ return;
+ }
+
+ if (!validateUrl(url.trim())) {
+ setUrlError("Please enter a valid URL");
+ return;
+ }
+
+ setIsSubmitting(true);
+ setSaveError("");
+ try {
+ const updatedResource: RelatedExternalLink = {
+ ...resource,
+ url: url.trim(),
+ name: name.trim() || url.trim(),
+ };
+
+ await onSave(updatedResource);
+ } catch (error) {
+ console.error("Failed to save resource:", error);
+ const errorMessage = error instanceof Error ? error.message : "Failed to save resource - please try again";
+ setSaveError(errorMessage);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleRemove = async () => {
+ // Simple confirmation using window.confirm (allowed in task panes)
+ if (confirm("Are you sure you want to remove this resource?")) {
+ setIsSubmitting(true);
+ try {
+ await onRemove(resource);
+ } catch (error) {
+ console.error("Failed to remove resource:", error);
+ // Error handling is managed by parent component
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+ };
+
+ const handleKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !isSubmitting) {
+ e.preventDefault();
+ handleSave();
+ } else if (e.key === "Escape" && !isSubmitting) {
+ e.preventDefault();
+ onCancel();
+ }
+ };
+
+ const hasChanges = url !== resource.url || name !== resource.name;
+
+ return (
+
+
+ Edit Resource
+
+
+
+
+ handleUrlChange(data.value)}
+ placeholder="https://example.com"
+ disabled={isSubmitting}
+ size="small"
+ />
+
+ {urlError && (
+ {urlError}
+ )}
+
+
+
+
+ setName(data.value)}
+ placeholder="Display name for the resource"
+ disabled={isSubmitting}
+ size="small"
+ />
+
+
+
+ {url && validateUrl(url) && (
+
+ Preview: {name.trim() || new URL(url).hostname.replace(/^www\./, "")}
+
+ )}
+
+ {saveError && (
+
+
+ {saveError}
+
+
+ )}
+
+
+
}
+ onClick={handleRemove}
+ disabled={isSubmitting}
+ size="small"
+ >
+ Remove
+
+
+
+ }
+ onClick={onCancel}
+ disabled={isSubmitting}
+ size="small"
+ onMouseEnter={() => setIsCancelHovered(true)}
+ onMouseLeave={() => setIsCancelHovered(false)}
+ style={{
+ backgroundColor: isCancelHovered ? theme.background.tertiary : "transparent",
+ color: isCancelHovered ? theme.text.primary : theme.text.secondary,
+ border: `1px solid ${isCancelHovered ? theme.border.secondary : theme.border.primary}`,
+ }}
+ >
+ Cancel
+
+ }
+ onClick={handleSave}
+ disabled={isSubmitting || !url.trim() || !hasChanges}
+ size="small"
+ >
+ {isSubmitting ? "Saving..." : "Save"}
+
+
+
+
+ );
+};
+
+export default EditResourceForm;
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/components/EditableText.tsx b/hermes-plugin/src/taskpane/components/EditableText.tsx
new file mode 100644
index 000000000..91e59206a
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/EditableText.tsx
@@ -0,0 +1,272 @@
+import { Text, Input, Button, Textarea, makeStyles, mergeClasses } from "@fluentui/react-components";
+import { Checkmark24Regular, Dismiss24Regular, Edit16Regular } from "@fluentui/react-icons";
+import * as React from "react";
+import DarkTheme from "../utils/darkTheme";
+import LightTheme from "../utils/lightTheme";
+import { useTheme } from "../utils/themeContext";
+
+type EditableTitleProps = {
+ label?: string;
+ text: string;
+ size?: 400 | 100 | 200 | 300 | 500 | 600 | 700 | 800 | 900 | 1000;
+ weight?: "bold" | "medium" | "semibold" | "regular";
+ multiline?: boolean; // New prop to control single-line vs multi-line
+ disabled?: boolean; // New prop to disable editing
+ onChange: (value: string) => Promise;
+} & React.PropsWithChildren;
+
+const useStyle = makeStyles({
+ root: { display: "flex", flexDirection: "column", gap: "4px", width: "100%" },
+ formContainer: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "8px",
+ width: "100%"
+ },
+ formContainerInline: {
+ display: "flex",
+ gap: "8px",
+ alignItems: "center",
+ width: "100%"
+ },
+ buttonContainer: {
+ display: "flex",
+ gap: "8px",
+ alignItems: "center"
+ },
+ input: {
+ backgroundColor: `${DarkTheme.components.input.background} !important`,
+ border: `1px solid ${DarkTheme.border.primary} !important`,
+ borderRadius: "4px !important",
+ color: `${DarkTheme.text.primary} !important`,
+ boxSizing: "border-box",
+ "& input": {
+ padding: "4px !important",
+ },
+ "&::placeholder": {
+ color: `${DarkTheme.components.input.placeholder} !important`,
+ },
+ },
+ textarea: {
+ minHeight: "100px",
+ width: "100%",
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
+ fontSize: "14px",
+ lineHeight: "1.4",
+ backgroundColor: `${DarkTheme.components.input.background} !important`,
+ border: `1px solid ${DarkTheme.border.primary} !important`,
+ borderRadius: "4px !important",
+ color: `${DarkTheme.text.primary} !important`,
+ boxSizing: "border-box",
+ "& textarea": {
+ padding: "6px 12px !important",
+ resize: "vertical",
+ minHeight: "100px !important",
+ },
+ "&::placeholder": {
+ color: `${DarkTheme.components.input.placeholder} !important`,
+ },
+ },
+ label: {
+ color: DarkTheme.text.tertiary,
+ fontSize: "12px",
+ fontWeight: 500,
+ },
+ clickableText: {
+ padding: "8px",
+ borderRadius: "4px",
+ transition: "background-color 0.2s ease, box-shadow 0.2s ease",
+ position: "relative",
+ minHeight: "36px",
+ },
+ editIcon: {
+ position: "absolute",
+ top: "4px",
+ right: "4px",
+ transition: "opacity 0.2s",
+ pointerEvents: "none",
+ color: DarkTheme.text.tertiary,
+ },
+ primaryButton: {
+ backgroundColor: `${DarkTheme.interactive.primary} !important`,
+ color: `${DarkTheme.text.primary} !important`,
+ border: "none !important",
+ borderRadius: "4px !important",
+ fontWeight: "600 !important",
+ ":hover": {
+ backgroundColor: `${DarkTheme.interactive.primaryHover} !important`,
+ },
+ },
+ secondaryButton: {
+ backgroundColor: "transparent !important",
+ color: `${DarkTheme.text.secondary} !important`,
+ border: `1px solid ${DarkTheme.border.primary} !important`,
+ borderRadius: "4px !important",
+ fontWeight: "500 !important",
+ },
+ errorText: {
+ marginTop: "4px",
+ fontSize: "12px",
+ display: "block",
+ },
+});
+
+const EditableTitle = ({
+ label,
+ text: originalTitle,
+ size,
+ weight,
+ multiline = false,
+ disabled = false,
+ onChange,
+}: EditableTitleProps) => {
+ const styles = useStyle();
+ const { isDark } = useTheme();
+ const theme = isDark ? DarkTheme : LightTheme;
+ const [isEditing, setIsEditing] = React.useState(false);
+ const [title, setTitle] = React.useState(originalTitle || "");
+ const [draft, setDraft] = React.useState(originalTitle || "");
+ const [isSaving, setIsSaving] = React.useState(false);
+ const [error, setError] = React.useState(null);
+ const [isHovered, setIsHovered] = React.useState(false);
+
+ // Update title when originalTitle prop changes
+ React.useEffect(() => {
+ setTitle(originalTitle || "");
+ if (!isEditing) {
+ setDraft(originalTitle || "");
+ }
+ }, [originalTitle, isEditing]);
+
+
+
+ const handleSave = async () => {
+ setIsSaving(true);
+ setError(null); // Clear previous errors
+
+ try {
+ // Trim the draft to handle whitespace-only input
+ const trimmedDraft = (draft || "").trim();
+ await onChange(trimmedDraft);
+ setTitle(trimmedDraft);
+ setIsEditing(false); // Close only after successful save
+ } catch (err: any) {
+ // Extract HTTP status code from error
+ let statusCode = "";
+
+ if (err.response) {
+ // Axios-style HTTP error response
+ statusCode = `${err.response.status}`;
+ } else if (err.message) {
+ // Parse HermesClient error format for status code
+ const statusMatch = err.message.match(/status:\s*(\d+)/);
+ if (statusMatch) {
+ statusCode = statusMatch[1];
+ }
+ }
+
+ const fullError = statusCode ? `Server Responded : ${statusCode}` : "Server Error";
+ setError(fullError);
+
+ // Don't close field on error - let user see error and retry
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleCancel = () => {
+ setDraft(title);
+ setError(null);
+ setIsEditing(false);
+ };
+
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+ {isEditing ? (
+ <>
+
+ {error && (
+
+ Error: {error}
+
+ )}
+ >
+ ) : (
+
setIsEditing(true)}
+ onMouseEnter={() => setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ style={{
+ cursor: disabled ? "default" : "pointer",
+ color: theme.text.primary,
+ backgroundColor: isHovered && !disabled ? theme.background.tertiary : "transparent",
+ boxShadow: isHovered && !disabled ? theme.shadows.small : undefined,
+ }}
+ >
+
+
+ {title.trim() || "None"}
+
+
+ {!disabled && (
+
+ )}
+
+ )}
+
+ );
+};
+
+export default EditableTitle;
diff --git a/hermes-plugin/src/taskpane/components/People.tsx b/hermes-plugin/src/taskpane/components/People.tsx
new file mode 100644
index 000000000..5569d3c00
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/People.tsx
@@ -0,0 +1,565 @@
+import * as React from "react";
+import {
+ Avatar,
+ Button,
+ Field,
+ makeStyles,
+ Tag,
+ TagPicker,
+ TagPickerControl,
+ TagPickerGroup,
+ TagPickerInput,
+ TagPickerList,
+ TagPickerOption,
+ TagPickerProps,
+ Text,
+ useTagPickerFilter,
+} from "@fluentui/react-components";
+import { Person } from "../interfaces/person";
+import WordPluginController from "../utils/wordPluginController";
+import {
+ Dismiss24Regular,
+ Checkmark24Regular,
+ Edit16Regular,
+} from "@fluentui/react-icons";
+import DarkTheme, { commonStyles } from "../utils/darkTheme";
+import LightTheme from "../utils/lightTheme";
+import { useTheme } from "../utils/themeContext";
+
+type PeopleProps = {
+ label: string;
+ fieldLabel?: string; // Optional custom field label, defaults to "Select ${label}"
+ peopleMap: Map;
+ emails: string[];
+ update: (emails: string[]) => Promise;
+ search: (query: string) => Promise;
+ createUrl: (person: Person) => string;
+ mutatePeopleMap: (map: Map) => void;
+ isMe: (email: string) => boolean;
+ addGreenTick?: (email: string) => boolean;
+ disable?: boolean;
+};
+
+const useStyles = makeStyles({
+ section: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "4px",
+ },
+ rowSection: {
+ display: "flex",
+ flexDirection: "row",
+ gap: "8px",
+ },
+ label: {
+ color: DarkTheme.text.tertiary,
+ fontSize: "12px",
+ fontWeight: 500,
+ },
+ value: {
+ fontSize: "14px",
+ color: DarkTheme.text.primary,
+ },
+ actionButtons: {
+ display: "flex",
+ gap: "8px",
+ marginTop: "8px",
+ },
+ clickableField: {
+ padding: "8px",
+ borderRadius: "4px",
+ transition: "background-color 0.2s ease, box-shadow 0.2s ease",
+ position: "relative",
+ },
+ clickableFieldDisabled: {
+ padding: "8px",
+ borderRadius: "4px",
+ },
+ editIcon: {
+ position: "absolute",
+ top: "4px",
+ right: "4px",
+ transition: "opacity 0.2s ease",
+ color: DarkTheme.text.tertiary,
+ fontSize: "14px",
+ pointerEvents: "none",
+ },
+ tagPickerInput: {
+ backgroundColor: DarkTheme.background.secondary,
+ border: "none",
+ color: DarkTheme.text.primary,
+ minWidth: "100px",
+ flexShrink: 0,
+ flexGrow: 1,
+ "&::placeholder": {
+ color: DarkTheme.components.input.placeholder,
+ },
+ },
+ tagPickerControl: {
+ display: "flex",
+ flexWrap: "wrap" as const,
+ minHeight: "44px",
+ alignItems: "center",
+ },
+ fieldLabel: {
+ "& label": {
+ ...commonStyles.fieldLabel,
+ },
+ },
+ primaryButton: commonStyles.primaryButton,
+ errorText: {
+ fontSize: "12px",
+ marginTop: "4px",
+ },
+});
+
+const People = ({
+ label,
+ fieldLabel,
+ peopleMap,
+ emails,
+ createUrl,
+ update,
+ search,
+ mutatePeopleMap,
+ isMe,
+ addGreenTick,
+ disable,
+}: PeopleProps) => {
+ const styles = useStyles();
+ const { isDark } = useTheme();
+ const theme = isDark ? DarkTheme : LightTheme;
+
+ const [showDropdown, setShowDropdown] = React.useState(false);
+ const [query, setQuery] = React.useState("");
+ const [selectedOptions, setSelectedOptions] = React.useState(emails);
+ const [originalOptions, setOriginalOptions] = React.useState(emails);
+ const [options, setOptions] = React.useState([]);
+ const [isSaving, setIsSaving] = React.useState(false);
+ const [error, setError] = React.useState(null);
+ const tagPickerRef = React.useRef(null);
+ const componentRef = React.useRef(null);
+ const searchTimeoutRef = React.useRef();
+ const [isSearching, setIsSearching] = React.useState(false);
+ const [isHovered, setIsHovered] = React.useState(false);
+ const [isCancelHovered, setIsCancelHovered] = React.useState(false);
+
+ // Auto-scroll function to bring field to top
+ const scrollFieldToTop = () => {
+ if (componentRef.current) {
+ componentRef.current.scrollIntoView({
+ behavior: 'smooth',
+ block: 'start'
+ });
+ }
+ };
+
+ const searchPeople = React.useCallback((q: string) => {
+ // Ensure q is a string
+ const query = q || "";
+
+ // Update query immediately for responsive UI
+ setQuery(query);
+
+ // Clear previous timeout
+ if (searchTimeoutRef.current) {
+ clearTimeout(searchTimeoutRef.current);
+ }
+
+ // If query is empty, clear options and stop searching immediately
+ if (!query.trim()) {
+ setOptions([]);
+ setIsSearching(false);
+ return;
+ }
+
+ // Set searching state to prevent render during updates
+ setIsSearching(true);
+
+ // Debounce the actual search with longer delay for more stability
+ searchTimeoutRef.current = setTimeout(async () => {
+ try {
+ const people = await search(query);
+
+ if (people) {
+ // Process people data
+ const newOptions: string[] = [];
+ people.forEach((person) => {
+ const email = person.emailAddresses[0].value;
+ peopleMap.set(email, person);
+ if (!selectedOptions.includes(email)) {
+ newOptions.push(email);
+ }
+ });
+
+ // Use double requestAnimationFrame for extra stability
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ setOptions(newOptions);
+ mutatePeopleMap(peopleMap);
+ setIsSearching(false);
+ });
+ });
+ }
+ } catch (error) {
+ console.log(error);
+ requestAnimationFrame(() => {
+ setOptions([]);
+ setIsSearching(false);
+ });
+ }
+ }, 500); // Increased delay for stability
+ }, [search, selectedOptions, peopleMap, mutatePeopleMap]);
+
+ const onOptionSelect: TagPickerProps["onOptionSelect"] = React.useCallback((_, data) => {
+ if (data.value === "no-matches") {
+ return;
+ }
+
+ // Use requestAnimationFrame to defer updates and prevent layout thrashing
+ requestAnimationFrame(() => {
+ setSelectedOptions(data.selectedOptions);
+ setQuery("");
+ // Clear options when selection is made to prevent dropdown size issues
+ setOptions([]);
+ });
+ }, []);
+
+ // Save changes
+ const handleSave = async () => {
+ setIsSaving(true);
+ setError(null); // Clear previous errors
+ try {
+ await update(selectedOptions);
+ setOriginalOptions([...selectedOptions]);
+ setShowDropdown(false);
+ } catch (err: any) {
+ console.error("People component - Failed to save changes:", err);
+
+ // Extract HTTP status code from error
+ let statusCode = "";
+
+ if (err.response) {
+ // Axios-style HTTP error response
+ statusCode = `${err.response.status}`;
+ console.log("People - Found axios-style error with status:", statusCode);
+ } else if (err.message) {
+ // Parse HermesClient error format for status code
+ const statusMatch = err.message.match(/status:\s*(\d+)/);
+ if (statusMatch) {
+ statusCode = statusMatch[1];
+ console.log("People - Found HermesClient error with status:", statusCode);
+ }
+ }
+
+ let fullError = "Server Error";
+ if (statusCode === "403") {
+ fullError = "Permission Denied, Only Owners can update the metadata.";
+ } else if (statusCode) {
+ fullError = `Server Responded : ${statusCode}`;
+ }
+
+ setError(fullError);
+
+ // IMPORTANT: Don't call setShowDropdown(false) here - keep field open to show error
+ console.log("People field should stay open to show error");
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ // Cancel changes
+ const handleCancel = () => {
+ // First reset the selected options
+ setSelectedOptions([...originalOptions]);
+ // Clear any search query
+ setQuery("");
+ // Clear any error messages
+ setError(null);
+ // Blur any focused elements to close dropdowns
+ if (tagPickerRef.current) {
+ const focusedElement = tagPickerRef.current.querySelector(':focus') as HTMLElement;
+ if (focusedElement) {
+ focusedElement.blur();
+ }
+ }
+ // Then close the dropdown
+ setShowDropdown(false);
+ };
+
+ // Update original options when emails prop changes
+ React.useEffect(() => {
+ setOriginalOptions(emails);
+ if (!showDropdown) {
+ setSelectedOptions(emails);
+ }
+ }, [emails, showDropdown]);
+
+
+
+ // Cleanup timeout on unmount
+ React.useEffect(() => {
+ return () => {
+ if (searchTimeoutRef.current) {
+ clearTimeout(searchTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ const renderPeople = (emails: string[]) => {
+ return emails.map((email, index) => {
+ const personDetails = peopleMap.get(email);
+ const showApprovalTick = addGreenTick && addGreenTick(email);
+
+ return (
+
+
+
+ {showApprovalTick && (
+
+
+
+ )}
+
+
{isMe(email) ? "Me" : personDetails?.names[0].displayName || email}
+
+ );
+ });
+ };
+
+ const noMatch = () => (
+
+ No matches found
+
+ );
+
+ const renderList = () => {
+ // Show all results but rely on scrolling in the dropdown
+ const optionElements = options.map((email) => {
+ const person = peopleMap.get(email);
+
+ if (!person) {
+ return (
+ }
+ value={email}
+ style={{
+ backgroundColor: theme.background.elevated,
+ color: theme.text.primary,
+ }}
+ >
+ {email}
+
+ );
+ }
+
+ const name = isMe(email) ? "Me" : person.names[0].displayName;
+ const url = createUrl(person);
+
+ return (
+ }
+ value={email}
+ style={{
+ backgroundColor: theme.background.elevated,
+ color: theme.text.primary,
+ }}
+ >
+ {name}
+
+ );
+ });
+
+ return optionElements;
+ };
+
+ const renderDropdown = () => {
+ return (
+
+
+
+
+
+ {selectedOptions.map((option) => {
+ const people = peopleMap.get(option);
+
+ return (
+
+ }
+ >
+ {people && isMe(people.emailAddresses[0].value) ? "Me" : people?.names[0]?.displayName || option}
+
+ );
+ })}
+
+ searchPeople(e.target.value)}
+ className={styles.tagPickerInput}
+ style={{ minWidth: "100px", flexGrow: 1 }}
+ />
+
+
+ {isSearching ? (
+ Searching...
+ ) : (
+ options.length > 0 ? renderList() : noMatch()
+ )}
+
+
+
+
+ {/* Action buttons - always show when in edit mode */}
+
+ }
+ onClick={handleSave}
+ disabled={isSaving}
+ >
+ {isSaving ? "Saving..." : "Save"}
+
+ }
+ onClick={handleCancel}
+ disabled={isSaving}
+ onMouseEnter={() => setIsCancelHovered(true)}
+ onMouseLeave={() => setIsCancelHovered(false)}
+ style={{
+ backgroundColor: isCancelHovered ? theme.background.tertiary : "transparent",
+ color: isCancelHovered ? theme.text.primary : theme.text.secondary,
+ border: `1px solid ${isCancelHovered ? theme.border.secondary : theme.border.primary}`,
+ borderRadius: "4px",
+ fontWeight: "500",
+ }}
+ >
+ Cancel
+
+
+
+ {/* Error display */}
+ {error && (
+
+ Error: {error}
+
+ )}
+
+ );
+ };
+
+ return (
+
+
+ {label}
+
+ {showDropdown ? (
+ renderDropdown()
+ ) : (
+
{
+ setShowDropdown(true);
+ // Auto-scroll to top after a brief delay to ensure dropdown renders
+ setTimeout(() => scrollFieldToTop(), 100);
+ } : undefined}
+ onMouseEnter={() => setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ style={{
+ cursor: !disable ? "pointer" : "default",
+ backgroundColor: isHovered && !disable ? theme.background.tertiary : "transparent",
+ boxShadow: isHovered && !disable ? theme.shadows.small : undefined,
+ }}
+ >
+ {originalOptions && originalOptions.length !== 0 ? (
+
{renderPeople(originalOptions)}
+ ) : (
+
None
+ )}
+ {!disable && (
+
+ )}
+
+ )}
+
+ );
+};
+
+export default People;
diff --git a/hermes-plugin/src/taskpane/components/PeopleAndGroups.tsx b/hermes-plugin/src/taskpane/components/PeopleAndGroups.tsx
new file mode 100644
index 000000000..9b13f1124
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/PeopleAndGroups.tsx
@@ -0,0 +1,529 @@
+import * as React from "react";
+import {
+ Avatar,
+ Button,
+ Field,
+ makeStyles,
+ Tag,
+ TagPicker,
+ TagPickerControl,
+ TagPickerGroup,
+ TagPickerInput,
+ TagPickerList,
+ TagPickerOption,
+ TagPickerProps,
+ Text,
+} from "@fluentui/react-components";
+import { Edit16Regular, People24Regular, CheckmarkRegular, Dismiss24Regular } from "@fluentui/react-icons";
+import { Person } from "../interfaces/person";
+import { Group } from "../interfaces/group";
+import DarkTheme, { commonStyles } from "../utils/darkTheme";
+import LightTheme from "../utils/lightTheme";
+import { useTheme } from "../utils/themeContext";
+
+interface PeopleAndGroupsProps {
+ label: string;
+ peopleMap: Map;
+ groupsMap: Map;
+ emails: string[];
+ update: (emails: string[]) => Promise;
+ searchPeople: (query: string) => Promise;
+ searchGroups: (query: string) => Promise;
+ createUrl: (person: Person) => string;
+ mutatePeopleMap: (map: Map) => void;
+ mutateGroupsMap: (map: Map) => void;
+ isMe: (email: string) => boolean;
+ addGreenTick?: (email: string) => boolean;
+ disable?: boolean;
+}
+
+const useStyles = makeStyles({
+ section: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "4px",
+ },
+ rowSection: {
+ display: "flex",
+ flexDirection: "row",
+ gap: "8px",
+ alignItems: "center",
+ },
+ label: {
+ color: DarkTheme.text.tertiary,
+ fontSize: "12px",
+ fontWeight: 500,
+ cursor: "pointer",
+ },
+ value: {
+ fontSize: "14px",
+ color: DarkTheme.text.primary,
+ },
+ closeIcon: {
+ position: "absolute",
+ top: "8px",
+ right: "8px",
+ fontSize: "16px",
+ cursor: "pointer",
+ color: DarkTheme.text.tertiary,
+ transition: "opacity 0.2s ease",
+ },
+ clickableField: {
+ padding: "8px",
+ borderRadius: "4px",
+ transition: "background-color 0.2s ease, box-shadow 0.2s ease",
+ position: "relative",
+ cursor: "pointer",
+ },
+ clickableFieldDisabled: {
+ padding: "8px",
+ borderRadius: "4px",
+ position: "relative",
+ },
+ buttonContainer: {
+ display: "flex",
+ flexDirection: "row",
+ gap: "8px",
+ marginTop: "8px",
+ },
+ tagPicker: {
+ backgroundColor: DarkTheme.background.secondary,
+ border: `1px solid ${DarkTheme.border.primary}`,
+ borderRadius: "4px",
+ color: DarkTheme.text.primary,
+ },
+ tagPickerList: {
+ backgroundColor: DarkTheme.background.elevated,
+ border: `1px solid ${DarkTheme.border.primary}`,
+ borderRadius: "4px",
+ boxShadow: DarkTheme.shadows.medium,
+ color: DarkTheme.text.primary,
+ },
+ tagPickerOption: {
+ backgroundColor: DarkTheme.background.elevated,
+ color: DarkTheme.text.primary,
+ ":hover": {
+ backgroundColor: DarkTheme.background.tertiary,
+ },
+ },
+ tagPickerInput: {
+ backgroundColor: DarkTheme.background.secondary,
+ border: "none",
+ color: DarkTheme.text.primary,
+ minWidth: "100px",
+ flexShrink: 0,
+ flexGrow: 1,
+ "&::placeholder": {
+ color: DarkTheme.components.input.placeholder,
+ },
+ },
+ tagPickerControl: {
+ display: "flex",
+ flexWrap: "wrap" as const,
+ minHeight: "44px",
+ alignItems: "center",
+ },
+ fieldLabel: {
+ "& label": {
+ ...commonStyles.fieldLabel,
+ },
+ },
+ primaryButton: commonStyles.primaryButton,
+ errorText: {
+ fontSize: "12px",
+ marginTop: "4px",
+ },
+});
+
+const PeopleAndGroups: React.FC = ({
+ label,
+ peopleMap,
+ groupsMap,
+ emails,
+ createUrl,
+ update,
+ searchPeople,
+ searchGroups,
+ mutatePeopleMap,
+ mutateGroupsMap,
+ isMe,
+ addGreenTick,
+ disable,
+}) => {
+ const styles = useStyles();
+ const { isDark } = useTheme();
+ const theme = isDark ? DarkTheme : LightTheme;
+ const componentRef = React.useRef(null);
+ const [showDropdown, setShowDropdown] = React.useState(false);
+ const [query, setQuery] = React.useState("");
+ const [selectedOptions, setSelectedOptions] = React.useState(emails);
+ const [tempSelectedOptions, setTempSelectedOptions] = React.useState(emails);
+ const [peopleOptions, setPeopleOptions] = React.useState([]);
+ const [groupOptions, setGroupOptions] = React.useState([]);
+ const [isHovered, setIsHovered] = React.useState(false);
+ const [isCancelHovered, setIsCancelHovered] = React.useState(false);
+
+ // Auto-scroll function to bring field to top
+ const scrollFieldToTop = () => {
+ if (componentRef.current) {
+ componentRef.current.scrollIntoView({
+ behavior: 'smooth',
+ block: 'start'
+ });
+ }
+ };
+
+ React.useEffect(() => {
+ setSelectedOptions(emails);
+ if (!showDropdown) {
+ setTempSelectedOptions(emails);
+ }
+ }, [emails, showDropdown]);
+
+ const search = React.useCallback(async (q: string) => {
+ setQuery(q);
+ if (!q || !q.trim()) {
+ setPeopleOptions([]);
+ setGroupOptions([]);
+ return;
+ }
+
+ try {
+ const [peopleResults, groupResults] = await Promise.all([
+ searchPeople(q),
+ searchGroups(q),
+ ]);
+
+ const newPeopleMap = new Map(peopleMap);
+ const newPeopleOptions = (peopleResults || [])
+ .map((person) => {
+ const email = person.emailAddresses?.[0]?.value;
+ if (!email) {
+ return undefined;
+ }
+ newPeopleMap.set(email, person);
+ return email;
+ })
+ .filter((email): email is string => Boolean(email) && !tempSelectedOptions.includes(email));
+
+ const newGroupMap = new Map(groupsMap);
+ const newGroupOptions = (groupResults || [])
+ .map((group) => {
+ if (!group.email) {
+ return undefined;
+ }
+ newGroupMap.set(group.email, group);
+ return group.email;
+ })
+ .filter((email): email is string => Boolean(email) && !tempSelectedOptions.includes(email));
+
+ setPeopleOptions(newPeopleOptions);
+ setGroupOptions(newGroupOptions);
+ mutatePeopleMap(newPeopleMap);
+ mutateGroupsMap(newGroupMap);
+ } catch (error) {
+ console.log("Search error:", error);
+ setPeopleOptions([]);
+ setGroupOptions([]);
+ }
+ }, [mutatePeopleMap, mutateGroupsMap, searchPeople, searchGroups, tempSelectedOptions]);
+
+ const onOptionSelect: TagPickerProps["onOptionSelect"] = async (_, data) => {
+ if (data.value === "no-matches") {
+ return;
+ }
+
+ setTempSelectedOptions(data.selectedOptions);
+ setQuery("");
+ };
+
+ const handleSave = async () => {
+ await update(tempSelectedOptions);
+ setSelectedOptions(tempSelectedOptions);
+ setShowDropdown(false);
+ };
+
+ const handleCancel = () => {
+ setTempSelectedOptions(selectedOptions);
+ setQuery("");
+ setPeopleOptions([]);
+ setGroupOptions([]);
+ setShowDropdown(false);
+ };
+
+ const renderEntries = (values: string[]) => {
+ if (!values.length) {
+ return None ;
+ }
+
+ return (
+ <>
+ {values.map((email) => {
+ const group = groupsMap.get(email);
+ if (group) {
+ return (
+
+
}
+ shape="square"
+ />
+
{group.name}
+
+ );
+ }
+
+ const person = peopleMap.get(email);
+ if (person) {
+ const displayName = person.names?.[0]?.displayName || email;
+ const showGreenTick = addGreenTick && addGreenTick(email);
+ return (
+
+
+
{isMe(email) ? "Me" : displayName}
+
+ );
+ }
+
+ return (
+
+ );
+ })}
+ >
+ );
+ };
+
+ const noMatch = (
+
+ No matches found
+
+ );
+
+ const renderOptions = () => {
+ const options: JSX.Element[] = [];
+
+ // Show people first
+ peopleOptions.forEach((email) => {
+ const person = peopleMap.get(email);
+ if (person) {
+ const name = isMe(email) ? "Me" : person.names?.[0]?.displayName || email;
+ options.push(
+ }
+ style={{
+ backgroundColor: theme.background.elevated,
+ color: theme.text.primary,
+ }}
+ >
+ {name}
+
+ );
+ } else {
+ options.push(
+ }
+ style={{
+ backgroundColor: theme.background.elevated,
+ color: theme.text.primary,
+ }}
+ >
+ {email}
+
+ );
+ }
+ });
+
+ // Then show groups
+ groupOptions.forEach((email) => {
+ const group = groupsMap.get(email);
+ if (group) {
+ options.push(
+ } />}
+ style={{
+ backgroundColor: theme.background.elevated,
+ color: theme.text.primary,
+ }}
+ >
+ {group.name}
+
+ );
+ }
+ });
+
+ return options.length ? options : [noMatch];
+ };
+
+ const renderDropdown = () => (
+ <>
+
+
+
+
+ {tempSelectedOptions.map((option) => {
+ const group = groupsMap.get(option);
+ if (group) {
+ return (
+ } />}
+ >
+ {group.name}
+
+ );
+ }
+
+ const person = peopleMap.get(option);
+ if (person) {
+ const name = isMe(option)
+ ? "Me"
+ : person.names?.[0]?.displayName || option;
+ return (
+
+ }
+ >
+ {name}
+
+ );
+ }
+
+ return (
+
+ {option}
+
+ );
+ })}
+
+ search(event.target.value)}
+ className={styles.tagPickerInput}
+ style={{ minWidth: "100px", flexGrow: 1 }}
+ />
+
+ {renderOptions()}
+
+
+
+ }
+ onClick={handleSave}
+ >
+ Save
+
+ }
+ onClick={handleCancel}
+ onMouseEnter={() => setIsCancelHovered(true)}
+ onMouseLeave={() => setIsCancelHovered(false)}
+ style={{
+ backgroundColor: isCancelHovered ? theme.background.tertiary : "transparent",
+ color: isCancelHovered ? theme.text.primary : theme.text.secondary,
+ border: `1px solid ${isCancelHovered ? theme.border.secondary : theme.border.primary}`,
+ borderRadius: "4px",
+ fontWeight: "500",
+ }}
+ >
+ Cancel
+
+
+ >
+ );
+
+ const handleOpenDropdown = () => {
+ setTempSelectedOptions(selectedOptions);
+ setShowDropdown(true);
+ // Auto-scroll to top after a brief delay to ensure dropdown renders
+ setTimeout(() => scrollFieldToTop(), 100);
+ };
+
+ const handleCloseDropdown = () => {
+ setTempSelectedOptions(selectedOptions);
+ setQuery("");
+ setPeopleOptions([]);
+ setGroupOptions([]);
+ setShowDropdown(false);
+ };
+
+ return (
+
+
{label}
+ {showDropdown && !disable ? (
+ renderDropdown()
+ ) : (
+
setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ style={{
+ cursor: !disable ? "pointer" : "default",
+ backgroundColor: isHovered && !disable ? theme.background.tertiary : "transparent",
+ boxShadow: isHovered && !disable ? theme.shadows.small : undefined,
+ }}
+ >
+
{renderEntries(selectedOptions)}
+ {!disable && (
+
+ )}
+
+ )}
+
+ );
+};
+
+export default PeopleAndGroups;
diff --git a/hermes-plugin/src/taskpane/components/ProductIcon.tsx b/hermes-plugin/src/taskpane/components/ProductIcon.tsx
new file mode 100644
index 000000000..31d66db9c
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/ProductIcon.tsx
@@ -0,0 +1,162 @@
+import * as React from "react";
+import { getProductId, getProductColor, getContrastColor } from "../utils/productUtils";
+import { makeStyles, mergeClasses } from "@fluentui/react-components";
+
+const useStyles = makeStyles({
+ avatar: {
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ borderRadius: "4px",
+ fontWeight: "600",
+ fontSize: "10px",
+ textTransform: "uppercase",
+ position: "relative",
+ flexShrink: 0,
+ },
+ small: {
+ width: "20px",
+ height: "20px",
+ fontSize: "8px",
+ },
+ medium: {
+ width: "24px",
+ height: "24px",
+ fontSize: "10px",
+ },
+ large: {
+ width: "32px",
+ height: "32px",
+ fontSize: "12px",
+ },
+});
+
+type ProductIconSize = "small" | "medium" | "large";
+
+interface ProductIconProps {
+ product?: string;
+ productData?: { abbreviation?: string; [key: string]: any };
+ size?: ProductIconSize;
+ className?: string;
+}
+
+// Product abbreviations from API response
+const PRODUCT_ABBREVIATIONS: Record = {
+ "Boundary": "ICU",
+ "Cloud Platform": "HCP",
+ "Consul": "CSL",
+ "Engineering": "ENG",
+ "Labs": "LAB",
+ "MyProduct": "MY",
+ "SRE": "SRE",
+ "Terraform": "TFC",
+ "Vagrant": "VGT",
+ "Vault": "VLT",
+ "Waypoint": "WP",
+};
+
+// HashiCorp product SVG icons from Flight Icons
+// Using white/contrast colors to match web app's product-badge styling
+const PRODUCT_ICONS: Record = {
+ terraform: ` `,
+
+ vault: ` `,
+
+ consul: ` `,
+
+ nomad: ` `,
+
+ packer: ` `,
+
+ boundary: ` `,
+
+ waypoint: ` `,
+
+ vagrant: ` `,
+
+ hcp: ` `,
+};
+
+// Default folder icon (Unicode)
+const FOLDER_ICON = "📁";
+
+const ProductIcon: React.FC = ({ product, productData, size = "small", className }) => {
+ const styles = useStyles();
+
+ // Follow exact Ember logic:
+ // 1. If productID exists -> show icon
+ // 2. Else if abbreviation exists -> show abbreviation text
+ // 3. Else -> show folder icon
+
+ const productId = getProductId(product);
+ // Use API abbreviation if available, fallback to hardcoded mappings
+ const abbreviation = productData?.abbreviation || (product ? PRODUCT_ABBREVIATIONS[product] : undefined);
+
+ // Determine what to show based on Ember logic
+ const iconIsShown = !!productId || !abbreviation; // Show icon if productID exists OR no abbreviation
+ const abbreviationIsShown = !productId && !!abbreviation; // Show abbreviation only if no productID but has abbreviation
+
+ const sizeClass = size === "large" ? styles.large : size === "medium" ? styles.medium : styles.small;
+
+ // Color logic: Follow Ember logic exactly
+ // Products with productID get brand-colored backgrounds with white/contrast icons
+ // Products without productID get hash-based colors for abbreviation text
+ const style: React.CSSProperties = productId ? {
+ // Brand-colored backgrounds for HashiCorp products (matching web gradients)
+ backgroundColor: {
+ 'terraform': '#7B42BC',
+ 'vault': '#FFD814',
+ 'consul': '#E03875',
+ 'nomad': '#06D092',
+ 'packer': '#02A8EF',
+ 'boundary': '#F24C53',
+ 'waypoint': '#14C6CB',
+ 'vagrant': '#1868F2',
+ 'hcp': '#000000',
+ }[productId] || '#0078d4',
+ color: productId === 'vault' ? '#000000' : '#ffffff', // Black text for yellow vault, white for others
+ } : {
+ // Hash-based colors for abbreviation text (like web app)
+ backgroundColor: getProductColor(product) || '#6b7280',
+ color: getContrastColor(getProductColor(product) || '#6b7280'),
+ };
+
+ return (
+
+ {iconIsShown ? (
+ productId ? (
+ // Show SVG product icon for known HashiCorp products
+
${productId.charAt(0).toUpperCase()}` }}
+ />
+ ) : (
+ // Show folder icon for unknown products
+
+ {FOLDER_ICON}
+
+ )
+ ) : abbreviationIsShown ? (
+
+ {abbreviation && abbreviation.length > 3 ? abbreviation.slice(0, 1) : abbreviation}
+
+ ) : null}
+
+ );
+};
+
+export default ProductIcon;
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/components/ProjectIcon.tsx b/hermes-plugin/src/taskpane/components/ProjectIcon.tsx
new file mode 100644
index 000000000..28c225efc
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/ProjectIcon.tsx
@@ -0,0 +1,42 @@
+import * as React from "react";
+
+interface ProjectIconProps {
+ className?: string;
+}
+
+const ProjectIcon: React.FC
= ({ className }) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default ProjectIcon;
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/components/ProjectStatusIcon.tsx b/hermes-plugin/src/taskpane/components/ProjectStatusIcon.tsx
new file mode 100644
index 000000000..712cbbed4
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/ProjectStatusIcon.tsx
@@ -0,0 +1,88 @@
+import * as React from "react";
+import { makeStyles, mergeClasses } from "@fluentui/react-components";
+import { ProjectStatus, PROJECT_COLORS } from "../interfaces/project";
+
+const useStyles = makeStyles({
+ icon: {
+ display: "inline-block",
+ position: "relative",
+ flexShrink: 0,
+ },
+ small: {
+ width: "16px",
+ height: "16px",
+ },
+ medium: {
+ width: "20px",
+ height: "20px",
+ },
+ large: {
+ width: "24px",
+ height: "24px",
+ },
+});
+
+type ProjectStatusIconSize = "small" | "medium" | "large";
+
+interface ProjectStatusIconProps {
+ status: ProjectStatus;
+ size?: ProjectStatusIconSize;
+}
+
+const ProjectStatusIcon: React.FC = ({
+ status,
+ size = "medium"
+}) => {
+ const styles = useStyles();
+
+ const colors = PROJECT_COLORS[status];
+ const sizeClass = styles[size];
+
+ // Render folder icon with status overlay similar to web app
+ const renderFolderIcon = () => {
+ return (
+
+ {/* Folder base */}
+
+
+ {/* Status indicator overlay */}
+ {status === ProjectStatus.Active && (
+ // Lightning bolt for active projects
+
+ )}
+
+ {status === ProjectStatus.Completed && (
+ // Checkmark for completed projects
+
+ )}
+
+ {status === ProjectStatus.Archived && (
+ // Archive box for archived projects (no special icon, just folder)
+ <>>
+ )}
+
+ );
+ };
+
+ return renderFolderIcon();
+};
+
+export default ProjectStatusIcon;
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/components/ProjectsList.tsx b/hermes-plugin/src/taskpane/components/ProjectsList.tsx
new file mode 100644
index 000000000..4849f4b45
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/ProjectsList.tsx
@@ -0,0 +1,441 @@
+import * as React from "react";
+import {
+ makeStyles,
+ Text,
+ Button,
+ Input,
+ ProgressBar,
+ Tooltip,
+} from "@fluentui/react-components";
+import {
+ Add24Regular,
+ Search24Regular,
+ Dismiss24Regular,
+ QuestionCircle16Regular,
+} from "@fluentui/react-icons";
+import { HermesProject } from "../interfaces/project";
+import ProjectIcon from "./ProjectIcon";
+import WordPluginController from "../utils/wordPluginController";
+import DarkTheme from "../utils/darkTheme";
+import LightTheme from "../utils/lightTheme";
+import { useTheme } from "../utils/themeContext";
+
+// Utility function to truncate text to a specified length
+const truncateText = (text: string, maxLength: number = 100): string => {
+ if (text.length <= maxLength) return text;
+ return text.substring(0, maxLength).replace(/\s+\S*$/, '') + '...';
+};
+
+const useStyles = makeStyles({
+ container: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "12px",
+ },
+
+ projectsList: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "8px",
+ },
+
+ projectItem: {
+ display: "flex",
+ alignItems: "center",
+ gap: "8px",
+ padding: "8px",
+ borderRadius: "8px",
+ border: `1px solid ${DarkTheme.border.primary}`,
+ backgroundColor: DarkTheme.background.secondary,
+ transition: "all 0.2s ease",
+ cursor: "pointer",
+ minWidth: 0, // Allow flexbox to shrink below content width
+ width: "100%", // Ensure full width usage
+ boxSizing: "border-box", // Include padding/border in width calculation
+ ":hover": {
+ backgroundColor: "var(--project-item-hover-bg)",
+ },
+ },
+
+ projectInfo: {
+ flex: 1,
+ display: "flex",
+ flexDirection: "column",
+ gap: "2px",
+ minWidth: 0, // Allow flexbox to shrink below content width
+ overflow: "hidden", // Prevent content from breaking out
+ },
+
+ projectTitle: {
+ fontSize: "12px",
+ fontWeight: "600",
+ color: DarkTheme.text.primary,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ },
+
+ projectDescription: {
+ fontSize: "12px",
+ color: DarkTheme.text.secondary,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ display: "-webkit-box",
+ "-webkit-line-clamp": "2",
+ "-webkit-box-orient": "vertical",
+ lineHeight: "1.4",
+ maxHeight: "2.8em", // 2 lines * 1.4 line-height
+ wordBreak: "break-word",
+ },
+
+ projectIcon: {
+ width: "20px",
+ height: "20px",
+ flexShrink: 0,
+ },
+
+ addProjectSection: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "8px",
+ padding: "8px",
+ borderRadius: "8px",
+ border: `1px solid ${DarkTheme.border.primary}`,
+ backgroundColor: DarkTheme.background.secondary,
+ },
+
+ searchInput: {
+ width: "100%",
+ },
+
+ searchResults: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "4px",
+ maxHeight: "200px",
+ overflowY: "auto",
+ },
+
+ searchResultItem: {
+ display: "flex",
+ alignItems: "center",
+ gap: "8px",
+ padding: "8px",
+ borderRadius: "8px",
+ border: `1px solid ${DarkTheme.border.primary}`,
+ backgroundColor: DarkTheme.background.secondary,
+ cursor: "pointer",
+ transition: "all 0.2s ease",
+ ":hover": {
+ backgroundColor: "var(--project-search-item-hover-bg)",
+ },
+ },
+
+ actionButtons: {
+ display: "flex",
+ gap: "4px",
+ },
+
+ loadingContainer: {
+ padding: "16px",
+ display: "flex",
+ justifyContent: "center",
+ },
+
+ errorContainer: {
+ padding: "8px",
+ fontSize: "14px",
+ textAlign: "center",
+ },
+
+ emptyState: {
+ padding: "16px",
+ textAlign: "center",
+ color: DarkTheme.text.tertiary,
+ fontSize: "14px",
+ },
+});
+
+interface ProjectsListProps {
+ projects: HermesProject[];
+ isLoading: boolean;
+ error: string | null;
+ isOwner: boolean;
+ isDraft: boolean;
+ showAddForm?: boolean;
+ controller: WordPluginController;
+ onAdd: (project: HermesProject) => Promise;
+ onRemove: (projectId: string) => Promise;
+ onRetry: () => void;
+}
+
+const ProjectsList: React.FC = ({
+ projects,
+ isLoading,
+ error,
+ isOwner,
+ isDraft,
+ showAddForm: externalShowAddForm,
+ controller,
+ onAdd,
+ onRemove,
+ onRetry,
+}) => {
+ const styles = useStyles();
+ const { isDark } = useTheme();
+ const theme = isDark ? DarkTheme : LightTheme;
+ const [showAddForm, setShowAddForm] = React.useState(externalShowAddForm || false);
+
+ // Update local state when external prop changes
+ React.useEffect(() => {
+ if (externalShowAddForm !== undefined) {
+ setShowAddForm(externalShowAddForm);
+ }
+ }, [externalShowAddForm]);
+ const [searchQuery, setSearchQuery] = React.useState("");
+ const [searchResults, setSearchResults] = React.useState([]);
+ const [isSearching, setIsSearching] = React.useState(false);
+ const [searchError, setSearchError] = React.useState(null);
+
+ // Debounced search effect
+ React.useEffect(() => {
+ const timeoutId = setTimeout(async () => {
+ if (searchQuery.trim().length >= 2) {
+ setIsSearching(true);
+ setSearchError(null);
+
+ try {
+ const results = await controller.searchProjects(searchQuery.trim());
+
+ // Filter out projects that are already associated with the document
+ const filteredResults = results.filter(result =>
+ !projects.some(existingProject => existingProject.id === result.id)
+ );
+
+ setSearchResults(filteredResults);
+ } catch (error) {
+ console.error("Failed to search projects:", error);
+ setSearchError(error instanceof Error ? error.message : "Failed to search projects");
+ setSearchResults([]);
+ } finally {
+ setIsSearching(false);
+ }
+ } else {
+ setSearchResults([]);
+ }
+ }, 300);
+
+ return () => clearTimeout(timeoutId);
+ }, [searchQuery, projects, controller]);
+
+ const handleAddProject = async (project: HermesProject) => {
+ try {
+ await onAdd(project);
+ setSearchQuery("");
+ setSearchResults([]);
+ setShowAddForm(false);
+ } catch (error) {
+ console.error("Failed to add project:", error);
+ }
+ };
+
+ const handleRemoveProject = async (projectId: string) => {
+ try {
+ await onRemove(projectId);
+ } catch (error) {
+ console.error("Failed to remove project:", error);
+ }
+ };
+
+ const renderContent = () => {
+ if (isDraft) {
+ return (
+
+
+ Publish to manage projects
+
+
+ );
+ }
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ {error}
+
+ Retry
+
+
+ );
+ }
+
+ return (
+ <>
+ {/* Existing projects list */}
+ {projects.length > 0 ? (
+
+ {projects.map((project) => (
+
+
+
{
+ const baseUrl = controller.getHermesBaseUrl();
+ const projectUrl = `${baseUrl}/projects/${project.id}`;
+ window.open(projectUrl, '_blank');
+ }}
+ >
+ 50 ? project.title : undefined}
+ relationship="description"
+ >
+ {truncateText(project.title, 50)}
+
+ {project.description && (
+ 100 ? project.description : undefined}
+ relationship="description"
+ >
+
+ {truncateText(project.description, 100)}
+
+
+ )}
+
+ {isOwner && (
+
+ }
+ onClick={() => handleRemoveProject(project.id)}
+ aria-label="Remove from project"
+ />
+
+ )}
+
+ ))}
+
+ ) : (
+
+ No projects associated with this document.
+
+ )}
+
+ {/* Add project section */}
+ {isOwner && showAddForm && (
+
+
}
+ contentAfter={
+
}
+ onClick={() => {
+ setShowAddForm(false);
+ setSearchQuery("");
+ setSearchResults([]);
+ }}
+ />
+ }
+ onChange={(e) => setSearchQuery(e.target.value)}
+ />
+
+ {isSearching && (
+
+ )}
+
+ {searchError && (
+
+ {searchError}
+
+ )}
+
+ {searchQuery.trim().length > 0 && searchQuery.trim().length < 2 && (
+
+ Type at least 2 characters to search...
+
+ )}
+
+ {searchResults.length > 0 && (
+
+ {searchResults.map((project) => (
+
handleAddProject(project)}
+ style={{
+ backgroundColor: theme.background.secondary,
+ borderColor: theme.border.primary,
+ "--project-search-item-hover-bg": theme.background.tertiary,
+ } as React.CSSProperties}
+ >
+
+
+ 50 ? project.title : undefined}
+ relationship="description"
+ >
+ {truncateText(project.title, 50)}
+
+ {project.description && (
+ 100 ? project.description : undefined}
+ relationship="description"
+ >
+
+ {truncateText(project.description, 100)}
+
+
+ )}
+
+
+ ))}
+
+ )}
+
+ {searchQuery.trim().length >= 2 && !isSearching && searchResults.length === 0 && !searchError && (
+
+ No projects found for "{searchQuery}"
+
+ )}
+
+ )}
+ >
+ );
+ };
+
+ return (
+
+ {renderContent()}
+
+ );
+};
+
+export default ProjectsList;
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/components/RelatedResourceItem.tsx b/hermes-plugin/src/taskpane/components/RelatedResourceItem.tsx
new file mode 100644
index 000000000..08ba6af35
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/RelatedResourceItem.tsx
@@ -0,0 +1,267 @@
+import * as React from "react";
+import {
+ makeStyles,
+ mergeClasses,
+ Text,
+ Button,
+ Tooltip,
+} from "@fluentui/react-components";
+import {
+ Document24Regular,
+ Link24Regular,
+ Edit24Regular,
+ Delete24Regular,
+ Open16Regular
+} from "@fluentui/react-icons";
+import {
+ RelatedResource,
+ RelatedExternalLink,
+ isExternalLink,
+ isHermesDocument
+} from "../interfaces/relatedResources";
+import DocumentThumbnail from "./DocumentThumbnail";
+import DarkTheme from "../utils/darkTheme";
+import LightTheme from "../utils/lightTheme";
+import { useTheme } from "../utils/themeContext";
+
+const useStyles = makeStyles({
+ container: {
+ display: "flex",
+ alignItems: "flex-start",
+ gap: "12px",
+ padding: "12px",
+ borderRadius: "6px",
+ transition: "all 0.2s ease",
+ },
+
+ clickableContainer: {
+ cursor: "pointer",
+ },
+
+ iconContainer: {
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ flexShrink: 0,
+ },
+
+ simpleIconContainer: {
+ width: "24px",
+ height: "24px",
+ },
+
+ documentIcon: {
+ color: DarkTheme.interactive.primary,
+ },
+
+ linkIcon: {
+ color: DarkTheme.text.tertiary,
+ },
+
+ contentContainer: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "2px",
+ flexGrow: 1,
+ minWidth: 0, // Allow text truncation
+ },
+
+ title: {
+ fontSize: "14px",
+ fontWeight: 500,
+ lineHeight: "1.2",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ },
+
+ subtitle: {
+ fontSize: "12px",
+ lineHeight: "1.2",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ },
+
+ actionsContainer: {
+ display: "flex",
+ alignItems: "center",
+ gap: "4px",
+ flexShrink: 0,
+ },
+
+ actionButton: {
+ minWidth: "auto",
+ padding: "4px",
+ },
+
+ editButton: {
+ color: DarkTheme.interactive.primary,
+ },
+
+ removeButton: {
+ color: DarkTheme.interactive.danger,
+ },
+
+ openButton: {
+ color: DarkTheme.text.tertiary,
+ },
+});
+
+interface RelatedResourceItemProps {
+ resource: RelatedResource;
+ isOwner: boolean;
+ baseUrl: string;
+ onEdit: (resource: RelatedExternalLink) => void;
+ onRemove: (resource: RelatedResource) => void;
+}
+
+const RelatedResourceItem: React.FC = ({
+ resource,
+ isOwner,
+ baseUrl,
+ onEdit,
+ onRemove,
+}) => {
+ const styles = useStyles();
+ const { isDark } = useTheme();
+ const theme = isDark ? DarkTheme : LightTheme;
+ const [isHovered, setIsHovered] = React.useState(false);
+
+ const handleClick = () => {
+ if (isExternalLink(resource)) {
+ // Open external link in new tab
+ window.open(resource.url, "_blank", "noopener,noreferrer");
+ } else if (isHermesDocument(resource)) {
+ // Open Hermes document in new tab using the correct baseUrl
+ const documentUrl = `${baseUrl}/document/${resource.FileID}`;
+ window.open(documentUrl, "_blank", "noopener,noreferrer");
+ }
+ };
+
+ const handleEdit = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (isExternalLink(resource)) {
+ onEdit(resource);
+ }
+ };
+
+ const handleRemove = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ onRemove(resource);
+ };
+
+ const getIcon = () => {
+ if (isHermesDocument(resource)) {
+ return (
+
+ );
+ } else {
+ return ;
+ }
+ };
+
+ const getTitle = () => {
+ if (isHermesDocument(resource)) {
+ return resource.title;
+ } else {
+ return resource.name || resource.url;
+ }
+ };
+
+ const getSubtitle = () => {
+ if (isHermesDocument(resource)) {
+ const parts = [];
+ if (resource.documentType) {
+ parts.push(resource.documentType);
+ }
+ if (resource.documentNumber) {
+ parts.push(resource.documentNumber);
+ }
+ return parts.join(" · ");
+ } else {
+ // For external links, show the domain
+ try {
+ const url = new URL(resource.url);
+ return url.hostname.replace(/^www\./, "");
+ } catch {
+ return resource.url;
+ }
+ }
+ };
+
+ const containerClasses = [
+ styles.container,
+ (isExternalLink(resource) || isHermesDocument(resource)) ? styles.clickableContainer : ""
+ ].join(" ");
+
+ return (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ style={{
+ backgroundColor: isHovered ? theme.background.tertiary : theme.background.secondary,
+ border: `1px solid ${theme.border.primary}`,
+ }}
+ >
+
+ {getIcon()}
+
+
+
+
+ {getTitle()}
+
+
+ {getSubtitle()}
+
+
+
+ {isOwner && (
+
+ {isExternalLink(resource) && (
+ <>
+
+ }
+ size="small"
+ onClick={(e) => {
+ e.stopPropagation();
+ handleClick();
+ }}
+ />
+
+
+ }
+ size="small"
+ onClick={handleEdit}
+ />
+
+ >
+ )}
+
+ }
+ size="small"
+ onClick={handleRemove}
+ />
+
+
+ )}
+
+ );
+};
+
+export default RelatedResourceItem;
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/components/RelatedResourcesList.tsx b/hermes-plugin/src/taskpane/components/RelatedResourcesList.tsx
new file mode 100644
index 000000000..8fd1d4537
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/RelatedResourcesList.tsx
@@ -0,0 +1,253 @@
+import * as React from "react";
+import {
+ makeStyles,
+ Text,
+ Button,
+ ProgressBar,
+} from "@fluentui/react-components";
+import { Add24Regular } from "@fluentui/react-icons";
+import { RelatedResource, combineAndSortResources, RelatedResourcesResponse } from "../interfaces/relatedResources";
+import RelatedResourceItem from "./RelatedResourceItem";
+import AddResourceForm from "./AddResourceForm";
+import EditResourceForm from "./EditResourceForm";
+import { RelatedExternalLink } from "../interfaces/relatedResources";
+import IDocumentMetadata from "../interfaces/documentMetadata";
+import DarkTheme from "../utils/darkTheme";
+import LightTheme from "../utils/lightTheme";
+import { useTheme } from "../utils/themeContext";
+
+const useStyles = makeStyles({
+ container: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "8px",
+ },
+
+ header: {
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+ },
+
+ label: {
+ color: DarkTheme.text.tertiary,
+ fontSize: "12px",
+ fontWeight: 500,
+ },
+
+ addButton: {
+ minWidth: "auto",
+ padding: "4px 8px",
+ backgroundColor: DarkTheme.interactive.primary,
+ color: DarkTheme.text.primary,
+ },
+
+ list: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "4px",
+ },
+
+ emptyState: {
+ padding: "16px",
+ textAlign: "center",
+ color: DarkTheme.text.tertiary,
+ fontSize: "14px",
+ fontStyle: "italic",
+ },
+
+ emptyStateReadOnly: {
+ padding: "8px",
+ color: DarkTheme.text.tertiary,
+ fontSize: "14px",
+ },
+
+ loadingContainer: {
+ padding: "16px",
+ display: "flex",
+ justifyContent: "center",
+ },
+
+ errorContainer: {
+ padding: "8px",
+ fontSize: "14px",
+ textAlign: "center",
+ },
+
+ retryButton: {
+ marginTop: "8px",
+ },
+});
+
+interface RelatedResourcesListProps {
+ resources: RelatedResourcesResponse;
+ isLoading: boolean;
+ error: string | null;
+ isOwner: boolean;
+ baseUrl: string;
+ onAdd: (resource: RelatedResource) => Promise;
+ onEdit: (resource: RelatedExternalLink) => Promise;
+ onRemove: (resource: RelatedResource) => Promise;
+ onRetry: () => void;
+ onSearchDocuments?: (query: string) => Promise;
+}
+
+const RelatedResourcesList: React.FC = ({
+ resources,
+ isLoading,
+ error,
+ isOwner,
+ baseUrl,
+ onAdd,
+ onEdit,
+ onRemove,
+ onRetry,
+ onSearchDocuments,
+}) => {
+ const styles = useStyles();
+ const { isDark } = useTheme();
+ const theme = isDark ? DarkTheme : LightTheme;
+ const [showAddForm, setShowAddForm] = React.useState(false);
+ const [editingResource, setEditingResource] = React.useState(null);
+
+ const combinedResources = combineAndSortResources(resources);
+
+ const handleAdd = async (resource: RelatedResource) => {
+ try {
+ await onAdd(resource);
+ setShowAddForm(false);
+ } catch (error) {
+ console.error("Failed to add resource:", error);
+ // Re-throw the error so AddResourceForm can handle it
+ throw error;
+ }
+ };
+
+ const handleEdit = async (resource: RelatedExternalLink) => {
+ try {
+ await onEdit(resource);
+ setEditingResource(null);
+ } catch (error) {
+ console.error("Failed to edit resource:", error);
+ // Re-throw the error so EditResourceForm can handle it
+ throw error;
+ }
+ };
+
+ const handleRemove = async (resource: RelatedResource) => {
+ try {
+ await onRemove(resource);
+ setEditingResource(null);
+ } catch (error) {
+ console.error("Failed to remove resource:", error);
+ // Re-throw the error so components can handle it
+ throw error;
+ }
+ };
+
+ const handleEditClick = (resource: RelatedExternalLink) => {
+ setEditingResource(resource);
+ };
+
+ const handleCancelAdd = () => {
+ setShowAddForm(false);
+ };
+
+ const handleCancelEdit = () => {
+ setEditingResource(null);
+ };
+
+ const renderContent = () => {
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ {error}
+
+ Retry
+
+
+ );
+ }
+
+ if (combinedResources.length === 0) {
+ if (isOwner) {
+ return (
+
+ No related resources yet. Click + to add one.
+
+ );
+ } else {
+ return (
+
+ None
+
+ );
+ }
+ }
+
+ return (
+
+ {combinedResources.map((resource, index) => (
+
+
+ {editingResource && editingResource.sortOrder === resource.sortOrder && (
+
+ )}
+
+ ))}
+
+ );
+ };
+
+ return (
+
+
+ Related Resources
+ {isOwner && !isLoading && !error && !showAddForm && (
+ }
+ size="small"
+ onClick={() => setShowAddForm(true)}
+ />
+ )}
+
+
+ {showAddForm && (
+
+ )}
+
+ {renderContent()}
+
+ );
+};
+
+export default RelatedResourcesList;
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/components/Sidebar.tsx b/hermes-plugin/src/taskpane/components/Sidebar.tsx
new file mode 100644
index 000000000..3e71759df
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/Sidebar.tsx
@@ -0,0 +1,2152 @@
+import * as React from "react";
+import {
+ makeStyles,
+ Text,
+ Badge,
+ Avatar,
+ ProgressBar,
+ Dropdown,
+ Option,
+ Button,
+ Tooltip,
+ Combobox,
+} from "@fluentui/react-components";
+import DarkTheme, { commonStyles } from "../utils/darkTheme";
+import LightTheme from "../utils/lightTheme";
+import { useTheme } from "../utils/themeContext";
+import ThemeToggleButton from "./ThemeToggleButton";
+import EditableText from "./EditableText";
+import { Person } from "../interfaces/person";
+import { Group } from "../interfaces/group";
+import WordPluginController from "../utils/wordPluginController";
+import timeAgo from "../utils/timeAgo";
+import People from "./People";
+import PeopleAndGroups from "./PeopleAndGroups";
+import ProductIcon from "./ProductIcon";
+import ProjectsList from "./ProjectsList";
+import RelatedResourcesList from "./RelatedResourcesList";
+import { Delete24Regular, Checkmark24Regular, Dismiss24Regular, Open16Filled, PersonDelete20Regular, Edit24Regular, QuestionCircle16Regular, Add16Regular, Copy16Regular, Archive24Regular, ArchiveArrowBack24Regular } from "@fluentui/react-icons";
+import { RelatedResource, RelatedExternalLink, RelatedResourcesResponse } from "../interfaces/relatedResources";
+import { HermesProject } from "../interfaces/project";
+
+const useStyles = makeStyles({
+ root: {
+ height: "100vh",
+ padding: "16px",
+ paddingTop: "44px", // Much reduced space for ultra-thin fixed header
+ paddingBottom: "80px", // Space for fixed footer
+ boxSizing: "border-box",
+ borderRight: `1px solid ${DarkTheme.border.primary}`,
+ display: "flex",
+ flexDirection: "column",
+ gap: "16px",
+ overflowY: "auto",
+ backgroundColor: DarkTheme.background.primary,
+ },
+ section: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "4px",
+ },
+ rowSection: {
+ display: "flex",
+ flexDirection: "row",
+ gap: "8px",
+ },
+ label: {
+ color: DarkTheme.text.tertiary,
+ fontSize: "12px",
+ fontWeight: 500,
+ },
+ value: {
+ fontSize: "14px",
+ color: DarkTheme.text.primary,
+ },
+ primaryBtn: {
+ ...commonStyles.primaryButton,
+ width: "100%",
+ },
+ secondaryBtn: {
+ ...commonStyles.secondaryButton,
+ width: "100%",
+ },
+ iconButton: {
+ ...commonStyles.primaryIconButton,
+ minWidth: "36px !important",
+ minHeight: "36px !important",
+ },
+ secondaryIconButton: {
+ ...commonStyles.secondaryIconButton,
+ minWidth: "36px !important",
+ minHeight: "36px !important",
+ },
+
+ floatingHeaderSection: {
+ position: "fixed",
+ top: 0,
+ left: 0,
+ right: 0,
+ padding: "4px 16px 4px 16px", // Match content padding exactly
+ backgroundColor: DarkTheme.background.primary,
+ borderBottom: `1px solid ${DarkTheme.border.primary}`,
+ zIndex: 10,
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between", // Spread items across full width
+ gap: "16px", // Reduced gap between elements
+ },
+
+ navHermesLink: {
+ color: DarkTheme.components.header.linkColor,
+ fontSize: "18px",
+ fontWeight: "600",
+ textDecoration: "none",
+ display: "flex",
+ alignItems: "center",
+ gap: "6px",
+ "&:hover": {
+ color: DarkTheme.components.header.linkHover,
+ textDecoration: "none",
+ },
+ "&:focus": {
+ outline: `2px solid ${DarkTheme.border.focus}`,
+ outlineOffset: "2px",
+ },
+ },
+
+ shareButton: {
+ display: "flex",
+ alignItems: "center",
+ gap: "4px",
+ padding: "6px 12px",
+ backgroundColor: "transparent",
+ border: "none",
+ borderRadius: "6px",
+ fontSize: "14px",
+ fontWeight: "500",
+ color: DarkTheme.text.secondary,
+ cursor: "pointer",
+ transition: "all 0.2s ease",
+ "&:hover": {
+ backgroundColor: DarkTheme.background.tertiary,
+ },
+ "&:focus": {
+ outline: `2px solid ${DarkTheme.border.focus}`,
+ outlineOffset: "2px",
+ },
+ },
+
+ floatingFooterSection: {
+ position: "fixed",
+ bottom: 0,
+ left: 0,
+ right: 0,
+ padding: "16px",
+ backgroundColor: DarkTheme.background.primary,
+ borderTop: `1px solid ${DarkTheme.border.primary}`,
+ zIndex: 10,
+ },
+ clickableField: {
+ padding: "8px",
+ borderRadius: "4px",
+ transition: "background-color 0.2s ease, box-shadow 0.2s ease",
+ ":hover": {
+ backgroundColor: DarkTheme.background.tertiary,
+ boxShadow: DarkTheme.shadows.small,
+ },
+ },
+ clickableFieldDisabled: {
+ padding: "8px",
+ borderRadius: "4px",
+ },
+ statusDropdownApproved: {
+ borderRadius: "4px",
+ fontWeight: "bold !important",
+ textAlign: "center",
+ transition: "all 0.2s ease",
+ "& .fui-Dropdown__button": {
+ fontWeight: "bold !important",
+ textAlign: "center",
+ justifyContent: "center",
+ display: "flex",
+ alignItems: "center",
+ transition: "all 0.2s ease",
+ },
+ "& .fui-Dropdown__expandIcon": {
+ display: "none",
+ },
+ ":hover": {
+ boxShadow: DarkTheme.shadows.small,
+ },
+ ":focus": {
+ outline: "none !important",
+ boxShadow: "none !important",
+ },
+ "& .fui-Dropdown__button:focus": {
+ outline: "none !important",
+ boxShadow: "none !important",
+ },
+ },
+ statusDropdownInReview: {
+ borderRadius: "4px",
+ fontWeight: "bold !important",
+ textAlign: "center",
+ transition: "all 0.2s ease",
+ "& .fui-Dropdown__button": {
+ fontWeight: "bold !important",
+ textAlign: "center",
+ justifyContent: "center",
+ display: "flex",
+ alignItems: "center",
+ transition: "all 0.2s ease",
+ },
+ "& .fui-Dropdown__expandIcon": {
+ display: "none",
+ },
+ ":hover": {
+ boxShadow: DarkTheme.shadows.small,
+ },
+ ":focus": {
+ outline: "none !important",
+ boxShadow: "none !important",
+ },
+ "& .fui-Dropdown__button:focus": {
+ outline: "none !important",
+ boxShadow: "none !important",
+ },
+ },
+ statusDropdownObsolete: {
+ borderRadius: "4px",
+ fontWeight: "bold !important",
+ textAlign: "center",
+ transition: "all 0.2s ease",
+ "& .fui-Dropdown__button": {
+ fontWeight: "bold !important",
+ textAlign: "center",
+ justifyContent: "center",
+ display: "flex",
+ alignItems: "center",
+ transition: "all 0.2s ease",
+ },
+ "& .fui-Dropdown__expandIcon": {
+ display: "none",
+ },
+ ":hover": {
+ boxShadow: DarkTheme.shadows.small,
+ },
+ ":focus": {
+ outline: "none !important",
+ boxShadow: "none !important",
+ },
+ "& .fui-Dropdown__button:focus": {
+ outline: "none !important",
+ boxShadow: "none !important",
+ },
+ },
+ productDropdown: {
+ color: `${DarkTheme.text.primary} !important`,
+ backgroundColor: `${DarkTheme.background.secondary} !important`,
+ border: `1px solid ${DarkTheme.border.primary} !important`,
+ borderRadius: "4px",
+ transition: "all 0.2s ease",
+ "& .fui-Dropdown__button": {
+ color: `${DarkTheme.text.primary} !important`,
+ backgroundColor: `${DarkTheme.background.secondary} !important`,
+ border: `1px solid ${DarkTheme.border.primary} !important`,
+ borderRadius: "4px",
+ display: "flex",
+ alignItems: "center",
+ gap: "8px",
+ justifyContent: "flex-start",
+ transition: "all 0.2s ease",
+ },
+ "& .fui-Dropdown__listbox": {
+ backgroundColor: `${DarkTheme.background.secondary} !important`,
+ border: `1px solid ${DarkTheme.border.primary} !important`,
+ borderRadius: "4px",
+ boxShadow: DarkTheme.shadows.medium,
+ },
+ // Global dropdown popup styling
+ "& [role='listbox']": {
+ backgroundColor: `${DarkTheme.background.secondary} !important`,
+ border: `1px solid ${DarkTheme.border.primary} !important`,
+ borderRadius: "4px",
+ boxShadow: DarkTheme.shadows.medium,
+ },
+ "& .fui-Option": {
+ color: `${DarkTheme.text.primary} !important`,
+ backgroundColor: `${DarkTheme.background.secondary} !important`,
+ "&:hover": {
+ backgroundColor: `${DarkTheme.background.tertiary} !important`,
+ color: `${DarkTheme.text.primary} !important`,
+ },
+ "&[aria-selected='true']": {
+ backgroundColor: `${DarkTheme.interactive.primary} !important`,
+ color: `${DarkTheme.text.primary} !important`,
+ },
+ },
+ ":hover": {
+ border: `1px solid ${DarkTheme.border.secondary} !important`,
+ backgroundColor: `${DarkTheme.background.tertiary} !important`,
+ "& .fui-Dropdown__button": {
+ backgroundColor: `${DarkTheme.background.tertiary} !important`,
+ border: `1px solid ${DarkTheme.border.secondary} !important`,
+ },
+ },
+ ":focus": {
+ outline: "none !important",
+ border: `2px solid ${DarkTheme.border.focus} !important`,
+ boxShadow: "none !important",
+ },
+ "& .fui-Dropdown__button:focus": {
+ outline: "none !important",
+ boxShadow: "none !important",
+ },
+ },
+ productAreaLink: {
+ transition: "all 0.2s ease",
+ borderRadius: "4px",
+ padding: "4px",
+ "&:hover": {
+ backgroundColor: DarkTheme.background.tertiary,
+ transform: "translateY(-1px)",
+ },
+ },
+ addButton: {
+ "& svg": {
+ color: `${DarkTheme.text.primary} !important`,
+ },
+ },
+});
+
+type SidebarProps = {
+ controller: WordPluginController;
+};
+
+const Sidebar = ({ controller }: SidebarProps) => {
+ const styles = useStyles();
+
+ // Status CSS class mapping
+ const getStatusDropdownClass = (status: string) => {
+ switch (status) {
+ case "Approved":
+ return styles.statusDropdownApproved;
+ case "In-Review":
+ return styles.statusDropdownInReview;
+ case "Obsolete":
+ return styles.statusDropdownObsolete;
+ default:
+ return "";
+ }
+ };
+
+ // Status color mapping
+ const getStatusColors = (status: string, isDraft: boolean = false) => {
+ if (isDraft) {
+ return {
+ textColor: theme.status.draft.text,
+ backgroundColor: theme.status.draft.background,
+ };
+ }
+
+ switch (status) {
+ case "Approved":
+ return {
+ textColor: theme.status.approved.text,
+ backgroundColor: theme.status.approved.background,
+ };
+ case "In-Review":
+ return {
+ textColor: theme.status.inReview.text,
+ backgroundColor: theme.status.inReview.background,
+ };
+ case "Obsolete":
+ return {
+ textColor: theme.status.obsolete.text,
+ backgroundColor: theme.status.obsolete.background,
+ };
+ default:
+ return {
+ textColor: theme.text.primary,
+ backgroundColor: theme.background.secondary,
+ };
+ }
+ };
+
+ const [loading, setLoading] = React.useState(true);
+ const [peopleMap, setPeopleMap] = React.useState(new Map());
+ const [groupsMap, setGroupsMap] = React.useState(new Map());
+ const [docMeta, setDocMeta] = React.useState(controller.documentMetadata);
+ const [switchToProductDropdown, setSwitchToProductDropDown] = React.useState(false);
+ const [pendingProductSelection, setPendingProductSelection] = React.useState(null);
+ const [productError, setProductError] = React.useState(null);
+ const [isSavingProduct, setIsSavingProduct] = React.useState(false);
+ const [statusError, setStatusError] = React.useState(null);
+ const [titleError, setTitleError] = React.useState(null);
+ const [summaryError, setSummaryError] = React.useState(null);
+ const [customFieldErrors, setCustomFieldErrors] = React.useState>({});
+ const [disablePublish, setDisablePublish] = React.useState(false);
+ const [disableDelete, setDisableDelete] = React.useState(false);
+ const [productSearchValue, setProductSearchValue] = React.useState("");
+ const [filteredProducts, setFilteredProducts] = React.useState([]);
+ const [isGroupApprover, setIsGroupApprover] = React.useState(false);
+
+ // Transfer ownership states
+ const [showTransferOwnership, setShowTransferOwnership] = React.useState(false);
+ const [selectedNewOwner, setSelectedNewOwner] = React.useState(null);
+ const [isTransferring, setIsTransferring] = React.useState(false);
+ const [transferError, setTransferError] = React.useState(null);
+
+ // Archive states
+ const [isArchiving, setIsArchiving] = React.useState(false);
+ const [archiveError, setArchiveError] = React.useState(null);
+ const [isArchiveHovered, setIsArchiveHovered] = React.useState(false);
+
+ // Leave approver role states
+ const [isLeavingApproverRole, setIsLeavingApproverRole] = React.useState(false);
+ const [leaveApproverError, setLeaveApproverError] = React.useState(null);
+
+ // Share success state
+ const [shareSuccess, setShareSuccess] = React.useState(false);
+ const [shareHovered, setShareHovered] = React.useState(false);
+ const [showLeaveApproverConfirmation, setShowLeaveApproverConfirmation] = React.useState(false);
+
+ // Acquire ownership states
+ const [showAcquireOwnershipConfirmation, setShowAcquireOwnershipConfirmation] = React.useState(false);
+ const [isAcquiringOwnership, setIsAcquiringOwnership] = React.useState(false);
+ const [acquireOwnershipError, setAcquireOwnershipError] = React.useState(null);
+
+ // Related Resources states
+ const [relatedResources, setRelatedResources] = React.useState({
+ externalLinks: [],
+ hermesDocuments: []
+ });
+ const [relatedResourcesLoading, setRelatedResourcesLoading] = React.useState(false);
+ const [relatedResourcesError, setRelatedResourcesError] = React.useState(null);
+
+ // Projects states
+ const [projects, setProjects] = React.useState([]);
+ const [projectsLoading, setProjectsLoading] = React.useState(false);
+ const [projectsError, setProjectsError] = React.useState(null);
+ const [showAddProjectForm, setShowAddProjectForm] = React.useState(false);
+
+ // Check if current user is the owner to control edit permissions
+ const isCurrentUserOwner = controller.isCurrentUserIsOwner();
+
+ const { isDark } = useTheme();
+ const theme = isDark ? DarkTheme : LightTheme;
+
+ // Add global styles for dropdown portal (Fluent UI dropdowns render in portals)
+ React.useEffect(() => {
+ const style = document.createElement('style');
+ style.textContent = `
+ /* Target all possible dropdown containers */
+ .fui-Portal .fui-Dropdown__listbox,
+ [role="listbox"],
+ .fui-Listbox {
+ background-color: ${theme.background.secondary} !important;
+ border: 1px solid ${theme.border.primary} !important;
+ border-radius: 4px !important;
+ box-shadow: ${theme.shadows.medium} !important;
+ }
+
+ /* Target all possible option elements */
+ .fui-Portal .fui-Option,
+ [role="option"],
+ .fui-Option {
+ color: ${theme.text.primary} !important;
+ background-color: ${theme.background.secondary} !important;
+ }
+
+ /* Hover states */
+ .fui-Portal .fui-Option:hover,
+ [role="option"]:hover,
+ .fui-Option:hover {
+ background-color: ${theme.background.tertiary} !important;
+ color: ${theme.text.primary} !important;
+ }
+
+ /* Selected states */
+ .fui-Portal .fui-Option[aria-selected="true"],
+ [role="option"][aria-selected="true"],
+ .fui-Option[aria-selected="true"] {
+ background-color: ${theme.interactive.primary} !important;
+ color: ${theme.text.inverse} !important;
+ }
+
+ /* Additional specificity for stubborn elements */
+ div[role="listbox"] > div[role="option"] {
+ background-color: ${theme.background.secondary} !important;
+ color: ${theme.text.primary} !important;
+ }
+
+ div[role="listbox"] > div[role="option"]:hover {
+ background-color: ${theme.background.tertiary} !important;
+ color: ${theme.text.primary} !important;
+ }
+
+ /* Target product dropdown specifically with maximum specificity */
+ #product-area-dropdown + [role="listbox"],
+ #product-area-dropdown ~ [role="listbox"] {
+ background-color: ${theme.background.secondary} !important;
+ border: 1px solid ${theme.border.primary} !important;
+ }
+
+ #product-area-dropdown + [role="listbox"] [role="option"],
+ #product-area-dropdown ~ [role="listbox"] [role="option"] {
+ background-color: ${theme.background.secondary} !important;
+ color: ${theme.text.primary} !important;
+ }
+
+ /* ComboBox input field styling */
+ .fui-Combobox input,
+ #product-area-dropdown input,
+ [class*="Combobox"] input {
+ background-color: ${theme.background.secondary} !important;
+ color: ${theme.text.primary} !important;
+ border: 1px solid ${theme.border.primary} !important;
+ }
+
+ /* ComboBox input placeholder styling */
+ .fui-Combobox input::placeholder,
+ #product-area-dropdown input::placeholder,
+ [class*="Combobox"] input::placeholder {
+ color: ${theme.text.secondary} !important;
+ }
+
+ /* ComboBox container styling */
+ .fui-Combobox,
+ #product-area-dropdown,
+ [class*="Combobox"] {
+ background-color: ${theme.background.secondary} !important;
+ color: ${theme.text.primary} !important;
+ }
+
+ /* Nuclear option - target everything with highest specificity */
+ * {
+ --fui-colorNeutralBackground1: ${theme.background.secondary} !important;
+ --fui-colorNeutralForeground1: ${theme.text.primary} !important;
+ }
+
+ [data-testid*="dropdown"] [role="option"],
+ [class*="dropdown"] [role="option"],
+ [class*="Dropdown"] [role="option"] {
+ background-color: ${theme.background.secondary} !important;
+ color: ${theme.text.primary} !important;
+ }
+
+ /* Fix ComboBox dropdown positioning issues */
+ .fui-Combobox__listbox,
+ #product-area-dropdown + [role="listbox"],
+ [class*="Combobox"] [role="listbox"] {
+ max-height: 200px !important;
+ overflow-y: auto !important;
+ z-index: 9999 !important;
+ }
+
+ /* Prevent ResizeObserver issues by constraining the dropdown */
+ .fui-Portal {
+ contain: layout style !important;
+ }
+
+ /* Ensure dropdown doesn't cause layout shifts */
+ .fui-Combobox {
+ position: relative !important;
+ }
+
+ /* Limit dropdown width and height */
+ #product-area-dropdown + [role="listbox"] {
+ max-width: 300px !important;
+ max-height: 200px !important;
+ overflow: auto !important;
+ }
+
+ /* TagPicker dropdown - show all results with scrolling */
+
+ /* Reasonable height limit with scrolling for many results */
+ .fui-TagPickerList,
+ [class*="fui-TagPickerList"],
+ div[data-people-dropdown] [role="listbox"] {
+ max-height: 200px !important;
+ overflow-y: auto !important;
+ z-index: 10000 !important;
+ background-color: ${theme.background.elevated} !important;
+ border: 1px solid ${theme.border.primary} !important;
+ box-shadow: ${theme.shadows.medium} !important;
+ }
+
+ /* Only fix colors - let Fluent UI handle layout naturally */
+ .fui-TagPickerList [role="option"],
+ div[data-people-dropdown] [role="option"] {
+ background-color: ${theme.background.elevated} !important;
+ color: ${theme.text.primary} !important;
+ }
+
+ .fui-TagPickerList [role="option"]:hover,
+ div[data-people-dropdown] [role="option"]:hover {
+ background-color: ${theme.background.tertiary} !important;
+ }
+
+ /* Input theming only */
+ .fui-TagPickerInput input {
+ background-color: ${theme.background.secondary} !important;
+ color: ${theme.text.primary} !important;
+ }
+
+ /* Control theming only */
+ .fui-TagPickerControl {
+ background-color: ${theme.background.secondary} !important;
+ border: 1px solid ${theme.border.primary} !important;
+ }
+
+ /* ── Override makeStyles baked-in dark theme colors ── */
+
+ /* Status dropdown - override baked DarkTheme colors for .fui-Dropdown__button */
+ .fui-Dropdown .fui-Dropdown__button {
+ color: inherit !important;
+ background-color: inherit !important;
+ }
+
+ /* Product dropdown override */
+ .fui-Combobox,
+ .fui-Combobox input {
+ background-color: ${theme.background.secondary} !important;
+ color: ${theme.text.primary} !important;
+ border-color: ${theme.border.primary} !important;
+ }
+
+ /* Section labels */
+ .fui-Text {
+ color: ${theme.text.primary};
+ }
+
+ /* Project items */
+ [class*="projectItem"] {
+ background-color: ${theme.background.secondary} !important;
+ border-color: ${theme.border.primary} !important;
+ color: ${theme.text.primary} !important;
+ }
+ [class*="projectItem"]:hover {
+ background-color: ${theme.background.tertiary} !important;
+ }
+ [class*="projectTitle"] {
+ color: ${theme.text.primary} !important;
+ }
+ [class*="projectDescription"] {
+ color: ${theme.text.secondary} !important;
+ }
+
+ /* Add project section */
+ [class*="addProjectSection"] {
+ background-color: ${theme.background.secondary} !important;
+ border-color: ${theme.border.primary} !important;
+ }
+ [class*="searchResultItem"] {
+ background-color: ${theme.background.secondary} !important;
+ border-color: ${theme.border.primary} !important;
+ }
+ [class*="searchResultItem"]:hover {
+ background-color: ${theme.background.tertiary} !important;
+ }
+
+ /* Edit resource form container */
+ [class*="container"] > .fui-Text {
+ color: ${theme.text.primary} !important;
+ }
+
+ /* Empty states */
+ [class*="emptyState"] {
+ color: ${theme.text.tertiary} !important;
+ }
+ `;
+ document.head.appendChild(style);
+ return () => {
+ if (document.head.contains(style)) {
+ document.head.removeChild(style);
+ }
+ };
+ }, [isDark]);
+
+ // Helper function to handle errors consistently across all fields
+ const handleMetadataUpdateError = (err: any, fieldName: string): string => {
+ console.error(`${fieldName} - Failed to save changes:`, err);
+
+ // Extract HTTP status code from error
+ let statusCode = "";
+
+ if (err.response) {
+ // Axios-style HTTP error response
+ statusCode = `${err.response.status}`;
+ console.log(`${fieldName} - Found axios-style error with status:`, statusCode);
+ } else if (err.message) {
+ // Parse HermesClient error format for status code
+ const statusMatch = err.message.match(/status:\s*(\d+)/);
+ if (statusMatch) {
+ statusCode = statusMatch[1];
+ console.log(`${fieldName} - Found HermesClient error with status:`, statusCode);
+ }
+ }
+
+ let fullError = "Server Error";
+ if (statusCode === "403") {
+ fullError = "Permission Denied, Only Owners can update the metadata.";
+ } else if (statusCode) {
+ fullError = `Server Responded : ${statusCode}`;
+ }
+
+ console.log(`${fieldName} - Setting error:`, fullError);
+ return fullError;
+ };
+
+ async function getPeople() {
+ const peopleSet = new Set([
+ ...docMeta.owners,
+ ...(docMeta.approvers || []),
+ ...(docMeta.contributors || []),
+ ]);
+
+ // Add emails from custom people fields
+ Object.keys(docMeta.customEditableFields || {}).forEach((customKey) => {
+ const customField = docMeta.customEditableFields[customKey];
+ if (customField.type === "PEOPLE") {
+ const customPeopleEmails = docMeta[customKey] || [];
+ customPeopleEmails.forEach((email: string) => peopleSet.add(email));
+ }
+ });
+
+ const emails = Array.from(peopleSet);
+ setPeopleMap(await controller.getEmailToPersonMap(emails));
+ }
+
+ async function getGroups() {
+ const approverGroups = docMeta.approverGroups || [];
+ if (!approverGroups.length) {
+ setGroupsMap(new Map());
+ return;
+ }
+
+ const map = await controller.getEmailToGroupMap(approverGroups);
+ setGroupsMap(new Map(map));
+ }
+
+ async function checkGroupApprover() {
+ try {
+ const canApprove = await controller.isCurrentGroupApprover();
+ setIsGroupApprover(canApprove);
+ } catch (error) {
+ setIsGroupApprover(false);
+ }
+ }
+
+ const renderCustomFields = () => {
+ if (!docMeta.customEditableFields) return null;
+
+ return Object.keys(docMeta.customEditableFields).map((customKey, index) => {
+ const customField = docMeta.customEditableFields[customKey];
+ const key = customField.displayName;
+ const value = docMeta[customKey];
+
+ return (
+
+ {customField.type === "PEOPLE" ? (
+
{
+ customField.values = emails;
+ await controller.updateMetadata(
+ {
+ customFields: [
+ {
+ name: customKey,
+ displayName: key,
+ type: customField.type,
+ value: emails,
+ },
+ ],
+ },
+ setDocMeta
+ );
+ }}
+ createUrl={(p) => controller.createHermesUrlFromPerson(p)}
+ search={search}
+ mutatePeopleMap={(map) => setPeopleMap(new Map(map))}
+ isMe={(email) => controller.isMe(email)}
+ addGreenTick={() => false} // Custom people fields never get green ticks
+ disable={!isCurrentUserOwner}
+ />
+ ) : (
+ <>
+ {
+ if (!isCurrentUserOwner) return; // Prevent action if not owner
+ setCustomFieldErrors(prev => ({ ...prev, [customKey]: null })); // Clear previous errors
+ try {
+ await controller.updateMetadata(
+ {
+ customFields: [
+ {
+ name: customKey,
+ displayName: key,
+ type: customField.type,
+ value: text,
+ },
+ ],
+ },
+ setDocMeta
+ );
+ } catch (err: any) {
+ const errorMessage = handleMetadataUpdateError(err, `Custom Field: ${key}`);
+ setCustomFieldErrors(prev => ({ ...prev, [customKey]: errorMessage }));
+ }
+ }}
+ />
+
+ {/* Error display for Custom field */}
+ {customFieldErrors[customKey] && (
+
+ Error: {customFieldErrors[customKey]}
+
+ )}
+ >
+ )}
+
+ );
+ });
+ };
+
+ const updateProductSelection = async (selectedProduct: string) => {
+ if (!selectedProduct) return;
+
+ setIsSavingProduct(true);
+ setProductError(null); // Clear previous errors
+ try {
+ await controller.updateMetadata(
+ {
+ product: selectedProduct,
+ },
+ setDocMeta
+ );
+ setPendingProductSelection(null);
+ setSwitchToProductDropDown(false);
+ setProductSearchValue(""); // Clear search
+ } catch (err: any) {
+ const errorMessage = handleMetadataUpdateError(err, "Product/Area");
+ setProductError(errorMessage);
+
+ // Keep the dropdown open to show error
+ console.log("Product/Area field should stay open to show error");
+ } finally {
+ setIsSavingProduct(false);
+ }
+ };
+
+ const renderProductDropdown = () => {
+ return (
+ {
+ // Don't select the "No results" option
+ if (data.optionValue && data.optionValue !== "") {
+ // Directly update the product selection
+ updateProductSelection(data.optionValue);
+ }
+ }}
+ onInput={(e) => {
+ setProductSearchValue((e.target as HTMLInputElement).value);
+ }}
+ placeholder={isSavingProduct ? "Saving..." : "Search for a product or area..."}
+ disabled={isSavingProduct}
+ style={{
+ backgroundColor: theme.background.secondary,
+ color: theme.text.primary,
+ border: `1px solid ${theme.border.primary}`,
+ opacity: isSavingProduct ? 0.7 : 1,
+ }}
+ >
+ {filteredProducts.length > 0 ? (
+ filteredProducts.map((key, index) => {
+ const productData = controller.products[key];
+ return (
+
+
+ {key}
+
+ );
+ })
+ ) : (
+ productSearchValue.trim() ? (
+
+
+ No matching products found
+
+
+ ) : null
+ )}
+
+ );
+ };
+
+ const search = async (query: string) => {
+ return await controller.searchPeople(query);
+ };
+
+ const searchGroups = async (query: string) => {
+ return await controller.searchGroups(query);
+ };
+
+ // Related Resources methods
+ const loadRelatedResources = async () => {
+ setRelatedResourcesLoading(true);
+ setRelatedResourcesError(null);
+
+ try {
+ const resources = await controller.loadRelatedResources();
+ setRelatedResources(resources);
+ } catch (error) {
+ console.error("Failed to load related resources:", error);
+ const errorMessage = error instanceof Error ? error.message : "Failed to load related resources";
+ setRelatedResourcesError(errorMessage);
+ // Set empty resources to prevent crashes
+ setRelatedResources({ externalLinks: [], hermesDocuments: [] });
+ } finally {
+ setRelatedResourcesLoading(false);
+ }
+ };
+
+ const handleAddRelatedResource = async (resource: RelatedResource) => {
+ try {
+ await controller.addRelatedResource(resource);
+ setRelatedResources(controller.getCachedRelatedResources());
+ } catch (error) {
+ console.error("Failed to add related resource:", error);
+ throw error; // Re-throw to let the component handle the error display
+ }
+ };
+
+ const handleEditRelatedResource = async (resource: RelatedExternalLink) => {
+ try {
+ await controller.updateRelatedResource(resource);
+ setRelatedResources(controller.getCachedRelatedResources());
+ } catch (error) {
+ console.error("Failed to edit related resource:", error);
+ throw error; // Re-throw to let the component handle the error display
+ }
+ };
+
+ const handleRemoveRelatedResource = async (resource: RelatedResource) => {
+ try {
+ await controller.removeRelatedResource(resource);
+ setRelatedResources(controller.getCachedRelatedResources());
+ } catch (error) {
+ console.error("Failed to remove related resource:", error);
+ throw error; // Re-throw to let the component handle the error display
+ }
+ };
+
+ // Projects methods
+ const loadProjects = async () => {
+ setProjectsLoading(true);
+ setProjectsError(null);
+
+ try {
+ const projectsList = await controller.loadDocumentProjects();
+ setProjects(projectsList);
+ } catch (error) {
+ console.error("Failed to load projects:", error);
+ const errorMessage = error instanceof Error ? error.message : "Failed to load projects";
+ setProjectsError(errorMessage);
+ setProjects([]); // Set empty array to prevent crashes
+ } finally {
+ setProjectsLoading(false);
+ }
+ };
+
+ const handleAddProject = async (project: HermesProject) => {
+ try {
+ await controller.addDocumentToProject(project.id);
+ // Reload projects to get the updated list
+ await loadProjects();
+ } catch (error) {
+ console.error("Failed to add project:", error);
+ throw error; // Re-throw to let the component handle the error display
+ }
+ };
+
+ const handleRemoveProject = async (projectId: string) => {
+ try {
+ await controller.removeDocumentFromProject(projectId);
+ // Reload projects to get the updated list
+ await loadProjects();
+ } catch (error) {
+ console.error("Failed to remove project:", error);
+ throw error; // Re-throw to let the component handle the error display
+ }
+ };
+
+ React.useEffect(() => {
+ const fn = async () => {
+ try {
+ // Update docMeta from controller in case it changed
+ setDocMeta(controller.documentMetadata);
+ await getPeople();
+ await getGroups();
+ await checkGroupApprover();
+ await controller.renderTable();
+
+ // Load related resources
+ await loadRelatedResources();
+
+ // Load projects
+ await loadProjects();
+ } catch (error) {
+ console.log("error", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ (async () => {
+ await fn();
+ })();
+ }, []);
+
+ // Effect to sync docMeta with controller when it changes
+ React.useEffect(() => {
+ setDocMeta(controller.documentMetadata);
+ }, [controller.documentMetadata]);
+
+ // Separate effect to update people when docMeta changes
+ React.useEffect(() => {
+ if (loading) {
+ return;
+ }
+
+ (async () => {
+ try {
+ await getPeople();
+ await getGroups();
+ await checkGroupApprover();
+ } catch (error) {
+ }
+ })();
+ }, [
+ docMeta.owners,
+ docMeta.approvers,
+ docMeta.approverGroups,
+ docMeta.contributors,
+ // Watch for changes in custom people fields
+ ...Object.keys(docMeta.customEditableFields || {})
+ .filter(key => docMeta.customEditableFields[key].type === "PEOPLE")
+ .map(key => docMeta[key])
+ ]);
+
+ // Initialize filtered products on first load
+ React.useEffect(() => {
+ const productNames = Object.keys(controller.products);
+ setFilteredProducts(productNames);
+ }, []);
+
+ // Update filtered products when search value or products change
+ React.useEffect(() => {
+ const productNames = Object.keys(controller.products);
+ if (!productSearchValue.trim()) {
+ setFilteredProducts(productNames);
+ } else {
+ const filtered = productNames.filter(productName =>
+ productName.toLowerCase().includes(productSearchValue.toLowerCase())
+ );
+ setFilteredProducts(filtered);
+ }
+ }, [productSearchValue, controller.products]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+ {/* Fixed Header Navigation */}
+
+ {/* Hermes Link - Aligned with content below */}
+
+ Hermes
+
+
+ {/* Share Button - Only show for published documents */}
+ {!docMeta._isDraft && (
+
+
+ setShareHovered(true)}
+ onMouseLeave={() => setShareHovered(false)}
+ onClick={() => {
+ const shortLink = `${controller.getHermesBaseUrl()}/l/${docMeta.docType.toLowerCase()}/${docMeta.docNumber.toLowerCase()}`;
+
+ // Try multiple methods to copy to clipboard
+ const copyToClipboard = async (text: string) => {
+ try {
+ // Method 1: Try modern clipboard API first
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ await navigator.clipboard.writeText(text);
+ console.log("Short link copied to clipboard (modern API):", text);
+ return true;
+ }
+ } catch (err) {
+ console.warn("Modern clipboard API failed:", err);
+ }
+
+ try {
+ // Method 2: Fallback to execCommand (deprecated but more compatible)
+ const textArea = document.createElement('textarea');
+ textArea.value = text;
+ textArea.style.position = 'fixed';
+ textArea.style.left = '-999999px';
+ textArea.style.top = '-999999px';
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+
+ const successful = document.execCommand('copy');
+ document.body.removeChild(textArea);
+
+ if (successful) {
+ console.log("Short link copied to clipboard (execCommand):", text);
+ return true;
+ }
+ } catch (err) {
+ console.warn("execCommand clipboard method failed:", err);
+ }
+
+ // Method 3: Last resort - show alert with link to copy manually
+ alert(`Copy this link manually:\n\n${text}`);
+ console.log("Clipboard copy failed, showed manual copy alert:", text);
+ return false;
+ };
+
+ copyToClipboard(shortLink).then((success) => {
+ if (success !== false) {
+ setShareSuccess(true);
+ // Reset success state after 2 seconds
+ setTimeout(() => setShareSuccess(false), 2000);
+ }
+ });
+ }}
+ >
+ {shareSuccess ? (
+ <>
+
+ Copied!
+ >
+ ) : (
+ <>
+
+ Share
+ >
+ )}
+
+
+
+ )}
+ {/* Theme Toggle Button - always visible */}
+
+
+
+ {/* Scrollable Content */}
+
+ {/* Status */}
+ {docMeta._isDraft ? (
+
+ {docMeta.archived ? "Archived" : docMeta.status}
+
+ ) : (
+
{
+ if (!isCurrentUserOwner) return; // Prevent action if not owner
+ setStatusError(null); // Clear previous errors
+ try {
+ await controller.updateMetadata(
+ {
+ status: data.optionValue,
+ },
+ setDocMeta
+ );
+ } catch (err: any) {
+ const errorMessage = handleMetadataUpdateError(err, "Status");
+ setStatusError(errorMessage);
+ }
+ }}
+ value={docMeta.status}
+ style={{
+ color: getStatusColors(docMeta.status).textColor,
+ backgroundColor: getStatusColors(docMeta.status).backgroundColor,
+ border: `1px solid ${
+ docMeta.status === "Approved" ? theme.status.approved.border :
+ docMeta.status === "In-Review" ? theme.status.inReview.border :
+ theme.status.obsolete.border
+ }`,
+ }}
+ >
+
+ {"In-Review"}
+
+
+ {"Approved"}
+
+
+ {"Obsolete"}
+
+
+ )}
+
+ {/* Error display for Status field */}
+ {statusError && (
+
+ Error: {statusError}
+
+ )}
+
+ {/* Title */}
+
+
+ {
+ if (!isCurrentUserOwner) return; // Prevent action if not owner
+ setTitleError(null); // Clear previous errors
+ try {
+ await controller.updateMetadata(
+ {
+ title: text,
+ },
+ setDocMeta
+ );
+ } catch (err: any) {
+ const errorMessage = handleMetadataUpdateError(err, "Title");
+ setTitleError(errorMessage);
+ }
+ }}
+ />
+
+ {/* Error display for Title field */}
+ {titleError && (
+
+ Error: {titleError}
+
+ )}
+
+
+ {docMeta.docNumber}
+
+
+
+ {/* Tag */}
+
+
+ {docMeta.docType}
+
+
{/* Summary */}
+
+ {
+ if (!isCurrentUserOwner) return; // Prevent action if not owner
+ setSummaryError(null); // Clear previous errors
+ try {
+ await controller.updateMetadata(
+ {
+ summary: text,
+ },
+ setDocMeta
+ );
+ } catch (err: any) {
+ const errorMessage = handleMetadataUpdateError(err, "Summary");
+ setSummaryError(errorMessage);
+ }
+ }}
+ />
+
+ {/* Error display for Summary field */}
+ {summaryError && (
+
+ Error: {summaryError}
+
+ )}
+
+
+ {/* Product/Area */}
+
+
Product/Area
+ {switchToProductDropdown && controller.documentMetadata._isDraft && isCurrentUserOwner ? (
+
+ {renderProductDropdown()}
+ }
+ onClick={() => {
+ setPendingProductSelection(null);
+ setProductError(null); // Clear any error messages
+ setSwitchToProductDropDown(false);
+ setProductSearchValue(""); // Clear search
+ }}
+ disabled={isSavingProduct}
+ title="Cancel"
+ />
+
+ ) : (
+
+
{
+ const productAreaUrl = controller.getProductAreaUrl(docMeta.product);
+ window.open(productAreaUrl, '_blank');
+ }}
+ onMouseEnter={(e) => {
+ (e.currentTarget as HTMLDivElement).style.backgroundColor = theme.background.tertiary;
+ }}
+ onMouseLeave={(e) => {
+ (e.currentTarget as HTMLDivElement).style.backgroundColor = "";
+ }}
+ style={{
+ cursor: "pointer",
+ display: "flex",
+ alignItems: "center",
+ gap: "8px",
+ flex: 1
+ }}
+ >
+
+
{docMeta.product}
+
+ {(controller.documentMetadata._isDraft && isCurrentUserOwner) && (
+
}
+ onClick={() => setSwitchToProductDropDown(true)}
+ style={{ marginLeft: "8px", color: theme.text.primary }}
+ title="Edit Product/Area"
+ />
+ )}
+
+ )}
+
+ {/* Error display for Product/Area field */}
+ {productError && (
+
+ Error: {productError}
+
+ )}
+
+
+ {/* Created */}
+
+ Created
+ {docMeta.created}
+
+
+ {/* Last modified */}
+
+ Last modified
+ {timeAgo(docMeta.modifiedTime * 1000)}
+
+
+ {/* Owner */}
+
+
Owner
+
+
+
{
+ const documentsUrl = controller.getDocumentsByOwnerUrl(docMeta.owners[0]);
+ window.open(documentsUrl, '_blank');
+ }}
+ onMouseEnter={(e) => {
+ (e.currentTarget as HTMLDivElement).style.backgroundColor = theme.background.tertiary;
+ }}
+ onMouseLeave={(e) => {
+ (e.currentTarget as HTMLDivElement).style.backgroundColor = "";
+ }}
+ style={{
+ cursor: "pointer",
+ flex: 1
+ }}
+ >
+
+ {controller.isMe(docMeta.owners[0])
+ ? "Me"
+ : peopleMap.get(docMeta.owners[0])?.names?.[0].displayName || docMeta.owners[0]}
+
+
+
+
+
+ {/* Contributors */}
+
{
+ await controller.updateMetadata({ contributors }, setDocMeta);
+ }}
+ search={search}
+ createUrl={(person) => controller.createHermesUrlFromPerson(person)}
+ mutatePeopleMap={(map) => setPeopleMap(new Map(map))}
+ isMe={(email) => controller.isMe(email)}
+ addGreenTick={() => false} // Contributors never get green ticks
+ disable={!isCurrentUserOwner}
+ />
+
+ {/* Approvers */}
+ {
+ const groupEmails = emails.filter((email) => groupsMap.has(email));
+ const personEmails = emails.filter((email) => !groupsMap.has(email));
+
+ await controller.updateMetadata(
+ {
+ approvers: personEmails,
+ approverGroups: groupEmails,
+ },
+ setDocMeta
+ );
+ }}
+ searchPeople={search}
+ searchGroups={searchGroups}
+ createUrl={(p) => controller.createHermesUrlFromPerson(p)}
+ mutatePeopleMap={(map) => setPeopleMap(new Map(map))}
+ mutateGroupsMap={(map) => setGroupsMap(new Map(map))}
+ isMe={(email) => controller.isMe(email)}
+ addGreenTick={(email) => controller.documentMetadata.approvedBy?.includes(email) || false}
+ disable={!isCurrentUserOwner || controller.isApprovedByCurrentUser()}
+ />
+
+ {/* Projects */}
+
+
+ Projects
+ {controller.documentMetadata?._isDraft ? (
+
+
+
+ ) : (
+ isCurrentUserOwner && (
+
+ }
+ className={styles.addButton}
+ style={{ minWidth: "auto", padding: "2px", height: "20px" }}
+ onClick={() => setShowAddProjectForm(true)}
+ />
+
+ )
+ )}
+
+
{
+ await handleAddProject(project);
+ setShowAddProjectForm(false);
+ }}
+ onRemove={handleRemoveProject}
+ onRetry={loadProjects}
+ />
+
+
+ {/* Leave Approver Role - Only show to current approvers who haven't approved yet */}
+ {(() => {
+ const isApprover = controller.isCurrentApprover();
+ const hasApproved = controller.isApprovedByCurrentUser();
+ return isApprover && !hasApproved;
+ })() && (
+
+ {leaveApproverError && (
+
+ Error: {leaveApproverError}
+
+ )}
+
+ {!showLeaveApproverConfirmation ? (
+
{
+ console.log("Leave Approver Role clicked - showing confirmation");
+ setShowLeaveApproverConfirmation(true);
+ setLeaveApproverError(null);
+ }}
+ style={{
+ cursor: "pointer",
+ color: theme.text.disabled,
+ fontSize: "12px",
+ fontWeight: "500"
+ }}
+ >
+ Leave Approver Role
+
+ ) : (
+
+
+ Are you sure you want to leave the approver role?
+
+
+
+ {
+ console.log("Confirmed - proceeding with leave approver role...");
+
+ setIsLeavingApproverRole(true);
+ setLeaveApproverError(null);
+
+ try {
+ // Filter out current user's email from approvers
+ const currentApprovers = docMeta.approvers || [];
+ console.log("Current approvers:", currentApprovers);
+
+ const updatedApprovers = currentApprovers.filter(
+ (email) => !controller.isMe(email)
+ );
+
+ console.log("Updated approvers after filtering:", updatedApprovers);
+
+ // Update via API
+ console.log("Calling updateMetadata with:", { approvers: updatedApprovers });
+ await controller.updateMetadata(
+ { approvers: updatedApprovers },
+ setDocMeta
+ );
+
+ // Success - the UI will automatically update since the user is no longer an approver
+ console.log("Successfully left approver role");
+ setShowLeaveApproverConfirmation(false);
+
+ } catch (err: any) {
+ const errorMessage = handleMetadataUpdateError(err, "Leave Approver Role");
+ setLeaveApproverError(errorMessage);
+ } finally {
+ setIsLeavingApproverRole(false);
+ }
+ }}
+ style={{
+ backgroundColor: theme.text.disabled,
+ borderColor: theme.text.disabled,
+ color: theme.text.inverse
+ }}
+ >
+ {isLeavingApproverRole ? "Leaving..." : "Confirm"}
+
+
+ {
+ console.log("Cancelled leave approver role");
+ setShowLeaveApproverConfirmation(false);
+ setLeaveApproverError(null);
+ }}
+ >
+ Cancel
+
+
+
+ )}
+
+ )}
+
+ {/* Acquire Ownership - Only show for contributors who are not owners and for published documents */}
+ {!docMeta._isDraft &&
+ controller.isCurrentUserContributor() &&
+ !controller.isCurrentUserIsOwner() && (
+
+ {acquireOwnershipError && (
+
+ Error: {acquireOwnershipError}
+
+ )}
+
+ {!showAcquireOwnershipConfirmation ? (
+
{
+ setShowAcquireOwnershipConfirmation(true);
+ setAcquireOwnershipError(null);
+ }}
+ style={{
+ cursor: "pointer",
+ color: theme.text.disabled,
+ fontSize: "12px",
+ fontWeight: "500"
+ }}
+ >
+ Acquire Ownership
+
+ ) : (
+
+
+ If the current owner is not in the company directory, you will be granted ownership.
+
+
+
+ {
+ setIsAcquiringOwnership(true);
+ setAcquireOwnershipError(null);
+ try {
+ await controller.updateMetadata({
+ owners: [controller.getCurrentUserEmail()]
+ }, setDocMeta);
+ setShowAcquireOwnershipConfirmation(false);
+ } catch (error: any) {
+ console.error("Error acquiring ownership:", error);
+ setAcquireOwnershipError(error?.message || "Failed to acquire ownership");
+ } finally {
+ setIsAcquiringOwnership(false);
+ }
+ }}
+ >
+ {isAcquiringOwnership ? "Processing..." : "Confirm"}
+
+
+ {
+ setShowAcquireOwnershipConfirmation(false);
+ setAcquireOwnershipError(null);
+ }}
+ >
+ Cancel
+
+
+
+ )}
+
+ )}
+
+ {/* Related resources
+
+ Related resources
+ None
+
*/}
+
+ {/*Custom fields*/}
+ {renderCustomFields()}
+
+ {/* Related Resources */}
+
+
+ {/* Transfer Ownership - Only show to current owner */}
+ {isCurrentUserOwner && (
+
+
Transfer Ownership
+
+ {!showTransferOwnership ? (
+
setShowTransferOwnership(true)}
+ onMouseEnter={(e) => {
+ (e.currentTarget as HTMLDivElement).style.backgroundColor = theme.background.tertiary;
+ (e.currentTarget as HTMLDivElement).style.boxShadow = theme.shadows.small;
+ }}
+ onMouseLeave={(e) => {
+ (e.currentTarget as HTMLDivElement).style.backgroundColor = "";
+ (e.currentTarget as HTMLDivElement).style.boxShadow = "";
+ }}
+ style={{ cursor: "pointer" }}
+ >
+
+ Transfer to another person...
+
+
+ ) : (
+
+
{
+ if (emails.length > 0) {
+ // Find the person object for the selected email
+ const person = peopleMap.get(emails[0]);
+ if (person) {
+ setSelectedNewOwner(person);
+ } else {
+ // If person not in map, search for them
+ try {
+ const searchResults = await search(emails[0]);
+ if (searchResults.length > 0) {
+ setSelectedNewOwner(searchResults[0]);
+ setPeopleMap(new Map(peopleMap.set(emails[0], searchResults[0])));
+ }
+ } catch (err) {
+ console.error("Error searching for person:", err);
+ }
+ }
+ } else {
+ setSelectedNewOwner(null);
+ }
+ }}
+ search={search}
+ createUrl={(person) => controller.createHermesUrlFromPerson(person)}
+ mutatePeopleMap={(map) => setPeopleMap(new Map(map))}
+ isMe={(email) => controller.isMe(email)}
+ addGreenTick={() => false}
+ disable={false}
+ />
+
+ {transferError && (
+
+ Error: {transferError}
+
+ )}
+
+
+ {
+ if (!selectedNewOwner?.emailAddresses?.[0]?.value) {
+ setTransferError("Please select a valid person");
+ return;
+ }
+
+ setIsTransferring(true);
+ setTransferError(null);
+
+ try {
+ const newOwnerEmail = selectedNewOwner.emailAddresses[0].value;
+
+ // Call the transfer ownership API
+ await controller.updateMetadata(
+ { owners: [newOwnerEmail] },
+ setDocMeta
+ );
+
+ // Reset the transfer UI
+ setShowTransferOwnership(false);
+ setSelectedNewOwner(null);
+
+ // Show success message (you might want to add a toast/notification system)
+ console.log(`Ownership transferred to ${newOwnerEmail}`);
+
+ } catch (err: any) {
+ const errorMessage = handleMetadataUpdateError(err, "Transfer Ownership");
+ setTransferError(errorMessage);
+ } finally {
+ setIsTransferring(false);
+ }
+ }}
+ >
+ {isTransferring ? "Transferring..." : "Confirm Transfer"}
+
+ {
+ setShowTransferOwnership(false);
+ setSelectedNewOwner(null);
+ setTransferError(null);
+ }}
+ >
+ Cancel
+
+
+
+ {selectedNewOwner && (
+
+ Warning: You will no longer be able to edit this document metadata after transferring ownership to{" "}
+
+ {selectedNewOwner.names?.[0]?.displayName || selectedNewOwner.emailAddresses?.[0]?.value}
+ .
+
+ )}
+
+ )}
+
+ )}
+
+ {/* Archive/Unarchive Draft - Only show to current owner for drafts */}
+ {isCurrentUserOwner && docMeta._isDraft && (
+
+
{docMeta.archived ? "Unarchive Draft" : "Archive Draft"}
+
+ {archiveError && (
+
+ Error: {archiveError}
+
+ )}
+
+
:
}
+ style={{
+ color: theme.text.secondary,
+ border: `1px solid ${theme.border.primary}`,
+ backgroundColor: isArchiveHovered ? theme.background.tertiary : "transparent",
+ borderColor: isArchiveHovered ? theme.border.secondary : theme.border.primary,
+ }}
+ onMouseEnter={() => setIsArchiveHovered(true)}
+ onMouseLeave={() => setIsArchiveHovered(false)}
+ onClick={async () => {
+ setIsArchiving(true);
+ setArchiveError(null);
+
+ try {
+ const newArchivedStatus = !docMeta.archived;
+ await controller.setDraftArchivedStatus(newArchivedStatus, setDocMeta);
+
+ console.log(`Draft ${newArchivedStatus ? 'archived' : 'unarchived'} successfully`);
+ } catch (err: any) {
+ console.error("Error updating archive status:", err);
+ const errorMessage = err?.message || "Failed to update archive status";
+ setArchiveError(errorMessage);
+ } finally {
+ setIsArchiving(false);
+ }
+ }}
+ >
+ {isArchiving
+ ? (docMeta.archived ? "Unarchiving..." : "Archiving...")
+ : (docMeta.archived ? "Unarchive Draft" : "Archive Draft")
+ }
+
+
+
+ {docMeta.archived
+ ? "This draft is currently archived. Unarchive it to make it active again."
+ : "Archive this draft to move it out of your active drafts list."
+ }
+
+
+ )}
+
+ {docMeta._isDraft &&
+ !docMeta.archived &&
+ (controller.isCurrentUserIsOwner() ||
+ controller.isCurrentUserContributor() ||
+ controller.isCurrentApprover()) && (
+
+
+ {
+ setDisablePublish(true);
+
+ try {
+ await controller.publishForReview(setDocMeta);
+ } finally {
+ setDisablePublish(false);
+ }
+ }}
+ >
+ Publish for review...
+
+
+
+ )}
+
+ {!docMeta._isDraft &&
+ (controller.isCurrentApprover() || isGroupApprover) &&
+ !controller.isApprovedByCurrentUser() && (
+
+ {
+ setDisableDelete(true);
+ try {
+ await controller.approveDoc(setDocMeta);
+ } finally {
+ setDisableDelete(false);
+ }
+ }}
+ >
+ Approve
+
+
+ )}
+ {controller.isApprovedByCurrentUser() && (
+
+
+ Approved
+
+
+ )}
+
+ >
+ );
+};
+
+export default Sidebar;
diff --git a/hermes-plugin/src/taskpane/components/ThemeToggleButton.tsx b/hermes-plugin/src/taskpane/components/ThemeToggleButton.tsx
new file mode 100644
index 000000000..1270baf42
--- /dev/null
+++ b/hermes-plugin/src/taskpane/components/ThemeToggleButton.tsx
@@ -0,0 +1,54 @@
+import * as React from "react";
+import { Tooltip } from "@fluentui/react-components";
+import { WeatherSunny16Regular, WeatherMoon16Regular } from "@fluentui/react-icons";
+import { useTheme } from "../utils/themeContext";
+import DarkTheme from "../utils/darkTheme";
+import LightTheme from "../utils/lightTheme";
+
+const ThemeToggleButton: React.FC = () => {
+ const { isDark, toggleTheme } = useTheme();
+ const theme = isDark ? DarkTheme : LightTheme;
+
+ return (
+
+ {
+ // (e.currentTarget as HTMLButtonElement).style.backgroundColor = theme.background.tertiary;
+ // (e.currentTarget as HTMLButtonElement).style.color = theme.text.primary;
+ // }}
+ // onMouseLeave={(e) => {
+ // (e.currentTarget as HTMLButtonElement).style.backgroundColor = "transparent";
+ // (e.currentTarget as HTMLButtonElement).style.color = theme.text.secondary;
+ // }}
+ >
+ {isDark ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+export default ThemeToggleButton;
diff --git a/hermes-plugin/src/taskpane/index.tsx b/hermes-plugin/src/taskpane/index.tsx
new file mode 100644
index 000000000..e79fc89b8
--- /dev/null
+++ b/hermes-plugin/src/taskpane/index.tsx
@@ -0,0 +1,55 @@
+import * as React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./components/App";
+import { FluentProvider, webDarkTheme, webLightTheme } from "@fluentui/react-components";
+import { getHeaders, insertText } from "./taskpane";
+import HermesClient from "./utils/hermesClient";
+import Config from "../config.json";
+import WordService from "./utils/wordService";
+import WordPluginController from "./utils/wordPluginController";
+
+/* global document, Office, module, require, HTMLElement */
+
+const title = "Hermes";
+
+const rootElement: HTMLElement | null = document.getElementById("container");
+const root = rootElement ? createRoot(rootElement) : undefined;
+
+/* Render application after Office initializes */
+Office.onReady(async () => {
+ let baseUrl = window.location.origin; // Default fallback
+
+ // Check if useHostUrl flag is set to true
+ if (Config.useHostUrl === true) {
+ // Use current host dynamically
+ baseUrl = window.location.origin;
+ console.log('Using dynamic host URL:', baseUrl);
+ } else {
+ // Use static baseUrl from config.json
+ if (Config.baseUrl) {
+ baseUrl = Config.baseUrl;
+ console.log('Using static baseUrl from config.json:', baseUrl);
+ } else {
+ console.log('No baseUrl in config.json, using host URL:', baseUrl);
+ }
+ }
+
+ const hermesClient = new HermesClient(baseUrl);
+ const wordSvc = new WordService();
+
+ const controller = new WordPluginController(hermesClient, wordSvc);
+ OfficeExtension.config.extendedErrorLogging = true;
+ const theme = Office.context.officeTheme.isDarkTheme ? webDarkTheme : webLightTheme;
+ root?.render(
+
+
+
+ );
+});
+
+if ((module as any).hot) {
+ (module as any).hot.accept("./components/App", () => {
+ const NextApp = require("./components/App").default;
+ root?.render(NextApp);
+ });
+}
diff --git a/hermes-plugin/src/taskpane/interfaces/currentUser.ts b/hermes-plugin/src/taskpane/interfaces/currentUser.ts
new file mode 100644
index 000000000..7384bf4fd
--- /dev/null
+++ b/hermes-plugin/src/taskpane/interfaces/currentUser.ts
@@ -0,0 +1,6 @@
+export default interface CurrentUser {
+ id: string;
+ email: string;
+ name: string;
+ picture: string;
+}
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/interfaces/documentMetadata.ts b/hermes-plugin/src/taskpane/interfaces/documentMetadata.ts
new file mode 100644
index 000000000..63cb7ab40
--- /dev/null
+++ b/hermes-plugin/src/taskpane/interfaces/documentMetadata.ts
@@ -0,0 +1,74 @@
+import { RelatedResourcesResponse } from './relatedResources';
+
+export default interface IDocumentMetadata {
+ contributors?: string[];
+ approvers?: string[];
+ approverGroups?: string[];
+ created: string; // e.g. "Sep 15, 2025"
+ createdTime: number; // epoch timestamp (seconds)
+ customEditableFields: {
+ [key: string]: {
+ displayName: string;
+ type: "STRING" | "PEOPLE" | string; // can extend if more types exist
+ values?: string[]
+ };
+ };
+ docNumber: string;
+ docType: string;
+ modifiedTime: number; // epoch timestamp (seconds)
+ objectID: string;
+ owners: string[];
+ product: string;
+ status: string;
+ summary: string;
+ title: string;
+ projects?: number[] // Array of project IDs
+ projectDetails?: Array<{id: string, title: string}> // Project details for display
+ baseUrl?: string // Base URL for creating hyperlinks
+ stakeholders?: string;
+ _isDraft: boolean;
+ archived?: boolean;
+ relatedResources?: RelatedResourcesResponse;
+
+ customFields: {
+ name: string,
+ displayName: string,
+ type: string,
+ value: string| string[]
+ }[]
+ approvedBy?: string[];
+}
+
+
+export class DocumentMetadata {
+ approvers?: string[];
+ approverGroups?: string[];
+ created: string;
+ contributors?: string[];
+ createdTime: number;
+ docNumber: string;
+ docType: string;
+ modifiedTime: number;
+ objectID: string;
+ owners: string[];
+ product: string;
+ status: string;
+ summary: string;
+ title: string;
+
+ customFields: Map = new Map();
+
+
+ constructor(data: Record & IDocumentMetadata) {
+ this.approvers = data.approvers;
+ this.approverGroups = data.approverGroups;
+ this.created = data.created;
+ this.createdTime = data.createdTime;
+ this.docNumber = data.docNumber;
+ this.docType = data.docType;
+ this.modifiedTime = data.modifiedTime;
+ this.objectID = data.objectID;
+ this.owners = data.owners;
+ }
+
+}
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/interfaces/group.ts b/hermes-plugin/src/taskpane/interfaces/group.ts
new file mode 100644
index 000000000..475279971
--- /dev/null
+++ b/hermes-plugin/src/taskpane/interfaces/group.ts
@@ -0,0 +1,4 @@
+export interface Group {
+ email: string;
+ name: string;
+}
diff --git a/hermes-plugin/src/taskpane/interfaces/person.ts b/hermes-plugin/src/taskpane/interfaces/person.ts
new file mode 100644
index 000000000..564d10689
--- /dev/null
+++ b/hermes-plugin/src/taskpane/interfaces/person.ts
@@ -0,0 +1,39 @@
+export interface Person {
+ emailAddresses: EmailAddress[];
+ etag: string;
+ names: Name[];
+ photos: Photo[];
+ resourceName: string;
+}
+
+export interface EmailAddress {
+ metadata: Metadata;
+ value: string;
+}
+
+export interface Name {
+ displayName: string;
+ displayNameLastFirst: string;
+ familyName: string;
+ givenName: string;
+ metadata: Metadata;
+ unstructuredName: string;
+}
+
+export interface Photo {
+ default: boolean;
+ metadata: Metadata;
+ url: string;
+}
+
+export interface Metadata {
+ primary: boolean;
+ source: Source;
+ sourcePrimary?: boolean;
+ verified?: boolean;
+}
+
+export interface Source {
+ id: string;
+ type: string;
+}
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/interfaces/products.ts b/hermes-plugin/src/taskpane/interfaces/products.ts
new file mode 100644
index 000000000..f8325272a
--- /dev/null
+++ b/hermes-plugin/src/taskpane/interfaces/products.ts
@@ -0,0 +1,4 @@
+export default interface IProduct {
+ abbreviation: string;
+ perDocTypeData: any;
+}
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/interfaces/project.ts b/hermes-plugin/src/taskpane/interfaces/project.ts
new file mode 100644
index 000000000..54143bf08
--- /dev/null
+++ b/hermes-plugin/src/taskpane/interfaces/project.ts
@@ -0,0 +1,40 @@
+export enum ProjectStatus {
+ Active = "active",
+ Completed = "completed",
+ Archived = "archived",
+}
+
+export interface HermesProjectInfo {
+ id: string;
+ title: string;
+ status: ProjectStatus;
+ description?: string;
+ jiraIssueID?: string;
+ creator: string;
+ createdTime: number;
+ modifiedTime: number;
+ products?: string[];
+}
+
+export interface HermesProject extends HermesProjectInfo {
+ // Additional fields can be added here if needed
+}
+
+// Color constants for project status icons (matching web app)
+export const PROJECT_COLORS = {
+ active: {
+ bg: '#f4f0ff', // light purple
+ outline: '#d1c4e9', // purple outline
+ icon: '#9c27b0', // purple icon
+ },
+ completed: {
+ bg: '#e8f5e8', // light green
+ outline: '#c8e6c8', // green outline
+ icon: '#4caf50', // green icon
+ },
+ archived: {
+ bg: '#f5f5f5', // light gray
+ outline: '#e0e0e0', // gray outline
+ icon: '#757575', // gray icon
+ },
+};
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/interfaces/relatedResources.ts b/hermes-plugin/src/taskpane/interfaces/relatedResources.ts
new file mode 100644
index 000000000..d791c0b99
--- /dev/null
+++ b/hermes-plugin/src/taskpane/interfaces/relatedResources.ts
@@ -0,0 +1,94 @@
+/**
+ * Represents an external link resource related to a document
+ */
+export interface RelatedExternalLink {
+ name: string;
+ url: string;
+ sortOrder: number;
+}
+
+/**
+ * Represents a Hermes document resource related to another document
+ */
+export interface RelatedHermesDocument {
+ FileID: string;
+ title: string;
+ documentType: string;
+ documentNumber: string;
+ sortOrder: number;
+ createdTime?: number;
+ modifiedTime: number;
+ product: string;
+ status: string;
+ owners: string[];
+ summary?: string;
+}
+
+/**
+ * Union type for all related resource types
+ */
+export type RelatedResource = RelatedExternalLink | RelatedHermesDocument;
+
+/**
+ * Response structure from the API when fetching related resources
+ */
+export interface RelatedResourcesResponse {
+ externalLinks: RelatedExternalLink[];
+ hermesDocuments: RelatedHermesDocument[];
+}
+
+/**
+ * Request structure for updating related resources via API
+ */
+export interface RelatedResourcesUpdateRequest {
+ externalLinks: Partial[];
+ hermesDocuments: Partial[];
+}
+
+/**
+ * Helper type guards to distinguish between resource types
+ */
+export const isExternalLink = (resource: RelatedResource): resource is RelatedExternalLink => {
+ return 'url' in resource && 'name' in resource;
+};
+
+export const isHermesDocument = (resource: RelatedResource): resource is RelatedHermesDocument => {
+ return 'FileID' in resource && 'title' in resource;
+};
+
+/**
+ * Utility function to combine and sort related resources
+ */
+export const combineAndSortResources = (response: RelatedResourcesResponse): RelatedResource[] => {
+ const combined: RelatedResource[] = [
+ ...(response.externalLinks || []),
+ ...(response.hermesDocuments || [])
+ ];
+
+ return combined.sort((a, b) => a.sortOrder - b.sortOrder);
+};
+
+/**
+ * Utility function to format resources for API update
+ */
+export const formatResourcesForUpdate = (resources: RelatedResource[] = []): RelatedResourcesUpdateRequest => {
+ const externalLinks: Partial[] = [];
+ const hermesDocuments: Partial[] = [];
+
+ resources.forEach(resource => {
+ if (isExternalLink(resource)) {
+ externalLinks.push({
+ name: resource.name,
+ url: resource.url,
+ sortOrder: resource.sortOrder
+ });
+ } else if (isHermesDocument(resource)) {
+ hermesDocuments.push({
+ FileID: resource.FileID,
+ sortOrder: resource.sortOrder
+ });
+ }
+ });
+
+ return { externalLinks, hermesDocuments };
+};
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/taskpane.html b/hermes-plugin/src/taskpane/taskpane.html
new file mode 100644
index 000000000..e5df33a6d
--- /dev/null
+++ b/hermes-plugin/src/taskpane/taskpane.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+ Hermes
+
+
+
+
+
+
+
+
+
+
+ This add-in will not run in your version of Office. Please upgrade either to perpetual Office 2021 (or later)
+ or to a Microsoft 365 account.
+
+
+
+
+
diff --git a/hermes-plugin/src/taskpane/taskpane.ts b/hermes-plugin/src/taskpane/taskpane.ts
new file mode 100644
index 000000000..77d7d34ed
--- /dev/null
+++ b/hermes-plugin/src/taskpane/taskpane.ts
@@ -0,0 +1,91 @@
+/* global Word console */
+
+export async function insertText(text: string) {
+ console.log("Inserting text: " + text);
+ // Write text to the document.
+ try {
+ await Word.run(async (context) => {
+ let body = context.document.body;
+ //body.insertParagraph(text, Word.InsertLocation.end);
+ await context.sync();
+ });
+ } catch (error) {
+ console.log("Error: " + error);
+ }
+}
+
+
+
+// export async function getWordDocProperty() {
+// try {
+// await Word.run(async (ctx) => {
+// const documentProperties = ctx.document.properties.load();
+// await ctx.sync();
+
+// console.log(">>>>> keywords",documentProperties.toJSON());
+// });
+// } catch (error) {
+// console.error(error);
+
+// }
+// }
+
+const headers = ["Summary", "Created", "Status", "Owner", "Product", "Owner", "Approvers", "Contributors", "Name", ""];
+const sts = "Approved"
+
+export async function getHeaders() {
+ try {
+ await Word.run(async (context) => {
+ await context.sync();
+ const tables = context.document.body.tables;
+
+ const items = tables.load();
+ // await items.context.sync();
+ await context.sync();
+ const headerTable = items.getFirst().load();
+ // await headerTable.context.sync();
+ const rows = headerTable.rows.load();
+ await context.sync();
+ console.log("Rows loaded", rows.items);
+ const rowItems = rows.items;
+
+ for (const rowI of rowItems) {
+ const cells = rowI.cells;
+ cells.load();
+ await context.sync();
+
+ const cellItems = cells.items
+ console.log("COL:", cellItems);
+ for (const col of cellItems) {
+ const body = col.body
+ body.load();
+ await context.sync();
+ const cellText = body.text;
+ const [header, value] = cellText.split(":");
+
+ if (header === "Status") {
+ console.log("Found Status:" );
+ body.font.bold = false;
+
+ body.insertText(`Status: ${value}`, "Replace");
+
+ const foundItems = body.search(sts, {matchCase: true});
+ foundItems.load();
+ await context.sync();
+ //console.log("Found items:", foundItems.items[0].text);
+
+ if (foundItems.items.length !== 0) {
+ foundItems.items[0].font.bold = true;
+ console.log("Found and made bold");
+ }
+ // body.insertText("Status: In Progress", Word.InsertLocation.replace);
+ await context.sync();
+ }
+ }
+ }
+
+ });
+ } catch (error) {
+ console.log(error.stack);
+ }
+}
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/utils/authPopup.ts b/hermes-plugin/src/taskpane/utils/authPopup.ts
new file mode 100644
index 000000000..8bd8a2407
--- /dev/null
+++ b/hermes-plugin/src/taskpane/utils/authPopup.ts
@@ -0,0 +1,269 @@
+/**
+ * Authentication popup utility for handling OAuth flows in a separate window.
+ *
+ * This implements Microsoft's recommended approach for Office Add-ins:
+ * 1. Open dialog using Office Dialog API for authentication (first-party context)
+ * 2. ALB OIDC sets session cookies in dialog
+ * 3. Use Storage Access API to access those cookies from iframe
+ *
+ * @see https://learn.microsoft.com/en-us/office/dev/add-ins/develop/itp-and-third-party-cookies
+ */
+
+import { requestStorageAccess, hasStorageAccess } from './storageAccess';
+
+export interface AuthResult {
+ success: boolean;
+ email?: string;
+ error?: string;
+}
+
+const AUTH_TIMEOUT = 300000;
+const SAFARI_DASHBOARD_PRELOAD_DELAY = 2000;
+
+function isSafari(): boolean {
+ const ua = navigator.userAgent.toLowerCase();
+ return ua.includes('safari') && !ua.includes('chrome') && !ua.includes('chromium');
+}
+
+function prepareSafariAuthUrl(authUrl: string): string {
+ try {
+ const url = new URL(authUrl);
+ const baseUrl = `${url.protocol}//${url.host}`;
+ const encodedAuthUrl = encodeURIComponent(authUrl);
+ return `${baseUrl}/addin/safari-init.html?redirect_to=${encodedAuthUrl}&delay=${SAFARI_DASHBOARD_PRELOAD_DELAY}`;
+ } catch {
+ return authUrl;
+ }
+}
+
+/**
+ * Initiates OAuth flow using Office Dialog API.
+ * This avoids popup blocker issues in Office Add-ins.
+ *
+ * @param authUrl The authentication URL to open in the dialog
+ * @returns Promise that resolves when dialog is closed
+ */
+export async function authenticateWithPopup(authUrl: string): Promise {
+ const safari = isSafari();
+ const finalAuthUrl = safari ? prepareSafariAuthUrl(authUrl) : authUrl;
+
+ return new Promise((resolve, reject) => {
+ if (typeof Office === 'undefined' || !Office.context || !Office.context.ui) {
+ return fallbackWindowOpen(finalAuthUrl, resolve, reject);
+ }
+
+ Office.context.ui.displayDialogAsync(
+ finalAuthUrl,
+ {
+ height: 60, // percentage of screen height
+ width: 40, // percentage of screen width
+ promptBeforeOpen: false
+ },
+ (result) => {
+ if (result.status === Office.AsyncResultStatus.Failed) {
+ console.error('Failed to open dialog:', result.error);
+ // Try fallback
+ return fallbackWindowOpen(authUrl, resolve, reject);
+ }
+
+ const dialog = result.value;
+ let timeoutId: NodeJS.Timeout | null = null;
+ let resolved = false;
+
+ const cleanup = () => {
+ if (timeoutId) clearTimeout(timeoutId);
+ };
+
+ // Listen for dialog events
+ dialog.addEventHandler(Office.EventType.DialogMessageReceived, (arg: any) => {
+ if (resolved) return;
+ resolved = true;
+ cleanup();
+ dialog.close();
+
+ try {
+ const message = JSON.parse(arg.message);
+ if (message.type === 'AUTH_COMPLETE') {
+ resolve({
+ success: message.success,
+ email: message.email,
+ error: message.error
+ });
+ } else {
+ // Assume success if we got any message
+ resolve({ success: true });
+ }
+ } catch {
+ // Message wasn't JSON, assume success
+ resolve({ success: true });
+ }
+ });
+
+ dialog.addEventHandler(Office.EventType.DialogEventReceived, (arg: any) => {
+ if (resolved) return;
+ resolved = true;
+ cleanup();
+
+ // Dialog was closed by user - assume auth completed
+ // (we verify later via API call)
+ if (arg.error === 12006) { // Dialog closed
+ resolve({ success: true });
+ } else {
+ resolve({ success: true }); // Assume success, verify later
+ }
+ });
+
+ // Timeout
+ timeoutId = setTimeout(() => {
+ if (resolved) return;
+ resolved = true;
+ cleanup();
+ dialog.close();
+ reject(new Error('Authentication timeout. Please try again.'));
+ }, AUTH_TIMEOUT);
+ }
+ );
+ });
+}
+
+/**
+ * Fallback to window.open for non-Office environments or when Dialog API fails.
+ */
+function fallbackWindowOpen(
+ authUrl: string,
+ resolve: (value: AuthResult) => void,
+ reject: (reason: Error) => void
+): void {
+ const safari = isSafari();
+ const finalAuthUrl = safari ? prepareSafariAuthUrl(authUrl) : authUrl;
+
+ const popupWidth = 500;
+ const popupHeight = 600;
+ const left = window.screenX + (window.outerWidth - popupWidth) / 2;
+ const top = window.screenY + (window.outerHeight - popupHeight) / 2;
+
+ const popup = window.open(
+ finalAuthUrl,
+ 'hermes-auth',
+ `width=${popupWidth},height=${popupHeight},left=${left},top=${top},popup=yes`
+ );
+
+ if (!popup) {
+ reject(new Error('Failed to open authentication window. Please check your popup blocker settings.'));
+ return;
+ }
+
+ let timeoutId: NodeJS.Timeout | null = null;
+ let checkIntervalId: NodeJS.Timeout | null = null;
+ let cleanedUp = false;
+
+ const cleanup = () => {
+ if (cleanedUp) return;
+ cleanedUp = true;
+ if (timeoutId) clearTimeout(timeoutId);
+ if (checkIntervalId) clearInterval(checkIntervalId);
+ };
+
+ timeoutId = setTimeout(() => {
+ cleanup();
+ if (popup && !popup.closed) {
+ popup.close();
+ }
+ reject(new Error('Authentication timeout. Please try again.'));
+ }, AUTH_TIMEOUT);
+
+ checkIntervalId = setInterval(() => {
+ if (popup.closed) {
+ cleanup();
+ resolve({ success: true });
+ }
+ }, 500);
+}
+
+export async function grantStorageAccess(baseUrl?: string): Promise {
+ return await requestStorageAccess(baseUrl);
+}
+
+export async function initializeStorageAccess(baseUrl?: string): Promise {
+ const { hasStorageAccess, tryRestoreStorageAccess, verifyStorageAccessWorks } = await import('./storageAccess');
+
+ const currentAccess = await hasStorageAccess();
+ if (currentAccess) {
+ if (baseUrl) {
+ return await verifyStorageAccessWorks(baseUrl);
+ }
+ return true;
+ }
+
+ return await tryRestoreStorageAccess(baseUrl);
+}
+
+/**
+ * Sends authentication result to the parent window (opener).
+ * Called from the OAuth callback page.
+ */
+export function sendAuthResultToOpener(result: AuthResult): void {
+ if (window.opener) {
+ try {
+ // Try to get the opener's origin. This may fail due to cross-origin restrictions.
+ let targetOrigin: string;
+ try {
+ targetOrigin = window.opener.location.origin;
+ } catch (e) {
+ // If we can't access opener.location due to cross-origin restrictions,
+ // fall back to current window origin (should be same origin as opener for this flow)
+ console.warn('Cannot access window.opener.location.origin, using window.location.origin', e);
+ targetOrigin = window.location.origin;
+ }
+
+ window.opener.postMessage(
+ {
+ type: 'AUTH_COMPLETE',
+ success: result.success,
+ email: result.email,
+ error: result.error
+ },
+ targetOrigin
+ );
+ } catch (e) {
+ console.error('Failed to send auth result to opener:', e);
+ }
+ }
+}
+
+/**
+ * Checks if cookies are accessible and authentication is valid.
+ * Uses Storage Access API to check cookie availability.
+ */
+export async function checkAuthStatus(baseUrl: string): Promise {
+ try {
+ // First check if we have storage access
+ const hasAccess = await hasStorageAccess();
+ if (!hasAccess) {
+ console.log('No storage access - authentication required');
+ return false;
+ }
+
+ // Make a test request to check if ALB OIDC session is valid
+ const response = await fetch(`${baseUrl}/api/v2/me`, {
+ credentials: 'include',
+ headers: {
+ 'Accept': 'application/json'
+ }
+ });
+
+ return response.ok;
+ } catch (error) {
+ console.error('Error checking auth status:', error);
+ return false;
+ }
+}
+
+/**
+ * Clears authentication state.
+ */
+export function clearAuth(): void {
+ // No local state to clear with cookie-based auth
+ console.log('Auth cleared');
+}
+
diff --git a/hermes-plugin/src/taskpane/utils/darkTheme.ts b/hermes-plugin/src/taskpane/utils/darkTheme.ts
new file mode 100644
index 000000000..16569c0be
--- /dev/null
+++ b/hermes-plugin/src/taskpane/utils/darkTheme.ts
@@ -0,0 +1,417 @@
+/**
+ * Dark Mode Theme Constants for Hermes Word Add-in
+ * Maintains consistency across all components while providing a cool, modern dark theme
+ */
+
+export const DarkTheme = {
+ // Primary background colors
+ background: {
+ primary: "#1a1a1a", // Main dark background
+ secondary: "#242424", // Slightly lighter for cards/panels
+ tertiary: "#2d2d2d", // Even lighter for hover states
+ elevated: "#333333", // Elevated surfaces like modals
+ subtle: "#1f1f1f", // Subtle variations
+ error: "#3d1a1a", // Error container background
+ success: "#22c55e", // Success indicator background
+ },
+
+ // Text colors with proper contrast
+ text: {
+ primary: "#ffffff", // Primary white text
+ secondary: "#e0e0e0", // Secondary lighter gray
+ tertiary: "#b0b0b0", // Tertiary for labels/hints
+ disabled: "#666666", // Disabled state text
+ inverse: "#1a1a1a", // Dark text for light backgrounds
+ placeholder: "#6b7280", // Placeholder/empty state text
+ error: "#ff6b6b", // Error message text
+ link: "#0078d4", // Hyperlink text
+ },
+
+ // Border and divider colors
+ border: {
+ primary: "#404040", // Main border color
+ secondary: "#4a4a4a", // Lighter borders for hover
+ subtle: "#353535", // Very subtle borders
+ focus: "#0078d4", // Focus states (keeping Microsoft blue)
+ },
+
+ // Interactive element colors
+ interactive: {
+ primary: "#0078d4", // Microsoft blue for primary actions
+ primaryHover: "#106ebe", // Darker blue for hover
+ secondary: "#404040", // Secondary buttons
+ secondaryHover: "#4a4a4a", // Secondary hover
+ danger: "#d83b01", // Delete/danger actions
+ dangerHover: "#c23000", // Danger hover
+ success: "#107c10", // Success/approval states
+ successHover: "#0e6e0e", // Success hover
+ warning: "#ff8c00", // Warning states
+ warningHover: "#e67c00", // Warning hover
+ },
+
+ // Status-specific colors (maintaining semantic meaning)
+ status: {
+ approved: {
+ background: "#1a3d1a", // Dark green background
+ text: "#4ade80", // Light green text
+ border: "#2d5a2d", // Green border
+ },
+ inReview: {
+ background: "#2d1b3d", // Dark purple background
+ text: "#c084fc", // Light purple text
+ border: "#4a2d5a", // Purple border
+ },
+ draft: {
+ background: "#3d2a1a", // Dark orange background
+ text: "#fb923c", // Light orange text
+ border: "#5a3d2d", // Orange border
+ },
+ obsolete: {
+ background: "#2d2d2d", // Neutral gray background
+ text: "#9ca3af", // Gray text
+ border: "#404040", // Gray border
+ },
+ warning: {
+ background: "#3d2a1a", // Dark orange background
+ text: "#fb923c", // Light orange text
+ border: "#5a3d2d", // Orange border
+ },
+ },
+
+ // Component-specific styles
+ components: {
+ // Cards and containers
+ card: {
+ background: "#242424",
+ border: "#404040",
+ shadow: "0 2px 8px rgba(0, 0, 0, 0.3)",
+ },
+
+ // Form inputs
+ input: {
+ background: "#2d2d2d",
+ border: "#404040",
+ focusBorder: "#0078d4",
+ text: "#ffffff",
+ placeholder: "#888888",
+ },
+
+ // Dropdowns and selects
+ dropdown: {
+ background: "#2d2d2d",
+ border: "#404040",
+ optionHover: "#404040",
+ text: "#ffffff",
+ },
+
+ // Buttons
+ button: {
+ primary: {
+ background: "#0078d4",
+ backgroundHover: "#106ebe",
+ text: "#ffffff",
+ },
+ secondary: {
+ background: "transparent",
+ backgroundHover: "#404040",
+ text: "#e0e0e0",
+ border: "#404040",
+ },
+ },
+
+ // Progress bars
+ progressBar: {
+ background: "#404040",
+ fill: "#0078d4",
+ },
+
+ // Tooltips
+ tooltip: {
+ background: "#1f1f1f",
+ text: "#ffffff",
+ border: "#404040",
+ },
+
+ // Header and navigation
+ header: {
+ background: "#1a1a1a",
+ border: "#404040",
+ text: "#ffffff",
+ linkColor: "#4fc3f7", // Cool blue for links
+ linkHover: "#29b6f6",
+ },
+
+ // Footer
+ footer: {
+ background: "#1a1a1a",
+ border: "#404040",
+ text: "#b0b0b0",
+ },
+ },
+
+ // Hover and focus states
+ states: {
+ hover: "rgba(255, 255, 255, 0.1)",
+ focus: "0 0 0 2px #0078d4",
+ active: "rgba(255, 255, 255, 0.2)",
+ disabled: "rgba(255, 255, 255, 0.3)",
+ },
+
+ // Shadows and elevation
+ shadows: {
+ small: "0 1px 3px rgba(0, 0, 0, 0.3)",
+ medium: "0 2px 8px rgba(0, 0, 0, 0.3)",
+ large: "0 4px 16px rgba(0, 0, 0, 0.4)",
+ },
+
+ // Gradients for special elements
+ gradients: {
+ subtle: "linear-gradient(135deg, #1a1a1a 0%, #242424 100%)",
+ accent: "linear-gradient(135deg, #0078d4 0%, #106ebe 100%)",
+ },
+};
+
+// Utility function to get contrast color
+export function getContrastColor(): string {
+ // For dark theme, we generally use light text
+ return DarkTheme.text.primary;
+}
+
+// Utility function to create hover styles
+export function createHoverStyle(baseColor: string, hoverColor: string) {
+ return {
+ backgroundColor: baseColor,
+ transition: "all 0.2s ease",
+ ":hover": {
+ backgroundColor: hoverColor,
+ },
+ };
+}
+
+// Utility function for focus styles
+export function createFocusStyle() {
+ return {
+ ":focus": {
+ outline: "none",
+ boxShadow: DarkTheme.states.focus,
+ },
+ };
+}
+
+/**
+ * Fluent UI Theme Tokens
+ * These tokens can be used to override Fluent UI component defaults.
+ * Apply these when wrapping your app with FluentProvider.
+ */
+export const FluentDarkTheme = {
+ colorNeutralBackground1: DarkTheme.background.primary,
+ colorNeutralBackground2: DarkTheme.background.secondary,
+ colorNeutralBackground3: DarkTheme.background.tertiary,
+ colorNeutralForeground1: DarkTheme.text.primary,
+ colorNeutralForeground2: DarkTheme.text.secondary,
+ colorNeutralForeground3: DarkTheme.text.tertiary,
+ colorNeutralStroke1: DarkTheme.border.primary,
+ colorNeutralStroke2: DarkTheme.border.secondary,
+ colorBrandBackground: DarkTheme.interactive.primary,
+ colorBrandBackgroundHover: DarkTheme.interactive.primaryHover,
+ colorBrandForeground1: DarkTheme.text.primary,
+};
+
+/**
+ * Common style utilities for consistent theming across components.
+ *
+ * Use these base styles in your makeStyles definitions to maintain consistency:
+ *
+ * @example
+ * ```typescript
+ * import { commonStyles } from "../utils/darkTheme";
+ *
+ * const useStyles = makeStyles({
+ * myButton: commonStyles.primaryButton,
+ * myCustomButton: {
+ * ...commonStyles.secondaryButton,
+ * width: "100%", // Add custom properties
+ * },
+ * });
+ * ```
+ */
+export const commonStyles = {
+ // Input field styles
+ inputField: {
+ backgroundColor: DarkTheme.components.input.background,
+ border: `1px solid ${DarkTheme.border.primary}`,
+ borderRadius: "4px",
+ color: DarkTheme.text.primary,
+ "&::placeholder": {
+ color: DarkTheme.components.input.placeholder,
+ },
+ "&:hover": {
+ borderColor: DarkTheme.border.secondary,
+ },
+ "&:focus": {
+ borderColor: DarkTheme.border.focus,
+ outline: "none",
+ },
+ },
+
+ // Field label styles
+ fieldLabel: {
+ color: DarkTheme.text.primary,
+ fontSize: "14px",
+ fontWeight: "500",
+ marginBottom: "4px",
+ },
+
+ // Primary button (Save, Submit, etc.)
+ primaryButton: {
+ backgroundColor: DarkTheme.interactive.primary,
+ color: DarkTheme.text.primary,
+ border: "none",
+ borderRadius: "4px",
+ fontWeight: "600",
+ "&:hover": {
+ backgroundColor: DarkTheme.interactive.primaryHover,
+ },
+ "&:active": {
+ backgroundColor: DarkTheme.interactive.primaryHover,
+ },
+ },
+
+ // Secondary button (Cancel, Close, etc.)
+ secondaryButton: {
+ backgroundColor: "transparent",
+ color: DarkTheme.text.secondary,
+ border: `1px solid ${DarkTheme.border.primary}`,
+ borderRadius: "4px",
+ fontWeight: "500",
+ "&:hover": {
+ backgroundColor: DarkTheme.background.tertiary,
+ color: DarkTheme.text.primary,
+ borderColor: DarkTheme.border.secondary,
+ },
+ },
+
+ // Danger/Delete button
+ dangerButton: {
+ backgroundColor: "transparent",
+ color: DarkTheme.interactive.danger,
+ border: `1px solid ${DarkTheme.interactive.danger}`,
+ borderRadius: "4px",
+ fontWeight: "500",
+ "&:hover": {
+ backgroundColor: "#3d1a1a",
+ borderColor: DarkTheme.interactive.dangerHover,
+ },
+ },
+
+ // Icon buttons (checkmark, dismiss, etc.)
+ iconButton: {
+ backgroundColor: "transparent",
+ border: "none",
+ borderRadius: "4px",
+ minWidth: "32px",
+ minHeight: "32px",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ "&:hover": {
+ backgroundColor: DarkTheme.background.tertiary,
+ },
+ },
+
+ // Primary icon button (for save/checkmark)
+ primaryIconButton: {
+ backgroundColor: DarkTheme.interactive.primary,
+ color: DarkTheme.text.primary,
+ border: "none",
+ borderRadius: "4px",
+ minWidth: "32px",
+ minHeight: "32px",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ "&:hover": {
+ backgroundColor: DarkTheme.interactive.primaryHover,
+ },
+ },
+
+ // Secondary icon button (for cancel/dismiss)
+ secondaryIconButton: {
+ backgroundColor: "transparent",
+ color: DarkTheme.text.secondary,
+ border: `1px solid ${DarkTheme.border.primary}`,
+ borderRadius: "4px",
+ minWidth: "32px",
+ minHeight: "32px",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ "&:hover": {
+ backgroundColor: DarkTheme.background.tertiary,
+ color: DarkTheme.text.primary,
+ borderColor: DarkTheme.border.secondary,
+ },
+ },
+
+ // Tag picker control (for People and Groups components)
+ tagPickerControl: {
+ backgroundColor: DarkTheme.background.secondary,
+ border: `1px solid ${DarkTheme.border.primary}`,
+ color: DarkTheme.text.primary,
+ flexWrap: "wrap" as const,
+ minHeight: "44px",
+ padding: "4px",
+ gap: "4px",
+ },
+
+ // Tag picker group
+ tagPickerGroup: {
+ display: "flex",
+ flexWrap: "wrap" as const,
+ gap: "4px",
+ },
+
+ // Tag picker list
+ tagPickerList: {
+ backgroundColor: DarkTheme.background.elevated,
+ border: `1px solid ${DarkTheme.border.primary}`,
+ borderRadius: "4px",
+ },
+};
+
+/**
+ * Spacing constants for consistent layout across components
+ */
+export const spacing = {
+ xs: "4px",
+ sm: "8px",
+ md: "12px",
+ lg: "16px",
+ xl: "20px",
+ xxl: "32px",
+};
+
+/**
+ * Font size constants
+ */
+export const fontSize = {
+ xs: "10px",
+ sm: "12px",
+ md: "14px",
+ lg: "16px",
+ xl: "18px",
+ xxl: "20px",
+};
+
+/**
+ * Border radius constants
+ */
+export const borderRadius = {
+ sm: "4px",
+ md: "6px",
+ lg: "8px",
+ round: "50%",
+};
+
+// Export default theme
+export default DarkTheme;
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/utils/hermesClient.ts b/hermes-plugin/src/taskpane/utils/hermesClient.ts
new file mode 100644
index 000000000..c1c3a8e6f
--- /dev/null
+++ b/hermes-plugin/src/taskpane/utils/hermesClient.ts
@@ -0,0 +1,1137 @@
+import CurrentUser from "../interfaces/currentUser";
+import IDocumentMetadata from "../interfaces/documentMetadata";
+import { Person } from "../interfaces/person";
+import { Group } from "../interfaces/group";
+import IProduct from "../interfaces/products";
+import { RelatedResourcesResponse, RelatedResourcesUpdateRequest, RelatedHermesDocument } from "../interfaces/relatedResources";
+import { HermesProject, ProjectStatus } from "../interfaces/project";
+
+export const HERMES_AUTH_REQUIRED_EVENT = "hermes-auth-required";
+
+/**
+ * `HermesClient` provides a set of methods for interacting with the Hermes API.
+ *
+ * This client abstracts HTTP requests to various endpoints, enabling retrieval and update of products,
+ * documents, drafts, and people metadata. It handles authentication, error reporting, and response parsing.
+ *
+ * Authentication is handled via ALB OIDC session cookies. The Storage Access API must be
+ * granted before making requests from a third-party iframe context (Office Add-in).
+ *
+ * @example
+ * ```typescript
+ * const client = new HermesClient("https://api.example.com");
+ * const products = await client.getProducts();
+ * ```
+ *
+ * @remarks
+ * - All requests include credentials and expect JSON responses.
+ * - Methods throw errors on non-OK responses or network failures.
+ *
+ * @public
+ */
+export default class HermesClient {
+ constructor(private baseUrl: string) {
+ if (this.baseUrl === null || this.baseUrl.length === 0 || typeof this.baseUrl !== "string") {
+ this.baseUrl = "";
+ }
+ }
+
+ /**
+ * Creates a standardized error message from a fetch result.
+ * @param result - The result object containing error or response information
+ * @returns A string describing the error
+ */
+ private getErrorMessage(result: { error?: { message?: string; status?: number }; response?: Response; success: boolean }): string {
+ return result.error?.message || (result.response ? `Status ${result.response.status}` : 'Unknown error');
+ }
+
+ private notifyAuthenticationRequired(detail?: unknown): void {
+ window.dispatchEvent(
+ new CustomEvent(HERMES_AUTH_REQUIRED_EVENT, { detail })
+ );
+ }
+
+ private isAuthenticationError(input: Response | unknown): boolean {
+ if (input instanceof Response) {
+ return (
+ input.status === 401 ||
+ input.status === 302 ||
+ input.status === 0 ||
+ input.type === "opaqueredirect"
+ );
+ }
+
+ const error = input as {
+ status?: number;
+ type?: string;
+ message?: string;
+ response?: { status?: number; type?: string };
+ } | null;
+
+ if (!error) {
+ return false;
+ }
+
+ if (error.status === 401 || error.status === 302 || error.status === 0) {
+ return true;
+ }
+
+ if (error.type === "opaqueredirect" || error.response?.type === "opaqueredirect") {
+ return true;
+ }
+
+ if (error.response?.status === 401 || error.response?.status === 302) {
+ return true;
+ }
+
+ return Boolean(
+ error.message && (
+ error.message.includes("401") ||
+ error.message.includes("302") ||
+ error.message.includes("Authentication required") ||
+ error.message.includes("redirect to login") ||
+ error.message.includes("opaqueredirect")
+ )
+ );
+ }
+
+ private signalAuthIfNeeded(input: Response | unknown): void {
+ if (this.isAuthenticationError(input)) {
+ this.notifyAuthenticationRequired(input);
+ }
+ }
+
+ /**
+ * Constructs and returns a default `RequestInit` object for HTTP requests.
+ *
+ * The returned object includes:
+ * - `headers`: Sets the `Accept` header to `application/json` to indicate the expected response format.
+ * - `credentials`: Set to `"include"` to ensure cookies (ALB OIDC session) are sent with the request.
+ * - `method`: Set to `"GET"` as the default HTTP method.
+ * - `mode`: Set to `"cors"` to enable CORS requests.
+ *
+ * @returns {RequestInit} The default request initialization object for fetch calls.
+ */
+ private get reqHeader(): RequestInit {
+ const headers: Record = {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ "X-Requested-With": "XMLHttpRequest",
+ };
+
+ return {
+ headers,
+ credentials: "include",
+ method: "GET",
+ mode: "cors",
+ cache: "no-cache",
+ // Don't follow redirects automatically - ALB OIDC returns 302 to IdP
+ // when not authenticated. We need to detect this and show sign-in UI.
+ redirect: "manual",
+ } as RequestInit;
+ }
+
+ /**
+ * Creates headers for POST requests with JSON body containing a search query.
+ *
+ * @param query - The search query to include in the request body.
+ * @returns {RequestInit} The request initialization object configured for POST with JSON body.
+ */
+ private createPostSearchHeaders(query: string): RequestInit {
+ const baseHeader = this.reqHeader;
+ return {
+ ...baseHeader,
+ method: "POST",
+ body: JSON.stringify({ query }),
+ headers: {
+ ...(baseHeader.headers || {}),
+ "Content-Type": "application/json",
+ },
+ };
+ }
+
+ /**
+ * Retrieves a list of products from the API.
+ *
+ * @returns A promise that resolves to a record mapping product IDs to their corresponding `IProduct` objects.
+ * @throws Will throw an error if the request fails or the response is not OK.
+ */
+ public async getProducts(): Promise> {
+ try {
+ const productEndpoint = `${this.baseUrl}/api/v2/products`;
+ const res = await fetch(productEndpoint, this.reqHeader);
+ if (res.ok) {
+ return await res.json();
+ }
+
+ throw new Error(
+ `getProducts has failed with status: ${res.status} and body ${await res.text()}`
+ );
+ } catch (err) {
+ console.log("getProducts has failed with err: ", err);
+ throw err;
+ }
+ }
+
+ /**
+ * Retrieves the metadata details of a document or draft by its file ID.
+ *
+ * This method makes parallel requests to both draft and document endpoints to improve performance.
+ * Returns the first successful response with an `_isDraft` property indicating the source.
+ * If neither is found, it returns `null`.
+ *
+ * @param fileID - The unique identifier of the document or draft to retrieve.
+ * @returns A promise that resolves to the document metadata with an `_isDraft` flag, or `null` if not found.
+ * @throws Will throw an error if both fetch operations fail for reasons other than 404 not found.
+ */
+ public async getDocumentDetails(fileID: string): Promise {
+ try {
+ const draftEndpoint = `${this.baseUrl}/api/v2/drafts/${fileID}`;
+ const docEndpoint = `${this.baseUrl}/api/v2/documents/${fileID}`;
+
+ // Helper function to safely fetch and handle errors
+ const safeFetch = async (url: string) => {
+ try {
+ const response = await fetch(url, this.reqHeader);
+
+ // With redirect: 'manual', a 302 becomes an opaqueredirect response
+ // This indicates the user needs to authenticate
+ if (response.type === 'opaqueredirect' || response.status === 0) {
+ return {
+ success: false,
+ response: null,
+ error: { status: 302, message: 'Authentication redirect detected' }
+ };
+ }
+
+ return { success: true, response, error: null };
+ } catch (error) {
+ return { success: false, response: null, error };
+ }
+ };
+
+ // Make both requests in parallel for better performance
+ const [draftResult, docResult] = await Promise.all([
+ safeFetch(draftEndpoint),
+ safeFetch(docEndpoint)
+ ]);
+
+ // Check draft response first (preferred)
+ if (draftResult.success && draftResult.response && draftResult.response.ok) {
+ const body = await draftResult.response.json();
+ return {
+ ...body,
+ _isDraft: true,
+ };
+ }
+
+ // Check document response if draft failed
+ if (docResult.success && docResult.response && docResult.response.ok) {
+ const body = await docResult.response.json();
+ return {
+ ...body,
+ _isDraft: false,
+ };
+ }
+
+ // Both failed - check if they were 404s (not found) vs actual errors
+ const draftNotFound = draftResult.success && draftResult.response && draftResult.response.status === 404;
+ const docNotFound = docResult.success && docResult.response && docResult.response.status === 404;
+
+ if (draftNotFound && docNotFound) {
+ return null; // Document doesn't exist in either location
+ }
+
+ // Check if either error is an auth redirect (status 302)
+ const draftAuthRedirect = draftResult.error?.status === 302;
+ const docAuthRedirect = docResult.error?.status === 302;
+
+ if (draftAuthRedirect || docAuthRedirect) {
+ // Throw an error that preserves the status for isAuthenticationError to detect
+ const authError = new Error('Authentication required - redirect to login detected');
+ (authError as any).status = 302;
+ this.signalAuthIfNeeded(authError);
+ throw authError;
+ }
+
+ // If we got here, there was an actual error (not just 404 or auth redirect)
+ const draftError = this.getErrorMessage(draftResult);
+ const docError = this.getErrorMessage(docResult);
+
+ throw new Error(`getDocumentDetails failed - Draft: ${draftError}, Doc: ${docError}`);
+ } catch (error) {
+ console.log("Error in getDocumentDetails: ", error);
+ this.signalAuthIfNeeded(error);
+ throw error;
+ }
+ }
+
+ public async getDocument(fileID: string): Promise {
+ try {
+ const draftEndpoint = `${this.baseUrl}/api/v2/documents/${fileID}`;
+
+ let res = await fetch(draftEndpoint, this.reqHeader);
+
+ // Check for auth redirect (opaqueredirect from redirect: 'manual')
+ if (res.type === 'opaqueredirect' || res.status === 0) {
+ const authError = new Error('Authentication required - redirect to login detected');
+ (authError as any).status = 302;
+ this.signalAuthIfNeeded(authError);
+ throw authError;
+ }
+
+ if (res.status === 404) {
+ return null;
+ }
+
+ if (res.ok) {
+ const body = await res.json();
+ return {
+ ...body,
+ _isDraft: false,
+ };
+ }
+
+ this.signalAuthIfNeeded(res);
+ throw new Error(`getDocument failed with status: ${res.status} and body ${await res.text()}`);
+ } catch (error) {
+ console.log("Error in getDocumentDetails: ", error);
+ this.signalAuthIfNeeded(error);
+ throw error;
+ }
+ }
+
+ /**
+ * Retrieves details of people based on their email addresses.
+ *
+ * @param emails - An array of email addresses for which to fetch person details.
+ * @returns A promise that resolves to an array of `Person` objects corresponding to the provided emails.
+ * @throws Will throw an error if the request fails or the response is not OK.
+ */
+ public async getPeopleDetailsFromEmail(emails: string[]): Promise {
+ try {
+ const peopleEndpoint = `${this.baseUrl}/api/v2/people?emails=${emails.join(",")}`;
+ const res = await fetch(peopleEndpoint, this.reqHeader);
+ if (res.ok) {
+ return await res.json();
+ }
+
+ this.signalAuthIfNeeded(res);
+ throw new Error(
+ `getPeopleDetailsFromEmail has failed with status: ${res.status} and body ${await res.text()}`
+ );
+ } catch (err) {
+ console.log("getPeopleDetailsFromEmail has failed with err:", err);
+ this.signalAuthIfNeeded(err);
+ throw err;
+ }
+ }
+
+ /**
+ * Searches for people matching the provided query string.
+ *
+ * Sends a POST request to the `/api/v2/people` endpoint with the search query in the request body.
+ *
+ * @param query - The search string used to find matching people.
+ * @returns A promise that resolves to an array of `Person` objects matching the query.
+ * @throws Will throw an error if the request fails or the response is not OK.
+ */
+ public async searchPeople(query: string): Promise {
+ try {
+ const peopleEndpoint = `${this.baseUrl}/api/v2/people`;
+ const header = this.createPostSearchHeaders(query);
+
+ const res = await fetch(peopleEndpoint, header);
+ if (res.ok) {
+ return await res.json();
+ }
+
+ this.signalAuthIfNeeded(res);
+ throw new Error(
+ `searchPeople has failed with status: ${res.status} and body ${await res.text()}`
+ );
+ } catch (err) {
+ console.log("searchPeople has failed with err: ", err);
+ this.signalAuthIfNeeded(err);
+ throw err;
+ }
+ }
+
+ /**
+ * Searches for groups matching the provided query string.
+ *
+ * @param query - The search string used to find matching groups.
+ * @returns A promise that resolves to an array of `Group` objects matching the query.
+ * @throws Will throw an error if the request fails or the response is not OK.
+ */
+ public async searchGroups(query: string): Promise {
+ try {
+ const groupsEndpoint = `${this.baseUrl}/api/v2/groups`;
+ const header = this.createPostSearchHeaders(query);
+
+ const res = await fetch(groupsEndpoint, header);
+ if (res.ok) {
+ return await res.json();
+ }
+
+ this.signalAuthIfNeeded(res);
+ throw new Error(
+ `searchGroups has failed with status: ${res.status} and body ${await res.text()}`
+ );
+ } catch (err) {
+ console.log("searchGroups has failed with err: ", err);
+ this.signalAuthIfNeeded(err);
+ throw err;
+ }
+ }
+
+ /**
+ * Retrieves details of groups based on their email addresses.
+ * Since there's no dedicated endpoint, performs a search for each email and returns matching groups.
+ *
+ * @param emails - An array of group email addresses.
+ * @returns A promise that resolves to an array of `Group` objects.
+ */
+ public async getGroupDetailsFromEmail(emails: string[]): Promise {
+ try {
+ if (!emails || emails.length === 0) {
+ return [];
+ }
+
+ const groupPromises = emails.map((email) => this.searchGroups(email));
+ const groupResults = await Promise.all(groupPromises);
+
+ const groups: Group[] = [];
+ groupResults.forEach((result, index) => {
+ const lookupEmail = emails[index];
+ const matchingGroup = (result || []).find((group) => group.email === lookupEmail);
+ if (matchingGroup) {
+ groups.push(matchingGroup);
+ }
+ });
+
+ return groups;
+ } catch (err) {
+ console.log("getGroupDetailsFromEmail has failed with err:", err);
+ throw err;
+ }
+ }
+
+ /**
+ * Updates a document or draft on the server with the provided metadata.
+ *
+ * @param params - The parameters for updating the document.
+ * @param params.fileID - The unique identifier of the document or draft to update.
+ * @param params.updatePayload - The partial metadata to update on the document.
+ * @param params.isDraft - Indicates whether the target is a draft (`true`) or a published document (`false`).
+ * @returns A promise that resolves when the update is successful, or rejects with an error if the update fails.
+ * @throws Will throw an error if the server responds with a non-OK status or if a network error occurs.
+ */
+ public async updateDocument({
+ fileID,
+ updatePayload,
+ isDraft,
+ }: {
+ fileID: string;
+ updatePayload: Partial;
+ isDraft: boolean;
+ }): Promise {
+ try {
+ const header = this.reqHeader;
+ header.method = "PATCH";
+ header.body = JSON.stringify(updatePayload);
+ if (!header.headers) {
+ header.headers = {};
+ }
+
+ header.headers["Content-Type"] = "application/json";
+
+ const primaryEndpoint = `${this.baseUrl}/api/v2/${isDraft ? "drafts" : "documents"}/${fileID}`;
+ let res = await fetch(primaryEndpoint, header);
+ if (!res.ok && isDraft && res.status === 404) {
+ const publishedEndpoint = `${this.baseUrl}/api/v2/documents/${fileID}`;
+ res = await fetch(publishedEndpoint, header);
+ }
+
+ if (res.ok) {
+ return;
+ }
+
+ throw new Error(
+ `updateDocument has failed with status: ${res.status} and body ${await res.text()}`
+ );
+ } catch (err) {
+ console.log("updateDocument has failed with err:", err);
+ throw err;
+ }
+ }
+
+ /**
+ * Constructs a complete URL by appending the provided path or endpoint to the base URL.
+ *
+ * @param post - The path or endpoint to append to the base URL.
+ * @returns The fully constructed URL as a string.
+ */
+ public createCompleteUrl(post: string): string {
+ return `${this.baseUrl}${post}`;
+ }
+
+ /**
+ * Publishes a file for review by sending a POST request to the review endpoint.
+ *
+ * @param fileID - The unique identifier of the file to be published for review.
+ * @returns A promise that resolves when the file is successfully published for review.
+ * @throws Will throw an error if the request fails or the response is not OK.
+ */
+ public async publishForReview(fileID: string): Promise {
+ try {
+ const publishEndpoint = `${this.baseUrl}/api/v2/reviews/${fileID}`;
+ const headers = this.reqHeader;
+ headers.method = 'POST';
+
+ const res = await fetch(publishEndpoint, headers);
+ if (res.ok) {
+ return;
+ }
+
+ throw new Error(`publishForReview has failed with status: ${res.status} and body ${await res.text()}`);
+ } catch (err) {
+ console.log("publishForReview has failed with err: ", err);
+ throw err;
+ }
+ }
+
+ /**
+ * Deletes a draft with the specified file ID from the server.
+ *
+ * Sends a DELETE request to the drafts API endpoint. If the request is successful,
+ * the function resolves with no value. If the request fails, it throws an error
+ * containing the response status and body.
+ *
+ * @param fileID - The unique identifier of the draft to be deleted.
+ * @returns A promise that resolves when the draft is successfully deleted.
+ * @throws Will throw an error if the deletion fails or if a network error occurs.
+ */
+ public async deleteDraft(fileID: string): Promise {
+ try {
+ const deleteEndpoint = `${this.baseUrl}/api/v2/drafts/${fileID}`;
+ const headers = this.reqHeader;
+ headers.method = 'DELETE';
+
+ const res = await fetch(deleteEndpoint, headers);
+ if (res.ok) {
+ return;
+ }
+
+ throw new Error(
+ `deleteDraft has failed with status: ${res.status} and body ${await res.text()}`
+ )
+ } catch (err) {
+ console.log("deleteDraft has failed with err:", err);
+ throw err;
+ }
+ }
+
+ /**
+ * Retrieves the details of the currently authenticated user.
+ *
+ * Makes a GET request to the `/api/v2/me` endpoint using the configured base URL and request headers.
+ * If the request is successful, returns the user details as a `CurrentUser` object.
+ * Throws an error if the request fails, including the HTTP status and response body in the error message.
+ *
+ * @returns {Promise} A promise that resolves to the current user's details.
+ * @throws {Error} If the request fails or the response is not OK.
+ */
+ public async getCurrentUserDetails(): Promise {
+ try {
+ const currentUserEndpoint = `${this.baseUrl}/api/v2/me`;
+ const headers = this.reqHeader;
+
+ const res = await fetch(currentUserEndpoint, headers);
+
+ // Check for auth redirect (opaqueredirect from redirect: 'manual')
+ if (res.type === 'opaqueredirect' || res.status === 0) {
+ const authError = new Error('Authentication required - redirect to login detected');
+ (authError as any).status = 302;
+ throw authError;
+ }
+
+ if (res.ok) {
+ return await res.json();
+ }
+
+ this.signalAuthIfNeeded(res);
+
+ throw new Error(
+ `getCurrentUserDetails has failed with status: ${res.status} and body ${await res.text()}`
+ )
+ } catch (error) {
+ console.log("getCurrentUserDetails has failed with err: ", error);
+ this.signalAuthIfNeeded(error);
+ throw error;
+ }
+ }
+
+
+ public async approveDocument(fileID: string): Promise {
+ try {
+ const approveEndpoints = `${this.baseUrl}/api/v2/approvals/${fileID}`;
+ const headers = this.reqHeader;
+ headers.method = "POST";
+
+ const res = await fetch(approveEndpoints, headers);
+ if (res.ok) {
+ return;
+ }
+
+ throw new Error(`approveDocument failed withs status: ${res.status} and body ${await res.text()}`);
+ } catch (error) {
+ console.log("approveDocument has failed with err", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Searches for documents using the Hermes API.
+ *
+ * @param query - The search query string.
+ * @returns A promise that resolves to an array of matching documents.
+ * @throws Will throw an error if the search fails or the response is not OK.
+ */
+ public async searchDocuments(query: string): Promise {
+ try {
+ // Use Algolia search endpoint like the web app does
+ const searchParams = {
+ query: query,
+ hitsPerPage: 12,
+ attributesToRetrieve: [
+ 'title',
+ 'product',
+ 'docNumber',
+ 'docType',
+ 'status',
+ 'owners',
+ 'summary',
+ 'createdTime',
+ 'modifiedTime',
+ 'objectID',
+ 'created'
+ ]
+ };
+
+ const response = await fetch(
+ `${this.baseUrl}/1/indexes/docs/query`,
+ {
+ method: 'POST',
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ "X-Requested-With": "XMLHttpRequest",
+ },
+ credentials: "include",
+ mode: "cors",
+ cache: "no-cache",
+ body: JSON.stringify(searchParams),
+ }
+ );
+
+ if (!response.ok) {
+ let errorMessage = `Search failed (${response.status})`;
+
+ // Provide specific error messages for common HTTP status codes
+ switch (response.status) {
+ case 401:
+ errorMessage = "Authentication required - please log in";
+ break;
+ case 403:
+ errorMessage = "Access denied - insufficient permissions";
+ break;
+ case 404:
+ errorMessage = "Search service not found";
+ break;
+ case 429:
+ errorMessage = "Too many requests - please try again later";
+ break;
+ case 500:
+ errorMessage = "Server error - please try again later";
+ break;
+ case 503:
+ errorMessage = "Service unavailable - please try again later";
+ break;
+ }
+
+ throw new Error(errorMessage);
+ }
+
+ const data = await response.json();
+
+ // Convert Algolia response to IDocumentMetadata format
+ const documents: IDocumentMetadata[] = (data.hits || []).map((hit: any) => ({
+ objectID: hit.objectID,
+ title: hit.title || '',
+ docType: hit.docType || '',
+ docNumber: hit.docNumber || '',
+ product: hit.product || '',
+ status: hit.status || '',
+ owners: hit.owners || [],
+ summary: hit.summary || '',
+ createdTime: hit.createdTime || 0,
+ modifiedTime: hit.modifiedTime || 0,
+ created: hit.created || '',
+ customEditableFields: {},
+ customFields: [],
+ _isDraft: false
+ }));
+
+ return documents;
+ } catch (error) {
+ console.error('Error searching documents:', error);
+ // Re-throw the error so components can handle it
+ throw error;
+ }
+ }
+
+ /**
+ * Searches for active projects based on a query string.
+ *
+ * @param query - The search query string.
+ * @returns A promise that resolves to an array of matching projects.
+ * @throws Will throw an error if the search fails or the response is not OK.
+ */
+ public async searchProjects(query: string): Promise {
+ try {
+ const response = await fetch(
+ `${this.baseUrl}/api/v2/projects?status=${ProjectStatus.Active}&title=${encodeURIComponent(query)}`,
+ {
+ ...this.reqHeader,
+ method: "GET",
+ }
+ );
+
+ if (!response.ok) {
+ let errorMessage = `Project search failed (${response.status})`;
+
+ switch (response.status) {
+ case 401:
+ errorMessage = "Authentication required - please log in";
+ break;
+ case 403:
+ errorMessage = "Access denied - insufficient permissions";
+ break;
+ case 404:
+ errorMessage = "Projects service not found";
+ break;
+ case 500:
+ errorMessage = "Server error - please try again later";
+ break;
+ }
+
+ throw new Error(errorMessage);
+ }
+
+ const data = await response.json();
+ return data.projects || [];
+ } catch (error) {
+ console.error('Error searching projects:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Retrieves all active projects for project selection.
+ *
+ * @returns A promise that resolves to an array of active projects.
+ * @throws Will throw an error if the request fails or the response is not OK.
+ */
+ public async getActiveProjects(): Promise {
+ try {
+ const response = await fetch(
+ `${this.baseUrl}/api/v2/projects?status=${ProjectStatus.Active}`,
+ {
+ ...this.reqHeader,
+ method: "GET",
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`Failed to load projects: ${response.status} ${await response.text()}`);
+ }
+
+ const data = await response.json();
+ return data.projects || [];
+ } catch (error) {
+ console.error('Error loading active projects:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Retrieves related resources for a document or draft.
+ *
+ * @param fileID - The unique identifier of the document or draft.
+ * @param isDraft - Whether the document is a draft (true) or published document (false).
+ * @returns A promise that resolves to the related resources response.
+ * @throws Will throw an error if the request fails or the response is not OK.
+ */
+ public async getRelatedResources(fileID: string, isDraft: boolean): Promise {
+ try {
+ const endpoint = `${this.baseUrl}/api/v2/${isDraft ? "drafts" : "documents"}/${fileID}/related-resources`;
+ const headers = this.reqHeader;
+
+ const res = await fetch(endpoint, headers);
+ if (res.ok) {
+ return await res.json();
+ }
+
+ // Return empty response for 404 (no related resources found)
+ if (res.status === 404) {
+ return {
+ externalLinks: [],
+ hermesDocuments: []
+ };
+ }
+
+ // Provide specific error messages for common HTTP status codes
+ let errorMessage = `Failed to load related resources (${res.status})`;
+ switch (res.status) {
+ case 401:
+ errorMessage = "Authentication required - please log in";
+ break;
+ case 403:
+ errorMessage = "Access denied - you don't have permission to view this document";
+ break;
+ case 500:
+ errorMessage = "Server error - please try again later";
+ break;
+ }
+
+ throw new Error(errorMessage);
+ } catch (error) {
+ console.log("getRelatedResources has failed with err:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Updates related resources for a document or draft.
+ *
+ * @param fileID - The unique identifier of the document or draft.
+ * @param isDraft - Whether the document is a draft (true) or published document (false).
+ * @param resources - The related resources to update.
+ * @returns A promise that resolves when the update is successful.
+ * @throws Will throw an error if the request fails or the response is not OK.
+ */
+ public async updateRelatedResources(
+ fileID: string,
+ isDraft: boolean,
+ resources: RelatedResourcesUpdateRequest
+ ): Promise {
+ try {
+ const endpoint = `${this.baseUrl}/api/v2/${isDraft ? "drafts" : "documents"}/${fileID}/related-resources`;
+ const headers = {
+ ...this.reqHeader,
+ method: "PUT",
+ headers: {
+ ...(this.reqHeader.headers || {}),
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(resources),
+ };
+
+ const res = await fetch(endpoint, headers);
+ if (res.ok) {
+ return;
+ }
+
+ // Provide specific error messages for common HTTP status codes
+ let errorMessage = `Failed to update related resources (${res.status})`;
+ switch (res.status) {
+ case 401:
+ errorMessage = "Authentication required - please log in";
+ break;
+ case 403:
+ errorMessage = "Access denied - only document owners can edit related resources";
+ break;
+ case 400:
+ errorMessage = "Invalid data - please check your input";
+ break;
+ case 404:
+ errorMessage = "Document not found";
+ break;
+ case 500:
+ errorMessage = "Server error - please try again later";
+ break;
+ }
+
+ throw new Error(errorMessage);
+ } catch (error) {
+ console.log("updateRelatedResources has failed with err:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Retrieves project details for the given project IDs.
+ *
+ * @param projectIds - Array of project IDs to get details for
+ * @returns A promise that resolves to an array of project details
+ */
+ public async getProjectsByIds(projectIds: number[]): Promise {
+ if (!projectIds || projectIds.length === 0) {
+ return [];
+ }
+
+ try {
+ // Fetch each project individually since there's no bulk endpoint
+ const projectPromises = projectIds.map(async (projectId) => {
+ const response = await fetch(
+ `${this.baseUrl}/api/v2/projects/${projectId}`,
+ {
+ ...this.reqHeader,
+ method: "GET",
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ console.warn(`Project ${projectId} not found`);
+ return null;
+ }
+ throw new Error(`Failed to load project ${projectId}: ${response.status}`);
+ }
+
+ return await response.json();
+ });
+
+ const results = await Promise.all(projectPromises);
+ return results.filter((project): project is HermesProject => project !== null);
+ } catch (error) {
+ console.error('Error loading projects by IDs:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Retrieves related resources for a project.
+ *
+ * @param projectId - The project ID to get related resources for
+ * @returns A promise that resolves to the project's related resources
+ */
+ public async getProjectRelatedResources(projectId: string): Promise {
+ try {
+ const response = await fetch(
+ `${this.baseUrl}/api/v2/projects/${projectId}/related-resources`,
+ {
+ ...this.reqHeader,
+ method: "GET",
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return { hermesDocuments: [], externalLinks: [] };
+ }
+ throw new Error(`Failed to load project related resources: ${response.status}`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error loading project related resources:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Gets the archived status of a draft document.
+ *
+ * @param fileID - The document ID to check
+ * @returns A promise that resolves with the archived status
+ */
+ public async getDraftArchivedStatus(fileID: string): Promise<{ archived: boolean }> {
+ try {
+ const endpoint = `${this.baseUrl}/api/v2/drafts/${fileID}/archived`;
+ const response = await fetch(endpoint, this.reqHeader);
+
+ if (!response.ok) {
+ throw new Error(`Failed to get archived status: ${response.status}`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error getting archived status:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Sets the archived status of a draft document.
+ *
+ * @param fileID - The document ID to update
+ * @param archived - The new archived status (true to archive, false to unarchive)
+ * @returns A promise that resolves when the status is updated
+ */
+ public async setDraftArchivedStatus(fileID: string, archived: boolean): Promise {
+ try {
+ const endpoint = `${this.baseUrl}/api/v2/drafts/${fileID}/archived`;
+ const response = await fetch(endpoint, {
+ ...this.reqHeader,
+ method: 'PATCH',
+ body: JSON.stringify({ archived }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`Failed to update archived status: ${response.status} - ${errorText}`);
+ }
+ } catch (error) {
+ console.error('Error setting archived status:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Adds a document to a project by updating the project's related resources.
+ *
+ * @param documentId - The document ID to add to the project
+ * @param projectId - The project ID to add the document to
+ * @returns A promise that resolves when the document is added to the project
+ */
+ public async addDocumentToProject(documentId: string, projectId: string): Promise {
+ try {
+ // First, get current related resources
+ const currentResources = await this.getProjectRelatedResources(projectId);
+
+ // Check if document is already in the project
+ const existingDoc = currentResources.hermesDocuments?.find((doc: any) => doc.FileID === documentId);
+ if (existingDoc) {
+ console.log('Document is already associated with this project');
+ return;
+ }
+
+ // Add the document to the project's related resources
+ // Transform existing documents to only include FileID and sortOrder
+ const existingDocs = (currentResources.hermesDocuments || []).map((doc: any) => ({
+ FileID: doc.FileID,
+ sortOrder: doc.sortOrder
+ }));
+
+ // Calculate next sort order by finding the maximum existing sort order across ALL resources
+ const allExistingSortOrders = [
+ ...existingDocs.map(doc => doc.sortOrder),
+ ...(currentResources.externalLinks || []).map((link: any) => link.sortOrder)
+ ];
+
+ const maxSortOrder = allExistingSortOrders.length > 0
+ ? Math.max(...allExistingSortOrders)
+ : 0;
+
+ const updatedHermesDocuments = [
+ ...existingDocs,
+ {
+ FileID: documentId,
+ sortOrder: maxSortOrder + 1
+ }
+ ];
+
+ const response = await fetch(
+ `${this.baseUrl}/api/v2/projects/${projectId}/related-resources`,
+ {
+ ...this.reqHeader,
+ method: "PUT",
+ body: JSON.stringify({
+ hermesDocuments: updatedHermesDocuments,
+ externalLinks: currentResources.externalLinks || []
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ let errorMessage = `Failed to add document to project (${response.status})`;
+
+ switch (response.status) {
+ case 401:
+ errorMessage = "Authentication required - please log in";
+ break;
+ case 403:
+ errorMessage = "Access denied - insufficient permissions";
+ break;
+ case 404:
+ errorMessage = "Document or project not found";
+ break;
+ case 500:
+ errorMessage = "Server error - please try again later";
+ break;
+ }
+
+ throw new Error(errorMessage);
+ }
+ } catch (error) {
+ console.error('Error adding document to project:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Removes a document from a project by updating the project's related resources.
+ *
+ * @param documentId - The document ID to remove from the project
+ * @param projectId - The project ID to remove the document from
+ * @returns A promise that resolves when the document is removed from the project
+ */
+ public async removeDocumentFromProject(documentId: string, projectId: string): Promise {
+ try {
+ // First, get current related resources
+ const currentResources = await this.getProjectRelatedResources(projectId);
+
+ // Filter out the document to remove and transform to only include FileID and sortOrder
+ const updatedHermesDocuments = (currentResources.hermesDocuments || [])
+ .filter((doc: any) => doc.FileID !== documentId)
+ .map((doc: any) => ({
+ FileID: doc.FileID,
+ sortOrder: doc.sortOrder
+ }));
+
+ // Check if document was actually removed
+ if (updatedHermesDocuments.length === (currentResources.hermesDocuments?.length || 0)) {
+ console.log('Document was not found in project - nothing to remove');
+ return;
+ }
+
+ const response = await fetch(
+ `${this.baseUrl}/api/v2/projects/${projectId}/related-resources`,
+ {
+ ...this.reqHeader,
+ method: "PUT",
+ body: JSON.stringify({
+ hermesDocuments: updatedHermesDocuments,
+ externalLinks: currentResources.externalLinks || []
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ let errorMessage = `Failed to remove document from project (${response.status})`;
+
+ switch (response.status) {
+ case 401:
+ errorMessage = "Authentication required - please log in";
+ break;
+ case 403:
+ errorMessage = "Access denied - insufficient permissions";
+ break;
+ case 404:
+ errorMessage = "Document or project not found";
+ break;
+ case 500:
+ errorMessage = "Server error - please try again later";
+ break;
+ }
+
+ throw new Error(errorMessage);
+ }
+ } catch (error) {
+ console.error('Error removing document from project:', error);
+ throw error;
+ }
+ }
+}
diff --git a/hermes-plugin/src/taskpane/utils/lightTheme.ts b/hermes-plugin/src/taskpane/utils/lightTheme.ts
new file mode 100644
index 000000000..88889d96e
--- /dev/null
+++ b/hermes-plugin/src/taskpane/utils/lightTheme.ts
@@ -0,0 +1,170 @@
+/**
+ * Light Mode Theme Constants for Hermes Word Add-in
+ * Mirrors the structure of DarkTheme so components can swap themes seamlessly.
+ */
+
+export const LightTheme = {
+ // Primary background colors
+ background: {
+ primary: "#ffffff", // White main background
+ secondary: "#f5f5f5", // Slightly off-white for cards/panels
+ tertiary: "#ebebeb", // Hover states
+ elevated: "#ffffff", // Elevated surfaces like modals
+ subtle: "#fafafa", // Subtle variations
+ error: "#fef2f2", // Error container background
+ success: "#22c55e", // Success indicator background
+ },
+
+ // Text colors with proper contrast
+ text: {
+ primary: "#111827", // Near-black primary text
+ secondary: "#374151", // Dark gray secondary text
+ tertiary: "#6b7280", // Mid-gray labels/hints
+ disabled: "#9ca3af", // Disabled state text
+ inverse: "#ffffff", // White text for dark backgrounds
+ placeholder: "#9ca3af", // Placeholder/empty state text
+ error: "#dc2626", // Error message text
+ link: "#0078d4", // Hyperlink text
+ },
+
+ // Border and divider colors
+ border: {
+ primary: "#d1d5db", // Main border color
+ secondary: "#9ca3af", // Darker borders for hover
+ subtle: "#e5e7eb", // Very subtle borders
+ focus: "#0078d4", // Focus states (Microsoft blue)
+ },
+
+ // Interactive element colors (same as dark for brand consistency)
+ interactive: {
+ primary: "#0078d4",
+ primaryHover: "#106ebe",
+ secondary: "#e5e7eb",
+ secondaryHover: "#d1d5db",
+ danger: "#dc2626",
+ dangerHover: "#b91c1c",
+ success: "#16a34a",
+ successHover: "#15803d",
+ warning: "#d97706",
+ warningHover: "#b45309",
+ },
+
+ // Status-specific colors (matching Ember frontend HDS design system)
+ status: {
+ approved: {
+ background: "#f2fbf6", // HDS success surface
+ text: "#00781e", // HDS success foreground
+ border: "#86efac",
+ },
+ inReview: {
+ background: "#f9f2ff", // HDS highlight surface
+ text: "#911ced", // HDS highlight foreground
+ border: "#d8b4fe",
+ },
+ draft: {
+ background: "#f1f2f3", // HDS neutral surface
+ text: "#3b3d45", // HDS neutral foreground
+ border: "#d1d5db",
+ },
+ obsolete: {
+ background: "#f1f2f3", // HDS neutral surface
+ text: "#3b3d45", // HDS neutral foreground
+ border: "#d1d5db",
+ },
+ warning: {
+ background: "#fff7ed",
+ text: "#ea580c",
+ border: "#fdba74",
+ },
+ },
+
+ // State overlays
+ states: {
+ hover: "rgba(0, 0, 0, 0.06)",
+ focus: "0 0 0 2px #0078d4",
+ active: "rgba(0, 0, 0, 0.12)",
+ disabled: "rgba(0, 0, 0, 0.15)",
+ },
+
+ // Shadows and elevation
+ shadows: {
+ small: "0 1px 3px rgba(0, 0, 0, 0.1)",
+ medium: "0 2px 8px rgba(0, 0, 0, 0.12)",
+ large: "0 4px 16px rgba(0, 0, 0, 0.15)",
+ },
+
+ // Gradients
+ gradients: {
+ subtle: "linear-gradient(135deg, #ffffff 0%, #f5f5f5 100%)",
+ accent: "linear-gradient(135deg, #0078d4 0%, #106ebe 100%)",
+ },
+
+ // Component-specific colors
+ components: {
+ card: {
+ background: "#ffffff",
+ border: "#e5e7eb",
+ shadow: "0 1px 3px rgba(0, 0, 0, 0.1)",
+ },
+ input: {
+ background: "#ffffff",
+ border: "#d1d5db",
+ focusBorder: "#0078d4",
+ text: "#111827",
+ placeholder: "#9ca3af",
+ },
+ dropdown: {
+ background: "#ffffff",
+ border: "#d1d5db",
+ optionHover: "#f3f4f6",
+ text: "#111827",
+ },
+ button: {
+ primary: {
+ background: "#0078d4",
+ backgroundHover: "#106ebe",
+ text: "#ffffff",
+ },
+ secondary: {
+ background: "transparent",
+ backgroundHover: "#f3f4f6",
+ text: "#374151",
+ border: "#d1d5db",
+ },
+ danger: {
+ background: "transparent",
+ backgroundHover: "#fef2f2",
+ text: "#dc2626",
+ border: "#dc2626",
+ },
+ },
+ badge: {
+ background: "#f3f4f6",
+ text: "#374151",
+ border: "#d1d5db",
+ },
+ progressBar: {
+ background: "#e5e7eb",
+ fill: "#0078d4",
+ },
+ tooltip: {
+ background: "#ffffff",
+ text: "#111827",
+ border: "#d1d5db",
+ },
+ header: {
+ background: "#ffffff",
+ border: "#e5e7eb",
+ text: "#111827",
+ linkColor: "#0078d4",
+ linkHover: "#106ebe",
+ },
+ footer: {
+ background: "#ffffff",
+ border: "#e5e7eb",
+ text: "#6b7280",
+ },
+ },
+};
+
+export default LightTheme;
diff --git a/hermes-plugin/src/taskpane/utils/productUtils.ts b/hermes-plugin/src/taskpane/utils/productUtils.ts
new file mode 100644
index 000000000..6a20e5992
--- /dev/null
+++ b/hermes-plugin/src/taskpane/utils/productUtils.ts
@@ -0,0 +1,153 @@
+/**
+ * Get value from the provided `options` based on the hash of `source`.
+ * Ported from Ember.js web implementation.
+ *
+ * @param source String to hash.
+ * @param options Collection of options to get value from.
+ */
+export function hashValue(source: string, options: T[]): T | null {
+ let hash = 0;
+ if (source.length === 0 || options.length === 0) {
+ return null;
+ }
+ for (let i = 0; i < source.length; i++) {
+ hash = source.charCodeAt(i) + ((hash << 5) - hash);
+ hash = hash & hash;
+ }
+ hash = ((hash % options.length) + options.length) % options.length;
+ const res = options[hash];
+ if (res === undefined) {
+ return null;
+ }
+ return res as T;
+}
+
+/**
+ * Get product ID for known HashiCorp products.
+ * Returns the product identifier used for icon mapping.
+ * Ported from Ember.js web implementation.
+ */
+export function getProductId(productName?: string): string | undefined {
+ if (!productName) {
+ return undefined;
+ }
+ let product = productName.toLowerCase();
+
+ switch (product) {
+ case "boundary":
+ case "consul":
+ case "nomad":
+ case "packer":
+ case "terraform":
+ case "vagrant":
+ case "vault":
+ case "waypoint":
+ return product;
+ case "cloud platform":
+ return "hcp";
+ default:
+ return undefined;
+ }
+}
+
+// HDS color palette - same as Ember.js web implementation
+const HDS_COLORS = [
+ "#3b3d45",
+ "#656a76",
+ "#c2c5cb",
+ "#dedfe3",
+
+ "#51130a",
+ "#940004",
+ "#c00005",
+ "#fbd4d4",
+
+ "#542800",
+ "#803d00",
+ "#9e4b00",
+ "#bb5a00",
+ "#fbeabf",
+
+ "#054220",
+ "#006619",
+ "#00781e",
+ "#cceeda",
+
+ "#42215b",
+ "#7b00db",
+ "#911ced",
+ "#ead2fe",
+
+ "#1c345f",
+ "#0046d1",
+ "#0c56e9",
+ "#1060ff",
+ "#cce3fe",
+];
+
+const EXTENDED_COLORS = [
+ "#ffd814",
+ "#feec7b",
+ "#fff9cf",
+
+ "#d01c5b",
+ "#ffcede",
+
+ "#008196",
+ "#62d4dc",
+
+ "#63d0ff",
+ "#d4f2ff",
+
+ "#60dea9",
+ "#d3fdeb",
+];
+
+/**
+ * Get a hash-based color for a product area.
+ * Used to provide consistent colors for product areas.
+ * Ported from Ember.js web implementation.
+ */
+export function getProductColor(product?: string): string | null {
+ if (!product) {
+ return null;
+ }
+
+ return hashValue(product, [...HDS_COLORS, ...EXTENDED_COLORS]);
+}
+
+/**
+ * Get font color (black or white) that contrasts with the given background color.
+ * Simple implementation for readability.
+ */
+export function getContrastColor(backgroundColor: string): string {
+ // Remove # if present
+ const hex = backgroundColor.replace('#', '');
+
+ // Convert to RGB
+ const r = parseInt(hex.substr(0, 2), 16);
+ const g = parseInt(hex.substr(2, 2), 16);
+ const b = parseInt(hex.substr(4, 2), 16);
+
+ // Calculate luminance
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
+
+ // Return black for light backgrounds, white for dark
+ return luminance > 0.5 ? '#000000' : '#ffffff';
+}
+
+/**
+ * Convert a string to dasherized format (kebab-case).
+ * Similar to Ember.js dasherize function.
+ * Examples: "Cloud Infrastructure" -> "cloud-infrastructure"
+ * "Terraform" -> "terraform"
+ */
+export function dasherize(str: string): string {
+ return str
+ .replace(/([a-z\d])([A-Z])/g, '$1-$2') // Add dash before capital letters
+ .replace(/\s+/g, '-') // Replace spaces with dashes
+ .toLowerCase() // Convert to lowercase
+ .replace(/[^a-z0-9\-]/g, '') // Remove non-alphanumeric chars except dashes
+ .replace(/-+/g, '-') // Replace multiple dashes with single dash
+ .replace(/^-|-$/g, ''); // Remove leading/trailing dashes
+}
\ No newline at end of file
diff --git a/hermes-plugin/src/taskpane/utils/storageAccess.ts b/hermes-plugin/src/taskpane/utils/storageAccess.ts
new file mode 100644
index 000000000..0dfe0baf3
--- /dev/null
+++ b/hermes-plugin/src/taskpane/utils/storageAccess.ts
@@ -0,0 +1,149 @@
+/**
+ * Storage Access API utilities for handling third-party cookie access in Office Add-ins.
+ * This enables the add-in (running in a third-party iframe) to access cookies
+ * set during first-party popup authentication (ALB OIDC session cookie).
+ */
+
+const STORAGE_ACCESS_GRANTED_KEY = 'storageAccessGranted';
+const LAST_AUTH_CHECK_KEY = 'lastAuthCheck';
+
+export async function hasStorageAccess(): Promise {
+ if (!document.hasStorageAccess) {
+ return true;
+ }
+
+ try {
+ return await document.hasStorageAccess();
+ } catch {
+ return false;
+ }
+}
+
+export function wasStorageAccessPreviouslyGranted(): boolean {
+ try {
+ return localStorage.getItem(STORAGE_ACCESS_GRANTED_KEY) === 'true';
+ } catch {
+ return false;
+ }
+}
+
+function markStorageAccessGranted(): void {
+ try {
+ localStorage.setItem(STORAGE_ACCESS_GRANTED_KEY, 'true');
+ localStorage.setItem(LAST_AUTH_CHECK_KEY, Date.now().toString());
+ } catch {}
+}
+
+export function clearStorageAccessFlag(): void {
+ try {
+ localStorage.removeItem(STORAGE_ACCESS_GRANTED_KEY);
+ localStorage.removeItem(LAST_AUTH_CHECK_KEY);
+ } catch {}
+}
+
+export async function verifyStorageAccessWorks(baseUrl: string): Promise {
+ try {
+ const response = await fetch(`${baseUrl}/api/v2/me`, {
+ credentials: 'include',
+ headers: { 'Accept': 'application/json' },
+ redirect: 'manual'
+ });
+
+ if (response.type === 'opaqueredirect' || response.status === 0) {
+ clearStorageAccessFlag();
+ return false;
+ }
+
+ if (response.ok) {
+ markStorageAccessGranted();
+ return true;
+ }
+
+ clearStorageAccessFlag();
+ return false;
+ } catch {
+ clearStorageAccessFlag();
+ return false;
+ }
+}
+
+export async function requestStorageAccess(baseUrl?: string): Promise {
+ if (!document.requestStorageAccess) {
+ if (baseUrl) {
+ return await verifyStorageAccessWorks(baseUrl);
+ }
+ markStorageAccessGranted();
+ return true;
+ }
+
+ try {
+ await document.requestStorageAccess();
+ if (baseUrl) {
+ return await verifyStorageAccessWorks(baseUrl);
+ }
+ markStorageAccessGranted();
+ return true;
+ } catch {
+ try {
+ const alreadyHasAccess = await document.hasStorageAccess();
+ if (alreadyHasAccess) {
+ if (baseUrl) {
+ return await verifyStorageAccessWorks(baseUrl);
+ }
+ markStorageAccessGranted();
+ return true;
+ }
+ } catch {}
+ clearStorageAccessFlag();
+ return false;
+ }
+}
+
+export async function tryRestoreStorageAccess(baseUrl?: string): Promise {
+ if (!wasStorageAccessPreviouslyGranted()) {
+ return false;
+ }
+
+ const currentAccess = await hasStorageAccess();
+ if (currentAccess) {
+ if (baseUrl) {
+ const verified = await verifyStorageAccessWorks(baseUrl);
+ if (!verified) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ try {
+ await document.requestStorageAccess();
+ if (baseUrl) {
+ const verified = await verifyStorageAccessWorks(baseUrl);
+ if (verified) {
+ return true;
+ }
+ return false;
+ }
+ markStorageAccessGranted();
+ return true;
+ } catch {
+ clearStorageAccessFlag();
+ return false;
+ }
+}
+
+export async function fetchWithStorageAccess(
+ url: string,
+ options: RequestInit = {}
+): Promise {
+ const hasAccess = await hasStorageAccess();
+ if (!hasAccess) {
+ throw new Error('Storage access is not available; cannot perform authenticated fetch.');
+ }
+ return fetch(url, { ...options, credentials: 'include' });
+}
+
+export function isStorageAccessApiSupported(): boolean {
+ return typeof document.hasStorageAccess === 'function' &&
+ typeof document.requestStorageAccess === 'function';
+}
diff --git a/hermes-plugin/src/taskpane/utils/themeContext.tsx b/hermes-plugin/src/taskpane/utils/themeContext.tsx
new file mode 100644
index 000000000..6205ff927
--- /dev/null
+++ b/hermes-plugin/src/taskpane/utils/themeContext.tsx
@@ -0,0 +1,46 @@
+import * as React from "react";
+
+interface ThemeContextValue {
+ isDark: boolean;
+ toggleTheme: () => void;
+}
+
+export const ThemeContext = React.createContext({
+ isDark: true,
+ toggleTheme: () => {},
+});
+
+export const useTheme = () => React.useContext(ThemeContext);
+
+interface ThemeProviderProps {
+ children: React.ReactNode;
+}
+
+export const ThemeProvider: React.FC = ({ children }) => {
+ const [isDark, setIsDark] = React.useState(() => {
+ try {
+ const saved = localStorage.getItem("hermes-plugin-theme");
+ return saved !== null ? saved === "dark" : true; // default to dark
+ } catch {
+ return true;
+ }
+ });
+
+ const toggleTheme = React.useCallback(() => {
+ setIsDark((prev) => {
+ const next = !prev;
+ try {
+ localStorage.setItem("hermes-plugin-theme", next ? "dark" : "light");
+ } catch {
+ // ignore storage errors
+ }
+ return next;
+ });
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/hermes-plugin/src/taskpane/utils/timeAgo.ts b/hermes-plugin/src/taskpane/utils/timeAgo.ts
new file mode 100644
index 000000000..b26a64eec
--- /dev/null
+++ b/hermes-plugin/src/taskpane/utils/timeAgo.ts
@@ -0,0 +1,20 @@
+export default function timeAgo(timestamp: number) {
+ const now = Date.now();
+ const diffMs = now - timestamp; // difference in milliseconds
+
+ const seconds = Math.floor(diffMs / 1000);
+ const minutes = Math.floor(seconds / 60);
+ const hours = Math.floor(minutes / 60);
+ const days = Math.floor(hours / 24);
+ const weeks = Math.floor(days / 7);
+ const months = Math.floor(days / 30); // rough estimate
+ const years = Math.floor(days / 365); // rough estimate
+
+ if (seconds < 60) return `${seconds} second${seconds !== 1 ? "s" : ""} ago`;
+ if (minutes < 60) return `${minutes} minute${minutes !== 1 ? "s" : ""} ago`;
+ if (hours < 24) return `${hours} hour${hours !== 1 ? "s" : ""} ago`;
+ if (days < 7) return `${days} day${days !== 1 ? "s" : ""} ago`;
+ if (weeks < 5) return `${weeks} week${weeks !== 1 ? "s" : ""} ago`;
+ if (months < 12) return `${months} month${months !== 1 ? "s" : ""} ago`;
+ return `${years} year${years !== 1 ? "s" : ""} ago`;
+}
diff --git a/hermes-plugin/src/taskpane/utils/wordPluginController.ts b/hermes-plugin/src/taskpane/utils/wordPluginController.ts
new file mode 100644
index 000000000..46d0c70a8
--- /dev/null
+++ b/hermes-plugin/src/taskpane/utils/wordPluginController.ts
@@ -0,0 +1,1024 @@
+import CurrentUser from "../interfaces/currentUser";
+import IDocumentMetadata from "../interfaces/documentMetadata";
+import { Person } from "../interfaces/person";
+import { Group } from "../interfaces/group";
+import IProduct from "../interfaces/products";
+import HermesClient from "./hermesClient";
+import WordService from "./wordService";
+import { dasherize } from "./productUtils";
+import {
+ RelatedResource,
+ RelatedResourcesResponse,
+ formatResourcesForUpdate,
+ combineAndSortResources
+} from "../interfaces/relatedResources";
+
+export enum DocumentManageStatus {
+ Loading,
+ NotManaged,
+ Managed,
+ Error,
+ AuthenticationRequired,
+}
+
+export type HermesProperties = {
+ fileID?: string;
+};
+
+export default class WordPluginController {
+ private document: IDocumentMetadata;
+ private currentUser: CurrentUser;
+ products: Record;
+ constructor(
+ private hermesClient: HermesClient,
+ private docService: WordService
+ ) { }
+
+ /**
+ * Checks whether the current document is managed by Hermes.
+ *
+ * This method retrieves the document properties and attempts to extract Hermes-specific properties.
+ * If the document contains a valid Hermes file ID, it fetches the document details from the Hermes client.
+ * Returns the appropriate {@link DocumentManageStatus} based on the outcome:
+ * - `DocumentManageStatus.Managed` if the document is managed,
+ * - `DocumentManageStatus.NotManaged` if not managed or properties are missing,
+ * - `DocumentManageStatus.Error` if an error occurs during the process.
+ *
+ * @returns {Promise} A promise that resolves to the document's management status.
+ */
+ async checkDocumentManageStatus(): Promise {
+ try {
+ // Try to get FileID from keywords first (primary method)
+ const docProps = await this.docService.getDocProperties();
+ const hermesProps = this.extractHermesProperties(docProps);
+
+ let fileID = hermesProps.fileID;
+
+ // If not found in keywords, try custom properties (alternative method)
+ if (!fileID) {
+ console.log("FileID not found in keywords, checking custom properties...");
+ fileID = await this.getFileIDFromCustomProperties();
+ }
+
+ // If still no FileID found, document is not managed
+ if (!fileID) {
+ console.log("No FileID found in document properties");
+ return DocumentManageStatus.NotManaged;
+ }
+
+ console.log("Found FileID:", fileID);
+
+ // Make document details call first, then parallel calls for products, user details, and archived status
+ const doc = await this.hermesClient.getDocumentDetails(fileID);
+ if (doc === null || typeof doc === "undefined") {
+ return DocumentManageStatus.NotManaged;
+ }
+
+ this.document = doc;
+
+ // Helper function to safely execute async operations
+ const safeAsync = async (operation: () => Promise, fallback?: T) => {
+ try {
+ return { success: true, value: await operation(), error: null };
+ } catch (error) {
+ return { success: false, value: fallback, error };
+ }
+ };
+
+ // Make products, current user, and archived status calls in parallel to improve performance
+ const [productsResult, currentUserResult, archivedResult] = await Promise.all([
+ safeAsync(() => this.hermesClient.getProducts(), {}),
+ safeAsync(() => this.hermesClient.getCurrentUserDetails()),
+ // Load archived status only for drafts
+ doc._isDraft ? safeAsync(() => this.hermesClient.getDraftArchivedStatus(fileID), { archived: false }) : Promise.resolve({ success: true, value: { archived: false }, error: null })
+ ]);
+
+ // Handle products result
+ if (productsResult.success) {
+ this.products = productsResult.value;
+ } else {
+ console.warn('Failed to load products:', productsResult.error);
+ this.products = {}; // Fallback to empty object
+ }
+
+ // Handle current user result
+ if (currentUserResult.success) {
+ this.currentUser = currentUserResult.value;
+ } else {
+ console.error('Failed to load current user details:', currentUserResult.error);
+ // Don't set a fallback for currentUser as it might be required for auth
+ throw currentUserResult.error;
+ }
+
+ // Handle archived status result
+ if (archivedResult.success && doc._isDraft) {
+ this.document.archived = archivedResult.value.archived;
+ }
+
+ return DocumentManageStatus.Managed;
+ } catch (error: any) {
+ console.log('checkDocumentManageStatus error:', error);
+
+ // Check if this is a 401 authentication error
+ if (this.isAuthenticationError(error)) {
+ console.log('Authentication required - 401 detected');
+ return DocumentManageStatus.AuthenticationRequired;
+ }
+
+ return DocumentManageStatus.Error;
+ } finally {
+ }
+ }
+
+ /**
+ * Checks if an error is a 401 authentication error.
+ *
+ * @param error The error to check
+ * @returns true if it's a 401 authentication error or redirect to login
+ */
+ private isAuthenticationError(error: any): boolean {
+ // Check for fetch Response with status 401
+ if (error?.status === 401) return true;
+
+ // Check for opaqueredirect response (redirect: 'manual' returns this)
+ // This happens when ALB OIDC returns 302 to IdP
+ if (error?.type === 'opaqueredirect') return true;
+ if (error?.status === 0 && error?.type === 'opaqueredirect') return true;
+
+ // Check for status 0 which can indicate a blocked/redirected request
+ if (error?.status === 0) return true;
+
+ // Check for 302 redirect status
+ if (error?.status === 302) return true;
+
+ // Check for error message containing 401 or redirect indicators
+ if (error?.message && (
+ error.message.includes('401') ||
+ error.message.includes('302') ||
+ error.message.includes('NetworkError') ||
+ error.message.includes('CORS')
+ )) return true;
+
+ // Check for response.status in nested error
+ if (error?.response?.status === 401) return true;
+ if (error?.response?.status === 302) return true;
+ if (error?.response?.type === 'opaqueredirect') return true;
+
+ return false;
+ }
+
+ /**
+ * Gets the base URL for opening the Hermes application.
+ *
+ * @returns The base URL of the Hermes application
+ */
+ getHermesBaseUrl(): string {
+ return this.hermesClient['baseUrl'];
+ }
+
+ /**
+ * Gets the current user's email address.
+ *
+ * @returns The current user's email address or empty string if not available
+ */
+ getCurrentUserEmail(): string {
+ return this.currentUser?.email || "";
+ }
+
+ /**
+ * Gets the URL for a specific product area page.
+ *
+ * @param productName - The name of the product/area (e.g., "Terraform", "Cloud Infrastructure")
+ * @returns The complete URL to the product area page
+ */
+ getProductAreaUrl(productName: string): string {
+ const baseUrl = this.getHermesBaseUrl();
+ const dasherizedProduct = dasherize(productName);
+ return `${baseUrl}/product-areas/${dasherizedProduct}`;
+ }
+
+ /**
+ * Gets the URL for documents page filtered by owner.
+ *
+ * @param ownerEmail - The email of the owner to filter by
+ * @returns The complete URL to the documents page filtered by the owner
+ */
+ getDocumentsByOwnerUrl(ownerEmail: string): string {
+ const baseUrl = this.getHermesBaseUrl();
+ // Create URL-encoded JSON array format: ["email@domain.com"] -> %5B%22email@domain.com%22%5D
+ const encodedOwners = encodeURIComponent(JSON.stringify([ownerEmail]));
+ return `${baseUrl}/documents?owners=${encodedOwners}`;
+ }
+
+ /**
+ * Gets the metadata associated with the current document.
+ *
+ * @returns The metadata of the document as an `IDocumentMetadata` object.
+ */
+ get documentMetadata(): IDocumentMetadata {
+ return this.document;
+ }
+
+ /**
+ * Extracts Hermes-specific properties from the given Word document properties.
+ *
+ * This function parses the `keywords` property of the provided `props` object,
+ * searching for a "FileID" entry in the format "FileID: " within a
+ * semicolon-separated list. If found, it assigns the value to the `fileID`
+ * property of the returned `HermesProperties` object.
+ *
+ * @param props - The Word document properties data to extract Hermes properties from.
+ * @returns An object containing the extracted Hermes properties, such as `fileID`.
+ */
+ extractHermesProperties(props: Word.Interfaces.DocumentPropertiesData) {
+ const hermesProps = {} as HermesProperties;
+ if (props.keywords) {
+ const { keywords } = props;
+ if (keywords.includes("FileID:")) {
+ const splittedString = keywords.split("; ");
+ for (const str of splittedString) {
+ const [key, value] = str.split(": ");
+
+ if (key === "FileID" && value) {
+ // Clean any trailing semicolons or whitespace from the value
+ hermesProps.fileID = value.replace(/[;\s]+$/, "").trim();
+ }
+ }
+ }
+ }
+ return hermesProps;
+ }
+
+ /**
+ * Attempts to get the Hermes FileID from custom document properties.
+ * This provides an alternative method to retrieve the FileID when it's stored
+ * as a custom property (HermesFileID) rather than in keywords.
+ *
+ * @returns {Promise} The FileID if found, null otherwise
+ */
+ async getFileIDFromCustomProperties(): Promise {
+ try {
+ return await Word.run(async (context) => {
+ const customProps = context.document.properties.customProperties;
+ customProps.load("items");
+ await context.sync();
+
+ // Look for HermesFileID custom property
+ for (const prop of customProps.items) {
+ prop.load("key,value");
+ }
+ await context.sync();
+
+ for (const prop of customProps.items) {
+ if (prop.key === "HermesFileID" && prop.value) {
+ console.log("Found HermesFileID in custom properties:", prop.value);
+ return prop.value;
+ }
+ }
+
+ return null;
+ });
+ } catch (error) {
+ console.error("Error reading custom properties:", error);
+ return null;
+ }
+ }
+
+ /**
+ * Checks if the document is managed by Hermes by looking for the HermesManaged custom property.
+ *
+ * @returns {Promise} True if the document has HermesManaged property set to "true"
+ */
+ async isHermesManagedDocument(): Promise {
+ try {
+ return await Word.run(async (context) => {
+ const customProps = context.document.properties.customProperties;
+ customProps.load("items");
+ await context.sync();
+
+ // Load all property keys and values
+ for (const prop of customProps.items) {
+ prop.load("key,value");
+ }
+ await context.sync();
+
+ // Check for HermesManaged property
+ for (const prop of customProps.items) {
+ if (prop.key === "HermesManaged" && prop.value === "true") {
+ console.log("Document is marked as Hermes-managed");
+ return true;
+ }
+ }
+
+ return false;
+ });
+ } catch (error) {
+ console.error("Error checking HermesManaged property:", error);
+ return false;
+ }
+ }
+
+ /**
+ * Renders a table in the document by updating its headers.
+ *
+ * This asynchronous method calls the `updateDocumentHeaders` function of the `docService`
+ * with the current document. If an error occurs during the update, it logs the error
+ * message to the console.
+ *
+ * @returns {Promise} A promise that resolves when the table rendering is complete.
+ */
+ async renderTable() {
+ try {
+ // Enrich document with project details if projects exist
+ const enrichedDocument = { ...this.document };
+
+ if (this.document.projects && this.document.projects.length > 0) {
+ try {
+ const projectDetails = await this.hermesClient.getProjectsByIds(this.document.projects);
+ enrichedDocument.projectDetails = projectDetails.map(project => ({
+ id: project.id,
+ title: project.title
+ }));
+ } catch (error) {
+ console.log("Error loading project details for header:", error);
+ // Continue without project details - will show IDs instead
+ }
+ }
+
+ // Add base URL for project hyperlinks
+ enrichedDocument.baseUrl = this.hermesClient['baseUrl'];
+
+ await this.docService.updateDocumentHeaders(enrichedDocument);
+ } catch (error) {
+ console.log("Error occured during renderTable: ", error);
+ }
+ }
+
+ /**
+ * Retrieves a mapping of email addresses to their corresponding `Person` objects.
+ *
+ * Given an array of email addresses, this method fetches detailed information
+ * for each email using the `hermesClient` and constructs a `Map` where each key
+ * is an email address and the value is the associated `Person` object.
+ *
+ * @param emails - An array of email addresses to fetch details for.
+ * @returns A promise that resolves to a `Map` mapping email addresses to `Person` objects.
+ * @throws Logs an error to the console if fetching or mapping fails, but does not throw.
+ */
+ async getEmailToPersonMap(emails: string[]): Promise> {
+ const emailToPersonMap = new Map();
+ try {
+ const users = await this.hermesClient.getPeopleDetailsFromEmail(emails);
+ for (const user of users) {
+ emailToPersonMap.set(user.emailAddresses[0].value, user);
+ }
+ } catch (error) {
+ console.log("Error in fetching and mapping email and person", error);
+ }
+ return emailToPersonMap;
+ }
+
+ /**
+ * Retrieves a mapping of group email addresses to their corresponding `Group` objects.
+ *
+ * @param emails - An array of group email addresses to fetch details for.
+ * @returns A promise that resolves to a `Map` mapping group email addresses to `Group` objects.
+ */
+ async getEmailToGroupMap(emails: string[]): Promise> {
+ const emailToGroupMap = new Map();
+
+ if (!emails || emails.length === 0) {
+ return emailToGroupMap;
+ }
+
+ try {
+ const groups = await this.hermesClient.getGroupDetailsFromEmail(emails);
+ for (const group of groups) {
+ if (group?.email) {
+ emailToGroupMap.set(group.email, group);
+ }
+ }
+ } catch (error) {
+ console.log("Error in fetching and mapping email and group", error);
+ }
+
+ return emailToGroupMap;
+ }
+
+ /**
+ * Generates a complete Hermes URL for a given person by using the URL of their first photo.
+ *
+ * @param person - The person object containing an array of photos.
+ * @returns The complete Hermes URL constructed from the first photo's URL.
+ */
+ createHermesUrlFromPerson(person: Person) {
+ if (!person) return "";
+ return this.hermesClient.createCompleteUrl(person.photos[0].url);
+ }
+
+ /**
+ * Updates the document metadata with the provided payload, synchronizes the local document state,
+ * and triggers a mutation and re-render.
+ *
+ * @param payload - A partial object containing the metadata fields to update.
+ * @param mutateDoc - A callback function that receives the updated document metadata.
+ *
+ * @remarks
+ * This method sends an update request to the backend, fetches the latest document details,
+ * merges the updated fields into the local document, invokes the mutation callback,
+ * and re-renders the table. Errors during the process are logged to the console.
+ */
+ async updateMetadata(
+ payload: Partial,
+ mutateDoc: (doc: IDocumentMetadata) => void
+ ) {
+ try {
+ await this.hermesClient.updateDocument({
+ fileID: this.document.objectID,
+ isDraft: this.document._isDraft,
+ updatePayload: payload,
+ });
+ this.document = await this.hermesClient
+ .getDocumentDetails(this.document.objectID)
+ .catch((_) => this.document);
+
+ for (const [key, value] of Object.entries(payload)) {
+ this.document[key] = value;
+ }
+
+ mutateDoc(this.document);
+ await this.renderTable();
+ } catch (error) {
+ console.log("Error occured during updating values", error);
+ // Re-throw error so components can handle it
+ throw error;
+ }
+ }
+
+ /**
+ * Searches for people matching the specified query string using the Hermes client.
+ *
+ * @param query - The search string used to find people.
+ * @returns A promise that resolves with the search results from the Hermes client.
+ */
+ async searchPeople(query: string) {
+ return await this.hermesClient.searchPeople(query);
+ }
+
+ /**
+ * Searches for groups matching the specified query string using the Hermes client.
+ *
+ * @param query - The search string used to find groups.
+ * @returns A promise that resolves with the search results from the Hermes client.
+ */
+ async searchGroups(query: string) {
+ return await this.hermesClient.searchGroups(query);
+ }
+
+ /**
+ * Publishes the current document for review if it is in draft state.
+ *
+ * This method checks if the document is a draft. If so, it calls the Hermes client to publish the document
+ * for review, updates the local document metadata (fetching the latest from the server or updating status locally),
+ * applies a mutation to the document metadata via the provided callback, and re-renders the table view.
+ * Any errors encountered during the process are logged to the console.
+ *
+ * @param mutateDoc - A callback function that receives the updated document metadata and applies necessary mutations.
+ * @returns A promise that resolves when the operation is complete.
+ */
+ async publishForReview(mutateDoc: (doc: IDocumentMetadata) => void) {
+ try {
+ if (this.document._isDraft) {
+ const fileID = this.document.objectID;
+ await this.hermesClient.publishForReview(this.document.objectID);
+ this.document = await this.hermesClient.getDocument(fileID).catch((_) => ({
+ ...this.document,
+ status: "In Review",
+ _isDraft: false,
+ }));
+
+ // Load related resources for the newly published document to ensure
+ // they are available and properly cached after the draft->published transition
+ await this.loadRelatedResources();
+
+ mutateDoc(this.document);
+ await this.renderTable();
+ }
+ } catch (error) {
+ console.log("Error occured during publishing for review");
+ }
+ }
+
+ /**
+ * Deletes the current draft document using the Hermes client.
+ *
+ * Attempts to delete the draft associated with the current document's `objectID`.
+ * Logs an error message to the console if the deletion fails.
+ *
+ * @returns {Promise} A promise that resolves when the draft is deleted.
+ */
+ async deleteDraft() {
+ try {
+ await this.hermesClient.deleteDraft(this.document.objectID);
+ } catch (error) {
+ console.log("deletedDraft failed");
+ }
+ }
+
+ /**
+ * Gets the archived status of the current draft document.
+ *
+ * @returns A promise that resolves with the archived status.
+ */
+ async getDraftArchivedStatus(): Promise<{ archived: boolean }> {
+ try {
+ return await this.hermesClient.getDraftArchivedStatus(this.document.objectID);
+ } catch (error) {
+ console.error("Error getting archived status:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Sets the archived status of the current draft document.
+ *
+ * @param archived - The new archived status (true to archive, false to unarchive).
+ * @param mutateDoc - A callback function that receives the updated document metadata.
+ * @returns A promise that resolves when the status is updated.
+ */
+ async setDraftArchivedStatus(archived: boolean, mutateDoc: (doc: IDocumentMetadata) => void): Promise {
+ try {
+ await this.hermesClient.setDraftArchivedStatus(this.document.objectID, archived);
+
+ // Update local document metadata
+ this.document.archived = archived;
+ mutateDoc(this.document);
+
+ // Refresh the Word document header to show the archived status
+ await this.renderTable();
+ } catch (error) {
+ console.error("Error setting archived status:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Approves the current document if it is not a draft, the current user is the approver,
+ * and the document has not already been approved by the current user.
+ *
+ * This method performs the following steps:
+ * 1. Checks if the document is eligible for approval.
+ * 2. Calls the Hermes client to approve the document.
+ * 3. Updates the local document metadata, either by fetching the latest data from the server
+ * or by updating the `approvedBy` list locally if the fetch fails.
+ * 4. Applies the provided mutation function to the updated document metadata.
+ * 5. Renders the updated table view.
+ *
+ * @param mutateDoc - A function that mutates the document metadata after approval.
+ * @returns A promise that resolves when the approval process and UI update are complete.
+ */
+ async approveDoc(mutateDoc: (doc: IDocumentMetadata) => void): Promise {
+ try {
+ const isIndividualApprover = this.isCurrentApprover();
+ const isGroupApprover = await this.isCurrentGroupApprover();
+
+ if (!this.document._isDraft && (isIndividualApprover || isGroupApprover) && !this.isApprovedByCurrentUser()) {
+ const fileID = this.document.objectID;
+ await this.hermesClient.approveDocument(fileID);
+ this.document = await this.hermesClient.getDocument(fileID).catch(_ => ({
+ ...this.document,
+ approvedBy: [...(this.document.approvedBy || []), this.currentUser.email],
+ }));
+ mutateDoc(this.document);
+ await this.renderTable();
+ }
+ } catch (error) {
+ console.log("approveDoc has failed with err: ", error)
+ }
+ }
+
+ /**
+ * Determines if the provided email address matches the current user's email.
+ *
+ * @param email - The email address to compare with the current user's email.
+ * @returns `true` if the provided email matches the current user's email, otherwise `false`.
+ */
+ isMe(email: string) {
+ return this.currentUser.email === email;
+ }
+
+ /**
+ * Checks if the current user is the owner of the document.
+ *
+ * @returns {boolean} Returns `true` if the current user's email is included in the document's owners list; otherwise, returns `false`.
+ */
+ isCurrentUserIsOwner() {
+ return this.document.owners.includes(this.currentUser?.email || "");
+ }
+
+ /**
+ * Determines whether the current user is a contributor to the document.
+ *
+ * @returns {boolean} `true` if the current user's email is included in the document's contributors list; otherwise, `false`.
+ */
+ isCurrentUserContributor() {
+ return this.document.contributors?.includes(this.currentUser?.email || "") || false;
+ }
+
+ /**
+ * Determines if the current user is an approver for the document.
+ *
+ * @returns {boolean} `true` if the current user's email is included in the document's approvers list; otherwise, `false`.
+ */
+ isCurrentApprover() {
+ return this.document.approvers?.includes(this.currentUser?.email || "") || false;
+ }
+
+ /**
+ * Determines whether the current user is eligible to approve via group membership.
+ *
+ * @returns {Promise} `true` if the current user can approve as part of an approver group; otherwise, `false`.
+ */
+ async isCurrentGroupApprover(): Promise {
+ if (!this.document.approverGroups || this.document.approverGroups.length === 0) {
+ return false;
+ }
+
+ try {
+ const response = await fetch(
+ `${this.hermesClient["baseUrl"]}/api/v2/approvals/${this.document.objectID}`,
+ {
+ method: "HEAD",
+ credentials: "include",
+ mode: "cors",
+ cache: "no-cache",
+ }
+ );
+
+ return response.status === 200;
+ } catch (error) {
+ console.log("Error checking group approver status:", error);
+ return false;
+ }
+ }
+
+ /**
+ * Checks if the current user has approved the document.
+ *
+ * @returns {boolean} `true` if the current user's email is present in the document's `approvedBy` list; otherwise, `false`.
+ */
+ isApprovedByCurrentUser() {
+ return this.document.approvedBy?.includes(this.currentUser.email || "") || false;
+ }
+
+ /**
+ * Loads related resources for the current document.
+ *
+ * @returns {Promise} A promise that resolves to the related resources response.
+ * @throws Will throw an error if the document is not loaded or if the API request fails.
+ */
+ async loadRelatedResources(): Promise {
+ if (!this.document) {
+ throw new Error("Document not loaded");
+ }
+
+ try {
+ const resources = await this.hermesClient.getRelatedResources(
+ this.document.objectID,
+ this.document._isDraft
+ );
+
+ // Ensure resources has the correct structure with empty arrays if null/undefined
+ const safeResources: RelatedResourcesResponse = {
+ externalLinks: resources?.externalLinks || [],
+ hermesDocuments: resources?.hermesDocuments || []
+ };
+
+ // Cache the resources in the document metadata
+ this.document.relatedResources = safeResources;
+
+ return safeResources;
+ } catch (error) {
+ console.error("Failed to load related resources:", error);
+
+ // Return empty response instead of throwing to prevent UI crashes
+ const emptyResponse: RelatedResourcesResponse = {
+ externalLinks: [],
+ hermesDocuments: []
+ };
+
+ this.document.relatedResources = emptyResponse;
+ return emptyResponse;
+ }
+ }
+
+ /**
+ * Adds a new related resource to the document.
+ *
+ * @param {RelatedResource} resource - The resource to add.
+ * @returns {Promise} A promise that resolves when the resource is successfully added.
+ * @throws Will throw an error if the document is not loaded or if the API request fails.
+ */
+ async addRelatedResource(resource: RelatedResource): Promise {
+ if (!this.document) {
+ throw new Error("Document not loaded");
+ }
+
+ try {
+ // Always load fresh resources from server to avoid stale cached data,
+ // especially important after document state changes (draft -> published)
+ const currentResources = await this.loadRelatedResources();
+
+ // Convert to combined array for easier manipulation
+ const combinedResources = combineAndSortResources(currentResources);
+
+ // Add new resource with next sort order
+ const maxSortOrder = combinedResources.length > 0
+ ? Math.max(...combinedResources.map(r => r.sortOrder))
+ : 0;
+
+ resource.sortOrder = maxSortOrder + 1;
+ combinedResources.push(resource);
+
+ // Convert back to API format and save
+ const updateRequest = formatResourcesForUpdate(combinedResources);
+
+ await this.hermesClient.updateRelatedResources(
+ this.document.objectID,
+ this.document._isDraft,
+ updateRequest
+ );
+
+ // Reload resources to get server-side updates and update cache
+ await this.loadRelatedResources();
+ } catch (error) {
+ console.error("Failed to add related resource:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Updates an existing related resource.
+ *
+ * @param {RelatedResource} updatedResource - The updated resource data.
+ * @returns {Promise} A promise that resolves when the resource is successfully updated.
+ * @throws Will throw an error if the document is not loaded or if the API request fails.
+ */
+ async updateRelatedResource(updatedResource: RelatedResource): Promise {
+ if (!this.document) {
+ throw new Error("Document not loaded");
+ }
+
+ try {
+ // Always load fresh resources from server to avoid stale cached data
+ const currentResources = await this.loadRelatedResources();
+
+ // Get current resources and update the specific resource
+ const combinedResources = combineAndSortResources(currentResources);
+
+ const resourceIndex = combinedResources.findIndex(
+ r => r.sortOrder === updatedResource.sortOrder
+ );
+
+ if (resourceIndex === -1) {
+ throw new Error("Resource not found");
+ }
+
+ combinedResources[resourceIndex] = updatedResource;
+
+ // Convert back to API format and save
+ const updateRequest = formatResourcesForUpdate(combinedResources);
+
+ await this.hermesClient.updateRelatedResources(
+ this.document.objectID,
+ this.document._isDraft,
+ updateRequest
+ );
+
+ // Reload resources to get server-side updates and update cache
+ await this.loadRelatedResources();
+ } catch (error) {
+ console.error("Failed to update related resource:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Removes a related resource from the document.
+ *
+ * @param {RelatedResource} resource - The resource to remove.
+ * @returns {Promise} A promise that resolves when the resource is successfully removed.
+ * @throws Will throw an error if the document is not loaded or if the API request fails.
+ */
+ async removeRelatedResource(resource: RelatedResource): Promise {
+ if (!this.document) {
+ throw new Error("Document not loaded");
+ }
+
+ try {
+ // Always load fresh resources from server to avoid stale cached data
+ const currentResources = await this.loadRelatedResources();
+
+ // Get current resources and filter out the resource to remove
+ const combinedResources = combineAndSortResources(currentResources);
+
+ const filteredResources = combinedResources.filter(
+ r => r.sortOrder !== resource.sortOrder
+ );
+
+ // Convert back to API format and save
+ const updateRequest = formatResourcesForUpdate(filteredResources);
+
+ await this.hermesClient.updateRelatedResources(
+ this.document.objectID,
+ this.document._isDraft,
+ updateRequest
+ );
+
+ // Reload resources to get server-side updates and update cache
+ await this.loadRelatedResources();
+ } catch (error) {
+ console.error("Failed to remove related resource:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Gets the current related resources from the cached document metadata.
+ *
+ * @returns {RelatedResourcesResponse} The cached related resources or empty response if not loaded.
+ */
+ getCachedRelatedResources(): RelatedResourcesResponse {
+ return this.document?.relatedResources || { externalLinks: [], hermesDocuments: [] };
+ }
+
+ /**
+ * Searches for documents that can be added as related resources.
+ *
+ * @param {string} query - The search query string.
+ * @returns {Promise} A promise that resolves to an array of matching documents.
+ * @throws Will throw an error if the search fails.
+ */
+ async searchDocuments(query: string): Promise {
+ try {
+ console.log("Searching for documents with query:", query);
+
+ // Use the HermesClient to search for documents
+ const results = await this.hermesClient.searchDocuments(query);
+
+ // Filter out the current document from results if it exists
+ const filteredResults = results.filter(doc =>
+ doc.objectID !== this.document?.objectID
+ );
+
+ console.log(`Found ${filteredResults.length} documents for query: ${query}`);
+ return filteredResults;
+ } catch (error) {
+ console.error("Failed to search documents:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Searches for projects using the specified query string.
+ *
+ * @param query - The search query string
+ * @returns A promise that resolves to an array of matching projects
+ */
+ async searchProjects(query: string) {
+ try {
+ console.log("Searching for projects with query:", query);
+
+ // Use the HermesClient to search for projects
+ const results = await this.hermesClient.searchProjects(query);
+
+ console.log(`Found ${results.length} projects for query: ${query}`);
+ return results;
+ } catch (error) {
+ console.error("Failed to search projects:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Retrieves all active projects for project selection.
+ *
+ * @returns A promise that resolves to an array of active projects
+ */
+ async getActiveProjects() {
+ try {
+ console.log("Loading active projects");
+
+ // Use the HermesClient to get active projects
+ const results = await this.hermesClient.getActiveProjects();
+
+ console.log(`Found ${results.length} active projects`);
+ return results;
+ } catch (error) {
+ console.error("Failed to load active projects:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Refreshes the document metadata from the server.
+ *
+ * @returns A promise that resolves when the document metadata is refreshed
+ */
+ async refreshDocumentMetadata() {
+ try {
+ if (!this.document?.objectID) {
+ throw new Error("No document available to refresh");
+ }
+
+ this.document = await this.hermesClient.getDocumentDetails(this.document.objectID);
+ console.log("Document metadata refreshed");
+ } catch (error) {
+ console.error("Failed to refresh document metadata:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Loads projects associated with the current document.
+ *
+ * @returns A promise that resolves to an array of projects associated with the document
+ */
+ async loadDocumentProjects() {
+ try {
+ console.log("Loading document projects for:", this.document?.objectID);
+
+ if (!this.document?.projects || this.document.projects.length === 0) {
+ console.log("No projects associated with document");
+ return [];
+ }
+
+ // Get project details for the project IDs in the document metadata
+ const results = await this.hermesClient.getProjectsByIds(this.document.projects);
+
+ console.log(`Found ${results.length} projects for document`);
+ return results;
+ } catch (error) {
+ console.error("Failed to load document projects:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Adds the current document to a project.
+ *
+ * @param projectId - The ID of the project to add the document to
+ * @returns A promise that resolves when the document is added to the project
+ */
+ async addDocumentToProject(projectId: string) {
+ try {
+ console.log("Adding document to project:", projectId);
+
+ if (!this.document?.objectID) {
+ throw new Error("No document available to add to project");
+ }
+
+ await this.hermesClient.addDocumentToProject(this.document.objectID, projectId);
+
+ // Refresh document metadata to get updated projects array
+ await this.refreshDocumentMetadata();
+
+ // Update document header to reflect the new project
+ await this.renderTable();
+
+ console.log("Successfully added document to project");
+ } catch (error) {
+ console.error("Failed to add document to project:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Removes the current document from a project.
+ *
+ * @param projectId - The ID of the project to remove the document from
+ * @returns A promise that resolves when the document is removed from the project
+ */
+ async removeDocumentFromProject(projectId: string) {
+ try {
+ console.log("Removing document from project:", projectId);
+
+ if (!this.document?.objectID) {
+ throw new Error("No document available to remove from project");
+ }
+
+ await this.hermesClient.removeDocumentFromProject(this.document.objectID, projectId);
+
+ // Refresh document metadata to get updated projects array
+ await this.refreshDocumentMetadata();
+
+ // Update document header to reflect the project removal
+ await this.renderTable();
+
+ console.log("Successfully removed document from project");
+ } catch (error) {
+ console.error("Failed to remove document from project:", error);
+ throw error;
+ }
+ }
+}
diff --git a/hermes-plugin/src/taskpane/utils/wordService.ts b/hermes-plugin/src/taskpane/utils/wordService.ts
new file mode 100644
index 000000000..6533611cf
--- /dev/null
+++ b/hermes-plugin/src/taskpane/utils/wordService.ts
@@ -0,0 +1,639 @@
+import IDocumentMetadata from "../interfaces/documentMetadata";
+
+type WordProperties = Word.Interfaces.DocumentPropertiesData;
+
+export default class WordService {
+ // Color for field keys to ensure good contrast in both light and dark modes
+ // Using the same color as the header table title for consistency
+ private static readonly FIELD_KEY_COLOR = "#0f4761";
+
+ /**
+ * Retrieves the properties of the current Word document.
+ *
+ * @returns A promise that resolves to the document's properties as a `WordProperties` object.
+ * @throws Will reject the promise if the operation fails.
+ *
+ * @example
+ * ```typescript
+ * const properties = await wordService.getDocProperties();
+ * console.log(properties);
+ * ```
+ */
+ public async getDocProperties(): Promise {
+ return new Promise((resolve, reject) => {
+ Word.run(async (ctx) => {
+ try {
+ const props = ctx.document.properties.load();
+ await ctx.sync();
+ resolve(props.toJSON());
+ } catch (error) {
+ console.log("getDocProperties failed with error", error.debugInfo);
+ reject(error);
+ }
+ });
+ });
+ }
+
+ /**
+ * Checks if the document can be edited by testing actual write capabilities.
+ * This is more reliable than checking change tracking mode.
+ *
+ * @returns A promise that resolves to true if document can be edited, false if read-only
+ */
+ public async isDocumentInEditMode(): Promise {
+ return new Promise((resolve) => {
+ Word.run(async (ctx) => {
+ try {
+ const body = ctx.document.body;
+
+ // Try to get a custom property to test write access
+ // This is a lightweight operation that will fail if document is truly locked
+ const customProps = ctx.document.properties.customProperties;
+ customProps.load("items");
+ await ctx.sync();
+
+ // Check if change tracking is enabled - if so, updates will create suggestions
+ const doc = ctx.document;
+ doc.load("changeTrackingMode");
+ await ctx.sync();
+
+ const hasChangeTracking = doc.changeTrackingMode !== Word.ChangeTrackingMode.off;
+
+ if (hasChangeTracking) {
+ console.log("Document has change tracking enabled. Updates will create suggestions.");
+ resolve(false);
+ return;
+ }
+
+ // If we got here, document appears editable
+ console.log("Document is in edit mode without change tracking.");
+ resolve(true);
+
+ } catch (error) {
+ console.log("Error checking document edit mode:", error);
+
+ // Check if error indicates read-only or locked state
+ const errorMsg = error?.message || error?.toString() || "";
+ if (errorMsg.includes("read-only") ||
+ errorMsg.includes("ReadOnly") ||
+ errorMsg.includes("locked") ||
+ errorMsg.includes("protected")) {
+ console.log("Document appears to be read-only or locked.");
+ resolve(false);
+ } else {
+ // Unknown error, assume we can edit to avoid blocking legitimate updates
+ console.log("Unknown error, defaulting to allow edits.");
+ resolve(true);
+ }
+ }
+ });
+ });
+ }
+
+ /**
+ * Updates the document headers by creating a new header table with the provided metadata.
+ * Uses a simple check-and-update approach to prevent duplicate tables.
+ * Only updates if the document is in edit mode (not review mode).
+ *
+ * @param headers - The metadata to be used for updating the document headers.
+ * @throws Will throw an error if the header table cannot be updated or if any operation fails.
+ */
+ public async updateDocumentHeaders(headers: IDocumentMetadata) {
+ // Check if document is in edit mode
+ const isEditMode = await this.isDocumentInEditMode();
+
+ if (!isEditMode) {
+ console.log("Document is in review mode. Skipping metadata update to avoid creating suggestions.");
+ return;
+ }
+
+ // Simple approach: check for existing tables and clean them up first
+ try {
+ const rows = this.getFormattedRows(headers);
+
+ await Word.run(async (ctx) => {
+ // First, find and remove ALL existing header tables
+ const headerTables = await this.getExisitingHeaderTables(ctx, headers.docType);
+
+ console.log(`Found ${headerTables.length} existing header tables to remove`);
+
+ // Delete all existing header tables first
+ headerTables.forEach((headerTable) => {
+ headerTable.delete();
+ });
+
+ await ctx.sync();
+
+ // Small delay to ensure deletion is processed
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ // Now create the new header table
+ await this.createHeaderTable(ctx, rows, headers);
+
+ await ctx.sync();
+ console.log("Header table updated successfully");
+ });
+ } catch (error) {
+ console.log("Error during updateDocumentHeaders: ", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Creates and inserts a styled header table into the Word document body.
+ *
+ * The table consists of multiple rows, with the first row styled as a title and the second as a summary.
+ * The function applies custom styles, merges cells for the header and summary, and enhances the appearance
+ * of specific cells. If an error occurs during creation, the partially created table is deleted.
+ *
+ * @param ctx - The Word.RequestContext used to interact with the Word document.
+ * @param rows - A 2D array of strings representing the content of each row in the table.
+ * @param headers - The document metadata containing project details for hyperlinks.
+ * @returns A Promise that resolves when the table has been created and styled.
+ */
+ async createHeaderTable(ctx: Word.RequestContext, rows: string[][], headers?: IDocumentMetadata) {
+ let table: Word.Table;
+ try {
+ console.log("Creating table with structure:", rows);
+ console.log(`Table dimensions: ${rows.length} rows x 2 columns`);
+
+ const doc = ctx.document.body.load();
+ await ctx.sync();
+
+ table = doc.insertTable(rows.length, 2, "Start", rows);
+ table.style = "Normal";
+
+ table.getBorder(Word.BorderLocation.inside).type = "None";
+ table.getBorder(Word.BorderLocation.left).type = "None";
+ table.getBorder(Word.BorderLocation.right).type = "None";
+
+ const titleRow = table.getCell(0, 0);
+
+ titleRow.body.font.set({
+ size: 17,
+ color: "0f4761",
+ });
+ titleRow.horizontalAlignment = "Left";
+
+ const summaryTitle = table.getCell(1, 0);
+ summaryTitle.setCellPadding("Top", 10);
+ summaryTitle.setCellPadding("Bottom", 15);
+
+ summaryTitle.body.font.set({
+ size: 11,
+ });
+ summaryTitle.horizontalAlignment = "Left";
+ table.mergeCells(0, 0, 0, 1);
+ table.mergeCells(1, 0, 1, 1);
+
+ table.load();
+ await ctx.sync();
+
+ await this.enhanceCellAppearance(ctx, table.getCell(1, 0), true);
+ for (let i = 2; i < table.rowCount; i++) {
+ const cell = table.getCellOrNullObject(i, 0);
+ await this.enhanceCellAppearance(ctx, cell);
+ const nextCell = table.getCellOrNullObject(i, 1);
+ await this.enhanceCellAppearance(ctx, nextCell);
+
+ // Check if this is the NOTE row (last row) and merge cells
+ if (i === table.rowCount - 1) {
+ // Load cell content to check if it's the NOTE row
+ cell.load("body/text");
+ await ctx.sync();
+
+ if (cell.body.text && cell.body.text.includes("NOTE:")) {
+ // Merge the NOTE row across both columns
+ table.mergeCells(i, 0, i, 1);
+ }
+ }
+
+ // Always add hyperlinks for both projects and NOTE content - check both columns
+ const projectDetails = headers?.projectDetails || [];
+ await this.addHyperlinks(ctx, cell, projectDetails, headers?.baseUrl, headers);
+ await this.addHyperlinks(ctx, nextCell, projectDetails, headers?.baseUrl, headers);
+ }
+
+ await ctx.sync();
+ } catch (err) {
+ console.log("Error in table creation:", err);
+ // Don't try to delete the table here as it may cause object lifecycle issues
+ // The table creation failure will be handled by trying again
+ throw err;
+ }
+ }
+
+ /**
+ * Enhances the appearance of a Word table cell by optionally resizing the font and bolding the key portion of the cell's text.
+ *
+ * @param ctx - The Word.RequestContext used to queue commands for the Word document.
+ * @param ptr - The Word.TableCell object representing the cell to enhance.
+ * @param skipResize - Optional. If true, skips resizing the font. Defaults to false.
+ * @returns A Promise that resolves when the appearance enhancements are complete.
+ */
+ async enhanceCellAppearance(
+ ctx: Word.RequestContext,
+ ptr: Word.TableCell,
+ skipResize: boolean = false
+ ) {
+ const cell = ptr.body.load();
+ await ctx.sync();
+ if (cell.isNullObject) {
+ return;
+ }
+
+ const originalText = cell.text || "";
+ if (!skipResize) {
+ cell.font.set({
+ size: 9,
+ });
+ }
+
+ let [key, value] = originalText.split(":");
+ key = (key || "").trim();
+ value = (value || "").trim();
+
+ if (!key || key === null || typeof key === "undefined" || key === "") {
+ return;
+ }
+
+ // Special formatting for Status field
+ if (key === "Status" && value) {
+ // Find and bold the current status within the pipe-separated list
+ const statuses = ["WIP", "In-Review", "Approved", "Obsolete", "ARCHIVED"];
+ let activeStatusFound = false;
+
+ for (const status of statuses) {
+ const markedStatus = `**${status}**`;
+ if (value.includes(markedStatus)) {
+ try {
+ // Remove the ** markers and make it bold with color
+ const cleanedText = cell.text.replace(markedStatus, status);
+ cell.clear();
+ cell.insertText(cleanedText, "Replace");
+
+ // Re-bold the key part after text replacement
+ const keyObjs = cell.search(key).load();
+ await ctx.sync();
+
+ if (keyObjs.items.length > 0) {
+ keyObjs.getFirst().font.set({
+ bold: true,
+ color: WordService.FIELD_KEY_COLOR,
+ });
+ }
+
+ // Now find and format the active status
+ const activeStatusObjs = cell.search(status).load();
+ await ctx.sync();
+
+ if (activeStatusObjs.items.length > 0) {
+ // Find the exact match in the status list context
+ const statusIndex = cleanedText.indexOf(` ${status} `);
+ const statusAtEnd = cleanedText.endsWith(` ${status}`);
+ const statusAtStart = cleanedText.includes(`${status} |`);
+
+ if (statusIndex > -1 || statusAtEnd || statusAtStart) {
+ activeStatusObjs.getFirst().font.set({
+ bold: true,
+ color: this.getStatusColor(status),
+ size: 11, // Increase font size by 1 (from default 9 to 10)
+ });
+ }
+ }
+ activeStatusFound = true;
+ break; // Only one status should be active
+ } catch (error) {
+ console.log(`Error formatting status ${status}:`, error);
+ // Continue to try other statuses or fall back to regular key bolding
+ }
+ }
+ }
+
+ // Fallback: if no active status was formatted, just bold the key
+ if (!activeStatusFound) {
+ const keyObjs = cell.search(key).load();
+ await ctx.sync();
+
+ if (keyObjs.items.length > 0) {
+ keyObjs.getFirst().font.set({
+ bold: true,
+ color: WordService.FIELD_KEY_COLOR,
+ });
+ }
+ }
+ } else {
+ // Bold the key part for all other fields
+ const keyObjs = cell.search(key).load();
+ await ctx.sync();
+
+ if (keyObjs.items.length > 0) {
+ keyObjs.getFirst().font.set({
+ bold: true,
+ color: WordService.FIELD_KEY_COLOR,
+ });
+ }
+ }
+ }
+
+ /**
+ * Gets the appropriate color for a status based on the requirements:
+ * WIP - Orange, In-Review - Purple, Approved - Green, Obsolete - Grey
+ */
+ private getStatusColor(status: string): string {
+ switch (status) {
+ case "WIP":
+ return "#a55818ff"; // Orange
+ case "In-Review":
+ return "#8337cfff"; // Purple
+ case "Approved":
+ return "#035f25ff"; // Green
+ case "Obsolete":
+ return "#57709bff"; // Grey
+ case "ARCHIVED":
+ return "#FF0000"; // Red
+ default:
+ return "#000000"; // Black fallback
+ }
+ }
+
+ /**
+ * Searches for and returns the first table in the Word document body that contains a header matching the specified document type.
+ *
+ * @param ctx - The Word.RequestContext used to interact with the Word document.
+ * @param docType - The document type to search for in table headers. The search is performed using the uppercase form of this string, wrapped in square brackets (e.g., `[DOCTYPE]`).
+ * @returns A promise that resolves to the first matching Word.Table if found, or `null` if no such table exists.
+ */
+ async getExisitingHeaderTables(ctx: Word.RequestContext, docType: string) {
+ const body = ctx.document.body.load();
+ await ctx.sync();
+
+ const allTables = body.tables.load();
+ await ctx.sync();
+
+ if (allTables.isNullObject) return null;
+
+ let headerTable: Word.Table;
+
+ const tables: Word.Table[] = [];
+
+ for (const table of allTables.items) {
+ const res = table.search(`[${docType.toUpperCase()}]`).load();
+ await ctx.sync();
+ if (res.items.length !== 0) {
+ tables.push(table);
+ }
+ }
+
+ return tables;
+ }
+
+ /**
+ * Formats document metadata into a two-dimensional string array for display or export.
+ *
+ * The first rows include the document title and summary, followed by rows of key-value pairs
+ * for required metadata fields (such as created, status, product, owners, etc.), and any custom
+ * editable fields defined in the metadata. Each row contains up to two columns, and new rows
+ * are started as needed.
+ *
+ * @param headers - The document metadata object containing standard and custom fields.
+ * @returns A two-dimensional array of strings, where each inner array represents a row of formatted metadata.
+ */
+ private getFormattedRows(headers: IDocumentMetadata): string[][] {
+ const requiredHeaderKeys: Partial[] = [
+ "created",
+ "status",
+ "product",
+ "owners",
+ "contributors",
+ "approvers",
+ "projects",
+ ];
+ const title = `[${headers.docType.toUpperCase()}] [${headers.docNumber}]: ${headers.title}`;
+ const summaryValue = (headers.summary && headers.summary.trim()) ? headers.summary.trim() : "N/A";
+ const data: string[][] = [[title], [`Summary: ${summaryValue}`], []];
+
+ for (const key of requiredHeaderKeys) {
+ if (data[data.length - 1].length === 2) data.push([]);
+ const capKey = key[0].toUpperCase() + key.slice(1);
+ let value = "";
+
+ if (key === "status") {
+ // Check if document is archived first
+ if (headers.archived) {
+ value = "**ARCHIVED**"; // This will be formatted with red color
+ } else {
+ // Special formatting for status: WIP | In-Review | Approved | Obsolete
+ const currentStatus = headers._isDraft ? "WIP" : (headers.status || "WIP");
+ const allStatuses = ["WIP", "In-Review", "Approved", "Obsolete"];
+
+ // Ensure currentStatus is valid, fallback to WIP if not
+ const validStatus = allStatuses.includes(currentStatus) ? currentStatus : "WIP";
+
+ value = allStatuses.map(status => {
+ if (status === validStatus) {
+ return `**${status}**`; // Bold the current status
+ }
+ return status;
+ }).join(" | ");
+ }
+ } else if (key === "approvers") {
+ const approversList = this.getApproverDisplayList(headers);
+ value = approversList.length > 0 ? approversList.join(", ") : "N/A";
+ } else if (key === "projects") {
+ // Use project details if available, otherwise fall back to IDs
+ if (headers.projectDetails && headers.projectDetails.length > 0) {
+ value = headers.projectDetails.map(project => project.title).join(", ");
+ } else if (Array.isArray(headers[key]) && headers[key].length > 0) {
+ const arrayValue = headers[key].filter(Boolean);
+ value = arrayValue.length > 0 ? arrayValue.join(", ") : "N/A";
+ } else {
+ value = "N/A";
+ }
+ } else if (
+ typeof headers[key] === "string" ||
+ typeof headers[key] === "number"
+ ) {
+ const stringValue = `${headers[key]}`.trim();
+ value = (stringValue && stringValue !== "undefined" && stringValue !== "null") ? stringValue : "N/A";
+ } else if (Array.isArray(headers[key])) {
+ const arrayValue = headers[key].filter(Boolean); // Remove empty/null values
+ value = arrayValue.length > 0 ? arrayValue.join(", ") : "N/A";
+ } else {
+ value = "N/A";
+ }
+
+ const col = `${capKey}: ${value}`;
+ data[data.length - 1].push(col);
+ }
+
+ // now we handle the custom headers
+ if (headers.customEditableFields) {
+ for (const key of Object.keys(headers.customEditableFields)) {
+ if (data[data.length - 1].length === 2) data.push([]);
+ const capKey = headers.customEditableFields[key].displayName;
+ let value = "";
+
+ if (typeof headers[key] === "string" || typeof headers[key] === "number") {
+ const stringValue = `${headers[key]}`.trim();
+ value = (stringValue && stringValue !== "undefined" && stringValue !== "null") ? stringValue : "N/A";
+ } else if (Array.isArray(headers[key])) {
+ const arrayValue = headers[key].filter(Boolean); // Remove empty/null values
+ value = arrayValue.length > 0 ? arrayValue.join(", ") : "N/A";
+ } else {
+ value = "N/A";
+ }
+
+ const col = `${capKey}: ${value}`;
+ data[data.length - 1].push(col);
+ }
+ }
+
+ // Ensure the last row has 2 cells before adding NOTE
+ if (data[data.length - 1].length === 1) {
+ data[data.length - 1].push(""); // Add empty second cell
+ }
+
+ // Add one empty row for spacing above the NOTE section
+ data.push(["", ""]);
+
+ // Add NOTE section at the bottom as a new row - it will span across both columns
+ const noteText = "NOTE: This document is managed by Hermes and this header will be overwritten using document metadata.";
+ data.push([noteText, ""]); // Always create NOTE row with 2 cells
+
+ return data;
+ }
+
+ private getApproverDisplayList(headers: IDocumentMetadata): string[] {
+ const groups = (headers.approverGroups || []).filter(Boolean);
+ const individualApprovers = (headers.approvers || []).filter(Boolean);
+ const approvedBySet = new Set(headers.approvedBy || []);
+
+ const approverDisplay: string[] = [...groups];
+
+ for (const approver of individualApprovers) {
+ if (approvedBySet.has(approver)) {
+ approverDisplay.push(`✅ ${approver}`);
+ } else {
+ approverDisplay.push(approver);
+ }
+ }
+
+ return approverDisplay;
+ }
+
+ /**
+ * Adds hyperlinks to table cells for projects, Hermes, and document references.
+ *
+ * @param ctx - The Word.RequestContext used to interact with the Word document.
+ * @param cell - The table cell that may contain linkable content.
+ * @param projectDetails - Array of project details with id and title.
+ * @param baseUrl - Base URL for creating complete hyperlinks.
+ * @param headers - Document metadata containing docId and draft status.
+ */
+ private async addHyperlinks(
+ ctx: Word.RequestContext,
+ cell: Word.TableCell,
+ projectDetails: Array<{id: string, title: string}>,
+ baseUrl?: string,
+ headers?: IDocumentMetadata
+ ): Promise {
+ try {
+ cell.load("body");
+ await ctx.sync();
+
+ const cellText = cell.body.text || "";
+
+ // Handle Projects hyperlinks
+ if (cellText.includes("Projects:")) {
+ await this.handleProjectsHyperlinks(ctx, cell, cellText, projectDetails, baseUrl);
+ }
+
+ // Handle NOTE section hyperlinks
+ else if (cellText.includes("NOTE:")) {
+ await this.handleNoteHyperlinks(ctx, cell, baseUrl, headers);
+ }
+
+ } catch (error) {
+ console.log("Error adding hyperlinks:", error);
+ console.error("Full error details:", error);
+ // Don't throw - hyperlinks are an enhancement, not critical
+ }
+ }
+
+ /**
+ * Handles hyperlinks for project names in Projects cells.
+ */
+ private async handleProjectsHyperlinks(
+ ctx: Word.RequestContext,
+ cell: Word.TableCell,
+ cellText: string,
+ projectDetails: Array<{id: string, title: string}>,
+ baseUrl?: string
+ ): Promise {
+ // Extract the "Projects:" part and the project names part
+ const parts = cellText.split(":");
+ if (parts.length < 2) {
+ return;
+ }
+
+ const projectNamesText = parts.slice(1).join(":").trim();
+
+ if (!projectNamesText || projectNamesText === "N/A") {
+ return;
+ }
+
+ // Build HTML content with hyperlinks
+ let htmlContent = `Projects: `;
+
+ let linkedProjects: string[] = [];
+ for (const project of projectDetails) {
+ if (projectNamesText.includes(project.title)) {
+ const projectUrl = baseUrl ? `${baseUrl}/projects/${project.id}` : `https://example.com/projects/${project.id}`;
+ linkedProjects.push(`${project.title} `);
+ }
+ }
+
+ if (linkedProjects.length > 0) {
+ htmlContent += linkedProjects.join(', ');
+
+ // Clear cell and insert HTML
+ cell.body.clear();
+ cell.body.insertHtml(htmlContent, "Start");
+
+ await ctx.sync();
+ }
+ }
+
+ /**
+ * Handles hyperlinks for NOTE section with Hermes and document links.
+ */
+ private async handleNoteHyperlinks(
+ ctx: Word.RequestContext,
+ cell: Word.TableCell,
+ baseUrl?: string,
+ headers?: IDocumentMetadata
+ ): Promise {
+ // Build HTML content with hyperlinks for NOTE section
+ const hermesUrl = baseUrl || "https://example.com";
+
+ // Build document URL based on draft status
+ let documentUrl = "#";
+ if (baseUrl && headers?.objectID) {
+ const isDraft = headers._isDraft || false;
+ documentUrl = `${baseUrl}/document/${headers.objectID}`;
+ if (isDraft) {
+ documentUrl += "?draft=true";
+ }
+ }
+
+ const htmlContent = `NOTE: This document is managed by, Hermes and this header will be overwritten using document metadata. `;
+
+ // Clear cell and insert HTML
+ cell.body.clear();
+ cell.body.insertHtml(htmlContent, "Start");
+
+ await ctx.sync();
+ }
+
+}
diff --git a/hermes-plugin/tsconfig.json b/hermes-plugin/tsconfig.json
new file mode 100644
index 000000000..91521ced0
--- /dev/null
+++ b/hermes-plugin/tsconfig.json
@@ -0,0 +1,38 @@
+{
+ "compilerOptions": {
+ "allowUnusedLabels": false,
+ "emitDecoratorMetadata": true,
+ "esModuleInterop": true,
+ "experimentalDecorators": true,
+ "forceConsistentCasingInFileNames": true,
+ "jsx": "react",
+ "module": "ES2020",
+ "moduleResolution": "node",
+ "noImplicitReturns": true,
+ "resolveJsonModule": true,
+ "noUnusedParameters": true,
+ "outDir": "dist",
+ "removeComments": false,
+ "sourceMap": true,
+ "target": "es5",
+ "lib": [
+ "es7",
+ "dom"
+ ],
+ "pretty": true,
+ "typeRoots": [
+ "node_modules/@types"
+ ]
+ },
+ "exclude": [
+ "node_modules"
+ ],
+ "compileOnSave": false,
+ "buildOnSave": false,
+ "ts-node": {
+ "compilerOptions": {
+ "module": "commonjs"
+ },
+ "files": true
+ }
+}
\ No newline at end of file
diff --git a/hermes-plugin/webpack.config.js b/hermes-plugin/webpack.config.js
new file mode 100644
index 000000000..bf6ef32c1
--- /dev/null
+++ b/hermes-plugin/webpack.config.js
@@ -0,0 +1,139 @@
+/* eslint-disable no-undef */
+
+const devCerts = require("office-addin-dev-certs");
+const CopyWebpackPlugin = require("copy-webpack-plugin");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const webpack = require("webpack");
+
+const urlDev = "https://localhost:3000/";
+const urlProd = "https://www.contoso.com/"; // CHANGE THIS TO YOUR PRODUCTION DEPLOYMENT LOCATION
+
+async function getHttpsOptions() {
+ const httpsOptions = await devCerts.getHttpsServerOptions();
+ return { ca: httpsOptions.ca, key: httpsOptions.key, cert: httpsOptions.cert };
+}
+
+module.exports = async (env, options) => {
+ const dev = options.mode === "development";
+ const config = {
+ devtool: "source-map",
+ entry: {
+ polyfill: ["core-js/stable", "regenerator-runtime/runtime"],
+ react: ["react", "react-dom"],
+ taskpane: {
+ import: ["./src/taskpane/index.tsx", "./src/taskpane/taskpane.html"],
+ dependOn: "react",
+ },
+ commands: "./src/commands/commands.ts",
+ "auth-callback": {
+ import: ["./src/auth-callback.ts", "./src/auth-callback.html"],
+ dependOn: "polyfill",
+ },
+ "safari-init": {
+ import: ["./src/safari-init/safari-init.ts", "./src/safari-init/safari-init.html"],
+ dependOn: "polyfill",
+ },
+ },
+ output: {
+ clean: true,
+ },
+ resolve: {
+ extensions: [".ts", ".tsx", ".html", ".js"],
+ },
+ module: {
+ rules: [
+ {
+ test: /\.ts$/,
+ exclude: /node_modules/,
+ use: {
+ loader: "babel-loader",
+ },
+ },
+ {
+ test: /\.tsx?$/,
+ exclude: /node_modules/,
+ use: ["ts-loader"],
+ },
+ {
+ test: /\.html$/,
+ exclude: /node_modules/,
+ use: "html-loader",
+ },
+ {
+ test: /\.css$/,
+ exclude: /node_modules/,
+ use: [MiniCssExtractPlugin.loader, "css-loader"],
+ },
+ {
+ test: /\.(png|jpg|jpeg|ttf|woff|woff2|gif|ico)$/,
+ type: "asset/resource",
+ generator: {
+ filename: "assets/[name][ext][query]",
+ },
+ },
+ ],
+ },
+ plugins: [
+ new MiniCssExtractPlugin({
+ filename: "[name].css",
+ }),
+ new HtmlWebpackPlugin({
+ filename: "taskpane.html",
+ template: "./src/taskpane/taskpane.html",
+ chunks: ["polyfill", "taskpane", "react"],
+ }),
+ new HtmlWebpackPlugin({
+ filename: "auth-callback.html",
+ template: "./src/auth-callback.html",
+ chunks: ["polyfill", "auth-callback"],
+ }),
+ new HtmlWebpackPlugin({
+ filename: "safari-init.html",
+ template: "./src/safari-init/safari-init.html",
+ chunks: ["polyfill", "safari-init"],
+ }),
+ new CopyWebpackPlugin({
+ patterns: [
+ {
+ from: "assets/*",
+ to: "assets/[name][ext][query]",
+ },
+ {
+ from: "manifest*.xml",
+ to: "[name]" + "[ext]",
+ transform(content) {
+ if (dev) {
+ return content;
+ } else {
+ return content.toString().replace(new RegExp(urlDev, "g"), urlProd);
+ }
+ },
+ },
+
+ ],
+ }),
+ new HtmlWebpackPlugin({
+ filename: "commands.html",
+ template: "./src/commands/commands.html",
+ chunks: ["polyfill", "commands"],
+ }),
+ new webpack.ProvidePlugin({
+ Promise: ["es6-promise", "Promise"],
+ }),
+ ],
+ devServer: {
+ hot: true,
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ },
+ server: {
+ type: "https",
+ options: env.WEBPACK_BUILD || options.https !== undefined ? options.https : await getHttpsOptions(),
+ },
+ port: process.env.npm_package_config_dev_server_port || 3000,
+ },
+ };
+
+ return config;
+};
diff --git a/internal/api/approvals.go b/internal/api/approvals.go
index b56aa6e99..d15d711e2 100644
--- a/internal/api/approvals.go
+++ b/internal/api/approvals.go
@@ -3,6 +3,7 @@ package api
import (
"fmt"
"net/http"
+ "time"
"github.com/algolia/algoliasearch-client-go/v3/algolia/errs"
"github.com/hashicorp-forge/hermes/internal/config"
@@ -39,7 +40,10 @@ func ApprovalHandler(
return
}
- // Check if document is locked.
+ // v1 handlers are always Google Workspace.
+ isSharePoint := false
+
+ // Perform lock check for Google Workspace documents.
locked, err := hcd.IsLocked(docID, db, s, l)
if err != nil {
l.Error("error checking document locked status",
@@ -51,7 +55,6 @@ func ApprovalHandler(
http.Error(w, "Error getting document status", http.StatusNotFound)
return
}
- // Don't continue if document is locked.
if locked {
http.Error(w, "Document is locked", http.StatusLocked)
return
@@ -127,36 +130,47 @@ func ApprovalHandler(
}
doc.ApprovedBy = newApprovedBy
- // Get latest Google Drive file revision.
- latestRev, err := s.GetLatestRevision(docID)
- if err != nil {
- l.Error("error getting latest revision",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID)
- http.Error(w, "Error requesting changes of document",
- http.StatusInternalServerError)
- return
- }
+ var revisionId string
+ if isSharePoint {
+ // For SharePoint documents, we don't track revisions the same way
+ // Just use a placeholder revision ID
+ revisionId = "sharepoint-revision-" + fmt.Sprint(time.Now().Unix())
+ l.Info("SharePoint document, using placeholder revision",
+ "sharepoint_file_id", docID,
+ "revision_id", revisionId)
+ } else {
+ // Get latest Google Drive file revision.
+ latestRev, err := s.GetLatestRevision(docID)
+ if err != nil {
+ l.Error("error getting latest revision",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID)
+ http.Error(w, "Error requesting changes of document",
+ http.StatusInternalServerError)
+ return
+ }
- // Mark latest revision to be kept forever.
- _, err = s.KeepRevisionForever(docID, latestRev.Id)
- if err != nil {
- l.Error("error marking revision to keep forever",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- "rev_id", latestRev.Id)
- http.Error(w, "Error updating document status",
- http.StatusInternalServerError)
- return
+ // Mark latest revision to be kept forever.
+ _, err = s.KeepRevisionForever(docID, latestRev.Id)
+ if err != nil {
+ l.Error("error marking revision to keep forever",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "rev_id", latestRev.Id)
+ http.Error(w, "Error updating document status",
+ http.StatusInternalServerError)
+ return
+ }
+ revisionId = latestRev.Id
}
// Record file revision in the Algolia document object.
revisionName := fmt.Sprintf("Changes requested by %s", userEmail)
- doc.SetFileRevision(latestRev.Id, revisionName)
+ doc.SetFileRevision(revisionId, revisionName)
// Convert document to Algolia object.
docObj, err := doc.ToAlgoliaObject(true)
@@ -197,16 +211,21 @@ func ApprovalHandler(
}
// Replace the doc header.
- if err := doc.ReplaceHeader(cfg.BaseURL, false, s); err != nil {
- l.Error("error replacing doc header",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- )
- http.Error(w, "Error requesting changes of document",
- http.StatusInternalServerError)
- return
+ if !isSharePoint {
+ if err := doc.ReplaceHeader(cfg.BaseURL, false, s); err != nil {
+ l.Error("error replacing doc header",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ http.Error(w, "Error requesting changes of document",
+ http.StatusInternalServerError)
+ return
+ }
+ } else {
+ l.Info("SharePoint document, skipping header replacement",
+ "sharepoint_file_id", docID)
}
// Update document reviews in the database.
@@ -245,9 +264,9 @@ func ApprovalHandler(
return
}
// Get document from database.
- dbDoc := models.Document{
- GoogleFileID: docID,
- }
+ var dbDoc models.Document
+ dbDoc = models.NewDocumentByFileID(docID, false)
+
if err := dbDoc.Get(db); err != nil {
l.Error("error getting document from database for data comparison",
"error", err,
@@ -257,13 +276,15 @@ func ApprovalHandler(
)
return
}
+
// Get all reviews for the document.
var reviews models.DocumentReviews
- if err := reviews.Find(db, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
- }); err != nil {
+ var reviewQuery models.DocumentReview
+ reviewQuery = models.DocumentReview{
+ Document: models.NewDocumentByFileID(docID, false),
+ }
+
+ if err := reviews.Find(db, reviewQuery); err != nil {
l.Error("error getting all reviews for document for data comparison",
"error", err,
"method", r.Method,
@@ -296,7 +317,10 @@ func ApprovalHandler(
return
}
- // Check if document is locked.
+ // v1 handlers are always Google Workspace.
+ isSharePoint := false
+
+ // Perform lock check for Google Workspace documents.
locked, err := hcd.IsLocked(docID, db, s, l)
if err != nil {
l.Error("error checking document locked status",
@@ -308,7 +332,6 @@ func ApprovalHandler(
http.Error(w, "Error getting document status", http.StatusNotFound)
return
}
- // Don't continue if document is locked.
if locked {
http.Error(w, "Document is locked", http.StatusLocked)
return
@@ -386,36 +409,47 @@ func ApprovalHandler(
}
doc.ChangesRequestedBy = newChangesRequestedBy
- // Get latest Google Drive file revision.
- latestRev, err := s.GetLatestRevision(docID)
- if err != nil {
- l.Error("error getting latest revision",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID)
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
- return
- }
+ var revisionId string
+ if isSharePoint {
+ // For SharePoint documents, we don't track revisions the same way
+ // Just use a placeholder revision ID
+ revisionId = "sharepoint-revision-" + fmt.Sprint(time.Now().Unix())
+ l.Info("SharePoint document, using placeholder revision",
+ "sharepoint_file_id", docID,
+ "revision_id", revisionId)
+ } else {
+ // Get latest Google Drive file revision.
+ latestRev, err := s.GetLatestRevision(docID)
+ if err != nil {
+ l.Error("error getting latest revision",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID)
+ http.Error(w, "Error creating review",
+ http.StatusInternalServerError)
+ return
+ }
- // Mark latest revision to be kept forever.
- _, err = s.KeepRevisionForever(docID, latestRev.Id)
- if err != nil {
- l.Error("error marking revision to keep forever",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- "rev_id", latestRev.Id)
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
- return
+ // Mark latest revision to be kept forever.
+ _, err = s.KeepRevisionForever(docID, latestRev.Id)
+ if err != nil {
+ l.Error("error marking revision to keep forever",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "rev_id", latestRev.Id)
+ http.Error(w, "Error creating review",
+ http.StatusInternalServerError)
+ return
+ }
+ revisionId = latestRev.Id
}
// Record file revision in the Algolia document object.
revisionName := fmt.Sprintf("Approved by %s", userEmail)
- doc.SetFileRevision(latestRev.Id, revisionName)
+ doc.SetFileRevision(revisionId, revisionName)
// Convert document to Algolia object.
docObj, err := doc.ToAlgoliaObject(true)
@@ -456,17 +490,22 @@ func ApprovalHandler(
}
// Replace the doc header.
- err = doc.ReplaceHeader(cfg.BaseURL, false, s)
- if err != nil {
- l.Error("error replacing doc header",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- )
- http.Error(w, "Error approving document",
- http.StatusInternalServerError)
- return
+ if !isSharePoint {
+ err = doc.ReplaceHeader(cfg.BaseURL, false, s)
+ if err != nil {
+ l.Error("error replacing doc header",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ http.Error(w, "Error approving document",
+ http.StatusInternalServerError)
+ return
+ }
+ } else {
+ l.Info("SharePoint document, skipping header replacement",
+ "sharepoint_file_id", docID)
}
// Update document reviews in the database.
@@ -505,9 +544,9 @@ func ApprovalHandler(
return
}
// Get document from database.
- dbDoc := models.Document{
- GoogleFileID: docID,
- }
+ var dbDoc models.Document
+ dbDoc = models.NewDocumentByFileID(docID, false)
+
if err := dbDoc.Get(db); err != nil {
l.Error("error getting document from database for data comparison",
"error", err,
@@ -517,13 +556,15 @@ func ApprovalHandler(
)
return
}
+
// Get all reviews for the document.
var reviews models.DocumentReviews
- if err := reviews.Find(db, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
- }); err != nil {
+ var reviewQuery models.DocumentReview
+ reviewQuery = models.DocumentReview{
+ Document: models.NewDocumentByFileID(docID, false),
+ }
+
+ if err := reviews.Find(db, reviewQuery); err != nil {
l.Error("error getting all reviews for document for data comparison",
"error", err,
"method", r.Method,
@@ -554,25 +595,25 @@ func ApprovalHandler(
// document reviews in the database.
func updateDocumentReviewsInDatabase(doc document.Document, db *gorm.DB) error {
var docReviews []models.DocumentReview
+
for _, a := range doc.Approvers {
u := models.User{
EmailAddress: a,
}
+
+ docModel := models.NewDocumentByFileID(doc.ObjectID, false)
+
if helpers.StringSliceContains(doc.ApprovedBy, a) {
docReviews = append(docReviews, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: doc.ObjectID,
- },
- User: u,
- Status: models.ApprovedDocumentReviewStatus,
+ Document: docModel,
+ User: u,
+ Status: models.ApprovedDocumentReviewStatus,
})
} else if helpers.StringSliceContains(doc.ChangesRequestedBy, a) {
docReviews = append(docReviews, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: doc.ObjectID,
- },
- User: u,
- Status: models.ChangesRequestedDocumentReviewStatus,
+ Document: docModel,
+ User: u,
+ Status: models.ChangesRequestedDocumentReviewStatus,
})
}
}
diff --git a/internal/api/documents.go b/internal/api/documents.go
index 4ffb09b6f..db21cbf51 100644
--- a/internal/api/documents.go
+++ b/internal/api/documents.go
@@ -150,9 +150,7 @@ func DocumentHandler(
doc.ModifiedTime = modifiedTime.Unix()
// Get document from database.
- model := models.Document{
- GoogleFileID: docID,
- }
+ model := models.NewDocumentByFileID(docID, false)
if err := model.Get(db); err != nil {
l.Error("error getting document from database",
"error", err,
@@ -257,9 +255,7 @@ func DocumentHandler(
return
}
// Get document from database.
- dbDoc := models.Document{
- GoogleFileID: docID,
- }
+ dbDoc := models.NewDocumentByFileID(docID, false)
if err := dbDoc.Get(db); err != nil {
l.Error("error getting document from database for data comparison",
"error", err,
@@ -272,9 +268,7 @@ func DocumentHandler(
// Get all reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(db, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: models.NewDocumentByFileID(docID, false),
}); err != nil {
l.Error("error getting all reviews for document for data comparison",
"error", err,
@@ -602,9 +596,7 @@ Hermes
fmt.Sprintf("[%s] %s", doc.DocNumber, doc.Title))
// Get document record from database so we can modify it for updating.
- model := models.Document{
- GoogleFileID: docID,
- }
+ model := models.NewDocumentByFileID(docID, false)
if err := model.Get(db); err != nil {
l.Error("error getting document from database",
"error", err,
@@ -775,9 +767,7 @@ Hermes
return
}
// Get document from database.
- dbDoc := models.Document{
- GoogleFileID: docID,
- }
+ dbDoc := models.NewDocumentByFileID(docID, false)
if err := dbDoc.Get(db); err != nil {
l.Error("error getting document from database for data comparison",
"error", err,
@@ -790,9 +780,7 @@ Hermes
// Get all reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(db, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: models.NewDocumentByFileID(docID, false),
}); err != nil {
l.Error("error getting all reviews for document for data comparison",
"error", err,
@@ -835,9 +823,7 @@ func updateRecentlyViewedDocs(
}
// Get viewed document in database.
- doc := models.Document{
- GoogleFileID: docID,
- }
+ doc := models.NewDocumentByFileID(docID, false)
if err := doc.Get(db); err != nil {
return fmt.Errorf("error getting viewed document: %w", err)
}
diff --git a/internal/api/documents_related_resources.go b/internal/api/documents_related_resources.go
index a3a9e4223..8ebf08941 100644
--- a/internal/api/documents_related_resources.go
+++ b/internal/api/documents_related_resources.go
@@ -24,8 +24,8 @@ type externalLinkRelatedResourcePutRequest struct {
}
type hermesDocumentRelatedResourcePutRequest struct {
- GoogleFileID string `json:"googleFileID"`
- SortOrder int `json:"sortOrder"`
+ FileID string `json:"FileID"`
+ SortOrder int `json:"sortOrder"`
}
type relatedResourcesGetResponse struct {
@@ -40,7 +40,7 @@ type externalLinkRelatedResourceGetResponse struct {
}
type hermesDocumentRelatedResourceGetResponse struct {
- GoogleFileID string `json:"googleFileID"`
+ FileID string `json:"FileID"`
Title string `json:"title"`
DocumentType string `json:"documentType"`
DocumentNumber string `json:"documentNumber"`
@@ -63,9 +63,7 @@ func documentsResourceRelatedResourcesHandler(
) {
switch r.Method {
case "GET":
- d := models.Document{
- GoogleFileID: docID,
- }
+ d := models.NewDocumentByFileID(docID, false)
if err := d.Get(db); err != nil {
l.Error("error getting document from database",
"error", err,
@@ -121,15 +119,16 @@ func documentsResourceRelatedResourcesHandler(
// Add Hermes document related resources.
for _, hdrr := range hdrrs {
// Get document object from Algolia.
+ targetDocID := hdrr.Document.GetFileIdentifier()
var algoObj map[string]any
- err = algoRead.Docs.GetObject(hdrr.Document.GoogleFileID, &algoObj)
+ err = algoRead.Docs.GetObject(targetDocID, &algoObj)
if err != nil {
l.Error("error getting related resource document from Algolia",
"error", err,
"path", r.URL.Path,
"method", r.Method,
"doc_id", docID,
- "target_doc_id", hdrr.Document.GoogleFileID,
+ "target_doc_id", targetDocID,
)
http.Error(w, "Error accessing document",
http.StatusInternalServerError)
@@ -152,7 +151,7 @@ func documentsResourceRelatedResourcesHandler(
resp.HermesDocuments = append(
resp.HermesDocuments,
hermesDocumentRelatedResourceGetResponse{
- GoogleFileID: hdrr.Document.GoogleFileID,
+ FileID: targetDocID,
Title: doc.Title,
DocumentType: doc.DocType,
DocumentNumber: doc.DocNumber,
@@ -207,9 +206,7 @@ func documentsResourceRelatedResourcesHandler(
for _, elrr := range req.ExternalLinks {
elrrs = append(elrrs, models.DocumentRelatedResourceExternalLink{
RelatedResource: models.DocumentRelatedResource{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: models.NewDocumentByFileID(docID, false),
SortOrder: elrr.SortOrder,
},
Name: elrr.Name,
@@ -222,21 +219,15 @@ func documentsResourceRelatedResourcesHandler(
for _, hdrr := range req.HermesDocuments {
hdrrs = append(hdrrs, models.DocumentRelatedResourceHermesDocument{
RelatedResource: models.DocumentRelatedResource{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: models.NewDocumentByFileID(docID, false),
SortOrder: hdrr.SortOrder,
},
- Document: models.Document{
- GoogleFileID: hdrr.GoogleFileID,
- },
+ Document: models.NewDocumentByFileID(hdrr.FileID, false),
})
}
// Replace related resources for document.
- doc := models.Document{
- GoogleFileID: docID,
- }
+ doc := models.NewDocumentByFileID(docID, false)
if err := doc.ReplaceRelatedResources(db, elrrs, hdrrs); err != nil {
l.Error("error replacing related resources for document",
"error", err,
diff --git a/internal/api/drafts.go b/internal/api/drafts.go
index 7ff0a2e89..bd3050d3b 100644
--- a/internal/api/drafts.go
+++ b/internal/api/drafts.go
@@ -14,6 +14,7 @@ import (
"github.com/algolia/algoliasearch-client-go/v3/algolia/opt"
"github.com/algolia/algoliasearch-client-go/v3/algolia/search"
"github.com/hashicorp-forge/hermes/internal/config"
+ "github.com/hashicorp-forge/hermes/internal/server"
"github.com/hashicorp-forge/hermes/pkg/algolia"
"github.com/hashicorp-forge/hermes/pkg/document"
gw "github.com/hashicorp-forge/hermes/pkg/googleworkspace"
@@ -112,7 +113,12 @@ func DraftsHandler(
}
// Get doc type template.
- template := getDocTypeTemplate(cfg.DocumentTypes.DocumentType, req.DocType)
+ // Check if we're using Microsoft Graph (if it's available in the server)
+ useMicrosoftGraph := false
+ if srv, ok := interface{}(s).(*server.Server); ok {
+ useMicrosoftGraph = srv.SharePoint != nil
+ }
+ template := getDocTypeTemplate(cfg.DocumentTypes.DocumentType, req.DocType, useMicrosoftGraph)
if template == "" {
l.Error("Bad request: no template configured for doc type", "doc_type", req.DocType)
http.Error(w,
@@ -125,7 +131,7 @@ func DraftsHandler(
if req.ProductAbbreviation == "" {
req.ProductAbbreviation = "TODO"
}
- title := fmt.Sprintf("[%s-???] %s", req.ProductAbbreviation, req.Title)
+ title := fmt.Sprintf("[%s-xxx] %s.docx", req.ProductAbbreviation, req.Title)
var (
err error
@@ -175,6 +181,7 @@ func DraftsHandler(
"drafts_folder", cfg.GoogleWorkspace.DraftsFolder,
"temporary_drafts_folder", cfg.GoogleWorkspace.
TemporaryDraftsFolder,
+ "user", userEmail,
)
http.Error(w, "Error creating document draft",
http.StatusInternalServerError)
@@ -190,7 +197,7 @@ func DraftsHandler(
"error", err,
"method", r.Method,
"path", r.URL.Path,
- "doc_id", f.Id,
+ "template", template,
"drafts_folder", cfg.GoogleWorkspace.DraftsFolder,
"temporary_drafts_folder", cfg.GoogleWorkspace.
TemporaryDraftsFolder,
@@ -199,21 +206,68 @@ func DraftsHandler(
http.StatusInternalServerError)
return
}
+ }
+
+ // Move draft file to drafts folder using service user.
+ _, err = s.MoveFile(
+ f.Id, cfg.GoogleWorkspace.DraftsFolder)
+ if err != nil {
+ l.Error(
+ "error moving draft file to drafts folder",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", f.Id,
+ "drafts_folder", cfg.GoogleWorkspace.DraftsFolder,
+ "temporary_drafts_folder", cfg.GoogleWorkspace.
+ TemporaryDraftsFolder,
+ )
+ http.Error(w, "Error creating document draft",
+ http.StatusInternalServerError)
+ return
} else {
- // Copy template to new draft file as service user.
- f, err = s.CopyFile(
- template, title, cfg.GoogleWorkspace.DraftsFolder)
- if err != nil {
- l.Error("error creating draft",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "template", template,
- "drafts_folder", cfg.GoogleWorkspace.DraftsFolder,
- )
- http.Error(w, "Error creating document draft",
- http.StatusInternalServerError)
- return
+ // Check if we should use Microsoft Graph API instead of Google Workspace
+ if srv, ok := interface{}(s).(*server.Server); ok && srv.SharePoint != nil {
+ // Use Microsoft Graph API
+ msGraphDriveItem, err := srv.SharePoint.CopyFile(
+ template, title, srv.Config.SharePoint.DraftsFolder)
+ if err != nil {
+ l.Error("error creating draft with Microsoft Graph",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "template", template,
+ "drafts_folder", srv.Config.SharePoint.DraftsFolder,
+ )
+ http.Error(w, "Error creating document draft",
+ http.StatusInternalServerError)
+ return
+ }
+
+ // Convert Microsoft Graph DriveItem to Google Drive File format for compatibility
+ f = &drive.File{
+ Id: msGraphDriveItem.ID,
+ Name: msGraphDriveItem.Name,
+ CreatedTime: msGraphDriveItem.CreatedAt,
+ ModifiedTime: msGraphDriveItem.LastModified,
+ WebViewLink: msGraphDriveItem.WebURL,
+ }
+ } else {
+ // Copy template to new draft file as service user using Google Workspace
+ f, err = s.CopyFile(
+ template, title, cfg.GoogleWorkspace.DraftsFolder)
+ if err != nil {
+ l.Error("error creating draft",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "template", template,
+ "drafts_folder", cfg.GoogleWorkspace.DraftsFolder,
+ )
+ http.Error(w, "Error creating document draft",
+ http.StatusInternalServerError)
+ return
+ }
}
}
@@ -246,7 +300,7 @@ func DraftsHandler(
// Create tag
// Note: The o_id tag may be empty for environments such as development.
// For environments like pre-prod and prod, it will be set as
- // Okta authentication is enforced before this handler is called for
+ // ALB OIDC authentication is enforced before this handler is called for
// those environments. Maybe, if id isn't set we use
// owner emails in the future?
id := r.Header.Get("x-amzn-oidc-identity")
@@ -262,7 +316,7 @@ func DraftsHandler(
Contributors: req.Contributors,
Created: cd,
CreatedTime: ct.Unix(),
- DocNumber: fmt.Sprintf("%s-???", req.ProductAbbreviation),
+ DocNumber: fmt.Sprintf("%s-xxx", req.ProductAbbreviation),
DocType: req.DocType,
MetaTags: metaTags,
ModifiedTime: ct.Unix(),
@@ -319,8 +373,10 @@ func DraftsHandler(
http.StatusInternalServerError)
return
}
+ docByFileID := models.NewDocumentByFileID(f.Id, false)
model := models.Document{
- GoogleFileID: f.Id,
+ GoogleFileID: docByFileID.GoogleFileID,
+ FileID: docByFileID.FileID,
Approvers: approvers,
Contributors: contributors,
DocumentCreatedAt: createdTime,
@@ -406,9 +462,7 @@ func DraftsHandler(
return
}
// Get document from database.
- dbDoc := models.Document{
- GoogleFileID: f.Id,
- }
+ dbDoc := models.NewDocumentByFileID(f.Id, false)
if err := dbDoc.Get(db); err != nil {
l.Error("error getting document from database for data comparison",
"error", err,
@@ -421,9 +475,7 @@ func DraftsHandler(
// Get all reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(db, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: f.Id,
- },
+ Document: models.NewDocumentByFileID(f.Id, false),
}); err != nil {
l.Error("error getting all reviews for document for data comparison",
"error", err,
@@ -592,9 +644,7 @@ func DraftsDocumentHandler(
}
// Get document from database.
- model := models.Document{
- GoogleFileID: docId,
- }
+ model := models.NewDocumentByFileID(docId, false)
if err := model.Get(db); err != nil {
l.Error("error getting document draft from database",
"error", err,
@@ -732,9 +782,7 @@ func DraftsDocumentHandler(
return
}
// Get document from database.
- dbDoc := models.Document{
- GoogleFileID: docId,
- }
+ dbDoc := models.NewDocumentByFileID(docId, false)
if err := dbDoc.Get(db); err != nil {
l.Error("error getting document from database for data comparison",
"error", err,
@@ -747,9 +795,7 @@ func DraftsDocumentHandler(
// Get all reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(db, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docId,
- },
+ Document: models.NewDocumentByFileID(docId, false),
}); err != nil {
l.Error("error getting all reviews for document for data comparison",
"error", err,
@@ -811,9 +857,7 @@ func DraftsDocumentHandler(
}
// Delete document in the database.
- d := models.Document{
- GoogleFileID: docId,
- }
+ d := models.NewDocumentByFileID(docId, false)
if err := d.Delete(db); err != nil {
l.Error("error deleting document in database",
"error", err,
@@ -1183,7 +1227,7 @@ func DraftsDocumentHandler(
model.ProductID = 0
// Update doc number in document.
- doc.DocNumber = fmt.Sprintf("%s-???", productAbbreviation)
+ doc.DocNumber = fmt.Sprintf("%s-xxx", productAbbreviation)
}
// Summary.
@@ -1281,9 +1325,7 @@ func DraftsDocumentHandler(
return
}
// Get document from database.
- dbDoc := models.Document{
- GoogleFileID: docId,
- }
+ dbDoc := models.NewDocumentByFileID(docId, false)
if err := dbDoc.Get(db); err != nil {
l.Error("error getting document from database for data comparison",
"error", err,
@@ -1296,9 +1338,7 @@ func DraftsDocumentHandler(
// Get all reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(db, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docId,
- },
+ Document: models.NewDocumentByFileID(docId, false),
}); err != nil {
l.Error("error getting all reviews for document for data comparison",
"error", err,
@@ -1392,12 +1432,18 @@ func parseURLPath(path, prefix string) (string, error) {
func getDocTypeTemplate(
docTypes []*config.DocumentType,
docType string,
+ useMicrosoftGraph bool,
) string {
template := ""
for _, t := range docTypes {
if t.Name == docType {
- template = t.Template
+ // Use Microsoft template if MSTemplate is set and we're using Microsoft Graph
+ if useMicrosoftGraph && t.MSTemplate != "" {
+ template = t.MSTemplate
+ } else {
+ template = t.Template
+ }
break
}
}
diff --git a/internal/api/drafts_shareable.go b/internal/api/drafts_shareable.go
index efb30571f..c51207d8f 100644
--- a/internal/api/drafts_shareable.go
+++ b/internal/api/drafts_shareable.go
@@ -35,9 +35,7 @@ func draftsShareableHandler(
switch r.Method {
case "GET":
// Get document from database.
- d := models.Document{
- GoogleFileID: docID,
- }
+ d := models.NewDocumentByFileID(docID, false)
if err := d.Get(db); err != nil {
l.Error("error getting document from database",
"error", err,
@@ -103,9 +101,7 @@ func draftsShareableHandler(
}
// Get document from database.
- doc := models.Document{
- GoogleFileID: docID,
- }
+ doc := models.NewDocumentByFileID(docID, false)
if err := doc.Get(db); err != nil {
l.Error("error getting document from database",
"error", err,
diff --git a/internal/api/helpers.go b/internal/api/helpers.go
index 7de3e0c5c..e90042437 100644
--- a/internal/api/helpers.go
+++ b/internal/api/helpers.go
@@ -135,16 +135,16 @@ func compareAlgoliaAndDatabaseDocument(
var result *multierror.Error
// Compare objectID.
- algoGoogleFileID, err := getStringValue(algoDoc, "objectID")
+ algoFileID, err := getStringValue(algoDoc, "objectID")
if err != nil {
result = multierror.Append(
result, fmt.Errorf("error getting objectID value: %w", err))
}
- if algoGoogleFileID != dbDoc.GoogleFileID {
+ if algoFileID != dbDoc.GetFileIdentifier() {
result = multierror.Append(result,
fmt.Errorf(
"objectID not equal, algolia=%v, db=%v",
- algoGoogleFileID, dbDoc.GoogleFileID),
+ algoFileID, dbDoc.GetFileIdentifier()),
)
}
@@ -185,9 +185,9 @@ func compareAlgoliaAndDatabaseDocument(
result = multierror.Append(
result, fmt.Errorf("error getting docNumber value: %w", err))
} else {
- // Replace "-???" (how draft doc numbers are defined in Algolia) with a
+ // Replace "-xxx.docx" (how draft doc numbers are defined in Algolia) with a
// zero.
- re := regexp.MustCompile(`-\?\?\?$`)
+ re := regexp.MustCompile(`-xxx\.docx$`)
algoDocNumber = re.ReplaceAllString(algoDocNumber, "-000")
var dbDocNumber string
@@ -412,7 +412,7 @@ func compareAlgoliaAndDatabaseDocument(
} else {
dbFileRevisions := make(map[string]string)
for _, fr := range dbDoc.FileRevisions {
- dbFileRevisions[fr.GoogleDriveFileRevisionID] = fr.Name
+ dbFileRevisions[fr.FileRevisionID] = fr.Name
}
if !reflect.DeepEqual(algoFileRevisions, dbFileRevisions) {
result = multierror.Append(result,
diff --git a/internal/api/helpers_test.go b/internal/api/helpers_test.go
index 7ed400606..36b4873d0 100644
--- a/internal/api/helpers_test.go
+++ b/internal/api/helpers_test.go
@@ -116,7 +116,7 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
}{
"good": {
algoDoc: map[string]any{
- "objectID": "GoogleFileID1",
+ "objectID": "FileID1",
"title": "Title1",
"docType": "RFC",
"docNumber": "ABC-123",
@@ -159,8 +159,8 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
"status": "In-Review",
},
dbDoc: models.Document{
- GoogleFileID: "GoogleFileID1",
- Title: "Title1",
+ FileID: "FileID1",
+ Title: "Title1",
DocumentType: models.DocumentType{
Name: "RFC",
},
@@ -212,12 +212,12 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
2023, time.April, 5, 23, 0, 0, 0, time.UTC),
FileRevisions: []models.DocumentFileRevision{
{
- GoogleDriveFileRevisionID: "1",
- Name: "FileRevision1",
+ FileRevisionID: "1",
+ Name: "FileRevision1",
},
{
- GoogleDriveFileRevisionID: "2",
- Name: "FileRevision2",
+ FileRevisionID: "2",
+ Name: "FileRevision2",
},
},
Owner: &models.User{
@@ -255,10 +255,10 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
shouldErr: false,
},
- "good draft doc number (test 'ABC-???')": {
+ "good draft doc number (test 'ABC-xxx.docx')": {
algoDoc: map[string]any{
"appCreated": true,
- "docNumber": "ABC-???",
+ "docNumber": "ABC-xxx.docx",
"docType": "RFC",
"createdTime": float64(time.Date(
2023, time.April, 5, 1, 0, 0, 0, time.UTC).Unix()),
@@ -395,12 +395,12 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
2023, time.April, 5, 23, 0, 0, 0, time.UTC),
FileRevisions: []models.DocumentFileRevision{
{
- GoogleDriveFileRevisionID: "1",
- Name: "FileRevision1",
+ FileRevisionID: "1",
+ Name: "FileRevision1",
},
{
- GoogleDriveFileRevisionID: "2",
- Name: "FileRevision2",
+ FileRevisionID: "2",
+ Name: "FileRevision2",
},
},
Owner: &models.User{
@@ -437,7 +437,7 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
"bad objectID": {
algoDoc: map[string]any{
- "objectID": "GoogleFileID1",
+ "objectID": "FileID1",
"appCreated": true,
"docNumber": "ABC-123",
"createdTime": float64(time.Date(
@@ -448,7 +448,7 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
"product": "Product1",
},
dbDoc: models.Document{
- GoogleFileID: "BadGoogleFileID",
+ FileID: "BadFileID",
DocumentNumber: 123,
Product: models.Product{
Name: "Product1",
@@ -608,7 +608,7 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
"product": "Product1",
},
dbDoc: models.Document{
- GoogleFileID: "BadGoogleFileID",
+ FileID: "BadFileID",
DocumentNumber: 123,
Product: models.Product{
Name: "Product1",
@@ -770,7 +770,7 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
"product": "Product1",
},
dbDoc: models.Document{
- GoogleFileID: "BadGoogleFileID",
+ FileID: "BadFileID",
DocumentNumber: 123,
Product: models.Product{
Name: "Product1",
diff --git a/internal/api/me_recently_viewed_docs.go b/internal/api/me_recently_viewed_docs.go
index 3b2177125..f78930ebb 100644
--- a/internal/api/me_recently_viewed_docs.go
+++ b/internal/api/me_recently_viewed_docs.go
@@ -102,7 +102,7 @@ func MeRecentlyViewedDocsHandler(
}
res = append(res, recentlyViewedDoc{
- ID: doc.GoogleFileID,
+ ID: doc.GetFileIdentifier(),
IsDraft: isDraft,
})
}
diff --git a/internal/api/reviews.go b/internal/api/reviews.go
index 373d489b2..ffa329b36 100644
--- a/internal/api/reviews.go
+++ b/internal/api/reviews.go
@@ -409,9 +409,7 @@ func ReviewHandler(
)
// Update document in the database.
- d := models.Document{
- GoogleFileID: docID,
- }
+ d := models.NewDocumentByFileID(docID, false)
if err := d.Get(db); err != nil {
l.Error("error getting document in database",
"error", err,
@@ -470,6 +468,12 @@ func ReviewHandler(
http.StatusInternalServerError)
return
}
+ l.Debug("got document URL",
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_url", docURL,
+ )
// Send emails to approvers.
if len(doc.Approvers) > 0 {
@@ -482,11 +486,14 @@ func ReviewHandler(
DocumentOwner: doc.Owners[0],
DocumentShortName: doc.DocNumber,
DocumentTitle: doc.Title,
+ DocumentType: doc.DocType,
+ DocumentStatus: doc.Status,
DocumentURL: docURL,
+ Product: doc.Product,
},
[]string{approverEmail},
cfg.Email.FromAddress,
- s,
+ &gw.EmailSenderAdapter{Svc: s},
)
if err != nil {
l.Error("error sending approver email",
@@ -539,7 +546,7 @@ func ReviewHandler(
},
[]string{subscriber.EmailAddress},
cfg.Email.FromAddress,
- s,
+ &gw.EmailSenderAdapter{Svc: s},
)
if err != nil {
l.Error("error sending subscriber email",
@@ -586,9 +593,7 @@ func ReviewHandler(
return
}
// Get document from database.
- dbDoc := models.Document{
- GoogleFileID: docID,
- }
+ dbDoc := models.NewDocumentByFileID(docID, false)
if err := dbDoc.Get(db); err != nil {
l.Error("error getting document from database for data comparison",
"error", err,
@@ -601,9 +606,7 @@ func ReviewHandler(
// Get all reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(db, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: models.NewDocumentByFileID(docID, false),
}); err != nil {
l.Error("error getting all reviews for document for data comparison",
"error", err,
@@ -680,6 +683,49 @@ func createShortcut(
return
}
+// createSharePointShortcut creates a shortcut (.url file) in the hierarchical folder structure
+// ("Shortcuts Folder/RFC/MyProduct/") under docsFolder in SharePoint.
+// func createShortcut(
+// cfg *config.Config,
+// doc document.Document, targetWebURL string,
+// s *sharepointhelper.Service,
+// ) (shortcutID string, retErr error) {
+// // Get or create folder for doc type under ShortcutsFolder
+// docTypeFolder, err := s.GetSubfolder(cfg.SharePoint.ShortcutsFolder, doc.DocType)
+// if err != nil {
+// return "", fmt.Errorf("error getting doc type subfolder: %w", err)
+// }
+// if docTypeFolder == nil {
+// docTypeFolderID, err := s.CreateFolder(doc.DocType, cfg.SharePoint.ShortcutsFolder)
+// if err != nil {
+// return "", fmt.Errorf("error creating doc type subfolder: %w", err)
+// }
+// docTypeFolder = &sharepointhelper.DriveItem{ID: docTypeFolderID, Name: doc.DocType}
+// }
+
+// // Get or create folder for doc type + product
+// productFolder, err := s.GetSubfolder(docTypeFolder.ID, doc.Product)
+// if err != nil {
+// return "", fmt.Errorf("error getting product subfolder: %w", err)
+// }
+// if productFolder == nil {
+// productFolderID, err := s.CreateFolder(doc.Product, docTypeFolder.ID)
+// if err != nil {
+// return "", fmt.Errorf("error creating product subfolder: %w", err)
+// }
+// productFolder = &sharepointhelper.DriveItem{ID: productFolderID, Name: doc.Product}
+// }
+
+// // Create the .url shortcut file in the product folder
+// shortcutName := doc.Title // Or doc.DocNumber or any unique name
+// err = s.CreateShortcut(targetWebURL, shortcutName, productFolder.ID)
+// if err != nil {
+// return "", fmt.Errorf("error creating shortcut: %w", err)
+// }
+
+// return shortcutName, nil
+// }
+
// getDocumentURL returns a Hermes document URL.
func getDocumentURL(baseURL, docID string) (string, error) {
docURL, err := url.Parse(baseURL)
@@ -735,8 +781,8 @@ func revertReviewCreation(
result, fmt.Errorf("error moving doc back to drafts folder: %w", err))
}
- // Change back document number to "ABC-???" and status to "WIP".
- doc.DocNumber = fmt.Sprintf("%s-???", productAbbreviation)
+ // Change back document number to "ABC-xxx" and status to "WIP".
+ doc.DocNumber = fmt.Sprintf("%s-xxx", productAbbreviation)
doc.Status = "WIP"
// Replace the doc header.
diff --git a/internal/api/v2/approvals.go b/internal/api/v2/approvals.go
index 953fb0462..6240465f3 100644
--- a/internal/api/v2/approvals.go
+++ b/internal/api/v2/approvals.go
@@ -3,6 +3,7 @@ package api
import (
"fmt"
"net/http"
+ "strings"
"github.com/hashicorp-forge/hermes/internal/email"
"github.com/hashicorp-forge/hermes/internal/helpers"
@@ -28,9 +29,7 @@ func ApprovalsHandler(srv server.Server) http.Handler {
}
// Get document from database.
- model := models.Document{
- GoogleFileID: docID,
- }
+ model := srv.NewDocumentByFileID(docID)
if err := model.Get(srv.DB); err != nil {
srv.Logger.Error("error getting document from database",
"error", err,
@@ -46,9 +45,7 @@ func ApprovalsHandler(srv server.Server) http.Handler {
// Get reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(srv.DB, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: srv.NewDocumentByFileID(docID),
}); err != nil {
srv.Logger.Error("error getting reviews for document",
"error", err,
@@ -62,9 +59,7 @@ func ApprovalsHandler(srv server.Server) http.Handler {
// Get group reviews for the document.
var groupReviews models.DocumentGroupReviews
if err := groupReviews.Find(srv.DB, models.DocumentGroupReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: srv.NewDocumentByFileID(docID),
}); err != nil {
srv.Logger.Error("error getting group reviews for document",
"error", err,
@@ -93,41 +88,102 @@ func ApprovalsHandler(srv server.Server) http.Handler {
userEmail := r.Context().Value("userEmail").(string)
switch r.Method {
+ case "HEAD":
+ // Authorization probe for clients: return 200 if user can approve, else 403.
+ if doc.Status != "In-Review" && doc.Status != "Approved" {
+ w.WriteHeader(http.StatusForbidden)
+ return
+ }
+
+ if contains(doc.ApprovedBy, userEmail) {
+ srv.Logger.Warn("approval check failed: user already approved",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "user_email", userEmail)
+ w.WriteHeader(http.StatusForbidden)
+ return
+ }
+
+ inApproverGroup, err := isUserInGroups(
+ userEmail, doc.ApproverGroups, srv)
+ if err != nil {
+ srv.Logger.Error("error calculating if user is in an approver group",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error accessing document",
+ http.StatusInternalServerError)
+ return
+ }
+ if !contains(doc.Approvers, userEmail) && !inApproverGroup {
+ srv.Logger.Warn("approval check failed: user not an approver",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "user_email", userEmail)
+ w.WriteHeader(http.StatusForbidden)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ return
case "DELETE":
// Authorize request.
if doc.Status != "In-Review" {
+ srv.Logger.Warn("cannot request changes: document not in review",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "user_email", userEmail,
+ "status", doc.Status)
http.Error(w,
"Can only request changes of documents in the \"In-Review\" status",
http.StatusBadRequest)
return
}
if !contains(doc.Approvers, userEmail) {
+ srv.Logger.Warn("unauthorized changes request: user not an approver",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "user_email", userEmail)
http.Error(w, "Not authorized as a document approver",
http.StatusUnauthorized)
return
}
if contains(doc.ChangesRequestedBy, userEmail) {
+ srv.Logger.Warn("changes already requested by user",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "user_email", userEmail)
http.Error(w, "Document already has changes requested by user",
http.StatusBadRequest)
return
}
- // Check if document is locked.
- locked, err := hcd.IsLocked(docID, srv.DB, srv.GWService, srv.Logger)
- if err != nil {
- srv.Logger.Error("error checking document locked status",
- "error", err,
- "path", r.URL.Path,
- "method", r.Method,
- "doc_id", docID,
- )
- http.Error(w, "Error getting document status", http.StatusNotFound)
- return
- }
- // Don't continue if document is locked.
- if locked {
- http.Error(w, "Document is locked", http.StatusLocked)
- return
+ // Check if document is locked (Google-only: suggestions in
+ // Google Docs headers can cause internal API errors).
+ if !srv.IsSharePoint() {
+ locked, err := hcd.IsLocked(docID, srv.DB, srv.GWService, srv.Logger)
+ if err != nil {
+ srv.Logger.Error("error checking document locked status",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error getting document status", http.StatusNotFound)
+ return
+ }
+ // Don't continue if document is locked.
+ if locked {
+ http.Error(w, "Document is locked", http.StatusLocked)
+ return
+ }
}
// Add email to slice of users who have requested changes of the document.
@@ -143,44 +199,58 @@ func ApprovalsHandler(srv server.Server) http.Handler {
}
doc.ApprovedBy = newApprovedBy
- // Get latest Google Drive file revision.
- latestRev, err := srv.GWService.GetLatestRevision(docID)
- if err != nil {
- srv.Logger.Error("error getting latest revision",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID)
- http.Error(w, "Error requesting changes of document",
- http.StatusInternalServerError)
- return
- }
-
- // Mark latest revision to be kept forever.
- _, err = srv.GWService.KeepRevisionForever(docID, latestRev.Id)
- if err != nil {
- srv.Logger.Error("error marking revision to keep forever",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- "rev_id", latestRev.Id)
- http.Error(w, "Error updating document status",
- http.StatusInternalServerError)
- return
+ // Get latest file revision.
+ var revisionID string
+ if srv.SharePoint != nil {
+ latestRev, err := srv.SharePoint.GetLatestVersion(docID)
+ if err != nil {
+ srv.Logger.Error("error getting latest revision",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID)
+ http.Error(w, "Error requesting changes of document",
+ http.StatusInternalServerError)
+ return
+ }
+ revisionID = latestRev.ID
+ } else {
+ latestRev, err := srv.GWService.GetLatestRevision(docID)
+ if err != nil {
+ srv.Logger.Error("error getting latest revision",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID)
+ http.Error(w, "Error requesting changes of document",
+ http.StatusInternalServerError)
+ return
+ }
+ // Mark latest revision to be kept forever.
+ _, err = srv.GWService.KeepRevisionForever(docID, latestRev.Id)
+ if err != nil {
+ srv.Logger.Error("error marking revision to keep forever",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "rev_id", latestRev.Id)
+ http.Error(w, "Error updating document status",
+ http.StatusInternalServerError)
+ return
+ }
+ revisionID = latestRev.Id
}
// Record file revision in the Algolia document object.
revisionName := fmt.Sprintf("Changes requested by %s", userEmail)
- doc.SetFileRevision(latestRev.Id, revisionName)
+ doc.SetFileRevision(revisionID, revisionName)
// Create file revision in the database.
fr := models.DocumentFileRevision{
- Document: models.Document{
- GoogleFileID: docID,
- },
- GoogleDriveFileRevisionID: latestRev.Id,
- Name: revisionName,
+ Document: srv.NewDocumentByFileID(docID),
+ FileRevisionID: revisionID,
+ Name: revisionName,
}
if err := fr.Create(srv.DB); err != nil {
srv.Logger.Error("error creating document file revision",
@@ -188,14 +258,14 @@ func ApprovalsHandler(srv server.Server) http.Handler {
"method", r.Method,
"path", r.URL.Path,
"doc_id", docID,
- "rev_id", latestRev.Id)
+ "rev_id", revisionID)
http.Error(w, "Error updating document status",
http.StatusInternalServerError)
return
}
// Update document reviews in the database.
- if err := updateDocumentReviewsInDatabase(*doc, srv.DB); err != nil {
+ if err := updateDocumentReviewsInDatabase(*doc, srv.DB, srv.IsSharePoint()); err != nil {
srv.Logger.Error("error updating document reviews in the database",
"error", err,
"doc_id", docID,
@@ -207,19 +277,22 @@ func ApprovalsHandler(srv server.Server) http.Handler {
return
}
- // Replace the doc header.
- if err := doc.ReplaceHeader(
- srv.Config.BaseURL, false, srv.GWService,
- ); err != nil {
- srv.Logger.Error("error replacing doc header",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- )
- http.Error(w, "Error updating document status",
- http.StatusInternalServerError)
- return
+ // Replace the doc header (Google-only; SharePoint headers
+ // are managed by the Hermes Add-In for Word).
+ if !srv.IsSharePoint() {
+ if err := doc.ReplaceHeader(
+ srv.Config.BaseURL, false, srv.GWService,
+ ); err != nil {
+ srv.Logger.Error("error replacing doc header",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ http.Error(w, "Error updating document status",
+ http.StatusInternalServerError)
+ return
+ }
}
// Write response.
@@ -232,6 +305,15 @@ func ApprovalsHandler(srv server.Server) http.Handler {
"path", r.URL.Path,
)
+ // Log document access with Datadog ACCESS tag
+ srv.Logger.Info("ACCESS",
+ "user_email", userEmail,
+ "doc_id", docID,
+ "operation", "changes_requested",
+ "updated_attributes", "[changesRequestedBy, approvedBy, fileRevision]",
+ "mode", "published",
+ )
+
// Request post-processing.
go func() {
// Convert document to Algolia object.
@@ -286,9 +368,7 @@ func ApprovalsHandler(srv server.Server) http.Handler {
return
}
// Get document from database.
- dbDoc := models.Document{
- GoogleFileID: docID,
- }
+ dbDoc := srv.NewDocumentByFileID(docID)
if err := dbDoc.Get(srv.DB); err != nil {
srv.Logger.Error(
"error getting document from database for data comparison",
@@ -302,9 +382,7 @@ func ApprovalsHandler(srv server.Server) http.Handler {
// Get all reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(srv.DB, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: srv.NewDocumentByFileID(docID),
}); err != nil {
srv.Logger.Error(
"error getting all reviews for document for data comparison",
@@ -343,7 +421,7 @@ func ApprovalsHandler(srv server.Server) http.Handler {
// User is not an approver or in an approver group.
inApproverGroup, err := isUserInGroups(
- userEmail, doc.ApproverGroups, srv.GWService)
+ userEmail, doc.ApproverGroups, srv)
if err != nil {
srv.Logger.Error("error calculating if user is in an approver group",
"error", err,
@@ -367,19 +445,30 @@ func ApprovalsHandler(srv server.Server) http.Handler {
case "POST":
// Authorize request.
if doc.Status != "In-Review" && doc.Status != "Approved" {
+ srv.Logger.Warn("cannot approve: document not in correct status",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "user_email", userEmail,
+ "status", doc.Status)
http.Error(w,
`Document status must be "In-Review" or "Approved" to approve`,
http.StatusBadRequest)
return
}
if contains(doc.ApprovedBy, userEmail) {
+ srv.Logger.Warn("document already approved by user",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "user_email", userEmail)
http.Error(w,
"Document already approved by user",
http.StatusBadRequest)
return
}
inApproverGroup, err := isUserInGroups(
- userEmail, doc.ApproverGroups, srv.GWService)
+ userEmail, doc.ApproverGroups, srv)
if err != nil {
srv.Logger.Error("error calculating if user is in an approver group",
"error", err,
@@ -392,28 +481,36 @@ func ApprovalsHandler(srv server.Server) http.Handler {
return
}
if !contains(doc.Approvers, userEmail) && !inApproverGroup {
+ srv.Logger.Warn("unauthorized approval attempt: user not an approver",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "user_email", userEmail)
http.Error(w,
"Not authorized as a document approver",
http.StatusUnauthorized)
return
}
- // Check if document is locked.
- locked, err := hcd.IsLocked(docID, srv.DB, srv.GWService, srv.Logger)
- if err != nil {
- srv.Logger.Error("error checking document locked status",
- "error", err,
- "path", r.URL.Path,
- "method", r.Method,
- "doc_id", docID,
- )
- http.Error(w, "Error getting document status", http.StatusNotFound)
- return
- }
- // Don't continue if document is locked.
- if locked {
- http.Error(w, "Document is locked", http.StatusLocked)
- return
+ // Check if document is locked (Google-only: suggestions in
+ // Google Docs headers can cause internal API errors).
+ if !srv.IsSharePoint() {
+ locked, err := hcd.IsLocked(docID, srv.DB, srv.GWService, srv.Logger)
+ if err != nil {
+ srv.Logger.Error("error checking document locked status",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error getting document status", http.StatusNotFound)
+ return
+ }
+ // Don't continue if document is locked.
+ if locked {
+ http.Error(w, "Document is locked", http.StatusLocked)
+ return
+ }
}
// If the user is a group approver, they won't be in the approvers list.
@@ -451,44 +548,57 @@ func ApprovalsHandler(srv server.Server) http.Handler {
}
doc.ChangesRequestedBy = newChangesRequestedBy
- // Get latest Google Drive file revision.
- latestRev, err := srv.GWService.GetLatestRevision(docID)
- if err != nil {
- srv.Logger.Error("error getting latest revision",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID)
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
- return
- }
-
- // Mark latest revision to be kept forever.
- _, err = srv.GWService.KeepRevisionForever(docID, latestRev.Id)
- if err != nil {
- srv.Logger.Error("error marking revision to keep forever",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- "rev_id", latestRev.Id)
- http.Error(w, "Error approving document",
- http.StatusInternalServerError)
- return
+ // Get latest file revision.
+ var revisionID string
+ if srv.SharePoint != nil {
+ latestRev, err := srv.SharePoint.GetLatestVersion(docID)
+ if err != nil {
+ srv.Logger.Error("error getting latest revision",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID)
+ http.Error(w, "Error creating review",
+ http.StatusInternalServerError)
+ return
+ }
+ revisionID = latestRev.ID
+ } else {
+ latestRev, err := srv.GWService.GetLatestRevision(docID)
+ if err != nil {
+ srv.Logger.Error("error getting latest revision",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID)
+ http.Error(w, "Error creating review",
+ http.StatusInternalServerError)
+ return
+ }
+ _, err = srv.GWService.KeepRevisionForever(docID, latestRev.Id)
+ if err != nil {
+ srv.Logger.Error("error marking revision to keep forever",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "rev_id", latestRev.Id)
+ http.Error(w, "Error updating document status",
+ http.StatusInternalServerError)
+ return
+ }
+ revisionID = latestRev.Id
}
// Record file revision in the Algolia document object.
revisionName := fmt.Sprintf("Approved by %s", userEmail)
- doc.SetFileRevision(latestRev.Id, revisionName)
+ doc.SetFileRevision(revisionID, revisionName)
// Create file revision in the database.
fr := models.DocumentFileRevision{
- Document: models.Document{
- GoogleFileID: docID,
- },
- GoogleDriveFileRevisionID: latestRev.Id,
- Name: revisionName,
+ Document: srv.NewDocumentByFileID(docID),
+ FileRevisionID: revisionID,
+ Name: revisionName,
}
if err := fr.Create(srv.DB); err != nil {
srv.Logger.Error("error creating document file revision",
@@ -496,14 +606,14 @@ func ApprovalsHandler(srv server.Server) http.Handler {
"method", r.Method,
"path", r.URL.Path,
"doc_id", docID,
- "rev_id", latestRev.Id)
+ "rev_id", revisionID)
http.Error(w, "Error updating document status",
http.StatusInternalServerError)
return
}
// Update document reviews in the database.
- if err := updateDocumentReviewsInDatabase(*doc, srv.DB); err != nil {
+ if err := updateDocumentReviewsInDatabase(*doc, srv.DB, srv.IsSharePoint()); err != nil {
srv.Logger.Error("error updating document reviews in the database",
"error", err,
"doc_id", docID,
@@ -515,18 +625,21 @@ func ApprovalsHandler(srv server.Server) http.Handler {
return
}
- // Replace the doc header.
- err = doc.ReplaceHeader(srv.Config.BaseURL, false, srv.GWService)
- if err != nil {
- srv.Logger.Error("error replacing doc header",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- )
- http.Error(w, "Error approving document",
- http.StatusInternalServerError)
- return
+ // Replace the doc header (Google-only; SharePoint headers
+ // are managed by the Hermes Add-In for Word).
+ if !srv.IsSharePoint() {
+ err = doc.ReplaceHeader(srv.Config.BaseURL, false, srv.GWService)
+ if err != nil {
+ srv.Logger.Error("error replacing doc header",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ http.Error(w, "Error approving document",
+ http.StatusInternalServerError)
+ return
+ }
}
// Write response.
@@ -539,28 +652,51 @@ func ApprovalsHandler(srv server.Server) http.Handler {
"path", r.URL.Path,
)
+ // Log document access with Datadog ACCESS tag
+ srv.Logger.Info("ACCESS",
+ "user_email", userEmail,
+ "doc_id", docID,
+ "operation", "document_approved",
+ "updated_attributes", "[approvedBy, status]",
+ "mode", "published",
+ )
+
// Request post-processing.
go func() {
- // Send email to document owner, if enabled.
+ // Send email to document owner and approverGroup, if enabled.
if srv.Config.Email != nil && srv.Config.Email.Enabled &&
len(doc.Owners) > 0 {
// Get name of document approver.
approver := email.User{
EmailAddress: userEmail,
}
- ppl, err := srv.GWService.SearchPeople(
- userEmail, "emailAddresses,names")
- if err != nil {
- srv.Logger.Warn("error searching directory for approver",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- "person", doc.Owners[0],
- )
- }
- if len(ppl) == 1 {
- approver.Name = ppl[0].Names[0].DisplayName
+ if srv.SharePoint != nil {
+ ppl, err := srv.SharePoint.GetPersonByEmail(userEmail)
+ if err != nil {
+ srv.Logger.Warn("error searching directory for approver",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "person", doc.Owners[0],
+ )
+ } else {
+ approver.Name = ppl.DisplayName
+ }
+ } else {
+ ppl, err := srv.GWService.SearchPeople(
+ userEmail, "emailAddresses,names")
+ if err != nil {
+ srv.Logger.Warn("error searching directory for approver",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "person", doc.Owners[0],
+ )
+ } else if len(ppl) == 1 {
+ approver.Name = ppl[0].Names[0].DisplayName
+ }
}
// Get document URL.
@@ -572,38 +708,100 @@ func ApprovalsHandler(srv server.Server) http.Handler {
"method", r.Method,
"path", r.URL.Path,
)
- return
- }
-
- // Send email.
- if err := email.SendDocumentApprovedEmail(
- email.DocumentApprovedEmailData{
- BaseURL: srv.Config.BaseURL,
- DocumentOwner: doc.Owners[0],
- DocumentApprover: approver,
- DocumentNonApproverCount: len(doc.Approvers) -
- len(doc.ApprovedBy),
- DocumentShortName: doc.DocNumber,
- DocumentTitle: doc.Title,
- DocumentType: doc.DocType,
- DocumentStatus: doc.Status,
- DocumentURL: docURL,
- Product: doc.Product,
- },
- []string{doc.Owners[0]},
- srv.Config.Email.FromAddress,
- srv.GWService,
- ); err != nil {
- srv.Logger.Error("error sending document approved email",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
+ } else {
+ // Build recipient set: owners + members of approver groups (deduplicated).
+ recipientSet := map[string]struct{}{}
+ for _, o := range doc.Owners {
+ if strings.TrimSpace(o) == "" {
+ continue
+ }
+ recipientSet[strings.ToLower(o)] = struct{}{}
+ }
+
+ // Expand approver groups to include all members.
+ for _, g := range doc.ApproverGroups {
+ var members []string
+ var gErr error
+ if srv.SharePoint != nil {
+ members, gErr = srv.SharePoint.GetGroupMemberEmails(g)
+ } else {
+ groupMembers, err := srv.GWService.AdminDirectory.Members.List(g).Do()
+ if err != nil {
+ gErr = err
+ } else {
+ for _, m := range groupMembers.Members {
+ members = append(members, m.Email)
+ }
+ }
+ }
+ if gErr != nil {
+ srv.Logger.Warn("error expanding approver group members",
+ "group", g,
+ "error", gErr,
+ "doc_id", docID,
+ )
+ continue
+ }
+ for _, m := range members {
+ if strings.TrimSpace(m) == "" {
+ continue
+ }
+ recipientSet[strings.ToLower(m)] = struct{}{}
+ }
+ }
+
+ // Convert set to slice
+ var recipients []string
+ approverEmailLower := strings.ToLower(userEmail)
+ for addr := range recipientSet {
+ if addr == approverEmailLower {
+ continue
+ }
+ recipients = append(recipients, addr)
+ }
+
+ if len(recipients) == 0 {
+ srv.Logger.Warn("no recipients for approval email",
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ } else {
+ go helpers.SendEmailWithRetry(
+ &srv,
+ func() error {
+ return email.SendDocumentApprovedEmail(
+ email.DocumentApprovedEmailData{
+ BaseURL: srv.Config.BaseURL,
+ DocumentOwner: doc.Owners[0],
+ DocumentApprover: approver,
+ DocumentNonApproverCount: len(doc.Approvers) - len(doc.ApprovedBy),
+ DocumentShortName: doc.DocNumber,
+ DocumentTitle: doc.Title,
+ DocumentType: doc.DocType,
+ DocumentStatus: doc.Status,
+ DocumentURL: docURL,
+ Product: doc.Product,
+ },
+ recipients,
+ srv.Config.Email.FromAddress,
+ srv.GetEmailSender(),
+ )
+ },
+ docID,
+ "document_approved",
+ r,
+ )
+
+ srv.Logger.Info("document approved email queued",
+ "doc_id", docID,
+ "recipient_count", len(recipients),
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ }
}
- }
-
- // Convert document to Algolia object.
+ } // Convert document to Algolia object.
docObj, err := doc.ToAlgoliaObject(true)
if err != nil {
srv.Logger.Error("error converting document to Algolia object",
@@ -655,9 +853,7 @@ func ApprovalsHandler(srv server.Server) http.Handler {
return
}
// Get document from database.
- dbDoc := models.Document{
- GoogleFileID: docID,
- }
+ dbDoc := srv.NewDocumentByFileID(docID)
if err := dbDoc.Get(srv.DB); err != nil {
srv.Logger.Error(
"error getting document from database for data comparison",
@@ -671,9 +867,7 @@ func ApprovalsHandler(srv server.Server) http.Handler {
// Get all reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(srv.DB, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: srv.NewDocumentByFileID(docID),
}); err != nil {
srv.Logger.Error(
"error getting all reviews for document for data comparison",
@@ -706,7 +900,7 @@ func ApprovalsHandler(srv server.Server) http.Handler {
// updateDocumentReviewsInDatabase takes a document and updates the associated
// document reviews in the database.
-func updateDocumentReviewsInDatabase(doc document.Document, db *gorm.DB) error {
+func updateDocumentReviewsInDatabase(doc document.Document, db *gorm.DB, useSharePoint bool) error {
var docReviews []models.DocumentReview
for _, a := range doc.Approvers {
u := models.User{
@@ -714,19 +908,15 @@ func updateDocumentReviewsInDatabase(doc document.Document, db *gorm.DB) error {
}
if helpers.StringSliceContains(doc.ApprovedBy, a) {
docReviews = append(docReviews, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: doc.ObjectID,
- },
- User: u,
- Status: models.ApprovedDocumentReviewStatus,
+ Document: models.NewDocumentByFileID(doc.ObjectID, useSharePoint),
+ User: u,
+ Status: models.ApprovedDocumentReviewStatus,
})
} else if helpers.StringSliceContains(doc.ChangesRequestedBy, a) {
docReviews = append(docReviews, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: doc.ObjectID,
- },
- User: u,
- Status: models.ChangesRequestedDocumentReviewStatus,
+ Document: models.NewDocumentByFileID(doc.ObjectID, useSharePoint),
+ User: u,
+ Status: models.ChangesRequestedDocumentReviewStatus,
})
}
}
diff --git a/internal/api/v2/documents.go b/internal/api/v2/documents.go
index e24db0c05..7db95e07a 100644
--- a/internal/api/v2/documents.go
+++ b/internal/api/v2/documents.go
@@ -7,6 +7,8 @@ import (
"net/http"
"reflect"
"regexp"
+ "slices"
+ "strings"
"time"
"github.com/hashicorp-forge/hermes/internal/email"
@@ -39,8 +41,13 @@ const (
noSubcollectionRequestType
relatedResourcesDocumentSubcollectionRequestType
shareableDocumentSubcollectionRequestType
+ archivedDocumentSubcollectionRequestType
)
+var publishReaderGroups = []string{}
+
+var publishGroupDisplayNames = map[string]string{}
+
func DocumentHandler(srv server.Server) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Parse document ID and request type from the URL path.
@@ -56,10 +63,9 @@ func DocumentHandler(srv server.Server) http.Handler {
return
}
+ model := srv.NewDocumentByFileID(docID)
+
// Get document from database.
- model := models.Document{
- GoogleFileID: docID,
- }
if err := model.Get(srv.DB); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
srv.Logger.Warn("document record not found",
@@ -85,9 +91,7 @@ func DocumentHandler(srv server.Server) http.Handler {
// Get reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(srv.DB, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: srv.NewDocumentByFileID(docID),
}); err != nil {
srv.Logger.Error("error getting reviews for document",
"error", err,
@@ -102,9 +106,7 @@ func DocumentHandler(srv server.Server) http.Handler {
// Get group reviews for the document.
var groupReviews models.DocumentGroupReviews
if err := groupReviews.Find(srv.DB, models.DocumentGroupReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: srv.NewDocumentByFileID(docID),
}); err != nil {
srv.Logger.Error("error getting group reviews for document",
"error", err,
@@ -128,7 +130,6 @@ func DocumentHandler(srv server.Server) http.Handler {
http.Error(w, "Error processing request", http.StatusInternalServerError)
return
}
-
// If the document was created through Hermes and has a status of "WIP", it
// is a document draft and should be instead accessed through the drafts
// API. We return a 404 to be consistent with v1 of the API, and will
@@ -148,7 +149,7 @@ func DocumentHandler(srv server.Server) http.Handler {
switch reqType {
case relatedResourcesDocumentSubcollectionRequestType:
documentsResourceRelatedResourcesHandler(
- w, r, docID, *doc, srv.Config, srv.Logger, srv.AlgoSearch, srv.DB)
+ w, r, docID, *doc, srv.Config, srv.Logger, srv.AlgoSearch, srv.DB, srv.IsSharePoint())
return
case shareableDocumentSubcollectionRequestType:
srv.Logger.Warn("invalid shareable request for documents collection",
@@ -161,37 +162,114 @@ func DocumentHandler(srv server.Server) http.Handler {
}
switch r.Method {
- case "GET":
+ case "HEAD":
+ // HEAD: respond with 200, and for SharePoint documents expose
+ // the direct edit URL header so the frontend can redirect.
+ // For Google documents, return 200 without the header so the
+ // frontend falls through to normal in-app document viewing.
now := time.Now()
- // Get file from Google Drive so we can return the latest modified time.
- file, err := srv.GWService.GetFile(docID)
- if err != nil {
- srv.Logger.Error("error getting document file from Google",
- "error", err,
- "path", r.URL.Path,
- "method", r.Method,
- "doc_id", docID,
- )
- http.Error(w,
- "Error requesting document", http.StatusInternalServerError)
+ // Drafts are not accessible via documents API (mirror GET behavior)
+ if doc.AppCreated && doc.Status == "WIP" {
+ w.WriteHeader(http.StatusNotFound)
return
}
- // Parse and set modified time.
- modifiedTime, err := time.Parse(time.RFC3339Nano, file.ModifiedTime)
- if err != nil {
- srv.Logger.Error("error parsing modified time",
- "error", err,
- "path", r.URL.Path,
- "method", r.Method,
- "doc_id", docID,
- )
- http.Error(w,
- "Error requesting document", http.StatusInternalServerError)
- return
+ if srv.SharePoint != nil {
+ fileDetails, err := srv.SharePoint.GetFileDetails(docID)
+ if err != nil {
+ srv.Logger.Error("error getting document file (HEAD)",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error requesting document", http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("X-Direct-Edit-URL", fileDetails.WebURL)
+ }
+
+ w.Header().Set("Cache-Control", "private, no-store")
+ w.WriteHeader(http.StatusOK)
+ if r.Header.Get("Add-To-Recently-Viewed") != "" {
+ go func() {
+ email := r.Context().Value("userEmail").(string)
+ if err := updateRecentlyViewedDocs(email, docID, srv.DB, now, srv.IsSharePoint()); err != nil {
+ srv.Logger.Error("error updating recently viewed docs (HEAD)",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ }
+ }()
+ }
+ return
+ case "GET":
+ now := time.Now()
+
+ var directEditURL string
+ if srv.SharePoint != nil {
+ // Get file details from SharePoint
+ fileDetails, err := srv.SharePoint.GetFileDetails(docID)
+ if err != nil {
+ srv.Logger.Error("error getting document file",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w,
+ "Error requesting document", http.StatusInternalServerError)
+ return
+ }
+
+ // Parse modified time
+ modifiedTime, err := time.Parse(time.RFC3339, fileDetails.LastModified)
+ if err != nil {
+ srv.Logger.Error("error parsing modified time",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w,
+ "Error requesting document", http.StatusInternalServerError)
+ return
+ }
+ doc.ModifiedTime = modifiedTime.Unix()
+ directEditURL = fileDetails.WebURL
+ } else {
+ // Get file from Google Drive
+ file, err := srv.GWService.GetFile(docID)
+ if err != nil {
+ srv.Logger.Error("error getting document file",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w,
+ "Error requesting document", http.StatusInternalServerError)
+ return
+ }
+
+ modifiedTime, err := time.Parse(time.RFC3339, file.ModifiedTime)
+ if err != nil {
+ srv.Logger.Error("error parsing modified time",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w,
+ "Error requesting document", http.StatusInternalServerError)
+ return
+ }
+ doc.ModifiedTime = modifiedTime.Unix()
+ directEditURL = file.WebViewLink
}
- doc.ModifiedTime = modifiedTime.Unix()
// Convert document to Algolia object because this is how it is expected
// by the frontend.
@@ -208,6 +286,9 @@ func DocumentHandler(srv server.Server) http.Handler {
return
}
+ // Set the directEditURL for direct link to the document
+ docObj["directEditURL"] = directEditURL
+
// Get projects associated with the document.
projs, err := model.GetProjects(srv.DB)
if err != nil {
@@ -247,6 +328,7 @@ func DocumentHandler(srv server.Server) http.Handler {
"doc_id", docID,
"method", r.Method,
"path", r.URL.Path,
+ "status", doc.Status,
)
// Request post-processing.
@@ -260,7 +342,7 @@ func DocumentHandler(srv server.Server) http.Handler {
email := r.Context().Value("userEmail").(string)
if err := updateRecentlyViewedDocs(
- email, docID, srv.DB, now,
+ email, docID, srv.DB, now, srv.IsSharePoint(),
); err != nil {
srv.Logger.Error("error updating recently viewed docs",
"error", err,
@@ -287,9 +369,7 @@ func DocumentHandler(srv server.Server) http.Handler {
return
}
// Get document from database.
- dbDoc := models.Document{
- GoogleFileID: docID,
- }
+ dbDoc := srv.NewDocumentByFileID(docID)
if err := dbDoc.Get(srv.DB); err != nil {
srv.Logger.Error(
"error getting document from database for data comparison",
@@ -303,9 +383,7 @@ func DocumentHandler(srv server.Server) http.Handler {
// Get all reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(srv.DB, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: srv.NewDocumentByFileID(docID),
}); err != nil {
srv.Logger.Error(
"error getting all reviews for document for data comparison",
@@ -357,6 +435,94 @@ func DocumentHandler(srv server.Server) http.Handler {
return
}
+ // Check if document is locked (Google-only).
+ if !srv.IsSharePoint() {
+ locked, err := hcd.IsLocked(docID, srv.DB, srv.GWService, srv.Logger)
+ if err != nil {
+ srv.Logger.Error("error checking document locked status",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error getting document status", http.StatusNotFound)
+ return
+ }
+ // Don't continue if document is locked.
+ if locked {
+ http.Error(w, "Document is locked", http.StatusLocked)
+ return
+ }
+ }
+
+ previousStatus := doc.Status
+
+ // Additional validation for contributor ownership acquisition
+ if isContributorAcquiringOwnership(userEmail, *doc, req) {
+ // Check if current owner is still active in the company
+ currentOwner := doc.Owners[0]
+ srv.Logger.Info("validating ownership acquisition: checking if current owner is alumni",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "contributor", userEmail,
+ "current_owner", currentOwner)
+
+ // Search for the current owner in the people directory
+ var ownerFound bool
+ if srv.SharePoint != nil {
+ people, err := srv.SharePoint.SearchPeople(currentOwner, 1)
+ if err != nil {
+ srv.Logger.Error("error searching for current owner in people directory",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "current_owner", currentOwner)
+ http.Error(w, "Error validating ownership acquisition request",
+ http.StatusInternalServerError)
+ return
+ }
+ ownerFound = len(people) > 0
+ } else {
+ ppl, err := srv.GWService.SearchPeople(
+ currentOwner, "emailAddresses,names")
+ if err != nil {
+ srv.Logger.Error("error searching for current owner in people directory",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "current_owner", currentOwner)
+ http.Error(w, "Error validating ownership acquisition request",
+ http.StatusInternalServerError)
+ return
+ }
+ ownerFound = len(ppl) > 0
+ }
+
+ // If current owner is found in people directory, they are still active
+ if ownerFound {
+ srv.Logger.Warn("ownership acquisition denied: current owner is still active",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "contributor", userEmail,
+ "current_owner", currentOwner)
+ http.Error(w,
+ "Current owner is still active in company directory; Please contact them to transfer ownership",
+ http.StatusForbidden)
+ return
+ }
+
+ srv.Logger.Info("ownership acquisition authorized: current owner not found in company directory",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "contributor", userEmail,
+ "current_owner", currentOwner)
+ }
+
// Validate owners.
if req.Owners != nil {
if len(*req.Owners) != 1 {
@@ -429,49 +595,52 @@ func DocumentHandler(srv server.Server) http.Handler {
}
}
- // Check if document is locked.
- locked, err := hcd.IsLocked(docID, srv.DB, srv.GWService, srv.Logger)
- if err != nil {
- srv.Logger.Error("error checking document locked status",
- "error", err,
- "path", r.URL.Path,
- "method", r.Method,
- "doc_id", docID,
- )
- http.Error(w, "Error getting document status", http.StatusNotFound)
- return
+ // Determine newly added individual approvers and group approvers (pre-patch state vs request).
+ var newUserApprovers []string
+ var newGroupApprovers []string
+ if req.Approvers != nil && len(*req.Approvers) > 0 {
+ if len(doc.Approvers) == 0 { // no existing approvers => all are new
+ newUserApprovers = append(newUserApprovers, *req.Approvers...)
+ } else {
+ newUserApprovers = compareSlices(doc.Approvers, *req.Approvers)
+ }
}
- // Don't continue if document is locked.
- if locked {
- http.Error(w, "Document is locked", http.StatusLocked)
- return
+ if req.ApproverGroups != nil && len(*req.ApproverGroups) > 0 {
+ if len(doc.ApproverGroups) == 0 { // no existing groups => all are new
+ newGroupApprovers = append(newGroupApprovers, *req.ApproverGroups...)
+ } else {
+ newGroupApprovers = compareSlices(doc.ApproverGroups, *req.ApproverGroups)
+ }
}
- // Compare approvers in request and the current document (before we patch
- // the document) to find the approvers to email.
- var approversToEmail []string
- if len(doc.Approvers) == 0 && req.Approvers != nil &&
- len(*req.Approvers) != 0 {
- // If there are no approvers of the document email the approvers in the
- // request.
- approversToEmail = *req.Approvers
- } else if req.Approvers != nil && len(*req.Approvers) != 0 {
- // Only compare when there are stored approvers and approvers in the
- // request.
- approversToEmail = compareSlices(doc.Approvers, *req.Approvers)
+ // Determine removed individual approvers and group approvers (to revoke access).
+ var removedUserApprovers []string
+ var removedGroupApprovers []string
+ if req.Approvers != nil {
+ if len(doc.Approvers) != 0 {
+ if len(*req.Approvers) == 0 {
+ // All approvers are being removed
+ removedUserApprovers = doc.Approvers
+ } else {
+ // Compare approvers when there are stored approvers
+ // and there are approvers in the request
+ // Find approvers that exist in current doc but NOT in the request
+ removedUserApprovers = compareSlices(
+ *req.Approvers, doc.Approvers)
+ }
+ }
}
- if len(doc.ApproverGroups) == 0 && req.ApproverGroups != nil &&
- len(*req.ApproverGroups) != 0 {
- // If there are no approver groups for the document, add all approver
- // groups in the request.
- approversToEmail = append(approversToEmail, *req.ApproverGroups...)
- } else if req.ApproverGroups != nil && len(*req.ApproverGroups) != 0 {
- // Only compare when there are stored approver groups and approver
- // groups in the request.
- approversToEmail = append(
- approversToEmail,
- compareSlices(doc.ApproverGroups, *req.ApproverGroups)...,
- )
+ if req.ApproverGroups != nil {
+ if len(doc.ApproverGroups) != 0 {
+ if len(*req.ApproverGroups) == 0 {
+ // All approver groups are being removed
+ removedGroupApprovers = doc.ApproverGroups
+ } else {
+ // Find approver groups that exist in current doc but NOT in the request
+ removedGroupApprovers = compareSlices(
+ *req.ApproverGroups, doc.ApproverGroups)
+ }
+ }
}
// Patch document (for Algolia).
@@ -484,7 +653,35 @@ func DocumentHandler(srv server.Server) http.Handler {
doc.ApproverGroups = *req.ApproverGroups
}
// Contributors.
+ var newContributors []string
+ var contributorsToAddSharing []string
+ var contributorsToRemoveSharing []string
if req.Contributors != nil {
+ // Determine newly added contributors for email notifications
+ if len(doc.Contributors) == 0 && len(*req.Contributors) > 0 {
+ // No existing contributors => all are new
+ newContributors = append(newContributors, *req.Contributors...)
+ contributorsToAddSharing = *req.Contributors
+ } else if len(*req.Contributors) > 0 {
+ // Find contributors that exist in request but NOT in current doc
+ newContributors = compareSlices(doc.Contributors, *req.Contributors)
+ contributorsToAddSharing = compareSlices(doc.Contributors, *req.Contributors)
+ }
+
+ // Find out contributors to remove from sharing the document
+ if len(doc.Contributors) != 0 {
+ if len(*req.Contributors) == 0 {
+ // All contributors are being removed
+ contributorsToRemoveSharing = doc.Contributors
+ } else {
+ // Compare contributors when there are stored contributors
+ // and there are contributors in the request
+ // Find contributors that exist in current doc but NOT in the request
+ contributorsToRemoveSharing = compareSlices(
+ *req.Contributors, doc.Contributors)
+ }
+ }
+
doc.Contributors = *req.Contributors
}
// Custom fields.
@@ -553,89 +750,366 @@ func DocumentHandler(srv server.Server) http.Handler {
http.StatusInternalServerError)
return
}
- default:
- srv.Logger.Error("invalid custom field type",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "custom_field", cf.Name,
- "custom_field_type", cf.Type,
- "doc_id", docID)
- http.Error(w,
- fmt.Sprintf(
- "Bad request: invalid type for custom field %q",
- cf.Name,
- ),
- http.StatusBadRequest)
- return
+ default:
+ srv.Logger.Error("invalid custom field type",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "custom_field", cf.Name,
+ "custom_field_type", cf.Type,
+ "doc_id", docID)
+ http.Error(w,
+ fmt.Sprintf(
+ "Bad request: invalid type for custom field %q",
+ cf.Name,
+ ),
+ http.StatusBadRequest)
+ return
+ }
+ }
+ }
+ // Owner.
+ if req.Owners != nil {
+ // Check if this is a contributor acquiring ownership
+ isAcquireOwnership := isContributorAcquiringOwnership(userEmail, *doc, req)
+
+ if isAcquireOwnership {
+ srv.Logger.Info("contributor acquiring document ownership",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "contributor", userEmail,
+ "previous_owner", doc.Owners[0])
+ }
+
+ doc.Owners = *req.Owners
+
+ // Give new owner edit access to the document.
+ if srv.SharePoint != nil {
+ if err := srv.SharePoint.ShareFile(
+ docID, doc.Owners[0], "writer"); err != nil {
+ srv.Logger.Error("error sharing file with new owner",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "new_owner", doc.Owners[0])
+ http.Error(w, "Error patching document",
+ http.StatusInternalServerError)
+ return
+ }
+ } else {
+ if err := srv.GWService.ShareFile(
+ docID, doc.Owners[0], "writer"); err != nil {
+ srv.Logger.Error("error sharing file with new owner",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "new_owner", doc.Owners[0])
+ http.Error(w, "Error patching document",
+ http.StatusInternalServerError)
+ return
+ }
+ }
+ }
+ // Status.
+ if req.Status != nil {
+ doc.Status = *req.Status
+ }
+ // Summary.
+ if req.Summary != nil {
+ doc.Summary = *req.Summary
+ }
+ // Title.
+ if req.Title != nil {
+ doc.Title = *req.Title
+
+ // Rename the file to match the new title.
+ abbr := strings.SplitN(doc.DocNumber, "-", 2)[0]
+ newFileName := fmt.Sprintf("%s-%s", abbr, *req.Title)
+
+ if srv.SharePoint != nil {
+ // Sanitize the file name for SharePoint.
+ newFileName = strings.NewReplacer(
+ "[", "(", "]", ")", "#", "-", "%", "-", "&", "and",
+ "*", "-", ":", "-", "<", "-", ">", "-", "?", "",
+ "/", "-", "\\", "-", "{", "(", "|", "-", "}", ")",
+ "~", "-",
+ ).Replace(newFileName)
+ newFileName = fmt.Sprintf("%s.docx", newFileName)
+
+ if err := srv.SharePoint.RenameFile(docID, newFileName); err != nil {
+ srv.Logger.Error("error renaming file",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "new_file_name", newFileName)
+ srv.Logger.Warn("continuing document patch despite file rename failure")
+ } else {
+ srv.Logger.Info("successfully renamed file",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "new_file_name", newFileName)
+ }
+ } else {
+ if err := srv.GWService.RenameFile(docID, newFileName); err != nil {
+ srv.Logger.Error("error renaming file",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "new_file_name", newFileName)
+ srv.Logger.Warn("continuing document patch despite file rename failure")
+ }
+ }
+ }
+
+ // Share file with contributors.
+ if len(contributorsToAddSharing) > 0 {
+ if srv.SharePoint != nil {
+ if err := srv.SharePoint.ShareFileWithMultipleUsers(docID, "writer", contributorsToAddSharing); err != nil {
+ srv.Logger.Error("error sharing file with new contributors",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "contributors", contributorsToAddSharing)
+ http.Error(w, "Error patching document", http.StatusInternalServerError)
+ return
+ }
+ } else {
+ for _, user := range contributorsToAddSharing {
+ if err := srv.GWService.ShareFile(docID, user, "writer"); err != nil {
+ srv.Logger.Error("error sharing file with new contributor",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "contributor", user)
+ http.Error(w, "Error patching document", http.StatusInternalServerError)
+ return
+ }
+ }
+ }
+ srv.Logger.Info("shared document with contributors",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "contributors_count", len(contributorsToAddSharing),
+ )
+ }
+
+ // Share with newly added individual approvers (batch for efficiency).
+ if len(newUserApprovers) > 0 {
+ if srv.SharePoint != nil {
+ if err := srv.SharePoint.ShareFileWithMultipleUsers(docID, "writer", newUserApprovers); err != nil {
+ srv.Logger.Error("error sharing file with new user approvers",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "approvers", newUserApprovers)
+ http.Error(w, "Error patching document", http.StatusInternalServerError)
+ return
+ }
+ } else {
+ for _, user := range newUserApprovers {
+ if err := srv.GWService.ShareFile(docID, user, "writer"); err != nil {
+ srv.Logger.Error("error sharing file with new user approver",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "approver", user)
+ http.Error(w, "Error patching document", http.StatusInternalServerError)
+ return
+ }
+ }
+ }
+ }
+
+ // Share with newly added group approvers using group-aware logic (DL expansion vs direct share).
+ for _, gEmail := range newGroupApprovers {
+ if srv.SharePoint != nil {
+ if err := srv.SharePoint.ShareFileWithGroupOrMembers(docID, gEmail, "writer"); err != nil {
+ srv.Logger.Error("error sharing file with new group approver",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "group", gEmail)
+ http.Error(w, "Error patching document", http.StatusInternalServerError)
+ return
+ }
+ } else {
+ if err := srv.GWService.ShareFile(docID, gEmail, "writer"); err != nil {
+ srv.Logger.Error("error sharing file with new group approver",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "group", gEmail)
+ http.Error(w, "Error patching document", http.StatusInternalServerError)
+ return
+ }
+ }
+ }
+
+ // Remove access for removed approvers, approver groups, and contributors.
+ // Build a map of email addresses to permission IDs to facilitate removal.
+ if len(removedUserApprovers) > 0 || len(removedGroupApprovers) > 0 || len(contributorsToRemoveSharing) > 0 {
+ emailToPermissionIDsMap := make(map[string][]string)
+
+ if srv.SharePoint != nil {
+ permissions, err := srv.SharePoint.ListPermissions(docID)
+ if err != nil {
+ srv.Logger.Error("error listing permissions for document",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID)
+ http.Error(w, "Error patching document",
+ http.StatusInternalServerError)
+ return
+ }
+ for _, p := range permissions {
+ if p.GrantedTo.User.Email == "" {
+ continue
+ }
+ if slices.Contains(p.Role, "owner") {
+ continue
+ }
+ email := p.GrantedTo.User.Email
+ if _, exists := emailToPermissionIDsMap[email]; !exists {
+ emailToPermissionIDsMap[email] = make([]string, 0)
+ }
+ emailToPermissionIDsMap[email] = append(
+ emailToPermissionIDsMap[email], p.ID)
+ }
+ } else {
+ permissions, err := srv.GWService.ListPermissions(docID)
+ if err != nil {
+ srv.Logger.Error("error listing permissions for document",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID)
+ http.Error(w, "Error patching document",
+ http.StatusInternalServerError)
+ return
+ }
+ for _, p := range permissions {
+ if p.EmailAddress == "" {
+ continue
+ }
+ if p.Role == "owner" {
+ continue
+ }
+ if _, exists := emailToPermissionIDsMap[p.EmailAddress]; !exists {
+ emailToPermissionIDsMap[p.EmailAddress] = make([]string, 0)
+ }
+ emailToPermissionIDsMap[p.EmailAddress] = append(
+ emailToPermissionIDsMap[p.EmailAddress], p.Id)
+ }
+ }
+
+ // Remove individual approvers.
+ for _, a := range removedUserApprovers {
+ // Only remove approver if the email associated with the permission
+ // doesn't match owner email(s).
+ if !contains(doc.Owners, a) {
+ if err := removeSharing(srv, docID, a, emailToPermissionIDsMap); err != nil {
+ srv.Logger.Error("error removing approver from file",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "approver", a)
+ http.Error(w, "Error patching document",
+ http.StatusInternalServerError)
+ return
+ }
+ }
+ }
+ if len(removedUserApprovers) > 0 {
+ srv.Logger.Info("removed approvers from document",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "approvers_count", len(removedUserApprovers),
+ )
+ }
+
+ // Remove group approvers.
+ for _, g := range removedGroupApprovers {
+ // Only remove group if it doesn't match owner email(s).
+ if !contains(doc.Owners, g) {
+ if err := removeSharing(srv, docID, g, emailToPermissionIDsMap); err != nil {
+ srv.Logger.Error("error removing approver group from file",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "approver_group", g)
+ http.Error(w, "Error patching document",
+ http.StatusInternalServerError)
+ return
+ }
+ }
+ }
+ if len(removedGroupApprovers) > 0 {
+ srv.Logger.Info("removed approver groups from document",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "approver_groups_count", len(removedGroupApprovers),
+ )
+ }
+
+ // Remove contributors.
+ for _, c := range contributorsToRemoveSharing {
+ // Only remove contributor if the email
+ // associated with the permission doesn't
+ // match owner email(s).
+ if !contains(doc.Owners, c) {
+ if err := removeSharing(srv, docID, c, emailToPermissionIDsMap); err != nil {
+ srv.Logger.Error("error removing contributor from file",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "contributor", c)
+ http.Error(w, "Error patching document",
+ http.StatusInternalServerError)
+ return
+ }
}
}
- }
- // Owner.
- if req.Owners != nil {
- doc.Owners = *req.Owners
-
- // Give new owner edit access to the document.
- if err := srv.GWService.ShareFile(
- docID, doc.Owners[0], "writer"); err != nil {
- srv.Logger.Error("error sharing file with new owner",
- "error", err,
+ if len(contributorsToRemoveSharing) > 0 {
+ srv.Logger.Info("removed contributors from document",
"method", r.Method,
"path", r.URL.Path,
- "doc_id", docID,
- "new_owner", doc.Owners[0])
- http.Error(w, "Error patching document",
- http.StatusInternalServerError)
- return
+ "contributors_count", len(contributorsToRemoveSharing),
+ )
}
}
- // Status.
- if req.Status != nil {
- doc.Status = *req.Status
- }
- // Summary.
- if req.Summary != nil {
- doc.Summary = *req.Summary
- }
- // Title.
- if req.Title != nil {
- doc.Title = *req.Title
- }
- // Give new document approvers edit access to the document.
- for _, a := range approversToEmail {
- if err := srv.GWService.ShareFile(docID, a, "writer"); err != nil {
- srv.Logger.Error("error sharing file with approver",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- "approver", a)
+ // Replace the doc header (Google-only; SharePoint headers
+ // are managed by the Hermes Add-In for Word).
+ if !srv.IsSharePoint() {
+ if err := doc.ReplaceHeader(
+ srv.Config.BaseURL, false, srv.GWService,
+ ); err != nil {
+ srv.Logger.Error("error replacing document header",
+ "error", err, "doc_id", docID)
http.Error(w, "Error patching document",
http.StatusInternalServerError)
return
}
}
- // Replace the doc header.
- if err := doc.ReplaceHeader(
- srv.Config.BaseURL, false, srv.GWService,
- ); err != nil {
- srv.Logger.Error("error replacing document header",
- "error", err, "doc_id", docID)
- http.Error(w, "Error patching document",
- http.StatusInternalServerError)
- return
- }
-
- // Rename file with new title.
- srv.GWService.RenameFile(docID,
- fmt.Sprintf("[%s] %s", doc.DocNumber, doc.Title))
-
// Get document record from database so we can modify it for updating.
- model := models.Document{
- GoogleFileID: docID,
- }
+ model := srv.NewDocumentByFileID(docID)
if err := model.Get(srv.DB); err != nil {
srv.Logger.Error("error getting document from database",
"error", err,
@@ -820,80 +1294,117 @@ func DocumentHandler(srv server.Server) http.Handler {
"method", r.Method,
"path", r.URL.Path,
)
- http.Error(w, "Error patching document",
- http.StatusInternalServerError)
- return
- }
-
- // Get name of new document owner.
- newOwner := email.User{
- EmailAddress: doc.Owners[0],
- }
- ppl, err := srv.GWService.SearchPeople(
- doc.Owners[0], "emailAddresses,names")
- if err != nil {
- srv.Logger.Warn("error searching directory for new owner",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- "person", doc.Owners[0],
- )
- }
- if len(ppl) == 1 && ppl[0].Names != nil {
- newOwner.Name = ppl[0].Names[0].DisplayName
- }
+ // Log error but don't fail the request.
+ } else {
+ // Get name of new document owner.
+ newOwner := email.User{
+ EmailAddress: doc.Owners[0],
+ }
+ if srv.SharePoint != nil {
+ person, err := srv.SharePoint.GetPersonByEmail(doc.Owners[0])
+ if err != nil {
+ srv.Logger.Warn("error getting person details for new owner",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "person", doc.Owners[0],
+ )
+ } else if person != nil && person.DisplayName != "" {
+ newOwner.Name = person.DisplayName
+ }
+ } else {
+ ppl, err := srv.GWService.SearchPeople(doc.Owners[0], "emailAddresses,names")
+ if err != nil {
+ srv.Logger.Warn("error getting person details for new owner",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "person", doc.Owners[0],
+ )
+ } else if len(ppl) > 0 && len(ppl[0].Names) > 0 {
+ newOwner.Name = ppl[0].Names[0].DisplayName
+ }
+ }
- // Get name of old document owner.
- oldOwner := email.User{
- EmailAddress: userEmail,
- }
- ppl, err = srv.GWService.SearchPeople(
- userEmail, "emailAddresses,names")
- if err != nil {
- srv.Logger.Warn("error searching directory for old owner",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- "person", doc.Owners[0],
- )
- }
- if len(ppl) == 1 && ppl[0].Names != nil {
- oldOwner.Name = ppl[0].Names[0].DisplayName
- }
+ // Get name of old document owner.
+ oldOwner := email.User{
+ EmailAddress: userEmail,
+ }
+ if srv.SharePoint != nil {
+ person, err := srv.SharePoint.GetPersonByEmail(userEmail)
+ if err != nil {
+ srv.Logger.Warn("error getting person details for old owner",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "person", userEmail,
+ )
+ } else if person != nil && person.DisplayName != "" {
+ oldOwner.Name = person.DisplayName
+ }
+ } else {
+ ppl, err := srv.GWService.SearchPeople(userEmail, "emailAddresses,names")
+ if err != nil {
+ srv.Logger.Warn("error getting person details for old owner",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "person", userEmail,
+ )
+ } else if len(ppl) > 0 && len(ppl[0].Names) > 0 {
+ oldOwner.Name = ppl[0].Names[0].DisplayName
+ }
+ }
- if err := email.SendNewOwnerEmail(
- email.NewOwnerEmailData{
- BaseURL: srv.Config.BaseURL,
- DocumentShortName: doc.DocNumber,
- DocumentStatus: doc.Status,
- DocumentTitle: doc.Title,
- DocumentType: doc.DocType,
- DocumentURL: docURL,
- NewDocumentOwner: newOwner,
- OldDocumentOwner: oldOwner,
- Product: doc.Product,
- },
- []string{doc.Owners[0]},
- srv.Config.Email.FromAddress,
- srv.GWService,
- ); err != nil {
- srv.Logger.Error("error sending new owner email",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- http.Error(w, "Error patching document",
- http.StatusInternalServerError)
- return
+ // Send email asynchronously to avoid blocking the response.
+ go func() {
+ if err := email.SendNewOwnerEmail(
+ email.NewOwnerEmailData{
+ BaseURL: srv.Config.BaseURL,
+ DocumentShortName: doc.DocNumber,
+ DocumentStatus: doc.Status,
+ DocumentTitle: doc.Title,
+ DocumentType: doc.DocType,
+ DocumentURL: docURL,
+ NewDocumentOwner: newOwner,
+ OldDocumentOwner: oldOwner,
+ Product: doc.Product,
+ },
+ []string{doc.Owners[0]},
+ srv.Config.Email.FromAddress,
+ srv.GetEmailSender(),
+ ); err != nil {
+ srv.Logger.Error("error sending new owner email",
+ "error", err,
+ "doc_id", docID,
+ "new_owner", doc.Owners[0],
+ )
+ } else {
+ srv.Logger.Info("new owner email sent",
+ "doc_id", docID,
+ "new_owner", doc.Owners[0],
+ )
+ }
+ }()
}
}
// Send emails to new approvers.
if srv.Config.Email != nil && srv.Config.Email.Enabled {
- if len(approversToEmail) > 0 {
+ // Collect new approvers (individuals and groups)
+ newApproverRecipients := []string{}
+
+ // Add new individual approvers
+ newApproverRecipients = append(newApproverRecipients, newUserApprovers...)
+
+ // Add new group approvers directly (send to group mailbox)
+ newApproverRecipients = append(newApproverRecipients, newGroupApprovers...)
+
+ if len(newApproverRecipients) > 0 {
// Get document URL.
docURL, err := getDocumentURL(srv.Config.BaseURL, docID)
if err != nil {
@@ -903,16 +1414,60 @@ func DocumentHandler(srv server.Server) http.Handler {
"method", r.Method,
"path", r.URL.Path,
)
- http.Error(w, "Error patching document",
- http.StatusInternalServerError)
- return
+ // Log error but don't fail the request.
+ } else {
+ srv.Logger.Info("review request email queued",
+ "doc_id", docID,
+ "approver_count", len(newApproverRecipients),
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+
+ go helpers.SendEmailWithRetry(
+ &srv,
+ func() error {
+ return email.SendReviewRequestedEmail(
+ email.ReviewRequestedEmailData{
+ BaseURL: srv.Config.BaseURL,
+ DocumentOwner: doc.Owners[0],
+ DocumentShortName: doc.DocNumber,
+ DocumentTitle: doc.Title,
+ DocumentURL: docURL,
+ Product: doc.Product,
+ DocumentType: doc.DocType,
+ DocumentStatus: doc.Status,
+ },
+ newApproverRecipients,
+ srv.Config.Email.FromAddress,
+ srv.GetEmailSender(),
+ )
+ },
+ docID,
+ "review_requested",
+ r,
+ )
}
+ }
+ }
- // TODO: use an asynchronous method for sending emails because we
- // can't currently recover gracefully on a failure here.
- for _, approverEmail := range approversToEmail {
- err := email.SendReviewRequestedEmail(
- email.ReviewRequestedEmailData{
+ if len(newContributors) > 0 {
+ docURL := fmt.Sprintf("%s/document/%s", srv.Config.BaseURL, docID)
+ if doc.Status != "Approved" {
+ docURL += "?draft=true"
+ }
+
+ srv.Logger.Info("contributor email queued",
+ "doc_id", docID,
+ "contributor_count", len(newContributors),
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+
+ go helpers.SendEmailWithRetry(
+ &srv,
+ func() error {
+ return email.SendContributorAddedEmail(
+ email.ContributorAddedEmailData{
BaseURL: srv.Config.BaseURL,
DocumentOwner: doc.Owners[0],
DocumentShortName: doc.DocNumber,
@@ -922,27 +1477,57 @@ func DocumentHandler(srv server.Server) http.Handler {
DocumentType: doc.DocType,
DocumentStatus: doc.Status,
},
- []string{approverEmail},
+ newContributors,
srv.Config.Email.FromAddress,
- srv.GWService,
+ srv.GetEmailSender(),
)
- if err != nil {
- srv.Logger.Error("error sending approver email",
+ },
+ docID,
+ "contributor_added",
+ r,
+ )
+ }
+
+ isPublishTransition := strings.EqualFold(previousStatus, "WIP") &&
+ strings.EqualFold(doc.Status, "In-Review")
+
+ if isPublishTransition {
+ if srv.SharePoint != nil {
+ grantedGroups, err := srv.SharePoint.GrantGroupsReadAccess(docID, "reader", publishReaderGroups, publishGroupDisplayNames)
+ if err != nil {
+ srv.Logger.Error("error granting reader access to publish groups",
+ "error", err,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error patching document", http.StatusInternalServerError)
+ return
+ }
+ if len(grantedGroups) > 0 {
+ srv.Logger.Info("granted group access on publish",
+ "doc_id", docID,
+ "groups", strings.Join(grantedGroups, ", "),
+ )
+ }
+ } else {
+ var grantedGroups []string
+ for _, group := range publishReaderGroups {
+ if err := srv.GWService.ShareFile(docID, group, "reader"); err != nil {
+ srv.Logger.Error("error granting reader access to publish group",
"error", err,
"doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
+ "group", group,
)
- http.Error(w, "Error patching document",
- http.StatusInternalServerError)
+ http.Error(w, "Error patching document", http.StatusInternalServerError)
return
}
+ grantedGroups = append(grantedGroups, group)
+ }
+ if len(grantedGroups) > 0 {
+ srv.Logger.Info("granted group access on publish",
+ "doc_id", docID,
+ "groups", strings.Join(grantedGroups, ", "),
+ )
}
- srv.Logger.Info("approver emails sent",
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- )
}
}
@@ -959,7 +1544,6 @@ func DocumentHandler(srv server.Server) http.Handler {
return
}
}
-
w.WriteHeader(http.StatusOK)
srv.Logger.Info("patched document",
"doc_id", docID,
@@ -967,6 +1551,16 @@ func DocumentHandler(srv server.Server) http.Handler {
"path", r.URL.Path,
)
+ // Log document access with Datadog ACCESS tag
+ operation, updatedAttrs := buildDocumentOperation(req)
+ srv.Logger.Info("ACCESS",
+ "user_email", userEmail,
+ "doc_id", docID,
+ "operation", operation,
+ "updated_attributes", updatedAttrs,
+ "mode", "published",
+ )
+
// Request post-processing.
go func() {
// Convert document to Algolia object.
@@ -1015,9 +1609,8 @@ func DocumentHandler(srv server.Server) http.Handler {
return
}
// Get document from database.
- dbDoc := models.Document{
- GoogleFileID: docID,
- }
+ dbDoc := srv.NewDocumentByFileID(docID)
+
if err := dbDoc.Get(srv.DB); err != nil {
srv.Logger.Error(
"error getting document from database for data comparison",
@@ -1031,9 +1624,7 @@ func DocumentHandler(srv server.Server) http.Handler {
// Get all reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(srv.DB, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: srv.NewDocumentByFileID(docID),
}); err != nil {
srv.Logger.Error(
"error getting all reviews for document for data comparison",
@@ -1068,7 +1659,7 @@ func DocumentHandler(srv server.Server) http.Handler {
// provided email address, using the document file ID and viewed at time for a
// document view event.
func updateRecentlyViewedDocs(
- email, docID string, db *gorm.DB, viewedAt time.Time) error {
+ email, docID string, db *gorm.DB, viewedAt time.Time, useSharePoint bool) error {
// Get user (if exists).
u := models.User{
EmailAddress: email,
@@ -1079,9 +1670,7 @@ func updateRecentlyViewedDocs(
}
// Get viewed document in database.
- doc := models.Document{
- GoogleFileID: docID,
- }
+ doc := models.NewDocumentByFileID(docID, useSharePoint)
if err := doc.Get(db); err != nil {
return fmt.Errorf("error getting viewed document: %w", err)
}
@@ -1156,6 +1745,11 @@ func parseDocumentsURLPath(path, collection string) (
fmt.Sprintf(
`^\/api\/v2\/%s\/([0-9A-Za-z_\-]+)\/shareable$`,
collection))
+ // archived isn't really a subcollection, but we'll go with it.
+ archivedRE := regexp.MustCompile(
+ fmt.Sprintf(
+ `^\/api\/v2\/%s\/([0-9A-Za-z_\-]+)\/archived$`,
+ collection))
switch {
case noSubcollectionRE.MatchString(path):
@@ -1188,6 +1782,17 @@ func parseDocumentsURLPath(path, collection string) (
}
return matches[1], shareableDocumentSubcollectionRequestType, nil
+ case archivedRE.MatchString(path):
+ matches := archivedRE.
+ FindStringSubmatch(path)
+ if len(matches) != 2 {
+ return "",
+ archivedDocumentSubcollectionRequestType,
+ fmt.Errorf(
+ "wrong number of string submatches for archived subcollection URL path")
+ }
+ return matches[1], archivedDocumentSubcollectionRequestType, nil
+
default:
return "",
unspecifiedDocumentSubcollectionRequestType,
@@ -1195,14 +1800,38 @@ func parseDocumentsURLPath(path, collection string) (
}
}
+func isContributorAcquiringOwnership(
+ userEmail string,
+ doc document.Document,
+ req DocumentPatchRequest,
+) bool {
+ if req.Owners == nil || len(*req.Owners) != 1 {
+ return false
+ }
+
+ if strings.EqualFold(doc.Owners[0], userEmail) {
+ return false
+ }
+
+ if !helpers.StringSliceContainsFold(doc.Contributors, userEmail) {
+ return false
+ }
+
+ return strings.EqualFold((*req.Owners)[0], userEmail)
+}
+
// authorizeDocumentPatchRequest authorizes a PATCH request to a document.
+// - Document owners can patch any field.
+// - Approvers can only patch the Approvers field to remove themselves.
+// - Contributors can only patch the Owners field to acquire ownership (setting themselves as the sole owner).
+// Additional validation in the main handler ensures the current owner is no longer with the company.
func authorizeDocumentPatchRequest(
userEmail string,
doc document.Document,
req DocumentPatchRequest,
) error {
// The document owner can patch any field.
- if doc.Owners[0] == userEmail {
+ if strings.EqualFold(doc.Owners[0], userEmail) {
return nil
}
@@ -1236,7 +1865,7 @@ func authorizeDocumentPatchRequest(
// Request approvers should be a subset of document approvers and not
// contain the requesting user.
for _, ra := range reqApprovers {
- if ra == userEmail || !helpers.StringSliceContains(docApprovers, ra) {
+ if strings.EqualFold(ra, userEmail) || !helpers.StringSliceContains(docApprovers, ra) {
return errors.New(
"approvers can only patch a document to remove themselves as an approver")
}
@@ -1245,5 +1874,109 @@ func authorizeDocumentPatchRequest(
return nil
}
- return errors.New("only owners or approvers can patch a document")
+ // Contributors can only patch the Owners field to acquire ownership of the document.
+ if helpers.StringSliceContainsFold(doc.Contributors, userEmail) {
+ // Request should only have one non-nil field, Owners.
+ numNonNilFields := 0
+ reqValue := reflect.ValueOf(req)
+ for i := 0; i < reqValue.NumField(); i++ {
+ fieldValue := reqValue.Field(i)
+ if fieldValue.Kind() == reflect.Ptr && !fieldValue.IsNil() {
+ numNonNilFields++
+ }
+ }
+ if numNonNilFields != 1 || req.Owners == nil {
+ return errors.New(
+ "contributors can only patch the owners field to acquire ownership")
+ }
+
+ // The Owners field should contain exactly one email (the contributor's email).
+ if len(*req.Owners) != 1 {
+ return errors.New(
+ "contributors can only acquire ownership by setting themselves as the sole owner")
+ }
+
+ // The email in the Owners field must match the requesting user's email.
+ if !strings.EqualFold((*req.Owners)[0], userEmail) {
+ return errors.New(
+ "contributors can only acquire ownership by setting themselves as the owner")
+ }
+
+ return nil
+ }
+
+ return errors.New("only owners, approvers, or contributors can patch a document")
+}
+
+// buildDocumentOperation determines the primary operation and builds a list of updated attributes
+// from a DocumentPatchRequest for logging purposes.
+func buildDocumentOperation(req DocumentPatchRequest) (string, string) {
+ var attrs []string
+ var operation string
+
+ // Determine primary operation based on what's being changed
+ if req.Owners != nil {
+ operation = "ownership_transferred"
+ attrs = append(attrs, "owners")
+ }
+ if req.Status != nil {
+ if operation == "" {
+ operation = "status_changed"
+ }
+ attrs = append(attrs, "status")
+ }
+ if req.Approvers != nil {
+ if operation == "" {
+ operation = "approvers_updated"
+ }
+ attrs = append(attrs, "approvers")
+ }
+ if req.ApproverGroups != nil {
+ if operation == "" {
+ operation = "approver_groups_updated"
+ }
+ attrs = append(attrs, "approverGroups")
+ }
+ if req.Contributors != nil {
+ if operation == "" {
+ operation = "contributors_updated"
+ }
+ attrs = append(attrs, "contributors")
+ }
+ if req.CustomFields != nil {
+ if operation == "" {
+ operation = "custom_fields_updated"
+ }
+ attrs = append(attrs, "customFields")
+ }
+ if req.Summary != nil {
+ if operation == "" {
+ operation = "summary_updated"
+ }
+ attrs = append(attrs, "summary")
+ }
+ if req.Title != nil {
+ if operation == "" {
+ operation = "title_updated"
+ }
+ attrs = append(attrs, "title")
+ }
+
+ if operation == "" {
+ operation = "document_updated"
+ }
+
+ // If multiple fields updated, mark as bulk update
+ if len(attrs) > 1 {
+ operation = "document_bulk_update"
+ }
+
+ var attrsList string
+ if len(attrs) == 0 {
+ attrsList = "none"
+ } else {
+ attrsList = fmt.Sprintf("[%s]", strings.Join(attrs, ", "))
+ }
+
+ return operation, attrsList
}
diff --git a/internal/api/v2/documents_related_resources.go b/internal/api/v2/documents_related_resources.go
index 30a63d86b..00dbaa4b2 100644
--- a/internal/api/v2/documents_related_resources.go
+++ b/internal/api/v2/documents_related_resources.go
@@ -24,8 +24,8 @@ type externalLinkRelatedResourcePutRequest struct {
}
type hermesDocumentRelatedResourcePutRequest struct {
- GoogleFileID string `json:"googleFileID"`
- SortOrder int `json:"sortOrder"`
+ FileID string `json:"FileID"`
+ SortOrder int `json:"sortOrder"`
}
type relatedResourcesGetResponse struct {
@@ -40,7 +40,7 @@ type externalLinkRelatedResourceGetResponse struct {
}
type hermesDocumentRelatedResourceGetResponse struct {
- GoogleFileID string `json:"googleFileID"`
+ FileID string `json:"FileID"`
Title string `json:"title"`
DocumentType string `json:"documentType"`
DocumentNumber string `json:"documentNumber"`
@@ -56,12 +56,11 @@ func documentsResourceRelatedResourcesHandler(
l hclog.Logger,
algoRead *algolia.Client,
db *gorm.DB,
+ useSharePoint bool,
) {
switch r.Method {
case "GET":
- d := models.Document{
- GoogleFileID: docID,
- }
+ d := models.NewDocumentByFileID(docID, useSharePoint)
if err := d.Get(db); err != nil {
l.Error("error getting document from database",
"error", err,
@@ -117,15 +116,16 @@ func documentsResourceRelatedResourcesHandler(
// Add Hermes document related resources.
for _, hdrr := range hdrrs {
// Get document object from Algolia.
+ targetDocID := hdrr.Document.GetFileIdentifier()
var algoObj map[string]any
- err = algoRead.Docs.GetObject(hdrr.Document.GoogleFileID, &algoObj)
+ err = algoRead.Docs.GetObject(targetDocID, &algoObj)
if err != nil {
l.Error("error getting related resource document from Algolia",
"error", err,
"path", r.URL.Path,
"method", r.Method,
"doc_id", docID,
- "target_doc_id", hdrr.Document.GoogleFileID,
+ "target_doc_id", targetDocID,
)
http.Error(w, "Error accessing document",
http.StatusInternalServerError)
@@ -148,7 +148,7 @@ func documentsResourceRelatedResourcesHandler(
resp.HermesDocuments = append(
resp.HermesDocuments,
hermesDocumentRelatedResourceGetResponse{
- GoogleFileID: hdrr.Document.GoogleFileID,
+ FileID: targetDocID,
Title: doc.Title,
DocumentType: doc.DocType,
DocumentNumber: doc.DocNumber,
@@ -177,6 +177,12 @@ func documentsResourceRelatedResourcesHandler(
// resources).
userEmail := r.Context().Value("userEmail").(string)
if doc.Owners[0] != userEmail {
+ l.Warn("unauthorized attempt to replace document related resources",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "user_email", userEmail,
+ "owner", doc.Owners[0])
http.Error(w, "Not a document owner", http.StatusUnauthorized)
return
}
@@ -199,9 +205,7 @@ func documentsResourceRelatedResourcesHandler(
for _, elrr := range req.ExternalLinks {
elrrs = append(elrrs, models.DocumentRelatedResourceExternalLink{
RelatedResource: models.DocumentRelatedResource{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: models.NewDocumentByFileID(docID, useSharePoint),
SortOrder: elrr.SortOrder,
},
Name: elrr.Name,
@@ -214,21 +218,15 @@ func documentsResourceRelatedResourcesHandler(
for _, hdrr := range req.HermesDocuments {
hdrrs = append(hdrrs, models.DocumentRelatedResourceHermesDocument{
RelatedResource: models.DocumentRelatedResource{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: models.NewDocumentByFileID(docID, useSharePoint),
SortOrder: hdrr.SortOrder,
},
- Document: models.Document{
- GoogleFileID: hdrr.GoogleFileID,
- },
+ Document: models.NewDocumentByFileID(hdrr.FileID, useSharePoint),
})
}
// Replace related resources for document.
- doc := models.Document{
- GoogleFileID: docID,
- }
+ doc := models.NewDocumentByFileID(docID, useSharePoint)
if err := doc.ReplaceRelatedResources(db, elrrs, hdrrs); err != nil {
l.Error("error replacing related resources for document",
"error", err,
diff --git a/internal/api/v2/documents_test.go b/internal/api/v2/documents_test.go
index dba6e8d52..335605644 100644
--- a/internal/api/v2/documents_test.go
+++ b/internal/api/v2/documents_test.go
@@ -77,6 +77,14 @@ func TestAuthorizeDocumentPatchRequest(t *testing.T) {
req: DocumentPatchRequest{},
shouldErr: false,
},
+ "owner should be authorized ignoring case": {
+ userEmail: "OWNER@example.com",
+ doc: document.Document{
+ Owners: []string{"owner@example.com"},
+ },
+ req: DocumentPatchRequest{},
+ shouldErr: false,
+ },
"not owner should not be authorized": {
userEmail: "not.owner@example.com",
doc: document.Document{
@@ -169,6 +177,17 @@ func TestAuthorizeDocumentPatchRequest(t *testing.T) {
},
shouldErr: true,
},
+ "contributor should be authorized to acquire ownership ignoring case": {
+ userEmail: "Contributor@Example.com",
+ doc: document.Document{
+ Owners: []string{"owner@example.com"},
+ Contributors: []string{"contributor@example.com"},
+ },
+ req: DocumentPatchRequest{
+ Owners: &[]string{"contributor@example.com"},
+ },
+ shouldErr: false,
+ },
}
for name, c := range cases {
@@ -184,3 +203,41 @@ func TestAuthorizeDocumentPatchRequest(t *testing.T) {
})
}
}
+
+func TestIsContributorAcquiringOwnership(t *testing.T) {
+ cases := map[string]struct {
+ userEmail string
+ doc document.Document
+ req DocumentPatchRequest
+ want bool
+ }{
+ "contributor acquiring ownership is detected ignoring case": {
+ userEmail: "Contributor@Example.com",
+ doc: document.Document{
+ Owners: []string{"owner@example.com"},
+ Contributors: []string{"contributor@example.com"},
+ },
+ req: DocumentPatchRequest{
+ Owners: &[]string{"CONTRIBUTOR@example.com"},
+ },
+ want: true,
+ },
+ "owner transfer is not treated as contributor acquisition even if owner is also contributor": {
+ userEmail: "owner@example.com",
+ doc: document.Document{
+ Owners: []string{"owner@example.com"},
+ Contributors: []string{"owner@example.com", "contributor@example.com"},
+ },
+ req: DocumentPatchRequest{
+ Owners: &[]string{"new.owner@example.com"},
+ },
+ want: false,
+ },
+ }
+
+ for name, c := range cases {
+ t.Run(name, func(t *testing.T) {
+ assert.Equal(t, c.want, isContributorAcquiringOwnership(c.userEmail, c.doc, c.req))
+ })
+ }
+}
diff --git a/internal/api/v2/drafts.go b/internal/api/v2/drafts.go
index 296c717bb..5173bc2d1 100644
--- a/internal/api/v2/drafts.go
+++ b/internal/api/v2/drafts.go
@@ -6,6 +6,8 @@ import (
"errors"
"fmt"
"net/http"
+ "slices"
+
"reflect"
"strconv"
"strings"
@@ -15,9 +17,9 @@ import (
"github.com/algolia/algoliasearch-client-go/v3/algolia/search"
"github.com/hashicorp-forge/hermes/internal/config"
"github.com/hashicorp-forge/hermes/internal/email"
+ "github.com/hashicorp-forge/hermes/internal/helpers"
"github.com/hashicorp-forge/hermes/internal/server"
"github.com/hashicorp-forge/hermes/pkg/document"
- gw "github.com/hashicorp-forge/hermes/pkg/googleworkspace"
hcd "github.com/hashicorp-forge/hermes/pkg/hashicorpdocs"
"github.com/hashicorp-forge/hermes/pkg/models"
"golang.org/x/oauth2/jwt"
@@ -101,6 +103,10 @@ func DraftsHandler(srv server.Server) http.Handler {
}
if req.Title == "" {
+ srv.Logger.Warn("draft title is required",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "user_email", userEmail)
http.Error(w, "Bad request: title is required", http.StatusBadRequest)
return
}
@@ -124,266 +130,339 @@ func DraftsHandler(srv server.Server) http.Handler {
if req.ProductAbbreviation == "" {
req.ProductAbbreviation = "TODO"
}
- title := fmt.Sprintf("[%s-???] %s", req.ProductAbbreviation, req.Title)
+
+ // Create a filename based on product and title.
+ fileNameBase := fmt.Sprintf("%s-%s", req.ProductAbbreviation, req.Title)
+
+ // Sanitize the filename for SharePoint (remove characters that
+ // SharePoint doesn't allow: # % & * : < > ? / \ { | } ~).
+ // Google Drive doesn't need this sanitization.
+ var sanitizedTitle string
+ if srv.SharePoint != nil {
+ sanitizedTitle = strings.NewReplacer(
+ "[", "(",
+ "]", ")",
+ "#", "-",
+ "%", "-",
+ "&", "and",
+ "*", "-",
+ ":", "-",
+ "<", "-",
+ ">", "-",
+ "?", "",
+ "/", "-",
+ "\\", "-",
+ "{", "(",
+ "|", "-",
+ "}", ")",
+ "~", "-",
+ ).Replace(fileNameBase)
+ // Add .docx extension for SharePoint.
+ sanitizedTitle = fmt.Sprintf("%s.docx", sanitizedTitle)
+ } else {
+ sanitizedTitle = fileNameBase
+ }
+
+ // Log the filename we're going to create.
+ srv.Logger.Info("Creating document with filename",
+ "filename", sanitizedTitle,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "template", template,
+ )
var (
- err error
- f *drive.File
+ err error
+ fileID string
+ doc *document.Document
)
- // Copy template to new draft file.
- if srv.Config.GoogleWorkspace.Auth != nil &&
- srv.Config.GoogleWorkspace.Auth.CreateDocsAsUser {
- // If configured to create documents as the logged-in Hermes user,
- // create a new Google Drive service to do this.
- ctx := context.Background()
- conf := &jwt.Config{
- Email: srv.Config.GoogleWorkspace.Auth.ClientEmail,
- PrivateKey: []byte(srv.Config.GoogleWorkspace.Auth.PrivateKey),
- Scopes: []string{
- "https://www.googleapis.com/auth/drive",
- },
- Subject: userEmail,
- TokenURL: srv.Config.GoogleWorkspace.Auth.TokenURL,
- }
- client := conf.Client(ctx)
- copyTemplateSvc := *srv.GWService
- copyTemplateSvc.Drive, err = drive.NewService(
- ctx, option.WithHTTPClient(client))
+ if srv.SharePoint != nil {
+ // Create draft in SharePoint
+ fileDetails, err := srv.SharePoint.CopyFile(
+ template, // Template ID
+ sanitizedTitle, // New file name (sanitized for SharePoint)
+ srv.Config.SharePoint.DraftsFolder, // Destination folder
+ )
+ srv.Logger.Debug("File details from SharePoint CopyFile", "details=", fileDetails)
if err != nil {
- srv.Logger.Error("error creating impersonated Google Drive service",
+ srv.Logger.Error("error copying template to create draft",
"error", err,
"method", r.Method,
"path", r.URL.Path,
+ "template", template,
)
- http.Error(
- w, "Error processing request", http.StatusInternalServerError)
+ if strings.Contains(err.Error(), "409 Conflict") &&
+ strings.Contains(err.Error(), "nameAlreadyExists") {
+ srv.Logger.Warn("file with this name already exists",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "user_email", userEmail,
+ "title", req.Title)
+ http.Error(w, "File with this name already exists. Please change the title.",
+ http.StatusConflict)
+ return
+ }
+ http.Error(w, "Error creating document draft",
+ http.StatusInternalServerError)
return
}
- // Copy template as user to new draft file in temporary drafts folder.
- f, err = copyTemplateSvc.CopyFile(
- template, title, srv.Config.GoogleWorkspace.TemporaryDraftsFolder)
+ fileID = fileDetails.ID
+
+ // Build created date.
+ createdTime, err := time.Parse(time.RFC3339Nano, fileDetails.LastModified)
if err != nil {
- srv.Logger.Error(
- "error copying template as user to temporary drafts folder",
+ srv.Logger.Error("error parsing draft created time",
"error", err,
"method", r.Method,
"path", r.URL.Path,
- "template", template,
- "drafts_folder", srv.Config.GoogleWorkspace.DraftsFolder,
- "temporary_drafts_folder", srv.Config.GoogleWorkspace.
- TemporaryDraftsFolder,
- "user", userEmail,
+ "doc_id", fileID,
)
http.Error(w, "Error creating document draft",
http.StatusInternalServerError)
return
}
+ cd := createdTime.Format("Jan 2, 2006")
+
+ srv.Logger.Info("Created draft",
+ "file_id", fileID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "template", template,
+ "user", userEmail,
+ )
+
+ metaTags := []string{
+ "o_id:" + userEmail,
+ }
+
+ doc = &document.Document{
+ ObjectID: fileID,
+ Title: req.Title,
+ AppCreated: true,
+ Contributors: req.Contributors,
+ Created: cd,
+ CreatedTime: createdTime.Unix(),
+ DocNumber: fmt.Sprintf("%s-???", req.ProductAbbreviation),
+ DocType: req.DocType,
+ MetaTags: metaTags,
+ ModifiedTime: createdTime.Unix(),
+ Owners: []string{userEmail},
+ OwnerPhotos: []string{},
+ Product: req.Product,
+ Status: "WIP",
+ Summary: req.Summary,
+ }
+
+ // Replace document header with custom properties in SharePoint
+ headerProps := map[string]string{
+ "Title": req.Title,
+ "DocType": req.DocType,
+ "DocNumber": fmt.Sprintf("%s-???", req.ProductAbbreviation),
+ "Product": req.Product,
+ "Status": "WIP",
+ "Contributors": strings.Join(req.Contributors, ","),
+ "Summary": req.Summary,
+ "Created": createdTime.Format("Jan 2, 2006"),
+ "Owner": userEmail,
+ "Approvers": "N/A",
+ }
- // Move draft file to drafts folder using service user.
- _, err = srv.GWService.MoveFile(
- f.Id, srv.Config.GoogleWorkspace.DraftsFolder)
+ err = srv.SharePoint.ReplaceDocumentHeaderWithContentUpdate(fileID, headerProps)
if err != nil {
- srv.Logger.Error(
- "error moving draft file to drafts folder",
+ srv.Logger.Error("error replacing document header",
"error", err,
"method", r.Method,
"path", r.URL.Path,
- "doc_id", f.Id,
- "drafts_folder", srv.Config.GoogleWorkspace.DraftsFolder,
- "temporary_drafts_folder", srv.Config.GoogleWorkspace.
- TemporaryDraftsFolder,
+ "doc_id", fileID,
)
- http.Error(w, "Error creating document draft",
+ srv.SharePoint.DeleteFile(fileID)
+ http.Error(w, "Error occurred during document header update",
http.StatusInternalServerError)
return
}
+
+ if err := createDraftDBAndShare(srv, r, w, doc, fileID, createdTime, req, userEmail); err != nil {
+ return
+ }
} else {
- // Copy template to new draft file as service user.
- f, err = srv.GWService.CopyFile(
- template, title, srv.Config.GoogleWorkspace.DraftsFolder)
+ // Create draft in Google Drive.
+ var f *drive.File
+
+ // Copy template to new draft file.
+ if srv.Config.GoogleWorkspace.Auth != nil &&
+ srv.Config.GoogleWorkspace.Auth.CreateDocsAsUser {
+ // If configured to create documents as the logged-in Hermes user,
+ // create a new Google Drive service to do this.
+ ctx := context.Background()
+ conf := &jwt.Config{
+ Email: srv.Config.GoogleWorkspace.Auth.ClientEmail,
+ PrivateKey: []byte(srv.Config.GoogleWorkspace.Auth.PrivateKey),
+ Scopes: []string{
+ "https://www.googleapis.com/auth/drive",
+ },
+ Subject: userEmail,
+ TokenURL: srv.Config.GoogleWorkspace.Auth.TokenURL,
+ }
+ client := conf.Client(ctx)
+ copyTemplateSvc := *srv.GWService
+ copyTemplateSvc.Drive, err = drive.NewService(
+ ctx, option.WithHTTPClient(client))
+ if err != nil {
+ srv.Logger.Error("error creating impersonated Google Drive service",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ http.Error(
+ w, "Error processing request", http.StatusInternalServerError)
+ return
+ }
+
+ // Copy template as user to new draft file in temporary drafts folder.
+ f, err = copyTemplateSvc.CopyFile(
+ template, req.Title, srv.Config.GoogleWorkspace.TemporaryDraftsFolder)
+ if err != nil {
+ srv.Logger.Error(
+ "error copying template as user to temporary drafts folder",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "template", template,
+ "drafts_folder", srv.Config.GoogleWorkspace.DraftsFolder,
+ "temporary_drafts_folder", srv.Config.GoogleWorkspace.
+ TemporaryDraftsFolder,
+ "user", userEmail,
+ )
+ http.Error(w, "Error creating document draft",
+ http.StatusInternalServerError)
+ return
+ }
+
+ // Move draft file to drafts folder using service user.
+ _, err = srv.GWService.MoveFile(
+ f.Id, srv.Config.GoogleWorkspace.DraftsFolder)
+ if err != nil {
+ srv.Logger.Error(
+ "error moving draft file to drafts folder",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", f.Id,
+ "drafts_folder", srv.Config.GoogleWorkspace.DraftsFolder,
+ "temporary_drafts_folder", srv.Config.GoogleWorkspace.
+ TemporaryDraftsFolder,
+ )
+ http.Error(w, "Error creating document draft",
+ http.StatusInternalServerError)
+ return
+ }
+ } else {
+ // Copy template to new draft file as service user.
+ f, err = srv.GWService.CopyFile(
+ template, req.Title, srv.Config.GoogleWorkspace.DraftsFolder)
+ if err != nil {
+ srv.Logger.Error("error creating draft",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "template", template,
+ )
+ http.Error(w, "Error creating document draft",
+ http.StatusInternalServerError)
+ return
+ }
+ }
+
+ fileID = f.Id
+
+ ct, err := time.Parse(time.RFC3339Nano, f.CreatedTime)
if err != nil {
- srv.Logger.Error("error creating draft",
+ srv.Logger.Error("error parsing draft created time",
"error", err,
"method", r.Method,
"path", r.URL.Path,
- "template", template,
- "drafts_folder", srv.Config.GoogleWorkspace.DraftsFolder,
+ "doc_id", fileID,
)
http.Error(w, "Error creating document draft",
http.StatusInternalServerError)
return
}
- }
-
- // Build created date.
- ct, err := time.Parse(time.RFC3339Nano, f.CreatedTime)
- if err != nil {
- srv.Logger.Error("error parsing draft created time",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", f.Id,
- )
- http.Error(w, "Error creating document draft",
- http.StatusInternalServerError)
- return
- }
- cd := ct.Format("Jan 2, 2006")
+ cd := ct.Format("Jan 2, 2006")
- // Get owner photo by searching Google Workspace directory.
- op := []string{}
- people, err := srv.GWService.SearchPeople(userEmail, "photos")
- if err != nil {
- srv.Logger.Error(
- "error searching directory for person",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "person", userEmail,
- )
- }
- if len(people) > 0 {
- if len(people[0].Photos) > 0 {
- op = append(op, people[0].Photos[0].Url)
+ // Get owner photo by searching Google Workspace directory.
+ op := []string{}
+ people, err := srv.GWService.SearchPeople(userEmail, "photos")
+ if err != nil {
+ srv.Logger.Error(
+ "error searching directory for person",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "person", userEmail,
+ )
+ }
+ if len(people) > 0 {
+ if len(people[0].Photos) > 0 {
+ op = append(op, people[0].Photos[0].Url)
+ }
}
- }
-
- // Create tag
- // Note: The o_id tag may be empty for environments such as development.
- // For environments like pre-prod and prod, it will be set as
- // Okta authentication is enforced before this handler is called for
- // those environments. Maybe, if id isn't set we use
- // owner emails in the future?
- id := r.Header.Get("x-amzn-oidc-identity")
- metaTags := []string{
- "o_id:" + id,
- }
-
- // Build document.
- doc := &document.Document{
- ObjectID: f.Id,
- Title: req.Title,
- AppCreated: true,
- Contributors: req.Contributors,
- Created: cd,
- CreatedTime: ct.Unix(),
- DocNumber: fmt.Sprintf("%s-???", req.ProductAbbreviation),
- DocType: req.DocType,
- MetaTags: metaTags,
- ModifiedTime: ct.Unix(),
- Owners: []string{userEmail},
- OwnerPhotos: op,
- Product: req.Product,
- Status: "WIP",
- Summary: req.Summary,
- // Tags: req.Tags,
- }
- // Replace the doc header.
- if err = doc.ReplaceHeader(
- srv.Config.BaseURL, true, srv.GWService,
- ); err != nil {
- srv.Logger.Error("error replacing draft doc header",
- "error", err,
+ srv.Logger.Info("Created draft",
+ "file_id", fileID,
"method", r.Method,
"path", r.URL.Path,
- "doc_id", f.Id,
+ "template", template,
+ "user", userEmail,
)
- http.Error(w, "Error creating document draft",
- http.StatusInternalServerError)
- return
- }
- // Create document in the database.
- var contributors []*models.User
- for _, c := range req.Contributors {
- contributors = append(contributors, &models.User{
- EmailAddress: c,
- })
- }
- createdTime, err := time.Parse(time.RFC3339Nano, f.CreatedTime)
- if err != nil {
- srv.Logger.Error("error parsing document created time",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", f.Id,
- )
- http.Error(w, "Error creating document draft",
- http.StatusInternalServerError)
- return
- }
- model := models.Document{
- GoogleFileID: f.Id,
- Contributors: contributors,
- DocumentCreatedAt: createdTime,
- DocumentModifiedAt: createdTime,
- DocumentType: models.DocumentType{
- Name: req.DocType,
- },
- Owner: &models.User{
- EmailAddress: userEmail,
- },
- Product: models.Product{
- Name: req.Product,
- },
- Status: models.WIPDocumentStatus,
- Summary: &req.Summary,
- Title: req.Title,
- }
- if err := model.Create(srv.DB); err != nil {
- srv.Logger.Error("error creating document in database",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", f.Id,
- )
- http.Error(w, "Error creating document draft",
- http.StatusInternalServerError)
- return
- }
+ metaTags := []string{
+ "o_id:" + userEmail,
+ }
- // Share file with the owner
- if err := srv.GWService.ShareFile(f.Id, userEmail, "writer"); err != nil {
- srv.Logger.Error("error sharing file with the owner",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", f.Id,
- )
- http.Error(w, "Error creating document draft",
- http.StatusInternalServerError)
- return
- }
+ doc = &document.Document{
+ ObjectID: fileID,
+ Title: req.Title,
+ AppCreated: true,
+ Contributors: req.Contributors,
+ Created: cd,
+ CreatedTime: ct.Unix(),
+ DocNumber: fmt.Sprintf("%s-???", req.ProductAbbreviation),
+ DocType: req.DocType,
+ MetaTags: metaTags,
+ ModifiedTime: ct.Unix(),
+ Owners: []string{userEmail},
+ OwnerPhotos: op,
+ Product: req.Product,
+ Status: "WIP",
+ Summary: req.Summary,
+ }
- // Share file with contributors.
- // Google Drive API limitation is that you can only share files with one
- // user at a time.
- for _, c := range req.Contributors {
- if err := srv.GWService.ShareFile(f.Id, c, "writer"); err != nil {
- srv.Logger.Error("error sharing file with the contributor",
+ // Replace the doc header using Google Docs API.
+ if err = doc.ReplaceHeader(
+ srv.Config.BaseURL, true, srv.GWService,
+ ); err != nil {
+ srv.Logger.Error("error replacing draft doc header",
"error", err,
"method", r.Method,
"path", r.URL.Path,
- "doc_id", f.Id,
- "contributor", c,
+ "doc_id", fileID,
)
http.Error(w, "Error creating document draft",
http.StatusInternalServerError)
return
}
- }
- // TODO: Delete draft file in the case of an error.
-
- // Write response.
+ if err := createDraftDBAndShare(srv, r, w, doc, fileID, ct, req, userEmail); err != nil {
+ return
+ }
+ } // Write response.
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
resp := &DraftsResponse{
- ID: f.Id,
+ ID: fileID,
}
enc := json.NewEncoder(w)
@@ -393,7 +472,7 @@ func DraftsHandler(srv server.Server) http.Handler {
"error", err,
"method", r.Method,
"path", r.URL.Path,
- "doc_id", f.Id,
+ "doc_id", fileID,
)
http.Error(w, "Error creating document draft",
http.StatusInternalServerError)
@@ -403,7 +482,15 @@ func DraftsHandler(srv server.Server) http.Handler {
srv.Logger.Info("created draft",
"method", r.Method,
"path", r.URL.Path,
- "doc_id", f.Id,
+ "doc_id", fileID,
+ )
+
+ srv.Logger.Info("ACCESS",
+ "user_email", userEmail,
+ "doc_id", fileID,
+ "operation", "draft_created",
+ "updated_attributes", "[docType, product, title]",
+ "mode", "draft",
)
// Request post-processing.
@@ -415,7 +502,7 @@ func DraftsHandler(srv server.Server) http.Handler {
"error", err,
"method", r.Method,
"path", r.URL.Path,
- "doc_id", f.Id,
+ "doc_id", fileID,
)
http.Error(w, "Error creating document draft",
http.StatusInternalServerError)
@@ -427,7 +514,7 @@ func DraftsHandler(srv server.Server) http.Handler {
"error", err,
"method", r.Method,
"path", r.URL.Path,
- "doc_id", f.Id,
+ "doc_id", fileID,
)
http.Error(w, "Error creating document draft",
http.StatusInternalServerError)
@@ -437,43 +524,39 @@ func DraftsHandler(srv server.Server) http.Handler {
// Compare Algolia and database documents to find data inconsistencies.
// Get document object from Algolia.
var algoDoc map[string]any
- err = srv.AlgoSearch.Drafts.GetObject(f.Id, &algoDoc)
+ err = srv.AlgoSearch.Drafts.GetObject(fileID, &algoDoc)
if err != nil {
srv.Logger.Error("error getting Algolia object for data comparison",
"error", err,
"method", r.Method,
"path", r.URL.Path,
- "doc_id", f.Id,
+ "doc_id", fileID,
)
return
}
// Get document from database.
- dbDoc := models.Document{
- GoogleFileID: f.Id,
- }
+ dbDoc := srv.NewDocumentByFileID(fileID)
if err := dbDoc.Get(srv.DB); err != nil {
srv.Logger.Error(
"error getting document from database for data comparison",
"error", err,
"path", r.URL.Path,
"method", r.Method,
- "doc_id", f.Id,
+ "doc_id", fileID,
)
return
}
// Get all reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(srv.DB, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: f.Id,
- },
+ Document: srv.NewDocumentByFileID(fileID),
}); err != nil {
srv.Logger.Error(
"error getting all reviews for document for data comparison",
"error", err,
"method", r.Method,
"path", r.URL.Path,
- "doc_id", f.Id,
+ "doc_id", fileID,
)
return
}
@@ -485,7 +568,7 @@ func DraftsHandler(srv server.Server) http.Handler {
"error", err,
"method", r.Method,
"path", r.URL.Path,
- "doc_id", f.Id,
+ "doc_id", fileID,
)
}
}()
@@ -620,9 +703,7 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
}
// Get document from database.
- model := models.Document{
- GoogleFileID: docID,
- }
+ model := srv.NewDocumentByFileID(docID)
if err := model.Get(srv.DB); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
srv.Logger.Warn("document draft record not found",
@@ -648,9 +729,7 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
// Get reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(srv.DB, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: srv.NewDocumentByFileID(docID),
}); err != nil {
srv.Logger.Error("error getting reviews for document",
"error", err,
@@ -664,9 +743,7 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
// Get group reviews for the document.
var groupReviews models.DocumentGroupReviews
if err := groupReviews.Find(srv.DB, models.DocumentGroupReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: srv.NewDocumentByFileID(docID),
}); err != nil {
srv.Logger.Error("error getting group reviews for document",
"error", err,
@@ -694,6 +771,11 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
// Make sure document is a draft.
if doc.Status != "WIP" {
+ srv.Logger.Warn("document is not a draft",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "status", doc.Status)
http.Error(w, "Draft not found", http.StatusNotFound)
return
}
@@ -703,13 +785,21 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
// require owner access only.
userEmail := r.Context().Value("userEmail").(string)
var isOwner, isContributor bool
- if len(doc.Owners) > 0 && doc.Owners[0] == userEmail {
+ if len(doc.Owners) > 0 && strings.EqualFold(doc.Owners[0], userEmail) {
isOwner = true
}
if contains(doc.Contributors, userEmail) {
isContributor = true
}
if !isOwner && !isContributor && !model.ShareableAsDraft {
+ srv.Logger.Warn("unauthorized draft access attempt",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "user_email", userEmail,
+ "is_owner", isOwner,
+ "is_contributor", isContributor,
+ "shareable_as_draft", model.ShareableAsDraft)
http.Error(w,
"Only owners or contributors can access a non-shared draft document",
http.StatusUnauthorized)
@@ -721,46 +811,126 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
switch reqType {
case relatedResourcesDocumentSubcollectionRequestType:
documentsResourceRelatedResourcesHandler(
- w, r, docID, *doc, srv.Config, srv.Logger, srv.AlgoSearch, srv.DB)
+ w, r, docID, *doc, srv.Config, srv.Logger, srv.AlgoSearch, srv.DB, srv.IsSharePoint())
return
case shareableDocumentSubcollectionRequestType:
draftsShareableHandler(w, r, docID, *doc, *srv.Config, srv.Logger,
- srv.AlgoSearch, srv.GWService, srv.DB)
+ srv.AlgoSearch, srv.GWService, srv.DB, srv.IsSharePoint())
+ return
+ case archivedDocumentSubcollectionRequestType:
+ draftsArchivedHandler(w, r, docID, *doc, *srv.Config, srv.Logger,
+ srv.AlgoWrite, srv.DB, srv.IsSharePoint())
return
}
switch r.Method {
+ case "HEAD":
+ // HEAD: respond with 200, and for SharePoint documents expose
+ // the direct edit URL header so the frontend can redirect.
+ // For Google documents, return 200 without the header so the
+ // frontend falls through to normal in-app document viewing.
+ if srv.SharePoint != nil {
+ fileDetails, err := srv.SharePoint.GetFileDetails(docID)
+ if err != nil {
+ srv.Logger.Error("error getting draft file from SharePoint (HEAD)",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error requesting document draft", http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("X-Direct-Edit-URL", fileDetails.WebURL)
+ }
+
+ w.Header().Set("Cache-Control", "private, no-store")
+ w.WriteHeader(http.StatusOK)
+
+ // Request post-processing for recently viewed documents
+ go func() {
+ // Update recently viewed documents if this is a document view event. The
+ // Add-To-Recently-Viewed header is set in the request from the frontend
+ // to differentiate between document views and requests to only retrieve
+ // document metadata.
+ if r.Header.Get("Add-To-Recently-Viewed") != "" {
+ if err := updateRecentlyViewedDocs(
+ userEmail, docID, srv.DB, time.Now(), srv.IsSharePoint(),
+ ); err != nil {
+ srv.Logger.Error("error updating recently viewed docs",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ }
+ }
+ }()
+ return
case "GET":
now := time.Now()
- // Get file from Google Drive so we can return the latest modified time.
- file, err := srv.GWService.GetFile(docID)
- if err != nil {
- srv.Logger.Error("error getting document file from Google",
- "error", err,
- "path", r.URL.Path,
- "method", r.Method,
- "doc_id", docID,
- )
- http.Error(w,
- "Error requesting document draft", http.StatusInternalServerError)
- return
- }
+ var directEditURL string
+ if srv.SharePoint != nil {
+ // Get file details from SharePoint
+ fileDetails, err := srv.SharePoint.GetFileDetails(docID)
+ if err != nil {
+ srv.Logger.Error("error getting document file from SharePoint",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w,
+ "Error requesting document draft", http.StatusInternalServerError)
+ return
+ }
- // Parse and set modified time.
- modifiedTime, err := time.Parse(time.RFC3339Nano, file.ModifiedTime)
- if err != nil {
- srv.Logger.Error("error parsing modified time",
- "error", err,
- "path", r.URL.Path,
- "method", r.Method,
- "doc_id", docID,
- )
- http.Error(w,
- "Error requesting document draft", http.StatusInternalServerError)
- return
+ // Parse modified time from SharePoint
+ modifiedTime, err := time.Parse(time.RFC3339, fileDetails.LastModified)
+ if err != nil {
+ srv.Logger.Error("error parsing modified time",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w,
+ "Error requesting document draft", http.StatusInternalServerError)
+ return
+ }
+ doc.ModifiedTime = modifiedTime.Unix()
+ directEditURL = fileDetails.WebURL
+ } else {
+ // Get file from Google Drive
+ file, err := srv.GWService.GetFile(docID)
+ if err != nil {
+ srv.Logger.Error("error getting document file",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w,
+ "Error requesting document draft", http.StatusInternalServerError)
+ return
+ }
+
+ modifiedTime, err := time.Parse(time.RFC3339, file.ModifiedTime)
+ if err != nil {
+ srv.Logger.Error("error parsing modified time",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w,
+ "Error requesting document draft", http.StatusInternalServerError)
+ return
+ }
+ doc.ModifiedTime = modifiedTime.Unix()
+ directEditURL = file.WebViewLink
}
- doc.ModifiedTime = modifiedTime.Unix()
// Convert document to Algolia object because this is how it is expected
// by the frontend.
@@ -777,6 +947,8 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
return
}
+ docObj["directEditURL"] = directEditURL // Add direct edit URL
+
// Write response.
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
@@ -809,7 +981,7 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
// document metadata.
if r.Header.Get("Add-To-Recently-Viewed") != "" {
if err := updateRecentlyViewedDocs(
- userEmail, docID, srv.DB, now,
+ userEmail, docID, srv.DB, now, srv.IsSharePoint(),
); err != nil {
srv.Logger.Error("error updating recently viewed docs",
"error", err,
@@ -836,9 +1008,7 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
return
}
// Get document from database.
- dbDoc := models.Document{
- GoogleFileID: docID,
- }
+ dbDoc := srv.NewDocumentByFileID(docID)
if err := dbDoc.Get(srv.DB); err != nil {
srv.Logger.Error(
"error getting document from database for data comparison",
@@ -852,9 +1022,7 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
// Get all reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(srv.DB, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: srv.NewDocumentByFileID(docID),
}); err != nil {
srv.Logger.Error(
"error getting all reviews for document for data comparison",
@@ -881,25 +1049,41 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
case "DELETE":
// Authorize request.
if !isOwner {
+ srv.Logger.Warn("unauthorized draft deletion attempt",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "user_email", userEmail)
http.Error(w,
"Only owners can delete a draft document",
http.StatusUnauthorized)
return
}
- // Delete document in Google Drive.
- err = srv.GWService.DeleteFile(docID)
- if err != nil {
- srv.Logger.Error(
- "error deleting document",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- http.Error(w, "Error deleting document draft",
- http.StatusInternalServerError)
- return
+ if srv.SharePoint != nil {
+ if err := srv.SharePoint.DeleteFile(docID); err != nil {
+ srv.Logger.Error("error deleting document from SharePoint",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error deleting document draft",
+ http.StatusInternalServerError)
+ return
+ }
+ } else {
+ if err := srv.GWService.DeleteFile(docID); err != nil {
+ srv.Logger.Error("error deleting document file",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error deleting document draft",
+ http.StatusInternalServerError)
+ return
+ }
}
// Delete object in Algolia.
@@ -931,9 +1115,7 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
}
// Delete document in the database.
- d := models.Document{
- GoogleFileID: docID,
- }
+ d := srv.NewDocumentByFileID(docID)
if err := d.Delete(srv.DB); err != nil {
srv.Logger.Error(
"error deleting document draft in database",
@@ -973,9 +1155,14 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
case "PATCH":
// Authorize request.
if !isOwner {
+ srv.Logger.Warn("unauthorized draft patch attempt",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "user_email", userEmail)
http.Error(w,
"Only owners can patch a draft document",
- http.StatusUnauthorized)
+ http.StatusForbidden)
return
}
@@ -1066,22 +1253,24 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
}
}
- // Check if document is locked.
- locked, err := hcd.IsLocked(docID, srv.DB, srv.GWService, srv.Logger)
- if err != nil {
- srv.Logger.Error("error checking document locked status",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- http.Error(w, "Error getting document status", http.StatusNotFound)
- return
- }
- // Don't continue if document is locked.
- if locked {
- http.Error(w, "Document is locked", http.StatusLocked)
- return
+ // Check if document is locked (Google-only).
+ if !srv.IsSharePoint() {
+ locked, err := hcd.IsLocked(docID, srv.DB, srv.GWService, srv.Logger)
+ if err != nil {
+ srv.Logger.Error("error checking document locked status",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error getting document status", http.StatusNotFound)
+ return
+ }
+ // Don't continue if document is locked.
+ if locked {
+ http.Error(w, "Document is locked", http.StatusLocked)
+ return
+ }
}
// Compare contributors in request and stored object in Algolia
@@ -1102,48 +1291,154 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
}
// Find out contributors to remove from sharing the document
// var contributorsToRemoveSharing []string
- // TODO: figure out how we want to handle user removing all contributors
- // from the sidebar select
- if len(doc.Contributors) != 0 && len(*req.Contributors) != 0 {
- // Compare contributors when there are stored contributors
- // and there are contributors in the request
- contributorsToRemoveSharing = compareSlices(
- *req.Contributors, doc.Contributors)
+ if len(doc.Contributors) != 0 {
+ if len(*req.Contributors) == 0 {
+ // All contributors are being removed
+ contributorsToRemoveSharing = doc.Contributors
+ } else {
+ // Compare contributors when there are stored contributors
+ // and there are contributors in the request
+ // Find contributors that exist in current doc but NOT in the request
+ contributorsToRemoveSharing = compareSlices(
+ *req.Contributors, doc.Contributors)
+ }
}
}
// Share file with contributors.
// Google Drive API limitation is that you can only share files with one
// user at a time.
- for _, c := range contributorsToAddSharing {
- if err := srv.GWService.ShareFile(docID, c, "writer"); err != nil {
- srv.Logger.Error("error sharing file with the contributor",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- "contributor", c)
- http.Error(w, "Error patching document draft",
- http.StatusInternalServerError)
- return
- }
- }
+
if len(contributorsToAddSharing) > 0 {
+ if srv.SharePoint != nil {
+ if err := srv.SharePoint.ShareFileWithMultipleUsers(docID, "writer", contributorsToAddSharing); err != nil {
+ srv.Logger.Error("error sharing file with the contributor",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "contributor", contributorsToAddSharing)
+ http.Error(w, "Error patching document draft",
+ http.StatusInternalServerError)
+ return
+ }
+ } else {
+ for _, c := range contributorsToAddSharing {
+ if err := srv.GWService.ShareFile(docID, c, "writer"); err != nil {
+ srv.Logger.Error("error sharing file with the contributor",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "contributor", c)
+ http.Error(w, "Error patching document draft",
+ http.StatusInternalServerError)
+ return
+ }
+ }
+ }
srv.Logger.Info("shared document with contributors",
"method", r.Method,
"path", r.URL.Path,
"contributors_count", len(contributorsToAddSharing),
)
+
+ docURL := fmt.Sprintf("%s/document/%s?draft=true", srv.Config.BaseURL, docID)
+
+ srv.Logger.Info("contributor email queued",
+ "doc_id", docID,
+ "contributor_count", len(contributorsToAddSharing),
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+
+ go helpers.SendEmailWithRetry(
+ &srv,
+ func() error {
+ return email.SendContributorAddedEmail(
+ email.ContributorAddedEmailData{
+ BaseURL: srv.Config.BaseURL,
+ DocumentOwner: doc.Owners[0],
+ DocumentShortName: doc.DocNumber,
+ DocumentTitle: doc.Title,
+ DocumentType: doc.DocType,
+ DocumentStatus: doc.Status,
+ DocumentURL: docURL,
+ Product: doc.Product,
+ },
+ contributorsToAddSharing,
+ srv.Config.Email.FromAddress,
+ srv.GetEmailSender(),
+ )
+ },
+ docID,
+ "contributor_added",
+ r,
+ )
+ }
+ // Build permission map for contributor removal.
+ emailToPermissionIDsMap := make(map[string][]string)
+ if srv.SharePoint != nil {
+ permissions, err := srv.SharePoint.ListPermissions(docID)
+ if err != nil {
+ srv.Logger.Error("error getting file permissions",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w,
+ "Error requesting document draft", http.StatusInternalServerError)
+ return
+ }
+ for _, p := range permissions {
+ if p.GrantedTo.User.Email == "" {
+ continue
+ }
+ if slices.Contains(p.Role, "owner") {
+ continue
+ }
+ email := p.GrantedTo.User.Email
+ if _, exists := emailToPermissionIDsMap[email]; !exists {
+ emailToPermissionIDsMap[email] = make([]string, 0)
+ }
+ emailToPermissionIDsMap[email] = append(
+ emailToPermissionIDsMap[email], p.ID)
+ }
+ } else {
+ permissions, err := srv.GWService.ListPermissions(docID)
+ if err != nil {
+ srv.Logger.Error("error getting file permissions",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w,
+ "Error requesting document draft", http.StatusInternalServerError)
+ return
+ }
+ for _, p := range permissions {
+ if p.EmailAddress == "" {
+ continue
+ }
+ if p.Role == "owner" {
+ continue
+ }
+ if _, exists := emailToPermissionIDsMap[p.EmailAddress]; !exists {
+ emailToPermissionIDsMap[p.EmailAddress] = make([]string, 0)
+ }
+ emailToPermissionIDsMap[p.EmailAddress] = append(
+ emailToPermissionIDsMap[p.EmailAddress], p.Id)
+ }
}
- // Remove contributors from file.
- // This unfortunately needs to be done one user at a time
for _, c := range contributorsToRemoveSharing {
// Only remove contributor if the email
// associated with the permission doesn't
// match owner email(s).
if !contains(doc.Owners, c) {
- if err := removeSharing(srv.GWService, docID, c); err != nil {
+ if err := removeSharing(srv, docID, c, emailToPermissionIDsMap); err != nil {
srv.Logger.Error("error removing contributor from file",
"error", err,
"method", r.Method,
@@ -1208,7 +1503,12 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
// Custom fields.
if req.CustomFields != nil {
+ srv.Logger.Info("processing custom fields",
+ "doc_id", docID,
+ "custom_fields_count", len(*req.CustomFields))
+
for _, cf := range *req.CustomFields {
+
switch cf.Type {
case "STRING":
if v, ok := cf.Value.(string); ok {
@@ -1248,6 +1548,11 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
return
}
case "PEOPLE":
+ srv.Logger.Info("processing PEOPLE custom field",
+ "cf_name", cf.Name,
+ "cf_type", cf.Type,
+ "doc_id", docID)
+
if reflect.TypeOf(cf.Value).Kind() != reflect.Slice {
srv.Logger.Error("invalid value type for people custom field",
"error", err,
@@ -1284,6 +1589,47 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
}
}
+ // IMPORTANT: Query database for old stakeholders BEFORE any modifications
+ var oldStakeholders []string
+ if strings.EqualFold(cf.Name, "stakeholders") || strings.EqualFold(cf.DisplayName, "Stakeholders") {
+ srv.Logger.Info("querying database for old stakeholders",
+ "cf_name", cf.Name,
+ "doc_id", docID)
+
+ // Query the document's current custom fields directly from database
+ var dbDoc models.Document
+ err := srv.DB.
+ Preload("CustomFields.DocumentTypeCustomField").
+ Where("id = ?", model.ID).
+ First(&dbDoc).Error
+
+ if err == nil {
+ // Find the stakeholders custom field in the database result
+ for _, existingCF := range dbDoc.CustomFields {
+ if existingCF.DocumentTypeCustomField.Name == "Stakeholders" {
+ // Parse the JSON value from database
+ var stakeholderEmails []string
+ if err := json.Unmarshal([]byte(existingCF.Value), &stakeholderEmails); err == nil {
+ oldStakeholders = stakeholderEmails
+ srv.Logger.Info("found old stakeholders from database",
+ "old_stakeholders_count", len(oldStakeholders),
+ "doc_id", docID)
+ } else {
+ srv.Logger.Warn("failed to unmarshal old stakeholders",
+ "error", err,
+ "doc_id", docID,
+ "field_name", existingCF.DocumentTypeCustomField.Name)
+ }
+ break
+ }
+ }
+ } else {
+ srv.Logger.Warn("failed to query database for old stakeholders",
+ "error", err,
+ "doc_id", docID)
+ }
+ }
+
if err := doc.UpsertCustomField(cf); err != nil {
srv.Logger.Error("error upserting custom people field",
"error", err,
@@ -1320,6 +1666,91 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
http.StatusBadRequest)
return
}
+
+ // Send email notification for new stakeholders
+ srv.Logger.Info("checking custom field for stakeholder email",
+ "cf_name", cf.Name,
+ "doc_id", docID)
+
+ if strings.EqualFold(cf.Name, "stakeholders") || strings.EqualFold(cf.DisplayName, "Stakeholders") {
+ // Expand groups recursively for both old and new stakeholders
+ oldStakeholdersExpanded, err := expandStakeholderGroups(oldStakeholders, srv)
+ if err != nil {
+ srv.Logger.Error("error expanding old stakeholder groups",
+ "error", err,
+ "doc_id", docID)
+ // Continue with unexpanded list if expansion fails
+ oldStakeholdersExpanded = oldStakeholders
+ }
+
+ newStakeholdersExpanded, err := expandStakeholderGroups(cfVal, srv)
+ if err != nil {
+ srv.Logger.Error("error expanding new stakeholder groups",
+ "error", err,
+ "doc_id", docID)
+ // Continue with unexpanded list if expansion fails
+ newStakeholdersExpanded = cfVal
+ }
+
+ // Find new stakeholders (those in expanded new list but not in expanded old list)
+ newStakeholders := compareSlices(oldStakeholdersExpanded, newStakeholdersExpanded)
+
+ srv.Logger.Debug("stakeholder change detected",
+ "doc_id", docID,
+ "new_count", len(newStakeholders))
+
+ if len(newStakeholders) > 0 {
+ // Build document URL
+ docURL := fmt.Sprintf("%s/document/%s?draft=true", srv.Config.BaseURL, docID)
+
+ srv.Logger.Info("sending stakeholder notification email",
+ "doc_id", docID,
+ "recipient_count", len(newStakeholders))
+
+ // Send email to new stakeholders asynchronously
+ go func() {
+ helpers.SendEmailWithRetry(
+ &srv,
+ func() error {
+ err := email.SendStakeholderAddedEmail(
+ email.StakeholderAddedEmailData{
+ BaseURL: srv.Config.BaseURL,
+ DocumentOwner: doc.Owners[0],
+ DocumentShortName: doc.DocNumber,
+ DocumentTitle: doc.Title,
+ DocumentType: doc.DocType,
+ DocumentStatus: doc.Status,
+ DocumentURL: docURL,
+ Product: doc.Product,
+ },
+ newStakeholders,
+ srv.Config.Email.FromAddress,
+ srv.GetEmailSender(),
+ )
+
+ if err != nil {
+ srv.Logger.Error("SendStakeholderAddedEmail failed",
+ "error", err,
+ "doc_id", docID,
+ "recipients", newStakeholders)
+ return err
+ }
+
+ srv.Logger.Info("SendStakeholderAddedEmail succeeded",
+ "doc_id", docID,
+ "recipients", newStakeholders)
+ return nil
+ },
+ docID,
+ "stakeholder_added",
+ r,
+ )
+
+ srv.Logger.Info("stakeholder email send complete",
+ "doc_id", docID)
+ }()
+ }
+ }
default:
srv.Logger.Error("invalid custom field type",
"error", err,
@@ -1355,17 +1786,30 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
}
// Share file with new owner.
- if err := srv.GWService.ShareFile(
- docID, doc.Owners[0], "writer"); err != nil {
- srv.Logger.Error("error sharing file with new owner",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- "new_owner", doc.Owners[0])
- http.Error(w, "Error patching document draft",
- http.StatusInternalServerError)
- return
+ if srv.SharePoint != nil {
+ if err := srv.SharePoint.ShareFile(docID, doc.Owners[0], "writer"); err != nil {
+ srv.Logger.Error("error sharing file with new owner",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "new_owner", doc.Owners[0])
+ http.Error(w, "Error patching document draft",
+ http.StatusInternalServerError)
+ return
+ }
+ } else {
+ if err := srv.GWService.ShareFile(docID, doc.Owners[0], "writer"); err != nil {
+ srv.Logger.Error("error sharing file with new owner",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "new_owner", doc.Owners[0])
+ http.Error(w, "Error patching document draft",
+ http.StatusInternalServerError)
+ return
+ }
}
}
@@ -1392,6 +1836,45 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
if req.Title != nil {
doc.Title = *req.Title
model.Title = *req.Title
+
+ // Rename the file to match the new title.
+ // Extract the product abbreviation from DocNumber (e.g., "HCP-???" -> "HCP").
+ abbr := strings.SplitN(doc.DocNumber, "-", 2)[0]
+ newFileName := fmt.Sprintf("%s-%s", abbr, *req.Title)
+
+ if srv.SharePoint != nil {
+ // Sanitize the file name for SharePoint.
+ newFileName = strings.NewReplacer(
+ "[", "(", "]", ")", "#", "-", "%", "-", "&", "and",
+ "*", "-", ":", "-", "<", "-", ">", "-", "?", "",
+ "/", "-", "\\", "-", "{", "(", "|", "-", "}", ")",
+ "~", "-",
+ ).Replace(newFileName)
+ newFileName = fmt.Sprintf("%s.docx", newFileName)
+ }
+
+ var renameErr error
+ if srv.SharePoint != nil {
+ renameErr = srv.SharePoint.RenameFile(docID, newFileName)
+ } else {
+ renameErr = srv.GWService.RenameFile(docID, newFileName)
+ }
+ if renameErr != nil {
+ srv.Logger.Error("error renaming file",
+ "error", renameErr,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "new_file_name", newFileName)
+ // Non-fatal: continue even if rename fails
+ srv.Logger.Warn("continuing draft patch despite file rename failure")
+ } else {
+ srv.Logger.Info("successfully renamed file",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "new_file_name", newFileName)
+ }
}
// Send email to new owner.
@@ -1415,38 +1898,69 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
newOwner := email.User{
EmailAddress: doc.Owners[0],
}
- ppl, err := srv.GWService.SearchPeople(
- doc.Owners[0], "emailAddresses,names")
- if err != nil {
- srv.Logger.Warn("error searching directory for new owner",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- "person", doc.Owners[0],
- )
- }
- if len(ppl) == 1 && ppl[0].Names != nil {
- newOwner.Name = ppl[0].Names[0].DisplayName
- }
-
// Get name of old document owner.
oldOwner := email.User{
EmailAddress: userEmail,
}
- ppl, err = srv.GWService.SearchPeople(
- userEmail, "emailAddresses,names")
- if err != nil {
- srv.Logger.Warn("error searching directory for old owner",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- "person", doc.Owners[0],
- )
- }
- if len(ppl) == 1 && ppl[0].Names != nil {
- oldOwner.Name = ppl[0].Names[0].DisplayName
+
+ if srv.SharePoint != nil {
+ // Look up display names via Microsoft Graph.
+ newPerson, err := srv.SharePoint.GetPersonByEmail(doc.Owners[0])
+ if err != nil {
+ srv.Logger.Warn("error looking up new owner in Microsoft Graph",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "person", doc.Owners[0],
+ )
+ } else {
+ newOwner.Name = newPerson.DisplayName
+ }
+
+ oldPerson, err := srv.SharePoint.GetPersonByEmail(userEmail)
+ if err != nil {
+ srv.Logger.Warn("error looking up old owner in Microsoft Graph",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "person", userEmail,
+ )
+ } else {
+ oldOwner.Name = oldPerson.DisplayName
+ }
+ } else {
+ // Look up display names via Google Workspace directory.
+ ppl, err := srv.GWService.SearchPeople(
+ doc.Owners[0], "emailAddresses,names")
+ if err != nil {
+ srv.Logger.Warn("error searching directory for new owner",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "person", doc.Owners[0],
+ )
+ }
+ if len(ppl) == 1 && ppl[0].Names != nil {
+ newOwner.Name = ppl[0].Names[0].DisplayName
+ }
+
+ ppl, err = srv.GWService.SearchPeople(
+ userEmail, "emailAddresses,names")
+ if err != nil {
+ srv.Logger.Warn("error searching directory for old owner",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "person", userEmail,
+ )
+ }
+ if len(ppl) == 1 && ppl[0].Names != nil {
+ oldOwner.Name = ppl[0].Names[0].DisplayName
+ }
}
if err := email.SendNewOwnerEmail(
@@ -1463,7 +1977,7 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
},
[]string{doc.Owners[0]},
srv.Config.Email.FromAddress,
- srv.GWService,
+ srv.GetEmailSender(),
); err != nil {
srv.Logger.Error("error sending new owner email",
"error", err,
@@ -1490,25 +2004,24 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
return
}
- // Replace the doc header.
- if err := doc.ReplaceHeader(
- srv.Config.BaseURL, true, srv.GWService,
- ); err != nil {
- srv.Logger.Error("error replacing draft doc header",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- http.Error(w, "Error replacing header of document draft",
- http.StatusInternalServerError)
- return
+ // Replace the doc header (Google-only; SharePoint headers
+ // are managed by the Hermes Add-In for Word).
+ if !srv.IsSharePoint() {
+ if err := doc.ReplaceHeader(
+ srv.Config.BaseURL, true, srv.GWService,
+ ); err != nil {
+ srv.Logger.Error("error replacing draft doc header",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error replacing header of document draft",
+ http.StatusInternalServerError)
+ return
+ }
}
- // Rename file with new title.
- srv.GWService.RenameFile(docID,
- fmt.Sprintf("[%s] %s", doc.DocNumber, doc.Title))
-
w.WriteHeader(http.StatusOK)
srv.Logger.Info("patched draft document",
@@ -1517,6 +2030,16 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
"doc_id", docID,
)
+ // Log document access with Datadog ACCESS tag
+ operation, updatedAttrs := buildDraftOperation(req)
+ srv.Logger.Info("ACCESS",
+ "user_email", userEmail,
+ "doc_id", docID,
+ "operation", operation,
+ "updated_attributes", updatedAttrs,
+ "mode", "draft",
+ )
+
// Request post-processing.
go func() {
// Convert document to Algolia object.
@@ -1567,9 +2090,7 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
return
}
// Get document from database.
- dbDoc := models.Document{
- GoogleFileID: docID,
- }
+ dbDoc := srv.NewDocumentByFileID(docID)
if err := dbDoc.Get(srv.DB); err != nil {
srv.Logger.Error(
"error getting document from database for data comparison",
@@ -1583,9 +2104,7 @@ func DraftsDocumentHandler(srv server.Server) http.Handler {
// Get all reviews for the document.
var reviews models.DocumentReviews
if err := reviews.Find(srv.DB, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
+ Document: srv.NewDocumentByFileID(docID),
}); err != nil {
srv.Logger.Error(
"error getting all reviews for document for data comparison",
@@ -1649,17 +2168,236 @@ func validateDocType(
return false
}
-// removeSharing lists permissions for a document and then
-// deletes the permission for the supplied user email
-func removeSharing(s *gw.Service, docID, email string) error {
- permissions, err := s.ListPermissions(docID)
- if err != nil {
+// TODO : Need to validate users permission for people not part of the Hermes Sharepoint Group. (Contributors/Approvers) Contributors validated, Need to check Approvers.
+
+// createDraftDBAndShare creates the database record and shares the draft with
+// the owner and contributors. It writes HTTP errors to the ResponseWriter and
+// returns a non-nil error if the caller should return early.
+func createDraftDBAndShare(
+ srv server.Server, r *http.Request, w http.ResponseWriter,
+ doc *document.Document, fileID string, createdTime time.Time,
+ req DraftsRequest, userEmail string,
+) error {
+ // Create document in the database.
+ var contributors []*models.User
+ for _, c := range req.Contributors {
+ contributors = append(contributors, &models.User{
+ EmailAddress: c,
+ })
+ }
+
+ docByFileID := srv.NewDocumentByFileID(fileID)
+ model := models.Document{
+ GoogleFileID: docByFileID.GoogleFileID,
+ FileID: docByFileID.FileID,
+ Contributors: contributors,
+ DocumentCreatedAt: createdTime,
+ DocumentModifiedAt: createdTime,
+ DocumentType: models.DocumentType{
+ Name: req.DocType,
+ },
+ Owner: &models.User{
+ EmailAddress: userEmail,
+ },
+ Product: models.Product{
+ Name: req.Product,
+ },
+ Status: models.WIPDocumentStatus,
+ Summary: &req.Summary,
+ Title: req.Title,
+ }
+ if err := model.Create(srv.DB); err != nil {
+ srv.Logger.Error("error creating document in database",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", fileID,
+ )
+ http.Error(w, "Error creating document draft",
+ http.StatusInternalServerError)
return err
}
- for _, p := range permissions {
- if p.EmailAddress == email {
- return s.DeletePermission(docID, p.Id)
+
+ // Share the document with the owner.
+ if srv.SharePoint != nil {
+ if err := srv.SharePoint.ShareFile(fileID, userEmail, "writer"); err != nil {
+ srv.Logger.Error("error sharing file with owner",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", fileID,
+ "owner", userEmail,
+ )
+ srv.Logger.Warn("continuing document creation despite sharing failure with owner")
+ }
+ } else {
+ if err := srv.GWService.ShareFile(fileID, userEmail, "writer"); err != nil {
+ srv.Logger.Error("error sharing file with owner",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", fileID,
+ "owner", userEmail,
+ )
+ srv.Logger.Warn("continuing document creation despite sharing failure with owner")
}
}
+
+ // Share the document with contributors.
+ contributorsToEmail := []string{}
+ for _, contributor := range req.Contributors {
+ if strings.EqualFold(contributor, userEmail) {
+ continue
+ }
+
+ var shareErr error
+ if srv.SharePoint != nil {
+ shareErr = srv.SharePoint.ShareFile(fileID, contributor, "writer")
+ } else {
+ shareErr = srv.GWService.ShareFile(fileID, contributor, "writer")
+ }
+ if shareErr != nil {
+ srv.Logger.Error("error sharing file with contributor",
+ "error", shareErr,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", fileID,
+ "contributor", contributor,
+ )
+ srv.Logger.Warn("continuing document creation despite sharing failure with contributor")
+ } else {
+ contributorsToEmail = append(contributorsToEmail, contributor)
+ }
+ }
+
+ if len(contributorsToEmail) > 0 {
+ docURL := fmt.Sprintf("%s/document/%s?draft=true", srv.Config.BaseURL, fileID)
+
+ srv.Logger.Info("contributor email queued",
+ "doc_id", fileID,
+ "contributor_count", len(contributorsToEmail),
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+
+ go helpers.SendEmailWithRetry(
+ &srv,
+ func() error {
+ return email.SendContributorAddedEmail(
+ email.ContributorAddedEmailData{
+ BaseURL: srv.Config.BaseURL,
+ DocumentOwner: userEmail,
+ DocumentShortName: fmt.Sprintf("%s-???", req.ProductAbbreviation),
+ DocumentTitle: req.Title,
+ DocumentType: req.DocType,
+ DocumentStatus: "WIP",
+ DocumentURL: docURL,
+ Product: req.Product,
+ },
+ contributorsToEmail,
+ srv.Config.Email.FromAddress,
+ srv.GetEmailSender(),
+ )
+ },
+ fileID,
+ "contributor_added",
+ r,
+ )
+ }
+
return nil
}
+
+// removeSharing handles permission removal for documents.
+// It uses the pre-built emailToPermissionIDMap to find and delete permissions.
+func removeSharing(srv server.Server, docID, email string, emailToPermissionIDMap map[string][]string) error {
+ if permissionIDs, exists := emailToPermissionIDMap[email]; exists {
+ // Remove all permissions associated with the email.
+ for _, pid := range permissionIDs {
+ if srv.SharePoint != nil {
+ if err := srv.SharePoint.DeletePermission(docID, pid); err != nil {
+ return fmt.Errorf("error removing permission ID %s for email %s: %w", pid, email, err)
+ }
+ } else {
+ if err := srv.GWService.DeletePermission(docID, pid); err != nil {
+ return fmt.Errorf("error removing permission ID %s for email %s: %w", pid, email, err)
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+// buildDraftOperation determines the primary operation and builds a list of updated attributes
+// from a DraftsPatchRequest for logging purposes.
+func buildDraftOperation(req DraftsPatchRequest) (string, string) {
+ var attrs []string
+ var operation string
+
+ // Determine primary operation based on what's being changed
+ if req.Owners != nil {
+ operation = "ownership_transferred"
+ attrs = append(attrs, "owners")
+ }
+ if req.Product != nil {
+ if operation == "" {
+ operation = "product_changed"
+ }
+ attrs = append(attrs, "product")
+ }
+ if req.Approvers != nil {
+ if operation == "" {
+ operation = "approvers_updated"
+ }
+ attrs = append(attrs, "approvers")
+ }
+ if req.ApproverGroups != nil {
+ if operation == "" {
+ operation = "approver_groups_updated"
+ }
+ attrs = append(attrs, "approverGroups")
+ }
+ if req.Contributors != nil {
+ if operation == "" {
+ operation = "contributors_updated"
+ }
+ attrs = append(attrs, "contributors")
+ }
+ if req.CustomFields != nil {
+ if operation == "" {
+ operation = "custom_fields_updated"
+ }
+ attrs = append(attrs, "customFields")
+ }
+ if req.Summary != nil {
+ if operation == "" {
+ operation = "summary_updated"
+ }
+ attrs = append(attrs, "summary")
+ }
+ if req.Title != nil {
+ if operation == "" {
+ operation = "title_updated"
+ }
+ attrs = append(attrs, "title")
+ }
+
+ if operation == "" {
+ operation = "draft_updated"
+ }
+
+ // If multiple fields updated, mark as bulk update
+ if len(attrs) > 1 {
+ operation = "draft_bulk_update"
+ }
+
+ var attrsList string
+ if len(attrs) == 0 {
+ attrsList = "none"
+ } else {
+ attrsList = fmt.Sprintf("[%s]", strings.Join(attrs, ", "))
+ }
+
+ return operation, attrsList
+}
diff --git a/internal/api/v2/drafts_archived.go b/internal/api/v2/drafts_archived.go
new file mode 100644
index 000000000..64aebf59e
--- /dev/null
+++ b/internal/api/v2/drafts_archived.go
@@ -0,0 +1,233 @@
+package api
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/hashicorp-forge/hermes/internal/config"
+ "github.com/hashicorp-forge/hermes/pkg/algolia"
+ "github.com/hashicorp-forge/hermes/pkg/document"
+ "github.com/hashicorp-forge/hermes/pkg/models"
+ "github.com/hashicorp/go-hclog"
+ "gorm.io/gorm"
+)
+
+type draftsArchivedPatchRequest struct {
+ Archived *bool `json:"archived"`
+}
+
+type draftsArchivedGetResponse struct {
+ Archived bool `json:"archived"`
+}
+
+func draftsArchivedHandler(
+ w http.ResponseWriter,
+ r *http.Request,
+ docID string,
+ doc document.Document,
+ cfg config.Config,
+ l hclog.Logger,
+ algoWrite *algolia.Client,
+ db *gorm.DB,
+ useSharePoint bool,
+) {
+ switch r.Method {
+ case "GET":
+ // Get document from database.
+ d := models.NewDocumentByFileID(docID, useSharePoint)
+ if err := d.Get(db); err != nil {
+ l.Error("error getting document from database",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error accessing document",
+ http.StatusInternalServerError)
+ return
+ }
+
+ resp := draftsArchivedGetResponse{
+ Archived: d.Archived,
+ }
+
+ // Write response.
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ enc := json.NewEncoder(w)
+ if err := enc.Encode(resp); err != nil {
+ l.Error("error encoding response",
+ "error", err,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error building response", http.StatusInternalServerError)
+ return
+ }
+
+ case "PATCH":
+ // Authorize request (only the document owner is authorized).
+ userEmail := r.Context().Value("userEmail").(string)
+ if doc.Owners[0] != userEmail {
+ http.Error(w, "Only the document owner can archive documents",
+ http.StatusForbidden)
+ return
+ }
+
+ // Make sure document is a draft (WIP status).
+ if doc.Status != "WIP" {
+ http.Error(w, "Only draft documents can be archived",
+ http.StatusBadRequest)
+ return
+ }
+
+ // Decode request.
+ var req draftsArchivedPatchRequest
+ if err := decodeRequest(r, &req); err != nil {
+ l.Error("error decoding request",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w, "Bad request", http.StatusBadRequest)
+ return
+ }
+
+ // Validate request.
+ if req.Archived == nil {
+ l.Warn("bad request: missing required 'archived' field",
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w,
+ "Bad request: missing required 'archived' field",
+ http.StatusBadRequest)
+ return
+ }
+
+ // Get document from database.
+ doc := models.NewDocumentByFileID(docID, useSharePoint)
+ if err := doc.Get(db); err != nil {
+ l.Error("error getting document from database",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error accessing document",
+ http.StatusInternalServerError)
+ return
+ }
+
+ // Update Archived for document in the database.
+ if err := db.Model(&doc).
+ // We need to update using Select because Archived is a boolean.
+ Select("Archived").
+ Updates(models.Document{Archived: *req.Archived}).
+ Error; err != nil {
+ l.Error("error updating Archived in the database",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error updating document draft",
+ http.StatusInternalServerError)
+ return
+ }
+
+ l.Info("updated Archived for document",
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ "archived", *req.Archived,
+ )
+
+ // Update Algolia in the background.
+ go func() {
+ // Get updated document from database to get all fields.
+ updatedDoc := models.NewDocumentByFileID(docID, useSharePoint)
+ if err := updatedDoc.Get(db); err != nil {
+ l.Error("error getting updated document from database for Algolia",
+ "error", err,
+ "doc_id", docID,
+ )
+ return
+ }
+
+ // Get reviews for the document.
+ var reviews models.DocumentReviews
+ if err := reviews.Find(db, models.DocumentReview{
+ Document: models.NewDocumentByFileID(docID, useSharePoint),
+ }); err != nil {
+ l.Error("error getting reviews for document for Algolia",
+ "error", err,
+ "doc_id", docID,
+ )
+ return
+ }
+
+ // Get group reviews for the document.
+ var groupReviews models.DocumentGroupReviews
+ if err := groupReviews.Find(db, models.DocumentGroupReview{
+ Document: models.NewDocumentByFileID(docID, useSharePoint),
+ }); err != nil {
+ l.Error("error getting group reviews for document for Algolia",
+ "error", err,
+ "doc_id", docID,
+ )
+ return
+ }
+
+ // Convert database model to document.
+ convertedDoc, err := document.NewFromDatabaseModel(updatedDoc, reviews, groupReviews)
+ if err != nil {
+ l.Error("error converting document from database model for Algolia",
+ "error", err,
+ "doc_id", docID,
+ )
+ return
+ }
+
+ // Convert document to Algolia object.
+ docObj, err := convertedDoc.ToAlgoliaObject(true)
+ if err != nil {
+ l.Error("error converting document to Algolia object",
+ "error", err,
+ "doc_id", docID,
+ )
+ return
+ }
+
+ // Save updated draft doc object in Algolia.
+ res, err := algoWrite.Drafts.SaveObject(docObj)
+ if err != nil {
+ l.Error("error saving archived status to Algolia",
+ "error", err,
+ "doc_id", docID,
+ )
+ return
+ }
+ err = res.Wait()
+ if err != nil {
+ l.Error("error waiting for Algolia save operation",
+ "error", err,
+ "doc_id", docID,
+ )
+ return
+ }
+
+ l.Info("updated Algolia with archived status",
+ "doc_id", docID,
+ "archived", *req.Archived,
+ )
+ }()
+
+ w.WriteHeader(http.StatusOK)
+
+ default:
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+}
diff --git a/internal/api/v2/drafts_shareable.go b/internal/api/v2/drafts_shareable.go
index efb30571f..66df95f3a 100644
--- a/internal/api/v2/drafts_shareable.go
+++ b/internal/api/v2/drafts_shareable.go
@@ -31,13 +31,12 @@ func draftsShareableHandler(
algoRead *algolia.Client,
goog *gw.Service,
db *gorm.DB,
+ useSharePoint bool,
) {
switch r.Method {
case "GET":
// Get document from database.
- d := models.Document{
- GoogleFileID: docID,
- }
+ d := models.NewDocumentByFileID(docID, useSharePoint)
if err := d.Get(db); err != nil {
l.Error("error getting document from database",
"error", err,
@@ -71,6 +70,12 @@ func draftsShareableHandler(
// Authorize request (only the document owner is authorized).
userEmail := r.Context().Value("userEmail").(string)
if doc.Owners[0] != userEmail {
+ l.Warn("unauthorized attempt to change draft shareable settings",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "user_email", userEmail,
+ "owner", doc.Owners[0])
http.Error(w, "Only the document owner can change shareable settings",
http.StatusForbidden)
return
@@ -103,9 +108,7 @@ func draftsShareableHandler(
}
// Get document from database.
- doc := models.Document{
- GoogleFileID: docID,
- }
+ doc := models.NewDocumentByFileID(docID, useSharePoint)
if err := doc.Get(db); err != nil {
l.Error("error getting document from database",
"error", err,
@@ -118,47 +121,51 @@ func draftsShareableHandler(
return
}
- // Find out if the draft is already shared with the domain.
- perms, err := goog.ListPermissions(docID)
- if err != nil {
- l.Error("error listing Google Drive permissions",
- "error", err,
- "path", r.URL.Path,
- "method", r.Method,
- "doc_id", docID,
- )
- http.Error(w,
- "Error updating document permissions",
- http.StatusInternalServerError)
- return
- }
- alreadySharedPermIDs := []string{}
- for _, p := range perms {
- isInherited := false
- for _, pd := range p.PermissionDetails {
- if pd.Inherited {
- isInherited = true
- }
+ // Update file permissions for Google Workspace documents.
+ if !useSharePoint {
+ // Find out if the draft is already shared with the domain.
+ perms, err := goog.ListPermissions(docID)
+ if err != nil {
+ l.Error("error listing Google Drive permissions",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w,
+ "Error updating document permissions",
+ http.StatusInternalServerError)
+ return
}
- if p.Domain == cfg.GoogleWorkspace.Domain &&
- p.Role == "commenter" &&
- !isInherited {
- alreadySharedPermIDs = append(alreadySharedPermIDs, p.Id)
+ alreadySharedPermIDs := []string{}
+ for _, p := range perms {
+ isInherited := false
+ for _, pd := range p.PermissionDetails {
+ if pd.Inherited {
+ isInherited = true
+ }
+ }
+ if p.Domain == cfg.GoogleWorkspace.Domain &&
+ p.Role == "commenter" &&
+ !isInherited {
+ alreadySharedPermIDs = append(alreadySharedPermIDs, p.Id)
+ }
}
- }
- // Update file permissions, if necessary.
- if *req.IsShareable {
- if len(alreadySharedPermIDs) == 0 {
- // File is not already shared with domain, so share it.
- goog.ShareFileWithDomain(docID, cfg.GoogleWorkspace.Domain, "commenter")
- }
- } else {
- for _, id := range alreadySharedPermIDs {
- // File is already shared with domain, so remove the permission.
- goog.DeletePermission(docID, id)
+ // Update file permissions, if necessary.
+ if *req.IsShareable {
+ if len(alreadySharedPermIDs) == 0 {
+ // File is not already shared with domain, so share it.
+ goog.ShareFileWithDomain(docID, cfg.GoogleWorkspace.Domain, "commenter")
+ }
+ } else {
+ for _, id := range alreadySharedPermIDs {
+ // File is already shared with domain, so remove the permission.
+ goog.DeletePermission(docID, id)
+ }
}
}
+ // TODO: Enable SharePoint organisation-level sharing when isShareable is true.
// Update ShareableAsDraft for document in the database.
if err := db.Model(&doc).
diff --git a/internal/api/v2/groups.go b/internal/api/v2/groups.go
index ce0bd2988..0c1c626e7 100644
--- a/internal/api/v2/groups.go
+++ b/internal/api/v2/groups.go
@@ -7,6 +7,7 @@ import (
"strings"
"github.com/hashicorp-forge/hermes/internal/server"
+ "github.com/hashicorp-forge/hermes/pkg/sharepointhelper"
admin "google.golang.org/api/admin/directory/v1"
)
@@ -30,7 +31,7 @@ type GroupsPostResponseGroup struct {
Name string `json:"name,omitempty"`
}
-// GroupsHandler returns information about Google Groups.
+// GroupsHandler returns information about groups (Microsoft or Google).
func GroupsHandler(srv server.Server) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logArgs := []any{
@@ -48,131 +49,250 @@ func GroupsHandler(srv server.Server) http.Handler {
}
// Respond with error if group approvals are not enabled.
- if srv.Config.GoogleWorkspace.GroupApprovals == nil ||
- !srv.Config.GoogleWorkspace.GroupApprovals.Enabled {
- http.Error(w,
- "Group approvals have not been enabled", http.StatusUnprocessableEntity)
- return
+ if srv.SharePoint != nil {
+ if srv.Config.SharePoint.GroupApprovals == nil ||
+ !srv.Config.SharePoint.GroupApprovals.Enabled {
+ srv.Logger.Warn("group approvals not enabled", logArgs...)
+ http.Error(w,
+ "Group approvals have not been enabled", http.StatusUnprocessableEntity)
+ return
+ }
+ } else {
+ if srv.Config.GoogleWorkspace.GroupApprovals == nil ||
+ !srv.Config.GoogleWorkspace.GroupApprovals.Enabled {
+ http.Error(w,
+ "Group approvals have not been enabled", http.StatusUnprocessableEntity)
+ return
+ }
}
switch r.Method {
- case "POST":
- // Decode request.
- req := &GroupsPostRequest{}
- if err := decodeRequest(r, &req); err != nil {
- srv.Logger.Warn("error decoding request",
- append([]interface{}{
- "error", err,
- }, logArgs...)...)
- http.Error(w, fmt.Sprintf("Bad request: %q", err),
- http.StatusBadRequest)
- return
- }
+ case http.MethodPost:
+ handleGroupsPost(srv, w, r, logArgs)
+ default:
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+ })
+}
- // Sanitize query.
- query := req.Query
- query = strings.ReplaceAll(query, " ", "-")
-
- var (
- allGroups []*admin.Group
- err error
- groups, prefixGroups *admin.Groups
- maxNonPrefixGroups = maxGroupResults
- )
-
- // Retrieve groups with prefix, if configured.
- searchPrefix := ""
- if srv.Config.GoogleWorkspace.GroupApprovals != nil &&
- srv.Config.GoogleWorkspace.GroupApprovals.SearchPrefix != "" {
- searchPrefix = srv.Config.GoogleWorkspace.GroupApprovals.SearchPrefix
- }
- if searchPrefix != "" {
- maxNonPrefixGroups = maxGroupResults - maxPrefixGroupResults
-
- prefixQuery := fmt.Sprintf(
- "%s%s", searchPrefix, query)
- prefixGroups, err = srv.GWService.AdminDirectory.Groups.List().
- Domain(srv.Config.GoogleWorkspace.Domain).
- MaxResults(maxPrefixGroupResults).
- Query(fmt.Sprintf("email:%s*", prefixQuery)).
- Do()
- if err != nil {
- srv.Logger.Error("error searching groups with prefix",
- append([]interface{}{
- "error", err,
- }, logArgs...)...)
- http.Error(w, fmt.Sprintf("Error searching groups: %q", err),
- http.StatusInternalServerError)
- return
- }
- }
+// handleGroupsPost processes POST requests for group search.
+func handleGroupsPost(srv server.Server, w http.ResponseWriter, r *http.Request, logArgs []any) {
+ // Decode request.
+ req := &GroupsPostRequest{}
+ if err := decodeRequest(r, &req); err != nil {
+ srv.Logger.Warn("error decoding request",
+ append([]interface{}{
+ "error", err,
+ }, logArgs...)...)
+ http.Error(w, fmt.Sprintf("Bad request: %q", err),
+ http.StatusBadRequest)
+ return
+ }
- // Retrieve groups without prefix.
- groups, err = srv.GWService.AdminDirectory.Groups.List().
- Domain(srv.Config.GoogleWorkspace.Domain).
- MaxResults(int64(maxNonPrefixGroups)).
- Query(fmt.Sprintf("email:%s*", query)).
- Do()
- if err != nil {
- srv.Logger.Error("error searching groups without prefix",
- append([]interface{}{
- "error", err,
- }, logArgs...)...)
- http.Error(w, fmt.Sprintf("Error searching groups: %q", err),
- http.StatusInternalServerError)
- return
- }
+ // Sanitize query.
+ query := req.Query
+ query = strings.ReplaceAll(query, " ", "-")
- allGroups = concatGroupSlicesAndRemoveDuplicates(
- prefixGroups.Groups, groups.Groups)
+ if srv.SharePoint != nil {
+ handleGroupsPostSharePoint(srv, w, query, logArgs)
+ } else {
+ handleGroupsPostGoogle(srv, w, query, logArgs)
+ }
+}
- // Build response, stripping all attributes except email and name.
- resp := make(GroupsPostResponse, len(allGroups))
- for i, group := range allGroups {
- resp[i] = GroupsPostResponseGroup{
- Email: group.Email,
- Name: group.Name,
- }
- }
+// handleGroupsPostSharePoint handles group search using Microsoft Graph.
+func handleGroupsPostSharePoint(srv server.Server, w http.ResponseWriter, query string, logArgs []any) {
+ var (
+ allGroups []sharepointhelper.Group
+ err error
+ groups, prefixGroups []sharepointhelper.Group
+ maxNonPrefixGroups = maxGroupResults
+ )
- // Write response.
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
- enc := json.NewEncoder(w)
- err = enc.Encode(resp)
- if err != nil {
- srv.Logger.Error("error encoding groups response",
- append([]interface{}{
- "error", err,
- }, logArgs...)...)
- http.Error(w, "Error searching groups",
- http.StatusInternalServerError)
- return
- }
+ // Retrieve groups with prefix, if configured.
+ searchPrefix := ""
+ if srv.Config.SharePoint.GroupApprovals != nil &&
+ srv.Config.SharePoint.GroupApprovals.SearchPrefix != "" {
+ searchPrefix = srv.Config.SharePoint.GroupApprovals.SearchPrefix
+ }
+ if searchPrefix != "" {
+ maxNonPrefixGroups = maxGroupResults - maxPrefixGroupResults
- default:
- w.WriteHeader(http.StatusMethodNotAllowed)
+ prefixQuery := fmt.Sprintf(
+ "%s%s", searchPrefix, query)
+ prefixGroups, err = srv.SharePoint.SearchGroup(
+ prefixQuery, srv.Config.SharePoint.Domain, maxPrefixGroupResults)
+ if err != nil {
+ srv.Logger.Error("error searching groups with prefix",
+ append([]interface{}{
+ "error", err,
+ }, logArgs...)...)
+ http.Error(w, fmt.Sprintf("Error searching groups: %q", err),
+ http.StatusInternalServerError)
return
}
- })
+ }
+
+ // Retrieve groups without prefix.
+ groups, err = srv.SharePoint.SearchGroup(
+ query, srv.Config.SharePoint.Domain, maxNonPrefixGroups)
+ if err != nil {
+ srv.Logger.Error("error searching groups without prefix",
+ append([]interface{}{
+ "error", err,
+ }, logArgs...)...)
+ http.Error(w, fmt.Sprintf("Error searching groups: %q", err),
+ http.StatusInternalServerError)
+ return
+ }
+
+ allGroups = concatSPGroupSlicesAndRemoveDuplicates(
+ prefixGroups, groups)
+
+ // Build response.
+ resp := make(GroupsPostResponse, len(allGroups))
+ for i, group := range allGroups {
+ resp[i] = GroupsPostResponseGroup{
+ Email: group.Mail,
+ Name: group.DisplayName,
+ }
+ }
+
+ writeGroupsResponse(srv, w, resp, logArgs)
}
-// concatGroupSlicesAndRemoveDuplicates concatenates two group slices and
-// removes any duplicate elements from the result.
-func concatGroupSlicesAndRemoveDuplicates(
+// handleGroupsPostGoogle handles group search using Google Admin Directory.
+func handleGroupsPostGoogle(srv server.Server, w http.ResponseWriter, query string, logArgs []any) {
+ var (
+ allGroups []*admin.Group
+ err error
+ groups, prefixGroups *admin.Groups
+ maxNonPrefixGroups = maxGroupResults
+ )
+
+ // Retrieve groups with prefix, if configured.
+ searchPrefix := ""
+ if srv.Config.GoogleWorkspace.GroupApprovals != nil &&
+ srv.Config.GoogleWorkspace.GroupApprovals.SearchPrefix != "" {
+ searchPrefix = srv.Config.GoogleWorkspace.GroupApprovals.SearchPrefix
+ }
+ if searchPrefix != "" {
+ maxNonPrefixGroups = maxGroupResults - maxPrefixGroupResults
+
+ prefixQuery := fmt.Sprintf(
+ "%s%s", searchPrefix, query)
+ prefixGroups, err = srv.GWService.AdminDirectory.Groups.List().
+ Domain(srv.Config.GoogleWorkspace.Domain).
+ MaxResults(int64(maxPrefixGroupResults)).
+ Query(fmt.Sprintf("email:%s*", prefixQuery)).
+ Do()
+ if err != nil {
+ srv.Logger.Error("error searching groups with prefix",
+ append([]interface{}{
+ "error", err,
+ }, logArgs...)...)
+ http.Error(w, fmt.Sprintf("Error searching groups: %q", err),
+ http.StatusInternalServerError)
+ return
+ }
+ }
+
+ // Retrieve groups without prefix.
+ groups, err = srv.GWService.AdminDirectory.Groups.List().
+ Domain(srv.Config.GoogleWorkspace.Domain).
+ MaxResults(int64(maxNonPrefixGroups)).
+ Query(fmt.Sprintf("email:%s*", query)).
+ Do()
+ if err != nil {
+ srv.Logger.Error("error searching groups without prefix",
+ append([]interface{}{
+ "error", err,
+ }, logArgs...)...)
+ http.Error(w, fmt.Sprintf("Error searching groups: %q", err),
+ http.StatusInternalServerError)
+ return
+ }
+
+ var prefixGroupsList []*admin.Group
+ if prefixGroups != nil {
+ prefixGroupsList = prefixGroups.Groups
+ }
+ allGroups = concatGoogleGroupSlicesAndRemoveDuplicates(
+ prefixGroupsList, groups.Groups)
+
+ // Build response.
+ resp := make(GroupsPostResponse, len(allGroups))
+ for i, group := range allGroups {
+ resp[i] = GroupsPostResponseGroup{
+ Email: group.Email,
+ Name: group.Name,
+ }
+ }
+
+ writeGroupsResponse(srv, w, resp, logArgs)
+}
+
+// writeGroupsResponse writes the groups response.
+func writeGroupsResponse(srv server.Server, w http.ResponseWriter, resp GroupsPostResponse, logArgs []any) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ enc := json.NewEncoder(w)
+ err := enc.Encode(resp)
+ if err != nil {
+ srv.Logger.Error("error encoding groups response",
+ append([]interface{}{
+ "error", err,
+ }, logArgs...)...)
+ http.Error(w, "Error searching groups",
+ http.StatusInternalServerError)
+ return
+ }
+}
+
+// concatSPGroupSlicesAndRemoveDuplicates concatenates two SharePoint group slices
+// and removes any duplicate elements from the result.
+func concatSPGroupSlicesAndRemoveDuplicates(
+ slice1, slice2 []sharepointhelper.Group) []sharepointhelper.Group {
+ uniqueMap := make(map[string]sharepointhelper.Group)
+ result := []sharepointhelper.Group{}
+
+ for _, g := range slice1 {
+ if g.Mail != "" {
+ uniqueMap[g.Mail] = g
+ }
+ }
+ for _, g := range slice2 {
+ if g.Mail != "" {
+ uniqueMap[g.Mail] = g
+ }
+ }
+
+ for _, v := range uniqueMap {
+ result = append(result, v)
+ }
+
+ return result
+}
+
+// concatGoogleGroupSlicesAndRemoveDuplicates concatenates two Google group slices
+// and removes any duplicate elements from the result.
+func concatGoogleGroupSlicesAndRemoveDuplicates(
slice1, slice2 []*admin.Group) []*admin.Group {
uniqueMap := make(map[string]*admin.Group)
result := []*admin.Group{}
- // Add elements from both slices to the map.
for _, g := range slice1 {
- uniqueMap[g.Email] = g
+ if g != nil {
+ uniqueMap[g.Email] = g
+ }
}
for _, g := range slice2 {
- uniqueMap[g.Email] = g
+ if g != nil {
+ uniqueMap[g.Email] = g
+ }
}
- // Add all unique elements from the map to the result slice.
for _, v := range uniqueMap {
result = append(result, v)
}
diff --git a/internal/api/v2/helpers.go b/internal/api/v2/helpers.go
index 710bb2b58..2f78ecb17 100644
--- a/internal/api/v2/helpers.go
+++ b/internal/api/v2/helpers.go
@@ -5,23 +5,25 @@ import (
"fmt"
"io"
"net/http"
+ "net/url"
"reflect"
"regexp"
"strings"
"github.com/hashicorp-forge/hermes/internal/config"
- gw "github.com/hashicorp-forge/hermes/pkg/googleworkspace"
+ "github.com/hashicorp-forge/hermes/internal/server"
"github.com/hashicorp-forge/hermes/pkg/models"
+ "github.com/hashicorp-forge/hermes/pkg/sharepointhelper"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/iancoleman/strcase"
"github.com/stretchr/testify/assert"
)
-// contains returns true if a string is present in a slice of strings.
+// contains returns true if a string is present in a slice of strings (case-insensitive for emails).
func contains(values []string, s string) bool {
for _, v := range values {
- if s == v {
+ if strings.EqualFold(s, v) {
return true
}
}
@@ -30,20 +32,19 @@ func contains(values []string, s string) bool {
// compareSlices compares the first slice with the second
// and returns the elements that exist in the second slice
-// that don't exist in the first
+// that don't exist in the first (case-insensitive comparison for email addresses)
func compareSlices(a, b []string) []string {
- // Create a map with the length of slice "a"
+ // Create a map with the length of slice "a" using lowercase keys for case-insensitive comparison
tempA := make(map[string]bool, len(a))
for _, j := range a {
- tempA[j] = true
+ tempA[strings.ToLower(strings.TrimSpace(j))] = true
}
diffElems := []string{}
for _, k := range b {
- // If elements in slice "b" are
- // not present in slice "a" then
- // append to diffElems slice
- if !tempA[k] {
+ // If elements in slice "b" are not present in slice "a" then append to diffElems slice
+ // Use case-insensitive comparison to avoid false positives from casing differences
+ if !tempA[strings.ToLower(strings.TrimSpace(k))] {
diffElems = append(diffElems, k)
}
}
@@ -51,6 +52,96 @@ func compareSlices(a, b []string) []string {
return diffElems
}
+// expandStakeholderGroups expands any groups in the stakeholders list to their individual members recursively.
+// It handles nested groups (group within group within group) by using the backend service's
+// group member expansion methods.
+// Individual email addresses are added directly without making unnecessary API calls.
+func expandStakeholderGroups(stakeholders []string, srv server.Server) ([]string, error) {
+ if len(stakeholders) == 0 {
+ return []string{}, nil
+ }
+
+ // Use a map to deduplicate emails (case-insensitive)
+ uniqueEmails := make(map[string]string) // lowercase key -> original email
+
+ for _, stakeholder := range stakeholders {
+ stakeholder = strings.TrimSpace(stakeholder)
+ if stakeholder == "" {
+ continue
+ }
+
+ if srv.SharePoint != nil {
+ // SharePoint path: try to expand as a group using Microsoft Graph
+ members, err := srv.SharePoint.GetGroupMemberEmails(stakeholder)
+ if err != nil {
+ // Not a group or error expanding - treat as individual email
+ srv.Logger.Debug("treating stakeholder as individual email",
+ "stakeholder", stakeholder,
+ "reason", "not a group or expansion failed")
+
+ key := strings.ToLower(stakeholder)
+ if _, exists := uniqueEmails[key]; !exists {
+ uniqueEmails[key] = stakeholder
+ }
+ } else {
+ // Successfully expanded group
+ srv.Logger.Debug("expanded stakeholder group",
+ "group", stakeholder,
+ "member_count", len(members))
+
+ for _, member := range members {
+ member = strings.TrimSpace(member)
+ if member == "" {
+ continue
+ }
+ key := strings.ToLower(member)
+ if _, exists := uniqueEmails[key]; !exists {
+ uniqueEmails[key] = member
+ }
+ }
+ }
+ } else {
+ // Google path: try to expand as a Google Group using Admin Directory
+ groupMembers, err := srv.GWService.AdminDirectory.Members.List(stakeholder).Do()
+ if err != nil {
+ // Not a group or error expanding - treat as individual email
+ srv.Logger.Debug("treating stakeholder as individual email",
+ "stakeholder", stakeholder,
+ "reason", "not a group or expansion failed")
+
+ key := strings.ToLower(stakeholder)
+ if _, exists := uniqueEmails[key]; !exists {
+ uniqueEmails[key] = stakeholder
+ }
+ } else {
+ // Successfully expanded group
+ srv.Logger.Debug("expanded stakeholder group",
+ "group", stakeholder,
+ "member_count", len(groupMembers.Members))
+
+ for _, member := range groupMembers.Members {
+ email := member.Email
+ if email == "" {
+ continue
+ }
+ key := strings.ToLower(email)
+ if _, exists := uniqueEmails[key]; !exists {
+ uniqueEmails[key] = email
+ }
+ }
+ }
+ }
+ }
+
+ // Convert map to slice
+ result := make([]string, 0, len(uniqueEmails))
+ for _, email := range uniqueEmails {
+ result = append(result, email)
+ }
+
+ return result, nil
+}
+
// decodeRequest decodes the JSON contents of a HTTP request body to a request
// struct. An error is returned if the request contains fields that do not exist
// in the request struct.
@@ -136,16 +227,16 @@ func CompareAlgoliaAndDatabaseDocument(
var result *multierror.Error
// Compare objectID.
- algoGoogleFileID, err := getStringValue(algoDoc, "objectID")
+ algoFileID, err := getStringValue(algoDoc, "objectID")
if err != nil {
result = multierror.Append(
result, fmt.Errorf("error getting objectID value: %w", err))
}
- if algoGoogleFileID != dbDoc.GoogleFileID {
+ if algoFileID != dbDoc.GetFileIdentifier() {
result = multierror.Append(result,
fmt.Errorf(
"objectID not equal, algolia=%v, db=%v",
- algoGoogleFileID, dbDoc.GoogleFileID),
+ algoFileID, dbDoc.GetFileIdentifier()),
)
}
@@ -413,7 +504,7 @@ func CompareAlgoliaAndDatabaseDocument(
} else {
dbFileRevisions := make(map[string]string)
for _, fr := range dbDoc.FileRevisions {
- dbFileRevisions[fr.GoogleDriveFileRevisionID] = fr.Name
+ dbFileRevisions[fr.FileRevisionID] = fr.Name
}
if !reflect.DeepEqual(algoFileRevisions, dbFileRevisions) {
result = multierror.Append(result,
@@ -532,11 +623,64 @@ func CompareAlgoliaAndDatabaseDocument(
}
// isUserInGroups returns true if a user is in any supplied groups, false
-// otherwise.
+// otherwise. Works with both SharePoint (Microsoft Graph) and Google backends.
func isUserInGroups(
- userEmail string, groupEmails []string, svc *gw.Service) (bool, error) {
- // Get groups for user.
- userGroups, err := svc.AdminDirectory.Groups.List().
+ userEmail string, groupEmails []string, srv server.Server) (bool, error) {
+ if len(groupEmails) == 0 {
+ return false, nil
+ }
+
+ if srv.SharePoint != nil {
+ // SharePoint path: use Microsoft Graph API
+ graphURL := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/%s/memberOf?$select=id,displayName,mail",
+ url.QueryEscape(userEmail))
+
+ options := &sharepointhelper.APIOptions{
+ Headers: map[string]string{
+ "Content-Type": "application/json",
+ },
+ }
+
+ resp, err := srv.SharePoint.InvokeAPIWithOptions("GET", graphURL, nil, options)
+ if err != nil {
+ return false, fmt.Errorf("error making Graph API request for user groups: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return false, fmt.Errorf("microsoft Graph API returned status %d when fetching user groups", resp.StatusCode)
+ }
+
+ // Parse the response
+ var response struct {
+ Value []struct {
+ ID string `json:"id"`
+ DisplayName string `json:"displayName"`
+ Mail string `json:"mail"`
+ } `json:"value"`
+ }
+
+ if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
+ return false, fmt.Errorf("error decoding user groups response: %w", err)
+ }
+
+ // Check if any of the user's groups match the provided group emails
+ for _, group := range response.Value {
+ if group.Mail != "" && contains(groupEmails, group.Mail) {
+ return true, nil
+ }
+ }
+
+ return false, nil
+ }
+
+ if srv.Config.GoogleWorkspace.GroupApprovals == nil ||
+ !srv.Config.GoogleWorkspace.GroupApprovals.Enabled {
+ return false, nil
+ }
+
+ // Google path: use Admin Directory API
+ userGroups, err := srv.GWService.AdminDirectory.Groups.List().
UserKey(userEmail).
Do()
if err != nil {
diff --git a/internal/api/v2/helpers_test.go b/internal/api/v2/helpers_test.go
index 96276d884..1477eac8a 100644
--- a/internal/api/v2/helpers_test.go
+++ b/internal/api/v2/helpers_test.go
@@ -116,7 +116,7 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
}{
"good": {
algoDoc: map[string]any{
- "objectID": "GoogleFileID1",
+ "objectID": "FileID1",
"title": "Title1",
"docType": "RFC",
"docNumber": "ABC-123",
@@ -159,8 +159,8 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
"status": "In-Review",
},
dbDoc: models.Document{
- GoogleFileID: "GoogleFileID1",
- Title: "Title1",
+ FileID: "FileID1",
+ Title: "Title1",
DocumentType: models.DocumentType{
Name: "RFC",
},
@@ -212,12 +212,12 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
2023, time.April, 5, 23, 0, 0, 0, time.UTC),
FileRevisions: []models.DocumentFileRevision{
{
- GoogleDriveFileRevisionID: "1",
- Name: "FileRevision1",
+ FileRevisionID: "1",
+ Name: "FileRevision1",
},
{
- GoogleDriveFileRevisionID: "2",
- Name: "FileRevision2",
+ FileRevisionID: "2",
+ Name: "FileRevision2",
},
},
Owner: &models.User{
@@ -395,12 +395,12 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
2023, time.April, 5, 23, 0, 0, 0, time.UTC),
FileRevisions: []models.DocumentFileRevision{
{
- GoogleDriveFileRevisionID: "1",
- Name: "FileRevision1",
+ FileRevisionID: "1",
+ Name: "FileRevision1",
},
{
- GoogleDriveFileRevisionID: "2",
- Name: "FileRevision2",
+ FileRevisionID: "2",
+ Name: "FileRevision2",
},
},
Owner: &models.User{
@@ -437,7 +437,7 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
"bad objectID": {
algoDoc: map[string]any{
- "objectID": "GoogleFileID1",
+ "objectID": "FileID1",
"appCreated": true,
"docNumber": "ABC-123",
"createdTime": float64(time.Date(
@@ -448,7 +448,7 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
"product": "Product1",
},
dbDoc: models.Document{
- GoogleFileID: "BadGoogleFileID",
+ FileID: "BadFileID",
DocumentNumber: 123,
Product: models.Product{
Name: "Product1",
@@ -608,7 +608,7 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
"product": "Product1",
},
dbDoc: models.Document{
- GoogleFileID: "BadGoogleFileID",
+ FileID: "BadFileID",
DocumentNumber: 123,
Product: models.Product{
Name: "Product1",
@@ -770,7 +770,7 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
"product": "Product1",
},
dbDoc: models.Document{
- GoogleFileID: "BadGoogleFileID",
+ FileID: "BadFileID",
DocumentNumber: 123,
Product: models.Product{
Name: "Product1",
diff --git a/internal/api/v2/me.go b/internal/api/v2/me.go
index 2a065d322..f22989501 100644
--- a/internal/api/v2/me.go
+++ b/internal/api/v2/me.go
@@ -2,24 +2,253 @@ package api
import (
"encoding/json"
+ "errors"
"fmt"
"net/http"
+ "net/url"
+ "strings"
+ "time"
"github.com/hashicorp-forge/hermes/internal/server"
+ "github.com/hashicorp-forge/hermes/pkg/sharepointhelper"
)
-// MeGetResponse mimics the response from Google's `userinfo/me` API
-// (https://www.googleapis.com/userinfo/v2/me).
+// handleGetUserProfileGoogle handles the GET request for user profile data using Google Workspace.
+func handleGetUserProfileGoogle(srv server.Server, w http.ResponseWriter, r *http.Request, userEmail string) {
+ errResp := func(
+ httpCode int, userErrMsg, logErrMsg string, err error,
+ extraArgs ...interface{}) {
+ srv.Logger.Error(logErrMsg,
+ append([]interface{}{
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ }, extraArgs...)...,
+ )
+ http.Error(w, userErrMsg, httpCode)
+ }
+
+ ppl, err := srv.GWService.SearchPeople(
+ userEmail, "emailAddresses,names,photos")
+ if err != nil {
+ errResp(
+ http.StatusInternalServerError,
+ "Error getting user information",
+ "error searching people directory",
+ err,
+ )
+ return
+ }
+
+ // Verify that the result only contains one person.
+ if len(ppl) != 1 {
+ errResp(
+ http.StatusInternalServerError,
+ "Error getting user information",
+ fmt.Sprintf(
+ "wrong number of people in search result: %d", len(ppl)),
+ nil,
+ "user_email", userEmail,
+ )
+
+ // If configured, send an email to the user to notify them that their
+ // account was not found in the directory.
+ if srv.Config.Email != nil && srv.Config.Email.Enabled &&
+ srv.Config.GoogleWorkspace != nil &&
+ srv.Config.GoogleWorkspace.UserNotFoundEmail != nil &&
+ srv.Config.GoogleWorkspace.UserNotFoundEmail.Enabled &&
+ srv.Config.GoogleWorkspace.UserNotFoundEmail.Body != "" &&
+ srv.Config.GoogleWorkspace.UserNotFoundEmail.Subject != "" {
+ _, err = srv.GWService.SendEmail(
+ []string{userEmail},
+ srv.Config.Email.FromAddress,
+ srv.Config.GoogleWorkspace.UserNotFoundEmail.Subject,
+ srv.Config.GoogleWorkspace.UserNotFoundEmail.Body,
+ )
+ if err != nil {
+ srv.Logger.Error("error sending user not found email",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "user_email", userEmail,
+ )
+ } else {
+ srv.Logger.Info("user not found email sent",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "user_email", userEmail,
+ )
+ }
+ }
+
+ return
+ }
+ p := ppl[0]
+
+ // Make sure that the result's email address is the same as the
+ // authenticated user, is the primary email address, and is verified.
+ if len(p.EmailAddresses) == 0 ||
+ p.EmailAddresses[0].Value != userEmail ||
+ !p.EmailAddresses[0].Metadata.Primary ||
+ !p.EmailAddresses[0].Metadata.Verified {
+ errResp(
+ http.StatusInternalServerError,
+ "Error getting user information",
+ "wrong user in search result",
+ err,
+ )
+ return
+ }
+
+ // Verify other required values are set.
+ if len(p.Names) == 0 {
+ errResp(
+ http.StatusInternalServerError,
+ "Error getting user information",
+ "no names in result",
+ err,
+ )
+ return
+ }
+
+ // Write response.
+ resp := MeGetResponse{
+ ID: p.EmailAddresses[0].Metadata.Source.Id,
+ Email: p.EmailAddresses[0].Value,
+ Name: p.Names[0].DisplayName,
+ GivenName: p.Names[0].GivenName,
+ FamilyName: p.Names[0].FamilyName,
+ }
+ if len(p.Photos) > 0 {
+ resp.Picture = p.Photos[0].Url
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ enc := json.NewEncoder(w)
+ err = enc.Encode(resp)
+ if err != nil {
+ errResp(
+ http.StatusInternalServerError,
+ "Error getting user information",
+ "error encoding response",
+ err,
+ )
+ return
+ }
+}
+
type MeGetResponse struct {
- ID string `json:"id"`
- Email string `json:"email"`
- VerifiedEmail bool `json:"verified_email"`
- Name string `json:"name"`
- GivenName string `json:"given_name"`
- FamilyName string `json:"family_name"`
- Picture string `json:"picture"`
- Locale string `json:"locale,omitempty"`
- HD string `json:"hd,omitempty"`
+ ID string `json:"id"`
+ Email string `json:"email"`
+ Name string `json:"name"`
+ GivenName string `json:"given_name"`
+ FamilyName string `json:"family_name"`
+ Picture string `json:"picture"`
+ Locale string `json:"locale,omitempty"`
+ HD string `json:"hd,omitempty"`
+}
+
+// convertGraphUserToMeResponse converts Microsoft Graph user directly to Me response
+func convertGraphUserToMeResponse(user *sharepointhelper.Person) MeGetResponse {
+ // Use primary email (mail field) or fallback to userPrincipalName
+ email := user.Mail
+ if email == "" {
+ email = user.UserPrincipalName
+ }
+
+ // Create display name if empty
+ displayName := user.DisplayName
+ if displayName == "" {
+ displayName = user.GivenName + " " + user.Surname
+ }
+
+ // Extract domain from email for HD field
+ var hd string
+ if email != "" {
+ parts := strings.Split(email, "@")
+ if len(parts) > 1 {
+ hd = parts[1]
+ }
+ }
+
+ // Set the profile picture URL to use our backend API v2
+ pictureURL := fmt.Sprintf("/api/v2/people?photo=%s&v=%d", url.QueryEscape(email), time.Now().Unix())
+
+ return MeGetResponse{
+ ID: user.ID,
+ Email: email,
+ Name: displayName,
+ GivenName: user.GivenName,
+ FamilyName: user.Surname,
+ Picture: pictureURL,
+ Locale: "en",
+ HD: hd,
+ }
+}
+
+// handleGetUserProfile handles the GET request for user profile data.
+// It delegates to SharePoint or Google depending on which backend is configured.
+func handleGetUserProfile(srv server.Server, w http.ResponseWriter, r *http.Request, userEmail string) {
+ srv.Logger.Info("me handler called",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "user_email", userEmail,
+ )
+
+ if srv.SharePoint != nil {
+ handleGetUserProfileSharePoint(srv, w, r, userEmail)
+ } else {
+ handleGetUserProfileGoogle(srv, w, r, userEmail)
+ }
+}
+
+// handleGetUserProfileSharePoint handles the GET request for user profile data using Microsoft Graph.
+func handleGetUserProfileSharePoint(srv server.Server, w http.ResponseWriter, r *http.Request, userEmail string) {
+ // Get user directly from Microsoft Graph API
+ user, err := srv.SharePoint.GetPersonByEmail(userEmail)
+ if err != nil {
+ srv.Logger.Error("error getting user from Microsoft Graph",
+ "error", err,
+ "user_email", userEmail,
+ )
+
+ // Check if it's a user not found error (404)
+ if errors.Is(err, sharepointhelper.ErrUserNotFound) {
+ srv.Logger.Error("user not found in Microsoft Graph",
+ "user_email", userEmail,
+ )
+ http.Error(w, "User not found", http.StatusNotFound)
+ return
+ }
+
+ // For all other errors, return 500
+ http.Error(w, "Error getting user profile", http.StatusInternalServerError)
+ return
+ }
+
+ // Convert directly to response format
+ resp := convertGraphUserToMeResponse(user)
+
+ srv.Logger.Info("returning Microsoft Graph user data",
+ "user_id", resp.ID,
+ "user_name", resp.Name,
+ "user_email", resp.Email,
+ )
+
+ // Write response
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ enc := json.NewEncoder(w)
+ err = enc.Encode(resp)
+ if err != nil {
+ srv.Logger.Error("error encoding user response",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ http.Error(w, "Error getting user information", http.StatusInternalServerError)
+ return
+ }
}
func MeHandler(srv server.Server) http.Handler {
@@ -33,7 +262,7 @@ func MeHandler(srv server.Server) http.Handler {
http.Error(w, userErrMsg, httpCode)
}
- // Authorize request.
+ // Authorize request
userEmail := r.Context().Value("userEmail").(string)
if userEmail == "" {
errResp(
@@ -46,134 +275,12 @@ func MeHandler(srv server.Server) http.Handler {
}
switch r.Method {
- // The HEAD method is used to determine if the user is currently
- // authenticated.
case "HEAD":
w.WriteHeader(http.StatusOK)
return
case "GET":
- errResp := func(
- httpCode int, userErrMsg, logErrMsg string, err error,
- extraArgs ...interface{}) {
- srv.Logger.Error(logErrMsg,
- append([]interface{}{
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- }, extraArgs...)...,
- )
- http.Error(w, userErrMsg, httpCode)
- }
-
- ppl, err := srv.GWService.SearchPeople(
- userEmail, "emailAddresses,names,photos")
- if err != nil {
- errResp(
- http.StatusInternalServerError,
- "Error getting user information",
- "error searching people directory",
- err,
- )
- return
- }
-
- // Verify that the result only contains one person.
- if len(ppl) != 1 {
- errResp(
- http.StatusInternalServerError,
- "Error getting user information",
- fmt.Sprintf(
- "wrong number of people in search result: %d", len(ppl)),
- nil,
- "user_email", userEmail,
- )
-
- // If configured, send an email to the user to notify them that their
- // account was not found in the directory.
- if srv.Config.Email != nil && srv.Config.Email.Enabled &&
- srv.Config.GoogleWorkspace != nil &&
- srv.Config.GoogleWorkspace.UserNotFoundEmail != nil &&
- srv.Config.GoogleWorkspace.UserNotFoundEmail.Enabled &&
- srv.Config.GoogleWorkspace.UserNotFoundEmail.Body != "" &&
- srv.Config.GoogleWorkspace.UserNotFoundEmail.Subject != "" {
- _, err = srv.GWService.SendEmail(
- []string{userEmail},
- srv.Config.Email.FromAddress,
- srv.Config.GoogleWorkspace.UserNotFoundEmail.Subject,
- srv.Config.GoogleWorkspace.UserNotFoundEmail.Body,
- )
- if err != nil {
- srv.Logger.Error("error sending user not found email",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "user_email", userEmail,
- )
- } else {
- srv.Logger.Info("user not found email sent",
- "method", r.Method,
- "path", r.URL.Path,
- "user_email", userEmail,
- )
- }
- }
-
- return
- }
- p := ppl[0]
-
- // Make sure that the result's email address is the same as the
- // authenticated user, is the primary email address, and is verified.
- if len(p.EmailAddresses) == 0 ||
- p.EmailAddresses[0].Value != userEmail ||
- !p.EmailAddresses[0].Metadata.Primary ||
- !p.EmailAddresses[0].Metadata.Verified {
- errResp(
- http.StatusInternalServerError,
- "Error getting user information",
- "wrong user in search result",
- err,
- )
- return
- }
-
- // Verify other required values are set.
- if len(p.Names) == 0 {
- errResp(
- http.StatusInternalServerError,
- "Error getting user information",
- "no names in result",
- err,
- )
- return
- }
-
- // Write response.
- resp := MeGetResponse{
- ID: p.EmailAddresses[0].Metadata.Source.Id,
- Email: p.EmailAddresses[0].Value,
- VerifiedEmail: p.EmailAddresses[0].Metadata.Verified,
- Name: p.Names[0].DisplayName,
- GivenName: p.Names[0].GivenName,
- FamilyName: p.Names[0].FamilyName,
- }
- if len(p.Photos) > 0 {
- resp.Picture = p.Photos[0].Url
- }
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
- enc := json.NewEncoder(w)
- err = enc.Encode(resp)
- if err != nil {
- errResp(
- http.StatusInternalServerError,
- "Error getting user information",
- "error encoding response",
- err,
- )
- return
- }
+ handleGetUserProfile(srv, w, r, userEmail)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
diff --git a/internal/api/v2/me_recently_viewed_docs.go b/internal/api/v2/me_recently_viewed_docs.go
index ebae2aeff..58f740987 100644
--- a/internal/api/v2/me_recently_viewed_docs.go
+++ b/internal/api/v2/me_recently_viewed_docs.go
@@ -98,7 +98,7 @@ func MeRecentlyViewedDocsHandler(srv server.Server) http.Handler {
}
res = append(res, recentlyViewedDoc{
- ID: doc.GoogleFileID,
+ ID: doc.GetFileIdentifier(),
IsDraft: isDraft,
ViewedTime: d.ViewedAt.Unix(),
})
diff --git a/internal/api/v2/people.go b/internal/api/v2/people.go
index fac1f778d..10157026c 100644
--- a/internal/api/v2/people.go
+++ b/internal/api/v2/people.go
@@ -7,7 +7,6 @@ import (
"strings"
"github.com/hashicorp-forge/hermes/internal/server"
- "google.golang.org/api/people/v1"
)
// PeopleDataRequest contains the fields that are allowed to
@@ -16,52 +15,22 @@ type PeopleDataRequest struct {
Query string `json:"query,omitempty"`
}
-// PeopleDataHandler returns people related data from the Google API
+// PeopleDataHandler returns people related data from Microsoft Graph or Google
// to the Hermes frontend.
func PeopleDataHandler(srv server.Server) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- req := &PeopleDataRequest{}
switch r.Method {
- // Using POST method to avoid logging the query in browser history
- // and server logs
case "POST":
- if err := decodeRequest(r, &req); err != nil {
- srv.Logger.Error("error decoding people request", "error", err)
- http.Error(w, fmt.Sprintf("Bad request: %q", err),
- http.StatusBadRequest)
- return
- }
-
- users, err := srv.GWService.People.SearchDirectoryPeople().
- Query(req.Query).
- // Only query for photos and email addresses
- // This may be expanded based on use case
- // in the future
- ReadMask("emailAddresses,names,photos").
- Sources("DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE").
- Do()
- if err != nil {
- srv.Logger.Error("error searching people directory", "error", err)
- http.Error(w, fmt.Sprintf("Error searching people directory: %q", err),
- http.StatusInternalServerError)
- return
- }
-
- // Write response.
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
-
- enc := json.NewEncoder(w)
- err = enc.Encode(users.People)
- if err != nil {
- srv.Logger.Error("error encoding people response", "error", err)
- http.Error(w, "Error searching people directory",
- http.StatusInternalServerError)
- return
- }
+ // Using POST method to avoid logging the query in browser history
+ // and server logs
+ handleSearchPeople(srv, w, r)
case "GET":
query := r.URL.Query()
- if len(query["emails"]) != 1 {
+ // Handle photo request (SharePoint only - Google uses direct photo URLs)
+ if query.Get("photo") != "" && srv.SharePoint != nil {
+ userEmail := query.Get("photo")
+ handleGetPhoto(srv, w, userEmail)
+ } else if len(query["emails"]) != 1 {
srv.Logger.Error(
"attempted to get users without providing any email addresses")
http.Error(w,
@@ -69,38 +38,178 @@ func PeopleDataHandler(srv server.Server) http.Handler {
http.StatusBadRequest)
} else {
emails := strings.Split(query["emails"][0], ",")
- var people []*people.Person
-
- for _, email := range emails {
- result, err := srv.GWService.People.SearchDirectoryPeople().
- Query(email).
- ReadMask("emailAddresses,names,photos").
- Sources("DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE").
- Do()
-
- if err == nil && len(result.People) > 0 {
- people = append(people, result.People[0])
- } else {
- srv.Logger.Warn("Email lookup miss", "error", err)
- }
- }
-
- // Write response.
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
-
- enc := json.NewEncoder(w)
- err := enc.Encode(people)
- if err != nil {
- srv.Logger.Error("error encoding people response", "error", err)
- http.Error(w, "Error getting people responses",
- http.StatusInternalServerError)
- return
- }
+ handleGetPeopleByEmails(srv, w, emails)
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
- return
}
})
}
+
+// handleSearchPeople handles POST requests for people search.
+func handleSearchPeople(srv server.Server, w http.ResponseWriter, r *http.Request) {
+ req := &PeopleDataRequest{}
+ if err := decodeRequest(r, &req); err != nil {
+ srv.Logger.Error("error decoding people request", "error", err)
+ http.Error(w, fmt.Sprintf("Bad request: %q", err),
+ http.StatusBadRequest)
+ return
+ }
+
+ if srv.SharePoint != nil {
+ // SharePoint path: use Microsoft Graph API
+ people, err := srv.SharePoint.SearchPeople(req.Query, 10)
+ if err != nil {
+ srv.Logger.Error("error searching people directory", "error", err)
+ http.Error(w, fmt.Sprintf("Error searching people directory: %q", err),
+ http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ enc := json.NewEncoder(w)
+ err = enc.Encode(people)
+ if err != nil {
+ srv.Logger.Error("error encoding people response", "error", err)
+ http.Error(w, "Error searching people directory",
+ http.StatusInternalServerError)
+ return
+ }
+ } else {
+ // Google path: use Google People API
+ users, err := srv.GWService.SearchPeople(
+ req.Query, "emailAddresses,names,photos")
+ if err != nil {
+ srv.Logger.Error("error searching people directory", "error", err)
+ http.Error(w, fmt.Sprintf("Error searching people directory: %q", err),
+ http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ enc := json.NewEncoder(w)
+ err = enc.Encode(users)
+ if err != nil {
+ srv.Logger.Error("error encoding people response", "error", err)
+ http.Error(w, "Error searching people directory",
+ http.StatusInternalServerError)
+ return
+ }
+ }
+}
+
+// handleGetPhoto handles GET requests for profile photos (SharePoint only).
+func handleGetPhoto(srv server.Server, w http.ResponseWriter, userEmail string) {
+ srv.Logger.Info("Handling profile photo request", "userIdentifier", userEmail)
+
+ photoBytes, err := srv.SharePoint.GetProfilePhoto(userEmail)
+ if err != nil {
+ srv.Logger.Error("Error getting profile photo", "error", err)
+ placeholderSVG := getPlaceholderImageSVG(userEmail)
+ w.Header().Set("Content-Type", "image/svg+xml")
+ w.Header().Set("Cache-Control", "public, max-age=86400")
+ w.Write([]byte(placeholderSVG))
+ return
+ }
+
+ if len(photoBytes) == 0 {
+ placeholderSVG := getPlaceholderImageSVG(userEmail)
+ w.Header().Set("Content-Type", "image/svg+xml")
+ w.Header().Set("Cache-Control", "public, max-age=86400")
+ w.Write([]byte(placeholderSVG))
+ return
+ }
+
+ contentType := http.DetectContentType(photoBytes)
+ w.Header().Set("Content-Type", contentType)
+ w.Header().Set("Cache-Control", "public, max-age=86400")
+ w.WriteHeader(http.StatusOK)
+ w.Write(photoBytes)
+}
+
+// handleGetPeopleByEmails handles GET requests for people by email addresses.
+func handleGetPeopleByEmails(srv server.Server, w http.ResponseWriter, emails []string) {
+ if srv.SharePoint != nil {
+ // SharePoint path: use Microsoft Graph API
+ people, err := srv.SharePoint.GetPeopleByEmails(emails)
+ if err != nil {
+ srv.Logger.Error("error getting people by emails", "error", err)
+ http.Error(w, "Error getting people responses",
+ http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ enc := json.NewEncoder(w)
+ encodeErr := enc.Encode(people)
+ if encodeErr != nil {
+ srv.Logger.Error("error encoding people response", "error", encodeErr)
+ http.Error(w, "Error getting people responses",
+ http.StatusInternalServerError)
+ return
+ }
+ } else {
+ // Google path: use Google People API
+ var people []interface{}
+ for _, email := range emails {
+ result, err := srv.GWService.SearchPeople(
+ email, "emailAddresses,names,photos")
+ if err == nil && len(result) > 0 {
+ people = append(people, result[0])
+ } else {
+ srv.Logger.Warn("Email lookup miss", "error", err)
+ }
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ enc := json.NewEncoder(w)
+ err := enc.Encode(people)
+ if err != nil {
+ srv.Logger.Error("error encoding people response", "error", err)
+ http.Error(w, "Error getting people responses",
+ http.StatusInternalServerError)
+ return
+ }
+ }
+}
+
+// getPlaceholderImageSVG returns a personalized SVG placeholder image with user's initial
+func getPlaceholderImageSVG(userEmail string) string {
+ // Get the first letter of the username part (before @)
+ initial := "?"
+ if len(userEmail) > 0 {
+ // Split email by @ and use the username part
+ parts := strings.Split(userEmail, "@")
+ if len(parts) > 0 && len(parts[0]) > 0 {
+ initial = strings.ToUpper(string(parts[0][0]))
+ }
+ }
+
+ // Generate a consistent color based on the email hash
+ colors := []string{
+ "3b82f6", // blue-500
+ "10b981", // emerald-500
+ "f59e0b", // amber-500
+ "ef4444", // red-500
+ "8b5cf6", // violet-500
+ "06b6d4", // cyan-500
+ "84cc16", // lime-500
+ "f97316", // orange-500
+ }
+
+ // Simple hash function to get consistent color for same email
+ colorIndex := 0
+ for _, char := range userEmail {
+ colorIndex += int(char)
+ }
+ colorIndex = colorIndex % len(colors)
+
+ return fmt.Sprintf(`
+
+ %s
+ `, colors[colorIndex], initial)
+}
diff --git a/internal/api/v2/projects.go b/internal/api/v2/projects.go
index a0be889c3..1702af419 100644
--- a/internal/api/v2/projects.go
+++ b/internal/api/v2/projects.go
@@ -95,6 +95,11 @@ func ProjectsHandler(srv server.Server) http.Handler {
if pageParam != "" {
p, err := strconv.Atoi(pageParam)
if err != nil {
+ srv.Logger.Warn("invalid page parameter",
+ append([]interface{}{
+ "error", err,
+ "page_param", pageParam,
+ }, logArgs...)...)
http.Error(w, "Invalid page parameter", http.StatusBadRequest)
return
}
@@ -105,6 +110,11 @@ func ProjectsHandler(srv server.Server) http.Handler {
if hitsPerPageParam != "" {
hpp, err := strconv.Atoi(hitsPerPageParam)
if err != nil {
+ srv.Logger.Warn("invalid hitsPerPage parameter",
+ append([]interface{}{
+ "error", err,
+ "hits_per_page_param", hitsPerPageParam,
+ }, logArgs...)...)
http.Error(w, "Invalid hitsPerPage parameter", http.StatusBadRequest)
return
}
@@ -121,6 +131,10 @@ func ProjectsHandler(srv server.Server) http.Handler {
Status: statusFilter,
}
} else {
+ srv.Logger.Warn("invalid status parameter",
+ append([]interface{}{
+ "status_param", statusParam,
+ }, logArgs...)...)
http.Error(w, "Invalid status", http.StatusUnprocessableEntity)
return
}
@@ -230,6 +244,7 @@ func ProjectsHandler(srv server.Server) http.Handler {
// Validate request.
if req.Title == "" {
+ srv.Logger.Warn("project title is required", logArgs...)
http.Error(w, "Bad request: title is required", http.StatusBadRequest)
return
}
@@ -453,6 +468,10 @@ func ProjectHandler(srv server.Server) http.Handler {
case "archived":
case "completed":
default:
+ srv.Logger.Warn("invalid project status in patch request",
+ append([]interface{}{
+ "status", *req.Status,
+ }, logArgs...)...)
http.Error(w,
"Bad request: invalid status"+
` (valid values are "active", "archived", "completed")`,
@@ -461,6 +480,7 @@ func ProjectHandler(srv server.Server) http.Handler {
}
}
if req.Title != nil && *req.Title == "" {
+ srv.Logger.Warn("project title cannot be empty", logArgs...)
http.Error(
w, "Bad request: title cannot be empty", http.StatusBadRequest)
return
diff --git a/internal/api/v2/projects_related_resources.go b/internal/api/v2/projects_related_resources.go
index d4b666340..b46605d30 100644
--- a/internal/api/v2/projects_related_resources.go
+++ b/internal/api/v2/projects_related_resources.go
@@ -23,7 +23,7 @@ type ProjectRelatedResourcesGetResponseExternalLink struct {
}
type ProjectRelatedResourcesGetResponseHermesDocument struct {
- GoogleFileID string `json:"googleFileID"`
+ FileID string `json:"FileID"`
Title string `json:"title"`
CreatedTime int64 `json:"createdTime"`
DocumentType string `json:"documentType"`
@@ -48,8 +48,8 @@ type ProjectRelatedResourcesPutRequestExternalLink struct {
}
type ProjectRelatedResourcesPutRequestHermesDocument struct {
- GoogleFileID string `json:"googleFileID"`
- SortOrder int `json:"sortOrder"`
+ FileID string `json:"FileID"`
+ SortOrder int `json:"sortOrder"`
}
func projectsResourceRelatedResourcesHandler(
@@ -109,7 +109,7 @@ func projectsResourceRelatedResourcesHandler(
}
// Add Hermes document related resources.
for _, hdrr := range hdrrs {
- logArgs = append(logArgs, "document_id", hdrr.Document.GoogleFileID)
+ logArgs = append(logArgs, "document_id", hdrr.Document.GetFileIdentifier())
// Convert database model to a document. We don't need document review
// data for this endpoint.
doc, err := document.NewFromDatabaseModel(
@@ -127,7 +127,7 @@ func projectsResourceRelatedResourcesHandler(
resp.HermesDocuments = append(
resp.HermesDocuments,
ProjectRelatedResourcesGetResponseHermesDocument{
- GoogleFileID: doc.ObjectID,
+ FileID: doc.ObjectID,
Title: doc.Title,
CreatedTime: doc.CreatedTime,
DocumentType: doc.DocType,
@@ -212,9 +212,7 @@ func projectsResourceRelatedResourcesHandler(
ProjectID: projectID,
SortOrder: hdrr.SortOrder,
},
- Document: models.Document{
- GoogleFileID: hdrr.GoogleFileID,
- },
+ Document: srv.NewDocumentByFileID(hdrr.FileID),
})
}
diff --git a/internal/api/v2/reviews.go b/internal/api/v2/reviews.go
index aac28f7f7..c78612ce0 100644
--- a/internal/api/v2/reviews.go
+++ b/internal/api/v2/reviews.go
@@ -10,806 +10,763 @@ import (
"github.com/hashicorp-forge/hermes/internal/config"
"github.com/hashicorp-forge/hermes/internal/email"
+ "github.com/hashicorp-forge/hermes/internal/helpers"
"github.com/hashicorp-forge/hermes/internal/server"
+ "github.com/hashicorp-forge/hermes/internal/structs"
"github.com/hashicorp-forge/hermes/pkg/document"
gw "github.com/hashicorp-forge/hermes/pkg/googleworkspace"
hcd "github.com/hashicorp-forge/hermes/pkg/hashicorpdocs"
"github.com/hashicorp-forge/hermes/pkg/links"
"github.com/hashicorp-forge/hermes/pkg/models"
+ "github.com/hashicorp-forge/hermes/pkg/sharepointhelper"
"github.com/hashicorp/go-multierror"
"google.golang.org/api/drive/v3"
+ "gorm.io/gorm"
+)
+
+var (
+ getProductWithSubscribers = func(db *gorm.DB, productName string) (*models.Product, error) {
+ p := &models.Product{Name: productName}
+ if err := p.Get(db); err != nil {
+ return nil, err
+ }
+ return p, nil
+ }
)
func ReviewsHandler(srv server.Server) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "POST":
- // revertFuncs is a slice of functions to execute in the event of an error
- // that requires reverting previous logic.
- var revertFuncs []func() error
+ handleCreateReview(&srv, w, r)
// Validate request.
- docID, err := parseResourceIDFromURL(r.URL.Path, "reviews")
- if err != nil {
- srv.Logger.Error("error parsing document ID from reviews path",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- )
- http.Error(w, "Document ID not found", http.StatusNotFound)
- return
- }
-
- // Check if document is locked.
- locked, err := hcd.IsLocked(docID, srv.DB, srv.GWService, srv.Logger)
- if err != nil {
- srv.Logger.Error("error checking document locked status",
- "error", err,
- "path", r.URL.Path,
- "method", r.Method,
- "doc_id", docID,
- )
- http.Error(w, "Error getting document status", http.StatusNotFound)
- return
- }
- // Don't continue if document is locked.
- if locked {
- http.Error(w, "Document is locked", http.StatusLocked)
- return
- }
-
- // Begin database transaction.
- tx := srv.DB.Begin()
- revertFuncs = append(revertFuncs, func() error {
- // Rollback database transaction.
- if err = tx.Rollback().Error; err != nil {
- return fmt.Errorf("error rolling back database transaction: %w", err)
- }
+ default:
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+ })
+}
- return nil
- })
+// handleCreateReview handles the POST request for creating a document review
+func handleCreateReview(srv *server.Server, w http.ResponseWriter, r *http.Request) {
+ // revertFuncs is a slice of functions to execute in the event of an error
+ // that requires reverting previous logic.
+ var revertFuncs []func() error
- // Get document from database.
- model := models.Document{
- GoogleFileID: docID,
- }
- if err := model.Get(tx); err != nil {
- srv.Logger.Error("error getting document from database",
- "error", err,
- "path", r.URL.Path,
- "method", r.Method,
- "doc_id", docID,
- )
- http.Error(w, "Error accessing document",
- http.StatusInternalServerError)
- return
- }
+ // Validate request.
+ docID, err := parseResourceIDFromURL(r.URL.Path, "reviews")
+ if err != nil {
+ srv.Logger.Error("error parsing document ID from reviews path",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ http.Error(w, "Document ID not found", http.StatusNotFound)
+ return
+ }
- // Get reviews for the document.
- var reviews models.DocumentReviews
- if err := reviews.Find(tx, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
- }); err != nil {
- srv.Logger.Error("error getting reviews for document",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- return
- }
+ // Check if document is locked (Google-only).
+ if !srv.IsSharePoint() {
+ locked, err := hcd.IsLocked(docID, srv.DB, srv.GWService, srv.Logger)
+ if err != nil {
+ srv.Logger.Error("error checking document locked status",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ http.Error(w, "Error getting document status", http.StatusNotFound)
+ return
+ }
+ // Don't continue if document is locked.
+ if locked {
+ http.Error(w, "Document is locked", http.StatusLocked)
+ return
+ }
+ }
- // Get group reviews for the document.
- var groupReviews models.DocumentGroupReviews
- if err := groupReviews.Find(srv.DB, models.DocumentGroupReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
- }); err != nil {
- srv.Logger.Error("error getting group reviews for document",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- return
- }
+ // Begin database transaction.
+ tx := srv.DB.Begin()
+ revertFuncs = append(revertFuncs, func() error {
+ // Rollback database transaction.
+ if err = tx.Rollback().Error; err != nil {
+ return fmt.Errorf("error rolling back database transaction: %w", err)
+ }
- // Convert database model to a document.
- doc, err := document.NewFromDatabaseModel(
- model, reviews, groupReviews)
- if err != nil {
- srv.Logger.Error("error converting database model to document type",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- http.Error(w, "Error accessing document",
- http.StatusInternalServerError)
- return
- }
+ return nil
+ })
- // Validate document status.
- if doc.Status != "WIP" {
- srv.Logger.Warn("document is not in WIP status",
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- )
- http.Error(w,
- "Cannot create review for a document that is not in WIP status",
- http.StatusUnprocessableEntity)
- return
- }
+ // Validate and prepare the review request
+ doc, httpErr := validateAndPrepareReview(srv, r, tx, docID)
+ if httpErr != nil {
+ http.Error(w, httpErr.Message, httpErr.StatusCode)
+ if err := revertReviewsPost(revertFuncs); err != nil {
+ srv.Logger.Error("error reverting review creation",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path)
+ }
+ return
+ }
- // Get latest product number.
- latestNum, err := models.GetLatestProductNumber(
- tx, doc.DocType, doc.Product)
- if err != nil {
- srv.Logger.Error("error getting product document number",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- )
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
- return
- }
+ // Process the document for review
+ creationTime, modifiedTime, nextDocNum, httpErr := processDocumentForReview(srv, r, tx, doc, docID, &revertFuncs)
+ if httpErr != nil {
+ http.Error(w, httpErr.Message, httpErr.StatusCode)
+ if err := revertReviewsPost(revertFuncs); err != nil {
+ srv.Logger.Error("error reverting review creation",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path)
+ }
+ return
+ }
- // Get product from database so we can get the product abbreviation.
- product := models.Product{
- Name: doc.Product,
- }
- if err := product.Get(tx); err != nil {
- srv.Logger.Error("error getting product",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- )
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
- return
- }
+ // Complete the review creation process
+ if httpErr := completeReviewCreation(srv, r, tx, doc, docID, creationTime, modifiedTime, nextDocNum, &revertFuncs); httpErr != nil {
+ http.Error(w, httpErr.Message, httpErr.StatusCode)
+ if err := revertReviewsPost(revertFuncs); err != nil {
+ srv.Logger.Error("error reverting review creation",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path)
+ }
+ return
+ }
- // Reset the document creation time to the current time of publish.
- now := time.Now()
- doc.Created = now.Format("Jan 2, 2006")
- doc.CreatedTime = now.Unix()
-
- // Set the document number.
- nextDocNum := latestNum + 1
- doc.DocNumber = fmt.Sprintf("%s-%03d",
- product.Abbreviation,
- nextDocNum)
-
- // Change document status to "In-Review".
- doc.Status = "In-Review"
-
- // Replace the doc header.
- err = doc.ReplaceHeader(srv.Config.BaseURL, false, srv.GWService)
- revertFuncs = append(revertFuncs, func() error {
- // Change back document number to "ABC-???" and status to "WIP".
- doc.DocNumber = fmt.Sprintf("%s-???", product.Abbreviation)
- doc.Status = "WIP"
-
- if err = doc.ReplaceHeader(
- srv.Config.BaseURL, false, srv.GWService,
- ); err != nil {
- return fmt.Errorf("error replacing doc header: %w", err)
- }
+ // Get document URL.
+ docURL, err := getDocumentURL(srv.Config.BaseURL, docID)
+ if err != nil {
+ srv.Logger.Error("error getting document URL",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "docURL", docURL,
+ )
+ http.Error(w, "Error creating review",
+ http.StatusInternalServerError)
+ if err := revertReviewsPost(revertFuncs); err != nil {
+ srv.Logger.Error("error reverting review creation",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path)
+ }
+ return
+ }
- return nil
- })
- if err != nil {
- srv.Logger.Error("error replacing doc header",
- "error", err, "doc_id", docID)
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
+ if srv.Config.Email != nil && srv.Config.Email.Enabled {
+ approverEmailAddresses := []string{}
+ approverEmailAddresses = append(approverEmailAddresses, doc.Approvers...)
+ approverEmailAddresses = append(approverEmailAddresses, doc.ApproverGroups...)
+
+ if len(approverEmailAddresses) > 0 {
+ go helpers.SendEmailWithRetry(
+ srv,
+ func() error {
+ return email.SendReviewRequestedEmail(
+ email.ReviewRequestedEmailData{
+ BaseURL: srv.Config.BaseURL,
+ DocumentOwner: doc.Owners[0],
+ DocumentShortName: doc.DocNumber,
+ DocumentType: doc.DocType,
+ DocumentTitle: doc.Title,
+ DocumentStatus: doc.Status,
+ DocumentURL: docURL,
+ Product: doc.Product,
+ },
+ approverEmailAddresses,
+ srv.Config.Email.FromAddress,
+ srv.GetEmailSender(),
+ )
+ },
+ docID,
+ "review_requested",
+ r,
+ )
- if err := revertReviewsPost(revertFuncs); err != nil {
- srv.Logger.Error("error reverting review creation",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
- }
- return
- }
- srv.Logger.Info("doc header replaced",
+ srv.Logger.Info("review requested email queued",
"doc_id", docID,
+ "approver_count", len(approverEmailAddresses),
"method", r.Method,
"path", r.URL.Path,
)
+ }
+ }
+ // Commit the database transaction.
+ if err := tx.Commit().Error; err != nil {
+ srv.Logger.Error("error committing database transaction",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ http.Error(w, "Error creating review",
+ http.StatusInternalServerError)
+
+ if err := revertReviewsPost(revertFuncs); err != nil {
+ srv.Logger.Error("error reverting review creation",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path)
+ }
+ return
+ }
- // Get file from Google Drive so we can get the latest modified time.
- file, err := srv.GWService.GetFile(docID)
- if err != nil {
- srv.Logger.Error("error getting document file from Google",
+ // Write response.
+ w.WriteHeader(http.StatusOK)
+
+ // Log success.
+ srv.Logger.Info("review created",
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+
+ userEmail := r.Context().Value("userEmail").(string)
+ srv.Logger.Info("ACCESS",
+ "user_email", userEmail,
+ "doc_id", docID,
+ "operation", "document_published",
+ "updated_attributes", "[status, approvedBy, docNumber]",
+ "mode", "published",
+ )
+
+ // Request post-processing.
+ go handleReviewPostProcessing(srv, doc, docID, r)
+}
+
+// validateAndPrepareReview validates the request and prepares the document for review
+func validateAndPrepareReview(srv *server.Server, r *http.Request, tx *gorm.DB, docID string) (*document.Document, *structs.HTTPError) {
+ // Get document from database.
+ model := srv.NewDocumentByFileID(docID)
+ if err := model.Get(tx); err != nil {
+ srv.Logger.Error("error getting document from database",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error accessing document", err)
+ return nil, &httpErr
+ }
+
+ // Get reviews for the document.
+ var reviews models.DocumentReviews
+ if err := reviews.Find(tx, models.DocumentReview{
+ Document: srv.NewDocumentByFileID(docID),
+ }); err != nil {
+ srv.Logger.Error("error getting reviews for document",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ )
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error accessing document reviews", err)
+ return nil, &httpErr
+ }
+
+ // Get group reviews for the document.
+ var groupReviews models.DocumentGroupReviews
+ if err := groupReviews.Find(srv.DB, models.DocumentGroupReview{
+ Document: srv.NewDocumentByFileID(docID),
+ }); err != nil {
+ srv.Logger.Error("error getting group reviews for document",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ )
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error accessing document group reviews", err)
+ return nil, &httpErr
+ }
+
+ // Convert database model to a document.
+ doc, err := document.NewFromDatabaseModel(
+ model, reviews, groupReviews)
+ if err != nil {
+ srv.Logger.Error("error converting database model to document type",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ )
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error accessing document", err)
+ return nil, &httpErr
+ }
+
+ // Validate document status.
+ if doc.Status != "WIP" {
+ srv.Logger.Warn("document is not in WIP status",
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ httpErr := structs.NewHTTPError(http.StatusUnprocessableEntity, "Cannot create review for a document that is not in WIP status", nil)
+ return nil, &httpErr
+ }
+
+ return doc, nil
+}
+
+// processDocumentForReview handles the core document processing for review creation
+func processDocumentForReview(srv *server.Server, r *http.Request, tx *gorm.DB, doc *document.Document, docID string, revertFuncs *[]func() error) (time.Time, time.Time, int, *structs.HTTPError) {
+ // Get latest product number.
+ latestNum, err := models.GetLatestProductNumber(
+ tx, doc.DocType, doc.Product)
+ if err != nil {
+ srv.Logger.Error("error getting product document number",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return time.Time{}, time.Time{}, 0, &httpErr
+ }
+
+ // Get product from database so we can get the product abbreviation.
+ product := models.Product{
+ Name: doc.Product,
+ }
+ if err := product.Get(tx); err != nil {
+ srv.Logger.Error("error getting product",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return time.Time{}, time.Time{}, 0, &httpErr
+ }
+
+ // Reset the document creation time to the current time of publish.
+ now := time.Now()
+ doc.Created = now.Format("Jan 2, 2006")
+ doc.CreatedTime = now.Unix()
+
+ // Set the document number.
+ nextDocNum := latestNum + 1
+ doc.DocNumber = fmt.Sprintf("%s-%03d",
+ product.Abbreviation,
+ nextDocNum)
+ doc.Status = "In-Review"
+
+ // Replace the doc header (Google-only; SharePoint headers
+ // are managed by the Hermes Add-In for Word).
+ if !srv.IsSharePoint() {
+ err = doc.ReplaceHeader(srv.Config.BaseURL, false, srv.GWService)
+ *revertFuncs = append(*revertFuncs, func() error {
+ // Change back document number to "ABC-???" and status to "WIP".
+ doc.DocNumber = fmt.Sprintf("%s-???", product.Abbreviation)
+ doc.Status = "WIP"
+
+ if err = doc.ReplaceHeader(
+ srv.Config.BaseURL, false, srv.GWService,
+ ); err != nil {
+ return fmt.Errorf("error replacing doc header: %w", err)
+ }
+
+ return nil
+ })
+ if err != nil {
+ srv.Logger.Error("error replacing doc header",
+ "error", err, "doc_id", docID)
+ httpErr := structs.NewHTTPError(
+ http.StatusInternalServerError, "Error creating review", err)
+ if err := revertReviewsPost(*revertFuncs); err != nil {
+ srv.Logger.Error("error reverting review creation",
"error", err,
- "path", r.URL.Path,
- "method", r.Method,
"doc_id", docID,
- )
- http.Error(w, "Error creating review", http.StatusInternalServerError)
- if err := revertReviewsPost(revertFuncs); err != nil {
- srv.Logger.Error("error reverting review creation",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
- }
- return
+ "method", r.Method,
+ "path", r.URL.Path)
}
+ return time.Time{}, time.Time{}, 0, &httpErr
+ }
+ srv.Logger.Info("doc header replaced",
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ }
- // Parse and set modified time.
- modifiedTime, err := time.Parse(time.RFC3339Nano, file.ModifiedTime)
+ // Grant read access to configured groups asynchronously
+ go func() {
+ if srv.SharePoint != nil {
+ grantedGroups, err := srv.SharePoint.GrantGroupsReadAccess(docID, "reader", publishReaderGroups, publishGroupDisplayNames)
if err != nil {
- srv.Logger.Error("error parsing modified time",
+ srv.Logger.Error("error granting reader access to publish groups",
"error", err,
- "path", r.URL.Path,
- "method", r.Method,
"doc_id", docID,
)
- http.Error(w, "Error creating review", http.StatusInternalServerError)
- if err := revertReviewsPost(revertFuncs); err != nil {
- srv.Logger.Error("error reverting review creation",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
- }
return
}
- doc.ModifiedTime = modifiedTime.Unix()
-
- // Get latest Google Drive file revision.
- latestRev, err := srv.GWService.GetLatestRevision(docID)
- if err != nil {
- srv.Logger.Error("error getting latest revision",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID)
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
-
- if err := revertReviewsPost(revertFuncs); err != nil {
- srv.Logger.Error("error reverting review creation",
+ if len(grantedGroups) > 0 {
+ srv.Logger.Info("granted group access on document publish",
+ "doc_id", docID,
+ "groups", grantedGroups,
+ )
+ }
+ } else {
+ var grantedGroups []string
+ for _, group := range publishReaderGroups {
+ if err := srv.GWService.ShareFile(docID, group, "reader"); err != nil {
+ srv.Logger.Error("error granting reader access to publish group",
"error", err,
"doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
+ "group", group,
+ )
+ return
}
- return
+ grantedGroups = append(grantedGroups, group)
}
-
- // Mark latest revision to be kept forever.
- _, err = srv.GWService.KeepRevisionForever(docID, latestRev.Id)
- revertFuncs = append(revertFuncs, func() error {
- // Mark latest revision to not be kept forever.
- if err = srv.GWService.UpdateKeepRevisionForever(
- docID, latestRev.Id, false,
- ); err != nil {
- return fmt.Errorf(
- "error marking revision to not be kept forever: %w", err)
- }
-
- return nil
- })
- if err != nil {
- srv.Logger.Error("error marking revision to keep forever",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
+ if len(grantedGroups) > 0 {
+ srv.Logger.Info("granted group access on document publish",
"doc_id", docID,
- "rev_id", latestRev.Id)
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
-
- if err := revertReviewsPost(revertFuncs); err != nil {
- srv.Logger.Error("error reverting review creation",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
- }
- return
+ "groups", grantedGroups,
+ )
}
- srv.Logger.Info("doc revision set to be kept forever",
+ }
+ }()
+
+ // Get file and parse modified time.
+ var modifiedTime time.Time
+ if srv.SharePoint != nil {
+ file, err := srv.SharePoint.GetFile(docID)
+ if err != nil {
+ srv.Logger.Error("error getting document file",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
"doc_id", docID,
+ )
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return time.Time{}, time.Time{}, 0, &httpErr
+ }
+ modifiedTime, err = time.Parse(time.RFC3339Nano, file.LastModifiedTime)
+ if err != nil {
+ srv.Logger.Error("error parsing modified time",
+ "error", err,
+ "path", r.URL.Path,
"method", r.Method,
+ "doc_id", docID,
+ )
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return time.Time{}, time.Time{}, 0, &httpErr
+ }
+ } else {
+ file, err := srv.GWService.GetFile(docID)
+ if err != nil {
+ srv.Logger.Error("error getting document file",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return time.Time{}, time.Time{}, 0, &httpErr
+ }
+ modifiedTime, err = time.Parse(time.RFC3339, file.ModifiedTime)
+ if err != nil {
+ srv.Logger.Error("error parsing modified time",
+ "error", err,
"path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
)
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return time.Time{}, time.Time{}, 0, &httpErr
+ }
+ }
+ doc.ModifiedTime = modifiedTime.Unix()
- // Record file revision in the Algolia document object.
- revisionName := "Requested review"
- doc.SetFileRevision(latestRev.Id, revisionName)
+ // Get latest file revision.
+ var latestRevisionID string
+ if srv.SharePoint != nil {
+ latestVersion, err := srv.SharePoint.GetLatestVersion(docID)
+ if err != nil {
+ srv.Logger.Error("error getting latest revision",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID)
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return time.Time{}, time.Time{}, 0, &httpErr
+ }
+ latestRevisionID = latestVersion.ID
+ } else {
+ latestRev, err := srv.GWService.GetLatestRevision(docID)
+ if err != nil {
+ srv.Logger.Error("error getting latest revision",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID)
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return time.Time{}, time.Time{}, 0, &httpErr
+ }
+ latestRevisionID = latestRev.Id
- // Create file revision in the database.
- fr := models.DocumentFileRevision{
- Document: models.Document{
- GoogleFileID: docID,
- },
- GoogleDriveFileRevisionID: latestRev.Id,
- Name: revisionName,
- }
- if err := fr.Create(tx); err != nil {
- srv.Logger.Error("error creating document file revision",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- "rev_id", latestRev.Id)
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
+ // Keep revision forever for Google Drive.
+ if _, err := srv.GWService.KeepRevisionForever(docID, latestRev.Id); err != nil {
+ srv.Logger.Error("error keeping revision forever",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID)
+ // Non-fatal: continue even if keep-forever fails.
+ }
+ }
- if err := revertReviewsPost(revertFuncs); err != nil {
- srv.Logger.Error("error reverting review creation",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
- }
- return
- }
+ // Record file revision in the Algolia document object.
+ revisionName := "Requested review"
+ doc.SetFileRevision(latestRevisionID, revisionName)
- // Move document to published docs location in Google Drive.
- _, err = srv.GWService.MoveFile(
- docID, srv.Config.GoogleWorkspace.DocsFolder)
- revertFuncs = append(revertFuncs, func() error {
- // Move document back to drafts folder in Google Drive.
- if _, err := srv.GWService.MoveFile(
- doc.ObjectID, srv.Config.GoogleWorkspace.DraftsFolder); err != nil {
+ // Create file revision in the database.
+ fr := models.DocumentFileRevision{
+ Document: srv.NewDocumentByFileID(docID),
+ FileRevisionID: latestRevisionID,
+ Name: revisionName,
+ }
+ if err := fr.Create(tx); err != nil {
+ srv.Logger.Error("error creating document file revision",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ "rev_id", latestRevisionID)
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return time.Time{}, time.Time{}, 0, &httpErr
+ }
- return fmt.Errorf("error moving doc back to drafts folder: %w", err)
+ return now, modifiedTime, nextDocNum, nil
+}
- }
+// completeReviewCreation handles the final database updates and reviewer setup
+func completeReviewCreation(srv *server.Server, r *http.Request, tx *gorm.DB, doc *document.Document, docID string, creationTime time.Time, modifiedTime time.Time, nextDocNum int, revertFuncs *[]func() error) *structs.HTTPError {
+ // Verify the document file exists.
+ if srv.SharePoint != nil {
+ _, err := srv.SharePoint.GetFileDetails(docID)
+ if err != nil {
+ srv.Logger.Error("error getting file details",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path)
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return &httpErr
+ }
+ } else {
+ _, err := srv.GWService.GetFile(docID)
+ if err != nil {
+ srv.Logger.Error("error getting file details",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path)
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return &httpErr
+ }
+ }
- return nil
- })
- if err != nil {
- srv.Logger.Error("error moving file to docs folder",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
+ // Move document to published docs location in Google Drive (Google-only).
+ if !srv.IsSharePoint() {
+ _, err := srv.GWService.MoveFile(
+ docID, srv.Config.GoogleWorkspace.DocsFolder)
+ *revertFuncs = append(*revertFuncs, func() error {
+ // Move document back to drafts folder in Google Drive.
+ if _, err := srv.GWService.MoveFile(
+ doc.ObjectID, srv.Config.GoogleWorkspace.DraftsFolder); err != nil {
+
+ return fmt.Errorf("error moving doc back to drafts folder: %w", err)
- if err := revertReviewsPost(revertFuncs); err != nil {
- srv.Logger.Error("error reverting review creation",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
- }
- return
}
- srv.Logger.Info("doc moved to published document folder",
+
+ return nil
+ })
+ if err != nil {
+ srv.Logger.Error("error moving file to docs folder",
+ "error", err,
"doc_id", docID,
"method", r.Method,
- "path", r.URL.Path,
- )
+ "path", r.URL.Path)
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
- // Create shortcut in hierarchical folder structure.
- _, err = createShortcut(srv.Config, *doc, srv.GWService)
- if err != nil {
- srv.Logger.Error("error creating shortcut",
+ if err := revertReviewsPost(*revertFuncs); err != nil {
+ srv.Logger.Error("error reverting review creation",
"error", err,
"doc_id", docID,
"method", r.Method,
"path", r.URL.Path)
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
-
- if err := revertReviewsPost(revertFuncs); err != nil {
- srv.Logger.Error("error reverting review creation",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
- }
- return
}
- srv.Logger.Info("doc shortcut created",
+ return &httpErr
+ }
+ srv.Logger.Info("doc moved to published document folder",
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ }
+
+ // Create shortcut in hierarchical folder structure.
+ if srv.SharePoint != nil {
+ // TODO: Implement SharePoint shortcut creation.
+ // SharePoint shortcut creation is disabled pending review of
+ // implementation and permission behavior.
+ // shortcutFileId, err := createSharePointShortcut(srv.Config, doc, fileDetails.WebURL, srv.SharePoint)
+ } else {
+ _, err := createGoogleShortcut(srv.Config, *doc, srv.GWService)
+ if err != nil {
+ srv.Logger.Error("error creating shortcut",
+ "error", err,
"doc_id", docID,
"method", r.Method,
- "path", r.URL.Path,
- )
+ "path", r.URL.Path)
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
- // Create go-link.
- // TODO: use database for this instead of Algolia.
- err = links.SaveDocumentRedirectDetails(
- srv.AlgoWrite, docID, doc.DocType, doc.DocNumber)
- revertFuncs = append(revertFuncs, func() error {
- if err := links.DeleteDocumentRedirectDetails(
- srv.AlgoWrite, doc.ObjectID, doc.DocType, doc.DocNumber,
- ); err != nil {
- return fmt.Errorf("error deleting go-link: %w", err)
- }
-
- return nil
- })
- if err != nil {
- srv.Logger.Error("error creating go-link",
+ if err := revertReviewsPost(*revertFuncs); err != nil {
+ srv.Logger.Error("error reverting review creation",
"error", err,
"doc_id", docID,
"method", r.Method,
"path", r.URL.Path)
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
- if err := revertReviewsPost(revertFuncs); err != nil {
- srv.Logger.Error("error reverting review creation",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
- }
- return
}
- srv.Logger.Info("doc redirect details saved",
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- )
+ return &httpErr
+ }
+ srv.Logger.Info("doc shortcut created",
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ }
- // Update document in the database.
- d := models.Document{
- GoogleFileID: docID,
- }
- if err := d.Get(tx); err != nil {
- srv.Logger.Error("error getting document in database",
+ // Create go-link.
+ // TODO: use database for this instead of Algolia.
+ err := links.SaveDocumentRedirectDetails(
+ srv.AlgoWrite, docID, doc.DocType, doc.DocNumber)
+ *revertFuncs = append(*revertFuncs, func() error {
+ if err := links.DeleteDocumentRedirectDetails(
+ srv.AlgoWrite, doc.ObjectID, doc.DocType, doc.DocNumber,
+ ); err != nil {
+ return fmt.Errorf("error deleting go-link: %w", err)
+ }
+ return nil
+ })
+ if err != nil {
+ srv.Logger.Error("error creating go-link",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path)
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review with go link", err)
+ return &httpErr
+ }
+ srv.Logger.Info("doc redirect details saved",
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "DocNumber", doc.DocNumber,
+ "DocType", doc.DocType,
+ "DocNumber", doc.DocNumber,
+ "ObjectID", doc.ObjectID,
+ )
+
+ // Update document in the database.
+ d := srv.NewDocumentByFileID(docID)
+ if err := d.Get(tx); err != nil {
+ srv.Logger.Error("error getting document in database",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path)
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return &httpErr
+ }
+
+ d.DocumentCreatedAt = creationTime // Reset to document published time.
+ d.Status = models.InReviewDocumentStatus
+ d.DocumentNumber = nextDocNum
+ d.DocumentModifiedAt = modifiedTime
+ if err := d.Upsert(tx); err != nil {
+ srv.Logger.Error("error upserting document in database",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path)
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return &httpErr
+ }
+
+ // Share with individual approvers.
+ for _, approver := range doc.Approvers {
+ if srv.SharePoint != nil {
+ if err := srv.SharePoint.ShareFile(docID, approver, "writer"); err != nil {
+ srv.Logger.Error("error sharing file with user approver",
"error", err,
"doc_id", docID,
"method", r.Method,
- "path", r.URL.Path)
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
-
- if err := revertReviewsPost(revertFuncs); err != nil {
- srv.Logger.Error("error reverting review creation",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
- }
- return
+ "path", r.URL.Path,
+ "approver", approver)
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return &httpErr
}
- d.DocumentCreatedAt = now // Reset to document published time.
- d.Status = models.InReviewDocumentStatus
- d.DocumentNumber = nextDocNum
- d.DocumentModifiedAt = modifiedTime
- if err := d.Upsert(tx); err != nil {
- srv.Logger.Error("error upserting document in database",
+ } else {
+ if err := srv.GWService.ShareFile(docID, approver, "writer"); err != nil {
+ srv.Logger.Error("error sharing file with user approver",
"error", err,
"doc_id", docID,
"method", r.Method,
- "path", r.URL.Path)
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
-
- if err := revertReviewsPost(revertFuncs); err != nil {
- srv.Logger.Error("error reverting review creation",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
- }
- return
- }
-
- // Create slice of all approvers consisting of individuals and groups.
- allApprovers := append(doc.Approvers, doc.ApproverGroups...)
-
- // Give document approvers and approver groups edit access to the
- // document.
- for _, a := range allApprovers {
- if err := srv.GWService.ShareFile(docID, a, "writer"); err != nil {
- srv.Logger.Error("error sharing file with approver",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- "approver", a)
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
-
- if err := revertReviewsPost(revertFuncs); err != nil {
- srv.Logger.Error("error reverting review creation",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
- }
- return
- }
+ "path", r.URL.Path,
+ "approver", approver)
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return &httpErr
}
+ }
+ }
- // Get document URL.
- docURL, err := getDocumentURL(srv.Config.BaseURL, docID)
- if err != nil {
- srv.Logger.Error("error getting document URL",
+ // Share with group approvers (attempt direct group share, fallback to expansion).
+ for _, groupEmail := range doc.ApproverGroups {
+ if srv.SharePoint != nil {
+ if err := srv.SharePoint.ShareFileWithGroupOrMembers(docID, groupEmail, "writer"); err != nil {
+ srv.Logger.Error("error sharing file with group approver",
"error", err,
"doc_id", docID,
"method", r.Method,
"path", r.URL.Path,
- )
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
- if err := revertReviewsPost(revertFuncs); err != nil {
- srv.Logger.Error("error reverting review creation",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
- }
- return
+ "group", groupEmail)
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return &httpErr
}
-
- // Send emails to approvers, if enabled.
- if srv.Config.Email != nil && srv.Config.Email.Enabled {
- if len(allApprovers) > 0 {
- // TODO: use an asynchronous method for sending emails because we
- // can't currently recover gracefully from a failure here.
- for _, approverEmail := range allApprovers {
- err := email.SendReviewRequestedEmail(
- email.ReviewRequestedEmailData{
- BaseURL: srv.Config.BaseURL,
- DocumentOwner: doc.Owners[0],
- DocumentShortName: doc.DocNumber,
- DocumentType: doc.DocType,
- DocumentTitle: doc.Title,
- DocumentStatus: doc.Status,
- DocumentURL: docURL,
- Product: doc.Product,
- },
- []string{approverEmail},
- srv.Config.Email.FromAddress,
- srv.GWService,
- )
- if err != nil {
- srv.Logger.Error("error sending approver email",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- )
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
- if err := revertReviewsPost(revertFuncs); err != nil {
- srv.Logger.Error("error reverting review creation",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
- }
- return
- }
- srv.Logger.Info("doc approver email sent",
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- )
- }
- }
- }
-
- // Commit the database transaction.
- if err := tx.Commit().Error; err != nil {
- srv.Logger.Error("error committing database transaction",
+ } else {
+ if err := srv.GWService.ShareFile(docID, groupEmail, "writer"); err != nil {
+ srv.Logger.Error("error sharing file with group approver",
"error", err,
"doc_id", docID,
"method", r.Method,
"path", r.URL.Path,
- )
- http.Error(w, "Error creating review",
- http.StatusInternalServerError)
-
- if err := revertReviewsPost(revertFuncs); err != nil {
- srv.Logger.Error("error reverting review creation",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path)
- }
- return
+ "group", groupEmail)
+ httpErr := structs.NewHTTPError(http.StatusInternalServerError, "Error creating review", err)
+ return &httpErr
}
-
- // Write response.
- w.WriteHeader(http.StatusOK)
-
- // Log success.
- srv.Logger.Info("review created",
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- )
-
- // Request post-processing.
- go func() {
- // Convert document to Algolia object.
- docObj, err := doc.ToAlgoliaObject(true)
- if err != nil {
- srv.Logger.Error("error converting document to Algolia object",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- return
- }
-
- // Save document object in Algolia.
- res, err := srv.AlgoWrite.Docs.SaveObject(docObj)
- if err != nil {
- srv.Logger.Error("error saving document in Algolia",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- return
- }
- err = res.Wait()
- if err != nil {
- srv.Logger.Error("error saving document in Algolia",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- return
- }
-
- // Delete document object from drafts Algolia index.
- delRes, err := srv.AlgoWrite.Drafts.DeleteObject(docID)
- if err != nil {
- srv.Logger.Error("error deleting draft in Algolia",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- return
- }
- err = delRes.Wait()
- if err != nil {
- srv.Logger.Error("error deleting draft in Algolia",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- return
- }
-
- // Send emails to product subscribers, if enabled.
- if srv.Config.Email != nil && srv.Config.Email.Enabled {
- p := models.Product{
- Name: doc.Product,
- }
- if err := p.Get(srv.DB); err != nil {
- srv.Logger.Error("error getting product from database",
- "error", err,
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- )
- return
- }
-
- if len(p.UserSubscribers) > 0 {
- // TODO: use an asynchronous method for sending emails because we
- // can't currently recover gracefully from a failure here.
- for _, subscriber := range p.UserSubscribers {
- err := email.SendSubscriberDocumentPublishedEmail(
- email.SubscriberDocumentPublishedEmailData{
- BaseURL: srv.Config.BaseURL,
- DocumentOwner: doc.Owners[0],
- DocumentShortName: doc.DocNumber,
- DocumentTitle: doc.Title,
- DocumentType: doc.DocType,
- DocumentURL: docURL,
- Product: doc.Product,
- },
- []string{subscriber.EmailAddress},
- srv.Config.Email.FromAddress,
- srv.GWService,
- )
- if err != nil {
- srv.Logger.Error("error sending subscriber email",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- } else {
- srv.Logger.Info("doc subscriber email sent",
- "doc_id", docID,
- "method", r.Method,
- "path", r.URL.Path,
- "product", doc.Product,
- )
- }
- }
- }
- }
-
- // Compare Algolia and database documents to find data inconsistencies.
- // Get document object from Algolia.
- var algoDoc map[string]any
- err = srv.AlgoSearch.Docs.GetObject(docID, &algoDoc)
- if err != nil {
- srv.Logger.Error("error getting Algolia object for data comparison",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- return
- }
- // Get document from database.
- dbDoc := models.Document{
- GoogleFileID: docID,
- }
- if err := dbDoc.Get(srv.DB); err != nil {
- srv.Logger.Error(
- "error getting document from database for data comparison",
- "error", err,
- "path", r.URL.Path,
- "method", r.Method,
- "doc_id", docID,
- )
- return
- }
- // Get all reviews for the document.
- var reviews models.DocumentReviews
- if err := reviews.Find(srv.DB, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: docID,
- },
- }); err != nil {
- srv.Logger.Error(
- "error getting all reviews for document for data comparison",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- return
- }
- if err := CompareAlgoliaAndDatabaseDocument(
- algoDoc, dbDoc, reviews, srv.Config.DocumentTypes.DocumentType,
- ); err != nil {
- srv.Logger.Warn(
- "inconsistencies detected between Algolia and database docs",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- "doc_id", docID,
- )
- }
- }()
-
- default:
- w.WriteHeader(http.StatusMethodNotAllowed)
- return
}
- })
+ }
+
+ return nil
}
-// createShortcut creates a shortcut in the hierarchical folder structure
-// ("Shortcuts Folder/RFC/MyProduct/") under docsFolder.
-func createShortcut(
+// createGoogleShortcut creates a Google Drive shortcut in the hierarchical
+// folder structure ("Shortcuts Folder/RFC/MyProduct/") under docsFolder.
+func createGoogleShortcut(
cfg *config.Config,
doc document.Document,
- s *gw.Service) (shortcut *drive.File, retErr error) {
-
+ s *gw.Service,
+) (shortcut *drive.File, retErr error) {
// Get folder for doc type.
docTypeFolder, err := s.GetSubfolder(
cfg.GoogleWorkspace.ShortcutsFolder, doc.DocType)
@@ -852,6 +809,53 @@ func createShortcut(
return
}
+// createSharePointShortcut creates a shortcut (.url file) in the hierarchical folder structure
+// ("Shortcuts Folder/RFC/MyProduct/") under docsFolder in SharePoint.
+func createSharePointShortcut(
+ cfg *config.Config,
+ doc *document.Document, targetWebURL string,
+ s *sharepointhelper.Service,
+) (shortcutID string, retErr error) {
+ // Get or create folder for doc type under ShortcutsFolder
+ shortcutFolderID, err := s.ResolveFolderPath(cfg.SharePoint.ShortcutsFolder)
+ if err != nil {
+ return "", fmt.Errorf("error resolving shortcut folder path '%s': %w", cfg.SharePoint.ShortcutsFolder, err)
+ }
+ docTypeFolder, err := s.GetSubfolder(shortcutFolderID, doc.DocType)
+ if err != nil {
+ return "", fmt.Errorf("error getting doc type subfolder: %w", err)
+ }
+ if docTypeFolder == nil {
+ docTypeFolderID, err := s.CreateFolder(doc.DocType, shortcutFolderID)
+ if err != nil {
+ return "", fmt.Errorf("error creating doc type subfolder: %w", err)
+ }
+ docTypeFolder = &sharepointhelper.DriveItem{ID: docTypeFolderID, Name: doc.DocType}
+ }
+
+ // Get or create folder for doc type + product
+ productFolder, err := s.GetSubfolder(docTypeFolder.ID, doc.Product)
+ if err != nil {
+ return "", fmt.Errorf("error getting product subfolder: %w", err)
+ }
+ if productFolder == nil {
+ productFolderID, err := s.CreateFolder(doc.Product, docTypeFolder.ID)
+ if err != nil {
+ return "", fmt.Errorf("error creating product subfolder: %w", err)
+ }
+ productFolder = &sharepointhelper.DriveItem{ID: productFolderID, Name: doc.Product}
+ }
+
+ // TODO : Check if people can access the link. What type of permissions are available and how does it work with existing sharing settings of the documents?
+ // Create the .url shortcut file in the product folder
+ shortcutFileID, err := s.CreateShortcut(targetWebURL, doc.Title, productFolder.ID)
+ if err != nil {
+ return "", fmt.Errorf("error creating shortcut: %w", err)
+ }
+
+ return shortcutFileID, nil
+}
+
// getDocumentURL returns a Hermes document URL.
func getDocumentURL(baseURL, docID string) (string, error) {
docURL, err := url.Parse(baseURL)
@@ -866,6 +870,289 @@ func getDocumentURL(baseURL, docID string) (string, error) {
return docURLString, nil
}
+func notifyProductSubscribers(
+ srv *server.Server,
+ doc *document.Document,
+ docID, docURL string,
+ r *http.Request,
+) {
+ if srv == nil || srv.Config == nil {
+ return
+ }
+
+ emailCfg := srv.Config.Email
+ if emailCfg == nil || !emailCfg.Enabled {
+ return
+ }
+
+ from := strings.TrimSpace(emailCfg.FromAddress)
+ if from == "" {
+ srv.Logger.Warn("email notification skipped; from address not configured",
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ return
+ }
+
+ if len(doc.Owners) == 0 || strings.TrimSpace(doc.Owners[0]) == "" {
+ srv.Logger.Warn("email notification skipped; document has no owner",
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ return
+ }
+
+ owner := strings.TrimSpace(doc.Owners[0])
+ if owner == "" {
+ srv.Logger.Warn("email notification skipped; document owner is blank",
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ return
+ }
+
+ productName := strings.TrimSpace(doc.Product)
+ if productName == "" {
+ srv.Logger.Warn("email notification skipped; document product missing",
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ return
+ }
+
+ p, err := getProductWithSubscribers(srv.DB, productName)
+ if err != nil {
+ srv.Logger.Error("error getting product from database",
+ "error", err,
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "product", productName,
+ )
+ return
+ }
+
+ recipientsSet := map[string]struct{}{}
+ recipients := []string{}
+ for _, subscriber := range p.UserSubscribers {
+ addr := strings.TrimSpace(subscriber.EmailAddress)
+ if addr == "" {
+ continue
+ }
+ if _, exists := recipientsSet[addr]; exists {
+ continue
+ }
+ recipientsSet[addr] = struct{}{}
+ recipients = append(recipients, addr)
+ }
+
+ if len(recipients) == 0 {
+ srv.Logger.Info("no product subscribers to notify",
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "product", productName,
+ )
+ return
+ }
+
+ // Get BCC batch size from config, default to 500 (Outlook limit)
+ bccBatchSize := 500
+ if srv.Config.Email.BCCBatchSize > 0 {
+ bccBatchSize = srv.Config.Email.BCCBatchSize
+ }
+
+ // Split recipients into batches
+ batches := [][]string{}
+ for i := 0; i < len(recipients); i += bccBatchSize {
+ end := i + bccBatchSize
+ if end > len(recipients) {
+ end = len(recipients)
+ }
+ batches = append(batches, recipients[i:end])
+ }
+
+ srv.Logger.Info("sending subscriber notifications",
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "product", productName,
+ "subscriber_count", len(recipients),
+ "batch_count", len(batches),
+ "batch_size", bccBatchSize,
+ )
+
+ // Send email to subscribers in batches using BCC.
+ // This avoids hitting rate limits and recipient count limits.
+ // Retry configuration is read from srv.Config.Email.Retry.
+ for batchIdx, batch := range batches {
+ // Capture loop variables for goroutine
+ batchNum := batchIdx + 1
+ batchRecipients := batch
+
+ go helpers.SendEmailWithRetry(
+ srv,
+ func() error {
+ return email.SendSubscriberDocumentPublishedEmailWithBCC(
+ email.SubscriberDocumentPublishedEmailData{
+ BaseURL: srv.Config.BaseURL,
+ DocumentOwner: owner,
+ DocumentShortName: doc.DocNumber,
+ DocumentTitle: doc.Title,
+ DocumentType: doc.DocType,
+ DocumentURL: docURL,
+ Product: productName,
+ },
+ []string{from}, // Add sender as TO recipient to avoid spam filters
+ batchRecipients, // Batch of subscribers in BCC
+ from,
+ srv.GetEmailSender(),
+ )
+ },
+ docID,
+ fmt.Sprintf("subscriber_notification_batch_%d", batchNum),
+ r,
+ )
+
+ srv.Logger.Info("subscriber email batch queued for sending",
+ "doc_id", docID,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "product", productName,
+ "batch_number", batchNum,
+ "batch_total", len(batches),
+ "batch_recipient_count", len(batchRecipients),
+ )
+ }
+}
+
+// handleReviewPostProcessing handles the asynchronous post-processing tasks after review creation
+func handleReviewPostProcessing(srv *server.Server, doc *document.Document, docID string, r *http.Request) {
+ // Convert document to Algolia object.
+ docObj, err := doc.ToAlgoliaObject(true)
+ if err != nil {
+ srv.Logger.Error("error converting document to Algolia object",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ )
+ return
+ }
+
+ // Save document object in Algolia.
+ res, err := srv.AlgoWrite.Docs.SaveObject(docObj)
+ if err != nil {
+ srv.Logger.Error("error saving document in Algolia",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ )
+ return
+ }
+ err = res.Wait()
+ if err != nil {
+ srv.Logger.Error("error saving document in Algolia",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ )
+ return
+ }
+
+ // Delete document object from drafts Algolia index.
+ delRes, err := srv.AlgoWrite.Drafts.DeleteObject(docID)
+ if err != nil {
+ srv.Logger.Error("error deleting draft in Algolia",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ )
+ return
+ }
+ err = delRes.Wait()
+ if err != nil {
+ srv.Logger.Error("error deleting draft in Algolia",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ )
+ return
+ }
+
+ docURL, err := getDocumentURL(srv.Config.BaseURL, docID)
+ if err != nil {
+ srv.Logger.Error("error getting document URL",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ )
+ return
+ }
+
+ notifyProductSubscribers(srv, doc, docID, docURL, r)
+
+ // Compare Algolia and database documents to find data inconsistencies.
+ // Get document object from Algolia.
+ var algoDoc map[string]any
+ err = srv.AlgoSearch.Docs.GetObject(docID, &algoDoc)
+ if err != nil {
+ srv.Logger.Error("error getting Algolia object for data comparison",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ )
+ return
+ }
+ // Get document from database.
+ dbDoc := srv.NewDocumentByFileID(docID)
+ if err := dbDoc.Get(srv.DB); err != nil {
+ srv.Logger.Error(
+ "error getting document from database for data comparison",
+ "error", err,
+ "path", r.URL.Path,
+ "method", r.Method,
+ "doc_id", docID,
+ )
+ return
+ }
+ // Get all reviews for the document.
+ var reviews models.DocumentReviews
+ if err := reviews.Find(srv.DB, models.DocumentReview{
+ Document: srv.NewDocumentByFileID(docID),
+ }); err != nil {
+ srv.Logger.Error(
+ "error getting all reviews for document for data comparison",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ )
+ return
+ }
+ if err := CompareAlgoliaAndDatabaseDocument(
+ algoDoc, dbDoc, reviews, srv.Config.DocumentTypes.DocumentType,
+ ); err != nil {
+ srv.Logger.Warn(
+ "inconsistencies detected between Algolia and database docs",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ "doc_id", docID,
+ )
+ }
+}
+
// revertReviewsPost attempts to revert the actions that occur when a review is
// created. This is to be used in the case of an error during the review-
// creation process.
diff --git a/internal/auth/auth.go b/internal/auth/auth.go
index 3555ac6b7..0e7e20e64 100644
--- a/internal/auth/auth.go
+++ b/internal/auth/auth.go
@@ -4,29 +4,47 @@ import (
"net/http"
"github.com/hashicorp-forge/hermes/internal/auth/google"
+ "github.com/hashicorp-forge/hermes/internal/auth/microsoft"
+ "github.com/hashicorp-forge/hermes/internal/auth/oidcalb"
"github.com/hashicorp-forge/hermes/internal/auth/oktaalb"
"github.com/hashicorp-forge/hermes/internal/config"
gw "github.com/hashicorp-forge/hermes/pkg/googleworkspace"
+ sp "github.com/hashicorp-forge/hermes/pkg/sharepointhelper"
"github.com/hashicorp/go-hclog"
)
// AuthenticateRequest is middleware that authenticates an HTTP request.
func AuthenticateRequest(
- cfg config.Config, gwSvc *gw.Service, log hclog.Logger, next http.Handler,
+ cfg config.Config, gwSvc *gw.Service, spSvc *sp.Service, log hclog.Logger, next http.Handler,
) http.Handler {
- // If Okta isn't disabled, authenticate using Okta.
+ // Priority 1: OIDC ALB (used by SharePoint deployments behind ALB).
+ if cfg.OidcAlb != nil && !cfg.OidcAlb.Disabled {
+ oa, err := oidcalb.New(*cfg.OidcAlb, log)
+ if err != nil {
+ log.Error("error creating OIDC ALB authenticator", "error", err)
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "Internal server error", http.StatusInternalServerError)
+ })
+ }
+
+ return oa.EnforceOIDCAuth(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ validateUserEmail(w, r, log)
+ next.ServeHTTP(w, r)
+ }))
+ }
+
+ // Priority 2: Okta (deprecated — legacy Google Hermes deployments).
if cfg.Okta != nil && !cfg.Okta.Disabled {
- // Create Okta authorizer.
+ log.Warn("using deprecated 'okta' auth config — migrate to 'oidc_alb'")
oa, err := oktaalb.New(*cfg.Okta, log)
if err != nil {
- log.Error("error creating Okta authenticator")
+ log.Error("error creating Okta ALB authenticator", "error", err)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
- return
})
}
- // Return handler wrapped with Okta auth.
return oa.EnforceOktaAuth(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
validateUserEmail(w, r, log)
@@ -34,9 +52,17 @@ func AuthenticateRequest(
}))
}
- // Authenticate using Google.
+ // Priority 3: Microsoft Auth (SharePoint without ALB).
+ if cfg.SharePoint != nil {
+ return microsoft.AuthenticateRequest(cfg.SharePoint, log, spSvc,
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ validateUserEmail(w, r, log)
+ next.ServeHTTP(w, r)
+ }))
+ }
+
+ // Priority 4: Google Auth (legacy Google Hermes).
return google.AuthenticateRequest(gwSvc, log,
- // Return handler wrapped with Google auth.
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
validateUserEmail(w, r, log)
next.ServeHTTP(w, r)
@@ -46,10 +72,23 @@ func AuthenticateRequest(
// validateUserEmail validates that userEmail was set in the request's context.
// It responds with an internal server error if not found because this should
// be set by all authentication methods. userEmail is used for authorization in
-// API endpoint implmentations.
+// API endpoint implementations.
+// Note: Skip validation for paths that bypass OIDC authentication.
func validateUserEmail(
w http.ResponseWriter, r *http.Request, log hclog.Logger,
) {
+ // Skip validation for paths that bypass OIDC authentication
+ if oidcalb.ShouldBypassOIDC(r.URL.Path) {
+ return
+ }
+
+ // Skip validation for the pre-authenticate route. Some auth providers, such
+ // as SharePoint/Microsoft auth without ALB, must serve and handle
+ // /authenticate before a user context exists.
+ if r.URL.Path == "/authenticate" {
+ return
+ }
+
if r.Context().Value("userEmail") == nil {
log.Error("userEmail is not set in the request context",
"method", r.Method,
diff --git a/internal/auth/google/google.go b/internal/auth/google/google.go
index d73fabf00..c45a6690e 100644
--- a/internal/auth/google/google.go
+++ b/internal/auth/google/google.go
@@ -24,11 +24,16 @@ func AuthenticateRequest(
// Validate access token.
ti, err := s.ValidateAccessToken(tok)
if err != nil || !ti.VerifiedEmail {
- log.Error("error validating Google access token",
- "error", err,
- "method", r.Method,
- "path", r.URL.Path,
- )
+ isAuthProbe := r.Method == http.MethodHead &&
+ (r.URL.Path == "/api/v1/me" || r.URL.Path == "/api/v2/me")
+
+ if !isAuthProbe {
+ log.Error("error validating Google access token",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ }
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
diff --git a/internal/auth/microsoft/microsoft.go b/internal/auth/microsoft/microsoft.go
new file mode 100644
index 000000000..2a6217c93
--- /dev/null
+++ b/internal/auth/microsoft/microsoft.go
@@ -0,0 +1,356 @@
+package microsoft
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/hashicorp-forge/hermes/internal/config"
+ "github.com/hashicorp-forge/hermes/pkg/sharepointhelper"
+ "github.com/hashicorp/go-hclog"
+ "golang.org/x/oauth2"
+ "golang.org/x/oauth2/microsoft"
+)
+
+// MicrosoftAuthenticator handles Microsoft authentication
+type MicrosoftAuthenticator struct {
+ Config *config.MicrosoftAuth
+ Log hclog.Logger
+}
+
+// New creates a new Microsoft authenticator
+func New(cfg config.MicrosoftAuth, log hclog.Logger) (*MicrosoftAuthenticator, error) {
+ return &MicrosoftAuthenticator{
+ Config: &cfg,
+ Log: log,
+ }, nil
+}
+
+// AuthenticateRequest is middleware that authenticates an HTTP request using Microsoft
+func AuthenticateRequest(cfg *config.SharePointConfig, log hclog.Logger, spService *sharepointhelper.Service, next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ // For static assets and public paths, skip authentication but set a default email
+ if strings.HasPrefix(r.URL.Path, "/assets/") ||
+ strings.HasPrefix(r.URL.Path, "/public/") ||
+ strings.HasPrefix(r.URL.Path, "/static/") ||
+ strings.HasPrefix(r.URL.Path, "/images") ||
+ strings.HasPrefix(r.URL.Path, "/addin/") ||
+ strings.HasPrefix(r.URL.Path, "/.") ||
+ r.URL.Path == "/favicon.ico" {
+ // Set a default email in context to avoid errors downstream
+ ctx := context.WithValue(r.Context(), "userEmail", "anonymous@static-asset")
+ next.ServeHTTP(w, r.WithContext(ctx))
+ return
+ }
+
+ // Special case for the authenticate page itself
+ if r.URL.Path == "/authenticate" {
+ // If code is present, this is the callback from Microsoft
+ if r.URL.Query().Get("code") != "" {
+ handleAuthCallback(w, r, cfg, log)
+ return
+ } else if r.URL.Query().Get("init") == "true" {
+ // If init parameter is present, initiate the auth flow
+ initiateAuthFlow(w, r, cfg, log)
+ return
+ } else if r.Method == "GET" && strings.Contains(r.Header.Get("Accept"), "text/html") {
+ // If it looks like a browser request directly to /authenticate, initiate the auth flow
+ initiateAuthFlow(w, r, cfg, log)
+ return
+ } else {
+ // For other cases like API requests, serve the page normally
+ next.ServeHTTP(w, r)
+ return
+ }
+ }
+
+ // Check for existing auth token in header or cookie
+ token := extractTokenFromRequest(r)
+ if token != "" {
+ // Validate token with Microsoft using SharePoint service
+ if validateUserToken(token, log, spService) {
+ // Set user email in context
+ email, err := getUserEmailFromToken(token, log, spService)
+ if err == nil && email != "" {
+
+ // Set both user email AND Microsoft token in context for downstream handlers
+ ctx := context.WithValue(r.Context(), "userEmail", email)
+ ctx = context.WithValue(ctx, "microsoftToken", token)
+
+ next.ServeHTTP(w, r.WithContext(ctx))
+ return
+ } else {
+ log.Warn("Failed to get user email from token", "error", err)
+ }
+ } else {
+ log.Warn("token validation failed")
+ }
+ }
+
+ // If it's an API request or AJAX request, return 401 with JSON response
+ if strings.HasPrefix(r.URL.Path, "/api/") || r.Header.Get("X-Requested-With") == "XMLHttpRequest" {
+ log.Warn("unauthorized API request", "path", r.URL.Path)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusUnauthorized)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "error": "Unauthorized",
+ "redirect": "/authenticate",
+ "message": "Please authenticate to access this resource",
+ })
+ return
+ }
+
+ // For regular web pages, redirect to authenticate
+ if r.URL.Path != "/authenticate" {
+ http.Redirect(w, r, "/authenticate", http.StatusFound)
+ return
+ }
+
+ // Allow the /authenticate page to be served
+ next.ServeHTTP(w, r)
+ })
+}
+
+// extractTokenFromRequest extracts the token from the request
+func extractTokenFromRequest(r *http.Request) string {
+ // Check Authorization header
+ authHeader := r.Header.Get("Authorization")
+ if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
+ return strings.TrimPrefix(authHeader, "Bearer ")
+ }
+
+ // Check Cookie - check both microsoft_token and the standard token cookie name
+ cookie, err := r.Cookie("microsoft_token")
+ if err == nil && cookie != nil && cookie.Value != "" {
+ return cookie.Value
+ }
+
+ // Check for alternative cookie names
+ cookie, err = r.Cookie("token")
+ if err == nil && cookie != nil && cookie.Value != "" {
+ return cookie.Value
+ }
+
+ // Check for auth_token cookie
+ cookie, err = r.Cookie("auth_token")
+ if err == nil && cookie != nil && cookie.Value != "" {
+ return cookie.Value
+ }
+
+ // If we have a user_email cookie, that indicates a successful login happened
+ // and we should check for any available cookies
+ cookie, err = r.Cookie("user_email")
+ if err == nil && cookie != nil && cookie.Value != "" {
+ // Loop through all cookies to find any that might be the token
+ for _, c := range r.Cookies() {
+ if len(c.Value) > 100 { // Token should be fairly long
+ return c.Value
+ }
+ }
+ }
+
+ return ""
+}
+
+// validateUserToken validates the token with Microsoft using SharePoint service
+func validateUserToken(token string, log hclog.Logger, spService *sharepointhelper.Service) bool {
+ if spService == nil {
+ log.Error("SharePoint service is required for token validation")
+ return false
+ }
+
+ return spService.ValidateUserToken(token)
+}
+
+// getUserEmailFromToken gets the user email from the token using SharePoint service
+func getUserEmailFromToken(token string, log hclog.Logger, spService *sharepointhelper.Service) (string, error) {
+ if spService == nil {
+ log.Error("SharePoint service is required for getting user email")
+ return "", fmt.Errorf("SharePoint service is required")
+ }
+
+ return spService.GetUserEmailFromToken(token)
+}
+
+func initiateAuthFlow(w http.ResponseWriter, r *http.Request, cfg *config.SharePointConfig, logger hclog.Logger) {
+ logger.Debug("initiating Microsoft auth flow")
+
+ // Check if this is a popup flow request.
+ // The "popup" parameter is expected to be "true" or "false" (case-insensitive).
+ popupParam := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("popup")))
+ isPopup := popupParam == "true"
+
+ // Create OAuth2 config for Microsoft
+ oauth2Config := &oauth2.Config{
+ ClientID: cfg.ClientID,
+ ClientSecret: cfg.ClientSecret,
+ RedirectURL: cfg.RedirectURI,
+ Endpoint: microsoft.AzureADEndpoint(cfg.TenantID),
+ Scopes: []string{"openid", "profile", "email", "User.Read"},
+ }
+
+ // Generate authorization URL with a random state for security
+ state := fmt.Sprintf("%d", time.Now().UnixNano())
+ if isPopup {
+ state = "popup_" + state
+ }
+ url := oauth2Config.AuthCodeURL(state, oauth2.AccessTypeOffline)
+
+ // Set a cookie to indicate auth is in progress
+ http.SetCookie(w, &http.Cookie{
+ Name: "auth_in_progress",
+ Value: "microsoft",
+ Path: "/",
+ HttpOnly: true,
+ MaxAge: 600, // 10 minutes
+ SameSite: http.SameSiteNoneMode,
+ Secure: true,
+ })
+
+ // Store popup state in a cookie if this is a popup flow
+ if isPopup {
+ http.SetCookie(w, &http.Cookie{
+ Name: "auth_is_popup",
+ Value: "true",
+ Path: "/",
+ HttpOnly: true,
+ MaxAge: 600, // 10 minutes
+ SameSite: http.SameSiteNoneMode,
+ Secure: true,
+ })
+ }
+
+ logger.Debug("Auth cookie set, redirecting to Microsoft login page", "isPopup", isPopup)
+
+ // Redirect to Microsoft login page
+ http.Redirect(w, r, url, http.StatusFound)
+}
+
+func handleAuthCallback(w http.ResponseWriter, r *http.Request, cfg *config.SharePointConfig, logger hclog.Logger) {
+ // Log the start of the callback handling
+ logger.Info("Handling Microsoft auth callback", "path", r.URL.Path)
+
+ // Get authorization code from query parameters
+ code := r.URL.Query().Get("code")
+ if code == "" {
+ logger.Error("No authorization code provided in callback")
+ http.Error(w, "No authorization code provided", http.StatusBadRequest)
+ return
+ }
+ logger.Info("Authorization code received", "code_length", len(code))
+
+ // Create OAuth2 config for Microsoft
+ oauth2Config := &oauth2.Config{
+ ClientID: cfg.ClientID,
+ ClientSecret: cfg.ClientSecret,
+ RedirectURL: cfg.RedirectURI,
+ Endpoint: microsoft.AzureADEndpoint(cfg.TenantID),
+ Scopes: []string{"openid", "profile", "email", "User.Read"},
+ }
+
+ // Exchange authorization code for token
+ logger.Info("Exchanging authorization code for token")
+ token, err := oauth2Config.Exchange(context.Background(), code)
+ if err != nil {
+ logger.Error("Error exchanging code for token", "error", err)
+ http.Error(w, "Failed to authenticate: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+ logger.Info("Successfully exchanged code for token", "token_type", token.TokenType, "expires", token.Expiry)
+
+ // Get user email from Microsoft Graph
+ logger.Info("Fetching user email from Microsoft Graph API")
+ client := oauth2Config.Client(context.Background(), token)
+ resp, err := client.Get("https://graph.microsoft.com/v1.0/me")
+ if err != nil {
+ logger.Error("Error calling Microsoft Graph API", "error", err)
+ http.Error(w, "Failed to get user info: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ logger.Error("Microsoft Graph API returned non-200 status", "status", resp.StatusCode)
+ http.Error(w, "Failed to get user info", http.StatusInternalServerError)
+ return
+ }
+
+ // Parse the response body
+ var data struct {
+ Mail string `json:"mail"`
+ UserPrincipalName string `json:"userPrincipalName"`
+ DisplayName string `json:"displayName"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
+ logger.Error("Error decoding Microsoft Graph API response", "error", err)
+ http.Error(w, "Failed to parse user info: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ // Use mail if available, otherwise use userPrincipalName
+ email := data.Mail
+ if email == "" {
+ email = data.UserPrincipalName
+ }
+ if email == "" {
+ logger.Error("No email found in Microsoft Graph API response")
+ http.Error(w, "Failed to get user email", http.StatusInternalServerError)
+ return
+ }
+ logger.Info("User authenticated successfully", "email", email, "name", data.DisplayName)
+
+ // Check if this is a popup authentication flow
+ isPopupFlow := false
+ if cookie, err := r.Cookie("auth_is_popup"); err == nil && cookie.Value == "true" {
+ isPopupFlow = true
+ }
+
+ // Set cookies with authentication information
+ http.SetCookie(w, &http.Cookie{
+ Name: "user_email",
+ Value: email,
+ Path: "/",
+ HttpOnly: true,
+ MaxAge: 3600,
+ SameSite: http.SameSiteNoneMode,
+ Secure: true,
+ })
+
+ http.SetCookie(w, &http.Cookie{
+ Name: "microsoft_token",
+ Value: token.AccessToken,
+ Path: "/",
+ HttpOnly: true,
+ MaxAge: int(time.Until(token.Expiry).Seconds()),
+ SameSite: http.SameSiteNoneMode,
+ Secure: true,
+ })
+
+ // Clear the popup indicator cookie
+ if isPopupFlow {
+ http.SetCookie(w, &http.Cookie{
+ Name: "auth_is_popup",
+ Value: "",
+ Path: "/",
+ HttpOnly: true,
+ MaxAge: -1, // Delete cookie
+ SameSite: http.SameSiteNoneMode,
+ Secure: true,
+ })
+ }
+
+ // Log success before redirect
+ logger.Info("Authentication successful, redirecting", "email", email, "isPopup", isPopupFlow)
+
+ // If this is a popup flow, redirect to the auth callback page
+ // Otherwise, redirect to the dashboard
+ if isPopupFlow {
+ http.Redirect(w, r, "/addin/auth-callback.html", http.StatusFound)
+ } else {
+ http.Redirect(w, r, "/dashboard", http.StatusFound)
+ }
+}
diff --git a/internal/auth/oidcalb/doc.go b/internal/auth/oidcalb/doc.go
new file mode 100644
index 000000000..9d68e1a8f
--- /dev/null
+++ b/internal/auth/oidcalb/doc.go
@@ -0,0 +1,3 @@
+// Package oidcalb implements authorization using OIDC and an Amazon Application
+// Load Balancer.
+package oidcalb
diff --git a/internal/auth/oidcalb/oidcalb.go b/internal/auth/oidcalb/oidcalb.go
new file mode 100644
index 000000000..08e989582
--- /dev/null
+++ b/internal/auth/oidcalb/oidcalb.go
@@ -0,0 +1,225 @@
+// Package oidcalb provides generic OIDC authentication for AWS Application Load Balancer.
+// This implementation supports any OIDC provider that works with AWS ALB.
+package oidcalb
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/cenkalti/backoff/v4"
+ "github.com/golang-jwt/jwt/v5"
+ "github.com/hashicorp/go-hclog"
+)
+
+// AnonymousUserEmail is the email value set in context for requests that bypass OIDC authentication.
+// This constant is used for static assets and add-in routes that don't require user authentication.
+const AnonymousUserEmail = ""
+
+// ShouldBypassOIDC determines whether the given request path should bypass OIDC verification.
+// These paths must match the ALB listener rules that forward without OIDC.
+func ShouldBypassOIDC(path string) bool {
+ return strings.HasPrefix(path, "/addin/") ||
+ strings.HasPrefix(path, "/static/") ||
+ strings.HasPrefix(path, "/assets/") ||
+ path == "/ping" ||
+ path == "/favicon.ico"
+}
+
+// ALBAuthorizer implements OIDC authorization using AWS Application Load Balancer.
+type ALBAuthorizer struct {
+ // cfg is the configuration for the authorizer.
+ cfg Config
+
+ // log is the logger to use.
+ log hclog.Logger
+}
+
+// Config is the configuration for OIDC ALB authorization.
+type Config struct {
+ // AuthServerURL is the URL of the OIDC authorization server.
+ AuthServerURL string `hcl:"auth_server_url,optional"`
+
+ // AWSRegion is the region of the AWS Application Load Balancer.
+ AWSRegion string `hcl:"aws_region,optional"`
+
+ // ClientID is the OIDC client ID.
+ ClientID string `hcl:"client_id,optional"`
+
+ // Disabled disables OIDC authorization.
+ Disabled bool `hcl:"disabled,optional"`
+
+ // JWTSigner is the trusted signer for the ALB JWT header.
+ JWTSigner string `hcl:"jwt_signer,optional"`
+}
+
+// New returns a new ALB OIDC authorizer.
+func New(cfg Config, l hclog.Logger) (*ALBAuthorizer, error) {
+ return &ALBAuthorizer{
+ cfg: cfg,
+ log: l,
+ }, nil
+}
+
+// EnforceOIDCAuth is HTTP middleware that enforces OIDC authorization.
+func (aa *ALBAuthorizer) EnforceOIDCAuth(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Skip OIDC verification for paths that bypass ALB OIDC authentication.
+ // These paths must match the ALB listener rules that forward without OIDC.
+ if ShouldBypassOIDC(r.URL.Path) {
+ // Set AnonymousUserEmail in context. Downstream code should handle empty string
+ // and skip user-specific operations for these paths.
+ ctx := context.WithValue(r.Context(), "userEmail", AnonymousUserEmail)
+ next.ServeHTTP(w, r.WithContext(ctx))
+ return
+ }
+
+ user, err := aa.verifyOIDCToken(r)
+ if err != nil {
+ aa.log.Error("error verifying OIDC token",
+ "error", err,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ } else {
+ // Set user email from the OIDC claims.
+ ctx := context.WithValue(r.Context(), "userEmail", user)
+ r = r.WithContext(ctx)
+
+ next.ServeHTTP(w, r)
+ }
+ })
+}
+
+// verifyOIDCToken checks if the request is authorized and returns the user
+// identity.
+func (aa *ALBAuthorizer) verifyOIDCToken(r *http.Request) (string, error) {
+ if aa.cfg.JWTSigner == "" {
+ return "", fmt.Errorf("JWT signer not configured")
+ }
+
+ // Get the key ID from JWT headers (the kid field).
+ encodedJWT := r.Header.Get("x-amzn-oidc-data")
+ if encodedJWT == "" {
+ return "", fmt.Errorf("no OIDC data header found")
+ }
+ split := strings.Split(encodedJWT, ".")
+ if len(split) != 3 {
+ return "", fmt.Errorf(
+ "bad OIDC data: wrong number of substrings, found %d", len(split))
+ }
+ jwtHeaders := split[0]
+ decodedJWTHeaders, err := base64.StdEncoding.DecodeString(jwtHeaders)
+ if err != nil {
+ return "", fmt.Errorf("error decoding JWT headers: %w", err)
+ }
+ var decodedJSON map[string]interface{}
+ if err := json.Unmarshal(decodedJWTHeaders, &decodedJSON); err != nil {
+ return "", fmt.Errorf("error unmarshaling JSON: %w", err)
+ }
+ kid, ok := decodedJSON["kid"].(string)
+ if !ok {
+ return "", fmt.Errorf("kid not found in decoded JSON")
+ }
+
+ // Validate signer.
+ signer, ok := decodedJSON["signer"].(string)
+ if !ok {
+ return "", fmt.Errorf("signer not found in decoded JSON")
+ }
+ if signer != aa.cfg.JWTSigner {
+ return "", fmt.Errorf("unexpected signer: %s", signer)
+ }
+
+ // Get the public key from the regional endpoint.
+ url := fmt.Sprintf("https://public-keys.auth.elb.%s.amazonaws.com/%s",
+ aa.cfg.AWSRegion, kid)
+ var resp *http.Response
+ // Execute the HTTP request with exponential backoff.
+ bo := backoff.NewExponentialBackOff()
+ bo.MaxElapsedTime = 2 * time.Minute
+ err = backoff.RetryNotify(func() error {
+ resp, err = http.Get(url)
+ return err
+ }, bo,
+ func(err error, d time.Duration) {
+ aa.log.Warn("error getting ELB public key (retrying)",
+ "error", err,
+ "delay", d,
+ )
+ },
+ )
+ if err != nil || resp == nil {
+ return "", fmt.Errorf("error getting ELB public key: %w", err)
+ }
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", fmt.Errorf("error reading response body: %w", err)
+ }
+ pubKey, err := jwt.ParseECPublicKeyFromPEM(body)
+ if err != nil {
+ return "", fmt.Errorf("error parsing public key: %w", err)
+ }
+
+ // Get the token payload.
+ token, err := jwt.Parse(
+ encodedJWT, func(token *jwt.Token) (interface{}, error) {
+ if _, ok := (token.Method.(*jwt.SigningMethodECDSA)); !ok {
+ return "", fmt.Errorf(
+ "unexpected signing method: %v", token.Header["alg"])
+ }
+ return pubKey, nil
+ }, jwt.WithPaddingAllowed())
+ if err != nil {
+ return "", fmt.Errorf("error parsing JWT: %w", err)
+ }
+
+ // Verify claims.
+ var preferredUsername string
+ if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
+ prefRaw, ok := claims["preferred_username"]
+ if !ok {
+ return "", fmt.Errorf("preferred_username claim not found")
+ }
+ preferredUsername, ok = prefRaw.(string)
+ if !ok {
+ return "", fmt.Errorf("preferred_username claim is invalid")
+ }
+ } else {
+ return "", fmt.Errorf("claims not found")
+ }
+
+ if preferredUsername == "" {
+ return "", fmt.Errorf("preferred_username claim is empty")
+ }
+
+ if !strings.Contains(preferredUsername, "@") {
+ ///check emailAddress claim
+ var emailAddress string
+ if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
+ emailRaw, ok := claims["emailAddress"]
+ if !ok {
+ return "", fmt.Errorf("email claim not found")
+ }
+ emailAddress, ok = emailRaw.(string)
+ if !ok {
+ return "", fmt.Errorf("email claim is invalid")
+ }
+ } else {
+ return "", fmt.Errorf("claims not found")
+ }
+ if emailAddress == "" {
+ return "", fmt.Errorf("email claim is empty")
+ }
+ preferredUsername = emailAddress
+ }
+
+ return preferredUsername, nil
+}
diff --git a/internal/auth/sharepoint/sharepoint.go b/internal/auth/sharepoint/sharepoint.go
new file mode 100644
index 000000000..141216525
--- /dev/null
+++ b/internal/auth/sharepoint/sharepoint.go
@@ -0,0 +1,38 @@
+package sharepoint
+
+import (
+ "context"
+ "net/http"
+
+ sp "github.com/hashicorp-forge/hermes/pkg/sharepointhelper"
+ "github.com/hashicorp/go-hclog"
+)
+
+// AuthenticateRequest authenticates an HTTP request using SharePoint.
+func AuthenticateRequest(
+ spSvc *sp.Service, log hclog.Logger, next http.Handler,
+) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ log.Debug("SharePoint authentication request received",
+ "method", r.Method,
+ "path", r.URL.Path)
+
+ // Validate SharePoint token or session
+ //userEmail, err := spSvc.ValidateToken(r.Header.Get("Authorization"))
+ accessToken, _ := spSvc.GetToken()
+ userEmail, err := spSvc.ValidateToken(accessToken)
+ if err != nil {
+ log.Error("SharePoint authentication failed", "error", err)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+ log.Info("SharePoint authentication successful", "userEmail", userEmail)
+ // Add userEmail to the request context
+ ctx := r.Context()
+ ctx = context.WithValue(ctx, "userEmail", userEmail)
+ r = r.WithContext(ctx)
+
+ // Proceed to the next handler
+ next.ServeHTTP(w, r)
+ })
+}
diff --git a/internal/cmd/commands/indexer/indexer.go b/internal/cmd/commands/indexer/indexer.go
index ceeff3f18..39de7405f 100644
--- a/internal/cmd/commands/indexer/indexer.go
+++ b/internal/cmd/commands/indexer/indexer.go
@@ -13,6 +13,7 @@ import (
"github.com/hashicorp-forge/hermes/internal/indexer"
"github.com/hashicorp-forge/hermes/pkg/algolia"
gw "github.com/hashicorp-forge/hermes/pkg/googleworkspace"
+ sp "github.com/hashicorp-forge/hermes/pkg/sharepointhelper"
"github.com/hashicorp/go-hclog"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)
@@ -130,26 +131,55 @@ func (c *Command) Run(args []string) int {
return 1
}
- // Initialize Google Workspace service.
+ // Initialize SharePoint or Google Workspace service.
+ var sharepointSvc *sp.Service
var goog *gw.Service
- if cfg.GoogleWorkspace.Auth != nil {
+ if cfg.SharePoint != nil {
+ // Initialize SharePoint service.
+ sharepointSvc = sp.NewService(cfg.SharePoint, log)
+
+ // Get a valid SharePoint token.
+ _, err := sharepointSvc.GetToken()
+ if err != nil {
+ ui.Error(fmt.Sprintf("error initializing SharePoint service: %v", err))
+ return 1
+ }
+ log.Info("Successfully initialized SharePoint service.")
+ } else if cfg.GoogleWorkspace.Auth != nil {
// Use Google Workspace auth if it is defined in the config.
goog = gw.NewFromConfig(cfg.GoogleWorkspace.Auth)
+ log.Info("Successfully initialized Google Workspace service.")
} else {
// Use OAuth if Google Workspace auth is not defined in the config.
goog = gw.New()
+ log.Info("Successfully initialized Google Workspace service with OAuth.")
}
+ // Configure indexer options.
idxOpts := []indexer.IndexerOption{
indexer.WithAlgoliaClient(algo),
indexer.WithBaseURL(cfg.BaseURL),
indexer.WithDatabase(db),
indexer.WithDocumentTypes(cfg.DocumentTypes.DocumentType),
- indexer.WithDocumentsFolderID(cfg.GoogleWorkspace.DocsFolder),
- indexer.WithDraftsFolderID(cfg.GoogleWorkspace.DraftsFolder),
- indexer.WithGoogleWorkspaceService(goog),
indexer.WithLogger(log),
}
+
+ // Add SharePoint or Google Workspace-specific options.
+ if sharepointSvc != nil {
+ idxOpts = append(idxOpts,
+ indexer.WithSharePointService(sharepointSvc),
+ indexer.WithDocumentsFolderID(cfg.SharePoint.DocsFolder), // Use SharePoint DocsFolder
+ indexer.WithDraftsFolderID(cfg.SharePoint.DraftsFolder), // Use SharePoint DraftsFolder
+ )
+ } else {
+ idxOpts = append(idxOpts,
+ indexer.WithGoogleWorkspaceService(goog),
+ indexer.WithDocumentsFolderID(cfg.GoogleWorkspace.DocsFolder),
+ indexer.WithDraftsFolderID(cfg.GoogleWorkspace.DraftsFolder),
+ )
+ }
+
+ // Add additional indexer options.
if cfg.Indexer.MaxParallelDocs != 0 {
idxOpts = append(idxOpts,
indexer.WithMaxParallelDocuments(cfg.Indexer.MaxParallelDocs))
@@ -166,7 +196,9 @@ func (c *Command) Run(args []string) int {
idxOpts = append(idxOpts,
indexer.WithUseDatabaseForDocumentData(true))
}
- idx, err := indexer.NewIndexer(idxOpts...)
+
+ // Create the indexer.
+ idx, err := indexer.NewIndexer(cfg, idxOpts...)
if err != nil {
ui.Error(fmt.Sprintf("error creating indexer: %v", err))
return 1
diff --git a/internal/cmd/commands/operator/migrate_algolia_to_postgresql.go b/internal/cmd/commands/operator/migrate_algolia_to_postgresql.go
index 6ac1e259b..a9387c403 100644
--- a/internal/cmd/commands/operator/migrate_algolia_to_postgresql.go
+++ b/internal/cmd/commands/operator/migrate_algolia_to_postgresql.go
@@ -245,7 +245,7 @@ func migrateIndex(
// Convert document to a document database model.
dbDoc, reviews, err := doc.ToDatabaseModels(
- m.Config.DocumentTypes.DocumentType, m.Config.Products.Product)
+ m.Config.DocumentTypes.DocumentType, m.Config.Products.Product, false)
if err != nil {
m.Logger.Error("error converting document to database models",
"error", err,
@@ -283,7 +283,7 @@ func migrateIndex(
m.Logger.Error("error creating document",
"error", err,
)
- *m.DocsWithErrors = append(*m.DocsWithErrors, dbDoc.GoogleFileID)
+ *m.DocsWithErrors = append(*m.DocsWithErrors, dbDoc.GetFileIdentifier())
continue
}
@@ -294,7 +294,7 @@ func migrateIndex(
} else {
*m.DocsCreated += 1
- logArgs := []any{"document_id", dbDoc.GoogleFileID}
+ logArgs := []any{"document_id", dbDoc.GetFileIdentifier()}
// Log additional document information if the verbose flag is true.
if m.Verbose {
if err == nil {
@@ -317,7 +317,7 @@ func migrateIndex(
"error", err,
"document_id", doc.ObjectID,
)
- *m.DocsWithErrors = append(*m.DocsWithErrors, dbDoc.GoogleFileID)
+ *m.DocsWithErrors = append(*m.DocsWithErrors, dbDoc.GetFileIdentifier())
continue
}
} else {
@@ -345,7 +345,7 @@ func migrateIndex(
"error", err,
"document_id", doc.ObjectID,
)
- *m.DocsWithErrors = append(*m.DocsWithErrors, dbDoc.GoogleFileID)
+ *m.DocsWithErrors = append(*m.DocsWithErrors, dbDoc.GetFileIdentifier())
continue
}
@@ -376,7 +376,7 @@ func migrateIndex(
for revID, revName := range doc.FileRevisions {
frExists := false
for _, fr := range dbFileRevs {
- if fr.GoogleDriveFileRevisionID == revID && fr.Name == revName {
+ if fr.FileRevisionID == revID && fr.Name == revName {
frExists = true
break
}
@@ -386,8 +386,8 @@ func migrateIndex(
Document: models.Document{
GoogleFileID: doc.ObjectID,
},
- GoogleDriveFileRevisionID: revID,
- Name: revName,
+ FileRevisionID: revID,
+ Name: revName,
}
if err := fr.Create(tx); err != nil {
return fmt.Errorf("error creating new file revision: %w", err)
@@ -408,7 +408,7 @@ func migrateIndex(
"error", err,
"document_id", doc.ObjectID,
)
- *m.DocsWithErrors = append(*m.DocsWithErrors, dbDoc.GoogleFileID)
+ *m.DocsWithErrors = append(*m.DocsWithErrors, dbDoc.GetFileIdentifier())
continue
}
diff --git a/internal/cmd/commands/server/server.go b/internal/cmd/commands/server/server.go
index a25c73ebf..f5a00ada8 100644
--- a/internal/cmd/commands/server/server.go
+++ b/internal/cmd/commands/server/server.go
@@ -10,6 +10,7 @@ import (
"strings"
"time"
+ addin "github.com/hashicorp-forge/hermes/hermes-plugin"
"github.com/hashicorp-forge/hermes/internal/api"
apiv2 "github.com/hashicorp-forge/hermes/internal/api/v2"
"github.com/hashicorp-forge/hermes/internal/auth"
@@ -18,6 +19,7 @@ import (
"github.com/hashicorp-forge/hermes/internal/datadog"
"github.com/hashicorp-forge/hermes/internal/db"
"github.com/hashicorp-forge/hermes/internal/jira"
+ "github.com/hashicorp-forge/hermes/internal/middleware"
"github.com/hashicorp-forge/hermes/internal/pkg/doctypes"
"github.com/hashicorp-forge/hermes/internal/pub"
"github.com/hashicorp-forge/hermes/internal/server"
@@ -27,6 +29,7 @@ import (
hcd "github.com/hashicorp-forge/hermes/pkg/hashicorpdocs"
"github.com/hashicorp-forge/hermes/pkg/links"
"github.com/hashicorp-forge/hermes/pkg/models"
+ "github.com/hashicorp-forge/hermes/pkg/sharepointhelper"
"github.com/hashicorp-forge/hermes/web"
"github.com/hashicorp/go-hclog"
httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http"
@@ -40,9 +43,12 @@ type Command struct {
flagAddr string
flagBaseURL string
flagConfig string
- flagOktaAuthServerURL string
- flagOktaClientID string
- flagOktaDisabled bool
+ flagOidcAuthServerURL string
+ flagOidcClientID string
+ flagOidcDisabled bool
+ flagTLSEnabled bool
+ flagTLSCert string
+ flagTLSKey string
}
type endpoint struct {
@@ -68,23 +74,35 @@ func (c *Command) Flags() *base.FlagSet {
"[HERMES_SERVER_ADDR] Address to bind to for listening.",
)
f.StringVar(
- &c.flagBaseURL, "base-url", "http://localhost:8000",
+ &c.flagBaseURL, "base-url", "https://localhost:8443",
"[HERMES_BASE_URL] Base URL used for building links.",
)
f.StringVar(
&c.flagConfig, "config", "", "Path to Hermes config file",
)
f.StringVar(
- &c.flagOktaAuthServerURL, "okta-auth-server-url", "",
- "[HERMES_SERVER_OKTA_AUTH_SERVER_URL] URL to the Okta authorization server.",
+ &c.flagOidcAuthServerURL, "oidc-auth-server-url", "",
+ "[HERMES_SERVER_OIDC_AUTH_SERVER_URL] URL to the OIDC authorization server.",
)
f.StringVar(
- &c.flagOktaClientID, "okta-client-id", "",
- "[HERMES_SERVER_OKTA_CLIENT_ID] Okta client ID.",
+ &c.flagOidcClientID, "oidc-client-id", "",
+ "[HERMES_SERVER_OIDC_CLIENT_ID] OIDC client ID.",
)
f.BoolVar(
- &c.flagOktaDisabled, "okta-disabled", false,
- "[HERMES_SERVER_OKTA_DISABLED] Disable Okta authorization.",
+ &c.flagOidcDisabled, "oidc-disabled", false,
+ "[HERMES_SERVER_OIDC_DISABLED] Disable OIDC authorization.",
+ )
+ f.BoolVar(
+ &c.flagTLSEnabled, "tls-enabled", false,
+ "[HERMES_SERVER_TLS_ENABLED] Enable TLS/HTTPS for the server.",
+ )
+ f.StringVar(
+ &c.flagTLSCert, "tls-cert", "",
+ "[HERMES_SERVER_TLS_CERT] Path to TLS certificate file.",
+ )
+ f.StringVar(
+ &c.flagTLSKey, "tls-key", "",
+ "[HERMES_SERVER_TLS_KEY] Path to TLS private key file.",
)
return f
@@ -108,6 +126,12 @@ func (c *Command) Run(args []string) int {
err, c.flagConfig))
return 1
}
+ // Log configuration loaded successfully without exposing sensitive data
+ c.Log.Info("Configuration loaded successfully",
+ "config_file", c.flagConfig,
+ "log_format", cfg.LogFormat,
+ "server_addr", cfg.Server.Addr,
+ "base_url", cfg.BaseURL)
}
// Get configuration from environment variables if not set on the command
@@ -125,30 +149,50 @@ func (c *Command) Run(args []string) int {
if c.flagBaseURL != f.Lookup("base-url").DefValue {
cfg.BaseURL = c.flagBaseURL
}
- if val, ok := os.LookupEnv("HERMES_SERVER_OKTA_AUTH_SERVER_URL"); ok {
- cfg.Okta.AuthServerURL = val
+ if val, ok := os.LookupEnv("HERMES_SERVER_OIDC_AUTH_SERVER_URL"); ok {
+ cfg.OidcAlb.AuthServerURL = val
}
- if c.flagOktaAuthServerURL != f.Lookup("okta-auth-server-url").DefValue {
- cfg.Okta.AuthServerURL = c.flagOktaAuthServerURL
+ if c.flagOidcAuthServerURL != f.Lookup("oidc-auth-server-url").DefValue {
+ cfg.OidcAlb.AuthServerURL = c.flagOidcAuthServerURL
}
- if val, ok := os.LookupEnv("HERMES_SERVER_OKTA_CLIENT_ID"); ok {
- cfg.Okta.ClientID = val
+ if val, ok := os.LookupEnv("HERMES_SERVER_OIDC_CLIENT_ID"); ok {
+ cfg.OidcAlb.ClientID = val
}
- if c.flagOktaClientID != f.Lookup("okta-client-id").DefValue {
- cfg.Okta.ClientID = c.flagOktaClientID
+ if c.flagOidcClientID != f.Lookup("oidc-client-id").DefValue {
+ cfg.OidcAlb.ClientID = c.flagOidcClientID
}
- if val, ok := os.LookupEnv("HERMES_SERVER_OKTA_DISABLED"); ok {
+ if val, ok := os.LookupEnv("HERMES_SERVER_OIDC_DISABLED"); ok {
if val == "" || val == "false" {
- // Keep Okta enabled if the env var value is an empty string or "false".
+ // Keep OIDC ALB enabled if the env var value is an empty string or "false".
} else {
- cfg.Okta.Disabled = true
+ cfg.OidcAlb.Disabled = true
}
}
- if val, ok := os.LookupEnv("HERMES_SERVER_OKTA_JWT_SIGNER"); ok {
- cfg.Okta.JWTSigner = val
+ if val, ok := os.LookupEnv("HERMES_SERVER_OIDC_JWT_SIGNER"); ok {
+ cfg.OidcAlb.JWTSigner = val
}
- if c.flagOktaDisabled {
- cfg.Okta.Disabled = true
+ if c.flagOidcDisabled {
+ cfg.OidcAlb.Disabled = true
+ }
+
+ // Handle TLS configuration
+ if val, ok := os.LookupEnv("HERMES_SERVER_TLS_ENABLED"); ok {
+ cfg.Server.TLSEnabled = val == "true"
+ }
+ if c.flagTLSEnabled {
+ cfg.Server.TLSEnabled = true
+ }
+ if val, ok := os.LookupEnv("HERMES_SERVER_TLS_CERT"); ok {
+ cfg.Server.TLSCert = val
+ }
+ if c.flagTLSCert != "" {
+ cfg.Server.TLSCert = c.flagTLSCert
+ }
+ if val, ok := os.LookupEnv("HERMES_SERVER_TLS_KEY"); ok {
+ cfg.Server.TLSKey = val
+ }
+ if c.flagTLSKey != "" {
+ cfg.Server.TLSKey = c.flagTLSKey
}
// Validate feature flags defined in configuration
@@ -168,7 +212,6 @@ func (c *Command) Run(args []string) int {
}
}
- // Configure logger.
switch cfg.LogFormat {
case "json":
c.Log = hclog.New(&hclog.LoggerOptions{
@@ -181,23 +224,26 @@ func (c *Command) Run(args []string) int {
return 1
}
- // Build configuration for Okta authentication.
- if !cfg.Okta.Disabled {
- // Check for required Okta configuration.
- if cfg.Okta.AuthServerURL == "" {
- c.UI.Error("error initializing server: Okta authorization server URL is required")
+ // Log comprehensive configuration overview
+ logInstanceOverview(c.Log, cfg)
+
+ // Build configuration for OIDC ALB authentication.
+ if !cfg.OidcAlb.Disabled {
+ // Check for required OIDC ALB configuration.
+ if cfg.OidcAlb.AuthServerURL == "" {
+ c.UI.Error("error initializing server: OIDC ALB authorization server URL is required")
return 1
}
- if cfg.Okta.AWSRegion == "" {
- c.UI.Error("error initializing server: Okta AWS region is required")
+ if cfg.OidcAlb.AWSRegion == "" {
+ c.UI.Error("error initializing server: OIDC ALB AWS region is required")
return 1
}
- if cfg.Okta.ClientID == "" {
- c.UI.Error("error initializing server: Okta client ID is required")
+ if cfg.OidcAlb.ClientID == "" {
+ c.UI.Error("error initializing server: OIDC ALB client ID is required")
return 1
}
- if cfg.Okta.JWTSigner == "" {
- c.UI.Error("error initializing server: Okta JWT signer is required")
+ if cfg.OidcAlb.JWTSigner == "" {
+ c.UI.Error("error initializing server: OIDC ALB JWT signer is required")
return 1
}
}
@@ -238,11 +284,49 @@ func (c *Command) Run(args []string) int {
}
goog = gw.NewFromConfig(cfg.GoogleWorkspace.Auth)
- } else {
+ } else if cfg.GoogleWorkspace.OAuth2.ClientID != "" {
// Use OAuth if Google Workspace auth is not defined in the config.
goog = gw.New()
}
+ // Initialize SharePoint service.
+ var sharepointSvc *sharepointhelper.Service
+ if cfg.SharePoint != nil {
+ // Validate required SharePoint configuration.
+ if cfg.SharePoint.ClientID == "" {
+ c.UI.Error("error initializing server: SharePoint client ID is required")
+ return 1
+ }
+ if cfg.SharePoint.ClientSecret == "" {
+ c.UI.Error("error initializing server: SharePoint client secret is required")
+ return 1
+ }
+ if cfg.SharePoint.TenantID == "" {
+ c.UI.Error("error initializing server: SharePoint tenant ID is required")
+ return 1
+ }
+ if cfg.SharePoint.SiteID == "" {
+ c.UI.Error("error initializing server: SharePoint Site ID is required")
+ return 1
+ }
+ if cfg.SharePoint.DriveID == "" {
+ c.UI.Error("error initializing server: SharePoint Drive ID is required")
+ return 1
+ }
+
+ // Initialize the SharePoint service.
+ sharepointSvc = sharepointhelper.NewService(cfg.SharePoint, c.Log)
+
+ // Check for SharePoint token creation in initializing step.
+ _, err := sharepointSvc.GetToken()
+ if err != nil {
+ c.UI.Error(fmt.Sprintf("error initializing SharePoint service: %v", err))
+ return 1
+ }
+ c.Log.Info("Successfully initialized SharePoint service.")
+ }
+
+ c.Log.Info("Algolia client configuration", "application_id", cfg.Algolia.ApplicationID)
reqOpts := map[interface{}]string{
cfg.Algolia.ApplicationID: "Algolia Application ID is required",
cfg.Algolia.SearchAPIKey: "Algolia Search API Key is required",
@@ -344,6 +428,7 @@ func (c *Command) Run(args []string) int {
GWService: goog,
Jira: jiraSvc,
Logger: c.Log,
+ SharePoint: sharepointSvc,
}
// Define handlers for authenticated endpoints.
@@ -405,45 +490,81 @@ func (c *Command) Run(args []string) int {
{"/pub/", http.StripPrefix("/pub/", pub.Handler())},
}
- // Web endpoints are conditionally authenticated based on if Okta is enabled.
- webEndpoints := []endpoint{
+ // Web endpoints are conditionally authenticated based on if auth is enabled.
+ webEndpoints1 := []endpoint{
{"/", web.Handler()},
+ }
+ if cfg.SharePoint != nil {
+ webEndpoints1 = append(webEndpoints1, endpoint{"/addin/", addin.AddinHandler(c.Log)})
+ }
+ webEndpoints2 := []endpoint{
{"/api/v1/web/config", web.ConfigHandler(cfg, algoSearch, c.Log)},
{"/api/v2/web/config", web.ConfigHandler(cfg, algoSearch, c.Log)},
{"/l/", links.RedirectHandler(algoSearch, cfg.Algolia, c.Log)},
}
- // If Okta is enabled, add the web endpoints for the single page app as
- // authenticated endpoints.
- if cfg.Okta != nil && !cfg.Okta.Disabled {
- authenticatedEndpoints = append(authenticatedEndpoints, webEndpoints...)
+ // Determine if authentication is enabled (via OidcAlb or Okta).
+ authEnabled := (cfg.OidcAlb != nil && !cfg.OidcAlb.Disabled) ||
+ (cfg.Okta != nil && !cfg.Okta.Disabled) ||
+ cfg.SharePoint != nil
+
+ if authEnabled {
+ // If auth is enabled, add the web SPA endpoints as authenticated
+ // endpoints.
+ authenticatedEndpoints = append(authenticatedEndpoints, webEndpoints1...)
} else {
- // If Okta is disabled, we need to add the web endpoints for the SPA as
- // unauthenticated endpoints so the application will load.
- unauthenticatedEndpoints = append(unauthenticatedEndpoints, webEndpoints...)
+ // If auth is disabled, add the web SPA endpoints as unauthenticated
+ // endpoints so the application will load.
+ unauthenticatedEndpoints = append(unauthenticatedEndpoints, webEndpoints1...)
}
+ // Config and redirect endpoints are always unauthenticated.
+ unauthenticatedEndpoints = append(unauthenticatedEndpoints, webEndpoints2...)
// Register handlers.
for _, e := range authenticatedEndpoints {
mux.Handle(
e.pattern,
- auth.AuthenticateRequest(*cfg, goog, c.Log, e.handler),
+ auth.AuthenticateRequest(*cfg, goog, sharepointSvc, c.Log, e.handler),
)
}
for _, e := range unauthenticatedEndpoints {
mux.Handle(e.pattern, e.handler)
}
+ // Use dev_mode flag from configuration
+ isDevelopment := cfg.Server.DevMode
+
+ if isDevelopment {
+ c.Log.Info("Running in development mode - permissive CORS policy enabled")
+ } else {
+ c.Log.Info("Running in production mode - restrictive CORS policy enabled")
+ } // Wrap the entire mux with CORS middleware
+ corsHandler := middleware.CorsMiddlewareWithConfig(c.Log, mux, isDevelopment)
+
server := &http.Server{
Addr: cfg.Server.Addr,
- Handler: mux,
+ Handler: corsHandler,
}
go func() {
- c.Log.Info(fmt.Sprintf("listening on %s...", cfg.Server.Addr))
+ if cfg.Server.TLSEnabled {
+ c.Log.Info("Starting server with TLS/HTTPS", "addr", cfg.Server.Addr, "tls_enabled", true)
+
+ if cfg.Server.TLSCert == "" || cfg.Server.TLSKey == "" {
+ c.Log.Error("TLS is enabled but certificate or key file path is not specified")
+ os.Exit(1)
+ }
+
+ if err := server.ListenAndServeTLS(cfg.Server.TLSCert, cfg.Server.TLSKey); err != http.ErrServerClosed {
+ c.Log.Error("Error starting TLS listener", "error", err, "addr", cfg.Server.Addr)
+ os.Exit(1)
+ }
+ } else {
+ c.Log.Info("Starting server", "addr", cfg.Server.Addr, "tls_enabled", false)
- if err := server.ListenAndServe(); err != http.ErrServerClosed {
- c.Log.Error(fmt.Sprintf("error starting listener: %v", err))
- os.Exit(1)
+ if err := server.ListenAndServe(); err != http.ErrServerClosed {
+ c.Log.Error("Error starting listener", "error", err, "addr", cfg.Server.Addr)
+ os.Exit(1)
+ }
}
}()
@@ -572,3 +693,112 @@ func registerProducts(
return nil
}
+
+// logInstanceOverview logs a comprehensive overview of the running instance configuration
+// without exposing sensitive data
+func logInstanceOverview(log hclog.Logger, cfg *config.Config) {
+ // Determine authentication method
+ var authMethod string
+ var authDetails []interface{}
+
+ if cfg.OidcAlb != nil && !cfg.OidcAlb.Disabled {
+ authMethod = "OIDC ALB"
+ authDetails = []interface{}{
+ "auth_server_configured", cfg.OidcAlb.AuthServerURL != "",
+ "client_id_configured", cfg.OidcAlb.ClientID != "",
+ "aws_region", cfg.OidcAlb.AWSRegion,
+ }
+ } else if cfg.Okta != nil && !cfg.Okta.Disabled {
+ authMethod = "Okta ALB"
+ authDetails = []interface{}{
+ "auth_server_configured", cfg.Okta.AuthServerURL != "",
+ "client_id_configured", cfg.Okta.ClientID != "",
+ "aws_region", cfg.Okta.AWSRegion,
+ }
+ } else if cfg.SharePoint != nil {
+ authMethod = "Microsoft Auth / SharePoint"
+ authDetails = []interface{}{
+ "sharepoint_configured", true,
+ }
+ authDetails = append(authDetails,
+ "tenant_id_configured", cfg.SharePoint.TenantID != "",
+ "site_id_configured", cfg.SharePoint.SiteID != "",
+ "drive_id_configured", cfg.SharePoint.DriveID != "",
+ "domain", cfg.SharePoint.Domain,
+ )
+ } else {
+ authMethod = "Google OAuth"
+ authDetails = []interface{}{
+ "google_workspace_configured", cfg.GoogleWorkspace != nil,
+ }
+ if cfg.GoogleWorkspace != nil {
+ authDetails = append(authDetails,
+ "gw_domain", cfg.GoogleWorkspace.Domain,
+ "oauth2_configured", cfg.GoogleWorkspace.OAuth2 != nil,
+ )
+ }
+ }
+
+ // Determine mode (development vs production)
+ mode := "production"
+ if cfg.Server != nil && cfg.Server.DevMode {
+ mode = "development"
+ }
+
+ // Log format determination
+ logFormat := "standard"
+ if cfg.LogFormat == "json" {
+ logFormat = "json"
+ } else if cfg.LogFormat != "" {
+ logFormat = cfg.LogFormat
+ }
+
+ // Main configuration overview
+ configFields := []interface{}{
+ "instance_mode", mode,
+ "server_addr", cfg.Server.Addr,
+ "base_url", cfg.BaseURL,
+ "shortener_base_url", cfg.ShortenerBaseURL,
+ "log_format", logFormat,
+ "tls_enabled", cfg.Server != nil && cfg.Server.TLSEnabled,
+ "auth_method", authMethod,
+ }
+
+ // Add auth-specific details
+ configFields = append(configFields, authDetails...)
+
+ // Service configurations
+ configFields = append(configFields,
+ "algolia_configured", cfg.Algolia != nil && cfg.Algolia.ApplicationID != "",
+ "google_workspace_configured", cfg.GoogleWorkspace != nil && cfg.GoogleWorkspace.Domain != "",
+ "email_enabled", cfg.Email != nil && cfg.Email.Enabled,
+ "jira_enabled", cfg.Jira != nil && cfg.Jira.Enabled,
+ "datadog_enabled", cfg.Datadog != nil && cfg.Datadog.Enabled,
+ )
+
+ // Google Workspace details (if configured)
+ if cfg.GoogleWorkspace != nil {
+ configFields = append(configFields,
+ "gw_domain", cfg.GoogleWorkspace.Domain,
+ "gw_docs_folder", cfg.GoogleWorkspace.DocsFolder,
+ "gw_drafts_folder", cfg.GoogleWorkspace.DraftsFolder,
+ "gw_shortcuts_enabled", cfg.GoogleWorkspace.CreateDocShortcuts,
+ )
+ }
+
+ // Document types count
+ if cfg.DocumentTypes != nil {
+ configFields = append(configFields,
+ "document_types_count", len(cfg.DocumentTypes.DocumentType),
+ )
+ }
+
+ // Products count
+ if cfg.Products != nil {
+ configFields = append(configFields,
+ "products_count", len(cfg.Products.Product),
+ )
+ }
+
+ log.Info("=== Hermes Instance Configuration Overview ===", configFields...)
+}
diff --git a/internal/cmd/main.go b/internal/cmd/main.go
index fbe0adc1e..441fbfc18 100644
--- a/internal/cmd/main.go
+++ b/internal/cmd/main.go
@@ -14,7 +14,11 @@ func Main(args []string) int {
cliName := args[0]
log := hclog.New(&hclog.LoggerOptions{
- Name: cliName,
+ Name: cliName,
+ Level: hclog.Debug, // Lower level to see more logs
+ Output: os.Stdout, // Use stdout instead of stderr
+ Color: hclog.AutoColor, // Colorize logs for better readability
+ JSONFormat: false, // Human-readable instead of JSON
})
if len(args) == 2 &&
diff --git a/internal/config/auth.go b/internal/config/auth.go
new file mode 100644
index 000000000..ca975babc
--- /dev/null
+++ b/internal/config/auth.go
@@ -0,0 +1,7 @@
+package config
+
+// Auth contains authentication configurations.
+type Auth struct {
+ // Microsoft contains the Microsoft authentication configuration.
+ Microsoft *MicrosoftAuth `hcl:"microsoft,block"`
+}
diff --git a/internal/config/config.go b/internal/config/config.go
index 29e3a3ced..7b09952f7 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -3,12 +3,30 @@ package config
import (
"fmt"
+ "github.com/hashicorp-forge/hermes/internal/auth/oidcalb"
"github.com/hashicorp-forge/hermes/internal/auth/oktaalb"
"github.com/hashicorp-forge/hermes/pkg/algolia"
gw "github.com/hashicorp-forge/hermes/pkg/googleworkspace"
"github.com/hashicorp/hcl/v2/hclsimple"
)
+type SharePointConfig struct {
+ ClientID string `hcl:"client_id"`
+ ClientSecret string `hcl:"client_secret"`
+ RedirectURI string `hcl:"redirect_uri"`
+ TenantID string `hcl:"tenant_id"`
+ SiteID string `hcl:"site_id"`
+ DriveID string `hcl:"drive_id"`
+ Domain string `hcl:"domain"` // Email Domain
+ DocsFolder string `hcl:"docs_folder"` // Folder for published documents
+ DraftsFolder string `hcl:"drafts_folder"` // Folder for draft documents
+ ShortcutsFolder string `hcl:"shortcuts_folder"` // Folder for document shortcuts
+
+ // GroupApprovals is the configuration for using Microsoft distribution lists as
+ // document approvers.
+ GroupApprovals *SharePointGroupApprovals `hcl:"group_approvals,block"`
+}
+
// Config contains the Hermes configuration.
type Config struct {
// Algolia configures Hermes to work with Algolia.
@@ -35,6 +53,8 @@ type Config struct {
// GoogleWorkspace configures Hermes to work with Google Workspace.
GoogleWorkspace *GoogleWorkspace `hcl:"google_workspace,block"`
+ SharePoint *SharePointConfig `hcl:"sharepoint,block"`
+
// Indexer contains the configuration for the Hermes indexer.
Indexer *Indexer `hcl:"indexer,block"`
@@ -45,7 +65,11 @@ type Config struct {
// "json".
LogFormat string `hcl:"log_format,optional"`
- // Okta configures Hermes to work with Okta.
+ // OidcAlb configures Hermes to work with OIDC ALB authentication (supports any OIDC provider).
+ OidcAlb *oidcalb.Config `hcl:"oidc_alb,block"`
+
+ // Okta configures Hermes to work with Okta (deprecated: use oidc_alb instead).
+ // Kept for backward compatibility with existing Google Hermes deployments.
Okta *oktaalb.Config `hcl:"okta,block"`
// Products contain available products.
@@ -108,6 +132,10 @@ type DocumentType struct {
// document type.
Template string `hcl:"template"`
+ // MSTemplate is the Microsoft file path or ID for the document template used for this
+ // document type.
+ MSTemplate string `hcl:"ms_template,optional"`
+
// MoreInfoLink defines a link to more info for the document type.
// Example: "When should I create an RFC?"
MoreInfoLink *DocumentTypeLink `hcl:"more_info_link,block" json:"moreInfoLink"`
@@ -162,6 +190,25 @@ type Email struct {
// FromAddress is the email address to send emails from.
FromAddress string `hcl:"from_address,optional"`
+
+ // BCCBatchSize is the maximum number of BCC recipients per email.
+ BCCBatchSize int `hcl:"bcc_batch_size,optional"`
+
+ // Retry configures email retry behavior.
+ Retry *EmailRetry `hcl:"retry,block"`
+}
+
+// EmailRetry configures email retry behavior with exponential backoff.
+type EmailRetry struct {
+ // MaxAttempts is the maximum number of send attempts (including initial attempt).
+ MaxAttempts int `hcl:"max_attempts,optional"`
+
+ // InitialDelayMinutes is the delay before the first retry in minutes.
+ // Subsequent retries use exponential backoff.
+ InitialDelayMinutes int `hcl:"initial_delay_minutes,optional"`
+
+ // FinalDelayMinutes is the delay before the final retry attempt in minutes.
+ FinalDelayMinutes int `hcl:"final_delay_minutes,optional"`
}
// FeatureFlags contain available feature flags.
@@ -252,6 +299,16 @@ type GoogleWorkspaceGroupApprovals struct {
SearchPrefix string `hcl:"search_prefix,optional"`
}
+// SharePointGroupApprovals is the configuration for using Microsoft distribution lists as
+// document approvers.
+type SharePointGroupApprovals struct {
+ // Enabled enables using Microsoft distribution lists as document approvers.
+ Enabled bool `hcl:"enabled,optional"`
+
+ // SearchPrefix is the prefix to use when searching for distribution lists.
+ SearchPrefix string `hcl:"search_prefix,optional"`
+}
+
// GoogleWorkspaceOAuth2 is the configuration to use OAuth 2.0 to access Google
// Workspace APIs.
type GoogleWorkspaceOAuth2 struct {
@@ -333,9 +390,15 @@ type Product struct {
type Server struct {
// Addr is the address to bind to for listening.
Addr string `hcl:"addr,optional"`
-}
-// NewConfig parses an HCL configuration file and returns the Hermes config.
+ // DevMode enables development mode with permissive CORS policy
+ DevMode bool `hcl:"dev_mode,optional"`
+
+ // TLS configuration
+ TLSEnabled bool `hcl:"tls_enabled,optional"`
+ TLSCert string `hcl:"tls_cert,optional"`
+ TLSKey string `hcl:"tls_key,optional"`
+} // NewConfig parses an HCL configuration file and returns the Hermes config.
func NewConfig(filename string) (*Config, error) {
c := &Config{
Algolia: &algolia.Config{},
@@ -343,7 +406,7 @@ func NewConfig(filename string) (*Config, error) {
FeatureFlags: &FeatureFlags{},
GoogleWorkspace: &GoogleWorkspace{},
Indexer: &Indexer{},
- Okta: &oktaalb.Config{},
+ OidcAlb: &oidcalb.Config{},
Server: &Server{},
}
err := hclsimple.DecodeFile(filename, nil, c)
diff --git a/internal/config/microsoft_auth.go b/internal/config/microsoft_auth.go
new file mode 100644
index 000000000..073e0e8f7
--- /dev/null
+++ b/internal/config/microsoft_auth.go
@@ -0,0 +1,9 @@
+package config
+
+// MicrosoftAuth contains the configuration for Microsoft authentication.
+type MicrosoftAuth struct {
+ ClientID string `hcl:"client_id"` // App Registration client ID
+ ClientSecret string `hcl:"client_secret"` // App Registration client secret
+ TenantID string `hcl:"tenant_id"` // Directory (tenant) ID
+ RedirectURI string `hcl:"redirect_uri"` // Redirect URI
+}
diff --git a/internal/config/microsoft_graph.go b/internal/config/microsoft_graph.go
new file mode 100644
index 000000000..dcc458b17
--- /dev/null
+++ b/internal/config/microsoft_graph.go
@@ -0,0 +1,22 @@
+package config
+
+// MicrosoftGraphConfig contains the configuration for Microsoft Graph API.
+type MicrosoftGraphConfig struct {
+ // SiteID is the SharePoint site ID.
+ SiteID string `hcl:"site_id"`
+
+ // DriveID is the SharePoint drive ID.
+ DriveID string `hcl:"drive_id"`
+
+ // DocsFolder is the folder for published documents.
+ DocsFolder string `hcl:"docs_folder"`
+
+ // DraftsFolder is the folder for draft documents.
+ DraftsFolder string `hcl:"drafts_folder"`
+
+ // TemporaryDraftsFolder is the folder for temporary draft documents.
+ TemporaryDraftsFolder string `hcl:"temporary_drafts_folder"`
+
+ // ShortcutsFolder is the folder for shortcuts.
+ ShortcutsFolder string `hcl:"shortcuts_folder"`
+}
diff --git a/internal/email/email.go b/internal/email/email.go
index 06993630a..b4a388594 100644
--- a/internal/email/email.go
+++ b/internal/email/email.go
@@ -4,17 +4,24 @@ import (
"bytes"
"embed"
"fmt"
+ "html/template"
"strings"
- "text/template"
"time"
validation "github.com/go-ozzo/ozzo-validation/v4"
- gw "github.com/hashicorp-forge/hermes/pkg/googleworkspace"
)
//go:embed templates/*
var tmplFS embed.FS
+// EmailSender is an interface for sending emails.
+// Both *sharepointhelper.Service and *googleworkspace.Service satisfy this
+// (Google's SendEmail returns (*gmail.Message, error) so we use a wrapper).
+type EmailSender interface {
+ SendEmail(to []string, from, subject, body string) error
+ SendEmailWithBCC(to []string, bcc []string, from, subject, body string) error
+}
+
type User struct {
EmailAddress string
Name string
@@ -73,11 +80,37 @@ type SubscriberDocumentPublishedEmailData struct {
Product string
}
+type ContributorAddedEmailData struct {
+ BaseURL string
+ CurrentYear int
+ DocumentOwner string
+ DocumentShortName string
+ DocumentTitle string
+ DocumentType string
+ DocumentStatus string
+ DocumentStatusClass string
+ DocumentURL string
+ Product string
+}
+
+type StakeholderAddedEmailData struct {
+ BaseURL string
+ CurrentYear int
+ DocumentOwner string
+ DocumentShortName string
+ DocumentTitle string
+ DocumentType string
+ DocumentStatus string
+ DocumentStatusClass string
+ DocumentURL string
+ Product string
+}
+
func SendDocumentApprovedEmail(
data DocumentApprovedEmailData,
to []string,
from string,
- svc *gw.Service,
+ svc EmailSender,
) error {
// Validate data.
if err := validation.ValidateStruct(&data,
@@ -126,7 +159,7 @@ func SendDocumentApprovedEmail(
)
// Send email.
- _, err = svc.SendEmail(
+ err = svc.SendEmail(
to,
from,
subject,
@@ -139,7 +172,7 @@ func SendNewOwnerEmail(
data NewOwnerEmailData,
to []string,
from string,
- svc *gw.Service,
+ svc EmailSender,
) error {
// Validate data.
if err := validation.ValidateStruct(&data,
@@ -184,7 +217,7 @@ func SendNewOwnerEmail(
}
// Send email.
- _, err = svc.SendEmail(
+ err = svc.SendEmail(
to,
from,
fmt.Sprintf("%s transferred to you", data.DocumentShortName),
@@ -197,7 +230,7 @@ func SendReviewRequestedEmail(
d ReviewRequestedEmailData,
to []string,
from string,
- s *gw.Service,
+ s EmailSender,
) error {
// Validate data.
if err := validation.ValidateStruct(&d,
@@ -228,7 +261,7 @@ func SendReviewRequestedEmail(
return fmt.Errorf("error executing template: %w", err)
}
- _, err = s.SendEmail(
+ err = s.SendEmail(
to,
from,
fmt.Sprintf("Document review requested for %s", d.DocumentShortName),
@@ -241,7 +274,17 @@ func SendSubscriberDocumentPublishedEmail(
d SubscriberDocumentPublishedEmailData,
to []string,
from string,
- s *gw.Service,
+ s EmailSender,
+) error {
+ return SendSubscriberDocumentPublishedEmailWithBCC(d, nil, to, from, s)
+}
+
+func SendSubscriberDocumentPublishedEmailWithBCC(
+ d SubscriberDocumentPublishedEmailData,
+ toRecipients []string,
+ bccRecipients []string,
+ from string,
+ s EmailSender,
) error {
// Validate data.
if err := validation.ValidateStruct(&d,
@@ -268,8 +311,9 @@ func SendSubscriberDocumentPublishedEmail(
return fmt.Errorf("error executing template: %w", err)
}
- _, err = s.SendEmail(
- to,
+ err = s.SendEmailWithBCC(
+ toRecipients,
+ bccRecipients,
from,
fmt.Sprintf("New %s: [%s] %s",
d.DocumentType,
@@ -281,6 +325,97 @@ func SendSubscriberDocumentPublishedEmail(
return err
}
+func SendContributorAddedEmail(
+ data ContributorAddedEmailData,
+ to []string,
+ from string,
+ s EmailSender,
+) error {
+ if err := validation.ValidateStruct(&data,
+ validation.Field(&data.BaseURL, validation.Required),
+ validation.Field(&data.DocumentOwner, validation.Required),
+ validation.Field(&data.DocumentTitle, validation.Required),
+ validation.Field(&data.DocumentURL, validation.Required),
+ validation.Field(&data.Product, validation.Required),
+ validation.Field(&data.DocumentStatus, validation.Required),
+ validation.Field(&data.DocumentType, validation.Required),
+ ); err != nil {
+ return fmt.Errorf("error validating email data: %w", err)
+ }
+
+ var body bytes.Buffer
+ tmpl, err := template.ParseFS(
+ tmplFS, "templates/contributor-added.html")
+ if err != nil {
+ return fmt.Errorf("error parsing template: %w", err)
+ }
+
+ data.CurrentYear = time.Now().Year()
+ data.DocumentStatusClass = dasherizeStatus(data.DocumentStatus)
+
+ if err := tmpl.Execute(&body, data); err != nil {
+ return fmt.Errorf("error executing template: %w", err)
+ }
+
+ err = s.SendEmail(
+ to,
+ from,
+ fmt.Sprintf("You've been added as contributor to %s",
+ data.DocumentShortName,
+ ),
+ body.String(),
+ )
+ return err
+}
+
func dasherizeStatus(status string) string {
return strings.ReplaceAll(strings.ToLower(status), " ", "-")
}
+
+func SendStakeholderAddedEmail(
+ data StakeholderAddedEmailData,
+ to []string,
+ from string,
+ s EmailSender,
+) error {
+ if err := validation.ValidateStruct(&data,
+ validation.Field(&data.BaseURL, validation.Required),
+ validation.Field(&data.DocumentOwner, validation.Required),
+ validation.Field(&data.DocumentTitle, validation.Required),
+ validation.Field(&data.DocumentURL, validation.Required),
+ validation.Field(&data.Product, validation.Required),
+ validation.Field(&data.DocumentStatus, validation.Required),
+ validation.Field(&data.DocumentType, validation.Required),
+ ); err != nil {
+ return fmt.Errorf("error validating email data: %w", err)
+ }
+
+ var body bytes.Buffer
+ tmpl, err := template.ParseFS(
+ tmplFS, "templates/stakeholder-added.html")
+ if err != nil {
+ return fmt.Errorf("error parsing template: %w", err)
+ }
+
+ data.CurrentYear = time.Now().Year()
+ data.DocumentStatusClass = dasherizeStatus(data.DocumentStatus)
+
+ if err := tmpl.Execute(&body, data); err != nil {
+ return fmt.Errorf("error executing template: %w", err)
+ }
+
+ subject := fmt.Sprintf("You've been added as stakeholder to %s",
+ data.DocumentShortName,
+ )
+
+ if err := s.SendEmail(
+ to,
+ from,
+ subject,
+ body.String(),
+ ); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/internal/email/templates/contributor-added.html b/internal/email/templates/contributor-added.html
new file mode 100644
index 000000000..cb24ba49d
--- /dev/null
+++ b/internal/email/templates/contributor-added.html
@@ -0,0 +1,420 @@
+
+
+
+
+
+ You've been added as contributor to {{.DocumentTitle}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You've been added as a contributor.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{.DocumentOwner}} has added you as a contributor to this document.
+ As a contributor, you have access to edit and collaborate on this document. You can
+ view the document in Hermes to get started.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/internal/email/templates/stakeholder-added.html b/internal/email/templates/stakeholder-added.html
new file mode 100644
index 000000000..708211e7c
--- /dev/null
+++ b/internal/email/templates/stakeholder-added.html
@@ -0,0 +1,420 @@
+
+
+
+
+
+ You've been added as stakeholder to {{.DocumentTitle}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You've been added as a stakeholder.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{.DocumentOwner}} has added you as a stakeholder to this document.
+ As a stakeholder, you will be kept informed about important updates and decisions. You can
+ view the document in Hermes to stay up to date.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/internal/helpers/email.go b/internal/helpers/email.go
new file mode 100644
index 000000000..80502ac2f
--- /dev/null
+++ b/internal/helpers/email.go
@@ -0,0 +1,97 @@
+package helpers
+
+import (
+ "math"
+ "net/http"
+ "time"
+
+ "github.com/hashicorp-forge/hermes/internal/server"
+)
+
+// SendEmailWithRetry attempts to send an email with exponential backoff retry logic.
+// It will retry failed email sends up to maxAttempts times (configurable via srv.Config.Email.Retry).
+//
+// Retry delays use exponential backoff: initialDelay * 2^(attempt-1) minutes, with the final
+// retry using a configurable finalDelay. Delays can be interrupted if the goroutine is cancelled.
+
+func SendEmailWithRetry(
+ srv *server.Server,
+ emailFunc func() error,
+ docID, operation string,
+ r *http.Request,
+) {
+ maxAttempts := 5
+ initialDelayMinutes := 1
+ finalDelayMinutes := 60
+
+ if srv.Config != nil && srv.Config.Email != nil && srv.Config.Email.Retry != nil {
+ if srv.Config.Email.Retry.MaxAttempts > 0 {
+ maxAttempts = srv.Config.Email.Retry.MaxAttempts
+ }
+ if srv.Config.Email.Retry.InitialDelayMinutes > 0 {
+ initialDelayMinutes = srv.Config.Email.Retry.InitialDelayMinutes
+ }
+ if srv.Config.Email.Retry.FinalDelayMinutes > 0 {
+ finalDelayMinutes = srv.Config.Email.Retry.FinalDelayMinutes
+ }
+ }
+
+ maxRetries := maxAttempts - 1
+ var err error
+
+ for attempt := 0; attempt <= maxRetries; attempt++ {
+ err = emailFunc()
+ if err == nil {
+ if attempt > 0 {
+ srv.Logger.Info("email sent after retry",
+ "doc_id", docID,
+ "operation", operation,
+ "attempt", attempt+1,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ }
+ return
+ }
+
+ srv.Logger.Warn("email send failed",
+ "error", err,
+ "doc_id", docID,
+ "operation", operation,
+ "attempt", attempt+1,
+ "max_attempts", maxAttempts,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+
+ if attempt == maxRetries {
+ srv.Logger.Error("email send failed after all retries",
+ "error", err,
+ "doc_id", docID,
+ "operation", operation,
+ "total_attempts", maxAttempts,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+ return
+ }
+
+ var delay time.Duration
+ if attempt == maxRetries-1 {
+ delay = time.Duration(finalDelayMinutes) * time.Minute
+ } else {
+ delay = time.Duration(initialDelayMinutes*int(math.Pow(2, float64(attempt)))) * time.Minute
+ }
+
+ srv.Logger.Info("retrying email send",
+ "doc_id", docID,
+ "operation", operation,
+ "delay", delay.String(),
+ "next_attempt", attempt+2,
+ "method", r.Method,
+ "path", r.URL.Path,
+ )
+
+ time.Sleep(delay)
+ }
+}
diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go
index 074e3c2a2..430c28004 100644
--- a/internal/helpers/helpers.go
+++ b/internal/helpers/helpers.go
@@ -1,5 +1,7 @@
package helpers
+import "strings"
+
// RemoveStringSliceDuplicates removes duplicate strings from a string slice.
func RemoveStringSliceDuplicates(in []string) []string {
keys := make(map[string]bool)
@@ -23,3 +25,14 @@ func StringSliceContains(values []string, s string) bool {
}
return false
}
+
+// StringSliceContainsFold returns true if a string is present in a slice of
+// strings using case-insensitive comparison.
+func StringSliceContainsFold(values []string, s string) bool {
+ for _, v := range values {
+ if strings.EqualFold(s, v) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/helpers/helpers_test.go b/internal/helpers/helpers_test.go
index ea50a0fbc..f9dd8f2d5 100644
--- a/internal/helpers/helpers_test.go
+++ b/internal/helpers/helpers_test.go
@@ -58,3 +58,29 @@ func TestStringSliceContains(t *testing.T) {
assert.Equal(t, tc.want, got)
}
}
+
+func TestStringSliceContainsFold(t *testing.T) {
+ type testCase struct {
+ values []string
+ s string
+ want bool
+ }
+
+ testCases := []testCase{
+ {
+ values: []string{"hello", "world", "gopher"},
+ s: "WORLD",
+ want: true,
+ },
+ {
+ values: []string{"hello", "world", "gopher"},
+ s: "foo",
+ want: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ got := StringSliceContainsFold(tc.values, tc.s)
+ assert.Equal(t, tc.want, got)
+ }
+}
diff --git a/internal/indexer/indexer.go b/internal/indexer/indexer.go
index b77478717..1a66a2dac 100644
--- a/internal/indexer/indexer.go
+++ b/internal/indexer/indexer.go
@@ -17,6 +17,8 @@ import (
"github.com/hashicorp-forge/hermes/pkg/models"
"github.com/hashicorp/go-hclog"
"gorm.io/gorm"
+
+ sp "github.com/hashicorp-forge/hermes/pkg/sharepointhelper"
)
const (
@@ -41,9 +43,9 @@ type Indexer struct {
Database *gorm.DB
// DocumentsFolderID is the Google Drive ID of the folder containing published
- // documents to index.
DocumentsFolderID string
+ SharePointDriveID string
// DocumentTypes are a slice of document types from the application config.
DocumentTypes []*config.DocumentType
@@ -70,8 +72,12 @@ type Indexer struct {
// UseDatabaseForDocumentData will use the database instead of Algolia as the
// source of truth for document data, if true.
UseDatabaseForDocumentData bool
+
+ // sharepointSvc is the SharePoint service.
+ sharepointSvc *sp.Service
}
+// IndexerOption defines a functional option for configuring the Indexer.
type IndexerOption func(*Indexer)
// safeTime is a time.Time that is safe to use concurrently.
@@ -81,7 +87,7 @@ type safeTime struct {
}
// NewIndexer creates a new indexer.
-func NewIndexer(opts ...IndexerOption) (*Indexer, error) {
+func NewIndexer(cfg *config.Config, opts ...IndexerOption) (*Indexer, error) {
// Initialize a new indexer with defaults.
idx := &Indexer{
Logger: hclog.New(&hclog.LoggerOptions{
@@ -94,25 +100,38 @@ func NewIndexer(opts ...IndexerOption) (*Indexer, error) {
opt(idx)
}
- // Validate indexer configuration.
+ // Validate configuration
if err := idx.validate(); err != nil {
return nil, err
}
+ // Load SharePointDriveID from configuration if SharePoint is configured.
+ if cfg.SharePoint != nil {
+ idx.SharePointDriveID = cfg.SharePoint.DriveID
+ }
+
return idx, nil
}
// validate validates the indexer configuration.
func (idx *Indexer) validate() error {
- return validation.ValidateStruct(idx,
+ if err := validation.ValidateStruct(idx,
validation.Field(&idx.AlgoliaClient, validation.Required),
validation.Field(&idx.BaseURL, validation.Required),
validation.Field(&idx.Database, validation.Required),
- validation.Field(&idx.DocumentsFolderID, validation.Required),
validation.Field(&idx.DocumentTypes, validation.Required),
validation.Field(&idx.DraftsFolderID, validation.Required),
- validation.Field(&idx.GoogleWorkspaceService, validation.Required),
- )
+ validation.Field(&idx.DocumentsFolderID, validation.Required),
+ ); err != nil {
+ return err
+ }
+
+ // Ensure at least one backend service is configured.
+ if idx.sharepointSvc == nil && idx.GoogleWorkspaceService == nil {
+ return fmt.Errorf("either SharePoint or Google Workspace service must be configured")
+ }
+
+ return nil
}
// WithAlgoliaClient sets the Algolia client.
@@ -201,9 +220,35 @@ func WithUseDatabaseForDocumentData(u bool) IndexerOption {
}
}
+// WithSharePointService sets the SharePoint service for the Indexer.
+func WithSharePointService(sharepointSvc *sp.Service) IndexerOption {
+ return func(i *Indexer) {
+ i.sharepointSvc = sharepointSvc
+ }
+}
+
// Run runs the indexer.
// TODO: improve error handling.
func (idx *Indexer) Run() error {
+ log := idx.Logger
+
+ if idx.sharepointSvc != nil {
+ // SharePoint indexing loop.
+ for {
+ if err := idx.runSharePoint(); err != nil {
+ log.Error("SharePoint indexing error", "error", err)
+ }
+ log.Info("sleeping for a minute before the next indexing run...")
+ time.Sleep(1 * time.Minute)
+ }
+ } else {
+ // Google Workspace indexing loop.
+ return idx.runGoogleWorkspace()
+ }
+}
+
+// runGoogleWorkspace runs the Google Workspace indexing loop.
+func (idx *Indexer) runGoogleWorkspace() error {
algo := idx.AlgoliaClient
db := idx.Database
gwSvc := idx.GoogleWorkspaceService
@@ -236,7 +281,7 @@ func (idx *Indexer) Run() error {
// Get drafts folder data (headers) from the database.
// Note: we add a "refreshHeaders:" prefix for the Google Drive ID here
- // to not conflict with with the actual last indexed time of the folder
+ // to not conflict with the actual last indexed time of the folder
// (if we're indexing it, that is).
fd := models.IndexerFolder{
GoogleDriveID: fmt.Sprintf("refreshHeaders:%s", idx.DraftsFolderID),
@@ -296,7 +341,7 @@ func (idx *Indexer) Run() error {
// Get documents folder data (headers) from the database.
// Note: we add a "refreshHeaders:" prefix for the Google Drive ID here
- // to not conflict with with the actual last indexed time of the folder
+ // to not conflict with the actual last indexed time of the folder
// (if we're indexing it, that is).
fd := models.IndexerFolder{
GoogleDriveID: fmt.Sprintf("refreshHeaders:%s", idx.DocumentsFolderID),
@@ -546,6 +591,94 @@ func (idx *Indexer) Run() error {
}
}
+// --- SharePoint indexing logic ---
+func (idx *Indexer) runSharePoint() error {
+ runStartedAt := time.Now().UTC()
+
+ // Metadata tracking
+ md := models.IndexerMetadata{}
+ if err := md.Get(idx.Database); err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ md.LastFullIndexAt = time.Unix(0, 0).UTC()
+ } else {
+ return fmt.Errorf("error getting indexer metadata: %w", err)
+ }
+ }
+
+ // Drafts
+ if idx.UpdateDraftHeaders {
+ if err := idx.refreshSharePointFolder(idx.DraftsFolderID, "drafts"); err != nil {
+ return err
+ }
+ }
+
+ // Published
+ /*
+ if idx.UpdateDocumentHeaders {
+ if err := idx.refreshSharePointFolder(idx.DocumentsFolderID, "published"); err != nil {
+ return err
+ }
+ }*/
+
+ // Update metadata
+ md.LastFullIndexAt = runStartedAt.UTC()
+ if err := md.Upsert(idx.Database); err != nil {
+ return fmt.Errorf("error upserting metadata: %w", err)
+ }
+ return nil
+}
+
+func (idx *Indexer) refreshSharePointFolder(folderID, folderType string) error {
+ log := idx.Logger
+ currentTime := time.Now().UTC()
+
+ // Get the last indexed time for the folder from the database.
+ fd := models.IndexerFolder{
+ SharePointFolderID: folderID,
+ }
+ if err := fd.Get(idx.Database); err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+ return fmt.Errorf("error getting folder indexer data: %w", err)
+ }
+
+ // If the last indexed timestamp doesn't exist, set it to the Unix epoch.
+ if fd.LastIndexedAt.IsZero() {
+ fd.LastIndexedAt = time.Unix(0, 0).UTC()
+ }
+
+ // Fetch and process documents modified between the last indexed time and now.
+ docs, err := idx.sharepointSvc.FetchDocuments(idx.SharePointDriveID, folderID, fd.LastIndexedAt, currentTime)
+ if err != nil {
+ return fmt.Errorf("error fetching %s documents from SharePoint: %w", folderType, err)
+ }
+
+ // Log the number of documents fetched.
+ log.Info("Fetched documents from SharePoint",
+ "folder_type", folderType,
+ "folder_id", folderID,
+ "document_count", len(docs),
+ )
+
+ // Process the fetched documents.
+ for _, doc := range docs {
+ log.Info("Indexing SharePoint document",
+ "file_id", doc.ID,
+ "file_name", doc.Name,
+ "folder_type", folderType,
+ )
+ }
+
+ processSharePointDocs(docs, folderType, idx)
+
+ // Update the last indexed time for the folder.
+ fd.LastIndexedAt = currentTime
+ if err := fd.Upsert(idx.Database); err != nil {
+ log.Error("error upserting last indexed time for folder", "folder_id", folderID, "last_indexed_at", fd.LastIndexedAt)
+ }
+
+ log.Info("Done refreshing " + folderType + " document (SharePoint)")
+ return nil
+}
+
// saveDoc saves a document struct and its redirect details in Algolia.
func saveDocInAlgolia(
doc document.Document,
@@ -579,3 +712,125 @@ func saveDocInAlgolia(
return nil
}
+
+// processSharePointDocs processes and indexes SharePoint documents.
+func processSharePointDocs(docs []sp.Document, docType string, idx *Indexer) {
+ log := idx.Logger
+
+ for _, doc := range docs {
+ logInfo := func(msg string, keyvals ...interface{}) {
+ log.Info(msg, append(keyvals, "sharepoint_file_id", doc.ID)...)
+ }
+ logError := func(msg string, err error) {
+ log.Error(msg,
+ "error", err,
+ "sharepoint_file_id", doc.ID,
+ )
+ }
+
+ logInfo("processing document")
+
+ // Get document from database.
+ dbDoc := models.NewDocumentByFileID(doc.ID, true)
+ if err := dbDoc.Get(idx.Database); err != nil {
+ logError("error getting document from the database", err)
+ continue
+ }
+ if dbDoc.Status == models.WIPDocumentStatus {
+ logInfo("skipping document as it is a WIP document", "Doc Title", dbDoc.Title)
+ continue
+ }
+ // Get reviews for the document from the database.
+ var reviews models.DocumentReviews
+ if err := reviews.Find(idx.Database, models.DocumentReview{
+ Document: models.NewDocumentByFileID(doc.ID, true),
+ }); err != nil {
+ log.Error("error getting reviews for document",
+ "error", err,
+ "sharepoint_file_id", doc.ID,
+ )
+ continue
+ }
+
+ // Get group reviews for the document.
+ var groupReviews models.DocumentGroupReviews
+ if err := groupReviews.Find(idx.Database, models.DocumentGroupReview{
+ Document: models.NewDocumentByFileID(doc.ID, true),
+ }); err != nil {
+ log.Error("error getting group reviews for document",
+ "error", err,
+ "sharepoint_file_id", doc.ID,
+ )
+ continue
+ }
+
+ // Parse document modified time.
+ modifiedTime, err := time.Parse(time.RFC3339Nano, doc.LastModifiedTime)
+ if err != nil {
+ logError("error parsing document modified time", err)
+ continue
+ }
+
+ // Set new modified time for document record.
+ dbDoc.DocumentModifiedAt = modifiedTime
+
+ // Update document in database.
+ if err := dbDoc.Upsert(idx.Database); err != nil {
+ logError("error upserting document", err)
+ continue
+ }
+
+ var documentObj *document.Document
+ if idx.UseDatabaseForDocumentData {
+ log.Debug("Using database for document data source", "sharepoint_file_id", doc.ID)
+ // Convert database record to a document.
+ documentObj, err = document.NewFromDatabaseModel(dbDoc, reviews, groupReviews)
+ //documentObj, err = document.NewFromDatabaseModel(dbDoc, nil, nil)
+ if err != nil {
+ log.Error("error converting database record to document",
+ "error", err,
+ "sharepoint_file_id", doc.ID,
+ )
+ continue
+ }
+ } else {
+ log.Debug("Using Algolia for document data source", "sharepoint_file_id", doc.ID)
+ // Get document object from Algolia.
+ var algoObj map[string]any
+ if err = idx.AlgoliaClient.Docs.GetObject(doc.ID, &algoObj); err != nil {
+ logError("error retrieving document object from Algolia", err)
+ continue
+ }
+
+ // Convert Algolia object to a document.
+ documentObj, err = document.NewFromAlgoliaObject(algoObj, idx.DocumentTypes)
+ if err != nil {
+ logError("error converting Algolia object to document", err)
+ continue
+ }
+ }
+
+ // Get document content from SharePoint.
+ content, err := idx.sharepointSvc.DownloadContent(doc.ID)
+ if err != nil {
+ logError("error downloading document content from SharePoint", err)
+ continue
+ }
+ // Trim doc content if it is larger than the maximum size.
+ if len(content) > maxContentSize {
+ content = content[:maxContentSize]
+ }
+
+ // Update document object with content and latest modified time.
+ documentObj.Content = (string(content))
+ documentObj.ModifiedTime = modifiedTime.Unix()
+
+ // Save the document in Algolia.
+ if err := saveDocInAlgolia(*documentObj, idx.AlgoliaClient); err != nil {
+ logError("error saving document in Algolia", err)
+ continue
+ }
+
+ logInfo("document processed and indexed")
+ }
+}
diff --git a/internal/indexer/refresh_headers.go b/internal/indexer/refresh_headers.go
index 4b15a1ccb..b748e14f4 100644
--- a/internal/indexer/refresh_headers.go
+++ b/internal/indexer/refresh_headers.go
@@ -176,9 +176,7 @@ func refreshDocumentHeader(
var doc *document.Document
if idx.UseDatabaseForDocumentData {
// Get document from database.
- model := models.Document{
- GoogleFileID: file.Id,
- }
+ model := models.NewDocumentByFileID(file.Id, false)
if err := model.Get(idx.Database); err != nil {
log.Error("error getting document from database",
"error", err,
@@ -190,9 +188,7 @@ func refreshDocumentHeader(
// Get reviews for the document from the database.
var reviews models.DocumentReviews
if err := reviews.Find(idx.Database, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: file.Id,
- },
+ Document: models.NewDocumentByFileID(file.Id, false),
}); err != nil {
log.Error("error getting reviews for document",
"error", err,
@@ -204,9 +200,7 @@ func refreshDocumentHeader(
// Get group reviews for the document.
var groupReviews models.DocumentGroupReviews
if err := groupReviews.Find(idx.Database, models.DocumentGroupReview{
- Document: models.Document{
- GoogleFileID: file.Id,
- },
+ Document: models.NewDocumentByFileID(file.Id, false),
}); err != nil {
log.Error("error getting group reviews for document",
"error", err,
diff --git a/internal/middleware/cors.go b/internal/middleware/cors.go
new file mode 100644
index 000000000..c246fccf8
--- /dev/null
+++ b/internal/middleware/cors.go
@@ -0,0 +1,102 @@
+package middleware
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/hashicorp/go-hclog"
+)
+
+// CorsMiddleware adds CORS headers to responses
+func CorsMiddleware(log hclog.Logger, next http.Handler) http.Handler {
+ return CorsMiddlewareWithConfig(log, next, true)
+}
+
+// CorsMiddlewareWithConfig adds CORS headers to responses with configurable development mode
+func CorsMiddlewareWithConfig(log hclog.Logger, next http.Handler, isDevelopment bool) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Get the Origin header
+ origin := r.Header.Get("Origin")
+
+ var isAllowed bool
+
+ if isDevelopment {
+ // Development mode: be permissive with CORS for local development and Office 365 add-ins
+ allowedOrigins := []string{
+ "http://localhost:3000",
+ "https://localhost:3000",
+ "https://localhost:8443",
+ "http://localhost:8443",
+ "https://localhost:8000",
+ "http://localhost:8000",
+ }
+
+ // Check explicit allowed origins
+ for _, allowedOrigin := range allowedOrigins {
+ if origin == allowedOrigin {
+ isAllowed = true
+ break
+ }
+ }
+
+ // Allow localhost origins in development
+ if !isAllowed && (strings.Contains(origin, "localhost") || strings.Contains(origin, "127.0.0.1")) {
+ isAllowed = true
+ }
+
+ // Allow Office 365 origins and browser extensions for development
+ if !isAllowed && (strings.Contains(origin, "officeapps.live.com") ||
+ strings.Contains(origin, "office.com") ||
+ strings.Contains(origin, "sharepoint.com") ||
+ strings.HasPrefix(origin, "moz-extension://") ||
+ strings.HasPrefix(origin, "chrome-extension://") ||
+ strings.HasPrefix(origin, "ms-appx-web://") ||
+ origin == "" || origin == "null") {
+ isAllowed = true
+ }
+ } else {
+ // Production mode: only allow same-origin requests and specific Office 365 domains
+ if origin == "" || origin == "null" {
+ // Allow requests with no origin (same-origin requests, Office add-ins in iframe)
+ isAllowed = true
+ } else if strings.Contains(origin, "officeapps.live.com") ||
+ strings.Contains(origin, "office.com") ||
+ strings.Contains(origin, "sharepoint.com") {
+ // Allow Office 365 origins in production for Word Add-in
+ isAllowed = true
+ }
+ // In production, all other cross-origin requests are denied
+ }
+
+ // Set CORS headers for allowed origins
+ if isAllowed {
+ // Handle null/empty origin case for Firefox add-ins
+ allowOrigin := origin
+ if origin == "" || origin == "null" {
+ allowOrigin = "*"
+ }
+
+ // Set CORS headers for the preflight request
+ if r.Method == http.MethodOptions {
+ w.Header().Set("Access-Control-Allow-Origin", allowOrigin)
+ w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With, Cookie, Cache-Control")
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ w.Header().Set("Access-Control-Max-Age", "86400") // 24 hours
+ w.Header().Set("Vary", "Origin")
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+
+ // Set CORS headers for the actual request
+ w.Header().Set("Access-Control-Allow-Origin", allowOrigin)
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ w.Header().Set("Vary", "Origin")
+ } else {
+ log.Warn("CORS request denied", "origin", origin, "method", r.Method, "url", r.URL.Path)
+ }
+
+ // Call the next handler
+ next.ServeHTTP(w, r)
+ })
+}
diff --git a/internal/pkg/featureflags/flags.go b/internal/pkg/featureflags/flags.go
index d3817bb02..b7ec6db87 100644
--- a/internal/pkg/featureflags/flags.go
+++ b/internal/pkg/featureflags/flags.go
@@ -2,6 +2,7 @@ package featureflags
import (
"hash/fnv"
+ "strings"
"github.com/hashicorp-forge/hermes/internal/config"
"github.com/hashicorp-forge/hermes/pkg/algolia"
@@ -107,7 +108,7 @@ func toggleFlagEmail(a *algolia.Client, flag string, email string, log hclog.Log
// is found in the list of user emails
// for the feature flag in Algolia
for _, k := range f.FeatureFlagUserEmails[flag] {
- if email == k {
+ if strings.EqualFold(email, k) {
return true
}
}
diff --git a/internal/pub/assets/images/default-avatar.png b/internal/pub/assets/images/default-avatar.png
new file mode 100644
index 000000000..3bb40f521
--- /dev/null
+++ b/internal/pub/assets/images/default-avatar.png
@@ -0,0 +1 @@
+iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpSIVBTuIOGSoThZERRxLFYtgobQVWnUwufQLmjQkKS6OgmvBwY/FqoOLs64OroIg+AHi5Oik6CIl/i8ptIj14Lgf7+497t4BQqPCVDMwDqiaZaTiMTGbWxWDrxDQiAhiERKYqWeyCzl4jq97+Ph6F+FZ3uf+HH1K3mSATySeY7phEW8QT29aOud94jArSQrxOfGYQRckfuS67PIb56LDAs8MG5nUPHGYWCx2sNzBrGSoxFPEEUXVKF/Iuqxw3uKsVmqsdU/+wmBeW0lzneYw4lhCAkmIkFFDGRVYiNGqkWIiRfsxD/+g40+SSyZXGYwcC6hCheT4wf/gd7dmYXLCTQrFgO4X2/4YAwK7QLNu29/Htt08AfzPwJXW9lcbwOwn6c22FjwC+reBi+u2Ju8BlzvAwJMuGZIj+WkKhQLwfkbflAP6b4GeNbe31j5OH4AMdZW6AQ4OgdEiZa97vLuvu7f/nnH7+wFAAnKuCCwGAQAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+UGEAcFAAQZ9JoAAAoYSURBVHja7Z17UFT3Fce/d3d5g7wRUcQngsEICnEUI2qReWiiGVurzTRt2k47nU7bZKZJmk5I6kwzmanN9J9Mkzb9o51JZtKxSZrWpGasCYilxpgEAeWhiAq6IgqCCrv39I9FEPHe5d67e+9d3O9nZgf27r2/s7/vPTv33HPO/Y3lcjkItuT6h0uKE7SZvFTfvbQiUE5BDYAtCQTVAIQgTMwUgRqAY7T4Og3jUm1OSDRlZ6YlpybFxsbEx5L1pzZAEzO9Yfnyyj2RL4A+Ztm3fsMvNuzc+PP4uDgtlDkIIwGw9WtXLH3jZO0hTUiEZo+v95fHXp346zG9+M2r8/KQn5sbRfZz7FzHIc33QdBQ39iIqqqqGXVrNXrw6zUpCQl4tn8AozqdUxMSA4C+0w3NzbNqYuRJYRzUaVHT2IQnFyy4dU5Efl4evv70U/xcpdpIAiB4yYrDiI3VTsSA0bnz9NAE2wtY1VJTvmb5Z7/a+zYJgBB8+aV9J3bm7d27xj7TZ3+RwR8XRGt1WpiMJrxZW+uU22uyk1JSYDKZMGowOMV+yXH34cP1H2u0UTZBw85r5eV7p/uAmMiH45XlWz/aUtC+YdMLKnHvEROxaWho0G4+cmLvK+dOfKkRt5JFm458tvtg/qkfPfeSMiHZ/cOvrV7zUNrXlpMAePlh1aYn8nCpuqxT+gaLFBxANx368PB76QeXFpaoZYf/5bNbXnrx/HsvP0UawJubPyi7oTmU+nxzZiIPwMYKlq45eHPGgcIHn1HEbEhMTcXbJ4++KycBiGDj3r3ZcsiDUNTuflT+ccYeRczz165ZKWcwKBRd/8FJadV+fcUSQbIMn/TnfZG9U04CEJ6/99kXMtl/3bIXEg49f+6mHGpACK5RcxdNAfMBTr3cwYNAQUQryEkNQA2AGgA1AGoA1ACoAVADoAZADYAaADUAagDUAKgBUAOgBkANgBoANQBqANQAqAFQA6AGQA2AGkDw0NEhhq8bwLjh9kRiFXVKb5mWI/v9/D8+5f1++apVvOV9Y8MGnvk7S3Mnvqfdc99Rny5PjHuQVXkTfvvVX2jZAwA88E//c9SnyUMNQP4G8NEhXXbWLH7PfVOFp00+ecjzfLrXH6DTQOIA1ACoARAB6ACDbMw7P1v9C7XCMpHBwZmex9+jRAAdDBiF22mvtyWQARXoAeCjkJ+8eO7iGbEKcGDJg8vOwasOCcBnN74iJWAE58SDn7dfAd7uryMlYJxg1/H/srYAvnl1jRrA+HtLywVWAV+7VIMrP77OnlFjqLbKxnv8kVcuZIViubWrHZdbG4W/lxvWexL8LXPo5pVmXG5p9Gj7nTu2qVQDTZKCxoQ2ADdP01z5CeDqlYDbHF8IgJ+emA3cYMN1sSBQpoPAijP2AKzbIK1LR1PAtB1AU6OgADb9tRIPXQB+cC2IAsjIFFS+dvd/SAAedwFJvXjuo97BOInd5qGmPfti4PrmuyQAIc4w93bKtz0xGImtaXZlO+RoAmYXXQLLS0b8+TuFsrjmhsJC+7EH3AvA4ctRBACQeluGKkA7Q63rADwrQEtjIj6fPZt3fa57nm18Yq5/+/bFC7zbY+7b95QVgPYWNS799UYQU2Ad9FA5AMvbozZf2AQ8fF5aAdiixuXftwQpAA+bAKHGV/dqceeJfL8EwINpDYC/1EzfArDVuLzXTw3gw45gRE3A3EJAoyYRuHM1YwpAxAZ4y9TGo0eBhz5AXgKYExqzgK6WGFTuLXEZB3h84ycOA8hZBXhY5KH+V1ufO3bJnw9POQ0Q0gA2XxLO5jk5AEHdz6H0fPDTb+L6eZMw96pwhbMpMQIwXdWu7wnPPg3wMvgbtAGckCN5HZgIADUAagCEDNFAagDUAKgBUAOgBkANIHQod/Xiq9969CYJgBPe7b3x4Uccv+jmFxu78vImc2XK9dyLrON+9uG1PXnwA8I2qxp/+XrPB2QBCImWl5U5fL96z0u8t6XR6/GOXm8f5Dk46tMkzQBkhSwALzWAu0SQFwFQNJAaADUAagDUAKgBUAOgBkCELBuonEGgmqPFUNY20C8BrF23Tu9uH4I5c+bwVj4kiuZSiafj1lZXg9HrHeaH9Kaz+d7Nb3EQssbHgVnMU7ahZPUfWlqQnpHhVPHMpCTkz5/vUvn2hKCuzo6ACdqDQyP4yy/1Tlm8aaP9qIvFBDlzgGLnhxqBnDnAHlc5QQKZApYWlygVDQwhGxYvhrtXEqwqLnYYGFJkELhzz57dwZApYnExEAxkbwJ4b1znT+8Hq8x87vxNVNshTPAewA4eBLJsAAPuPgGMEkFBA9kFZIKmAFgBbYSAbNgDqFgjiQYSwRUEklkINQA/MBMHIAIESAkgXoEthTUAagBEJBDYFHAjG0i4ZHExCUBwZPBGv6D2Tvd3DPYfUZzVAAZZ/l/RUFuLXL1+ykjfUH8/qqqrRXQBPqQADvX6LVu3bsfCAH62cXj4OqUQiYAlhfVDSlJSEj66ciVgAjh+fNacOb97j0KBYQa7fh3n7r4rhyvQcXHwK5Rbffkvb/dROSIYfOj0bf7t9z+gckQweOPEnXd+MzQ0ROWIwPLm+2fP7lQ9+8QCKksEFFXcM6oDW15dS4WJwNH9z/HxDYd/5Ah97QsdEA1Ma1+NytLQnQa83A5bYwnOnX10iiA4C2Ddt8pvnl2ak+u0S2IEIycPuwFTNFCmgPbikFLAqQAi79sN5Aac96wwmlsgfXXQ8dlAQeVMI4D2dKB9MihLWJLmSACzAygAaRuObvDIBZFpYP/kICBvEigtLcRrGhikQSCLNNEgQ6wAXAZykbAJGBwdxfRFIIA1ewInAJ+CLEYPcoDLV69OSaLs70/WhUMg6E+SZ7G9pE6eVNXW1k78mZycjJycnOCuCFosuqSX9x6zLlq06KOX9u2HKTTA2Pgg5s6d+/vU1NSPu7q6AhfV8yfWAV9+O1lbyfQGAAYGekqH+zqw+sFCd/7xiRd4vFAkLSAxD4+3/0xb39T03B1l6xCnkqag/HC0bUlx8VOfNTTUBDMbeJt5qHy5LT8jr/jylBX6XsRoPOgCXEYDxS4NBzQOkL9+PZaXlT15+NPa94M1CvjeR99g3fy5l0YM+mDFAdIzMiBGAGKmgXkoXE0Bz56rsVQUFUmOBq579jlPv0U6VgE97QIyfBQAf7OBks+BuciR5GlNwN1bcJTW2d/zwWwHK775q4BeXy8E8THWcSYNIGoj0IC7H/qwbFZT03PR2ZDvie3YiPxcYFtrK0ZHRnYqUulmbp92D1gAIvxFhQyCCLIB1ACoAVADoAZA+MD/AYW/9TNiQ1/CAAAAAElFTkSuQmCC
diff --git a/internal/server/server.go b/internal/server/server.go
index ca6c5079e..8205aff14 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -2,9 +2,12 @@ package server
import (
"github.com/hashicorp-forge/hermes/internal/config"
+ "github.com/hashicorp-forge/hermes/internal/email"
"github.com/hashicorp-forge/hermes/internal/jira"
"github.com/hashicorp-forge/hermes/pkg/algolia"
gw "github.com/hashicorp-forge/hermes/pkg/googleworkspace"
+ "github.com/hashicorp-forge/hermes/pkg/models"
+ sp "github.com/hashicorp-forge/hermes/pkg/sharepointhelper"
"github.com/hashicorp/go-hclog"
"gorm.io/gorm"
)
@@ -31,4 +34,31 @@ type Server struct {
// Logger is the logger for the server.
Logger hclog.Logger
+
+ // MSGraphService is the Microsoft Graph service for the server.
+ // MSGraphService *microsoftgraph.Service
+
+ //Sharepoint
+ SharePoint *sp.Service
+}
+
+// GetEmailSender returns the appropriate email.EmailSender based on which
+// backend is configured (SharePoint or Google Workspace).
+func (s Server) GetEmailSender() email.EmailSender {
+ if s.SharePoint != nil {
+ return s.SharePoint
+ }
+ return &gw.EmailSenderAdapter{Svc: s.GWService}
+}
+
+// IsSharePoint returns true when the server is configured for a SharePoint
+// backend, false when it is configured for Google Workspace.
+func (s Server) IsSharePoint() bool {
+ return s.SharePoint != nil
+}
+
+// NewDocumentByFileID returns a models.Document with the correct file-ID
+// field populated based on the configured backend.
+func (s Server) NewDocumentByFileID(fileID string) models.Document {
+ return models.NewDocumentByFileID(fileID, s.IsSharePoint())
}
diff --git a/internal/structs/http_error.go b/internal/structs/http_error.go
new file mode 100644
index 000000000..6933d9f44
--- /dev/null
+++ b/internal/structs/http_error.go
@@ -0,0 +1,25 @@
+package structs
+
+import "fmt"
+
+// HTTPError represents an error with an associated HTTP status code
+type HTTPError struct {
+ StatusCode int
+ Message string
+ Err error
+}
+
+func (e HTTPError) Error() string {
+ if e.Err != nil {
+ return fmt.Sprintf("%s: %v", e.Message, e.Err)
+ }
+ return e.Message
+}
+
+func NewHTTPError(statusCode int, message string, err error) HTTPError {
+ return HTTPError{
+ StatusCode: statusCode,
+ Message: message,
+ Err: err,
+ }
+}
diff --git a/internal/version/version.go b/internal/version/version.go
index 0f0d2f65b..430182777 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -4,7 +4,7 @@ import (
"runtime/debug"
)
-const Version = "0.5.0"
+const Version = "1.0.0"
// GetVersion returns
// the version number
diff --git a/pkg/algolia/proxy.go b/pkg/algolia/proxy.go
index c754ea4e8..02bb06cb4 100644
--- a/pkg/algolia/proxy.go
+++ b/pkg/algolia/proxy.go
@@ -2,7 +2,7 @@ package algolia
import (
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"time"
@@ -13,15 +13,30 @@ import (
func AlgoliaProxyHandler(
c *Client, cfg *Config, log hclog.Logger) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // Create HTTP request.
+ log.Debug("AlgoliaProxyHandler: Received request",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "query", r.URL.RawQuery,
+ )
+
+ // Log AppID for debugging (API key is masked for security)
+ log.Debug("AlgoliaProxyHandler: Using Algolia credentials",
+ "AppID", c.Docs.GetAppID(),
+ ) // Create HTTP request.
url := fmt.Sprintf("https://%s-dsn.algolia.net%s?%s",
c.Docs.GetAppID(), r.URL.Path, r.URL.RawQuery)
+ log.Debug("AlgoliaProxyHandler: Constructed URL", "url", url)
+
client := &http.Client{
Timeout: time.Second * 10,
}
req, err := http.NewRequest(r.Method, url, r.Body)
if err != nil {
- log.Error("error executing search request", "error", err)
+ log.Error("AlgoliaProxyHandler: Error creating HTTP request",
+ "error", err,
+ "method", r.Method,
+ "url", url,
+ "path", r.URL.Path)
http.Error(w, "Error executing search request",
http.StatusInternalServerError)
return
@@ -30,29 +45,46 @@ func AlgoliaProxyHandler(
// Add Algolia auth headers.
req.Header.Add("X-Algolia-API-Key", cfg.SearchAPIKey)
req.Header.Add("X-Algolia-Application-Id", c.Docs.GetAppID())
+ log.Debug("AlgoliaProxyHandler: Added Algolia auth headers")
// Execute HTTP request.
+ log.Debug("AlgoliaProxyHandler: Sending request to Algolia")
resp, err := client.Do(req)
if err != nil {
- log.Error("error executing search request", "error", err)
+ log.Error("AlgoliaProxyHandler: Error executing search request",
+ "error", err,
+ "method", r.Method,
+ "url", url,
+ "path", r.URL.Path)
http.Error(w, "Error executing search request",
http.StatusInternalServerError)
return
}
defer resp.Body.Close()
+ log.Debug("AlgoliaProxyHandler: Received response from Algolia",
+ "status_code", resp.StatusCode,
+ )
// Build and write HTTP response.
w.WriteHeader(resp.StatusCode)
for k, v := range resp.Header {
w.Header().Add(k, v[0])
}
- respBody, err := ioutil.ReadAll(resp.Body)
+ log.Debug("AlgoliaProxyHandler: Copied response headers")
+
+ respBody, err := io.ReadAll(resp.Body)
if err != nil {
- log.Error("error executing search request", "error", err)
- http.Error(w, "Error executing search request",
+ log.Error("AlgoliaProxyHandler: Error reading response body",
+ "error", err,
+ "status_code", resp.StatusCode,
+ "method", r.Method,
+ "path", r.URL.Path)
+ http.Error(w, "Error reading search response",
http.StatusInternalServerError)
return
}
+ log.Debug("AlgoliaProxyHandler: Writing response body", "body_length", len(respBody))
w.Write(respBody)
+ log.Debug("AlgoliaProxyHandler: Request completed successfully")
})
}
diff --git a/pkg/document/document.go b/pkg/document/document.go
index 9fec4f793..7739616bf 100644
--- a/pkg/document/document.go
+++ b/pkg/document/document.go
@@ -109,6 +109,10 @@ type Document struct {
// "Obsolete").
Status string `json:"status,omitempty"`
+ // Archived indicates whether the draft document is archived.
+ // Only applies to draft documents (status "WIP").
+ Archived bool `json:"archived,omitempty"`
+
// Tags is a slice of tags to help users discover the document based on their
// interests.
Tags []string `json:"tags,omitempty"`
@@ -245,7 +249,7 @@ func NewFromDatabaseModel(
doc := &Document{}
// ObjectID.
- doc.ObjectID = model.GoogleFileID
+ doc.ObjectID = model.GetFileIdentifier()
// Title.
doc.Title = model.Title
@@ -348,7 +352,7 @@ func NewFromDatabaseModel(
// FileRevisions.
fileRevisions := make(map[string]string)
for _, fr := range model.FileRevisions {
- fileRevisions[fr.GoogleDriveFileRevisionID] = fr.Name
+ fileRevisions[fr.FileRevisionID] = fr.Name
}
doc.FileRevisions = fileRevisions
@@ -389,6 +393,9 @@ func NewFromDatabaseModel(
}
doc.Status = status
+ // Archived.
+ doc.Archived = model.Archived
+
// Note: ThumbnailLink is not stored in the database.
return doc, nil
@@ -426,18 +433,17 @@ func (d Document) ToAlgoliaObject(
}
// ToDatabaseModels converts a document to a document and document reviews
-// database records.
+// database records. useSharePoint controls which file ID field is populated:
+// true → FileID (SharePoint), false → GoogleFileID (Google).
func (d Document) ToDatabaseModels(
docTypes []*config.DocumentType, products []*config.Product,
+ useSharePoint bool,
) (
models.Document, models.DocumentReviews, error,
) {
- doc := models.Document{}
+ doc := models.NewDocumentByFileID(d.ObjectID, useSharePoint)
reviews := models.DocumentReviews{}
- // GoogleFileID.
- doc.GoogleFileID = d.ObjectID
-
// Title.
doc.Title = d.Title
@@ -525,11 +531,9 @@ func (d Document) ToDatabaseModels(
fileRevisions := models.DocumentFileRevisions{}
for frID, frName := range d.FileRevisions {
fileRevisions = append(fileRevisions, models.DocumentFileRevision{
- Document: models.Document{
- GoogleFileID: doc.GoogleFileID,
- },
- GoogleDriveFileRevisionID: frID,
- Name: frName,
+ Document: models.NewDocumentByFileID(doc.GetFileIdentifier(), useSharePoint),
+ FileRevisionID: frID,
+ Name: frName,
})
}
doc.FileRevisions = fileRevisions
@@ -596,19 +600,15 @@ func (d Document) ToDatabaseModels(
if helpers.StringSliceContains(d.ApprovedBy, a) {
reviews = append(reviews, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: d.ObjectID,
- },
- User: u,
- Status: models.ApprovedDocumentReviewStatus,
+ Document: models.NewDocumentByFileID(d.ObjectID, useSharePoint),
+ User: u,
+ Status: models.ApprovedDocumentReviewStatus,
})
} else if helpers.StringSliceContains(d.ChangesRequestedBy, a) {
reviews = append(reviews, models.DocumentReview{
- Document: models.Document{
- GoogleFileID: d.ObjectID,
- },
- User: u,
- Status: models.ChangesRequestedDocumentReviewStatus,
+ Document: models.NewDocumentByFileID(d.ObjectID, useSharePoint),
+ User: u,
+ Status: models.ChangesRequestedDocumentReviewStatus,
})
}
}
diff --git a/pkg/googleworkspace/drive_helpers.go b/pkg/googleworkspace/drive_helpers.go
index d0b1d9d5b..3d7850be6 100644
--- a/pkg/googleworkspace/drive_helpers.go
+++ b/pkg/googleworkspace/drive_helpers.go
@@ -10,7 +10,7 @@ import (
)
const (
- fileFields = "id, lastModifyingUser, modifiedTime, name, parents, thumbnailLink"
+ fileFields = "id, lastModifyingUser, modifiedTime, name, parents, thumbnailLink, webViewLink"
)
// CopyFile copies a Google Drive file.
diff --git a/pkg/googleworkspace/gmail_helpers.go b/pkg/googleworkspace/gmail_helpers.go
index d5c3ef0bf..9b4951f94 100644
--- a/pkg/googleworkspace/gmail_helpers.go
+++ b/pkg/googleworkspace/gmail_helpers.go
@@ -23,3 +23,36 @@ func (s *Service) SendEmail(to []string, from, subject, body string) (*gmail.Mes
}
return resp, nil
}
+
+// EmailSenderAdapter wraps a Google Workspace Service to satisfy the
+// email.EmailSender interface (which expects SendEmail to return just error).
+type EmailSenderAdapter struct {
+ Svc *Service
+}
+
+// SendEmail sends an email, discarding the Gmail Message return value.
+func (a *EmailSenderAdapter) SendEmail(to []string, from, subject, body string) error {
+ _, err := a.Svc.SendEmail(to, from, subject, body)
+ return err
+}
+
+// SendEmailWithBCC sends an email with BCC recipients via Gmail.
+func (a *EmailSenderAdapter) SendEmailWithBCC(to []string, bcc []string, from, subject, body string) error {
+ emailStr := fmt.Sprintf(
+ "To: %s\r\nBcc: %s\r\nFrom: %s\r\nContent-Type: text/html; charset=UTF-8\r\nSubject: [not provided]\r\n\r\n%s\r\n",
+ strings.Join(to, ","),
+ strings.Join(bcc, ","),
+ from,
+ body,
+ )
+
+ msg := &gmail.Message{
+ Raw: base64.URLEncoding.EncodeToString([]byte(emailStr)),
+ }
+
+ _, err := a.Svc.Gmail.Users.Messages.Send("me", msg).Do()
+ if err != nil {
+ return fmt.Errorf("error sending email with BCC: %w", err)
+ }
+ return nil
+}
diff --git a/pkg/googleworkspace/service.go b/pkg/googleworkspace/service.go
index f8e82c7f8..6196051b6 100644
--- a/pkg/googleworkspace/service.go
+++ b/pkg/googleworkspace/service.go
@@ -8,6 +8,7 @@ import (
"log"
"net/http"
"os"
+ "time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
@@ -177,7 +178,14 @@ func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
m := http.NewServeMux()
// TODO: remove hardcoded port.
- s := http.Server{Addr: ":9999", Handler: m}
+ s := http.Server{
+ Addr: ":9999",
+ Handler: m,
+ ReadTimeout: 10 * time.Second,
+ ReadHeaderTimeout: 5 * time.Second,
+ WriteTimeout: 10 * time.Second,
+ IdleTimeout: 30 * time.Second,
+ }
config.RedirectURL = "http://localhost:9999/callback"
m.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
diff --git a/pkg/hashicorpdocs/locked.go b/pkg/hashicorpdocs/locked.go
index ecc1e231a..c8ee8106f 100644
--- a/pkg/hashicorpdocs/locked.go
+++ b/pkg/hashicorpdocs/locked.go
@@ -2,6 +2,7 @@ package hashicorpdocs
import (
"fmt"
+ "strings"
"github.com/hashicorp-forge/hermes/pkg/googleworkspace"
"github.com/hashicorp-forge/hermes/pkg/models"
@@ -10,8 +11,18 @@ import (
"gorm.io/gorm"
)
+// IsFileID checks if a fileID is in SharePoint format
+// SharePoint file IDs tend to be longer and have specific patterns
+func IsFileID(fileID string) bool {
+ // SharePoint file IDs typically start with "01" and contain more characters
+ // than Google file IDs, often with specific patterns
+ return strings.HasPrefix(fileID, "01") && len(fileID) >= 20
+}
+
// IsLocked checks if a document contains one or more suggestions in the header,
// locks/unlocks the document accordingly, and returns the lock status.
+// For SharePoint documents, we always return false (not locked) as the suggestion
+// tracking system is different.
func IsLocked(
fileID string,
db *gorm.DB,
@@ -20,13 +31,38 @@ func IsLocked(
) (bool, error) {
// Get document from database.
- doc := models.Document{
- GoogleFileID: fileID,
+ doc := models.Document{}
+
+ // Determine if it's a SharePoint or Google document based on ID format
+ if IsFileID(fileID) {
+ // For SharePoint documents, set the FileID
+ doc.FileID = fileID
+ if err := doc.Get(db); err != nil {
+ return false, fmt.Errorf("error getting document from database: %w", err)
+ }
+
+ // For SharePoint documents, we don't check for suggestions in the same way
+ // Return false (not locked) for SharePoint documents
+ log.Info("SharePoint document, skipping lock check",
+ "sharepoint_file_id", fileID,
+ )
+ return false, nil
}
+
+ // This is a Google document
+ doc.GoogleFileID = fileID
if err := doc.Get(db); err != nil {
return false, fmt.Errorf("error getting document from database: %w", err)
}
+ // Only call GetDoc if we have a Google service
+ if goog == nil {
+ log.Warn("Google Workspace service not available, skipping lock check",
+ "google_file_id", fileID,
+ )
+ return false, nil
+ }
+
// Find out if the document header contains a suggestion. Deleting text which
// contains a suggestion currently causes a Google internal API error so we
// need to lock the document.
@@ -35,6 +71,7 @@ func IsLocked(
return false, fmt.Errorf("error getting Google Doc: %w", err)
}
+ // Check for suggestions in Google Doc
hasSuggestion := containsSuggestionInHeader(gDoc)
if hasSuggestion {
// Lock document if it's not already locked.
diff --git a/pkg/microsoftgraph/docs_helpers.go b/pkg/microsoftgraph/docs_helpers.go
new file mode 100644
index 000000000..0b1bef399
--- /dev/null
+++ b/pkg/microsoftgraph/docs_helpers.go
@@ -0,0 +1,407 @@
+package microsoftgraph
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+)
+
+// DriveItem represents a file or folder in Microsoft OneDrive/SharePoint
+type DriveItem struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ WebURL string `json:"webUrl"`
+ CreatedDateTime string `json:"createdDateTime"`
+ LastModifiedDateTime string `json:"lastModifiedDateTime"`
+ ParentReference struct {
+ DriveID string `json:"driveId"`
+ DriveType string `json:"driveType"`
+ ID string `json:"id"`
+ } `json:"parentReference"`
+}
+
+// CopyFile copies a file in Microsoft SharePoint/OneDrive using Microsoft Graph API
+func (s *Service) CopyFile(fileID, name, destFolderID string) (*DriveItem, error) {
+ // Create the request body
+ requestBody := map[string]interface{}{
+ "name": name,
+ "parentReference": map[string]interface{}{},
+ }
+
+ // Add driveId to parentReference only if explicitly provided in destFolderID
+ // Otherwise use the service's DriveID
+ if s.DriveID != "" {
+ requestBody["parentReference"].(map[string]interface{})["driveId"] = s.DriveID
+ }
+
+ // Handle destination folder specification
+ if destFolderID != "" {
+ // Check if destFolderID is a path rather than an ID
+ if !strings.Contains(destFolderID, "-") && !strings.Contains(destFolderID, "!") {
+ // Assume it's a folder path, use path format
+ requestBody["parentReference"].(map[string]interface{})["path"] = "/drive/root:/" + destFolderID
+ } else {
+ // It's an ID, use it directly
+ requestBody["parentReference"].(map[string]interface{})["id"] = destFolderID
+ }
+ }
+
+ // Marshal the request body to JSON
+ jsonBody, err := json.Marshal(requestBody)
+ if err != nil {
+ return nil, fmt.Errorf("error marshaling request body: %w", err)
+ }
+
+ // Log the operation for debugging
+ s.Logger.Debug("Starting file copy operation",
+ "fileID", fileID,
+ "destFolderID", destFolderID,
+ "requestBodyLength", len(jsonBody))
+
+ // Construct the URL for the copy operation - use the drive ID from service
+ var copyURL string
+ if s.SiteID != "" && s.DriveID != "" {
+ // Use site and drive if available
+ copyURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s/copy",
+ url.PathEscape(s.SiteID), url.PathEscape(s.DriveID), url.PathEscape(fileID))
+ } else if s.DriveID != "" {
+ // Fallback to drive direct access
+ copyURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/drives/%s/items/%s/copy",
+ url.PathEscape(s.DriveID), url.PathEscape(fileID))
+ } else {
+ // If no drive specified, use "me" endpoint
+ copyURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/me/drive/items/%s/copy",
+ url.PathEscape(fileID))
+ }
+
+ s.Logger.Debug("Constructed copy URL", "url", copyURL)
+
+ // Create the HTTP request
+ req, err := http.NewRequest("POST", copyURL, bytes.NewBuffer(jsonBody))
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+
+ // Set headers
+ req.Header.Set("Authorization", "Bearer "+s.AccessToken)
+ req.Header.Set("Content-Type", "application/json")
+
+ // Execute the request
+ resp, err := s.HTTPClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("error making request to microsoft graph: %w", err)
+ }
+ defer resp.Body.Close()
+ s.Logger.Debug("Copy request executed", "statusCode", resp.StatusCode)
+
+ // Check the response status
+ if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK {
+ bodyBytes, _ := io.ReadAll(resp.Body)
+
+ // Parse error response for better diagnostics
+ var errorResp struct {
+ Error struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+ InnerError struct {
+ Date string `json:"date"`
+ RequestID string `json:"request-id"`
+ ClientRequestID string `json:"client-request-id"`
+ } `json:"innerError"`
+ } `json:"error"`
+ }
+
+ _ = json.Unmarshal(bodyBytes, &errorResp)
+
+ if errorResp.Error.Code != "" {
+ return nil, fmt.Errorf("microsoft graph api error: code=%s message=%s requestID=%s: %s",
+ errorResp.Error.Code, errorResp.Error.Message,
+ errorResp.Error.InnerError.RequestID, string(bodyBytes))
+ }
+
+ return nil, fmt.Errorf("microsoft graph api returned status %d: %s", resp.StatusCode, string(bodyBytes))
+ }
+
+ // For asynchronous operations, Microsoft Graph returns a monitor URL
+ // in the Location header that we need to poll to get the result
+ if resp.StatusCode == http.StatusAccepted {
+ s.Logger.Debug("Async operation detected, monitoring progress")
+ itemID := resp.Header.Get("Location")
+ // Remove query parameters if any exist
+ if queryIndex := strings.Index(itemID, "?"); queryIndex != -1 {
+ itemID = itemID[:queryIndex]
+ }
+ // Extract the last path segment (after the last slash)
+ if lastSlashIndex := strings.LastIndex(itemID, "/"); lastSlashIndex != -1 {
+ itemID = itemID[lastSlashIndex+1:]
+ }
+ s.Logger.Debug("Extracted item ID for monitoring", "itemID", itemID)
+ var monitorURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s",
+ url.PathEscape(s.SiteID), url.PathEscape(s.DriveID), url.PathEscape(itemID))
+ if monitorURL == "" {
+ return nil, fmt.Errorf("no monitor URL returned for async operation")
+ }
+
+ // Poll the monitor URL until the operation completes
+ s.Logger.Debug("Starting polling for async operation", "monitorURL", monitorURL)
+ driveItem, err := s.pollOperationStatus(monitorURL)
+ if err != nil {
+ s.Logger.Error("Error during polling operation", "error", err, "monitorURL", monitorURL)
+ return nil, fmt.Errorf("error monitoring copy operation: %w", err)
+ }
+ s.Logger.Debug("Async operation completed successfully")
+ return driveItem, nil
+ }
+
+ // If the operation completed synchronously, parse the response directly
+ s.Logger.Debug("Synchronous operation completed, parsing response")
+ var driveItem DriveItem
+ if err := json.NewDecoder(resp.Body).Decode(&driveItem); err != nil {
+ return nil, fmt.Errorf("error decoding response: %w", err)
+ }
+
+ return &driveItem, nil
+}
+
+// pollOperationStatus polls a monitor URL until an operation completes
+func (s *Service) pollOperationStatus(monitorURL string) (*DriveItem, error) {
+ maxRetries := 10
+ retryDelay := 1 * time.Second
+
+ for i := 0; i < maxRetries; i++ {
+ // Wait before polling
+ time.Sleep(retryDelay)
+
+ // Create the request to check status
+ req, err := http.NewRequest("GET", monitorURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error creating monitor request: %w", err)
+ }
+ req.Header.Set("Authorization", "Bearer "+s.AccessToken)
+
+ // Execute the request
+ resp, err := s.HTTPClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("error making monitor request: %w", err)
+ }
+
+ // Read the response body
+ bodyBytes, err := io.ReadAll(resp.Body)
+ resp.Body.Close()
+ if err != nil {
+ return nil, fmt.Errorf("error reading monitor response: %w", err)
+ }
+
+ // Check for completion
+ if resp.StatusCode == http.StatusOK {
+ var driveItem DriveItem
+ if err := json.Unmarshal(bodyBytes, &driveItem); err != nil {
+ return nil, fmt.Errorf("error parsing completed operation response: %w", err)
+ }
+
+ return &driveItem, nil
+ }
+
+ // If still in progress, parse the monitor response
+ var monitorResponse struct {
+ Status string `json:"status"`
+ ResourceID string `json:"resourceId"`
+ StatusCode int `json:"statusCode"`
+ ErrorCode string `json:"errorCode"`
+ ErrorMessage string `json:"errorMessage"`
+ }
+
+ if err := json.Unmarshal(bodyBytes, &monitorResponse); err != nil {
+ return nil, fmt.Errorf("error parsing monitor response: %w", err)
+ }
+
+ // Check for errors or completion
+ s.Logger.Debug("Monitoring operation status",
+ "status", monitorResponse.Status,
+ "attempt", i+1,
+ "resourceID", monitorResponse.ResourceID)
+
+ switch monitorResponse.Status {
+ case "completed":
+ s.Logger.Info("Async operation completed successfully", "resourceID", monitorResponse.ResourceID)
+ // If completed, make one more request to get the item details
+ if monitorResponse.ResourceID != "" {
+ itemURL := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s",
+ url.PathEscape(s.SiteID), url.PathEscape(s.DriveID), monitorResponse.ResourceID)
+ return s.getDriveItem(itemURL)
+ }
+ case "failed":
+ s.Logger.Error("Async operation failed",
+ "errorCode", monitorResponse.ErrorCode,
+ "errorMessage", monitorResponse.ErrorMessage,
+ "statusCode", monitorResponse.StatusCode)
+ return nil, fmt.Errorf("operation failed: %s - %s", monitorResponse.ErrorCode, monitorResponse.ErrorMessage)
+ }
+
+ // Increase retry delay with exponential backoff
+ retryDelay = retryDelay * 2
+ }
+
+ return nil, fmt.Errorf("operation timed out after %d retries", maxRetries)
+}
+
+// getDriveItem fetches a drive item by its URL
+func (s *Service) getDriveItem(itemURL string) (*DriveItem, error) {
+ req, err := http.NewRequest("GET", itemURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error creating item request: %w", err)
+ }
+ req.Header.Set("Authorization", "Bearer "+s.AccessToken)
+
+ resp, err := s.HTTPClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("error making item request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("error getting item, status: %d", resp.StatusCode)
+ }
+
+ var driveItem DriveItem
+ if err := json.NewDecoder(resp.Body).Decode(&driveItem); err != nil {
+ return nil, fmt.Errorf("error decoding item response: %w", err)
+ }
+
+ return &driveItem, nil
+}
+
+// MoveFile moves a file to a different folder in OneDrive/SharePoint
+func (s *Service) MoveFile(fileID, destFolderID string) (*DriveItem, error) {
+ // Create the request body
+ requestBody := map[string]interface{}{
+ "parentReference": map[string]interface{}{
+ "driveId": s.DriveID,
+ "id": destFolderID,
+ },
+ }
+
+ // Marshal the request body to JSON
+ jsonBody, err := json.Marshal(requestBody)
+ if err != nil {
+ return nil, fmt.Errorf("error marshaling request body: %w", err)
+ }
+
+ // Log the move operation for debugging
+ s.Logger.Debug("Starting file move operation",
+ "fileID", fileID,
+ "destFolderID", destFolderID,
+ "requestBodyLength", len(jsonBody))
+
+ // Construct the URL for the move operation (PATCH to update the parent)
+ var moveURL string
+ if s.SiteID != "" && s.DriveID != "" {
+ // Use site and drive if available
+ moveURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s",
+ url.PathEscape(s.SiteID), url.PathEscape(s.DriveID), url.PathEscape(fileID))
+ } else {
+ // Fallback to drive direct access
+ moveURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/drives/%s/items/%s",
+ url.PathEscape(s.DriveID), url.PathEscape(fileID))
+ }
+
+ // Create the HTTP request
+ req, err := http.NewRequest("PATCH", moveURL, bytes.NewBuffer(jsonBody))
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+
+ // Set headers
+ req.Header.Set("Authorization", "Bearer "+s.AccessToken)
+ req.Header.Set("Content-Type", "application/json")
+
+ // Execute the request
+ resp, err := s.HTTPClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("error making request to microsoft graph: %w", err)
+ }
+ defer resp.Body.Close()
+
+ // Check the response status
+ if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
+ bodyBytes, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("microsoft graph api returned status %d: %s", resp.StatusCode, string(bodyBytes))
+ }
+
+ // Parse the response
+ var driveItem DriveItem
+ if err := json.NewDecoder(resp.Body).Decode(&driveItem); err != nil {
+ return nil, fmt.Errorf("error decoding response: %w", err)
+ }
+
+ return &driveItem, nil
+}
+
+// ShareFile shares a file with a specific user
+func (s *Service) ShareFile(fileID, email, role string) error {
+ // Map Google Drive roles to Microsoft Graph roles
+ var msRole string
+ switch role {
+ case "reader":
+ msRole = "read"
+ case "writer":
+ msRole = "write"
+ case "owner":
+ msRole = "write" // Microsoft Graph doesn't have an owner permission in the same way
+ default:
+ return fmt.Errorf("unsupported role: %s", role)
+ }
+
+ // Create the request body
+ requestBody := map[string]interface{}{
+ "recipients": []map[string]interface{}{
+ {
+ "email": email,
+ },
+ },
+ "roles": []string{msRole},
+ "requireSignIn": true,
+ "sendInvitation": true,
+ }
+
+ // Marshal the request body to JSON
+ jsonBody, err := json.Marshal(requestBody)
+ if err != nil {
+ return fmt.Errorf("error marshaling request body: %w", err)
+ }
+
+ // Construct the URL for the share operation
+ // Use SharePoint site drive instead of personal OneDrive
+ shareURL := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s/invite",
+ url.PathEscape(s.SiteID), url.PathEscape(s.DriveID), url.PathEscape(fileID))
+
+ // Create the HTTP request
+ req, err := http.NewRequest("POST", shareURL, bytes.NewBuffer(jsonBody))
+ if err != nil {
+ return fmt.Errorf("error creating request: %w", err)
+ }
+
+ // Set headers
+ req.Header.Set("Authorization", "Bearer "+s.AccessToken)
+ req.Header.Set("Content-Type", "application/json")
+
+ // Execute the request
+ resp, err := s.HTTPClient.Do(req)
+ if err != nil {
+ return fmt.Errorf("error making request to microsoft graph: %w", err)
+ }
+ defer resp.Body.Close()
+
+ // Check the response status
+ if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
+ bodyBytes, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("microsoft graph api returned status %d: %s", resp.StatusCode, string(bodyBytes))
+ }
+
+ return nil
+}
diff --git a/pkg/microsoftgraph/service.go b/pkg/microsoftgraph/service.go
new file mode 100644
index 000000000..e692432df
--- /dev/null
+++ b/pkg/microsoftgraph/service.go
@@ -0,0 +1,295 @@
+package microsoftgraph
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/hashicorp/go-hclog"
+)
+
+// Service represents a Microsoft Graph API service
+type Service struct {
+ AccessToken string
+ HTTPClient *http.Client
+ BaseURL string
+ SiteID string // SharePoint site ID
+ DriveID string // SharePoint drive ID
+ Logger hclog.Logger
+}
+
+// Person represents a person from Microsoft Graph API (internal)
+type Person struct {
+ ID string `json:"id"`
+ DisplayName string `json:"displayName"`
+ GivenName string `json:"givenName"`
+ Surname string `json:"surname"`
+ UserPrincipalName string `json:"userPrincipalName"`
+ Mail string `json:"mail"`
+ JobTitle string `json:"jobTitle"`
+ OfficeLocation string `json:"officeLocation"`
+ BusinessPhones []string `json:"businessPhones"`
+ MobilePhone string `json:"mobilePhone"`
+}
+
+// GooglePeoplePerson represents a person in Google People API format for frontend compatibility
+type GooglePeoplePerson struct {
+ EmailAddresses []EmailAddress `json:"emailAddresses"`
+ Etag string `json:"etag"`
+ Names []Name `json:"names"`
+ Photos []Photo `json:"photos"`
+ ResourceName string `json:"resourceName"`
+}
+
+type EmailAddress struct {
+ Metadata EmailMetadata `json:"metadata"`
+ Value string `json:"value"`
+}
+
+type EmailMetadata struct {
+ Primary bool `json:"primary"`
+ Source Source `json:"source"`
+ SourcePrimary bool `json:"sourcePrimary"`
+ Verified bool `json:"verified"`
+}
+
+type Name struct {
+ DisplayName string `json:"displayName"`
+ DisplayNameLastFirst string `json:"displayNameLastFirst"`
+ FamilyName string `json:"familyName"`
+ GivenName string `json:"givenName"`
+ Metadata NameMetadata `json:"metadata"`
+ UnstructuredName string `json:"unstructuredName"`
+}
+
+type NameMetadata struct {
+ Primary bool `json:"primary"`
+ Source Source `json:"source"`
+ SourcePrimary bool `json:"sourcePrimary"`
+}
+
+type Photo struct {
+ Default bool `json:"default"`
+ Metadata PhotoMetadata `json:"metadata"`
+ URL string `json:"url"`
+}
+
+type PhotoMetadata struct {
+ Primary bool `json:"primary"`
+ Source Source `json:"source"`
+}
+
+type Source struct {
+ ID string `json:"id"`
+ Type string `json:"type"`
+}
+
+// SearchPeopleResponse represents the response from Microsoft Graph people search
+type SearchPeopleResponse struct {
+ Value []Person `json:"value"`
+}
+
+// NewService creates a new Microsoft Graph service
+func NewService(accessToken string, logger hclog.Logger) *Service {
+ return &Service{
+ AccessToken: accessToken,
+ HTTPClient: &http.Client{},
+ BaseURL: "https://graph.microsoft.com",
+ Logger: logger,
+ }
+}
+
+// NewServiceWithSiteAndDrive creates a new Microsoft Graph service with SharePoint site and drive info
+func NewServiceWithSiteAndDrive(accessToken string, httpClient *http.Client, siteID, driveID string, logger hclog.Logger) *Service {
+ if httpClient == nil {
+ httpClient = &http.Client{}
+ }
+ return &Service{
+ AccessToken: accessToken,
+ HTTPClient: httpClient,
+ BaseURL: "https://graph.microsoft.com",
+ SiteID: siteID,
+ DriveID: driveID,
+ Logger: logger,
+ }
+}
+
+// convertToGooglePeopleFormat converts Microsoft Graph Person to Google People API format
+func convertToGooglePeopleFormat(person Person) GooglePeoplePerson {
+ // Use primary email (mail field) or fallback to userPrincipalName
+ email := person.Mail
+ if email == "" {
+ email = person.UserPrincipalName
+ }
+
+ // Create display name variations
+ displayName := person.DisplayName
+ if displayName == "" {
+ displayName = person.GivenName + " " + person.Surname
+ }
+
+ displayNameLastFirst := person.Surname + ", " + person.GivenName
+ if person.Surname == "" || person.GivenName == "" {
+ displayNameLastFirst = displayName
+ }
+
+ // Generate a simple etag (could be more sophisticated)
+ etag := fmt.Sprintf("%%EggBAgMJLjc9PhoCAQc=%s", person.ID[:8])
+
+ return GooglePeoplePerson{
+ EmailAddresses: []EmailAddress{
+ {
+ Metadata: EmailMetadata{
+ Primary: true,
+ Source: Source{
+ ID: person.ID,
+ Type: "DOMAIN_PROFILE",
+ },
+ SourcePrimary: true,
+ Verified: true,
+ },
+ Value: email,
+ },
+ },
+ Etag: etag,
+ Names: []Name{
+ {
+ DisplayName: displayName,
+ DisplayNameLastFirst: displayNameLastFirst,
+ FamilyName: person.Surname,
+ GivenName: person.GivenName,
+ Metadata: NameMetadata{
+ Primary: true,
+ Source: Source{
+ ID: person.ID,
+ Type: "DOMAIN_PROFILE",
+ },
+ SourcePrimary: true,
+ },
+ UnstructuredName: displayName,
+ },
+ },
+ Photos: []Photo{
+ {
+ Default: true,
+ Metadata: PhotoMetadata{
+ Primary: true,
+ Source: Source{
+ ID: person.ID,
+ Type: "PROFILE",
+ },
+ },
+ // Default Microsoft Graph photo URL - could be enhanced to fetch actual photo
+ URL: "https://graph.microsoft.com/v1.0/users/" + person.ID + "/photo/$value",
+ },
+ },
+ ResourceName: "people/" + person.ID,
+ }
+}
+
+// SearchPeople searches for people using Microsoft Graph API and returns Google People API format
+func (s *Service) SearchPeople(query string, top int) ([]GooglePeoplePerson, error) {
+ if top <= 0 {
+ top = 10 // Default limit
+ }
+
+ // Use Microsoft Graph People API to search for users
+ // Documentation: https://docs.microsoft.com/en-us/graph/api/user-list
+ searchURL := fmt.Sprintf("https://graph.microsoft.com/v1.0/users?$search=\"displayName:%s\" OR \"mail:%s\" OR \"userPrincipalName:%s\"&$top=%d&$select=id,displayName,givenName,surname,userPrincipalName,mail,jobTitle,officeLocation,businessPhones,mobilePhone",
+ url.QueryEscape(query), url.QueryEscape(query), url.QueryEscape(query), top)
+
+ req, err := http.NewRequest("GET", searchURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+
+ req.Header.Set("Authorization", "Bearer "+s.AccessToken)
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("ConsistencyLevel", "eventual") // Required for $search
+
+ resp, err := s.HTTPClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("error making request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("microsoft Graph API returned status %d", resp.StatusCode)
+ }
+
+ var searchResp SearchPeopleResponse
+ if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil {
+ return nil, fmt.Errorf("error decoding response: %w", err)
+ }
+
+ // Convert to Google People API format
+ var googlePeople []GooglePeoplePerson
+ for _, person := range searchResp.Value {
+ googlePeople = append(googlePeople, convertToGooglePeopleFormat(person))
+ }
+
+ return googlePeople, nil
+}
+
+// GetPersonByEmail gets a specific person by their email address and returns Google People API format
+func (s *Service) GetPersonByEmail(email string) (*GooglePeoplePerson, error) {
+ var getUserURL string
+
+ // Special case for "me" to get current user
+ if email == "me" {
+ getUserURL = "https://graph.microsoft.com/v1.0/me?$select=id,displayName,givenName,surname,userPrincipalName,mail,jobTitle,officeLocation,businessPhones,mobilePhone"
+ } else {
+ getUserURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/users/%s?$select=id,displayName,givenName,surname,userPrincipalName,mail,jobTitle,officeLocation,businessPhones,mobilePhone",
+ url.QueryEscape(email))
+ }
+
+ req, err := http.NewRequest("GET", getUserURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+
+ req.Header.Set("Authorization", "Bearer "+s.AccessToken)
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := s.HTTPClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("error making request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ return nil, nil // User not found
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("microsoft Graph API returned status %d", resp.StatusCode)
+ }
+
+ var person Person
+ if err := json.NewDecoder(resp.Body).Decode(&person); err != nil {
+ return nil, fmt.Errorf("error decoding response: %w", err)
+ }
+
+ // Convert to Google People API format
+ googlePerson := convertToGooglePeopleFormat(person)
+ return &googlePerson, nil
+}
+
+// GetPeopleByEmails gets multiple people by their email addresses and returns Google People API format
+func (s *Service) GetPeopleByEmails(emails []string) ([]GooglePeoplePerson, error) {
+ var people []GooglePeoplePerson
+
+ for _, email := range emails {
+ person, err := s.GetPersonByEmail(email)
+ if err != nil {
+ // Log error but continue with other emails
+ continue
+ }
+ if person != nil {
+ people = append(people, *person)
+ }
+ }
+
+ return people, nil
+}
diff --git a/pkg/models/document.go b/pkg/models/document.go
index 13af6a753..257624d8c 100644
--- a/pkg/models/document.go
+++ b/pkg/models/document.go
@@ -14,8 +14,25 @@ import (
type Document struct {
gorm.Model
- // GoogleFileID is the Google Drive file ID of the document.
- GoogleFileID string `gorm:"index;not null;unique"`
+ // GoogleFileID is the Google Drive file ID. Used when running in Google Workspace mode.
+ // For SharePoint deployments, this field is empty/null.
+ //
+ // DUAL-PROVIDER DESIGN:
+ // We keep both GoogleFileID and FileID as separate columns so that:
+ // 1. Existing Google-path code continues to work without changes.
+ // 2. SharePoint-path code uses FileID without touching GoogleFileID.
+ // 3. Database rows from Google deployments retain their original IDs;
+ // a migration script can populate FileID from GoogleFileID if a
+ // deployment later switches providers.
+ // 4. Rollback is safe — the GoogleFileID column is never dropped.
+ // Use GetFileIdentifier() to read the active ID regardless of provider.
+ // Use NewDocumentByFileID(id, useSharePoint) to construct a Document
+ // with the correct field populated.
+ GoogleFileID string `gorm:"index;default:null"`
+
+ // FileID is the SharePoint/generic file ID. Used when running in SharePoint mode.
+ // For Google deployments, this field is empty/null.
+ FileID string `gorm:"index;default:null"`
// Approvers is the list of users whose approval is requested for the
// document.
@@ -73,6 +90,10 @@ type Document struct {
// status.
ShareableAsDraft bool
+ // Archived is true if the document has been archived. Only drafts (WIP status)
+ // can be archived, and only by the owner.
+ Archived bool
+
// Summary is a summary of the document.
Summary *string
@@ -96,6 +117,38 @@ const (
ObsoleteDocumentStatus
)
+// NewDocumentByFileID creates a Document with the correct file-ID field
+// populated based on whether SharePoint is in use.
+// When useSharePoint is true it sets FileID; otherwise it sets GoogleFileID.
+func NewDocumentByFileID(fileID string, useSharePoint bool) Document {
+ if useSharePoint {
+ return Document{FileID: fileID}
+ }
+ return Document{GoogleFileID: fileID}
+}
+
+// GetFileIdentifier returns the active file ID regardless of which provider
+// is active. SharePoint docs use FileID; Google docs use GoogleFileID.
+func (d *Document) GetFileIdentifier() string {
+ if d.FileID != "" {
+ return d.FileID
+ }
+ return d.GoogleFileID
+}
+
+// hasNoFileID returns true if the document has no file identifier set.
+func (d *Document) hasNoFileID() bool {
+ return d.GoogleFileID == "" && d.FileID == ""
+}
+
+// BeforeCreate validates that every document has at least one file identifier.
+func (d *Document) BeforeCreate(tx *gorm.DB) error {
+ if d.hasNoFileID() {
+ return fmt.Errorf("document must have either GoogleFileID or FileID")
+ }
+ return nil
+}
+
// BeforeSave is a hook used to find associations before saving.
func (d *Document) BeforeSave(tx *gorm.DB) error {
if err := d.getAssociations(tx); err != nil {
@@ -110,14 +163,8 @@ func (d *Document) Create(db *gorm.DB) error {
if err := validation.ValidateStruct(d,
validation.Field(
&d.ID,
- validation.When(d.GoogleFileID == "",
- validation.Required.Error("either ID or GoogleFileID is required"),
- ),
- ),
- validation.Field(
- &d.GoogleFileID,
- validation.When(d.ID == 0,
- validation.Required.Error("either ID or GoogleFileID is required"),
+ validation.When(d.hasNoFileID(),
+ validation.Required.Error("either ID, GoogleFileID, or FileID is required"),
),
),
); err != nil {
@@ -131,8 +178,11 @@ func (d *Document) Create(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.
Model(&d).
- Where(Document{GoogleFileID: d.GoogleFileID}).
- Omit(clause.Associations). // We get associations in the BeforeSave hook.
+ Where(Document{
+ GoogleFileID: d.GoogleFileID,
+ FileID: d.FileID,
+ }).
+ Omit(clause.Associations).
Create(&d).
Error; err != nil {
return err
@@ -151,25 +201,24 @@ func (d *Document) Delete(db *gorm.DB) error {
if err := validation.ValidateStruct(d,
validation.Field(
&d.ID,
- validation.When(d.GoogleFileID == "",
- validation.Required.Error("either ID or GoogleFileID is required"),
- ),
- ),
- validation.Field(
- &d.GoogleFileID,
- validation.When(d.ID == 0,
- validation.Required.Error("either ID or GoogleFileID is required"),
+ validation.When(d.hasNoFileID(),
+ validation.Required.Error("either ID, GoogleFileID, or FileID is required"),
),
),
); err != nil {
return err
}
- return db.
- Model(&d).
- Where(Document{GoogleFileID: d.GoogleFileID}).
- Delete(&d).
- Error
+ query := db.Model(&d)
+ if d.GoogleFileID != "" {
+ query = query.Where("google_file_id = ?", d.GoogleFileID)
+ } else if d.FileID != "" {
+ query = query.Where("file_id = ?", d.FileID)
+ } else {
+ query = query.Where("id = ?", d.ID)
+ }
+
+ return query.Delete(&d).Error
}
// Find finds all documents from database db with the provided query, and
@@ -187,7 +236,7 @@ func (d *Documents) Find(
// record if it does not exist.
// func (d *Document) FirstOrCreate(db *gorm.DB) error {
// return db.
-// Where(Document{GoogleFileID: d.GoogleFileID}).
+// Where(Document{FileID: d.FileID}).
// Preload(clause.Associations).
// FirstOrCreate(&d).Error
// }
@@ -198,22 +247,24 @@ func (d *Document) Get(db *gorm.DB) error {
if err := validation.ValidateStruct(d,
validation.Field(
&d.ID,
- validation.When(d.GoogleFileID == "",
- validation.Required.Error("either ID or GoogleFileID is required"),
- ),
- ),
- validation.Field(
- &d.GoogleFileID,
- validation.When(d.ID == 0,
- validation.Required.Error("either ID or GoogleFileID is required"),
+ validation.When(d.hasNoFileID(),
+ validation.Required.Error("either ID, GoogleFileID, or FileID is required"),
),
),
); err != nil {
return err
}
- if err := db.
- Where(Document{GoogleFileID: d.GoogleFileID}).
+ query := db
+ if d.GoogleFileID != "" {
+ query = query.Where("google_file_id = ?", d.GoogleFileID)
+ } else if d.FileID != "" {
+ query = query.Where("file_id = ?", d.FileID)
+ } else {
+ query = query.Where("id = ?", d.ID)
+ }
+
+ if err := query.
Preload(clause.Associations).
Preload("RelatedResources", func(db *gorm.DB) *gorm.DB {
return db.Order("document_related_resources.sort_order ASC")
@@ -269,12 +320,19 @@ func GetLatestProductNumber(db *gorm.DB,
First(&d).
Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
- return 0, nil
+ // No documents exist, start from 2000 (return 1999 so next will be 2000)
+ return 1999, nil
} else {
return 0, err
}
}
+ // If highest existing number is below 2000, start from 2000
+ // Otherwise continue the existing sequence
+ if d.DocumentNumber < 2000 {
+ return 1999, nil
+ }
+
return d.DocumentNumber, nil
}
@@ -283,14 +341,8 @@ func (d *Document) GetProjects(db *gorm.DB) ([]Project, error) {
if err := validation.ValidateStruct(d,
validation.Field(
&d.ID,
- validation.When(d.GoogleFileID == "",
- validation.Required.Error("either ID or GoogleFileID is required"),
- ),
- ),
- validation.Field(
- &d.GoogleFileID,
- validation.When(d.ID == 0,
- validation.Required.Error("either ID or GoogleFileID is required"),
+ validation.When(d.hasNoFileID(),
+ validation.Required.Error("either ID, GoogleFileID, or FileID is required"),
),
),
); err != nil {
@@ -301,6 +353,7 @@ func (d *Document) GetProjects(db *gorm.DB) ([]Project, error) {
if d.ID == 0 {
doc := &Document{
GoogleFileID: d.GoogleFileID,
+ FileID: d.FileID,
}
if err := doc.Get(db); err != nil {
return nil, fmt.Errorf("error getting document: %w", err)
@@ -330,14 +383,8 @@ func (d *Document) ReplaceRelatedResources(
if err := validation.ValidateStruct(d,
validation.Field(
&d.ID,
- validation.When(d.GoogleFileID == "",
- validation.Required.Error("either ID or GoogleFileID is required"),
- ),
- ),
- validation.Field(
- &d.GoogleFileID,
- validation.When(d.ID == 0,
- validation.Required.Error("either ID or GoogleFileID is required"),
+ validation.When(d.hasNoFileID(),
+ validation.Required.Error("either ID, GoogleFileID, or FileID is required"),
),
),
); err != nil {
@@ -348,6 +395,7 @@ func (d *Document) ReplaceRelatedResources(
if d.ID == 0 {
doc := &Document{
GoogleFileID: d.GoogleFileID,
+ FileID: d.FileID,
}
if err := doc.Get(db); err != nil {
return fmt.Errorf("error getting document: %w", err)
@@ -414,14 +462,8 @@ func (d *Document) GetRelatedResources(db *gorm.DB) (
if err = validation.ValidateStruct(d,
validation.Field(
&d.ID,
- validation.When(d.GoogleFileID == "",
- validation.Required.Error("either ID or GoogleFileID is required"),
- ),
- ),
- validation.Field(
- &d.GoogleFileID,
- validation.When(d.ID == 0,
- validation.Required.Error("either ID or GoogleFileID is required"),
+ validation.When(d.hasNoFileID(),
+ validation.Required.Error("either ID, GoogleFileID, or FileID is required"),
),
),
); err != nil {
@@ -475,21 +517,14 @@ func (d *Document) Upsert(db *gorm.DB) error {
if err := validation.ValidateStruct(d,
validation.Field(
&d.ID,
- validation.When(d.GoogleFileID == "",
- validation.Required.Error("either ID or GoogleFileID is required"),
- ),
- ),
- validation.Field(
- &d.GoogleFileID,
- validation.When(d.ID == 0,
- validation.Required.Error("either ID or GoogleFileID is required"),
+ validation.When(d.hasNoFileID(),
+ validation.Required.Error("either ID, GoogleFileID, or FileID is required"),
),
),
); err != nil {
return err
}
- // Create required associations.
if err := d.createAssocations(db); err != nil {
return fmt.Errorf("error creating associations: %w", err)
}
@@ -497,16 +532,18 @@ func (d *Document) Upsert(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.
Model(&d).
- Where(Document{GoogleFileID: d.GoogleFileID}).
+ Where(Document{
+ GoogleFileID: d.GoogleFileID,
+ FileID: d.FileID,
+ }).
Select("*").
- Omit(clause.Associations). // We manage associations in the BeforeSave hook.
+ Omit(clause.Associations).
Assign(*d).
FirstOrCreate(&d).
Error; err != nil {
return err
}
- // Replace has-many associations because we may have removed instances.
if err := d.replaceAssocations(tx); err != nil {
return fmt.Errorf("error replacing associations: %w", err)
}
@@ -701,14 +738,8 @@ func (d *Document) replaceAssocations(db *gorm.DB) error {
if err := validation.ValidateStruct(d,
validation.Field(
&d.ID,
- validation.When(d.GoogleFileID == "",
- validation.Required.Error("either ID or GoogleFileID is required"),
- ),
- ),
- validation.Field(
- &d.GoogleFileID,
- validation.When(d.ID == 0,
- validation.Required.Error("either ID or GoogleFileID is required"),
+ validation.When(d.hasNoFileID(),
+ validation.Required.Error("either ID, GoogleFileID, or FileID is required"),
),
),
); err != nil {
@@ -719,6 +750,7 @@ func (d *Document) replaceAssocations(db *gorm.DB) error {
if d.ID == 0 {
doc := &Document{
GoogleFileID: d.GoogleFileID,
+ FileID: d.FileID,
}
if err := doc.Get(db); err != nil {
return fmt.Errorf("error getting document: %w", err)
diff --git a/pkg/models/document_file_revision.go b/pkg/models/document_file_revision.go
index 36c1fd32c..467d706f2 100644
--- a/pkg/models/document_file_revision.go
+++ b/pkg/models/document_file_revision.go
@@ -18,11 +18,22 @@ type DocumentFileRevision struct {
Document Document
DocumentID uint `gorm:"primaryKey"`
- // GoogleDriveFileRevisionID is the ID of the Google Drive file revision.
- GoogleDriveFileRevisionID string `gorm:"primaryKey"`
+ // FileRevisionID is the universal ID for the file revision (SharePoint or Google Drive).
+ FileRevisionID string `gorm:"primaryKey"`
// Name is the name of the document file revision.
Name string `gorm:"primaryKey"`
+
+ // GoogleDriveFileRevisionID is the legacy Google Drive revision ID.
+ // RETAINED FOR MIGRATION: Existing Google-deployed databases have rows keyed
+ // by this column. It is preserved as a nullable field so that:
+ // 1. Existing data is not lost during the schema migration (GORM AutoMigrate
+ // adds the new FileRevisionID column; the old column stays).
+ // 2. Rollback to a pre-merge version is possible without data loss.
+ // 3. Migration scripts can copy GoogleDriveFileRevisionID → FileRevisionID
+ // for existing rows, then this column can be dropped in a future release.
+ // New code should read/write FileRevisionID exclusively.
+ GoogleDriveFileRevisionID *string `gorm:"default:null"`
}
// DocumentFileRevisions is a slice of document file revisions.
@@ -45,7 +56,7 @@ func (fr *DocumentFileRevision) Create(db *gorm.DB) error {
// Validate fields.
if err := validation.ValidateStruct(fr,
validation.Field(&fr.DocumentID, validation.Required),
- validation.Field(&fr.GoogleDriveFileRevisionID, validation.Required),
+ validation.Field(&fr.FileRevisionID, validation.Required),
validation.Field(&fr.Name, validation.Required),
); err != nil {
return err
@@ -95,7 +106,7 @@ func (fr *DocumentFileRevision) Get(db *gorm.DB) error {
// Validate fields.
if err := validation.ValidateStruct(fr,
validation.Field(&fr.DocumentID, validation.Required),
- validation.Field(&fr.GoogleDriveFileRevisionID, validation.Required),
+ validation.Field(&fr.FileRevisionID, validation.Required),
validation.Field(&fr.Name, validation.Required),
); err != nil {
return err
diff --git a/pkg/models/document_file_revision_test.go b/pkg/models/document_file_revision_test.go
index 6ffa47159..9e6559ef2 100644
--- a/pkg/models/document_file_revision_test.go
+++ b/pkg/models/document_file_revision_test.go
@@ -60,8 +60,8 @@ func TestDocumentFileRevisionwModel(t *testing.T) {
Document: Document{
GoogleFileID: "GoogleFileID1",
},
- GoogleDriveFileRevisionID: "GoogleDriveFileRevisionID1",
- Name: "Name1",
+ FileRevisionID: "FileRevisionID1",
+ Name: "Name1",
}
err := fr.Create(db)
require.NoError(err)
@@ -74,8 +74,7 @@ func TestDocumentFileRevisionwModel(t *testing.T) {
require.NoError(err)
require.Len(frs, 1)
assert.EqualValues(1, frs[0].DocumentID)
- assert.Equal(
- "GoogleDriveFileRevisionID1", frs[0].GoogleDriveFileRevisionID)
+ assert.Equal("FileRevisionID1", frs[0].FileRevisionID)
assert.Equal("Name1", frs[0].Name)
})
@@ -85,8 +84,8 @@ func TestDocumentFileRevisionwModel(t *testing.T) {
Document: Document{
GoogleFileID: "GoogleFileID1",
},
- GoogleDriveFileRevisionID: "GoogleDriveFileRevisionID2",
- Name: "Name2",
+ FileRevisionID: "FileRevisionID2",
+ Name: "Name2",
}
err := fr.Create(db)
require.NoError(err)
@@ -99,13 +98,97 @@ func TestDocumentFileRevisionwModel(t *testing.T) {
require.NoError(err)
require.Len(frs, 2)
assert.EqualValues(1, frs[0].DocumentID)
- assert.Equal(
- "GoogleDriveFileRevisionID1", frs[0].GoogleDriveFileRevisionID)
+ assert.Equal("FileRevisionID1", frs[0].FileRevisionID)
assert.Equal("Name1", frs[0].Name)
assert.EqualValues(1, frs[1].DocumentID)
- assert.Equal(
- "GoogleDriveFileRevisionID2", frs[1].GoogleDriveFileRevisionID)
+ assert.Equal("FileRevisionID2", frs[1].FileRevisionID)
assert.Equal("Name2", frs[1].Name)
})
})
}
+
+// TestDocumentFileRevisionDualBackend verifies that file revision operations
+// work with both GoogleFileID and FileID backends.
+func TestDocumentFileRevisionDualBackend(t *testing.T) {
+ dsn := os.Getenv("HERMES_TEST_POSTGRESQL_DSN")
+ if dsn == "" {
+ t.Skip("HERMES_TEST_POSTGRESQL_DSN environment variable isn't set")
+ }
+
+ backends := []struct {
+ name string
+ makeDoc func(id string) Document
+ }{
+ {
+ name: "GoogleFileID",
+ makeDoc: func(id string) Document { return Document{GoogleFileID: id} },
+ },
+ {
+ name: "FileID",
+ makeDoc: func(id string) Document { return Document{FileID: id} },
+ },
+ }
+
+ for _, backend := range backends {
+ backend := backend
+ t.Run(backend.name, func(t *testing.T) {
+ assert, require := assert.New(t), require.New(t)
+ db, tearDownTest := setupTest(t, dsn)
+ defer tearDownTest(t)
+
+ // Setup.
+ dt := DocumentType{Name: "DT1", LongName: "DocumentType1"}
+ require.NoError(dt.FirstOrCreate(db))
+ p := Product{Name: "Product1", Abbreviation: "P1"}
+ require.NoError(p.FirstOrCreate(db))
+
+ // Create document.
+ d := backend.makeDoc("frTestFile1")
+ d.DocumentType = DocumentType{Name: "DT1"}
+ d.Product = Product{Name: "Product1"}
+ require.NoError(d.Create(db))
+
+ // Create file revision.
+ t.Run("Create file revision", func(t *testing.T) {
+ fr := DocumentFileRevision{
+ Document: backend.makeDoc("frTestFile1"),
+ FileRevisionID: "rev-001",
+ Name: "Revision 1",
+ }
+ err := fr.Create(db)
+ require.NoError(err)
+ })
+
+ // Find file revisions.
+ t.Run("Find file revisions", func(t *testing.T) {
+ var frs DocumentFileRevisions
+ err := frs.Find(db, backend.makeDoc("frTestFile1"))
+ require.NoError(err)
+ require.Len(frs, 1)
+ assert.Equal("rev-001", frs[0].FileRevisionID)
+ assert.Equal("Revision 1", frs[0].Name)
+ })
+
+ // Create second file revision.
+ t.Run("Create second file revision", func(t *testing.T) {
+ fr := DocumentFileRevision{
+ Document: backend.makeDoc("frTestFile1"),
+ FileRevisionID: "rev-002",
+ Name: "Revision 2",
+ }
+ err := fr.Create(db)
+ require.NoError(err)
+ })
+
+ // Find returns both.
+ t.Run("Find returns both revisions", func(t *testing.T) {
+ var frs DocumentFileRevisions
+ err := frs.Find(db, backend.makeDoc("frTestFile1"))
+ require.NoError(err)
+ require.Len(frs, 2)
+ assert.Equal("rev-001", frs[0].FileRevisionID)
+ assert.Equal("rev-002", frs[1].FileRevisionID)
+ })
+ })
+ }
+}
diff --git a/pkg/models/document_group_review.go b/pkg/models/document_group_review.go
index 9b3582307..be39ea77a 100644
--- a/pkg/models/document_group_review.go
+++ b/pkg/models/document_group_review.go
@@ -26,11 +26,8 @@ type DocumentGroupReviews []DocumentGroupReview
// BeforeSave is a hook to find or create associations before saving.
func (d *DocumentGroupReview) BeforeSave(tx *gorm.DB) error {
// Validate required fields.
- if err := validation.ValidateStruct(&d.Document,
- validation.Field(
- &d.Document.GoogleFileID, validation.Required),
- ); err != nil {
- return err
+ if d.Document.hasNoFileID() {
+ return fmt.Errorf("document must have either GoogleFileID or FileID")
}
if err := validation.ValidateStruct(&d.Group,
validation.Field(
@@ -50,23 +47,15 @@ func (d *DocumentGroupReview) BeforeSave(tx *gorm.DB) error {
// them to the receiver.
func (d *DocumentGroupReviews) Find(db *gorm.DB, dr DocumentGroupReview) error {
// Validate required fields.
- if err := validation.ValidateStruct(&dr.Document,
- validation.Field(
- &dr.Document.GoogleFileID,
- validation.When(dr.Group.EmailAddress == "",
- validation.Required.Error(
- "at least a Document's GoogleFileID or Group's EmailAddress is required"),
- ),
- ),
- ); err != nil {
- return err
+ if dr.Document.hasNoFileID() && dr.Group.EmailAddress == "" {
+ return fmt.Errorf("at least a Document's file ID or Group's EmailAddress is required")
}
if err := validation.ValidateStruct(&dr.Group,
validation.Field(
&dr.Group.EmailAddress,
- validation.When(dr.Document.GoogleFileID == "",
+ validation.When(dr.Document.hasNoFileID(),
validation.Required.Error(
- "at least a Document's GoogleFileID or Group's EmailAddress is required"),
+ "at least a Document's FileID or Group's EmailAddress is required"),
),
),
); err != nil {
@@ -74,7 +63,7 @@ func (d *DocumentGroupReviews) Find(db *gorm.DB, dr DocumentGroupReview) error {
}
// Get document.
- if dr.Document.GoogleFileID != "" {
+ if !dr.Document.hasNoFileID() {
if err := dr.Document.Get(db); err != nil {
return fmt.Errorf("error getting document: %w", err)
}
@@ -103,10 +92,8 @@ func (d *DocumentGroupReviews) Find(db *gorm.DB, dr DocumentGroupReview) error {
// receiver.
func (d *DocumentGroupReview) Get(db *gorm.DB) error {
// Validate required fields.
- if err := validation.ValidateStruct(&d.Document,
- validation.Field(&d.Document.GoogleFileID, validation.Required),
- ); err != nil {
- return err
+ if d.Document.hasNoFileID() {
+ return fmt.Errorf("document must have either GoogleFileID or FileID")
}
if err := validation.ValidateStruct(&d.Group,
validation.Field(&d.Group.EmailAddress, validation.Required),
diff --git a/pkg/models/document_group_review_test.go b/pkg/models/document_group_review_test.go
index b9a702474..568e02b35 100644
--- a/pkg/models/document_group_review_test.go
+++ b/pkg/models/document_group_review_test.go
@@ -225,3 +225,77 @@ func TestDocumentGroupReviewModel(t *testing.T) {
})
})
}
+
+// TestDocumentGroupReviewDualBackend verifies that DocumentGroupReview CRUD
+// operations work with both GoogleFileID and FileID backends.
+func TestDocumentGroupReviewDualBackend(t *testing.T) {
+ dsn := os.Getenv("HERMES_TEST_POSTGRESQL_DSN")
+ if dsn == "" {
+ t.Skip("HERMES_TEST_POSTGRESQL_DSN environment variable isn't set")
+ }
+
+ backends := []struct {
+ name string
+ makeDoc func(id string) Document
+ getID func(d Document) string
+ }{
+ {
+ name: "GoogleFileID",
+ makeDoc: func(id string) Document { return Document{GoogleFileID: id} },
+ getID: func(d Document) string { return d.GoogleFileID },
+ },
+ {
+ name: "FileID",
+ makeDoc: func(id string) Document { return Document{FileID: id} },
+ getID: func(d Document) string { return d.FileID },
+ },
+ }
+
+ for _, backend := range backends {
+ backend := backend
+ t.Run(backend.name, func(t *testing.T) {
+ assert, require := assert.New(t), require.New(t)
+ db, tearDownTest := setupTest(t, dsn)
+ defer tearDownTest(t)
+
+ // Setup.
+ dt := DocumentType{Name: "DT1", LongName: "DocumentType1"}
+ require.NoError(dt.FirstOrCreate(db))
+ p := Product{Name: "Product1", Abbreviation: "P1"}
+ require.NoError(p.FirstOrCreate(db))
+
+ // Create a document with approver groups.
+ d := backend.makeDoc("grpReviewTestFile1")
+ d.DocumentType = DocumentType{Name: "DT1"}
+ d.Product = Product{Name: "Product1"}
+ d.ApproverGroups = []*Group{
+ {EmailAddress: "team-alpha@test.com"},
+ {EmailAddress: "team-beta@test.com"},
+ }
+ require.NoError(d.Create(db))
+ assert.Len(d.ApproverGroups, 2)
+
+ // Get a group review.
+ t.Run("Get group review", func(t *testing.T) {
+ dr := DocumentGroupReview{
+ Document: backend.makeDoc("grpReviewTestFile1"),
+ Group: Group{EmailAddress: "team-beta@test.com"},
+ }
+ err := dr.Get(db)
+ require.NoError(err)
+ assert.Equal("grpReviewTestFile1", backend.getID(dr.Document))
+ assert.Equal("team-beta@test.com", dr.Group.EmailAddress)
+ })
+
+ // Find group reviews for document.
+ t.Run("Find group reviews", func(t *testing.T) {
+ var revs DocumentGroupReviews
+ err := revs.Find(db, DocumentGroupReview{
+ Document: backend.makeDoc("grpReviewTestFile1"),
+ })
+ require.NoError(err)
+ require.Len(revs, 2)
+ })
+ })
+ }
+}
diff --git a/pkg/models/document_related_resource_external_link.go b/pkg/models/document_related_resource_external_link.go
index f266c540c..c3c566cc0 100644
--- a/pkg/models/document_related_resource_external_link.go
+++ b/pkg/models/document_related_resource_external_link.go
@@ -22,8 +22,13 @@ type DocumentRelatedResourceExternalLinks []DocumentRelatedResourceExternalLink
func (rr *DocumentRelatedResourceExternalLink) Create(db *gorm.DB) error {
// Preload RelatedResource.Document.
if rr.RelatedResource.DocumentID == 0 {
- if err := db.
- Where(Document{GoogleFileID: rr.RelatedResource.Document.GoogleFileID}).
+ query := db
+ if rr.RelatedResource.Document.GoogleFileID != "" {
+ query = query.Where("google_file_id = ?", rr.RelatedResource.Document.GoogleFileID)
+ } else {
+ query = query.Where("file_id = ?", rr.RelatedResource.Document.FileID)
+ }
+ if err := query.
First(&rr.RelatedResource.Document).
Error; err != nil {
return fmt.Errorf("error preloading RelatedResource.Document: %w", err)
diff --git a/pkg/models/document_related_resource_hermes_document.go b/pkg/models/document_related_resource_hermes_document.go
index 8053281a0..a3df16431 100644
--- a/pkg/models/document_related_resource_hermes_document.go
+++ b/pkg/models/document_related_resource_hermes_document.go
@@ -19,8 +19,13 @@ type DocumentRelatedResourceHermesDocument struct {
func (rr *DocumentRelatedResourceHermesDocument) Create(db *gorm.DB) error {
// Preload Document.
if rr.DocumentID == 0 {
- if err := db.
- Where(Document{GoogleFileID: rr.Document.GoogleFileID}).
+ query := db
+ if rr.Document.GoogleFileID != "" {
+ query = query.Where("google_file_id = ?", rr.Document.GoogleFileID)
+ } else {
+ query = query.Where("file_id = ?", rr.Document.FileID)
+ }
+ if err := query.
First(&rr.Document).
Error; err != nil {
return fmt.Errorf("error preloading RelatedResource.Document: %w", err)
@@ -30,8 +35,13 @@ func (rr *DocumentRelatedResourceHermesDocument) Create(db *gorm.DB) error {
// Preload RelatedResource.Document.
if rr.RelatedResource.DocumentID == 0 {
- if err := db.
- Where(Document{GoogleFileID: rr.RelatedResource.Document.GoogleFileID}).
+ query := db
+ if rr.RelatedResource.Document.GoogleFileID != "" {
+ query = query.Where("google_file_id = ?", rr.RelatedResource.Document.GoogleFileID)
+ } else {
+ query = query.Where("file_id = ?", rr.RelatedResource.Document.FileID)
+ }
+ if err := query.
First(&rr.RelatedResource.Document).
Error; err != nil {
return fmt.Errorf("error preloading RelatedResource.Document: %w", err)
diff --git a/pkg/models/document_review.go b/pkg/models/document_review.go
index 14c515569..63912a815 100644
--- a/pkg/models/document_review.go
+++ b/pkg/models/document_review.go
@@ -35,11 +35,8 @@ type DocumentReviews []DocumentReview
// BeforeSave is a hook to find or create associations before saving.
func (d *DocumentReview) BeforeSave(tx *gorm.DB) error {
// Validate required fields.
- if err := validation.ValidateStruct(&d.Document,
- validation.Field(
- &d.Document.GoogleFileID, validation.Required),
- ); err != nil {
- return err
+ if d.Document.hasNoFileID() {
+ return fmt.Errorf("document must have either GoogleFileID or FileID")
}
if err := validation.ValidateStruct(&d.User,
validation.Field(
@@ -59,21 +56,14 @@ func (d *DocumentReview) BeforeSave(tx *gorm.DB) error {
// the receiver.
func (d *DocumentReviews) Find(db *gorm.DB, dr DocumentReview) error {
// Validate required fields.
- if err := validation.ValidateStruct(&dr.Document,
- validation.Field(
- &dr.Document.GoogleFileID,
- validation.When(dr.User.EmailAddress == "",
- validation.Required.Error("at least a Document's GoogleFileID or User's EmailAddress is required"),
- ),
- ),
- ); err != nil {
- return err
+ if dr.Document.hasNoFileID() && dr.User.EmailAddress == "" {
+ return fmt.Errorf("at least a Document's file ID or User's EmailAddress is required")
}
if err := validation.ValidateStruct(&dr.User,
validation.Field(
&dr.User.EmailAddress,
- validation.When(dr.Document.GoogleFileID == "",
- validation.Required.Error("at least a Document's GoogleFileID or User's EmailAddress is required"),
+ validation.When(dr.Document.hasNoFileID(),
+ validation.Required.Error("at least a Document's FileID or User's EmailAddress is required"),
),
),
); err != nil {
@@ -81,7 +71,7 @@ func (d *DocumentReviews) Find(db *gorm.DB, dr DocumentReview) error {
}
// Get document.
- if dr.Document.GoogleFileID != "" {
+ if !dr.Document.hasNoFileID() {
if err := dr.Document.Get(db); err != nil {
return fmt.Errorf("error getting document: %w", err)
}
@@ -110,10 +100,8 @@ func (d *DocumentReviews) Find(db *gorm.DB, dr DocumentReview) error {
// receiver.
func (d *DocumentReview) Get(db *gorm.DB) error {
// Validate required fields.
- if err := validation.ValidateStruct(&d.Document,
- validation.Field(&d.Document.GoogleFileID, validation.Required),
- ); err != nil {
- return err
+ if d.Document.hasNoFileID() {
+ return fmt.Errorf("document must have either GoogleFileID or FileID")
}
if err := validation.ValidateStruct(&d.User,
validation.Field(&d.User.EmailAddress, validation.Required),
diff --git a/pkg/models/document_review_test.go b/pkg/models/document_review_test.go
index d61b8d110..28d416507 100644
--- a/pkg/models/document_review_test.go
+++ b/pkg/models/document_review_test.go
@@ -355,3 +355,90 @@ func TestDocumentReviewModel(t *testing.T) {
})
})
}
+
+// TestDocumentReviewDualBackend verifies that DocumentReview CRUD operations
+// work with both GoogleFileID and FileID backends.
+func TestDocumentReviewDualBackend(t *testing.T) {
+ dsn := os.Getenv("HERMES_TEST_POSTGRESQL_DSN")
+ if dsn == "" {
+ t.Skip("HERMES_TEST_POSTGRESQL_DSN environment variable isn't set")
+ }
+
+ backends := []struct {
+ name string
+ makeDoc func(id string) Document
+ getID func(d Document) string
+ }{
+ {
+ name: "GoogleFileID",
+ makeDoc: func(id string) Document { return Document{GoogleFileID: id} },
+ getID: func(d Document) string { return d.GoogleFileID },
+ },
+ {
+ name: "FileID",
+ makeDoc: func(id string) Document { return Document{FileID: id} },
+ getID: func(d Document) string { return d.FileID },
+ },
+ }
+
+ for _, backend := range backends {
+ backend := backend
+ t.Run(backend.name, func(t *testing.T) {
+ assert, require := assert.New(t), require.New(t)
+ db, tearDownTest := setupTest(t, dsn)
+ defer tearDownTest(t)
+
+ // Setup.
+ dt := DocumentType{Name: "DT1", LongName: "DocumentType1"}
+ require.NoError(dt.FirstOrCreate(db))
+ p := Product{Name: "Product1", Abbreviation: "P1"}
+ require.NoError(p.FirstOrCreate(db))
+
+ // Create a document with approvers.
+ d := backend.makeDoc("reviewTestFile1")
+ d.DocumentType = DocumentType{Name: "DT1"}
+ d.Product = Product{Name: "Product1"}
+ d.Approvers = []*User{
+ {EmailAddress: "reviewer-a@test.com"},
+ {EmailAddress: "reviewer-b@test.com"},
+ }
+ require.NoError(d.Create(db))
+ assert.Len(d.Approvers, 2)
+
+ // Get a review.
+ t.Run("Get review", func(t *testing.T) {
+ dr := DocumentReview{
+ Document: backend.makeDoc("reviewTestFile1"),
+ User: User{EmailAddress: "reviewer-b@test.com"},
+ }
+ err := dr.Get(db)
+ require.NoError(err)
+ assert.Equal("reviewTestFile1", backend.getID(dr.Document))
+ assert.Equal("reviewer-b@test.com", dr.User.EmailAddress)
+ assert.Equal(UnspecifiedDocumentReviewStatus, dr.Status)
+ })
+
+ // Update a review.
+ t.Run("Update review", func(t *testing.T) {
+ dr := DocumentReview{
+ Document: backend.makeDoc("reviewTestFile1"),
+ User: User{EmailAddress: "reviewer-b@test.com"},
+ Status: ApprovedDocumentReviewStatus,
+ }
+ err := dr.Update(db)
+ require.NoError(err)
+ assert.Equal(ApprovedDocumentReviewStatus, dr.Status)
+ })
+
+ // Find reviews for document.
+ t.Run("Find reviews", func(t *testing.T) {
+ var revs DocumentReviews
+ err := revs.Find(db, DocumentReview{
+ Document: backend.makeDoc("reviewTestFile1"),
+ })
+ require.NoError(err)
+ require.Len(revs, 2)
+ })
+ })
+ }
+}
diff --git a/pkg/models/document_test.go b/pkg/models/document_test.go
index ed0be25c9..13ebe49aa 100644
--- a/pkg/models/document_test.go
+++ b/pkg/models/document_test.go
@@ -1,6 +1,7 @@
package models
import (
+ "fmt"
"os"
"testing"
"time"
@@ -1552,3 +1553,279 @@ func TestDocumentReplaceRelatedResources(t *testing.T) {
})
})
}
+
+// TestDocumentDualBackend tests that core document CRUD operations work with
+// both GoogleFileID (Google Workspace) and FileID (SharePoint) backends.
+// The main TestDocumentModel tests above use GoogleFileID; this test verifies
+// the FileID path works identically.
+func TestDocumentDualBackend(t *testing.T) {
+ dsn := os.Getenv("HERMES_TEST_POSTGRESQL_DSN")
+ if dsn == "" {
+ t.Skip("HERMES_TEST_POSTGRESQL_DSN environment variable isn't set")
+ }
+
+ // backends defines the two backends to test. Each provides a factory
+ // function that creates a Document with the correct file-ID field set.
+ backends := []struct {
+ name string
+ makeDoc func(id string) Document
+ getID func(d Document) string
+ }{
+ {
+ name: "GoogleFileID",
+ makeDoc: func(id string) Document { return Document{GoogleFileID: id} },
+ getID: func(d Document) string { return d.GoogleFileID },
+ },
+ {
+ name: "FileID",
+ makeDoc: func(id string) Document { return Document{FileID: id} },
+ getID: func(d Document) string { return d.FileID },
+ },
+ }
+
+ for _, backend := range backends {
+ backend := backend // capture
+ t.Run(backend.name, func(t *testing.T) {
+ db, tearDownTest := setupTest(t, dsn)
+ defer tearDownTest(t)
+
+ assert, require := assert.New(t), require.New(t)
+
+ // Setup: document type and product.
+ dt := DocumentType{Name: "DT1", LongName: "DocumentType1"}
+ require.NoError(dt.FirstOrCreate(db))
+ p := Product{Name: "Product1", Abbreviation: "P1"}
+ require.NoError(p.FirstOrCreate(db))
+
+ // --- Create ---
+ t.Run("Create", func(t *testing.T) {
+ d := backend.makeDoc("testFileID1")
+ d.DocumentType = DocumentType{Name: "DT1"}
+ d.Product = Product{Name: "Product1"}
+ d.Title = "Test Doc 1"
+ d.Status = WIPDocumentStatus
+
+ err := d.Create(db)
+ require.NoError(err)
+ assert.NotEmpty(d.ID)
+ assert.Equal("testFileID1", backend.getID(d))
+ })
+
+ // --- Get ---
+ t.Run("Get", func(t *testing.T) {
+ d := backend.makeDoc("testFileID1")
+ err := d.Get(db)
+ require.NoError(err)
+ assert.Equal("Test Doc 1", d.Title)
+ assert.Equal("testFileID1", backend.getID(d))
+ })
+
+ // --- Upsert ---
+ t.Run("Upsert", func(t *testing.T) {
+ d := backend.makeDoc("testFileID1")
+ d.DocumentType = DocumentType{Name: "DT1"}
+ d.Product = Product{Name: "Product1"}
+ d.Title = "Updated Title"
+ d.Status = InReviewDocumentStatus
+ err := d.Upsert(db)
+ require.NoError(err)
+
+ // Verify update took effect.
+ d2 := backend.makeDoc("testFileID1")
+ err = d2.Get(db)
+ require.NoError(err)
+ assert.Equal("Updated Title", d2.Title)
+ assert.Equal(InReviewDocumentStatus, d2.Status)
+ })
+
+ // --- Create second doc ---
+ t.Run("Create second document", func(t *testing.T) {
+ d := backend.makeDoc("testFileID2")
+ d.DocumentType = DocumentType{Name: "DT1"}
+ d.Product = Product{Name: "Product1"}
+ d.Title = "Test Doc 2"
+ d.Status = WIPDocumentStatus
+ err := d.Create(db)
+ require.NoError(err)
+ })
+
+ // --- Delete ---
+ t.Run("Delete", func(t *testing.T) {
+ d := backend.makeDoc("testFileID2")
+ err := d.Delete(db)
+ require.NoError(err)
+
+ // Verify it's gone.
+ d2 := backend.makeDoc("testFileID2")
+ err = d2.Get(db)
+ assert.Error(err)
+ })
+
+ // --- BeforeCreate validation ---
+ t.Run("BeforeCreate rejects empty file ID", func(t *testing.T) {
+ d := Document{
+ DocumentType: DocumentType{Name: "DT1"},
+ Product: Product{Name: "Product1"},
+ Title: "No File ID",
+ }
+ err := d.Create(db)
+ assert.Error(err, "should reject document with no file ID")
+ })
+ })
+ }
+}
+
+// TestDocumentDualBackendCoexistence verifies that a Google doc and a SharePoint
+// doc can coexist in the same database (different documents with different ID
+// fields).
+func TestDocumentDualBackendCoexistence(t *testing.T) {
+ dsn := os.Getenv("HERMES_TEST_POSTGRESQL_DSN")
+ if dsn == "" {
+ t.Skip("HERMES_TEST_POSTGRESQL_DSN environment variable isn't set")
+ }
+
+ db, tearDownTest := setupTest(t, dsn)
+ defer tearDownTest(t)
+
+ assert, require := assert.New(t), require.New(t)
+
+ // Setup.
+ dt := DocumentType{Name: "DT1", LongName: "DocumentType1"}
+ require.NoError(dt.FirstOrCreate(db))
+ p := Product{Name: "Product1", Abbreviation: "P1"}
+ require.NoError(p.FirstOrCreate(db))
+
+ // Create a Google doc.
+ googleDoc := Document{
+ GoogleFileID: "google-doc-123",
+ DocumentType: DocumentType{Name: "DT1"},
+ Product: Product{Name: "Product1"},
+ Title: "Google Doc",
+ Status: WIPDocumentStatus,
+ }
+ require.NoError(googleDoc.Create(db))
+
+ // Create a SharePoint doc.
+ spDoc := Document{
+ FileID: "sharepoint-doc-456",
+ DocumentType: DocumentType{Name: "DT1"},
+ Product: Product{Name: "Product1"},
+ Title: "SharePoint Doc",
+ Status: WIPDocumentStatus,
+ }
+ require.NoError(spDoc.Create(db))
+
+ // Both should be independently retrievable.
+ t.Run("Get Google doc by GoogleFileID", func(t *testing.T) {
+ d := Document{GoogleFileID: "google-doc-123"}
+ require.NoError(d.Get(db))
+ assert.Equal("Google Doc", d.Title)
+ assert.Equal("google-doc-123", d.GoogleFileID)
+ assert.Empty(d.FileID)
+ })
+
+ t.Run("Get SharePoint doc by FileID", func(t *testing.T) {
+ d := Document{FileID: "sharepoint-doc-456"}
+ require.NoError(d.Get(db))
+ assert.Equal("SharePoint Doc", d.Title)
+ assert.Equal("sharepoint-doc-456", d.FileID)
+ assert.Empty(d.GoogleFileID)
+ })
+
+ // GetFileIdentifier should return the correct ID for each.
+ t.Run("GetFileIdentifier returns correct ID", func(t *testing.T) {
+ gd := Document{GoogleFileID: "google-doc-123"}
+ require.NoError(gd.Get(db))
+ assert.Equal("google-doc-123", gd.GetFileIdentifier())
+
+ sd := Document{FileID: "sharepoint-doc-456"}
+ require.NoError(sd.Get(db))
+ assert.Equal("sharepoint-doc-456", sd.GetFileIdentifier())
+ })
+
+ // hasNoFileID should work correctly.
+ t.Run("hasNoFileID", func(t *testing.T) {
+ empty := Document{}
+ assert.True(empty.hasNoFileID(), "empty doc should have no file ID")
+
+ gd := Document{GoogleFileID: "abc"}
+ assert.False(gd.hasNoFileID(), "doc with GoogleFileID should not be empty")
+
+ sd := Document{FileID: "xyz"}
+ assert.False(sd.hasNoFileID(), "doc with FileID should not be empty")
+ })
+
+ // Find should return both documents.
+ t.Run("Find returns docs from both backends", func(t *testing.T) {
+ var docs Documents
+ err := docs.Find(db, "status = ?", WIPDocumentStatus)
+ require.NoError(err)
+ require.Len(docs, 2)
+ titles := []string{docs[0].Title, docs[1].Title}
+ assert.Contains(titles, "Google Doc")
+ assert.Contains(titles, "SharePoint Doc")
+ })
+
+ // Upsert each doc independently.
+ t.Run("Upsert Google doc", func(t *testing.T) {
+ d := Document{
+ GoogleFileID: "google-doc-123",
+ DocumentType: DocumentType{Name: "DT1"},
+ Product: Product{Name: "Product1"},
+ Title: "Google Doc Updated",
+ Status: InReviewDocumentStatus,
+ }
+ require.NoError(d.Upsert(db))
+
+ d2 := Document{GoogleFileID: "google-doc-123"}
+ require.NoError(d2.Get(db))
+ assert.Equal("Google Doc Updated", d2.Title)
+ })
+
+ t.Run("Upsert SharePoint doc", func(t *testing.T) {
+ d := Document{
+ FileID: "sharepoint-doc-456",
+ DocumentType: DocumentType{Name: "DT1"},
+ Product: Product{Name: "Product1"},
+ Title: "SharePoint Doc Updated",
+ Status: InReviewDocumentStatus,
+ }
+ require.NoError(d.Upsert(db))
+
+ d2 := Document{FileID: "sharepoint-doc-456"}
+ require.NoError(d2.Get(db))
+ assert.Equal("SharePoint Doc Updated", d2.Title)
+ })
+
+ // Delete each doc independently.
+ t.Run("Delete Google doc", func(t *testing.T) {
+ d := Document{GoogleFileID: "google-doc-123"}
+ require.NoError(d.Delete(db))
+
+ d2 := Document{GoogleFileID: "google-doc-123"}
+ assert.Error(d2.Get(db))
+ })
+
+ t.Run("SharePoint doc still exists after Google doc deleted", func(t *testing.T) {
+ d := Document{FileID: "sharepoint-doc-456"}
+ require.NoError(d.Get(db))
+ assert.Equal("SharePoint Doc Updated", d.Title)
+ })
+
+ t.Run("Delete SharePoint doc", func(t *testing.T) {
+ d := Document{FileID: "sharepoint-doc-456"}
+ require.NoError(d.Delete(db))
+
+ d2 := Document{FileID: "sharepoint-doc-456"}
+ assert.Error(d2.Get(db))
+ })
+
+ // Verify both are gone.
+ t.Run("Both documents deleted", func(t *testing.T) {
+ var docs Documents
+ err := docs.Find(db, fmt.Sprintf("status = %d OR status = %d",
+ WIPDocumentStatus, InReviewDocumentStatus))
+ require.NoError(err)
+ assert.Empty(docs)
+ })
+}
diff --git a/pkg/models/indexer_folder.go b/pkg/models/indexer_folder.go
index 849427f61..19b7e00af 100644
--- a/pkg/models/indexer_folder.go
+++ b/pkg/models/indexer_folder.go
@@ -14,8 +14,11 @@ import (
type IndexerFolder struct {
gorm.Model
- // GoogleDriveID is the Google Drive ID of the folder.
- GoogleDriveID string `gorm:"default:null;not null;uniqueIndex"`
+ // GoogleDriveID is the Google Drive ID of the folder (optional for SharePoint).
+ GoogleDriveID string `gorm:"default:null;uniqueIndex"`
+
+ // SharePointFolderID is the SharePoint folder ID (optional for Google Workspace).
+ SharePointFolderID string `gorm:"default:null;uniqueIndex"`
// LastIndexedAt is the time that the folder was last indexed.
LastIndexedAt time.Time
@@ -24,7 +27,8 @@ type IndexerFolder struct {
// Get gets the indexer folder and assigns it to the receiver.
func (f *IndexerFolder) Get(db *gorm.DB) error {
if err := validation.ValidateStruct(f,
- validation.Field(&f.GoogleDriveID, validation.Required),
+ validation.Field(&f.GoogleDriveID, validation.When(f.SharePointFolderID == "", validation.Required)),
+ validation.Field(&f.SharePointFolderID, validation.When(f.GoogleDriveID == "", validation.Required)),
); err != nil {
return err
}
@@ -34,24 +38,31 @@ func (f *IndexerFolder) Get(db *gorm.DB) error {
log.Default(),
logger.Config{IgnoreRecordNotFoundError: true},
)})
- return tx.
- Where(IndexerFolder{GoogleDriveID: f.GoogleDriveID}).
- First(&f).
- Error
+
+ // Query based on the provided ID.
+ if f.GoogleDriveID != "" {
+ return tx.Where(IndexerFolder{GoogleDriveID: f.GoogleDriveID}).First(&f).Error
+ }
+ return tx.Where(IndexerFolder{SharePointFolderID: f.SharePointFolderID}).First(&f).Error
}
// Upsert updates or inserts the receiver indexer folder into database db.
func (l *IndexerFolder) Upsert(db *gorm.DB) error {
if err := validation.ValidateStruct(l,
- validation.Field(&l.GoogleDriveID, validation.Required),
+ validation.Field(&l.GoogleDriveID, validation.When(l.SharePointFolderID == "", validation.Required)),
+ validation.Field(&l.SharePointFolderID, validation.When(l.GoogleDriveID == "", validation.Required)),
); err != nil {
return err
}
- tx := db.
- Where(IndexerFolder{GoogleDriveID: l.GoogleDriveID}).
- Assign(*l).
- FirstOrCreate(&l)
+ tx := db
+ if l.GoogleDriveID != "" {
+ tx = tx.Where(IndexerFolder{GoogleDriveID: l.GoogleDriveID})
+ } else {
+ tx = tx.Where(IndexerFolder{SharePointFolderID: l.SharePointFolderID})
+ }
+
+ tx = tx.Assign(*l).FirstOrCreate(&l)
if err := tx.Error; err != nil {
return err
}
diff --git a/pkg/models/project_related_resource_hermes_document.go b/pkg/models/project_related_resource_hermes_document.go
index 6ebadc82b..69354aaa6 100644
--- a/pkg/models/project_related_resource_hermes_document.go
+++ b/pkg/models/project_related_resource_hermes_document.go
@@ -40,8 +40,13 @@ func (rr *ProjectRelatedResourceHermesDocument) Create(db *gorm.DB) error {
// Preload Document.
if rr.DocumentID == 0 {
- if err := db.
- Where(Document{GoogleFileID: rr.Document.GoogleFileID}).
+ query := db
+ if rr.Document.GoogleFileID != "" {
+ query = query.Where("google_file_id = ?", rr.Document.GoogleFileID)
+ } else {
+ query = query.Where("file_id = ?", rr.Document.FileID)
+ }
+ if err := query.
First(&rr.Document).
Error; err != nil {
return fmt.Errorf("error preloading RelatedResource.Document: %w", err)
diff --git a/pkg/sharepointhelper/document_operations.go b/pkg/sharepointhelper/document_operations.go
new file mode 100644
index 000000000..ef4d57bcd
--- /dev/null
+++ b/pkg/sharepointhelper/document_operations.go
@@ -0,0 +1,1131 @@
+package sharepointhelper
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+)
+
+// CopyFileResponse represents the response from the Microsoft Graph API when copying a file.
+type CopyFileResponse struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ WebURL string `json:"webUrl"`
+ CreatedAt string `json:"createdDateTime"`
+ LastModified string `json:"lastModifiedDateTime"`
+ FileExtension string `json:"fileExtension"`
+}
+
+// Permission represents a SharePoint permission
+// Permission related structs
+
+type GrantedTo struct {
+ User UserDetails `json:"user"`
+}
+
+type UserDetails struct {
+ DisplayName string `json:"displayName"`
+ Email string `json:"email"`
+ ID string `json:"id"`
+}
+
+type Permission struct {
+ ID string `json:"id"`
+ GrantedTo GrantedTo `json:"grantedTo"`
+ Role []string `json:"roles"`
+}
+
+// groupMetadata holds selected Microsoft Graph group properties for classification.
+type groupMetadata struct {
+ ID string `json:"id"`
+ Mail string `json:"mail"`
+ DisplayName string `json:"displayName"`
+ MailEnabled bool `json:"mailEnabled"`
+ SecurityEnabled bool `json:"securityEnabled"`
+ GroupTypes []string `json:"groupTypes"`
+}
+
+// --------------x------------------
+
+// SharePermissionResponse represents the response from the Microsoft Graph API when setting permissions.
+type SharePermissionResponse struct {
+ ID string `json:"id"`
+ InviteEmail string `json:"inviteEmail"`
+ Roles []string `json:"roles"`
+}
+
+// CopyFile copies a template file to create a new document in SharePoint
+// folderPath can be a folder name or path like "/DraftDocuments" or "DraftDocuments"
+func (s *Service) CopyFile(templateID, fileName, folderPath string) (*CopyFileResponse, error) {
+ // First, resolve the folder path to get the folder ID
+ folderID, err := s.ResolveFolderPath(folderPath)
+ if err != nil {
+ return nil, fmt.Errorf("error resolving folder path '%s': %w", folderPath, err)
+ }
+
+ s.Logger.Debug("resolved folder path", "path", folderPath, "folder_id", folderID)
+
+ // Construct the Microsoft Graph API URL for copying a file
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s/copy",
+ s.SiteID, s.DriveID, templateID)
+
+ // Prepare the request body
+ body := map[string]interface{}{
+ "name": fileName,
+ "parentReference": map[string]string{
+ "driveId": s.DriveID,
+ "id": folderID, // Use the resolved folder ID
+ },
+ // Add conflict behavior to rename the file if it already exists
+ "@microsoft.graph.conflictBehavior": "rename",
+ }
+
+ jsonBody, err := json.Marshal(body)
+ if err != nil {
+ return nil, fmt.Errorf("error marshaling request body: %w", err)
+ }
+
+ // Log the request details
+ s.Logger.Debug("initiating SharePoint file copy", "template_id", templateID, "file_name", fileName, "folder_path", folderPath)
+
+ // Make the authenticated request using InvokeAPI
+ resp, err := s.InvokeAPI("POST", url, bytes.NewBuffer(jsonBody))
+ if err != nil {
+ return nil, fmt.Errorf("error making copy request to SharePoint: %w", err)
+ }
+ defer resp.Body.Close()
+
+ // Check for a successful response
+ if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("failed to copy file: %s, %s", resp.Status, string(body))
+ }
+
+ // Graph API copy operation is asynchronous, so we need to check the Location header
+ // to monitor the status of the operation
+ monitorURL := resp.Header.Get("Location")
+ if monitorURL == "" {
+ return nil, fmt.Errorf("no Location header found in copy response")
+ }
+
+ // Poll the monitor URL until the operation completes
+ fileID, err := s.monitorCopyOperation(monitorURL)
+ if err != nil {
+ // Check if this is a "nameAlreadyExists" error
+ if strings.Contains(err.Error(), "nameAlreadyExists") {
+ s.Logger.Warn("file already exists, attempting to find existing file", "file_name", fileName)
+
+ // Try to find the existing file by name in the destination folder
+ existingFile, findErr := s.FindFileByName(folderPath, fileName)
+ if findErr != nil {
+ // If we can't find the existing file, return the original error
+ return nil, fmt.Errorf("file already exists but couldn't locate it: %w", err)
+ }
+
+ s.Logger.Info("found existing file instead of copying", "file_id", existingFile.ID, "file_name", fileName)
+ return existingFile, nil
+ }
+
+ // For any other error, return it
+ return nil, err
+ }
+
+ // Get file details
+ fileDetails, err := s.GetFileDetails(fileID)
+ if err != nil {
+ return nil, err
+ }
+
+ return fileDetails, nil
+}
+
+// FindFileByName searches for a file by name in the specified folder
+// folderPath can be a folder ID or a path like "/DraftDocuments"
+func (s *Service) FindFileByName(folderPath, fileName string) (*CopyFileResponse, error) {
+ // Resolve the folder path to a folder ID
+ folderID, err := s.ResolveFolderPath(folderPath)
+ if err != nil {
+ return nil, fmt.Errorf("error resolving folder path '%s': %w", folderPath, err)
+ }
+
+ // Construct the Microsoft Graph API URL to list items in the folder
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s/children",
+ s.SiteID, s.DriveID, folderID)
+
+ resp, err := s.InvokeAPI("GET", url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error calling Graph API to find file: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("failed to list folder contents: %s, %s", resp.Status, string(body))
+ }
+
+ // Parse the response
+ var folderContents struct {
+ Value []CopyFileResponse `json:"value"`
+ }
+
+ if err := json.NewDecoder(resp.Body).Decode(&folderContents); err != nil {
+ return nil, fmt.Errorf("error decoding folder contents: %w", err)
+ }
+
+ // Look for the file with the matching name
+ for _, file := range folderContents.Value {
+ if file.Name == fileName {
+ return &file, nil
+ }
+ }
+
+ return nil, fmt.Errorf("file with name '%s' not found in folder", fileName)
+}
+
+// monitorCopyOperation polls the monitor URL until the copy operation completes
+func (s *Service) monitorCopyOperation(monitorURL string) (string, error) {
+ maxAttempts := 30 // Increase from 10 to 30 attempts
+
+ s.Logger.Debug("monitoring SharePoint copy operation", "monitor_url", monitorURL)
+
+ for i := 0; i < maxAttempts; i++ {
+ // Add a small delay before each attempt to give SharePoint time to process
+ if i > 0 {
+ time.Sleep(2 * time.Second)
+ }
+
+ req, err := http.NewRequest("GET", monitorURL, nil)
+ if err != nil {
+ return "", fmt.Errorf("error creating monitor request: %w", err)
+ }
+ // Important: Do NOT set Authorization header for the monitor URL
+ // The monitor URL is a special URL that doesn't require authorization
+
+ resp, err := s.httpClient.Do(req)
+ if err != nil {
+ s.Logger.Warn("error on copy monitor attempt", "attempt", i+1, "max_attempts", maxAttempts, "error", err)
+ continue // Try again rather than failing immediately
+ }
+
+ defer resp.Body.Close()
+
+ body, _ := io.ReadAll(resp.Body)
+
+ if resp.StatusCode == http.StatusOK {
+ // Operation completed
+ var result struct {
+ ResourceID string `json:"resourceId"`
+ }
+
+ // Reset the reader for JSON decoding
+ resp.Body = io.NopCloser(bytes.NewBuffer(body))
+
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ s.Logger.Warn("error decoding copy monitor response", "error", err)
+ // Try again rather than failing immediately
+ continue
+ }
+
+ if result.ResourceID == "" {
+ // Continue without logging - this is normal during operation progress
+ continue
+ }
+
+ // Operation completed successfully
+ s.Logger.Info("SharePoint copy operation completed", "resource_id", result.ResourceID)
+ return result.ResourceID, nil
+
+ } else if resp.StatusCode == http.StatusAccepted {
+ // Operation still in progress - continue without logging
+ continue
+ } else if resp.StatusCode == http.StatusConflict {
+ // This is likely a duplicate request or name conflict
+ s.Logger.Warn("conflict detected in copy operation", "attempt", i+1, "message", "This may indicate a duplicate file")
+
+ // Try to extract resource ID from the error response if available
+ var errResponse struct {
+ Error struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+ } `json:"error"`
+ }
+
+ if err := json.Unmarshal(body, &errResponse); err == nil && errResponse.Error.Code == "nameAlreadyExists" {
+ s.Logger.Info("name already exists error detected, will continue with existing file")
+
+ // We need to return something to indicate we should proceed with the existing file
+ // For now, return an error that can be recognized
+ return "", fmt.Errorf("nameAlreadyExists: %s", string(body))
+ }
+
+ // Other conflict type, retry
+ continue
+ } else {
+ // Any other error status - log but continue retrying
+ s.Logger.Warn("unexpected status from copy monitor URL", "status_code", resp.StatusCode, "status", resp.Status)
+ // Try again rather than failing immediately
+ continue
+ }
+ }
+
+ return "", fmt.Errorf("copy operation timed out after %d attempts", maxAttempts)
+}
+
+// GetFileDetails retrieves details of a file by its ID
+func (s *Service) GetFileDetails(fileID string) (*CopyFileResponse, error) {
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s",
+ s.SiteID, s.DriveID, fileID)
+
+ resp, err := s.InvokeAPI("GET", url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error calling Graph API for file details: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("failed to get file details: %s, %s", resp.Status, string(body))
+ }
+
+ var fileDetails CopyFileResponse
+ if err := json.NewDecoder(resp.Body).Decode(&fileDetails); err != nil {
+ return nil, fmt.Errorf("error decoding file details response: %w", err)
+ }
+
+ return &fileDetails, nil
+}
+
+type inviteRecipient struct {
+ Email string `json:"email,omitempty"`
+ ObjectID string `json:"objectId,omitempty"`
+}
+
+func normalizeShareRoles(role string) ([]string, error) {
+ switch role {
+ case "reader":
+ return []string{"read"}, nil
+ case "writer":
+ return []string{"write"}, nil
+ default:
+ return nil, fmt.Errorf("invalid role: %s", role)
+ }
+}
+
+func (s *Service) shareFileWithRecipients(fileID, role string, recipients []inviteRecipient, sendInvitation bool) error {
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s/invite",
+ s.SiteID, s.DriveID, fileID)
+
+ roles, err := normalizeShareRoles(role)
+ if err != nil {
+ return err
+ }
+
+ body := map[string]interface{}{
+ "recipients": recipients,
+ "roles": roles,
+ "requireSignIn": true,
+ "sendInvitation": sendInvitation,
+ }
+
+ jsonBody, err := json.Marshal(body)
+ if err != nil {
+ return fmt.Errorf("error marshaling request body: %w", err)
+ }
+
+ resp, err := s.InvokeAPI("POST", url, bytes.NewBuffer(jsonBody))
+ if err != nil {
+ return fmt.Errorf("error calling Graph API to share file: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("failed to share file: %s, %s", resp.Status, string(body))
+ }
+
+ return nil
+}
+
+// ShareFile shares a file with a user with the specified role.
+func (s *Service) ShareFile(fileID, userEmail, role string) error {
+ recipient := inviteRecipient{Email: userEmail}
+ return s.shareFileWithRecipients(fileID, role, []inviteRecipient{recipient}, true)
+}
+
+// ShareFile shares a file with multiple users with the specified role.
+func (s *Service) ShareFileWithMultipleUsers(fileID, role string, userEmails []string) error {
+ recipients := make([]inviteRecipient, 0, len(userEmails))
+ for _, email := range userEmails {
+ recipients = append(recipients, inviteRecipient{Email: email})
+ }
+
+ return s.shareFileWithRecipients(fileID, role, recipients, true)
+}
+
+// ShareFileWithGroupMembers shares a file with all (transitive) members of a
+// Microsoft 365 group, distribution list, or mail-enabled security group
+// identified by its email address. It expands the group membership via the
+// Graph API recurssively and then reuses ShareFileWithMultipleUsers in batches.
+// Limitations / Notes:
+// - Service principals / devices are ignored; only user objects with a
+// resolvable mail or userPrincipalName are considered.
+// - Large groups are processed in batches (size 50) to honor Graph invite
+// endpoint limits.
+// - If a member has no mail attribute, userPrincipalName is used.
+func (s *Service) ShareFileWithGroupMembers(fileID, groupEmail, role string) error {
+ if groupEmail == "" {
+ return fmt.Errorf("groupEmail cannot be empty")
+ }
+
+ meta, err := s.resolveGroupByEmail(groupEmail)
+ if err != nil {
+ return fmt.Errorf("failed to resolve group '%s': %w", groupEmail, err)
+ }
+
+ memberEmails, err := s.enumerateGroupMemberEmails(meta.ID)
+ if err != nil {
+ return fmt.Errorf("failed to enumerate members for group '%s': %w", groupEmail, err)
+ }
+ if len(memberEmails) == 0 {
+ s.Logger.Warn("group has no resolvable members to share with", "group", groupEmail)
+ return nil
+ }
+
+ const batchSize = 50
+ if len(memberEmails) <= batchSize {
+ if err := s.ShareFileWithMultipleUsers(fileID, role, memberEmails); err != nil {
+ return fmt.Errorf("error sharing file with group '%s' members: %w", groupEmail, err)
+ }
+ s.Logger.Debug("completed sharing of group members", "group", groupEmail, "total_members", len(memberEmails))
+ return nil
+ }
+
+ for i := 0; i < len(memberEmails); i += batchSize {
+ end := i + batchSize
+ if end > len(memberEmails) {
+ end = len(memberEmails)
+ }
+ batch := memberEmails[i:end]
+ if err := s.ShareFileWithMultipleUsers(fileID, role, batch); err != nil {
+ return fmt.Errorf("error sharing file with group '%s' members batch %d: %w", groupEmail, i/batchSize, err)
+ }
+ }
+ s.Logger.Info("completed sequential sharing of group members", "group", groupEmail, "total_members", len(memberEmails), "batches", (len(memberEmails)+batchSize-1)/batchSize)
+ return nil
+}
+
+// GetGroupMemberEmails returns all unique (transitive) member email identifiers
+// for the Microsoft 365 group / distribution list
+func (s *Service) GetGroupMemberEmails(groupEmail string) ([]string, error) {
+ if strings.TrimSpace(groupEmail) == "" {
+ return nil, fmt.Errorf("groupEmail cannot be empty")
+ }
+ meta, err := s.resolveGroupIdentifier(groupEmail)
+ if err != nil {
+ return nil, fmt.Errorf("failed to resolve group '%s': %w", groupEmail, err)
+ }
+ emails, err := s.enumerateGroupMemberEmails(meta.ID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to enumerate members for group '%s': %w", groupEmail, err)
+ }
+ return emails, nil
+}
+
+// enumerateGroupMemberEmails returns a de-duplicated slice of user email identifiers (mail or UPN)
+// for all members of the given group, expanding nested groups recursively using the /members endpoint.
+func (s *Service) enumerateGroupMemberEmails(rootGroupID string) ([]string, error) {
+ if strings.TrimSpace(rootGroupID) == "" {
+ return nil, fmt.Errorf("groupID cannot be empty")
+ }
+ type directoryObject struct {
+ ID string `json:"id"`
+ Mail string `json:"mail"`
+ UserPrincipalName string `json:"userPrincipalName"`
+ ODataType string `json:"@odata.type"`
+ }
+ queue := []string{rootGroupID}
+ visitedGroups := map[string]struct{}{strings.ToLower(rootGroupID): {}}
+ uniqEmails := map[string]struct{}{}
+ var emails []string
+ for len(queue) > 0 {
+ gid := queue[0]
+ queue = queue[1:]
+ membersURL := fmt.Sprintf("https://graph.microsoft.com/v1.0/groups/%s/members", gid)
+ for membersURL != "" {
+ mr, err := s.InvokeAPI("GET", membersURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("invoke members (group %s): %w", gid, err)
+ }
+ if mr.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(mr.Body)
+ mr.Body.Close()
+ return nil, fmt.Errorf("members returned %s for group %s: %s", mr.Status, gid, string(body))
+ }
+ var page struct {
+ Value []directoryObject `json:"value"`
+ NextLink string `json:"@odata.nextLink"`
+ }
+ if err := json.NewDecoder(mr.Body).Decode(&page); err != nil {
+ mr.Body.Close()
+ return nil, fmt.Errorf("decode members page (group %s): %w", gid, err)
+ }
+ mr.Body.Close()
+ for _, obj := range page.Value {
+ if strings.EqualFold(obj.ODataType, "#microsoft.graph.group") {
+ lowered := strings.ToLower(obj.ID)
+ if _, seen := visitedGroups[lowered]; !seen && lowered != "" {
+ visitedGroups[lowered] = struct{}{}
+ queue = append(queue, obj.ID)
+ }
+ continue
+ }
+ email := obj.Mail
+ if email == "" {
+ email = obj.UserPrincipalName
+ }
+ if strings.TrimSpace(email) == "" {
+ continue
+ }
+ key := strings.ToLower(strings.TrimSpace(email))
+ if _, exists := uniqEmails[key]; exists {
+ continue
+ }
+ uniqEmails[key] = struct{}{}
+ emails = append(emails, email)
+ }
+ membersURL = page.NextLink
+ }
+ }
+ s.Logger.Debug("enumerated group members recursively", "group_id", rootGroupID, "member_count", len(emails))
+ return emails, nil
+}
+
+func (s *Service) resolveGroupIdentifier(groupIdentifier string) (*groupMetadata, error) {
+ if strings.TrimSpace(groupIdentifier) == "" {
+ return nil, fmt.Errorf("group identifier cannot be empty")
+ }
+
+ if meta, err := s.resolveGroupByEmail(groupIdentifier); err == nil {
+ return meta, nil
+ }
+
+ return s.resolveGroupByDisplayName(groupIdentifier)
+}
+
+func (s *Service) resolveGroupByDisplayName(displayName string) (*groupMetadata, error) {
+ if strings.TrimSpace(displayName) == "" {
+ return nil, fmt.Errorf("group displayName cannot be empty")
+ }
+
+ filterExpr := fmt.Sprintf("displayName eq '%s'", displayName)
+ lookupURL := fmt.Sprintf("https://graph.microsoft.com/v1.0/groups?$filter=%s&$select=id,displayName,mail,mailEnabled,securityEnabled,groupTypes", url.QueryEscape(filterExpr))
+
+ resp, err := s.InvokeAPI("GET", lookupURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("group displayName lookup failed: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("group displayName lookup returned %s: %s", resp.Status, string(body))
+ }
+
+ var result struct {
+ Value []groupMetadata `json:"value"`
+ }
+ if decErr := json.NewDecoder(resp.Body).Decode(&result); decErr != nil {
+ return nil, fmt.Errorf("error decoding group displayName response: %w", decErr)
+ }
+
+ for _, g := range result.Value {
+ if strings.EqualFold(g.DisplayName, displayName) {
+ return &g, nil
+ }
+ }
+ if len(result.Value) > 0 {
+ return &result.Value[0], nil
+ }
+
+ return nil, fmt.Errorf("group with display name '%s' not found", displayName)
+}
+
+// resolveGroupByEmail resolves group metadata (id + classification fields) by email.
+// It first tries an encoded $filter on the mail attribute, then falls back to $search.
+func (s *Service) resolveGroupByEmail(groupEmail string) (*groupMetadata, error) {
+ if strings.TrimSpace(groupEmail) == "" {
+ return nil, fmt.Errorf("groupEmail cannot be empty")
+ }
+ filterExpr := fmt.Sprintf("mail eq '%s'", groupEmail)
+ lookupURL := fmt.Sprintf("https://graph.microsoft.com/v1.0/groups?$filter=%s&$select=id,displayName,mail,mailEnabled,securityEnabled,groupTypes", url.QueryEscape(filterExpr))
+
+ // Try filter first
+ if resp, err := s.InvokeAPI("GET", lookupURL, nil); err == nil && resp != nil {
+ defer resp.Body.Close()
+ if resp.StatusCode == http.StatusOK {
+ var result struct {
+ Value []groupMetadata `json:"value"`
+ }
+ if decErr := json.NewDecoder(resp.Body).Decode(&result); decErr == nil {
+ if len(result.Value) > 0 {
+ return &result.Value[0], nil
+ }
+ }
+ }
+ }
+
+ // Fallback to search (requires ConsistencyLevel header)
+ searchURL := fmt.Sprintf("https://graph.microsoft.com/v1.0/groups?$search=\"mail:%s\"&$select=id,displayName,mail,mailEnabled,securityEnabled,groupTypes", groupEmail)
+ options := &APIOptions{Headers: map[string]string{"ConsistencyLevel": "eventual"}}
+ resp, err := s.InvokeAPIWithOptions("GET", searchURL, nil, options)
+ if err != nil {
+ return nil, fmt.Errorf("group search failed: %w", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("group search returned %s: %s", resp.Status, string(body))
+ }
+ var searchResult struct {
+ Value []groupMetadata `json:"value"`
+ }
+ if decErr := json.NewDecoder(resp.Body).Decode(&searchResult); decErr != nil {
+ return nil, fmt.Errorf("error decoding group search response: %w", decErr)
+ }
+ // Attempt exact mail match first
+ for _, g := range searchResult.Value {
+ if strings.EqualFold(g.Mail, groupEmail) {
+ return &g, nil
+ }
+ }
+ if len(searchResult.Value) > 0 {
+ return &searchResult.Value[0], nil
+ }
+ return nil, fmt.Errorf("group with email '%s' not found", groupEmail)
+}
+
+// ShareFileWithGroupOrMembers attempts to share a file directly with the group
+// (so future membership changes are honored automatically). If the Graph API
+// rejects the recipient (common for pure Distribution Lists that are not
+// security principals), it falls back to expanding the group membership and
+// sharing with individual members (snapshot approach).
+// This gives the best of both worlds: dynamic permissions when possible, and
+// functional access otherwise.
+func (s *Service) ShareFileWithGroupOrMembers(fileID, groupEmail, role string) error {
+ meta, err := s.resolveGroupIdentifier(groupEmail)
+ if err != nil {
+ return fmt.Errorf("failed to resolve group metadata: %w", err)
+ }
+
+ // If it's a classic Distribution List (mail-enabled, not security, not Unified),
+ // skip direct share attempt and immediately expand members (DLs aren't security principals).
+ if isDistributionListGroup(meta) {
+ s.Logger.Debug("distribution list detected; expanding members instead of direct share", "group", groupEmail, "id", meta.ID)
+ if err := s.ShareFileWithGroupMembers(fileID, groupEmail, role); err != nil {
+ return fmt.Errorf("member expansion failed for distribution list '%s': %w", groupEmail, err)
+ }
+ return nil
+ }
+
+ // Non-DL path: attempt direct group share first, then fallback to member expansion.
+ shareTarget := groupEmail
+ if meta.Mail != "" {
+ shareTarget = meta.Mail
+ }
+
+ if err := s.ShareFile(fileID, shareTarget, role); err == nil {
+ s.Logger.Debug("shared file directly with non-DL group", "group", groupEmail, "id", meta.ID)
+ return nil
+ } else {
+ s.Logger.Info("direct group share failed; falling back to member expansion", "group", groupEmail, "error", err)
+ if expErr := s.ShareFileWithGroupMembers(fileID, groupEmail, role); expErr != nil {
+ return fmt.Errorf("fallback member expansion failed for group '%s': %w (original direct error: %v)", groupEmail, expErr, err)
+ }
+ return nil
+ }
+}
+
+// isDistributionListGroup returns true if the group behaves like a classic Distribution List:
+// mailEnabled = true, securityEnabled = false, groupTypes does NOT contain "Unified".
+func isDistributionListGroup(m *groupMetadata) bool {
+ if m == nil {
+ return false
+ }
+ if !m.MailEnabled || m.SecurityEnabled { // must be mail-enabled and NOT security enabled
+ return false
+ }
+ for _, gt := range m.GroupTypes {
+ if strings.EqualFold(gt, "Unified") { // M365 group
+ return false
+ }
+ }
+ return true
+}
+
+// GrantGroupsReadAccess grants file access to multiple groups without sending email notifications.
+// If a group cannot be resolved by its identifier, it tries the display name from the provided map.
+// Returns the list of successfully granted group identifiers.
+func (s *Service) GrantGroupsReadAccess(fileID, role string, groupIdentifiers []string, displayNameMap map[string]string) ([]string, error) {
+ if len(groupIdentifiers) == 0 {
+ return nil, nil
+ }
+
+ seenGroups := make(map[string]struct{})
+ recipients := make([]inviteRecipient, 0, len(groupIdentifiers))
+ successfulGroups := make([]string, 0, len(groupIdentifiers))
+
+ for _, groupIdentifier := range groupIdentifiers {
+ identifier := strings.TrimSpace(groupIdentifier)
+ if identifier == "" {
+ continue
+ }
+
+ normalizedKey := strings.ToLower(identifier)
+ if _, exists := seenGroups[normalizedKey]; exists {
+ continue
+ }
+ seenGroups[normalizedKey] = struct{}{}
+
+ groupMeta := s.resolveGroupWithRetry(identifier, displayNameMap)
+ if groupMeta == nil {
+ continue
+ }
+
+ recipient := s.buildRecipient(groupMeta)
+ recipients = append(recipients, recipient)
+ successfulGroups = append(successfulGroups, identifier)
+ }
+
+ if len(recipients) == 0 {
+ s.Logger.Warn("no valid groups found to share with")
+ return nil, nil
+ }
+
+ if err := s.shareFileWithRecipients(fileID, role, recipients, false); err != nil {
+ return nil, err
+ }
+
+ return successfulGroups, nil
+}
+
+// resolveGroupWithRetry attempts to resolve a group, retrying with display name if initial lookup fails
+func (s *Service) resolveGroupWithRetry(identifier string, displayNameMap map[string]string) *groupMetadata {
+ groupMeta, err := s.resolveGroupIdentifier(identifier)
+ if err == nil {
+ return groupMeta
+ }
+
+ // Retry with display name from map if available
+ if displayNameMap != nil {
+ if displayName, exists := displayNameMap[identifier]; exists {
+ s.Logger.Info("retry group lookup",
+ "identifier", identifier,
+ "display_name", displayName,
+ )
+ groupMeta, err = s.resolveGroupByDisplayName(displayName)
+ if err == nil {
+ return groupMeta
+ }
+ }
+ }
+
+ s.Logger.Warn("group not found",
+ "identifier", identifier,
+ "error", err,
+ )
+ return nil
+}
+
+// buildRecipient creates an inviteRecipient from group metadata
+func (s *Service) buildRecipient(groupMeta *groupMetadata) inviteRecipient {
+ if groupMeta == nil || strings.TrimSpace(groupMeta.ID) == "" {
+ s.Logger.Warn("invalid group metadata")
+ return inviteRecipient{}
+ }
+
+ if strings.TrimSpace(groupMeta.Mail) != "" {
+ return inviteRecipient{Email: groupMeta.Mail}
+ }
+ return inviteRecipient{ObjectID: groupMeta.ID}
+}
+
+// ListPermissions lists all permissions for a file
+func (s *Service) ListPermissions(fileID string) ([]Permission, error) {
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s/permissions",
+ s.SiteID, s.DriveID, fileID)
+
+ resp, err := s.InvokeAPI("GET", url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error calling Graph API to list permissions: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("failed to list permissions: %s, %s", resp.Status, string(body))
+ }
+
+ var result struct {
+ Value []Permission `json:"value"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return nil, fmt.Errorf("error decoding permissions response: %w", err)
+ }
+
+ return result.Value, nil
+}
+
+// DeletePermission deletes a permission by ID
+func (s *Service) DeletePermission(fileID, permissionID string) error {
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s/permissions/%s",
+ s.SiteID, s.DriveID, fileID, permissionID)
+
+ resp, err := s.InvokeAPI("DELETE", url, nil)
+ if err != nil {
+ return fmt.Errorf("error calling Graph API to delete permission: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("failed to delete permission: %s, %s", resp.Status, string(body))
+ }
+
+ return nil
+}
+
+// ReplaceDocumentHeader replaces the header in a SharePoint document
+// This downloads the DOCX file, updates the document.xml content, and re-uploads it
+func (s *Service) ReplaceDocumentHeader(fileID string, properties map[string]string) error {
+ // Use the DOCX operations to download, modify, and upload the document
+ return s.ReplaceDocumentHeaderWithContentUpdate(fileID, properties)
+}
+
+// MoveFile moves a file to a new folder
+func (s *Service) MoveFile(fileID, folderPath string) (*CopyFileResponse, error) {
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s",
+ s.SiteID, s.DriveID, fileID)
+
+ body := map[string]interface{}{
+ "parentReference": map[string]string{
+ "driveId": s.DriveID,
+ "path": "/drive/root:" + folderPath,
+ },
+ }
+
+ jsonBody, err := json.Marshal(body)
+ if err != nil {
+ return nil, fmt.Errorf("error marshaling request body: %w", err)
+ }
+
+ resp, err := s.InvokeAPI("PATCH", url, bytes.NewBuffer(jsonBody))
+ if err != nil {
+ return nil, fmt.Errorf("error calling Graph API to move file: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("failed to move file: %s, %s", resp.Status, string(body))
+ }
+
+ var result CopyFileResponse
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return nil, fmt.Errorf("error decoding move file response: %w", err)
+ }
+
+ return &result, nil
+}
+
+// ResolveFolderPath resolves a folder path like "/DraftDocuments" to a folder ID
+func (s *Service) ResolveFolderPath(folderPath string) (string, error) {
+ // Construct the URL to get the folder by path
+ destFolderURL := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/root:/%s",
+ s.SiteID, s.DriveID, folderPath)
+
+ // Create the HTTP request
+ resp, err := s.InvokeAPI("GET", destFolderURL, nil)
+ if err != nil {
+ return "", fmt.Errorf("error calling Graph API for destination folder: %w", err)
+ }
+ defer resp.Body.Close()
+
+ // Execute the request
+
+ // Check for successful response
+ if resp.StatusCode == http.StatusOK {
+ // Parse the JSON response
+ var destinationFolder struct {
+ ID string `json:"id"`
+ }
+
+ if err := json.NewDecoder(resp.Body).Decode(&destinationFolder); err != nil {
+ return "", fmt.Errorf("error decoding destination folder response: %w", err)
+ }
+
+ destinationFolderID := destinationFolder.ID
+ s.Logger.Debug("found destination folder", "folder_id", destinationFolderID)
+
+ return destinationFolderID, nil
+ } else {
+ // Handle error response
+ body, _ := io.ReadAll(resp.Body)
+ return "", fmt.Errorf("failed to find destination folder '%s': %s, %s",
+ folderPath, resp.Status, string(body))
+ }
+
+}
+
+// CreateFolder creates a folder in a SharePoint document library.
+func (s *Service) CreateFolder(folderName, destFolderID string) (string, error) {
+ if folderName == "" {
+ return "", fmt.Errorf("folder name is required")
+ }
+ if destFolderID == "" {
+ return "", fmt.Errorf("destination folder ID is required")
+ }
+
+ // Construct the Microsoft Graph API URL to create a folder
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s/children",
+ s.SiteID, s.DriveID, destFolderID)
+
+ payload := map[string]interface{}{
+ "name": folderName,
+ "folder": map[string]interface{}{},
+ "@microsoft.graph.conflictBehavior": "rename",
+ }
+ body, _ := json.Marshal(payload)
+
+ resp, err := s.InvokeAPI("POST", url, bytes.NewBuffer(body))
+ if err != nil {
+ return "", fmt.Errorf("error calling Graph API to create folder: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusCreated {
+ b, _ := io.ReadAll(resp.Body)
+ return "", fmt.Errorf("failed to create folder: %s", string(b))
+ }
+
+ // Parse the response to get the new folder's ID (optional)
+ var result struct {
+ ID string `json:"id"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return "", fmt.Errorf("error decoding create folder response: %w", err)
+ }
+
+ return result.ID, nil
+}
+
+// DriveItem represents a SharePoint/OneDrive item (simplified for folders)
+type DriveItem struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ WebURL string `json:"webUrl"`
+ CreatedDateTime time.Time `json:"createdDateTime"`
+ LastModifiedDateTime time.Time `json:"lastModifiedDateTime"`
+ Folder *struct{} `json:"folder,omitempty"` // Will be non-nil for folders
+}
+
+// GetSubfolder returns the subfolder DriveItem if the specified folder contains a
+// subfolder with the specified name, and nil if not found.
+func (s *Service) GetSubfolder(parentFolderID, subfolderName string) (*DriveItem, error) {
+ if parentFolderID == "" {
+ return nil, fmt.Errorf("parent folder ID is required")
+ }
+ if subfolderName == "" {
+ return nil, fmt.Errorf("subfolder name is required")
+ }
+
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s/children", s.SiteID, s.DriveID, parentFolderID)
+ resp, err := s.InvokeAPI("GET", url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error calling Graph API to get subfolders: %w", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ b, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("failed to get subfolders: %s", string(b))
+ }
+
+ var result struct {
+ Value []DriveItem `json:"value"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return nil, fmt.Errorf("error decoding response: %w", err)
+ }
+
+ for _, f := range result.Value {
+ if f.Folder != nil && f.Name == subfolderName {
+ return &f, nil
+ }
+ }
+ return nil, nil
+}
+
+// CreateShortcut creates a .url shortcut file in the specified SharePoint folder.
+// targetFileWebURL: the URL to the target document
+// shortcutName: the name for the shortcut (without .url extension)
+// destFolderID: the ID of the destination folder
+func (s *Service) CreateShortcut(targetFileWebURL, shortcutName, destFolderID string) (string, error) {
+ if targetFileWebURL == "" {
+ return "", fmt.Errorf("target file web URL is required")
+ }
+ if shortcutName == "" {
+ return "", fmt.Errorf("shortcut name is required")
+ }
+ if destFolderID == "" {
+ return "", fmt.Errorf("destination folder ID is required")
+ }
+ conflictBehavior := "replace"
+ uploadURL := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s:/%s.url:/content?@microsoft.graph.conflictBehavior=%s",
+ s.SiteID, s.DriveID, destFolderID, shortcutName, conflictBehavior)
+
+ content := fmt.Sprintf("[InternetShortcut]\nURL=%s\n", targetFileWebURL)
+
+ resp, err := s.InvokeAPI("PUT", uploadURL, bytes.NewBufferString(content))
+ if err != nil {
+ return "", fmt.Errorf("error calling Graph API to create shortcut: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
+ b, _ := io.ReadAll(resp.Body)
+ return "", fmt.Errorf("failed to create shortcut: %s", string(b))
+ }
+
+ var result DriveItem
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return "", fmt.Errorf("error decoding response: %w", err)
+ }
+
+ return result.ID, nil
+}
+
+// DriveItemVersion represents a SharePoint/OneDrive file version
+type DriveItemVersion struct {
+ ID string `json:"id"`
+ LastModifiedDateTime string `json:"lastModifiedDateTime"`
+ Size int64 `json:"size"`
+ WebURL string `json:"webUrl"`
+}
+
+// GetLatestVersion returns the latest version for a SharePoint file.
+func (s *Service) GetLatestVersion(fileID string) (*DriveItemVersion, error) {
+ if fileID == "" {
+ return nil, fmt.Errorf("file ID is required")
+ }
+
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s/versions", s.SiteID, s.DriveID, fileID)
+ resp, err := s.InvokeAPI("GET", url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error calling Graph API to list versions: %w", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ b, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("failed to list versions: %s", string(b))
+ }
+
+ var result struct {
+ Value []DriveItemVersion `json:"value"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return nil, fmt.Errorf("error decoding response: %w", err)
+ }
+
+ if len(result.Value) == 0 {
+ return nil, fmt.Errorf("no versions found")
+ }
+
+ return &result.Value[0], nil // Latest version is first in the list
+}
+
+// RenameFile renames a file in SharePoint/OneDrive.
+// Uses the "Prefer: bypass-shared-lock" header to allow renaming files that are
+// currently open in a co-authoring session.
+func (s *Service) RenameFile(fileID, newName string) error {
+ if fileID == "" {
+ return fmt.Errorf("file ID is required")
+ }
+ if newName == "" {
+ return fmt.Errorf("new name is required")
+ }
+
+ if s.Logger != nil {
+ s.Logger.Info("renaming file in SharePoint",
+ "file_id", fileID,
+ "new_name", newName)
+ }
+
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s", s.SiteID, s.DriveID, fileID)
+ payload := map[string]string{
+ "name": newName,
+ }
+ body, _ := json.Marshal(payload)
+ resp, err := s.InvokeAPIWithOptions("PATCH", url, bytes.NewBuffer(body), &APIOptions{
+ Headers: map[string]string{
+ "Prefer": "bypass-shared-lock",
+ },
+ })
+ if err != nil {
+ return fmt.Errorf("error calling Graph API to rename file: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ b, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("failed to rename file: %s", string(b))
+ }
+
+ if s.Logger != nil {
+ s.Logger.Info("successfully renamed file in SharePoint",
+ "file_id", fileID,
+ "new_name", newName)
+ }
+ return nil
+}
+
+// ShareFileWithDomain shares a SharePoint file with the entire organization (domain) by creating an org-scoped sharing link.
+func (s *Service) ShareFileWithDomain(fileID, role string) (string, error) {
+ if fileID == "" {
+ return "", fmt.Errorf("file ID is required")
+ }
+ if role != "view" && role != "edit" {
+ return "", fmt.Errorf("role must be 'view' or 'edit'")
+ }
+
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s/createLink", s.SiteID, s.DriveID, fileID)
+ payload := map[string]string{
+ "type": role, // "view" or "edit"
+ "scope": "organization",
+ }
+ body, _ := json.Marshal(payload)
+ resp, err := s.InvokeAPI("POST", url, bytes.NewBuffer(body))
+ if err != nil {
+ return "", fmt.Errorf("error calling Graph API to create sharing link: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted {
+ b, _ := io.ReadAll(resp.Body)
+ return "", fmt.Errorf("failed to create sharing link: %s", string(b))
+ }
+
+ var result struct {
+ Link struct {
+ WebURL string `json:"webUrl"`
+ } `json:"link"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return "", fmt.Errorf("error decoding response: %w", err)
+ }
+
+ return result.Link.WebURL, nil
+}
diff --git a/pkg/sharepointhelper/docx_operations.go b/pkg/sharepointhelper/docx_operations.go
new file mode 100644
index 000000000..e6d912d98
--- /dev/null
+++ b/pkg/sharepointhelper/docx_operations.go
@@ -0,0 +1,1050 @@
+package sharepointhelper
+
+import (
+ "archive/zip"
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "html"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+ "time"
+)
+
+// We'll use direct string manipulation for the core properties XML
+// since the XML structure in DOCX files is very particular and requires
+// specific namespace handling that's difficult with Go's XML marshaling
+
+// UpdateDocxCoreProperties updates the core properties in a DOCX document
+func UpdateDocxCoreProperties(docxPath string, owner string, fileId string, title string) error {
+ // Open the DOCX file for reading
+ reader, err := zip.OpenReader(docxPath)
+ if err != nil {
+ return fmt.Errorf("error opening DOCX file: %w", err)
+ }
+ defer reader.Close()
+
+ // Create a new ZIP file for writing
+ tmpFile := docxPath + ".core.tmp"
+ outFile, err := os.Create(tmpFile)
+ if err != nil {
+ return fmt.Errorf("error creating temporary file: %w", err)
+ }
+ defer func() {
+ outFile.Close()
+ // Clean up in case of error
+ if err != nil {
+ os.Remove(tmpFile)
+ }
+ }()
+
+ // Create a new ZIP writer
+ archive := zip.NewWriter(outFile)
+ defer archive.Close()
+
+ // Flag to track if we've processed core.xml
+ corePropertiesProcessed := false
+
+ // Current time in W3CDTF format (ISO 8601)
+ now := time.Now().UTC().Format("2006-01-02T15:04:05Z")
+ // Process each file in the original DOCX
+ for _, file := range reader.File {
+ if strings.EqualFold(file.Name, "docProps/core.xml") {
+ // This is the core properties XML - we need to modify it
+ corePropertiesProcessed = true
+
+ // Open the core.xml file
+ fileReader, err := file.Open()
+ if err != nil {
+ return fmt.Errorf("error opening core.xml: %w", err)
+ }
+
+ // Read the XML content as a string
+ contentBytes, err := io.ReadAll(fileReader)
+ fileReader.Close()
+ if err != nil {
+ return fmt.Errorf("error reading core.xml: %w", err)
+ }
+
+ xmlContent := string(contentBytes)
+
+ // Create new XML content
+ // First, extract existing values from the XML content or use defaults
+ creatorValue := owner
+ if creatorValue == "" {
+ // Try to extract existing creator value
+ creatorMatch := regexp.MustCompile(`([^<]+) `).FindStringSubmatch(xmlContent)
+ if len(creatorMatch) > 1 {
+ creatorValue = creatorMatch[1]
+ }
+ }
+ lastModByValue := owner
+ if lastModByValue == "" {
+ // Try to extract existing lastModifiedBy value
+ lastModByMatch := regexp.MustCompile(`([^<]+) `).FindStringSubmatch(xmlContent)
+ if len(lastModByMatch) > 1 {
+ lastModByValue = lastModByMatch[1]
+ }
+ }
+ titleValue := title
+ if titleValue == "" {
+ // Try to extract existing title value
+ titleMatch := regexp.MustCompile(`([^<]+) `).FindStringSubmatch(xmlContent)
+ if len(titleMatch) > 1 {
+ titleValue = titleMatch[1]
+ }
+ }
+ keywordsValue := ""
+ keywordsMatch := regexp.MustCompile(`([^<]+) `).FindStringSubmatch(xmlContent)
+ if len(keywordsMatch) > 1 {
+ keywordsValue = keywordsMatch[1]
+ }
+ // Add fileId to keywords
+ if fileId != "" {
+ if keywordsValue != "" {
+ keywordsValue = keywordsValue + "; FileID: " + fileId
+ } else {
+ keywordsValue = "FileID: " + fileId
+ }
+ }
+ // Build a properly formatted core.xml
+ updatedXML := `
+`
+
+ if titleValue != "" {
+ updatedXML += fmt.Sprintf("\n %s ", html.EscapeString(titleValue))
+ }
+
+ if creatorValue != "" {
+ updatedXML += fmt.Sprintf("\n %s ", html.EscapeString(creatorValue))
+ }
+
+ if lastModByValue != "" {
+ updatedXML += fmt.Sprintf("\n %s ", html.EscapeString(lastModByValue))
+ }
+
+ if keywordsValue != "" {
+ updatedXML += fmt.Sprintf("\n %s ", html.EscapeString(keywordsValue))
+ }
+
+ // Add created date
+ updatedXML += fmt.Sprintf("\n %s ", now)
+
+ // Add modified date
+ updatedXML += fmt.Sprintf("\n %s ", now)
+
+ updatedXML += "\n "
+
+ // Create a new file in the archive
+ writer, err := archive.Create(file.Name)
+ if err != nil {
+ return fmt.Errorf("error creating core.xml in archive: %w", err)
+ }
+
+ // Write the modified content
+ _, err = writer.Write([]byte(updatedXML))
+ if err != nil {
+ return fmt.Errorf("error writing modified core.xml: %w", err)
+ }
+ } else {
+ // Copy file directly without modification
+ writer, err := archive.Create(file.Name)
+ if err != nil {
+ return fmt.Errorf("error creating file in archive: %w", err)
+ }
+
+ reader, err := file.Open()
+ if err != nil {
+ return fmt.Errorf("error opening file from original archive: %w", err)
+ }
+
+ _, err = io.Copy(writer, reader)
+ reader.Close()
+ if err != nil {
+ return fmt.Errorf("error copying file to new archive: %w", err)
+ }
+ }
+ }
+
+ // If we didn't find the core.xml file, create it
+ if !corePropertiesProcessed {
+ // Create a new core properties file
+ // Build a properly formatted core.xml
+ updatedXML := `
+`
+
+ if title != "" {
+ updatedXML += fmt.Sprintf("\n %s ", html.EscapeString(title))
+ }
+ if owner != "" {
+ updatedXML += fmt.Sprintf("\n %s ", html.EscapeString(owner))
+ updatedXML += fmt.Sprintf("\n %s ", html.EscapeString(owner))
+ }
+
+ if fileId != "" {
+ updatedXML += fmt.Sprintf("\n FileID: %s ", html.EscapeString(fileId))
+ }
+
+ // Add created date
+ updatedXML += fmt.Sprintf("\n %s ", now)
+
+ // Add modified date
+ updatedXML += fmt.Sprintf("\n %s ", now)
+
+ updatedXML += "\n "
+
+ // Create a new file in the archive
+ writer, err := archive.Create("docProps/core.xml")
+ if err != nil {
+ return fmt.Errorf("error creating core.xml in archive: %w", err)
+ }
+
+ // Write the modified content
+ _, err = writer.Write([]byte(updatedXML))
+ if err != nil {
+ return fmt.Errorf("error writing modified core.xml: %w", err)
+ }
+ }
+
+ // Close the zip writer before renaming files
+ if err := archive.Close(); err != nil {
+ return fmt.Errorf("error closing zip archive: %w", err)
+ }
+
+ // Close the output file
+ if err := outFile.Close(); err != nil {
+ return fmt.Errorf("error closing output file: %w", err)
+ }
+
+ // Replace the original file with the modified one
+ if err := os.Rename(tmpFile, docxPath); err != nil {
+ return fmt.Errorf("error replacing original file: %w", err)
+ }
+
+ return nil
+}
+
+// UpdateDocxHeaderTable updates a DOCX document to include a properly formatted header table
+// with the provided properties. This replaces or adds a table at the beginning of the document.
+func UpdateDocxHeaderTable(docxPath string, properties map[string]string) error {
+ // Open the DOCX file for reading
+ reader, err := zip.OpenReader(docxPath)
+ if err != nil {
+ return fmt.Errorf("error opening DOCX file: %w", err)
+ }
+ defer reader.Close()
+
+ // Create a new ZIP file for writing
+ tmpFile := docxPath + ".tmp"
+ outFile, err := os.Create(tmpFile)
+ if err != nil {
+ return fmt.Errorf("error creating temporary file: %w", err)
+ }
+ defer func() {
+ outFile.Close()
+ // Clean up in case of error
+ if err != nil {
+ os.Remove(tmpFile)
+ }
+ }()
+
+ // Create a new ZIP writer
+ archive := zip.NewWriter(outFile)
+ defer archive.Close()
+
+ // Flag to track if we've processed document.xml
+ documentProcessed := false
+
+ // Process each file in the original DOCX
+ for _, file := range reader.File {
+ if strings.EqualFold(file.Name, "word/document.xml") {
+ // This is the main document XML - we need to modify it
+ documentProcessed = true
+
+ // Open the document.xml file
+ fileReader, err := file.Open()
+ if err != nil {
+ return fmt.Errorf("error opening document.xml: %w", err)
+ }
+
+ // Read the XML content
+ xmlContent, err := io.ReadAll(fileReader)
+ if err != nil {
+ fileReader.Close()
+ return fmt.Errorf("error reading document.xml: %w", err)
+ }
+ fileReader.Close()
+
+ // Create the new header table XML
+ headerTable := generateHeaderTableXML(properties)
+
+ // Find where to insert the header table
+ modifiedXML, err := insertHeaderTableIntoDocument(xmlContent, headerTable)
+ if err != nil {
+ return fmt.Errorf("error inserting header table into document: %w", err)
+ }
+
+ // Create a new file in the archive
+ writer, err := archive.Create(file.Name)
+ if err != nil {
+ return fmt.Errorf("error creating document.xml in archive: %w", err)
+ }
+
+ // Write the modified content
+ _, err = writer.Write(modifiedXML)
+ if err != nil {
+ return fmt.Errorf("error writing modified document.xml: %w", err)
+ }
+ } else if strings.EqualFold(file.Name, "docProps/core.xml") {
+ // Skip copying core.xml - it will be updated by UpdateDocxCoreProperties
+ // If we copy it here, we'll preserve any empty/minimal properties from the template
+ continue
+ } else {
+ // Copy file directly without modification
+ writer, err := archive.Create(file.Name)
+ if err != nil {
+ return fmt.Errorf("error creating file in archive: %w", err)
+ }
+
+ reader, err := file.Open()
+ if err != nil {
+ return fmt.Errorf("error opening file from original archive: %w", err)
+ }
+
+ _, err = io.Copy(writer, reader)
+ reader.Close()
+ if err != nil {
+ return fmt.Errorf("error copying file to new archive: %w", err)
+ }
+ }
+ }
+
+ // Ensure we processed the document.xml file
+ if !documentProcessed {
+ return fmt.Errorf("document.xml not found in DOCX file")
+ }
+
+ // Close the zip writer before renaming files
+ if err := archive.Close(); err != nil {
+ return fmt.Errorf("error closing zip archive: %w", err)
+ }
+
+ // Close the output file
+ if err := outFile.Close(); err != nil {
+ return fmt.Errorf("error closing output file: %w", err)
+ }
+
+ // Replace the original file with the modified one
+ if err := os.Rename(tmpFile, docxPath); err != nil {
+ return fmt.Errorf("error replacing original file: %w", err)
+ }
+
+ return nil
+}
+
+// insertHeaderTableIntoDocument inserts the header table XML at the beginning of the document body
+// or replaces an existing header table if one is found
+func insertHeaderTableIntoDocument(docXML []byte, tableXML string) ([]byte, error) {
+ // Convert to string for easier manipulation
+ docContent := string(docXML)
+
+ // Find the body tag
+ bodyStartRegex := regexp.MustCompile(`]*>`)
+ bodyStartMatch := bodyStartRegex.FindStringIndex(docContent)
+ if bodyStartMatch == nil {
+ return nil, fmt.Errorf("could not find document body in XML")
+ }
+
+ // Find position after the body tag
+ insertPos := bodyStartMatch[1]
+
+ // Check if there's already a table at the beginning of the document
+ // Look for the opening table tag after the body tag
+ tableStartRegex := regexp.MustCompile(``)
+ tableEndMatch := tableEndRegex.FindStringIndex(docContent[insertPos+tableMatch[0]:])
+
+ if tableEndMatch != nil {
+ // Calculate positions in the full document string
+ tableStartPos := insertPos + tableMatch[0]
+ tableEndPos := insertPos + tableMatch[0] + tableEndMatch[1]
+
+ // Replace the existing table with our new one
+ newContent = docContent[:tableStartPos] + tableXML + docContent[tableEndPos:]
+ return []byte(newContent), nil
+ }
+ }
+ }
+
+ // If we get here, either no table was found or the table wasn't at the beginning
+ // Insert our table at the beginning of the body
+ newContent = docContent[:insertPos] + tableXML + docContent[insertPos:]
+ return []byte(newContent), nil
+}
+
+// getProperty safely gets a property value with a default if not present
+func getProperty(properties map[string]string, key string, defaultValue string) string {
+ if value, exists := properties[key]; exists && value != "" {
+ return value
+ }
+ return defaultValue
+}
+
+// xmlEscape escapes special characters for XML
+func xmlEscape(s string) string {
+ var b strings.Builder
+ xml.EscapeText(&b, []byte(s))
+ return b.String()
+}
+
+// generateStatusXML creates the XML for status values, with the current status in bold
+// and other statuses in normal text. Takes the formatted status string that includes current status data.
+func generateStatusXML(formattedStatus string) string {
+ var result strings.Builder
+
+ // Extract current status from formatted string
+ parts := strings.Split(formattedStatus, "|")
+ if len(parts) < 1 {
+ return ""
+ }
+
+ currentStatus := parts[0]
+
+ // The standard statuses
+ allStatuses := []string{"WIP", "In-Review", "Approved", "Obsolete"}
+
+ // Add each status with appropriate formatting
+ for i, status := range allStatuses {
+ if i > 0 {
+ // Add separator between status values with explicit spacing
+ result.WriteString(`
+
+
+
+
+
+
+ |
+ `)
+ }
+
+ if strings.EqualFold(status, currentStatus) {
+ // Current status - use bold
+ result.WriteString(`
+
+
+
+
+
+
+
+ `)
+ result.WriteString(status)
+ result.WriteString(`
+ `)
+ } else {
+ // Not current status - use normal text
+ result.WriteString(`
+
+
+
+
+
+
+ `)
+ result.WriteString(status)
+ result.WriteString(`
+ `)
+ }
+ }
+
+ return result.String()
+}
+
+// generateTableRow creates a table row with two columns, properly handling status fields
+func generateTableRow(leftKey, leftValue, rightKey, rightValue string, leftIsStatus, rightIsStatus bool) string {
+ var rowXML strings.Builder
+
+ rowXML.WriteString(`
+
+
+
+
+
+
+
+
+
+
+ `)
+
+ // Add left column key and colon (with proper spacing)
+ rowXML.WriteString(fmt.Sprintf("%s: ", leftKey))
+ rowXML.WriteString(`
+ `)
+
+ if leftIsStatus {
+ // Add explicit space before the status values
+ rowXML.WriteString(`
+
+
+
+
+
+
+
+ `)
+
+ // Special handling for Status in left column
+ rowXML.WriteString(generateStatusXML(leftValue))
+ } else {
+ // Normal value for left column - explicitly add a space as a separate run before the value
+ rowXML.WriteString(`
+
+
+
+
+
+
+
+ `)
+
+ rowXML.WriteString(fmt.Sprintf(`
+
+
+
+
+
+
+ %s
+ `, leftValue))
+ }
+
+ rowXML.WriteString(`
+
+
+
+ `)
+
+ // Only add right column content if there is a right key
+ if rightKey != "" {
+ rowXML.WriteString(fmt.Sprintf(`
+
+
+
+
+
+
+
+ %s:
+ `, rightKey))
+
+ if rightIsStatus {
+ // Add explicit space before the status values
+ rowXML.WriteString(`
+
+
+
+
+
+
+
+ `)
+
+ // Special handling for Status in right column
+ rowXML.WriteString(generateStatusXML(rightValue))
+ } else {
+ // Normal value for right column - explicitly add a space as a separate run before the value
+ rowXML.WriteString(`
+
+
+
+
+
+
+
+ `)
+
+ rowXML.WriteString(fmt.Sprintf(`
+
+
+
+
+
+
+ %s
+ `, rightValue))
+ }
+ } else {
+ // Empty right column
+ rowXML.WriteString(`
+
+
+
+
+
+
+
+ `)
+ }
+
+ rowXML.WriteString(`
+
+
+ `)
+
+ return rowXML.String()
+}
+
+// generateHeaderTableXML creates a Word table XML structure with property information
+func generateHeaderTableXML(properties map[string]string) string {
+ // Extract special properties with defaults for title and summary handling
+ docType := getProperty(properties, "DocType", "")
+ docNumber := getProperty(properties, "DocNumber", "")
+ title := getProperty(properties, "Title", "")
+ summary := getProperty(properties, "Summary", "")
+
+ // Format the title with document type and number if available
+ titleWithTypeAndNumber := title
+ if docType != "" && docNumber != "" {
+ titleWithTypeAndNumber = fmt.Sprintf("[%s] [%s]: %s", docType, docNumber, title)
+ } else if docNumber != "" {
+ titleWithTypeAndNumber = fmt.Sprintf("[%s] %s", docNumber, title)
+ }
+
+ // XML escape title and summary values
+ titleWithTypeAndNumber = xmlEscape(titleWithTypeAndNumber)
+ summary = xmlEscape(summary)
+
+ // Create a map to hold all other properties that will be displayed in the table
+ // Skip properties that are already handled specially (Title, DocNumber, Summary, DocType)
+ tableAttributes := make(map[string]string)
+
+ // Standard attribute display names
+ displayNames := map[string]string{
+ "DocType": "Document Type",
+ "Product": "Product",
+ "Creator": "Author",
+ "CreatedDate": "Created",
+ "Status": "Status",
+ "Contributors": "Contributors",
+ "Approvers": "Approvers",
+ "Owner": "Owner",
+ // Add any other standard mappings here
+ }
+
+ // Track the mandatory fields we've processed
+ processedMandatoryFields := make(map[string]bool)
+ processedMandatoryFields["Title"] = true
+ processedMandatoryFields["Summary"] = true
+ processedMandatoryFields["DocNumber"] = true
+ processedMandatoryFields["DocType"] = true
+
+ // Copy all properties to tableAttributes with proper display names
+ for key, value := range properties {
+ // Skip special properties that are already handled
+ if key == "Title" || key == "DocNumber" || key == "Summary" || key == "DocType" {
+ continue
+ }
+
+ // Get display name, or use the key with first letter capitalized if not found
+ displayName, exists := displayNames[key]
+ if !exists {
+ // Convert camelCase or snake_case to proper case
+ displayName = strings.ReplaceAll(key, "_", " ")
+
+ // Capitalize first letter of each word
+ words := strings.Fields(displayName)
+ for i, word := range words {
+ if len(word) > 0 {
+ words[i] = strings.ToUpper(word[0:1]) + word[1:]
+ }
+ }
+ displayName = strings.Join(words, " ")
+ }
+
+ // Special handling for Status field - show all possible statuses with only the current one in bold
+ if key == "Status" {
+ // The standard statuses
+ allStatuses := []string{"WIP", "In-Review", "Approved", "Obsolete"}
+
+ // Create a map to store which status is current
+ statusMap := make(map[string]bool)
+ for _, status := range allStatuses {
+ statusMap[status] = strings.EqualFold(status, value)
+ }
+
+ // Store the formatted status data as JSON encoded string that can be parsed in the XML generation
+ formattedStatus := fmt.Sprintf("%s|%v", xmlEscape(value), statusMap)
+ tableAttributes[displayName] = formattedStatus
+ processedMandatoryFields[key] = true
+ continue
+ }
+
+ // Add to table attributes map with XML escaped value
+ tableAttributes[displayName] = xmlEscape(value)
+ processedMandatoryFields[key] = true
+ }
+
+ // Build the table XML - using Aptos font styling with minimal borders
+ tableXML := `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+
+ // Add the title row that spans the entire width (first row) - using Aptos Display font
+ tableXML += fmt.Sprintf(`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %s
+
+
+
+ `, titleWithTypeAndNumber)
+
+ // Add summary row that spans the entire width if present (second row)
+ if summary != "" {
+ tableXML += fmt.Sprintf(`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Summary:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %s
+
+
+
+ `, summary)
+ }
+
+ // Create ordered list of mandatory fields to display first in the specified order
+ orderedMandatoryPairs := [][]string{
+ {"Created", "Status"},
+ {"Product", "Owner"},
+ {"Contributors", "Approvers"},
+ }
+
+ // First, add the mandatory fields in the specified order
+ for _, pair := range orderedMandatoryPairs {
+ leftKey := pair[0]
+ rightKey := pair[1]
+
+ leftDisplayName, leftExists := displayNames[leftKey]
+ if !leftExists {
+ leftDisplayName = leftKey
+ }
+
+ rightDisplayName, rightExists := displayNames[rightKey]
+ if !rightExists {
+ rightDisplayName = rightKey
+ }
+
+ leftValue, leftFound := tableAttributes[leftDisplayName]
+ rightValue, rightFound := tableAttributes[rightDisplayName]
+
+ // If at least one of the fields exists, create a row
+ if leftFound || rightFound {
+ leftValueToUse := leftValue
+ if !leftFound {
+ leftValueToUse = ""
+ }
+
+ rightValueToUse := rightValue
+ if !rightFound {
+ rightValueToUse = ""
+ }
+
+ // Special handling for Status field
+ leftIsStatus := leftDisplayName == "Status"
+ rightIsStatus := rightDisplayName == "Status"
+
+ // Add the row to the table
+ tableXML += generateTableRow(leftDisplayName, leftValueToUse, rightDisplayName, rightValueToUse, leftIsStatus, rightIsStatus)
+
+ // Mark these fields as processed
+ delete(tableAttributes, leftDisplayName)
+ delete(tableAttributes, rightDisplayName)
+ }
+ }
+
+ // Then, add any remaining fields
+ var remainingKeys []string
+ for key := range tableAttributes {
+ remainingKeys = append(remainingKeys, key)
+ }
+
+ // Sort the remaining keys for consistent output
+ sort.Strings(remainingKeys)
+
+ // Process remaining attributes in pairs (two columns per row)
+ for i := 0; i < len(remainingKeys); i += 2 {
+ leftKey := remainingKeys[i]
+ leftValue := tableAttributes[leftKey]
+
+ // Check if we have a right column
+ hasRightColumn := (i + 1) < len(remainingKeys)
+ rightKey := ""
+ rightValue := ""
+ if hasRightColumn {
+ rightKey = remainingKeys[i+1]
+ rightValue = tableAttributes[rightKey]
+ }
+
+ // Special handling for Status field
+ leftIsStatus := leftKey == "Status"
+ rightIsStatus := hasRightColumn && rightKey == "Status"
+
+ // Add the row to the table
+ if hasRightColumn {
+ tableXML += generateTableRow(leftKey, leftValue, rightKey, rightValue, leftIsStatus, rightIsStatus)
+ } else {
+ // Last row with only one attribute
+ tableXML += generateTableRow(leftKey, leftValue, "", "", leftIsStatus, false)
+ }
+ }
+
+ // Close the table
+ tableXML += `
+
+
+`
+ return tableXML
+}
+
+// ReplaceDocumentHeaderWithContentUpdate updates the document header table
+// by downloading the document, modifying its content to include a properly formatted
+// header table with the given properties, and uploading it back to SharePoint.
+func (s *Service) ReplaceDocumentHeaderWithContentUpdate(fileID string, properties map[string]string) error {
+ if s.Logger != nil {
+ s.Logger.Info("Starting document header update with content modification",
+ "file_id", fileID)
+ }
+
+ // Get file details first to get the file name and extension
+ fileDetails, err := s.GetFileDetails(fileID)
+ if err != nil {
+ return fmt.Errorf("error getting file details: %w", err)
+ }
+
+ // Check if this is a DOCX file
+ if !strings.HasSuffix(strings.ToLower(fileDetails.Name), ".docx") {
+ return fmt.Errorf("file is not a DOCX document: %s", fileDetails.FileExtension)
+ }
+
+ // Create a temporary directory for processing
+ tempDir, err := os.MkdirTemp("", "sharepoint_doc")
+ if err != nil {
+ return fmt.Errorf("error creating temp directory: %w", err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ if s.Logger != nil {
+ s.Logger.Debug("Processing document",
+ "temp_dir", tempDir,
+ "file_name", fileDetails.Name)
+ }
+
+ // Download the file to the temporary directory
+ tempFilePath := filepath.Join(tempDir, fileDetails.Name)
+
+ if err := s.downloadFile(fileID, tempFilePath); err != nil {
+ return fmt.Errorf("error downloading document: %w", err)
+ }
+
+ if s.Logger != nil {
+ s.Logger.Debug("Document downloaded successfully")
+ }
+
+ // Verify file exists and has content
+ fileInfo, err := os.Stat(tempFilePath)
+ if err != nil {
+ return fmt.Errorf("error checking downloaded file: %w", err)
+ }
+ if fileInfo.Size() == 0 {
+ return fmt.Errorf("downloaded file is empty")
+ }
+
+ // Update the document header table
+ if err := UpdateDocxHeaderTable(tempFilePath, properties); err != nil {
+ return fmt.Errorf("error updating document header table: %w", err)
+ }
+
+ // Get the title and owner from the properties
+ title := getProperty(properties, "Title", "")
+ owner := getProperty(properties, "Owner", "")
+
+ // Update the document core properties with owner and fileID
+ if err := UpdateDocxCoreProperties(tempFilePath, owner, fileID, title); err != nil {
+ return fmt.Errorf("error updating document core properties: %w", err)
+ }
+
+ // Check file size after update
+ fileInfo, err = os.Stat(tempFilePath)
+ if err != nil {
+ return fmt.Errorf("error checking updated file: %w", err)
+ }
+
+ if s.Logger != nil {
+ s.Logger.Debug("Document updates completed successfully",
+ "updated_file_size", fileInfo.Size())
+ }
+
+ // Upload the modified file back to SharePoint
+ if err := s.uploadModifiedFile(tempFilePath, fileID); err != nil {
+ return fmt.Errorf("error uploading modified document: %w", err)
+ }
+
+ if s.Logger != nil {
+ s.Logger.Info("Document updates completed successfully")
+ }
+
+ return nil
+}
+
+// downloadFile downloads a file from SharePoint by its ID to the specified local path.
+func (s *Service) downloadFile(fileID string, localPath string) error {
+ // Construct the Microsoft Graph API URL for file content
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s/content", s.SiteID, s.DriveID, fileID)
+
+ // Make the authenticated request
+ resp, err := s.InvokeAPI("GET", url, nil)
+ if err != nil {
+ return fmt.Errorf("error making request to SharePoint: %w", err)
+ }
+ defer resp.Body.Close()
+
+ // Check for a successful response
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("failed to download content: %s, %s", resp.Status, string(body))
+ }
+
+ // Create output file
+ out, err := os.Create(localPath)
+ if err != nil {
+ return fmt.Errorf("error creating local file: %w", err)
+ }
+ defer out.Close()
+
+ // Copy binary data directly from response body to file
+ _, err = io.Copy(out, resp.Body)
+ if err != nil {
+ return fmt.Errorf("error writing file content: %w", err)
+ }
+
+ return nil
+}
+
+// uploadModifiedFile uploads a file from a local path to SharePoint, replacing an existing file.
+func (s *Service) uploadModifiedFile(localPath, fileID string) error {
+ // Read the file content
+ content, err := os.ReadFile(localPath)
+ if err != nil {
+ return fmt.Errorf("error reading local file: %w", err)
+ }
+
+ // Construct the Microsoft Graph API URL for uploading content to an existing file
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s/content",
+ s.SiteID, s.DriveID, fileID)
+
+ // Upload the file content
+ resp, err := s.InvokeAPI("PUT", url, bytes.NewReader(content))
+ if err != nil {
+ return fmt.Errorf("error uploading file: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("error uploading file: %s, %s", resp.Status, string(body))
+ }
+
+ return nil
+}
diff --git a/pkg/sharepointhelper/docx_operations_test.go b/pkg/sharepointhelper/docx_operations_test.go
new file mode 100644
index 000000000..4f6c8c983
--- /dev/null
+++ b/pkg/sharepointhelper/docx_operations_test.go
@@ -0,0 +1,112 @@
+package sharepointhelper
+
+import (
+ "html"
+ "strings"
+ "testing"
+)
+
+// TestXMLEscaping verifies that special characters are properly escaped in XML properties
+func TestXMLEscaping(t *testing.T) {
+ testCases := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "Ampersand",
+ input: "Terraform & Platform",
+ expected: "Terraform & Platform",
+ },
+ {
+ name: "Less than",
+ input: "Cost < $50,000",
+ expected: "Cost < $50,000",
+ },
+ {
+ name: "Greater than",
+ input: "Score > 95%",
+ expected: "Score > 95%",
+ },
+ {
+ name: "Double quotes",
+ input: `Product "Alpha" Version`,
+ expected: "Product "Alpha" Version",
+ },
+ {
+ name: "Single quote",
+ input: "O'Brien's Project",
+ expected: "O'Brien's Project",
+ },
+ {
+ name: "Multiple special chars",
+ input: `"Terraform & Cloud" `,
+ expected: ""Terraform & Cloud" <beta>",
+ },
+ {
+ name: "Normal text",
+ input: "ORG-HCP Terraform Platform Reliability",
+ expected: "ORG-HCP Terraform Platform Reliability",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ escaped := html.EscapeString(tc.input)
+ if escaped != tc.expected {
+ t.Errorf("Expected %q, got %q", tc.expected, escaped)
+ }
+
+ // Verify it can be used in XML without breaking
+ xmlSnippet := `` + escaped + ` `
+ if !strings.Contains(xmlSnippet, "") || !strings.Contains(xmlSnippet, " ") {
+ t.Errorf("XML snippet is malformed: %s", xmlSnippet)
+ }
+ })
+ }
+}
+
+// TestProblematicDocumentTitles tests real-world document titles that caused corruption
+func TestProblematicDocumentTitles(t *testing.T) {
+ problematicTitles := []string{
+ "ORG-HCP Terraform & Platform Reliability",
+ "API Gateway: Auth & Authorization",
+ "Cost Analysis < Q4 2024",
+ "Project \"Phoenix\" Roadmap",
+ "O'Connor's Design Doc",
+ "Build & Deploy Pipeline",
+ `Configuration: "Production" vs "Development"`,
+ }
+
+ for _, title := range problematicTitles {
+ t.Run(title, func(t *testing.T) {
+ // Escape the title
+ escaped := html.EscapeString(title)
+
+ // Verify no unescaped special characters remain
+ if strings.Contains(escaped, "&") && !strings.Contains(escaped, "&") &&
+ !strings.Contains(escaped, "<") && !strings.Contains(escaped, ">") &&
+ !strings.Contains(escaped, "") {
+ t.Errorf("Unescaped ampersand found in: %s", escaped)
+ }
+
+ // Build a complete XML property
+ xml := `
+
+ ` + escaped + `
+ FileID: 01ABC123
+ `
+
+ // Basic validation that XML structure is intact
+ if !strings.Contains(xml, "") {
+ t.Error("XML structure broken - missing closing tag")
+ }
+ if !strings.Contains(xml, "FileID: 01ABC123") {
+ t.Error("Keywords (FileID) missing - corruption occurred")
+ }
+ })
+ }
+}
diff --git a/pkg/sharepointhelper/email_helper.go b/pkg/sharepointhelper/email_helper.go
new file mode 100644
index 000000000..180158779
--- /dev/null
+++ b/pkg/sharepointhelper/email_helper.go
@@ -0,0 +1,64 @@
+package sharepointhelper
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+)
+
+func (s *Service) SendEmail(to []string, from, subject, body string) error {
+ return s.SendEmailWithBCC(to, nil, from, subject, body)
+}
+
+func (s *Service) SendEmailWithBCC(to []string, bcc []string, from, subject, body string) error {
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/%s/sendMail", from)
+
+ // Construct the email payload
+ message := map[string]interface{}{
+ "subject": subject,
+ "body": map[string]string{
+ "contentType": "HTML",
+ "content": body,
+ },
+ "toRecipients": func() []map[string]map[string]string {
+ recipients := []map[string]map[string]string{}
+ for _, addr := range to {
+ recipients = append(recipients, map[string]map[string]string{
+ "emailAddress": {"address": addr},
+ })
+ }
+ return recipients
+ }(),
+ }
+
+ // Add BCC recipients if provided
+ if len(bcc) > 0 {
+ message["bccRecipients"] = func() []map[string]map[string]string {
+ recipients := []map[string]map[string]string{}
+ for _, addr := range bcc {
+ recipients = append(recipients, map[string]map[string]string{
+ "emailAddress": {"address": addr},
+ })
+ }
+ return recipients
+ }()
+ }
+
+ payload := map[string]interface{}{
+ "message": message,
+ "saveToSentItems": "true",
+ }
+
+ b, _ := json.Marshal(payload)
+ resp, err := s.InvokeAPI("POST", url, bytes.NewBuffer(b))
+ if err != nil {
+ return fmt.Errorf("error calling Graph API to send email: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("failed to send email, status: %s", resp.Status)
+ }
+ return nil
+}
diff --git a/pkg/sharepointhelper/groups_helper.go b/pkg/sharepointhelper/groups_helper.go
new file mode 100644
index 000000000..52e2a1fd4
--- /dev/null
+++ b/pkg/sharepointhelper/groups_helper.go
@@ -0,0 +1,70 @@
+package sharepointhelper
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+// Group represents a Microsoft distribution list or security group from Graph API
+type Group struct {
+ ID string `json:"id"`
+ DisplayName string `json:"displayName"`
+ Mail string `json:"mail"`
+ GroupTypes []string `json:"groupTypes"`
+}
+
+// GroupsResponse represents the response from Microsoft Graph groups search
+type GroupsResponse struct {
+ Value []Group `json:"value"`
+}
+
+// SearchGroup searches for Microsoft groups
+// that match the query using Microsoft Graph API
+func (s *Service) SearchGroup(query string, domain string, maxResults int) ([]Group, error) {
+ if query == "" {
+ return []Group{}, nil
+ }
+
+ // Build $search clause for displayName or mail
+ // Escape embedded double quotes; @ can remain (will be URL-encoded as %40 in param)
+ safe := strings.ReplaceAll(query, "\"", `\\"`)
+ searchClause := fmt.Sprintf(`"displayName:%s" OR "mail:%s"`, safe, safe)
+ encoded := url.QueryEscape(searchClause)
+ searchURL := fmt.Sprintf("https://graph.microsoft.com/v1.0/groups?$search=%s&$top=%d&$select=id,displayName,mail,groupTypes", encoded, maxResults)
+
+ s.Logger.Debug("searching groups via $search", "query", query, "search_clause", searchClause)
+
+ options := &APIOptions{Headers: map[string]string{
+ "ConsistencyLevel": "eventual",
+ "Content-Type": "application/json",
+ }}
+
+ resp, err := s.InvokeAPIWithOptions("GET", searchURL, nil, options)
+ if err != nil {
+ return nil, fmt.Errorf("error calling Graph API groups $search: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("groups $search status %d", resp.StatusCode)
+ }
+
+ var response GroupsResponse
+ if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
+ return nil, fmt.Errorf("error decoding groups $search response: %w", err)
+ }
+
+ var results []Group
+ for _, g := range response.Value {
+ if g.Mail != "" { // maintain prior filter behavior
+ results = append(results, g)
+ if maxResults > 0 && len(results) >= maxResults {
+ break
+ }
+ }
+ }
+ return results, nil
+}
diff --git a/pkg/sharepointhelper/people_helper.go b/pkg/sharepointhelper/people_helper.go
new file mode 100644
index 000000000..72b53a6d5
--- /dev/null
+++ b/pkg/sharepointhelper/people_helper.go
@@ -0,0 +1,316 @@
+package sharepointhelper
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+)
+
+// ErrUserNotFound is returned when a user is not found (404 from Graph API)
+var ErrUserNotFound = errors.New("user not found")
+
+// Person represents a person from Microsoft Graph API (internal)
+type Person struct {
+ ID string `json:"id"`
+ DisplayName string `json:"displayName"`
+ GivenName string `json:"givenName"`
+ Surname string `json:"surname"`
+ UserPrincipalName string `json:"userPrincipalName"`
+ Mail string `json:"mail"`
+ JobTitle string `json:"jobTitle"`
+ OfficeLocation string `json:"officeLocation"`
+ BusinessPhones []string `json:"businessPhones"`
+ MobilePhone string `json:"mobilePhone"`
+}
+
+// People represents a person for frontend compatibility
+type People struct {
+ EmailAddresses []EmailAddress `json:"emailAddresses"`
+ Etag string `json:"etag"`
+ Names []Name `json:"names"`
+ Photos []Photo `json:"photos"`
+ ResourceName string `json:"resourceName"`
+}
+
+type EmailAddress struct {
+ Metadata EmailMetadata `json:"metadata"`
+ Value string `json:"value"`
+}
+
+type EmailMetadata struct {
+ Primary bool `json:"primary"`
+ Source Source `json:"source"`
+ SourcePrimary bool `json:"sourcePrimary"`
+ Verified bool `json:"verified"`
+}
+
+type Name struct {
+ DisplayName string `json:"displayName"`
+ DisplayNameLastFirst string `json:"displayNameLastFirst"`
+ FamilyName string `json:"familyName"`
+ GivenName string `json:"givenName"`
+ Metadata NameMetadata `json:"metadata"`
+ UnstructuredName string `json:"unstructuredName"`
+}
+
+type NameMetadata struct {
+ Primary bool `json:"primary"`
+ Source Source `json:"source"`
+ SourcePrimary bool `json:"sourcePrimary"`
+}
+
+type Photo struct {
+ Default bool `json:"default"`
+ Metadata PhotoMetadata `json:"metadata"`
+ URL string `json:"url"`
+}
+
+type PhotoMetadata struct {
+ Primary bool `json:"primary"`
+ Source Source `json:"source"`
+}
+
+type Source struct {
+ ID string `json:"id"`
+ Type string `json:"type"`
+}
+
+// SearchPeopleResponse represents the response from Microsoft Graph people search
+type SearchPeopleResponse struct {
+ Value []Person `json:"value"`
+}
+
+// convertToPeopleFormat converts Microsoft Graph Person to People format
+func convertToPeopleFormat(person Person) People {
+ // Use primary email (mail field) or fallback to userPrincipalName
+ email := person.Mail
+ if email == "" {
+ email = person.UserPrincipalName
+ }
+
+ // Create display name variations
+ displayName := person.DisplayName
+ if displayName == "" {
+ displayName = person.GivenName + " " + person.Surname
+ }
+
+ displayNameLastFirst := person.Surname + ", " + person.GivenName
+ if person.Surname == "" || person.GivenName == "" {
+ displayNameLastFirst = displayName
+ }
+
+ // Generate a simple etag (could be more sophisticated)
+ etag := fmt.Sprintf("%%EggBAgMJLjc9PhoCAQc=%s", person.ID[:8])
+
+ return People{
+ EmailAddresses: []EmailAddress{
+ {
+ Metadata: EmailMetadata{
+ Primary: true,
+ Source: Source{
+ ID: person.ID,
+ Type: "DOMAIN_PROFILE",
+ },
+ SourcePrimary: true,
+ Verified: true,
+ },
+ Value: email,
+ },
+ },
+ Etag: etag,
+ Names: []Name{
+ {
+ DisplayName: displayName,
+ DisplayNameLastFirst: displayNameLastFirst,
+ FamilyName: person.Surname,
+ GivenName: person.GivenName,
+ Metadata: NameMetadata{
+ Primary: true,
+ Source: Source{
+ ID: person.ID,
+ Type: "DOMAIN_PROFILE",
+ },
+ SourcePrimary: true,
+ },
+ UnstructuredName: displayName,
+ },
+ },
+ Photos: []Photo{
+ {
+ Default: true,
+ Metadata: PhotoMetadata{
+ Primary: true,
+ Source: Source{
+ ID: person.ID,
+ Type: "PROFILE",
+ },
+ },
+ URL: fmt.Sprintf("/api/v2/people?photo=%s&v=%d", url.QueryEscape(email), time.Now().Unix()),
+ },
+ },
+ ResourceName: "people/" + person.ID,
+ }
+}
+
+// SearchPeople searches for people using Microsoft Graph API and returns People format
+func (s *Service) SearchPeople(query string, top int) ([]People, error) {
+ if top <= 0 {
+ top = 10
+ }
+ rawQuery := query
+
+ // Escape any embedded double quotes in user input (Graph expects balanced quotes)
+ escapedUser := strings.ReplaceAll(rawQuery, `"`, `\"`)
+
+ // Build the search clause (three OR terms). Each term is quoted per Graph $search syntax.
+ searchClause := fmt.Sprintf(`"displayName:%s" OR "mail:%s" OR "userPrincipalName:%s"`, escapedUser, escapedUser, escapedUser)
+
+ // URL-encode the entire clause exactly once
+ searchParam := url.QueryEscape(searchClause)
+
+ searchURL := fmt.Sprintf(
+ "https://graph.microsoft.com/v1.0/users?$search=%s&$top=%d&$select=id,displayName,givenName,surname,userPrincipalName,mail,jobTitle,officeLocation,businessPhones,mobilePhone",
+ searchParam, top,
+ )
+
+ s.Logger.Debug("people search request",
+ "url", searchURL,
+ "raw_query", rawQuery,
+ "search_clause", searchClause,
+ )
+
+ options := &APIOptions{
+ Headers: map[string]string{
+ "ConsistencyLevel": "eventual",
+ "Content-Type": "application/json",
+ },
+ }
+
+ resp, err := s.InvokeAPIWithOptions("GET", searchURL, nil, options)
+ if err != nil {
+ return nil, fmt.Errorf("error making request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
+ }
+
+ var searchResp SearchPeopleResponse
+ if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil {
+ return nil, fmt.Errorf("error decoding response: %w", err)
+ }
+
+ var people []People
+ for _, person := range searchResp.Value {
+ people = append(people, convertToPeopleFormat(person))
+ }
+ return people, nil
+}
+
+// GetPersonByEmail gets a specific person by their email address and returns People format
+func (s *Service) GetPersonByEmail(email string) (*Person, error) {
+ var getUserURL string
+
+ // Special case for "me" to get current user
+ if email == "me" {
+ getUserURL = "https://graph.microsoft.com/v1.0/me?$select=id,displayName,givenName,surname,userPrincipalName,mail,jobTitle,officeLocation,businessPhones,mobilePhone"
+ } else {
+ getUserURL = fmt.Sprintf("https://graph.microsoft.com/v1.0/users/%s?$select=id,displayName,givenName,surname,userPrincipalName,mail,jobTitle,officeLocation,businessPhones,mobilePhone",
+ url.QueryEscape(email))
+ }
+
+ options := &APIOptions{
+ Headers: map[string]string{
+ "Content-Type": "application/json",
+ },
+ }
+
+ resp, err := s.InvokeAPIWithOptions("GET", getUserURL, nil, options)
+ if err != nil {
+ return nil, fmt.Errorf("error calling Graph API to get person by email: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ return nil, ErrUserNotFound
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("microsoft Graph API returned status %d", resp.StatusCode)
+ }
+
+ var person Person
+ if err := json.NewDecoder(resp.Body).Decode(&person); err != nil {
+ return nil, fmt.Errorf("error decoding response: %w", err)
+ }
+
+ return &person, nil
+}
+
+// GetPeopleByEmails gets multiple people by their email addresses and returns People format
+func (s *Service) GetPeopleByEmails(emails []string) ([]People, error) {
+ var people []People
+
+ for _, email := range emails {
+ person, err := s.GetPersonByEmail(email)
+ if err != nil {
+ // Log error but continue with other emails
+ continue
+ }
+ if person != nil {
+ personRecord := convertToPeopleFormat(*person)
+ people = append(people, personRecord)
+ }
+ }
+
+ return people, nil
+}
+
+// GetProfilePhoto gets a user's profile photo using Microsoft Graph API
+// Accepts either email address or user ID
+func (s *Service) GetProfilePhoto(userEmail string) ([]byte, error) {
+ // Try to get a token
+ token, err := s.GetToken()
+ if err != nil {
+ return nil, fmt.Errorf("error getting token: %w", err)
+ }
+
+ photoURL := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/%s/photo/$value", url.QueryEscape(userEmail))
+
+ req, err := http.NewRequest("GET", photoURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+
+ req.Header.Set("Authorization", "Bearer "+token)
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("error making request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ return nil, nil // Photo not found
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("API returned status %d for photo request", resp.StatusCode)
+ }
+
+ // Read the photo data
+ photoData, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("error reading photo data: %w", err)
+ }
+
+ return photoData, nil
+}
diff --git a/pkg/sharepointhelper/service.go b/pkg/sharepointhelper/service.go
new file mode 100644
index 000000000..7904cc7e6
--- /dev/null
+++ b/pkg/sharepointhelper/service.go
@@ -0,0 +1,694 @@
+package sharepointhelper
+
+import (
+ "archive/zip"
+ "bytes"
+ "encoding/json"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/hashicorp-forge/hermes/internal/config"
+ "github.com/hashicorp/go-hclog"
+)
+
+// TokenCache represents an in-memory token cache
+type TokenCache struct {
+ mu sync.RWMutex
+ token string
+ expiry time.Time
+}
+
+// tokenExpirySkew defines how long before the actual expiry we should
+// consider the token "effectively expired" so we refresh proactively.
+// This avoids edge cases where a token expires in-flight during an API call.
+// Adjust as needed; kept modest to reduce unnecessary refreshes.
+const tokenExpirySkew = 5 * time.Minute
+
+// Get retrieves the cached token if it's still valid
+func (tc *TokenCache) Get() (string, bool) {
+ tc.mu.RLock()
+ defer tc.mu.RUnlock()
+
+ if tc.token == "" {
+ return "", false
+ }
+
+ // If expiry is zero or within the skew window, force refresh.
+ timeUntilExpiry := time.Until(tc.expiry)
+ if tc.expiry.IsZero() || timeUntilExpiry <= tokenExpirySkew {
+ return "", false
+ }
+ return tc.token, true
+}
+
+// Set stores a token with its expiry time
+func (tc *TokenCache) Set(token string, expiry time.Time) {
+ tc.mu.Lock()
+ defer tc.mu.Unlock()
+
+ tc.token = token
+ tc.expiry = expiry
+}
+
+// Clear removes the cached token
+func (tc *TokenCache) Clear() {
+ tc.mu.Lock()
+ defer tc.mu.Unlock()
+
+ tc.token = ""
+ tc.expiry = time.Time{}
+}
+
+// NewTokenCache creates a new token cache instance
+func NewTokenCache() *TokenCache {
+ return &TokenCache{}
+}
+
+type Service struct {
+ ClientID string
+ ClientSecret string
+ TenantID string
+ SiteID string
+ DriveID string
+ Logger hclog.Logger
+ tokenCache *TokenCache // Private cache with controlled access
+ httpClient *http.Client // Base HTTP client
+}
+
+type GraphResponse struct {
+ Mail string `json:"mail"`
+ User string `json:"userPrincipalName"`
+}
+
+// Document represents a SharePoint document fetched via the Microsoft Graph API.
+type Document struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ LastModifiedTime string `json:"lastModifiedDateTime"`
+ Size int64 `json:"size"`
+ WebURL string `json:"webUrl"`
+ FileExtension string `json:"fileExtension"`
+}
+
+// NewService creates a new SharePoint service instance with the provided logger
+func NewService(cfg *config.SharePointConfig, logger hclog.Logger) *Service {
+ // Use null logger if none provided
+ if logger == nil {
+ logger = hclog.NewNullLogger()
+ }
+
+ return &Service{
+ ClientID: cfg.ClientID,
+ ClientSecret: cfg.ClientSecret,
+ TenantID: cfg.TenantID,
+ SiteID: cfg.SiteID,
+ DriveID: cfg.DriveID,
+ Logger: logger,
+ tokenCache: NewTokenCache(), // Initialize token cache
+ httpClient: &http.Client{}, // Initialize base HTTP client
+ }
+}
+
+// NewServiceWithCache creates a new SharePoint service instance with a custom token cache
+func NewServiceWithCache(cfg *config.SharePointConfig, logger hclog.Logger, tokenCache *TokenCache) *Service {
+ // Use null logger if none provided
+ if logger == nil {
+ logger = hclog.NewNullLogger()
+ }
+
+ return &Service{
+ ClientID: cfg.ClientID,
+ ClientSecret: cfg.ClientSecret,
+ TenantID: cfg.TenantID,
+ SiteID: cfg.SiteID,
+ DriveID: cfg.DriveID,
+ Logger: logger,
+ tokenCache: tokenCache, // Injected token cache
+ httpClient: &http.Client{}, // Initialize base HTTP client
+ }
+}
+
+// ValidateToken validates the SharePoint access token by calling the Microsoft Graph API.
+func (s *Service) ValidateToken(token string) (string, error) {
+ // Remove "Bearer " prefix if present
+ token = strings.TrimPrefix(token, "Bearer ")
+
+ // Call the Microsoft Graph API to validate the token
+ url := "https://graph.microsoft.com/v1.0/me"
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return "", err
+ }
+ req.Header.Set("Authorization", "Bearer "+token)
+
+ resp, err := s.httpClient.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ // Check if the response status is 200 OK
+ if resp.StatusCode != http.StatusOK {
+ return "", errors.New("invalid token or unauthorized access")
+ }
+
+ // Parse the response to extract the user's email
+ var graphResp GraphResponse
+ if err := json.NewDecoder(resp.Body).Decode(&graphResp); err != nil {
+ return "", err
+ }
+
+ // Return the user's email (prefer `mail`, fallback to `userPrincipalName`)
+ if graphResp.Mail != "" {
+ return graphResp.Mail, nil
+ }
+ if graphResp.User != "" {
+ return graphResp.User, nil
+ }
+
+ return "", errors.New("unable to extract user email from token")
+}
+
+// getNewToken generates a new SharePoint token.
+func (s *Service) getNewToken() (string, time.Time, error) {
+ tokenURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", s.TenantID)
+ data := map[string]string{
+ "grant_type": "client_credentials",
+ "client_id": s.ClientID,
+ "client_secret": s.ClientSecret,
+ "scope": "https://graph.microsoft.com/.default",
+ }
+
+ form := make(map[string][]string)
+ for k, v := range data {
+ form[k] = []string{v}
+ }
+
+ resp, err := http.PostForm(tokenURL, form)
+ if err != nil {
+ return "", time.Time{}, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return "", time.Time{}, fmt.Errorf("failed to get token: %s", resp.Status)
+ }
+
+ var result struct {
+ AccessToken string `json:"access_token"`
+ ExpiresIn int `json:"expires_in"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return "", time.Time{}, err
+ }
+
+ expiry := time.Now().Add(time.Duration(result.ExpiresIn) * time.Second)
+
+ s.Logger.Debug("generated new SharePoint token",
+ "expires_in_seconds", result.ExpiresIn,
+ "expiry_time", expiry.Format(time.RFC3339),
+ "time_until_expiry", time.Until(expiry).String())
+
+ return result.AccessToken, expiry, nil
+}
+
+// GetToken manages the SharePoint token lifecycle using in-memory cache.
+func (s *Service) GetToken() (string, error) {
+ // Check if we have a valid cached token
+ if token, valid := s.tokenCache.Get(); valid {
+ s.Logger.Debug("using cached SharePoint token from memory")
+ return token, nil
+ }
+
+ // Generate a new token
+ s.Logger.Debug("generating new SharePoint token")
+ token, expiry, err := s.getNewToken()
+ if err != nil {
+ s.Logger.Error("error generating new SharePoint token", "error", err)
+ return "", fmt.Errorf("error generating new SharePoint token: %w", err)
+ }
+
+ // Cache the new token in memory
+ s.tokenCache.Set(token, expiry)
+
+ s.Logger.Debug("successfully generated and cached new SharePoint token", "expiry", expiry)
+ return token, nil
+}
+
+// APIOptions contains options for making API requests
+type APIOptions struct {
+ Headers map[string]string
+ Timeout time.Duration
+}
+
+// InvokeAPI creates an HTTP request with proper authentication, executes it, and returns the response
+// Automatically handles token refresh on 401 Unauthorized responses
+func (s *Service) InvokeAPI(method, url string, body io.Reader) (*http.Response, error) {
+ return s.invokeAPIWithRetry(method, url, body, nil)
+}
+
+// invokeAPIWithRetry handles the core API invocation logic with automatic token refresh on 401 errors
+func (s *Service) invokeAPIWithRetry(method, url string, body io.Reader, options *APIOptions) (*http.Response, error) {
+ // Store the original body content for potential retry
+ var bodyBytes []byte
+ if body != nil {
+ var err error
+ bodyBytes, err = io.ReadAll(body)
+ if err != nil {
+ return nil, fmt.Errorf("error reading request body: %w", err)
+ }
+ }
+
+ // Attempt the request with current token
+ resp, err := s.makeAuthenticatedRequest(method, url, bodyBytes, options)
+ if err != nil {
+ return nil, err
+ }
+
+ // Check if we got 401 Unauthorized - token might be expired
+ if resp.StatusCode == 401 {
+ s.Logger.Debug("received 401 Unauthorized, clearing token cache and retrying")
+
+ // Close the failed response
+ resp.Body.Close()
+
+ // Clear the cached token to force refresh
+ s.tokenCache.Clear()
+
+ // Retry with fresh token
+ resp, err = s.makeAuthenticatedRequest(method, url, bodyBytes, options)
+ if err != nil {
+ return nil, fmt.Errorf("error on retry after token refresh: %w", err)
+ }
+ }
+
+ return resp, nil
+}
+
+// makeAuthenticatedRequest creates and executes an HTTP request with authentication
+func (s *Service) makeAuthenticatedRequest(method, url string, bodyBytes []byte, options *APIOptions) (*http.Response, error) {
+ // Get access token
+ token, err := s.GetToken()
+ if err != nil {
+ return nil, fmt.Errorf("error getting access token: %w", err)
+ }
+
+ // Prepare body reader
+ var bodyReader io.Reader
+ if bodyBytes != nil {
+ bodyReader = bytes.NewReader(bodyBytes)
+ }
+
+ // Create the HTTP request
+ req, err := http.NewRequest(method, url, bodyReader)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+
+ // Set authentication header
+ req.Header.Set("Authorization", "Bearer "+token)
+ req.Header.Set("Content-Type", "application/json")
+
+ // Set custom headers if provided
+ if options != nil && options.Headers != nil {
+ for key, value := range options.Headers {
+ req.Header.Set(key, value)
+ }
+ }
+
+ // Use base HTTP client or create a new one with custom timeout
+ client := s.httpClient
+ if options != nil && options.Timeout > 0 {
+ client = &http.Client{Timeout: options.Timeout}
+ }
+
+ // Execute the request
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("error executing request: %w", err)
+ }
+
+ return resp, nil
+}
+
+// InvokeAPIWithOptions creates an HTTP request with proper authentication, custom headers, and timeout, executes it, and returns the response
+// Automatically handles token refresh on 401 Unauthorized responses
+func (s *Service) InvokeAPIWithOptions(method, url string, body io.Reader, options *APIOptions) (*http.Response, error) {
+ return s.invokeAPIWithRetry(method, url, body, options)
+}
+
+// InvokeAPIWithUserToken creates an HTTP request with user-provided token, executes it, and returns the response
+func (s *Service) InvokeAPIWithUserToken(method, url string, userToken string, body io.Reader) (*http.Response, error) {
+ // Create the HTTP request
+ req, err := http.NewRequest(method, url, body)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+
+ // Use the provided user token instead of service token
+ req.Header.Set("Authorization", "Bearer "+userToken)
+ req.Header.Set("Content-Type", "application/json")
+
+ // Execute the request using the base HTTP client
+ resp, err := s.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("error executing request: %w", err)
+ }
+
+ return resp, nil
+}
+
+// ValidateUserToken validates a user's Microsoft access token by calling the Graph API
+func (s *Service) ValidateUserToken(userToken string) bool {
+ resp, err := s.InvokeAPIWithUserToken("GET", "https://graph.microsoft.com/v1.0/me", userToken, nil)
+ if err != nil {
+ s.Logger.Error("error validating user token", "error", err)
+ return false
+ }
+ defer resp.Body.Close()
+
+ isValid := resp.StatusCode == http.StatusOK
+ if !isValid {
+ s.Logger.Warn("user token validation failed", "status_code", resp.StatusCode)
+ }
+
+ return isValid
+}
+
+// GetUserInfoFromToken retrieves user information using the provided user token
+func (s *Service) GetUserInfoFromToken(userToken string) (*Person, error) {
+ resp, err := s.InvokeAPIWithUserToken("GET", "https://graph.microsoft.com/v1.0/me", userToken, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error calling Microsoft Graph API: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ s.Logger.Error("Microsoft Graph API returned non-200 status", "status_code", resp.StatusCode)
+ return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
+ }
+
+ var userInfo Person
+ if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
+ s.Logger.Error("error decoding Microsoft Graph API response", "error", err)
+ return nil, fmt.Errorf("error decoding response: %w", err)
+ }
+
+ return &userInfo, nil
+}
+
+// GetUserEmailFromToken gets the user email from the provided user token
+func (s *Service) GetUserEmailFromToken(userToken string) (string, error) {
+ userInfo, err := s.GetUserInfoFromToken(userToken)
+ if err != nil {
+ return "", err
+ }
+
+ // Use mail if available, otherwise use userPrincipalName
+ email := userInfo.Mail
+ if email == "" {
+ email = userInfo.UserPrincipalName
+ }
+
+ if email == "" {
+ s.Logger.Error("no email found in Microsoft Graph API response")
+ return "", fmt.Errorf("no email found in user info")
+ }
+
+ return email, nil
+}
+
+// ClearTokenCache clears the cached access token, forcing a fresh token on next request
+func (s *Service) ClearTokenCache() {
+ s.tokenCache.Clear()
+ s.Logger.Debug("cleared SharePoint token cache")
+}
+
+// HasCachedToken returns true if a valid token is currently cached
+func (s *Service) HasCachedToken() bool {
+ _, valid := s.tokenCache.Get()
+ return valid
+}
+
+// GetCachedToken returns the cached token if valid, empty string if not
+func (s *Service) GetCachedToken() string {
+ token, _ := s.tokenCache.Get()
+ return token
+}
+
+func (s *Service) FetchDocuments(driveID, folderID string, after, before time.Time) ([]Document, error) {
+ // Server-side date filtering is NOT supported by Microsoft Graph children endpoint
+ // Use optimized query parameters to reduce payload and improve performance
+
+ // Construct the Microsoft Graph API URL with optimizations:
+ // - $select: reduce payload by requesting only needed fields
+ // - $orderby: get recent items first (helps with early termination)
+ // - $top: limit page size for better performance
+ url := fmt.Sprintf(
+ "https://graph.microsoft.com/v1.0/sites/%s/drives/%s/root:/%s:/children?$select=id,name,lastModifiedDateTime,size,webUrl,file",
+ s.SiteID,
+ driveID,
+ folderID,
+ )
+
+ // Log the request with optimization details
+ s.Logger.Debug("fetching SharePoint documents with client-side filtering and optimizations",
+ "drive_id", driveID,
+ "folder_id", folderID,
+ "after", after.Format("2006-01-02"),
+ "before", before.Format("2006-01-02"),
+ "optimizations", "$select+$orderby+$top",
+ "url", url)
+
+ var allDocuments []Document
+ pageCount := 0
+ filteredCount := 0
+
+ // Paginate through all results with client-side filtering
+ for url != "" {
+ pageCount++
+ s.Logger.Debug("fetching optimized page with client-side filtering", "page", pageCount, "url", url)
+
+ // Make the authenticated request
+ resp, err := s.InvokeAPI("GET", url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error making request to SharePoint (page %d): %w", pageCount, err)
+ }
+ defer resp.Body.Close()
+
+ // Log the response status for errors only
+ if resp.StatusCode != http.StatusOK {
+ s.Logger.Error("SharePoint API request failed", "page", pageCount, "status_code", resp.StatusCode, "status", resp.Status)
+ }
+
+ // Check for a successful response
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("failed to fetch documents (page %d): %s, %s", pageCount, resp.Status, string(body))
+ }
+
+ // Parse the response
+ var result struct {
+ Value []Document `json:"value"`
+ NextLink string `json:"@odata.nextLink"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return nil, fmt.Errorf("error decoding response (page %d): %w", pageCount, err)
+ }
+
+ // Apply client-side filtering by date since server-side filtering is not supported
+ for _, doc := range result.Value {
+ // Parse the lastModifiedDateTime
+ if modifiedTime, err := time.Parse(time.RFC3339, doc.LastModifiedTime); err == nil {
+ // Check if the document is within our date range
+ if (modifiedTime.Equal(after) || modifiedTime.After(after)) &&
+ (modifiedTime.Equal(before) || modifiedTime.Before(before)) {
+ allDocuments = append(allDocuments, doc)
+ filteredCount++
+ }
+ } else {
+ // If we can't parse the date, log and include the document to be safe
+ s.Logger.Warn("could not parse lastModifiedDateTime for document",
+ "document_name", doc.Name,
+ "lastModifiedDateTime", doc.LastModifiedTime,
+ "error", err)
+ allDocuments = append(allDocuments, doc)
+ filteredCount++
+ }
+ }
+
+ // Log progress for this page
+ s.Logger.Debug("processed page with client-side filtering",
+ "page", pageCount,
+ "page_total", len(result.Value),
+ "page_filtered", filteredCount-len(allDocuments)+len(result.Value),
+ "total_filtered", filteredCount)
+
+ // Set URL for next page (will be empty string if no more pages)
+ url = result.NextLink
+ }
+
+ // Log summary of fetched documents with client-side filtering results
+ s.Logger.Info("fetched SharePoint documents with optimized client-side filtering",
+ "total_filtered_count", len(allDocuments),
+ "pages_processed", pageCount,
+ "drive_id", driveID,
+ "folder_id", folderID,
+ "after", after.Format("2006-01-02"),
+ "before", before.Format("2006-01-02"),
+ "optimization_used", "$select+$orderby+$top")
+
+ return allDocuments, nil
+}
+
+func (s *Service) DownloadContent(fileID string) (string, error) {
+ // Construct the Microsoft Graph API URL for file content
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s/content", s.SiteID, s.DriveID, fileID)
+
+ // Make the authenticated request
+ resp, err := s.InvokeAPI("GET", url, nil)
+ if err != nil {
+ return "", fmt.Errorf("error making request to SharePoint: %w", err)
+ }
+ defer resp.Body.Close()
+
+ // Check for a successful response
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return "", fmt.Errorf("failed to download content: %s, %s", resp.Status, string(body))
+ }
+
+ // Read the response body (the .docx file content)
+ content, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", fmt.Errorf("error reading response body: %w", err)
+ }
+
+ // Extract text from the .docx file
+ text, err := extractTextFromDocx(content)
+ if err != nil {
+ return "", fmt.Errorf("error extracting text from .docx: %w", err)
+ }
+
+ // Log summary of extracted text content
+ s.Logger.Debug("extracted document text content", "file_id", fileID, "text_length", len(text))
+
+ // Return the extracted text
+ return text, nil
+}
+
+// Helper function to extract text from a .docx file
+func extractTextFromDocx(content []byte) (string, error) {
+ // Open the .docx file as a ZIP archive
+ reader, err := zip.NewReader(bytes.NewReader(content), int64(len(content)))
+ if err != nil {
+ return "", fmt.Errorf("error opening .docx as ZIP: %w", err)
+ }
+
+ var text strings.Builder
+
+ // Iterate through the files in the ZIP archive
+ for _, file := range reader.File {
+ // Look for the main document XML file
+ if file.Name == "word/document.xml" {
+ rc, err := file.Open()
+ if err != nil {
+ return "", fmt.Errorf("error opening document.xml: %w", err)
+ }
+ defer rc.Close()
+
+ // Read the XML content
+ xmlContent, err := io.ReadAll(rc)
+ if err != nil {
+ return "", fmt.Errorf("error reading document.xml: %w", err)
+ }
+
+ // Extract text from the XML
+ text.WriteString(extractTextFromXML(xmlContent))
+ }
+ }
+
+ return text.String(), nil
+}
+
+// Helper function to extract text from XML content
+func extractTextFromXML(xmlContent []byte) string {
+ var text strings.Builder
+
+ decoder := xml.NewDecoder(bytes.NewReader(xmlContent))
+ for {
+ tok, err := decoder.Token()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ break
+ }
+
+ // Look for text nodes
+ switch elem := tok.(type) {
+ case xml.StartElement:
+ if elem.Name.Local == "t" { // contains text in Word documents
+ var charData string
+ decoder.DecodeElement(&charData, &elem)
+ text.WriteString(charData)
+ }
+ }
+ }
+
+ return text.String()
+}
+
+// GetFile retrieves a file obj from SharePoint by its ID.
+func (s *Service) GetFile(fileID string) (*Document, error) {
+ // Construct the Microsoft Graph API URL for the file
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s", s.SiteID, s.DriveID, fileID)
+
+ // Make the authenticated request
+ resp, err := s.InvokeAPI("GET", url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error making request to SharePoint: %w", err)
+ }
+ defer resp.Body.Close()
+
+ // Check for a successful response
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("failed to get file: %s, %s", resp.Status, string(body))
+ }
+
+ // Parse the response body into a Document object
+ var doc Document
+ if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil {
+ return nil, fmt.Errorf("error decoding response body: %w", err)
+ }
+
+ return &doc, nil
+}
+
+func (s *Service) DeleteFile(fileID string) error {
+ if fileID == "" {
+ return fmt.Errorf("file ID is required")
+ }
+ url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/items/%s", s.SiteID, s.DriveID, fileID)
+
+ // Make the authenticated request
+ resp, err := s.InvokeAPI("DELETE", url, nil)
+ if err != nil {
+ return fmt.Errorf("error making request to SharePoint: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusNoContent {
+ b, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("failed to delete file: %s", string(b))
+ }
+
+ return nil
+}
diff --git a/web/.ember-cli b/web/.ember-cli
new file mode 100644
index 000000000..465c4050d
--- /dev/null
+++ b/web/.ember-cli
@@ -0,0 +1,7 @@
+{
+ /**
+ Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
+ rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
+ */
+ "isTypeScriptProject": false
+}
diff --git a/web/.eslintignore b/web/.eslintignore
new file mode 100644
index 000000000..768fab33b
--- /dev/null
+++ b/web/.eslintignore
@@ -0,0 +1,14 @@
+# unconventional js
+/blueprints/*/files/
+
+# compiled output
+/declarations/
+/dist/
+
+# misc
+/coverage/
+!.*
+.*/
+
+# ember-try
+/.node_modules.ember-try/
diff --git a/web/.eslintrc.js b/web/.eslintrc.js
index 67052bbd2..5835e19d4 100644
--- a/web/.eslintrc.js
+++ b/web/.eslintrc.js
@@ -1,8 +1,11 @@
module.exports = {
+ root: true,
plugins: ["ember", "@typescript-eslint"],
parser: "@typescript-eslint/parser",
parserOptions: {
- project: "web/tsconfig.json",
+ project: "./tsconfig.json",
+ ecmaVersion: 'latest',
+ sourceType: 'module',
},
ignorePatterns: ["*.js", "/mirage/**/*", "/node_modules/**/*", "/dist/**/*"],
extends: [
@@ -12,6 +15,7 @@ module.exports = {
],
rules: {
+ "@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",
@@ -41,4 +45,41 @@ module.exports = {
"no-empty": "off",
"require-yield": "off",
},
+ overrides: [
+ // node files
+ {
+ files: [
+ './.eslintrc.js',
+ './.prettierrc.js',
+ './.stylelintrc.js',
+ './.template-lintrc.js',
+ './ember-cli-build.js',
+ './testem.js',
+ './blueprints/*/index.js',
+ './config/**/*.js',
+ './lib/*/index.js',
+ './server/**/*.js',
+ ],
+ parser: '@babel/eslint-parser',
+ parserOptions: {
+ sourceType: 'script',
+ requireConfigFile: false,
+ babelOptions: {
+ plugins: [
+ ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }],
+ ],
+ },
+ },
+ env: {
+ browser: false,
+ node: true,
+ },
+ extends: ['plugin:n/recommended'],
+ },
+ {
+ // test files
+ files: ['tests/**/*-test.{js,ts}'],
+ extends: ['plugin:qunit/recommended'],
+ },
+ ],
};
diff --git a/web/.github/workflows/ci.yml b/web/.github/workflows/ci.yml
new file mode 100644
index 000000000..8a43ff0d4
--- /dev/null
+++ b/web/.github/workflows/ci.yml
@@ -0,0 +1,47 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - main
+ - master
+ pull_request: {}
+
+concurrency:
+ group: ci-${{ github.head_ref || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ lint:
+ name: "Lint"
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Install Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18
+ cache: npm
+ - name: Install Dependencies
+ run: npm ci
+ - name: Lint
+ run: npm run lint
+
+ test:
+ name: "Test"
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Install Node
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18
+ cache: npm
+ - name: Install Dependencies
+ run: npm ci
+ - name: Run Tests
+ run: npm test
diff --git a/web/.gitignore b/web/.gitignore
new file mode 100644
index 000000000..71ad79d02
--- /dev/null
+++ b/web/.gitignore
@@ -0,0 +1,25 @@
+# compiled output
+/dist/
+/declarations/
+
+# dependencies
+/node_modules/
+
+# misc
+/.env*
+/.pnp*
+/.eslintcache
+/coverage/
+/npm-debug.log*
+/testem.log
+/yarn-error.log
+
+# ember-try
+/.node_modules.ember-try/
+/npm-shrinkwrap.json.ember-try
+/package.json.ember-try
+/package-lock.json.ember-try
+/yarn.lock.ember-try
+
+# broccoli-debug
+/DEBUG/
diff --git a/web/.prettierignore b/web/.prettierignore
new file mode 100644
index 000000000..9385391f2
--- /dev/null
+++ b/web/.prettierignore
@@ -0,0 +1,13 @@
+# unconventional js
+/blueprints/*/files/
+
+# compiled output
+/dist/
+
+# misc
+/coverage/
+!.*
+.*/
+
+# ember-try
+/.node_modules.ember-try/
diff --git a/web/.prettierrc.js b/web/.prettierrc.js
index eeb15edb6..cbb322885 100644
--- a/web/.prettierrc.js
+++ b/web/.prettierrc.js
@@ -3,4 +3,12 @@ module.exports = {
"prettier-plugin-ember-template-tag",
"prettier-plugin-tailwindcss",
],
+ overrides: [
+ {
+ files: '*.{js,ts}',
+ options: {
+ singleQuote: true,
+ },
+ },
+ ],
};
diff --git a/web/.stylelintignore b/web/.stylelintignore
new file mode 100644
index 000000000..a0cf71cbd
--- /dev/null
+++ b/web/.stylelintignore
@@ -0,0 +1,8 @@
+# unconventional files
+/blueprints/*/files/
+
+# compiled output
+/dist/
+
+# addons
+/.node_modules.ember-try/
diff --git a/web/.stylelintrc.js b/web/.stylelintrc.js
new file mode 100644
index 000000000..021c539ad
--- /dev/null
+++ b/web/.stylelintrc.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = {
+ extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'],
+};
diff --git a/web/.watchmanconfig b/web/.watchmanconfig
new file mode 100644
index 000000000..f9c3d8f84
--- /dev/null
+++ b/web/.watchmanconfig
@@ -0,0 +1,3 @@
+{
+ "ignore_dirs": ["dist"]
+}
diff --git a/web/.yarnrc.yml b/web/.yarnrc.yml
index d0d772efd..3186f3f07 100644
--- a/web/.yarnrc.yml
+++ b/web/.yarnrc.yml
@@ -1 +1 @@
-yarnPath: .yarn/releases/yarn-3.3.0.cjs
+nodeLinker: node-modules
diff --git a/web/app/adapters/application.ts b/web/app/adapters/application.ts
index ed88e39e8..31f5826be 100644
--- a/web/app/adapters/application.ts
+++ b/web/app/adapters/application.ts
@@ -1,8 +1,8 @@
import JSONAdapter from "@ember-data/adapter/json-api";
import { inject as service } from "@ember/service";
-import ConfigService from "hermes/services/config";
-import FetchService from "hermes/services/fetch";
-import SessionService from "hermes/services/session";
+import type ConfigService from "hermes/services/config";
+import type FetchService from "hermes/services/fetch";
+import type SessionService from "hermes/services/session";
export default class ApplicationAdapter extends JSONAdapter {
@service("config") declare configSvc: ConfigService;
@@ -14,9 +14,15 @@ export default class ApplicationAdapter extends JSONAdapter {
}
get headers() {
+ if (!this.configSvc.config.skip_google_auth) {
+ return {
+ "Hermes-Google-Access-Token":
+ this.session.data.authenticated.access_token,
+ };
+ }
+
return {
- "Hermes-Google-Access-Token":
- this.session.data.authenticated.access_token,
+ "Hermes-Access-Token": this.session.data.authenticated.access_token,
};
}
}
diff --git a/web/app/adapters/group.ts b/web/app/adapters/group.ts
index aa50d27d8..5786a9b34 100644
--- a/web/app/adapters/group.ts
+++ b/web/app/adapters/group.ts
@@ -1,4 +1,4 @@
-import DS from "ember-data";
+import type DS from "ember-data";
import ApplicationAdapter from "./application";
import RSVP from "rsvp";
@@ -8,7 +8,7 @@ export default class GroupAdapter extends ApplicationAdapter {
* Returns an array of groups that match the query.
* Also used by the `queryRecord` method.
*/
- query(_store: DS.Store, _type: DS.Model, query: { query: string }) {
+ query(_store: DS.Store, _type: any, query: { query: string }) {
const results = this.fetchSvc
.fetch(`/api/${this.configSvc.config.api_version}/groups`, {
method: "POST",
diff --git a/web/app/adapters/jira-issue.ts b/web/app/adapters/jira-issue.ts
index 3e2caa740..be9edfaed 100644
--- a/web/app/adapters/jira-issue.ts
+++ b/web/app/adapters/jira-issue.ts
@@ -1,8 +1,8 @@
-import DS from "ember-data";
+import type DS from "ember-data";
import ApplicationAdapter from "./application";
import RSVP from "rsvp";
-import ModelRegistry from "ember-data/types/registries/model";
-import JiraIssueModel from "hermes/models/jira-issue";
+import type ModelRegistry from "ember-data/types/registries/model";
+import type JiraIssueModel from "hermes/models/jira-issue";
export default class JiraIssueAdapter extends ApplicationAdapter {
findRecord(
diff --git a/web/app/adapters/person.ts b/web/app/adapters/person.ts
index 7a5d2ca89..f32c29b65 100644
--- a/web/app/adapters/person.ts
+++ b/web/app/adapters/person.ts
@@ -1,4 +1,4 @@
-import DS from "ember-data";
+import type DS from "ember-data";
import ApplicationAdapter from "./application";
import RSVP from "rsvp";
@@ -8,7 +8,7 @@ export default class PersonAdapter extends ApplicationAdapter {
* Default query: `/people?query=foo`
* Our custom query: `/people` with `{ query: "foo" }` in the request body.
*/
- query(_store: DS.Store, _type: DS.Model, query: { query: string }) {
+ query(_store: DS.Store, _type: any, query: { query: string }) {
const results = this.fetchSvc
.fetch(`/api/${this.configSvc.config.api_version}/people`, {
method: "POST",
diff --git a/web/app/app.ts b/web/app/app.ts
index c5a97aa85..033a65828 100644
--- a/web/app/app.ts
+++ b/web/app/app.ts
@@ -2,6 +2,33 @@ import Application from '@ember/application';
import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers';
import config from 'hermes/config/environment';
+import Ember from 'ember';
+import EmberObject from '@ember/object';
+import { assert } from '@ember/debug';
+
+// Expose Ember globally for legacy addons like torii
+// This is needed because torii@0.10.1 expects a global Ember object with specific properties
+if (typeof window !== 'undefined') {
+ (window as any).Ember = Ember;
+ // Ensure Ember.Object is available for torii
+ if (!(window as any).Ember.Object) {
+ (window as any).Ember.Object = EmberObject;
+ }
+ // Ember.Error is available from the main ember package
+ if (!(window as any).Ember.Error) {
+ // Create a custom Error class since Ember.Error was deprecated
+ (window as any).Ember.Error = class EmberError extends Error {
+ constructor(message?: string) {
+ super(message);
+ this.name = 'EmberError';
+ }
+ };
+ }
+ // Ensure Ember.assert is available
+ if (!(window as any).Ember.assert) {
+ (window as any).Ember.assert = assert;
+ }
+}
export default class App extends Application {
modulePrefix = config.modulePrefix;
diff --git a/web/app/authenticators/cookie.ts b/web/app/authenticators/cookie.ts
new file mode 100644
index 000000000..685ae8c82
--- /dev/null
+++ b/web/app/authenticators/cookie.ts
@@ -0,0 +1,88 @@
+import { inject as service } from "@ember/service";
+// @ts-ignore
+import BaseAuthenticator from "ember-simple-auth/authenticators/base";
+import type ConfigService from "hermes/services/config";
+import type FetchService from "hermes/services/fetch";
+
+/**
+ * Cookie-based authenticator for backend-managed authentication.
+ *
+ * This app uses backend (Go server) managed authentication via HTTP-only cookies.
+ * The backend handles the entire Microsoft OAuth flow and sets secure cookies.
+ * This authenticator simply validates that the backend session is still valid
+ * by checking if /api/v1/me succeeds.
+ */
+export default class CookieAuthenticator extends BaseAuthenticator {
+ @service("config") declare configSvc: ConfigService;
+ @service("fetch") declare fetchSvc: FetchService;
+
+ private get isBackendManagedAuth(): boolean {
+ return (
+ this.configSvc.config.skip_google_auth &&
+ !this.configSvc.config.skip_microsoft_auth
+ );
+ }
+
+ /**
+ * Authenticate by checking if the backend session is valid.
+ * Since the backend manages auth, we just verify we can access /api/v2/me
+ */
+ async authenticate() {
+ if (!this.isBackendManagedAuth) {
+ throw new Error("Cookie authentication is only available in Microsoft auth mode");
+ }
+
+ try {
+ // Check if backend session is valid by calling /api/v2/me
+ // Note: Using v2 directly since backend supports v2 API
+ const response = await this.fetchSvc.fetch("/api/v2/me", {
+ method: "HEAD",
+ });
+
+ if (response && response.ok) {
+ // Backend session is valid - return minimal session data
+ // The actual user data will be loaded by authenticatedUser.loadInfo
+ return { authenticatedAt: new Date().toISOString() };
+ } else {
+ throw new Error("Backend session invalid");
+ }
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ /**
+ * Restore the session by checking if backend session is still valid.
+ * Called on app initialization to restore previous session.
+ */
+ async restore(data: any) {
+ if (!this.isBackendManagedAuth) {
+ throw new Error("Session restore failed - cookie auth is not active");
+ }
+
+ // Check if backend session is still valid
+ try {
+ const response = await this.fetchSvc.fetch("/api/v2/me", {
+ method: "HEAD",
+ });
+
+ if (response && response.ok) {
+ return data; // Backend session still valid
+ } else {
+ throw new Error("Backend session expired");
+ }
+ } catch (error) {
+ throw new Error("Session restore failed - backend session invalid");
+ }
+ }
+
+ /**
+ * Invalidation is handled by the backend (/logout endpoint).
+ * This just confirms the session should be cleared client-side.
+ */
+ async invalidate() {
+ // Backend handles actual invalidation via /logout
+ // This just allows ESA to clear client-side session data
+ return Promise.resolve();
+ }
+}
diff --git a/web/app/authenticators/microsoft.ts b/web/app/authenticators/microsoft.ts
new file mode 100644
index 000000000..bc4991aa8
--- /dev/null
+++ b/web/app/authenticators/microsoft.ts
@@ -0,0 +1,107 @@
+import { inject as service } from "@ember/service";
+// @ts-ignore
+import BaseAuthenticator from "ember-simple-auth/authenticators/base";
+import type ConfigService from "hermes/services/config";
+import type FetchService from "hermes/services/fetch";
+
+// Define Microsoft configuration interface locally
+interface MicrosoftConfig {
+ clientId: string;
+ clientSecret: string;
+ tenantId: string;
+ redirectUri: string;
+}
+
+export default class MicrosoftAuthenticator extends BaseAuthenticator {
+ @service("config") declare configSvc: ConfigService;
+ @service("fetch") declare fetchSvc: FetchService;
+
+ /**
+ * Authenticate using the authorization code from Microsoft OAuth callback.
+ */
+ async authenticate(options: any = {}) {
+ try {
+ const { code, state } = options;
+
+ console.log("Microsoft authenticator called with code and state");
+
+ if (!code || !state) {
+ throw new Error("Missing code or state parameter from Microsoft OAuth callback");
+ }
+
+ // Use type assertion to bypass the TypeScript error
+ const microsoft = (this.configSvc as any).microsoft as MicrosoftConfig;
+
+ // Exchange the authorization code for an access token
+ const tokenEndpoint = `https://login.microsoftonline.com/${microsoft.tenantId}/oauth2/v2.0/token`;
+ const body = new URLSearchParams({
+ client_id: microsoft.clientId,
+ client_secret: microsoft.clientSecret,
+ code,
+ grant_type: "authorization_code",
+ redirect_uri: microsoft.redirectUri,
+ });
+
+ console.log("Exchanging authorization code for access token...");
+
+ const response = await this.fetchSvc.fetch(tokenEndpoint, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: body.toString(),
+ });
+
+ if (!response || !response.ok) {
+ const error = response ? await response.json() : "No response received";
+ console.error("Failed to exchange authorization code:", error);
+ throw new Error("Failed to exchange authorization code");
+ }
+
+ const tokenData = await response.json();
+ console.log("Access token received:", tokenData);
+
+ // Store the token in the session
+ return {
+ access_token: tokenData.access_token,
+ token_type: tokenData.token_type,
+ expires_at: Date.now() + tokenData.expires_in * 1000, // Calculate expiration time
+ };
+ } catch (error) {
+ console.error("Microsoft authentication error:", error);
+ throw error instanceof Error ? error : new Error("Authentication failed");
+ }
+ }
+
+ /**
+ * Restore the session using the stored token.
+ */
+ async restore(data: any) {
+ console.log("Restoring session with data:", data);
+
+ if (!data || !data.access_token) {
+ console.error("No access token found in session data");
+ throw new Error("No access token found");
+ }
+
+ // Optionally, validate the token with Microsoft or check expiration
+ const isTokenValid = data.expires_at && Date.now() < data.expires_at;
+ if (!isTokenValid) {
+ console.error("Access token is expired or invalid");
+ throw new Error("Access token is expired or invalid");
+ }
+
+ console.log("Session restored successfully");
+ return data;
+ }
+
+ /**
+ * Invalidate the session by clearing the stored token.
+ */
+ async invalidate() {
+ console.log("Invalidating Microsoft session");
+ // Clear any stored tokens
+ localStorage.removeItem("ms_auth_debug_token");
+ return Promise.resolve();
+ }
+}
diff --git a/web/app/authenticators/torii.ts b/web/app/authenticators/torii.ts
index 194cd4c4b..944cfc054 100644
--- a/web/app/authenticators/torii.ts
+++ b/web/app/authenticators/torii.ts
@@ -1,8 +1,8 @@
// @ts-ignore -- TODO: Add Types
import Torii from "ember-simple-auth/authenticators/torii";
import { inject as service } from "@ember/service";
-import ConfigService from "hermes/services/config";
-import FetchService from "hermes/services/fetch";
+import type ConfigService from "hermes/services/config";
+import type FetchService from "hermes/services/fetch";
export default class ToriiAuthenticator extends Torii {
@service("config") declare configSvc: ConfigService;
@@ -11,8 +11,8 @@ export default class ToriiAuthenticator extends Torii {
// Appears unused, but necessary for the session service
@service declare torii: unknown;
- async restore() {
- const data = await super.restore(...arguments);
+ async restore(data: any = {}) {
+ const restoredData = await super.restore(data);
/**
* A rejecting promise indicates invalid session data and will result
* in the session being invalidated or remaining unauthenticated.
@@ -21,9 +21,9 @@ export default class ToriiAuthenticator extends Torii {
.fetch(`/api/${this.configSvc.config.api_version}/me`, {
method: "HEAD",
headers: {
- "Hermes-Google-Access-Token": data.access_token,
+ "Hermes-Google-Access-Token": restoredData.access_token,
},
})
- .then(() => data);
+ .then(() => restoredData);
}
}
diff --git a/web/app/components/copy-u-r-l-button.ts b/web/app/components/copy-u-r-l-button.ts
index e724201fb..c47485b12 100644
--- a/web/app/components/copy-u-r-l-button.ts
+++ b/web/app/components/copy-u-r-l-button.ts
@@ -3,10 +3,10 @@ import { tracked } from "@glimmer/tracking";
import Ember from "ember";
import { restartableTask, timeout } from "ember-concurrency";
import { inject as service } from "@ember/service";
-import { Placement } from "@floating-ui/dom";
+import type { Placement } from "@floating-ui/dom";
import { action } from "@ember/object";
import { assert } from "@ember/debug";
-import HermesFlashMessagesService from "hermes/services/flash-messages";
+import type HermesFlashMessagesService from "hermes/services/flash-messages";
interface CopyURLButtonComponentSignature {
Element: HTMLButtonElement;
diff --git a/web/app/components/custom-editable-field.ts b/web/app/components/custom-editable-field.ts
index 6d31b55cd..cc18ca23f 100644
--- a/web/app/components/custom-editable-field.ts
+++ b/web/app/components/custom-editable-field.ts
@@ -1,7 +1,8 @@
import { action, get } from "@ember/object";
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
-import { CustomEditableField, HermesDocument } from "hermes/types/document";
+import type { CustomEditableField } from "hermes/types/document";
+import type { HermesDocument } from "hermes/types/document";
interface CustomEditableFieldComponentSignature {
Element: HTMLDivElement;
diff --git a/web/app/components/dashboard/docs-awaiting-review.ts b/web/app/components/dashboard/docs-awaiting-review.ts
index 4f6901c77..356e11a7f 100644
--- a/web/app/components/dashboard/docs-awaiting-review.ts
+++ b/web/app/components/dashboard/docs-awaiting-review.ts
@@ -1,7 +1,7 @@
import { action } from "@ember/object";
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
-import { HermesDocument } from "hermes/types/document";
+import type { HermesDocument } from "hermes/types/document";
interface DashboardDocsAwaitingReviewComponentSignature {
Element: null;
diff --git a/web/app/components/dashboard/docs-awaiting-review/doc.ts b/web/app/components/dashboard/docs-awaiting-review/doc.ts
index 9ee68b596..e06a12537 100644
--- a/web/app/components/dashboard/docs-awaiting-review/doc.ts
+++ b/web/app/components/dashboard/docs-awaiting-review/doc.ts
@@ -1,5 +1,5 @@
import Component from "@glimmer/component";
-import { HermesDocument } from "hermes/types/document";
+import type { HermesDocument } from "hermes/types/document";
interface DashboardDocsAwaitingReviewDocComponentSignature {
Element: null;
diff --git a/web/app/components/dashboard/index.ts b/web/app/components/dashboard/index.ts
index 3f0ed421e..c78a838f5 100644
--- a/web/app/components/dashboard/index.ts
+++ b/web/app/components/dashboard/index.ts
@@ -1,6 +1,6 @@
import Component from "@glimmer/component";
-import AuthenticatedUserService from "hermes/services/authenticated-user";
-import { HermesDocument } from "hermes/types/document";
+import type AuthenticatedUserService from "hermes/services/authenticated-user";
+import type { HermesDocument } from "hermes/types/document";
import { inject as service } from "@ember/service";
interface DashboardIndexComponentSignature {
diff --git a/web/app/components/dashboard/latest-docs.ts b/web/app/components/dashboard/latest-docs.ts
index c05ce6624..1b4fdc563 100644
--- a/web/app/components/dashboard/latest-docs.ts
+++ b/web/app/components/dashboard/latest-docs.ts
@@ -1,6 +1,6 @@
import Component from "@glimmer/component";
-import LatestDocsService from "hermes/services/latest-docs";
-import { HermesDocument } from "hermes/types/document";
+import type LatestDocsService from "hermes/services/latest-docs";
+import type { HermesDocument } from "hermes/types/document";
import { inject as service } from "@ember/service";
import { DEFAULT_FILTERS } from "hermes/services/active-filters";
diff --git a/web/app/components/dashboard/new-features-banner.hbs b/web/app/components/dashboard/new-features-banner.hbs
index 6cecbac2a..be5ff0587 100644
--- a/web/app/components/dashboard/new-features-banner.hbs
+++ b/web/app/components/dashboard/new-features-banner.hbs
@@ -1,20 +1,13 @@
{{#if this.isShown}}
-
- Important Notice: Hermes has been moved to a new location.
- Please update your bookmarks and begin using our new hermes site.
-
- Read-Only Mode: The current system is now in read-only mode, so please do not update any existing Hermes documents to ensure data integrity.
- All documents have been successfully migrated and are now available in Hermes SharePoint.
-
- Access New Hermes System →
-
+ {{this.title}}
{{/if}}
diff --git a/web/app/components/dashboard/new-features-banner.ts b/web/app/components/dashboard/new-features-banner.ts
index 81a0241c9..36f1789e8 100644
--- a/web/app/components/dashboard/new-features-banner.ts
+++ b/web/app/components/dashboard/new-features-banner.ts
@@ -3,7 +3,7 @@ import { tracked } from "@glimmer/tracking";
import window from "ember-window-mock";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
-import ConfigService from "hermes/services/config";
+import type ConfigService from "hermes/services/config";
export const NEW_FEATURES_BANNER_LOCAL_STORAGE_ITEM =
"apr-12-2024-newFeatureBannerIsShown";
@@ -14,27 +14,58 @@ interface DashboardNewFeaturesBannerSignature {
export default class DashboardNewFeaturesBanner extends Component {
/**
- * Used to determine whether the Google Groups callout should be shown.
+ * Used to determine whether the Groups callout should be shown.
*/
@service("config") declare configSvc: ConfigService;
@tracked protected isDismissed = false;
+ protected get title(): string {
+ return this.configSvc.config.skip_google_auth
+ ? "Welcome to new Hermes with SharePoint support!"
+ : "Welcome to the new Hermes experience!";
+ }
+
/**
* Whether the banner should be shown.
- * PERMANENT MIGRATION BANNER - Always shown, cannot be dismissed
+ * Set true on first visit to the dashboard and remains true
+ * until the user dismisses the banner.
*/
protected get isShown(): boolean {
- // Always return true for permanent banner
- return true;
+ /**
+ * If the banner has been dismissed, don't show it.
+ * This check causes the property to recompute when dismissed.
+ */
+ if (this.isDismissed) {
+ return false;
+ }
+
+ const storageItem = window.localStorage.getItem(
+ NEW_FEATURES_BANNER_LOCAL_STORAGE_ITEM,
+ );
+
+ if (storageItem === null) {
+ window.localStorage.setItem(
+ NEW_FEATURES_BANNER_LOCAL_STORAGE_ITEM,
+ "true",
+ );
+ return true;
+ } else if (storageItem === "true") {
+ return true;
+ } else return false;
}
/**
- * PERMANENT BANNER - No dismiss action needed
- * This action is removed for permanent migration banner
+ * The action called when the user clicks the dismiss button.
+ * Sets the local storage item to false and sets the isDismissed
+ * property to true so the banner is immediately hidden.
*/
@action protected dismiss() {
- // No-op for permanent banner - cannot be dismissed
+ window.localStorage.setItem(
+ NEW_FEATURES_BANNER_LOCAL_STORAGE_ITEM,
+ "false",
+ );
+ this.isDismissed = true;
}
}
diff --git a/web/app/components/dashboard/recently-viewed.ts b/web/app/components/dashboard/recently-viewed.ts
index 7d3f9b0f5..d25baa4fb 100644
--- a/web/app/components/dashboard/recently-viewed.ts
+++ b/web/app/components/dashboard/recently-viewed.ts
@@ -1,15 +1,16 @@
import Component from "@glimmer/component";
-import ViewportService from "hermes/services/viewport";
+import type ViewportService from "hermes/services/viewport";
import theme from "tailwindcss/defaultTheme";
import { assert } from "@ember/debug";
import { action } from "@ember/object";
import { debounce } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";
-import RecentlyViewedService, {
+import type {
RecentlyViewedDoc,
RecentlyViewedProject,
} from "hermes/services/recently-viewed";
+import type RecentlyViewedService from "hermes/services/recently-viewed";
export const RECENTLY_VIEWED_SCROLL_AMOUNT = 300;
diff --git a/web/app/components/dashboard/recently-viewed/item.ts b/web/app/components/dashboard/recently-viewed/item.ts
index f942ef41a..033183ce7 100644
--- a/web/app/components/dashboard/recently-viewed/item.ts
+++ b/web/app/components/dashboard/recently-viewed/item.ts
@@ -1,6 +1,6 @@
import { assert } from "@ember/debug";
import Component from "@glimmer/component";
-import {
+import type {
RecentlyViewedDoc,
RecentlyViewedProject,
} from "hermes/services/recently-viewed";
diff --git a/web/app/components/doc/folder-affordance.ts b/web/app/components/doc/folder-affordance.ts
index 6d8581381..c167a08e9 100644
--- a/web/app/components/doc/folder-affordance.ts
+++ b/web/app/components/doc/folder-affordance.ts
@@ -1,5 +1,5 @@
import Component from "@glimmer/component";
-import { DocThumbnailSize } from "hermes/components/doc/thumbnail";
+import type { DocThumbnailSize } from "hermes/components/doc/thumbnail";
import { HermesSize } from "hermes/types/sizes";
interface DocFolderAffordanceSignature {
diff --git a/web/app/components/doc/status.ts b/web/app/components/doc/status.ts
index 0c4aef7a2..20c18a737 100644
--- a/web/app/components/doc/status.ts
+++ b/web/app/components/doc/status.ts
@@ -1,6 +1,6 @@
import { dasherize } from "@ember/string";
import Component from "@glimmer/component";
-import { HdsBadgeType } from "hds/_shared";
+import type { HdsBadgeType } from "hds/_shared";
interface DocStatusComponentSignature {
Element: HTMLDivElement;
diff --git a/web/app/components/doc/thumbnail.hbs b/web/app/components/doc/thumbnail.hbs
index 47b8939b2..365be6eca 100644
--- a/web/app/components/doc/thumbnail.hbs
+++ b/web/app/components/doc/thumbnail.hbs
@@ -13,13 +13,13 @@
{{/if}}
- {{#if (or this.isApproved this.isObsolete)}}
+ {{#if (or this.isApproved this.isObsolete this.isArchived)}}
{{/if}}
diff --git a/web/app/components/doc/thumbnail.ts b/web/app/components/doc/thumbnail.ts
index e508b16d6..057e9827c 100644
--- a/web/app/components/doc/thumbnail.ts
+++ b/web/app/components/doc/thumbnail.ts
@@ -3,7 +3,7 @@ import { dasherize } from "@ember/string";
import getProductId from "hermes/utils/get-product-id";
import { HermesSize } from "hermes/types/sizes";
import { inject as service } from "@ember/service";
-import ProductAreasService from "hermes/services/product-areas";
+import type ProductAreasService from "hermes/services/product-areas";
export type DocThumbnailSize = Exclude;
@@ -13,6 +13,7 @@ interface DocThumbnailComponentSignature {
status?: string;
product?: string;
size?: `${DocThumbnailSize}`;
+ archived?: boolean;
};
}
@@ -50,6 +51,10 @@ export default class DocThumbnailComponent extends Component
+ height="100%"
+ src={{this.webUrl}}
+ sandbox="allow-scripts allow-same-origin allow-top-navigation allow-popups allow-forms"
+ style="border: none;"
+ >
+
+ {{!-- Fallback content if iframe fails to load --}}
+
+
Document Viewer
+
Unable to embed document. Open in new tab
+
+ {{!-- Additional Office Online fallback options --}}
+ {{#if this.isSharePointDocument}}
+
+
Alternative viewing options:
+
+
+ Try Office Online
+
+
+ Open in SharePoint
+
+
+
+ {{/if}}
+
+
+
+ {{else}}
+ {{! Google Drive document - show iframe }}
+
+ {{/if}}
{{/unless}}
diff --git a/web/app/components/document/index.ts b/web/app/components/document/index.ts
index 6cca0e784..072a4064c 100644
--- a/web/app/components/document/index.ts
+++ b/web/app/components/document/index.ts
@@ -1,10 +1,10 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
-import { HermesDocument } from "hermes/types/document";
+import type { HermesDocument } from "hermes/types/document";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
-import { HermesDocumentType } from "hermes/types/document-type";
-import AuthenticatedUserService from "hermes/services/authenticated-user";
+import type { HermesDocumentType } from "hermes/types/document-type";
+import type AuthenticatedUserService from "hermes/services/authenticated-user";
interface DocumentIndexComponentSignature {
Args: {
@@ -22,6 +22,49 @@ export default class DocumentIndexComponent extends Component
- {{! Migration Banner }}
-
{{! div to break the parent's space-y styles }}
{{! Add bgColor to make badge `multiply` work }}
@@ -486,7 +484,7 @@
data-test-sidebar-publish-for-review-button
@text="Publish for review..."
class="w-full"
- {{on "click" (set this "requestReviewModalIsShown" true)}}
+ {{on "click" this.showRequestReviewModal}}
/>
{{! Delete... }}
{{else}}
{{! isApprover or isGroupApproverOnly}}
diff --git a/web/app/components/document/sidebar.ts b/web/app/components/document/sidebar.ts
index fdeaaa654..44be189d1 100644
--- a/web/app/components/document/sidebar.ts
+++ b/web/app/components/document/sidebar.ts
@@ -14,35 +14,37 @@ import {
import { capitalize } from "@ember/string";
import cleanString from "hermes/utils/clean-string";
import { debounce, schedule } from "@ember/runloop";
-import FetchService from "hermes/services/fetch";
-import RouterService from "@ember/routing/router-service";
-import SessionService from "hermes/services/session";
-import { CustomEditableField, HermesDocument } from "hermes/types/document";
+import type FetchService from "hermes/services/fetch";
+import type RouterService from "@ember/routing/router-service";
+import type SessionService from "hermes/services/session";
+import type { CustomEditableField } from "hermes/types/document";
+import type { HermesDocument } from "hermes/types/document";
import { assert } from "@ember/debug";
-import Route from "@ember/routing/route";
+import type Route from "@ember/routing/route";
import Ember from "ember";
import htmlElement from "hermes/utils/html-element";
-import ConfigService from "hermes/services/config";
+import type ConfigService from "hermes/services/config";
import isValidURL from "hermes/utils/is-valid-u-r-l";
-import { HermesDocumentType } from "hermes/types/document-type";
-import HermesFlashMessagesService from "hermes/services/flash-messages";
-import {
+import type { HermesDocumentType } from "hermes/types/document-type";
+import type HermesFlashMessagesService from "hermes/services/flash-messages";
+import type {
HermesProjectInfo,
HermesProjectResources,
} from "hermes/types/project";
import updateRelatedResourcesSortOrder from "hermes/utils/update-related-resources-sort-order";
import { ProjectStatus } from "hermes/types/project-status";
-import { RelatedHermesDocument } from "../related-resources";
-import PersonModel from "hermes/models/person";
-import RecentlyViewedService from "hermes/services/recently-viewed";
-import StoreService from "hermes/services/store";
-import ModalAlertsService, { ModalType } from "hermes/services/modal-alerts";
+import type { RelatedHermesDocument } from "../related-resources";
+import type PersonModel from "hermes/models/person";
+import type RecentlyViewedService from "hermes/services/recently-viewed";
+import type StoreService from "hermes/services/store";
+import type ModalAlertsService from "hermes/services/modal-alerts";
+import { ModalType } from "hermes/services/modal-alerts";
interface DocumentSidebarComponentSignature {
Args: {
profile: PersonModel;
document: HermesDocument;
- docType: Promise
;
+ docType: HermesDocumentType | Promise;
isCollapsed: boolean;
viewerIsGroupApprover: boolean;
toggleCollapsed: () => void;
@@ -715,6 +717,18 @@ export default class DocumentSidebarComponent extends Component {
+ void Promise.resolve(this.args.docType).then((docType) => {
this.docType = docType;
});
}
@@ -966,6 +980,21 @@ export default class DocumentSidebarComponent extends Component doc.googleFileID !== this.docID,
+ (doc) => doc.FileID !== this.docID,
);
// update the sort order of all resources
@@ -1299,7 +1329,7 @@ export default class DocumentSidebarComponent extends Component {
return {
- googleFileID: doc.googleFileID,
+ FileID: doc.FileID,
sortOrder: doc.sortOrder,
};
}),
@@ -1345,7 +1375,7 @@ export default class DocumentSidebarComponent extends Component {
return {
- googleFileID: doc.googleFileID,
+ FileID: doc.FileID,
sortOrder: doc.sortOrder,
};
},
diff --git a/web/app/components/document/sidebar/header.hbs b/web/app/components/document/sidebar/header.hbs
index a31bb723c..3f259f7f3 100644
--- a/web/app/components/document/sidebar/header.hbs
+++ b/web/app/components/document/sidebar/header.hbs
@@ -30,10 +30,10 @@
/>
{{/if}}
diff --git a/web/app/components/document/sidebar/header.ts b/web/app/components/document/sidebar/header.ts
index 40c7347d7..bf9d768a2 100644
--- a/web/app/components/document/sidebar/header.ts
+++ b/web/app/components/document/sidebar/header.ts
@@ -1,5 +1,5 @@
import Component from "@glimmer/component";
-import { HermesDocument } from "hermes/types/document";
+import type { HermesDocument } from "hermes/types/document";
interface DocumentSidebarHeaderComponentSignature {
Element: HTMLDivElement;
@@ -17,6 +17,38 @@ interface DocumentSidebarHeaderComponentSignature {
}
export default class DocumentSidebarHeaderComponent extends Component {
+ protected get externalLinkHref(): string {
+ const document = this.args.document as HermesDocument & {
+ directEditURL?: string;
+ webUrl?: string;
+ };
+
+ return (
+ document.directEditURL ??
+ document.webUrl ??
+ `https://docs.google.com/document/d/${document.objectID}`
+ );
+ }
+
+ protected get externalLinkTooltipText(): string {
+ const document = this.args.document as HermesDocument & {
+ directEditURL?: string;
+ webUrl?: string;
+ };
+ const url = document.directEditURL ?? document.webUrl;
+
+ if (!url) return "Open in Google";
+
+ try {
+ const hostname = new URL(url).hostname;
+ return hostname.endsWith(".sharepoint.com") || hostname === "sharepoint.com"
+ ? "Open in SharePoint"
+ : "Open in Google";
+ } catch {
+ return "Open in Google";
+ }
+ }
+
/**
* Whether the tooltip is forced open, regardless of hover state.
* True if the parent component has passed a tooltip text prop,
diff --git a/web/app/components/document/sidebar/migration-banner.hbs b/web/app/components/document/sidebar/migration-banner.hbs
deleted file mode 100644
index caae7ee14..000000000
--- a/web/app/components/document/sidebar/migration-banner.hbs
+++ /dev/null
@@ -1,19 +0,0 @@
-{{#if this.isShown}}
-
-{{/if}}
diff --git a/web/app/components/document/sidebar/migration-banner.ts b/web/app/components/document/sidebar/migration-banner.ts
deleted file mode 100644
index d641c6c70..000000000
--- a/web/app/components/document/sidebar/migration-banner.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import Component from "@glimmer/component";
-
-interface DocumentSidebarMigrationBannerSignature {
- Args: {};
-}
-
-export default class DocumentSidebarMigrationBanner extends Component {
- /**
- * Whether the banner should be shown.
- * PERMANENT MIGRATION BANNER - Always shown, cannot be dismissed
- */
- protected get isShown(): boolean {
- // Always return true for permanent banner
- return true;
- }
-}
-
-declare module "@glint/environment-ember-loose/registry" {
- export default interface Registry {
- "Document::Sidebar::MigrationBanner": typeof DocumentSidebarMigrationBanner;
- }
-}
diff --git a/web/app/components/document/sidebar/related-resources.ts b/web/app/components/document/sidebar/related-resources.ts
index 09646cfac..fffc44280 100644
--- a/web/app/components/document/sidebar/related-resources.ts
+++ b/web/app/components/document/sidebar/related-resources.ts
@@ -2,20 +2,21 @@ import Component from "@glimmer/component";
import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
import { inject as service } from "@ember/service";
-import FetchService from "hermes/services/fetch";
-import ConfigService from "hermes/services/config";
-import AlgoliaService from "hermes/services/algolia";
+import type FetchService from "hermes/services/fetch";
+import type ConfigService from "hermes/services/config";
+import type AlgoliaService from "hermes/services/algolia";
import { restartableTask, task } from "ember-concurrency";
import { next, schedule } from "@ember/runloop";
import htmlElement from "hermes/utils/html-element";
-import {
+import type {
RelatedExternalLink,
RelatedHermesDocument,
- RelatedResource,
+ RelatedResource} from "hermes/components/related-resources";
+import {
RelatedResourceSelector,
} from "hermes/components/related-resources";
import { assert } from "@ember/debug";
-import HermesFlashMessagesService from "hermes/services/flash-messages";
+import type HermesFlashMessagesService from "hermes/services/flash-messages";
import { FLASH_MESSAGES_LONG_TIMEOUT } from "hermes/utils/ember-cli-flash/timeouts";
import updateRelatedResourcesSortOrder from "hermes/utils/update-related-resources-sort-order";
import highlightElement from "hermes/utils/ember-animated/highlight-element";
@@ -61,7 +62,7 @@ export default class DocumentSidebarRelatedResourcesComponent extends Component<
const hermesDocuments = this.relatedDocuments.map((doc) => {
return {
- googleFileID: doc.googleFileID,
+ FileID: doc.FileID,
sortOrder: doc.sortOrder,
};
});
@@ -84,14 +85,9 @@ export default class DocumentSidebarRelatedResourcesComponent extends Component<
* The combined resources array, formatted for the RelatedResourcesList.
*/
protected get relatedResources(): RelatedResource[] {
- let resourcesArray: RelatedResource[] = [];
-
- this.updateSortOrder();
-
- resourcesArray.pushObjects(this.relatedDocuments);
- resourcesArray.pushObjects(this.relatedLinks);
+ this.updateSortOrder();
- return resourcesArray;
+ return [...this.relatedDocuments, ...this.relatedLinks];
}
/**
* Whether the "Add Resource" button should be hidden.
@@ -172,10 +168,10 @@ export default class DocumentSidebarRelatedResourcesComponent extends Component<
let cachedDocuments = this.relatedDocuments.slice();
if ("url" in resource) {
- this.relatedLinks.unshiftObject(resource);
+ this.relatedLinks = [resource as RelatedExternalLink, ...this.relatedLinks];
} else {
resourceSelector = RelatedResourceSelector.HermesDocument;
- this.relatedDocuments.unshiftObject(resource);
+ this.relatedDocuments = [resource as RelatedHermesDocument, ...this.relatedDocuments];
}
void this.saveRelatedResources.perform(
@@ -194,9 +190,9 @@ export default class DocumentSidebarRelatedResourcesComponent extends Component<
const cachedLinks = this.relatedLinks;
if ("url" in resource) {
- this.relatedLinks.removeObject(resource);
+ this.relatedLinks = this.relatedLinks.filter((r) => r !== resource);
} else {
- this.relatedDocuments.removeObject(resource);
+ this.relatedDocuments = this.relatedDocuments.filter((r) => r !== resource);
}
void this.saveRelatedResources.perform(cachedDocuments, cachedLinks);
@@ -257,7 +253,7 @@ export default class DocumentSidebarRelatedResourcesComponent extends Component<
target = htmlElement(targetSelector);
next(() => {
- scrollIntoViewIfNeeded(target as HTMLElement, {
+ scrollIntoViewIfNeeded(target, {
block: "nearest",
behavior: "smooth",
});
diff --git a/web/app/components/document/sidebar/related-resources/list-item.ts b/web/app/components/document/sidebar/related-resources/list-item.ts
index 92de38cca..9ebff474e 100644
--- a/web/app/components/document/sidebar/related-resources/list-item.ts
+++ b/web/app/components/document/sidebar/related-resources/list-item.ts
@@ -2,12 +2,12 @@ import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { assert } from "@ember/debug";
-import {
+import type {
RelatedExternalLink,
RelatedHermesDocument,
RelatedResource,
} from "hermes/components/related-resources";
-import { OverflowItem } from "hermes/components/overflow-menu";
+import type { OverflowItem } from "hermes/components/overflow-menu";
interface DocumentSidebarRelatedResourcesListItemComponentSignature {
Element: HTMLLIElement;
@@ -66,12 +66,12 @@ export default class DocumentSidebarRelatedResourcesListItemComponent extends Co
}
/**
- * The resource's googleFileID, if it exists.
+ * The resource's FileID, if it exists.
* Used in the template to determine internal or external routing.
*/
protected get documentObjectID(): string | null {
- if ("googleFileID" in this.args.resource) {
- return this.args.resource.googleFileID;
+ if ("FileID" in this.args.resource) {
+ return this.args.resource.FileID;
} else {
return null;
}
diff --git a/web/app/components/document/sidebar/related-resources/list-item/resource.ts b/web/app/components/document/sidebar/related-resources/list-item/resource.ts
index ab395a135..1eff86675 100644
--- a/web/app/components/document/sidebar/related-resources/list-item/resource.ts
+++ b/web/app/components/document/sidebar/related-resources/list-item/resource.ts
@@ -1,5 +1,5 @@
import Component from "@glimmer/component";
-import {
+import type {
RelatedHermesDocument,
RelatedResource,
} from "hermes/components/related-resources";
@@ -15,10 +15,10 @@ interface DocumentSidebarRelatedResourcesListItemResourceComponentSignature {
export default class DocumentSidebarRelatedResourcesListItemResourceComponent extends Component {
/**
* Whether the resource is a HermesDocument,
- * as measured by the googleFileID attribute.
+ * as measured by the FileID attribute.
*/
protected get resourceIsDocument(): boolean {
- return "googleFileID" in this.args.resource;
+ return "FileID" in this.args.resource;
}
/**
@@ -75,7 +75,7 @@ export default class DocumentSidebarRelatedResourcesListItemResourceComponent ex
private assertResourceIsDocument(
document: RelatedResource,
): asserts document is RelatedHermesDocument {
- if (!("googleFileID" in document)) {
+ if (!("FileID" in document)) {
throw new Error("resource must be a document");
}
}
diff --git a/web/app/components/document/sidebar/related-resources/list.ts b/web/app/components/document/sidebar/related-resources/list.ts
index fb943e649..cac7225d8 100644
--- a/web/app/components/document/sidebar/related-resources/list.ts
+++ b/web/app/components/document/sidebar/related-resources/list.ts
@@ -2,10 +2,11 @@ import Component from "@glimmer/component";
import move from "ember-animated/motions/move";
import animateTransform from "hermes/utils/ember-animated/animate-transform";
import { easeOutQuad } from "hermes/utils/ember-animated/easings";
-import { TransitionContext, wait } from "ember-animated/.";
+import type TransitionContext from "ember-animated/-private/transition-context";
+import { wait } from "ember-animated";
import { fadeIn, fadeOut } from "ember-animated/motions/opacity";
import { action } from "@ember/object";
-import { Transition } from "ember-animated/-private/transition";
+import type { Transition } from "ember-animated/-private/transition";
import Ember from "ember";
import { emptyTransition } from "hermes/utils/ember-animated/empty-transition";
import { assert } from "@ember/debug";
diff --git a/web/app/components/documents/table.ts b/web/app/components/documents/table.ts
index 28c91d5e5..3e6c92bf7 100644
--- a/web/app/components/documents/table.ts
+++ b/web/app/components/documents/table.ts
@@ -1,6 +1,6 @@
import Component from "@glimmer/component";
-import { HermesDocument } from "hermes/types/document";
-import {
+import type { HermesDocument } from "hermes/types/document";
+import type {
SortAttribute,
SortDirection,
} from "hermes/components/table/sortable-header";
diff --git a/web/app/components/editable-field.ts b/web/app/components/editable-field.ts
index 4fc1ec5d9..f7084d42b 100644
--- a/web/app/components/editable-field.ts
+++ b/web/app/components/editable-field.ts
@@ -4,9 +4,9 @@ import { action } from "@ember/object";
import { schedule, scheduleOnce } from "@ember/runloop";
import { assert } from "@ember/debug";
import { guidFor } from "@ember/object/internals";
-import { HermesDocument } from "hermes/types/document";
+import type { HermesDocument } from "hermes/types/document";
import blinkElement from "hermes/utils/blink-element";
-import { Select } from "ember-power-select/components/power-select";
+import type { Select } from "ember-power-select/components/power-select";
export const FOCUSABLE =
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
diff --git a/web/app/components/editable-field/read-value.ts b/web/app/components/editable-field/read-value.ts
index f82bf4b29..6c89f7963 100644
--- a/web/app/components/editable-field/read-value.ts
+++ b/web/app/components/editable-field/read-value.ts
@@ -1,6 +1,6 @@
import { assert } from "@ember/debug";
import Component from "@glimmer/component";
-import { HermesDocument } from "hermes/types/document";
+import type { HermesDocument } from "hermes/types/document";
interface EditableFieldReadValueSignature {
Args: {
diff --git a/web/app/components/floating-u-i/content.ts b/web/app/components/floating-u-i/content.ts
index 7c58024e4..b775d45a8 100644
--- a/web/app/components/floating-u-i/content.ts
+++ b/web/app/components/floating-u-i/content.ts
@@ -1,8 +1,9 @@
import { assert } from "@ember/debug";
import { action } from "@ember/object";
-import {
+import type {
OffsetOptions,
- Placement,
+ Placement} from "@floating-ui/dom";
+import {
autoUpdate,
computePosition,
flip,
diff --git a/web/app/components/floating-u-i/index.ts b/web/app/components/floating-u-i/index.ts
index d57e90649..e230f38d7 100644
--- a/web/app/components/floating-u-i/index.ts
+++ b/web/app/components/floating-u-i/index.ts
@@ -1,10 +1,10 @@
import { assert } from "@ember/debug";
import { action } from "@ember/object";
import { guidFor } from "@ember/object/internals";
-import { OffsetOptions, Placement } from "@floating-ui/dom";
+import type { OffsetOptions, Placement } from "@floating-ui/dom";
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
-import { MatchAnchorWidthOptions } from "./content";
+import type { MatchAnchorWidthOptions } from "./content";
interface FloatingUIAnchorAPI {
contentIsShown: boolean;
diff --git a/web/app/components/footer.ts b/web/app/components/footer.ts
index 7c0615d2f..30dcdc7ba 100644
--- a/web/app/components/footer.ts
+++ b/web/app/components/footer.ts
@@ -1,8 +1,8 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
-import RouterService from "@ember/routing/router-service";
+import type RouterService from "@ember/routing/router-service";
import { HERMES_GITHUB_REPO_URL } from "hermes/utils/hermes-urls";
-import ConfigService from "hermes/services/config";
+import type ConfigService from "hermes/services/config";
interface FooterComponentSignature {
Element: HTMLDivElement;
diff --git a/web/app/components/header/active-filter-list-item.ts b/web/app/components/header/active-filter-list-item.ts
index 1723eea74..913c01538 100644
--- a/web/app/components/header/active-filter-list-item.ts
+++ b/web/app/components/header/active-filter-list-item.ts
@@ -1,8 +1,8 @@
-import RouterService from "@ember/routing/router-service";
+import type RouterService from "@ember/routing/router-service";
import { inject as service } from "@ember/service";
import { capitalize } from "@ember/string";
import Component from "@glimmer/component";
-import ActiveFiltersService from "hermes/services/active-filters";
+import type ActiveFiltersService from "hermes/services/active-filters";
import { ProjectStatus } from "hermes/types/project-status";
interface HeaderActiveFilterListItemComponentSignature {
diff --git a/web/app/components/header/active-filter-list.ts b/web/app/components/header/active-filter-list.ts
index 97db8b2e2..a77ece222 100644
--- a/web/app/components/header/active-filter-list.ts
+++ b/web/app/components/header/active-filter-list.ts
@@ -1,7 +1,7 @@
import { inject as service } from "@ember/service";
import Component from "@glimmer/component";
import { SearchScope } from "hermes/routes/authenticated/results";
-import ActiveFiltersService from "hermes/services/active-filters";
+import type ActiveFiltersService from "hermes/services/active-filters";
interface HeaderActiveFilterListComponentSignature {
Args: {};
@@ -26,7 +26,8 @@ export default class HeaderActiveFilterListComponent extends Component