diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 53b33b6..08d4131 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -32,7 +32,6 @@ jobs: run: npm run test - name: Run the integration tests run: npm run test:it - - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: @@ -64,6 +63,13 @@ jobs: - name: Post-Deployment Integration Test run: npm run test:postdeploy + env: + IT_IMS_STAGE_ENDPOINT: ${{ secrets.IT_IMS_STAGE_ENDPOINT }} + IT_IMS_STAGE_CLIENT_ID_LIMITED_USER: ${{ secrets.IT_IMS_STAGE_CLIENT_ID_LIMITED_USER }} + IT_IMS_STAGE_CLIENT_SECRET_LIMITED_USER: ${{ secrets.IT_IMS_STAGE_CLIENT_SECRET_LIMITED_USER }} + IT_IMS_STAGE_CLIENT_ID_SUPER_USER: ${{ secrets.IT_IMS_STAGE_CLIENT_ID_SUPER_USER }} + IT_IMS_STAGE_CLIENT_SECRET_SUPER_USER: ${{ secrets.IT_IMS_STAGE_CLIENT_SECRET_SUPER_USER }} + IT_IMS_STAGE_SCOPES: ${{ secrets.IT_IMS_STAGE_SCOPES }} - name: Semantic Release (Dry Run) run: npm run semantic-release-dry env: diff --git a/.gitignore b/.gitignore index e6453c5..8229992 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ coverage # Wrangler temp .wrangler +wrangler-versioned.toml +.deployment-env # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt diff --git a/deploy-ci-version.sh b/deploy-ci-version.sh new file mode 100755 index 0000000..4e97e21 --- /dev/null +++ b/deploy-ci-version.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +# Copyright 2025 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. + +set -e + +ENVIRONMENT="ci" + +# Prepare version and capture version +echo "Preparing version for environment: $ENVIRONMENT" +VERSION=$(node prepare-deploy.js) + +if [ -z "$VERSION" ]; then + echo "Error: Failed to get version from prepare-deploy.js" + exit 1 +fi + +# Get current git branch name +BRANCH=$(git rev-parse --abbrev-ref HEAD) + +if [ -z "$BRANCH" ]; then + echo "Error: Failed to get git branch name" + exit 1 +fi + +echo "Creating version: $VERSION from branch: $BRANCH" + +# Deploy with branch tag and version message and capture output +# wrangler deploy -e "$ENVIRONMENT" -c wrangler-versioned.toml --message "v$VERSION" --tag "$BRANCH" +OUTPUT=$(wrangler versions upload -e "$ENVIRONMENT" -c wrangler-versioned.toml --message "$ENVIRONMENT: v$VERSION - branch: $BRANCH" --tag "$BRANCH" 2>&1) + +# Display the output +echo "$OUTPUT" + +# Parse the deployment information +WORKER_VERSION_ID=$(echo "$OUTPUT" | grep "Worker Version ID:" | sed 's/.*Worker Version ID: //') +WORKER_PREVIEW_URL=$(echo "$OUTPUT" | grep "Version Preview URL:" | sed 's/.*Version Preview URL: //') + +# Write to a file that can be sourced (for local use) +cat > .deployment-env << EOF +export WORKER_VERSION_ID="$WORKER_VERSION_ID" +export WORKER_PREVIEW_URL="$WORKER_PREVIEW_URL" +export WORKER_PREVIEW_BRANCH="$BRANCH" +EOF + +# If running in GitHub Actions, also write to GITHUB_ENV +if [ -n "$GITHUB_ENV" ]; then + echo "WORKER_VERSION_ID=$WORKER_VERSION_ID" >> "$GITHUB_ENV" + echo "WORKER_PREVIEW_URL=$WORKER_PREVIEW_URL" >> "$GITHUB_ENV" + echo "WORKER_PREVIEW_BRANCH=$BRANCH" >> "$GITHUB_ENV" + echo "Variables exported to GitHub Actions environment" +fi + +# Deploy the version +wrangler versions deploy -y -e ci --version-id "$WORKER_VERSION_ID" + +echo "" +echo "Version deployment complete!" +echo "----------------------------------------" +echo "Deployment information: (copy inside a .deployment-env file to run locally)" +echo "export WORKER_VERSION_ID=$WORKER_VERSION_ID" +echo "export WORKER_PREVIEW_URL=$WORKER_PREVIEW_URL" +echo "export WORKER_PREVIEW_BRANCH=$BRANCH" +echo "----------------------------------------" + diff --git a/integration-tests.md b/integration-tests.md new file mode 100644 index 0000000..3854bd8 --- /dev/null +++ b/integration-tests.md @@ -0,0 +1,135 @@ +# Integration Tests + +The `da-admin` worker includes a suite of integration tests designed as "smoke tests". These tests validate: + +- **Deployment Integrity**: Ensures the worker can be successfully deployed to the Cloudflare Workers runtime. +- **Core Functionality**: Verifies critical features such as authentication, read/write operations, and permission handling function correctly end-to-end. + +## Architecture + +The test entry point is [`./test/it/smoke.test.js`](./test/it/smoke.test.js), which sets up the environment and executes the test suite defined in [`./test/it/it-tests.js`](./test/it/it-tests.js). The tests can run in two modes: + +1. **Local Mode**: Runs entirely on the local machine using mocks and local servers. +2. **Stage Mode**: Runs against a deployed version of the worker on Cloudflare (used in CI). + +### 1. Local Mode + +**Local Mode** is the default for development. It orchestrates a local environment consisting of: +- **`wrangler dev`**: Runs the `da-admin` worker locally. +- **`S3rver`**: A local S3-compatible object storage server to mock R2/S3. +- **Mock IMS Server**: A local HTTP server simulating Adobe IMS for authentication. + +**Configuration:** +- Environment variables are automatically loaded from [`./.dev.vars.it`](./.dev.vars.it). +- No manual configuration is typically required. +- **Note**: In **Local Mode**, the DA configuration is ephemeral and set up before each test run. + +**How to Run:** +```bash +npm run test:it +``` + +### 2. Stage Mode (CI/CD) + +In **Stage Mode**, tests execute against a live worker deployed to Cloudflare. This verifies the actual deployment artifacts and Cloudflare environment behavior. + +The tests create a repository `test-repo-` under the `da-admin-ci-it-org` located in the `aem-content-stage` bucket. The config is predefined for this org and defines permissions for the test users (see below). + +#### CI/CD Pipeline Flow + +The GitHub Actions workflow executes these tests in two phases: + +1. **Deployment**: + - `npm run deploy:ci`: Uploads a new version of the worker (tagged with the branch name) to the `ci` environment. + - Generates a `.deployment-env` file containing the `WORKER_VERSION_ID`, `WORKER_PREVIEW_URL` and `WORKER_PREVIEW_BRANCH`. + +2. **Verification**: + - `npm run test:postdeploy`: Sources the `.deployment-env` file and runs the test suite. + - When `WORKER_PREVIEW_URL` is present in the environment, [`smoke.test.js`](./test/it/smoke.test.js) switches to **Stage Mode**. + - Tests authenticates against a real IMS environment (Stage/Prod) and requests are sent to the deployed worker. + +#### Running Stage Tests Locally + +To debug CI failures or test against a deployed worker from your local machine: + +1. **Deploy the Worker**: + ```bash + npm run deploy:ci + ``` + This script will generate the `.deployment-env` file in your root directory. + +2. **Configure Credentials**: + Create a `.env` file (or set environment variables) with the required IMS credentials for the test accounts: + ```env + IT_IMS_STAGE_ENDPOINT=https://ims-na1.adobelogin.com + IT_IMS_STAGE_CLIENT_ID_SUPER_USER= + IT_IMS_STAGE_CLIENT_SECRET_SUPER_USER= + IT_IMS_STAGE_CLIENT_ID_LIMITED_USER= + IT_IMS_STAGE_CLIENT_SECRET_LIMITED_USER= + IT_IMS_STAGE_SCOPES=openid,AdobeID,aem.frontend.all,read_organizations,additional_info.projectedProductContext + ``` + +3. **Run the Tests**: + ```bash + # Loads the deployment vars and runs the tests + npm run test:postdeploy2 + ``` + +### Persistence & Configuration + +In **Stage Mode**, the tests rely on the `DA_CONFIG_STAGE` KV storage for permissions. This configuration is persistent. + +If the configuration is lost or needs to be reset, the expected permission model is: + +```json +{ + "total": 2, + "limit": 2, + "offset": 0, + "data": [ + { + "path": "CONFIG", + "groups": "", + "actions": "write" + }, + { + "path": "/+**", + "groups": "", + "actions": "write" + } + ], + ":type": "sheet", + ":sheetname": "permissions" +} +``` + +## IMS Configuration + +In **Stage Mode**, tests execute against the **IMS Stage** environment. + +### Prerequisites + +1. **Worker Configuration**: The `IMS_ORIGIN` secret for the `da-admin` worker (CI environment) must point to the IMS Stage endpoint. +2. **User Existence**: Test users must exist and belong to an IMS Stage organization. No specific organization permissions are required beyond basic membership. +3. **DA Configuration**: The test users must be explicitly granted permissions in the `DA_CONFIG` (as shown in the [Persistence & Configuration](#persistence--configuration) section). + +### Test Users Setup + +The integration tests use dedicated service accounts defined in the [Adobe Stage Developer Console](https://developer-stage.adobe.com/) (VPN required) under the `Document Authoring Stage` organization. Two distinct projects were created to simulate different user roles: + +- **Authenticated User Project**: + - **Purpose**: Simulates a user who is logged in but may not have specific permissions (used for negative testing or basic access). + - **Credentials**: Defined in CI secrets as `IT_IMS_STAGE_CLIENT_ID_SUPER_USER` / `IT_IMS_STAGE_CLIENT_SECRET_SUPER_USER`. + - **API**: Uses `Edge Delivery Service` to create OAuth Server-to-Server credentials. + +- **Authorized User Project**: + - **Purpose**: Simulates a user with full read/write permissions. + - **Credentials**: (Currently the tests primarily use one set of credentials which are authorized in the config). + - **API**: Uses `Edge Delivery Service` to create OAuth Server-to-Server credentials. + +> **Notes on Setup:** +> 1. **Multiple Projects**: Two separate projects were created because generating multiple independent credentials within a single project was not supported. +> 2. **Role Distinction**: The distinction between "authenticated" and "authorized" is managed entirely within the `DA_CONFIG` permissions sheet, not in IMS. The project naming reflects the intended use case. +> 3. **API Selection**: The `Edge Delivery Service` API was selected for convenience. Any API service can be used provided it: +> - Supports IMS connection. +> - Includes the `read_organizations` scope. diff --git a/package-lock.json b/package-lock.json index 2c238f6..90c3757 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@adobe/helix-shared-process-queue": "3.1.3", "@aws-sdk/client-s3": "3.726.1", "@aws-sdk/s3-request-presigner": "^3.468.0", - "@cloudflare/workers-types": "4.20251126.0", + "@cloudflare/workers-types": "4.20251205.0", "@ssttevee/cfw-formdata-polyfill": "0.2.1", "jose": "6.0.10" }, @@ -24,6 +24,7 @@ "@semantic-release/git": "10.0.1", "aws-sdk-client-mock": "4.0.0", "c8": "10.0.0", + "dotenv": "17.2.3", "eslint": "9.4.0", "esmock": "2.7.3", "husky": "9.1.7", @@ -34,7 +35,7 @@ "semantic-release-slack-bot": "4.0.2", "sinon": "21.0.0", "tree-kill": "1.2.2", - "wrangler": "4.10.0" + "wrangler": "4.53.0" } }, "node_modules/@actions/core": { @@ -447,6 +448,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.726.0.tgz", "integrity": "sha512-5JzTX9jwev7+y2Jkzjz0pd1wobB5JQfPOQF3N2DrJ5Pao0/k6uRYwE4NqB0p0HlGrMTDm7xNq7OSPPIPG575Jw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -500,6 +502,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.726.1.tgz", "integrity": "sha512-qh9Q9Vu1hrM/wMBOBIaskwnE4GTFaZu26Q6WHwyWNfj7J8a40vBxpW16c2vYXHLBtwRKM1be8uRLkmDwghpiNw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -1570,9 +1573,9 @@ "license": "MIT" }, "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz", - "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.1.tgz", + "integrity": "sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { @@ -1583,14 +1586,14 @@ } }, "node_modules/@cloudflare/unenv-preset": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.3.1.tgz", - "integrity": "sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==", + "version": "2.7.13", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.13.tgz", + "integrity": "sha512-NulO1H8R/DzsJguLC0ndMuk4Ufv0KSlN+E54ay9rn9ZCQo0kpAPwwh3LhgpZ96a3Dr6L9LqW57M4CqC34iLOvw==", "dev": true, "license": "MIT OR Apache-2.0", "peerDependencies": { - "unenv": "2.0.0-rc.15", - "workerd": "^1.20250320.0" + "unenv": "2.0.0-rc.24", + "workerd": "^1.20251202.0" }, "peerDependenciesMeta": { "workerd": { @@ -1599,9 +1602,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20250409.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250409.0.tgz", - "integrity": "sha512-smA9yq77xsdQ1NMLhFz3JZxMHGd01lg0bE+X3dTFmIUs+hHskJ+HJ/IkMFInkCCeEFlUkoL4yO7ilaU/fin/xA==", + "version": "1.20251202.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20251202.0.tgz", + "integrity": "sha512-/uvEAWEukTWb1geHhbjGUeZqcSSSyYzp0mvoPUBl+l0ont4NVGao3fgwM0q8wtKvgoKCHSG6zcG23wj9Opj3Nw==", "cpu": [ "x64" ], @@ -1616,9 +1619,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20250409.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250409.0.tgz", - "integrity": "sha512-oLVcf+Y5Qun8JHcy1VcR/YnbA5U2ne0czh3XNhDqdHZFK8+vKeC7MnVPX+kEqQA3+uLcMM1/FsIDU1U4Na0h1g==", + "version": "1.20251202.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20251202.0.tgz", + "integrity": "sha512-f52xRvcI9cWRd6400EZStRtXiRC5XKEud7K5aFIbbUv0VeINltujFQQ9nHWtsF6g1quIXWkjhh5u01gPAYNNXA==", "cpu": [ "arm64" ], @@ -1633,9 +1636,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20250409.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250409.0.tgz", - "integrity": "sha512-D31B4kdC3a0RD5yfpdIa89//kGHbYsYihZmejm1k4S4NHOho3MUDHAEh4aHtafQNXbZdydGHlSyiVYjTdQ9ILQ==", + "version": "1.20251202.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20251202.0.tgz", + "integrity": "sha512-HYXinF5RBH7oXbsFUMmwKCj+WltpYbf5mRKUBG5v3EuPhUjSIFB84U+58pDyfBJjcynHdy3EtvTWcvh/+lcgow==", "cpu": [ "x64" ], @@ -1650,9 +1653,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20250409.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250409.0.tgz", - "integrity": "sha512-Sr59P0TREayil5OQ7kcbjuIn6L6OTSRLI91LKu0D8vi1hss2q9FUwBcwxg0+Yd/x+ty/x7IISiAK5QBkAMeITQ==", + "version": "1.20251202.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20251202.0.tgz", + "integrity": "sha512-++L02Jdoxz7hEA9qDaQjbVU1RzQS+S+eqIi22DkPe2Tgiq2M3UfNpeu+75k5L9DGRIkZPYvwMBMbcmKvQqdIIg==", "cpu": [ "arm64" ], @@ -1667,9 +1670,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20250409.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250409.0.tgz", - "integrity": "sha512-dK9I8zBX5rR7MtaaP2AhICQTEw3PVzHcsltN8o46w7JsbYlMvFOj27FfYH5dhs3IahgmIfw2e572QXW2o/dbpg==", + "version": "1.20251202.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20251202.0.tgz", + "integrity": "sha512-gzeU6eDydTi7ib+Q9DD/c0hpXtqPucnHk2tfGU03mljPObYxzMkkPGgB5qxpksFvub3y4K0ChjqYxGJB4F+j3g==", "cpu": [ "x64" ], @@ -1684,10 +1687,11 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "4.20251126.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20251126.0.tgz", - "integrity": "sha512-DSeI1Q7JYmh5/D/tw5eZCjrKY34v69rwj63hHt60nSQW5QLwWCbj/lLtNz9f2EPa+JCACwpLXHgCXfzJ29x66w==", - "license": "MIT OR Apache-2.0" + "version": "4.20251205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20251205.0.tgz", + "integrity": "sha512-7pup7fYkuQW5XD8RUS/vkxF9SXlrGyCXuZ4ro3uVQvca/GTeSa+8bZ8T4wbq1Aea5lmLIGSlKbhl2msME7bRBA==", + "license": "MIT OR Apache-2.0", + "peer": true }, "node_modules/@colors/colors": { "version": "1.6.0", @@ -1771,9 +1775,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", "cpu": [ "ppc64" ], @@ -1788,9 +1792,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", "cpu": [ "arm" ], @@ -1805,9 +1809,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", "cpu": [ "arm64" ], @@ -1822,9 +1826,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", "cpu": [ "x64" ], @@ -1839,9 +1843,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", "cpu": [ "arm64" ], @@ -1856,9 +1860,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", "cpu": [ "x64" ], @@ -1873,9 +1877,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", "cpu": [ "arm64" ], @@ -1890,9 +1894,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", "cpu": [ "x64" ], @@ -1907,9 +1911,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", "cpu": [ "arm" ], @@ -1924,9 +1928,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", "cpu": [ "arm64" ], @@ -1941,9 +1945,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", "cpu": [ "ia32" ], @@ -1958,9 +1962,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", "cpu": [ "loong64" ], @@ -1975,9 +1979,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", "cpu": [ "mips64el" ], @@ -1992,9 +1996,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", "cpu": [ "ppc64" ], @@ -2009,9 +2013,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", "cpu": [ "riscv64" ], @@ -2026,9 +2030,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", "cpu": [ "s390x" ], @@ -2043,9 +2047,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", "cpu": [ "x64" ], @@ -2060,9 +2064,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", "cpu": [ "arm64" ], @@ -2077,9 +2081,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", "cpu": [ "x64" ], @@ -2094,9 +2098,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", "cpu": [ "arm64" ], @@ -2111,9 +2115,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", "cpu": [ "x64" ], @@ -2127,10 +2131,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", "cpu": [ "x64" ], @@ -2145,9 +2166,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", "cpu": [ "arm64" ], @@ -2162,9 +2183,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", "cpu": [ "ia32" ], @@ -2179,9 +2200,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", "cpu": [ "x64" ], @@ -3037,6 +3058,7 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -3183,6 +3205,7 @@ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -3442,6 +3465,48 @@ "node": ">=12" } }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/dumper/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -3576,6 +3641,19 @@ "npm": ">=10" } }, + "node_modules/@redocly/cli/node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/@redocly/config": { "version": "0.26.4", "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.26.4.tgz", @@ -4303,6 +4381,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/is": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.1.tgz", + "integrity": "sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", @@ -5107,6 +5198,13 @@ "text-hex": "1.0.x" } }, + "node_modules/@speed-highlight/core": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.12.tgz", + "integrity": "sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/@ssttevee/blob-ponyfill": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@ssttevee/blob-ponyfill/-/blob-ponyfill-0.1.0.tgz", @@ -5286,6 +5384,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5343,6 +5442,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5601,16 +5701,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/as-table": { - "version": "1.0.55", - "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", - "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "printable-characters": "^1.0.42" - } - }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -6837,13 +6927,6 @@ "dev": true, "license": "MIT" }, - "node_modules/data-uri-to-buffer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", - "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", - "dev": true, - "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", @@ -6995,13 +7078,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "dev": true, - "license": "MIT" - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -7046,7 +7122,6 @@ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -7133,9 +7208,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -7440,6 +7515,16 @@ "dev": true, "license": "MIT" }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -7597,9 +7682,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7610,31 +7695,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" } }, "node_modules/escalade": { @@ -7673,6 +7759,7 @@ "integrity": "sha512-sjc7Y8cUD1IlwYcTS9qPSvGjAC8Ne9LctpxKKu3x/1IC9bnOg98Zy6GxEJUfr1NojMgVPlyANXYns8oE2c1TAA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8047,13 +8134,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "dev": true, - "license": "MIT" - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -8601,17 +8681,6 @@ "node": ">= 0.4" } }, - "node_modules/get-source": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", - "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", - "dev": true, - "license": "Unlicense", - "dependencies": { - "data-uri-to-buffer": "^2.0.0", - "source-map": "^0.6.1" - } - }, "node_modules/get-stream": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", @@ -9254,8 +9323,7 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/is-async-function": { "version": "2.1.1", @@ -9939,6 +10007,7 @@ "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 10.16.0" } @@ -10086,6 +10155,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/koa": { "version": "2.16.3", "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.3.tgz", @@ -10806,6 +10885,7 @@ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -11246,9 +11326,9 @@ } }, "node_modules/miniflare": { - "version": "4.20250409.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250409.0.tgz", - "integrity": "sha512-Hu02dYZvFR+MyrI57O6rSrOUTofcO9EIvcodgq2SAHzAeWSJw2E0oq9lylOrcckFwPMcwxUAb/cQN1LIoCyySw==", + "version": "4.20251202.1", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20251202.1.tgz", + "integrity": "sha512-cRp2QNgnt9wpLMoNs4MOzzomyfe9UTS9sPRxIpUvxMl+mweCZ0FHpWWQvCnU7wWlfAP8VGZrHwqSsV5ERA6ahQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11257,11 +11337,12 @@ "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", + "sharp": "^0.33.5", "stoppable": "1.1.0", - "undici": "^5.28.5", - "workerd": "1.20250409.0", + "undici": "7.14.0", + "workerd": "1.20251202.0", "ws": "8.18.0", - "youch": "3.3.4", + "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { @@ -11285,16 +11366,13 @@ } }, "node_modules/miniflare/node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.14.0.tgz", + "integrity": "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==", "dev": true, "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, "engines": { - "node": ">=14.0" + "node": ">=20.18.1" } }, "node_modules/miniflare/node_modules/ws": { @@ -11358,6 +11436,7 @@ "integrity": "sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/mobx" @@ -11579,16 +11658,6 @@ "dev": true, "license": "MIT" }, - "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", - "bin": { - "mustache": "bin/mustache" - } - }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -13835,6 +13904,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14149,13 +14219,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "dev": true, - "license": "MIT" - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -14624,6 +14687,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14854,13 +14918,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/printable-characters": { - "version": "1.0.42", - "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", - "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", - "dev": true, - "license": "Unlicense" - }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -15002,6 +15059,7 @@ "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -15012,6 +15070,7 @@ "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -15632,6 +15691,7 @@ "integrity": "sha512-6qGjWccl5yoyugHt3jTgztJ9Y0JVzyH8/Voc/D8PlLat9pwxQYXz7W1Dpnq5h0/G5GCYGUaDSlYcyk3AMh5A6g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -16099,7 +16159,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", @@ -16139,7 +16198,6 @@ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" @@ -16154,7 +16212,6 @@ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -16444,7 +16501,6 @@ "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "is-arrayish": "^0.3.1" } @@ -16663,17 +16719,6 @@ "node": "*" } }, - "node_modules/stacktracey": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", - "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", - "dev": true, - "license": "Unlicense", - "dependencies": { - "as-table": "^1.0.36", - "get-source": "^2.0.12" - } - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -17005,6 +17050,7 @@ "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@emotion/is-prop-valid": "1.2.2", "@emotion/unitless": "0.8.1", @@ -17670,13 +17716,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "dev": true, - "license": "MIT" - }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -17728,17 +17767,14 @@ "license": "MIT" }, "node_modules/unenv": { - "version": "2.0.0-rc.15", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.15.tgz", - "integrity": "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==", + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "defu": "^6.1.4", - "exsolve": "^1.0.4", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "ufo": "^1.5.4" + "pathe": "^2.0.3" } }, "node_modules/unicode-emoji-modifier-base": { @@ -18191,12 +18227,13 @@ "license": "MIT" }, "node_modules/workerd": { - "version": "1.20250409.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250409.0.tgz", - "integrity": "sha512-hqjX9swiHvrkOI3jlH9lrZsZRRv9lddUwcMe8Ua76jnyQz+brybWznNjHu8U5oswwcrFwvky1A4CcLjcLY31gQ==", + "version": "1.20251202.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20251202.0.tgz", + "integrity": "sha512-p08YfrUMHkjCECNdT36r+6DpJIZX4kixbZ4n6GMUcLR5Gh18fakSCsiQrh72iOm4M9QHv/rM7P8YvCrUPWT5sg==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "bin": { "workerd": "bin/workerd" }, @@ -18204,11 +18241,11 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20250409.0", - "@cloudflare/workerd-darwin-arm64": "1.20250409.0", - "@cloudflare/workerd-linux-64": "1.20250409.0", - "@cloudflare/workerd-linux-arm64": "1.20250409.0", - "@cloudflare/workerd-windows-64": "1.20250409.0" + "@cloudflare/workerd-darwin-64": "1.20251202.0", + "@cloudflare/workerd-darwin-arm64": "1.20251202.0", + "@cloudflare/workerd-linux-64": "1.20251202.0", + "@cloudflare/workerd-linux-arm64": "1.20251202.0", + "@cloudflare/workerd-windows-64": "1.20251202.0" } }, "node_modules/workerpool": { @@ -18219,34 +18256,33 @@ "license": "Apache-2.0" }, "node_modules/wrangler": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.10.0.tgz", - "integrity": "sha512-fTE4hZ79msEUt8+HEjl/8Q72haCyzPLu4PgrU3L81ysmjrMEdiYfUPqnvCkBUVtJvrDNdctTEimkufT1Y0ipNg==", + "version": "4.53.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.53.0.tgz", + "integrity": "sha512-/wvnHlRnlHsqaeIgGbmcEJE5NFYdTUWHCKow+U5Tv2XwQXI9vXUqBwCLAGy/BwqyS5nnycRt2kppqCzgHgyb7Q==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { - "@cloudflare/kv-asset-handler": "0.4.0", - "@cloudflare/unenv-preset": "2.3.1", + "@cloudflare/kv-asset-handler": "0.4.1", + "@cloudflare/unenv-preset": "2.7.13", "blake3-wasm": "2.1.5", - "esbuild": "0.24.2", - "miniflare": "4.20250409.0", + "esbuild": "0.27.0", + "miniflare": "4.20251202.1", "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.15", - "workerd": "1.20250409.0" + "unenv": "2.0.0-rc.24", + "workerd": "1.20251202.0" }, "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "optionalDependencies": { - "fsevents": "~2.3.2", - "sharp": "^0.33.5" + "fsevents": "~2.3.2" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20250409.0" + "@cloudflare/workers-types": "^4.20251202.0" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -18535,15 +18571,42 @@ } }, "node_modules/youch": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", - "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", "dev": true, "license": "MIT", "dependencies": { - "cookie": "^0.7.1", - "mustache": "^4.2.0", - "stacktracey": "^2.1.8" + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/youch/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/zod": { diff --git a/package.json b/package.json index 0bcdcc3..2bfcff5 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,9 @@ "test": "c8 mocha --spec=test/**/*.test.js --ignore=test/it/**/*.test.js", "test:it": "mocha test/it/**/*.test.js", "test:all": "npm run test && npm run test:it", - "test:postdeploy": "echo 'todo'", + "test:postdeploy": "if [ -f .deployment-env ]; then . ./.deployment-env; fi && npm run test:it", "dev": "wrangler dev -e dev", - "deploy:ci": "node prepare-deploy.js && wrangler deploy -e ci -c wrangler-versioned.toml", + "deploy:ci": "./deploy-ci-version.sh ci", "deploy:prod": "node prepare-deploy.js && wrangler deploy -e production -c wrangler-versioned.toml", "deploy:stage": "node prepare-deploy.js && wrangler deploy -e stage -c wrangler-versioned.toml", "start": "wrangler dev", @@ -34,6 +34,7 @@ "@semantic-release/git": "10.0.1", "aws-sdk-client-mock": "4.0.0", "c8": "10.0.0", + "dotenv": "17.2.3", "eslint": "9.4.0", "esmock": "2.7.3", "husky": "9.1.7", @@ -44,7 +45,7 @@ "semantic-release-slack-bot": "4.0.2", "sinon": "21.0.0", "tree-kill": "1.2.2", - "wrangler": "4.10.0" + "wrangler": "4.53.0" }, "lint-staged": { "*.js": "eslint", @@ -54,7 +55,7 @@ "@adobe/helix-shared-process-queue": "3.1.3", "@aws-sdk/client-s3": "3.726.1", "@aws-sdk/s3-request-presigner": "^3.468.0", - "@cloudflare/workers-types": "4.20251126.0", + "@cloudflare/workers-types": "4.20251205.0", "@ssttevee/cfw-formdata-polyfill": "0.2.1", "jose": "6.0.10" } diff --git a/prepare-deploy.js b/prepare-deploy.js index 9b16498..838a643 100644 --- a/prepare-deploy.js +++ b/prepare-deploy.js @@ -23,6 +23,9 @@ try { const { version } = JSON.parse(await readFile(resolve(__dirname, 'package.json'))); toml = toml.replaceAll('@@VERSION@@', version); await writeFile(resolve(__dirname, 'wrangler-versioned.toml'), toml, 'utf-8'); + + // Export version for use in deploy scripts + console.log(version); } catch (e) { console.error(e); process.exitCode = 1; diff --git a/test/it/bucket/aem-content-local/.gitignore b/test/it/bucket/aem-content-local/.gitignore new file mode 100644 index 0000000..2d6e00e --- /dev/null +++ b/test/it/bucket/aem-content-local/.gitignore @@ -0,0 +1,6 @@ +# Ignore all S3rver generated files during integration tests +* +# But keep this directory in git +!.gitkeep +!.gitignore + diff --git a/test/it/bucket/aem-content-local/.gitkeep b/test/it/bucket/aem-content-local/.gitkeep new file mode 100644 index 0000000..72696af --- /dev/null +++ b/test/it/bucket/aem-content-local/.gitkeep @@ -0,0 +1,2 @@ +# This file ensures the empty directory is tracked by git +# S3rver will use this directory to store objects during integration tests \ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_metadata.json b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_metadata.json deleted file mode 100644 index 3a48491..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "content-type": "application/json", - "x-amz-meta-id": "4875c756-42ce-4898-ae86-9b0d88146fbe", - "x-amz-meta-path": "test-repo/test-folder.props", - "x-amz-meta-timestamp": "1764844348129", - "x-amz-meta-users": "[{\"email\":\"anonymous\"}]", - "x-amz-meta-version": "7dd6da34-02a3-4d7f-be02-8170a695199b" -} \ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object deleted file mode 100644 index 9e26dfe..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object.md5 b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object.md5 deleted file mode 100644 index a3f7110..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object.md5 +++ /dev/null @@ -1 +0,0 @@ -99914b932bd37a50b983c5e7c90ae93b \ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_metadata.json b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_metadata.json deleted file mode 100644 index 860d8e5..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_metadata.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "content-type": "text/html" -} \ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_object b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_object deleted file mode 100644 index cafecd1..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_object +++ /dev/null @@ -1 +0,0 @@ -

Page 1

\ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_object.md5 b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_object.md5 deleted file mode 100644 index fbe1eba..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_object.md5 +++ /dev/null @@ -1 +0,0 @@ -faf1fc7148f6811144bc58803c37cb7a \ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_metadata.json b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_metadata.json deleted file mode 100644 index 860d8e5..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_metadata.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "content-type": "text/html" -} \ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_object b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_object deleted file mode 100644 index 79ab2a5..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_object +++ /dev/null @@ -1 +0,0 @@ -

Page 2

\ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_object.md5 b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_object.md5 deleted file mode 100644 index 10a8e8f..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_object.md5 +++ /dev/null @@ -1 +0,0 @@ -552f5670094f56c281f89df8a933514b \ No newline at end of file diff --git a/test/it/it-tests.js b/test/it/it-tests.js new file mode 100644 index 0000000..25170cd --- /dev/null +++ b/test/it/it-tests.js @@ -0,0 +1,518 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import assert from 'node:assert'; + +// eslint-disable-next-line func-names +export default (ctx) => describe('Integration Tests: it tests', function () { + // Enable bail to stop on first failure - tests are interdependent + this.bail(true); + + it('[super user] should set org config', async function shouldSetOrgConfig() { + if (!ctx.local) { + // in stage, the config is already set and we should not overwrite it + // to preserve the setup and be able to access the content + this.skip(); + } + const { + serverUrl, org, superUser, + } = ctx; + const configData = JSON.stringify({ + total: 2, + limit: 2, + offset: 0, + data: [ + { path: 'CONFIG', groups: superUser.email, actions: 'write' }, + { path: '/+**', groups: superUser.email, actions: 'write' }, + ], + ':type': 'sheet', + ':sheetname': 'permissions', + }); + + const formData = new FormData(); + formData.append('config', configData); + + const url = `${serverUrl}/config/${org}`; + const resp = await fetch(url, { + method: 'POST', + body: formData, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + + assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status} - user: ${superUser.email}`); + }); + + it('[super user] should get org config', async () => { + const { + serverUrl, org, superUser, + } = ctx; + const url = `${serverUrl}/config/${org}`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status} - user: ${superUser.email}`); + + const body = await resp.json(); + // check initial config is clean + assert.strictEqual(body.total, 2, `Expected 2, got ${body.total}`); + assert.strictEqual(body.data[0].path, 'CONFIG', `Expected CONFIG, got ${body.data[0].path}`); + assert.strictEqual(body.data[0].groups, superUser.email, `Expected user email, got ${body.data[0].groups}`); + assert.strictEqual(body.data[0].actions, 'write', `Expected write, got ${body.data[0].actions}`); + assert.strictEqual(body.data[1].path, '/+**', `Expected /+**, got ${body.data[1].path}`); + assert.strictEqual(body.data[1].groups, superUser.email, `Expected user email, got ${body.data[1].groups}`); + assert.strictEqual(body.data[1].actions, 'write', `Expected write, got ${body.data[1].actions}`); + assert.strictEqual(body[':type'], 'sheet', `Expected sheet, got ${body[':type']}`); + assert.strictEqual(body[':sheetname'], 'permissions', `Expected permissions, got ${body[':sheetname']}`); + }); + + it('[anonymous] cannot delete root folder', async () => { + const { + serverUrl, org, repo, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}`; + const resp = await fetch(url, { + method: 'DELETE', + }); + assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + }); + + it('[limited user] cannot delete root folder', async () => { + const { + serverUrl, org, repo, limitedUser, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}`; + const resp = await fetch(url, { + method: 'DELETE', + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); + }); + + it('[super user] should delete root folder to cleanup the bucket', async () => { + const { + serverUrl, org, repo, superUser, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}`; + const resp = await fetch(url, { + method: 'DELETE', + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status} - user: ${superUser.email}`); + + // validate bucket is empty + const listResp = await fetch(`${serverUrl}/list/${org}/${repo}`, { + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + assert.strictEqual(listResp.status, 200, `Expected 200 OK, got ${listResp.status} - user: ${superUser.email}`); + const listBody = await listResp.json(); + assert.strictEqual(listBody.length, 0, `Expected 0 items, got ${listBody.length} - user: ${superUser.email}`); + }); + + it('[super user] should create a repo', async () => { + const { + serverUrl, org, repo, superUser, + } = ctx; + + const resp = await fetch(`${serverUrl}/source/${org}/${repo}`, { + method: 'PUT', + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + assert.ok([200, 201].includes(resp.status), `Expected 200 or 201 for marker, got ${resp.status} - user: ${superUser.email}`); + }); + + it('[anonymous] not allowed to read', async () => { + const { + serverUrl, org, repo, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}`; + const resp = await fetch(url, { + method: 'GET', + }); + assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + }); + + it('[limited user] not allowed to read', async () => { + const { + serverUrl, org, repo, limitedUser, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}`; + const resp = await fetch(url, { + method: 'GET', + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); + }); + + it('[anonymous] cannot list repos', async () => { + const { + serverUrl, org, + } = ctx; + const url = `${serverUrl}/list/${org}`; + const resp = await fetch(url); + assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + }); + + it('[limited user] cannot list repos', async () => { + const { + serverUrl, org, limitedUser, + } = ctx; + const url = `${serverUrl}/list/${org}`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); + }); + + it('[super user] should list repos', async () => { + const { + serverUrl, org, repo, superUser, + } = ctx; + const url = `${serverUrl}/list/${org}`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status} - user: ${superUser.email}`); + + const body = await resp.json(); + assert.ok(body.length > 0, `Expected at least 1 repo, got ${body.length} - user: ${superUser.email}`); + // need to find the current repo in the list + const repoItem = body.find((item) => item.name === repo); + assert.ok(repoItem, `Expected ${repo} to be in the list - user: ${superUser.email}`); + }); + + it('[anonymous] cannot create a page', async () => { + const { + serverUrl, org, repo, + } = ctx; + // Now create the actual page + const key = 'test-folder/page1'; + const ext = '.html'; + + // Create FormData with the HTML file + const formData = new FormData(); + const blob = new Blob(['

Page 1

'], { type: 'text/html' }); + const file = new File([blob], 'page1.html', { type: 'text/html' }); + formData.append('data', file); + + const url = `${serverUrl}/source/${org}/${repo}/${key}${ext}`; + const resp = await fetch(url, { + method: 'POST', + body: formData, + }); + + assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + }); + + it('[limited user] cannot create a page', async () => { + const { + serverUrl, org, repo, limitedUser, + } = ctx; + // Now create the actual page + const key = 'test-folder/page1'; + const ext = '.html'; + + // Create FormData with the HTML file + const formData = new FormData(); + const blob = new Blob(['

Page 1

'], { type: 'text/html' }); + const file = new File([blob], 'page1.html', { type: 'text/html' }); + formData.append('data', file); + + const url = `${serverUrl}/source/${org}/${repo}/${key}${ext}`; + const resp = await fetch(url, { + method: 'POST', + body: formData, + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); + }); + + it('[super user] should create pages', async () => { + const { + serverUrl, org, repo, superUser, + } = ctx; + // Now create the actual page + const key = 'test-folder/page1'; + const ext = '.html'; + + // Create FormData with the HTML file + const formData = new FormData(); + const blob = new Blob(['

Page 1

'], { type: 'text/html' }); + const file = new File([blob], 'page1.html', { type: 'text/html' }); + formData.append('data', file); + + const url = `${serverUrl}/source/${org}/${repo}/${key}${ext}`; + let resp = await fetch(url, { + method: 'POST', + body: formData, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + + assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status} - user: ${superUser.email}`); + + let body = await resp.json(); + assert.strictEqual(body.source.editUrl, `https://da.live/edit#/${org}/${repo}/${key}`); + assert.strictEqual(body.source.contentUrl, `https://content.da.live/${org}/${repo}/${key}`); + assert.strictEqual(body.aem.previewUrl, `https://main--${repo}--${org}.aem.page/${key}`); + assert.strictEqual(body.aem.liveUrl, `https://main--${repo}--${org}.aem.live/${key}`); + + // validate page is here (include extension in GET request) + resp = await fetch(`${serverUrl}/source/${org}/${repo}/${key}${ext}`, { + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status} - user: ${superUser.email}`); + + body = await resp.text(); + assert.strictEqual(body, '

Page 1

'); + + // create another page + const key2 = 'test-folder/page2'; + const ext2 = '.html'; + const formData2 = new FormData(); + const htmlBlob2 = new Blob(['

Page 2

'], { type: 'text/html' }); + const htmlFile2 = new File([htmlBlob2], 'page2.html', { type: 'text/html' }); + formData2.append('data', htmlFile2); + resp = await fetch(`${serverUrl}/source/${org}/${repo}/${key2}${ext2}`, { + method: 'POST', + body: formData2, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status} - user: ${superUser.email}`); + }); + + it('[limited user] cannot read page1', async () => { + const { + serverUrl, org, repo, limitedUser, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}/test-folder/page1.html`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); + }); + + it('[limited user] cannot read page2', async () => { + const { + serverUrl, org, repo, limitedUser, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}/test-folder/page2.html`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); + }); + + it('[super user] should update the config to allow limited user to read page2', async () => { + const { + serverUrl, org, repo, superUser, limitedUser, + } = ctx; + // read config + const url = `${serverUrl}/config/${org}`; + let resp = await fetch(url, { + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + const body = await resp.json(); + + // add the new config data + const newConfigData = [ + ...body.data, + { path: `/${repo}/test-folder/page2.html`, groups: limitedUser.email, actions: 'read' }, + ]; + + // post the new config + const formData = new FormData(); + formData.append('config', JSON.stringify({ + total: newConfigData.length, + limit: newConfigData.length, + offset: 0, + data: newConfigData, + ':type': 'sheet', + ':sheetname': 'permissions', + })); + resp = await fetch(url, { + method: 'POST', + body: formData, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 201, `Expected 201 Created, got ${resp.status} - user: ${superUser.email}`); + }); + + it('[limited user] can now read page2', async () => { + const { + serverUrl, org, repo, limitedUser, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}/test-folder/page2.html`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + }); + + it('[limited user] still cannot read page1', async () => { + const { + serverUrl, org, repo, limitedUser, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}/test-folder/page1.html`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); + }); + + it('[super user] should remove added entries to clean up the config', async () => { + const { + serverUrl, org, repo, superUser, + } = ctx; + const url = `${serverUrl}/config/${org}`; + let resp = await fetch(url, { + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + let body = await resp.json(); + const newConfigData = body.data.filter((item) => item.path !== `/${repo}/test-folder/page2.html`); + const formData = new FormData(); + formData.append('config', JSON.stringify({ + total: newConfigData.length, + limit: newConfigData.length, + offset: 0, + data: newConfigData, + ':type': 'sheet', + ':sheetname': 'permissions', + })); + resp = await fetch(url, { + method: 'POST', + body: formData, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 201, `Expected 201 Created, got ${resp.status} - user: ${superUser.email}`); + resp = await fetch(url, { + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + body = await resp.json(); + assert.strictEqual(body.total, 2, `Expected 2, got ${body.total}`); + assert.strictEqual(body.data[0].path, 'CONFIG', `Expected CONFIG, got ${body.data[0].path}`); + assert.strictEqual(body.data[0].groups, superUser.email, `Expected user email, got ${body.data[0].groups}`); + assert.strictEqual(body.data[0].actions, 'write', `Expected write, got ${body.data[0].actions}`); + assert.strictEqual(body.data[1].path, '/+**', `Expected /+**, got ${body.data[1].path}`); + assert.strictEqual(body.data[1].groups, superUser.email, `Expected user email, got ${body.data[1].groups}`); + assert.strictEqual(body.data[1].actions, 'write', `Expected write, got ${body.data[1].actions}`); + assert.strictEqual(body[':type'], 'sheet', `Expected sheet, got ${body[':type']}`); + assert.strictEqual(body[':sheetname'], 'permissions', `Expected permissions, got ${body[':sheetname']}`); + }); + + it('[limited user] cannot read page2 anymore', async () => { + const { + serverUrl, org, repo, limitedUser, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}/test-folder/page2.html`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); + }); + + it('[anonymous] cannot list objects', async () => { + const { + serverUrl, org, repo, + } = ctx; + const url = `${serverUrl}/list/${org}/${repo}`; + const resp = await fetch(url); + assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + }); + + it('[super user] should list objects', async () => { + const { + serverUrl, org, repo, superUser, + } = ctx; + const key = 'test-folder'; + + const url = `${serverUrl}/list/${org}/${repo}/${key}`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status} - user: ${superUser.email}`); + + const body = await resp.json(); + + const fileNames = body.map((item) => item.name); + assert.ok(fileNames.includes('page1'), 'Should list page1'); + assert.ok(fileNames.includes('page2'), 'Should list page2'); + }); + + it('[anonymous] cannot delete an object', async () => { + const { + serverUrl, org, repo, key, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}/${key}`; + const resp = await fetch(url, { + method: 'DELETE', + }); + assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + }); + + it('[super user] should delete an object', async () => { + const { + serverUrl, org, repo, superUser, + } = ctx; + const key = 'test-folder/page2'; + const ext = '.html'; + + const url = `${serverUrl}/source/${org}/${repo}/${key}${ext}`; + let resp = await fetch(url, { + method: 'DELETE', + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status} - user: ${superUser.email}`); + + // validate page is not here + resp = await fetch(`${serverUrl}/source/${org}/${repo}/${key}${ext}`, { + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 404, `Expected 404 Not Found, got ${resp.status} - user: ${superUser.email}`); + }); + + it('[super user] should do a final delete of the root folder', async () => { + const { + serverUrl, org, repo, superUser, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}`; + const resp = await fetch(url, { + method: 'DELETE', + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status} - user: ${superUser.email}`); + }); + + it('[limited user] should logout', async () => { + const { serverUrl, limitedUser } = ctx; + const url = `${serverUrl}/logout`; + const resp = await fetch(url, { + method: 'POST', + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status} - user: ${limitedUser.email}`); + }); + + it('[super user] should logout', async () => { + const { serverUrl, superUser } = ctx; + const url = `${serverUrl}/logout`; + const resp = await fetch(url, { + method: 'POST', + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status} - user: ${superUser.userId}`); + }); +}); diff --git a/test/it/smoke.test.js b/test/it/smoke.test.js index f3f57c6..3dfac19 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -10,60 +10,145 @@ * governing permissions and limitations under the License. */ /* eslint-disable prefer-arrow-callback, func-names */ -import assert from 'node:assert'; -import S3rver from 's3rver'; import { spawn } from 'child_process'; import path from 'path'; +import fs from 'fs'; import kill from 'tree-kill'; +import config from 'dotenv'; import { generateKeyPair, exportJWK, SignJWT } from 'jose'; import { createServer } from 'http'; +import S3rver from 's3rver'; + +import itTests from './it-tests.js'; + +config.config(); const S3_PORT = 4569; const SERVER_PORT = 8788; -const IMS_PORT = 9999; -const SERVER_URL = `http://localhost:${SERVER_PORT}`; + +const LOCAL_SERVER_URL = `http://localhost:${SERVER_PORT}`; + +const IMS_LOCAL_PORT = 9999; +const IMS_LOCAL_KID = 'ims'; + +const IMS_STAGE = { + ENDPOINT: process.env.IT_IMS_STAGE_ENDPOINT, + CLIENT_ID_SUPER_USER: process.env.IT_IMS_STAGE_CLIENT_ID_SUPER_USER, + CLIENT_SECRET_SUPER_USER: process.env.IT_IMS_STAGE_CLIENT_SECRET_SUPER_USER, + CLIENT_ID_LIMITED_USER: process.env.IT_IMS_STAGE_CLIENT_ID_LIMITED_USER, + CLIENT_SECRET_LIMITED_USER: process.env.IT_IMS_STAGE_CLIENT_SECRET_LIMITED_USER, + SCOPES: process.env.IT_IMS_STAGE_SCOPES, +}; + const S3_DIR = './test/it/bucket'; -const ORG = 'test-org'; -const REPO = 'test-repo'; +const IT_ORG = 'da-admin-ci-it-org'; +const IT_DEFAULT_REPO = 'test-repo'; describe('Integration Tests: smoke tests', function () { let s3rver; let devServer; let imsServer; - let accessToken; let publicKeyJwk; - before(async function () { - // Increase timeout for server startup - this.timeout(30000); + const context = { + serverUrl: LOCAL_SERVER_URL, + org: IT_ORG, + repo: IT_DEFAULT_REPO, + accessToken: '', + }; - // Clear wrangler state to start fresh - needed only for local testing - const fs = await import('fs'); + const cleanupWranglerState = () => { const wranglerState = path.join(process.cwd(), '.wrangler/state'); if (fs.existsSync(wranglerState)) { fs.rmSync(wranglerState, { recursive: true }); } + }; + + const getIMSProfile = async (accessToken) => { + const res = await fetch(`${IMS_STAGE.ENDPOINT}/ims/profile/v1`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + if (res.ok) { + const json = await res.json(); + return json; + } + throw new Error(`Failed to fetch IMS profile: ${res.status}`); + }; + + const connectToIMS = async (clientId, clientSecret) => { + const postData = { + grant_type: 'client_credentials', + client_id: clientId, + client_secret: clientSecret, + scope: IMS_STAGE.SCOPES, + }; + + const form = new FormData(); + Object.entries(postData).forEach(([k, v]) => { + form.append(k, v); + }); + + let res; + try { + res = await fetch(`${IMS_STAGE.ENDPOINT}/ims/token/v3`, { + method: 'POST', + body: form, + }); + } catch (e) { + throw new Error(`cannot send request to IMS: ${e.message}`); + } + + if (res.ok) { + const json = await res.json(); + const profile = await getIMSProfile(json.access_token); + return { + accessToken: json.access_token, + email: profile.email, + userId: profile.userId, + }; + } + throw new Error(`error response from IMS with status: ${res.status} and body: ${await res.text()}`); + }; + + /* eslint-disable max-len */ + const connectAsSuperUser = async () => connectToIMS(IMS_STAGE.CLIENT_ID_SUPER_USER, IMS_STAGE.CLIENT_SECRET_SUPER_USER); + + /* eslint-disable max-len */ + const connectAsLimitedUser = async () => connectToIMS(IMS_STAGE.CLIENT_ID_LIMITED_USER, IMS_STAGE.CLIENT_SECRET_LIMITED_USER); + + const localTokenCache = {}; + let IMSPrivateKey; - // Generate JWT token for authentication - const kid = 'test-key-id'; - const { publicKey, privateKey } = await generateKeyPair('RS256'); + const setupIMSLocalKey = async () => { + const { privateKey, publicKey } = await generateKeyPair('RS256'); + IMSPrivateKey = privateKey; publicKeyJwk = await exportJWK(publicKey); publicKeyJwk.use = 'sig'; - publicKeyJwk.kid = kid; + publicKeyJwk.kid = IMS_LOCAL_KID; publicKeyJwk.alg = 'RS256'; + }; - // Create JWT with timestamps in milliseconds (as IMS does) - const now = Date.now(); - accessToken = await new SignJWT({ - user_id: 'test_user', + const getIMSLocalToken = async (userId) => { + const email = `${userId}@example.com`; + const accessToken = await new SignJWT({ type: 'access_token', - created_at: now, // milliseconds since epoch - expires_in: 3600000, // milliseconds (1 hour) + user_id: email, + created_at: String(Date.now() - 1000), + expires_in: '86400000', }) - .setProtectedHeader({ alg: 'RS256', kid }) - .sign(privateKey); - + .setProtectedHeader({ alg: 'RS256', kid: IMS_LOCAL_KID }) + .sign(IMSPrivateKey); + + localTokenCache[accessToken] = { + accessToken, + email, + userId: email, + }; + return localTokenCache[accessToken]; + }; + + const setupIMSServer = async () => { // Start mock IMS server imsServer = createServer((req, res) => { res.setHeader('Content-Type', 'application/json'); @@ -80,10 +165,16 @@ describe('Integration Tests: smoke tests', function () { res.writeHead(200); res.end(JSON.stringify({ keys: [publicKeyJwk] })); } else if (req.url === '/ims/profile/v1') { + const cachedToken = localTokenCache[req.headers.authorization.split(' ').pop()]; + if (!cachedToken) { + res.writeHead(401); + res.end(JSON.stringify({ error: 'Unauthorized' })); + return; + } res.writeHead(200); res.end(JSON.stringify({ - email: 'test@example.com', - userId: 'test_user', + email: cachedToken.email, + userId: cachedToken.userId, })); } else if (req.url === '/ims/organizations/v5') { res.writeHead(200); @@ -96,9 +187,11 @@ describe('Integration Tests: smoke tests', function () { }); await new Promise((resolve) => { - imsServer.listen(IMS_PORT, '127.0.0.1', resolve); + imsServer.listen(IMS_LOCAL_PORT, '127.0.0.1', resolve); }); + }; + const setupS3rver = async () => { s3rver = new S3rver({ port: S3_PORT, address: '127.0.0.1', @@ -106,12 +199,14 @@ describe('Integration Tests: smoke tests', function () { silent: true, }); await s3rver.run(); + }; + const setupDevServer = async () => { devServer = spawn('npx', [ 'wrangler', 'dev', '--port', SERVER_PORT.toString(), '--env', 'it', - '--log-level', 'debug', + // '--log-level', 'debug', ], { stdio: 'pipe', // Capture output for debugging detached: false, // Keep in same process group for easier cleanup @@ -123,7 +218,7 @@ describe('Integration Tests: smoke tests', function () { devServer.stdout.on('data', (data) => { const str = data.toString(); // Always log wrangler output including errors - console.log('[Wrangler]', str.trim()); + // console.log('[Wrangler]', str.trim()); if (str.includes('Ready on http://localhost') && !started) { started = true; resolve(); @@ -136,9 +231,50 @@ describe('Integration Tests: smoke tests', function () { devServer.on('error', reject); }); + }; + + before(async function () { + // Increase timeout for server startup + this.timeout(30000); + + if (process.env.WORKER_PREVIEW_URL) { + if (!IMS_STAGE.ENDPOINT + || !IMS_STAGE.CLIENT_ID_SUPER_USER + || !IMS_STAGE.CLIENT_SECRET_SUPER_USER + || !IMS_STAGE.CLIENT_ID_LIMITED_USER + || !IMS_STAGE.CLIENT_SECRET_LIMITED_USER + || !IMS_STAGE.SCOPES) { + throw new Error('IT_IMS_STAGE_ENDPOINT, IT_IMS_STAGE_CLIENT_ID_SUPER_USER, IT_IMS_STAGE_CLIENT_SECRET_SUPER_USER, IT_IMS_STAGE_CLIENT_ID_LIMITED_USER, IT_IMS_STAGE_CLIENT_SECRET_LIMITED_USER, and IT_IMS_STAGE_SCOPES must be set'); + } + context.local = false; + context.serverUrl = process.env.WORKER_PREVIEW_URL; + const branch = process.env.WORKER_PREVIEW_BRANCH; + if (!branch) { + throw new Error('WORKER_PREVIEW_BRANCH must be set'); + } + context.repo += `-${branch.toLowerCase().replace(/[ /_]/g, '-')}`; + context.superUser = await connectAsSuperUser(); + context.limitedUser = await connectAsLimitedUser(); + } else { + context.local = true; + await setupIMSLocalKey(); + context.superUser = await getIMSLocalToken('super-user-id'); + context.limitedUser = await getIMSLocalToken('limited-user-id'); + + cleanupWranglerState(); + await setupIMSServer(); + await setupS3rver(); + await setupDevServer(); + } + + console.log('Running tests with context:', context); }); after(async function () { + if (process.env.WORKER_PREVIEW_URL) { + return; + } + this.timeout(10000); // Cleanup - forcefully kill processes if (devServer && devServer.pid) { @@ -172,149 +308,5 @@ describe('Integration Tests: smoke tests', function () { } }); - it('should get a object via HTTP request', async () => { - const pathname = 'test-folder/page1.html'; - - const url = `${SERVER_URL}/source/${ORG}/${REPO}/${pathname}`; - const resp = await fetch(url, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - - if (resp.status !== 200) { - const errorText = await resp.text(); - console.error('Error response:', errorText); - } - - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); - - const body = await resp.text(); - assert.strictEqual(body, '

Page 1

'); - }); - - it('should list objects via HTTP request', async () => { - const key = 'test-folder'; - - const url = `${SERVER_URL}/list/${ORG}/${REPO}/${key}`; - const resp = await fetch(url, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); - - const body = await resp.json(); - - const fileNames = body.map((item) => item.name); - assert.ok(fileNames.includes('page1'), 'Should list page1'); - assert.ok(fileNames.includes('page2'), 'Should list page2'); - }); - - it('should post an object via HTTP request', async () => { - const key = 'test-folder/page3'; - const ext = '.html'; - - // Create FormData with the HTML file - const formData = new FormData(); - const htmlBlob = new Blob(['

Page 3

'], { type: 'text/html' }); - const htmlFile = new File([htmlBlob], 'page3.html', { type: 'text/html' }); - formData.append('data', htmlFile); - - const url = `${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`; - let resp = await fetch(url, { - method: 'POST', - headers: { Authorization: `Bearer ${accessToken}` }, - body: formData, - }); - - assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); - - let body = await resp.json(); - assert.strictEqual(body.source.editUrl, `https://da.live/edit#/${ORG}/${REPO}/${key}`); - assert.strictEqual(body.source.contentUrl, `https://content.da.live/${ORG}/${REPO}/${key}`); - assert.strictEqual(body.aem.previewUrl, `https://main--${REPO}--${ORG}.aem.page/${key}`); - assert.strictEqual(body.aem.liveUrl, `https://main--${REPO}--${ORG}.aem.live/${key}`); - - // validate page is here (include extension in GET request) - resp = await fetch(`${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); - - body = await resp.text(); - assert.strictEqual(body, '

Page 3

'); - }); - - it('should logout via HTTP request', async () => { - const url = `${SERVER_URL}/logout`; - const resp = await fetch(url, { - method: 'POST', - headers: { Authorization: `Bearer ${accessToken}` }, - }); - - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); - }); - - it('should list repos via HTTP request', async () => { - const url = `${SERVER_URL}/list/${ORG}`; - const resp = await fetch(url, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); - - const body = await resp.json(); - assert.strictEqual(body.length, 1, `Expected 1 repo, got ${body.length}`); - assert.strictEqual(body[0].name, REPO, `Expected ${REPO}, got ${body[0].name}`); - }); - - it('should deal with no config found via HTTP request', async () => { - const url = `${SERVER_URL}/config/${ORG}`; - const resp = await fetch(url, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - - assert.strictEqual(resp.status, 404, `Expected 404, got ${resp.status}`); - }); - - it('should post and get org config via HTTP request', async () => { - // First POST the config - must include CONFIG write permission - const configData = JSON.stringify({ - total: 2, - limit: 2, - offset: 0, - data: [ - { path: 'CONFIG', actions: 'write', groups: 'test@example.com' }, - { key: 'admin.role.all', value: 'test-value' }, - ], - ':type': 'sheet', - ':sheetname': 'permissions', - }); - - const formData = new FormData(); - formData.append('config', configData); - - let url = `${SERVER_URL}/config/${ORG}`; - let resp = await fetch(url, { - method: 'POST', - headers: { Authorization: `Bearer ${accessToken}` }, - body: formData, - }); - - assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); - - // Now GET the config - url = `${SERVER_URL}/config/${ORG}`; - resp = await fetch(url, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); - - const body = await resp.json(); - assert.strictEqual(body.total, 2, `Expected 2, got ${body.total}`); - assert.strictEqual(body.data[0].path, 'CONFIG', `Expected CONFIG, got ${body.data[0].path}`); - assert.strictEqual(body.data[0].actions, 'write', `Expected write, got ${body.data[0].actions}`); - assert.strictEqual(body.data[1].key, 'admin.role.all', `Expected admin.role.all, got ${body.data[1].key}`); - assert.strictEqual(body.data[1].value, 'test-value', `Expected test-value, got ${body.data[1].value}`); - }); + itTests(context); });