From 20ffb2b8722cbaaf6dca05a29ed648a08751af07 Mon Sep 17 00:00:00 2001 From: David Martin Garcia <11503528+dmartingarcia@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:34:31 +0200 Subject: [PATCH 01/13] feat: add public/internal OpenAPI spec filter tooling Adds scripts/filter-internal.js to split openapi.yaml into a public spec (x-internal properties removed entirely) and a full spec (markers stripped, fields kept) for handing to integration partners that need them. --- .gitignore | 2 + package-lock.json | 45 ++++++++ package.json | 15 +++ scripts/filter-internal.js | 107 ++++++++++++++++++ scripts/filter-internal.test.js | 195 ++++++++++++++++++++++++++++++++ 5 files changed, 364 insertions(+) create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/filter-internal.js create mode 100644 scripts/filter-internal.test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ebdb746 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist-internal/ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8aaaeea --- /dev/null +++ b/package-lock.json @@ -0,0 +1,45 @@ +{ + "name": "api-v1-docs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "api-v1-docs", + "version": "1.0.0", + "devDependencies": { + "js-yaml": "^4.1.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0acaeb7 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "api-v1-docs", + "private": true, + "version": "1.0.0", + "description": "Build tooling for the Quipu API v1 public documentation (Scalar)", + "type": "module", + "scripts": { + "test": "node --test scripts/", + "build": "node scripts/filter-internal.js", + "lint:spec": "node -e \"require('js-yaml').load(require('fs').readFileSync('openapi.yaml', 'utf8')); console.log('openapi.yaml: valid YAML')\"" + }, + "devDependencies": { + "js-yaml": "^4.1.0" + } +} diff --git a/scripts/filter-internal.js b/scripts/filter-internal.js new file mode 100644 index 0000000..d87d354 --- /dev/null +++ b/scripts/filter-internal.js @@ -0,0 +1,107 @@ +// Splits openapi.yaml (the source of truth, which may contain fields tagged +// `x-internal: true`) into two artifacts: +// +// - a PUBLIC spec with every `x-internal` property removed entirely +// (published to GitHub Pages) +// - a FULL spec with every field intact, only the `x-internal` marker +// stripped (uploaded as a private CI artifact, e.g. for Aplifisa) +// +// See ../README.md for the full pipeline and the reasoning behind it. + +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import yaml from 'js-yaml' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const ROOT = path.resolve(__dirname, '..') + +// Recursively removes any `properties` entry whose schema is tagged +// `x-internal: true`, and drops its name from a sibling `required` array +// if present. Mutates and returns `node`. +export function removeInternalProperties(node) { + if (Array.isArray(node)) { + node.forEach(removeInternalProperties) + return node + } + + if (node === null || typeof node !== 'object') { + return node + } + + if (node.properties && typeof node.properties === 'object') { + const removedKeys = [] + + for (const [key, schema] of Object.entries(node.properties)) { + if (schema && typeof schema === 'object' && schema['x-internal'] === true) { + delete node.properties[key] + removedKeys.push(key) + } + } + + if (Array.isArray(node.required) && removedKeys.length > 0) { + node.required = node.required.filter((key) => !removedKeys.includes(key)) + } + } + + for (const value of Object.values(node)) { + removeInternalProperties(value) + } + + return node +} + +// Recursively deletes the `x-internal` marker key itself, leaving the +// field it was attached to untouched. Mutates and returns `node`. +export function stripInternalMarkers(node) { + if (Array.isArray(node)) { + node.forEach(stripInternalMarkers) + return node + } + + if (node === null || typeof node !== 'object') { + return node + } + + delete node['x-internal'] + + for (const value of Object.values(node)) { + stripInternalMarkers(value) + } + + return node +} + +export function buildSpecs(sourceYaml) { + const source = yaml.load(sourceYaml) + + const full = stripInternalMarkers(structuredClone(source)) + const filtered = removeInternalProperties(structuredClone(source)) + stripInternalMarkers(filtered) // belt-and-braces: no stray markers should survive on kept fields + + return { full, filtered } +} + +function main() { + const sourcePath = path.join(ROOT, 'openapi.yaml') + const sourceYaml = fs.readFileSync(sourcePath, 'utf8') + + const { full, filtered } = buildSpecs(sourceYaml) + + // Public artifact: overwrites openapi.yaml in place. This only ever runs in the + // CI checkout (ephemeral) right before the Pages upload — the committed source + // file with the x-internal tags is never touched. + fs.writeFileSync(sourcePath, yaml.dump(filtered, { lineWidth: -1 })) + + // Full artifact: JSON, for the private "openapi-full" CI artifact. + const fullOutDir = path.join(ROOT, 'dist-internal') + fs.mkdirSync(fullOutDir, { recursive: true }) + fs.writeFileSync(path.join(fullOutDir, 'openapi.json'), JSON.stringify(full, null, 2)) + + console.log(`Wrote filtered ${sourcePath}`) + console.log(`Wrote ${path.join(fullOutDir, 'openapi.json')}`) +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main() +} diff --git a/scripts/filter-internal.test.js b/scripts/filter-internal.test.js new file mode 100644 index 0000000..0a771dc --- /dev/null +++ b/scripts/filter-internal.test.js @@ -0,0 +1,195 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import yaml from 'js-yaml' +import { removeInternalProperties, stripInternalMarkers, buildSpecs } from './filter-internal.js' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +test('removeInternalProperties drops only x-internal properties', () => { + const doc = { + type: 'object', + properties: { + public_field: { type: 'string' }, + secret_field: { type: 'string', 'x-internal': true } + } + } + + removeInternalProperties(doc) + + assert.deepEqual(Object.keys(doc.properties), ['public_field']) +}) + +test('removeInternalProperties cleans up the required array', () => { + const doc = { + type: 'object', + required: ['public_field', 'secret_field'], + properties: { + public_field: { type: 'string' }, + secret_field: { type: 'string', 'x-internal': true } + } + } + + removeInternalProperties(doc) + + assert.deepEqual(doc.required, ['public_field']) +}) + +test('removeInternalProperties recurses into nested schemas (allOf, items, additionalProperties)', () => { + const doc = { + components: { + schemas: { + Widget: { + allOf: [ + { + type: 'object', + properties: { + internal_nested: { type: 'string', 'x-internal': true }, + kept_nested: { type: 'string' } + } + } + ] + }, + WidgetList: { + type: 'array', + items: { + type: 'object', + properties: { + internal_in_item: { type: 'string', 'x-internal': true } + } + } + }, + WidgetMap: { + type: 'object', + additionalProperties: { + type: 'object', + properties: { + internal_in_map: { type: 'string', 'x-internal': true }, + kept_in_map: { type: 'number' } + } + } + } + } + } + } + + removeInternalProperties(doc) + + const { schemas } = doc.components + assert.deepEqual(Object.keys(schemas.Widget.allOf[0].properties), ['kept_nested']) + assert.deepEqual(Object.keys(schemas.WidgetList.items.properties), []) + assert.deepEqual(Object.keys(schemas.WidgetMap.additionalProperties.properties), ['kept_in_map']) +}) + +test('removeInternalProperties leaves a doc with no internal fields unchanged', () => { + const doc = { + properties: { + a: { type: 'string' }, + b: { type: 'number' } + } + } + const before = structuredClone(doc) + + removeInternalProperties(doc) + + assert.deepEqual(doc, before) +}) + +test('stripInternalMarkers removes the marker but keeps the field', () => { + const doc = { + properties: { + secret_field: { type: 'string', 'x-internal': true, description: 'shh' } + } + } + + stripInternalMarkers(doc) + + assert.deepEqual(doc.properties.secret_field, { type: 'string', description: 'shh' }) +}) + +test('buildSpecs: filtered spec has no x-internal fields and no "x-internal" strings left anywhere', () => { + const source = yaml.dump({ + components: { + schemas: { + Thing: { + properties: { + open: { type: 'string', description: 'visible to everyone' }, + hidden: { type: 'string', 'x-internal': true, description: 'Vendor integration only.' } + } + } + } + } + }) + + const { filtered, full } = buildSpecs(source) + + const filteredProps = filtered.components.schemas.Thing.properties + assert.deepEqual(Object.keys(filteredProps), ['open']) + assert.equal(JSON.stringify(filtered).includes('x-internal'), false) + + // full keeps both fields, marker stripped + const fullProps = full.components.schemas.Thing.properties + assert.deepEqual(Object.keys(fullProps).sort(), ['hidden', 'open']) + assert.equal(JSON.stringify(full).includes('x-internal'), false) +}) + +// Finds every (schemaName, propertyName) pair tagged x-internal directly under +// components.schemas.*.properties in the ORIGINAL doc (field names can repeat +// across unrelated schemas, e.g. "document_type" exists on both ContactAttributes +// (public, unrelated) and BookEntryAttributes (internal) — so checks must be +// scoped per schema, never a whole-document substring search). +function findTaggedFields(sourceDoc) { + const pairs = [] + for (const [schemaName, schema] of Object.entries(sourceDoc.components.schemas)) { + if (!schema || typeof schema !== 'object' || !schema.properties) continue + for (const [propName, propSchema] of Object.entries(schema.properties)) { + if (propSchema && propSchema['x-internal'] === true) { + pairs.push({ schemaName, propName }) + } + } + } + return pairs +} + +test('the real openapi.yaml: every x-internal field disappears from the filtered spec, survives in the full spec', () => { + const sourcePath = path.join(__dirname, '..', 'openapi.yaml') + const sourceYaml = fs.readFileSync(sourcePath, 'utf8') + const sourceDoc = yaml.load(sourceYaml) + + const tagged = findTaggedFields(sourceDoc) + assert.ok(tagged.length > 0, 'expected at least one x-internal field in openapi.yaml') + + const { filtered, full } = buildSpecs(sourceYaml) + + for (const { schemaName, propName } of tagged) { + assert.equal( + Object.prototype.hasOwnProperty.call(filtered.components.schemas[schemaName].properties, propName), + false, + `${schemaName}.${propName} leaked into the public spec` + ) + assert.ok( + Object.prototype.hasOwnProperty.call(full.components.schemas[schemaName].properties, propName), + `${schemaName}.${propName} missing from the full spec` + ) + } + + // No non-internal field should have been caught in the crossfire. + assert.equal(Object.keys(filtered.components.schemas.ItemAttributes.properties).includes('concept'), true) + assert.equal(Object.keys(filtered.components.schemas.ContactAttributes.properties).includes('account_code'), true) + assert.equal(Object.keys(filtered.components.schemas.ContactAttributes.properties).includes('document_type'), true) + assert.equal( + Object.keys(filtered.components.schemas.AccountingCategoryAttributes.properties).includes('accounting_digits_number'), + true + ) +}) + +test('the real openapi.yaml: no "Aplifisa" mention survives in the public spec', () => { + const sourcePath = path.join(__dirname, '..', 'openapi.yaml') + const sourceYaml = fs.readFileSync(sourcePath, 'utf8') + + const { filtered } = buildSpecs(sourceYaml) + + assert.equal(JSON.stringify(filtered).toLowerCase().includes('aplifisa'), false) +}) From a463ae4f508b606f333546253d197dcf8d80279e Mon Sep 17 00:00:00 2001 From: David Martin Garcia <11503528+dmartingarcia@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:34:37 +0200 Subject: [PATCH 02/13] docs: document Aplifisa integration fields in openapi.yaml Adds the API v1 fields that support the Aplifisa accounting integration (chart-of-accounts codes, AEAT/Verifactu/SII tax classification, retention data). Fields only ever returned when the owner has the integration enabled are tagged x-internal: true so they're stripped from the published public spec; a few fields that are always present regardless of integration status are documented normally, without naming the partner. --- openapi.yaml | 202 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/openapi.yaml b/openapi.yaml index ac23647..39d8abd 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1868,6 +1868,70 @@ components: deductible_expense_percent: type: number description: Deductible expense percentage + counterpart_account_code: + x-internal: true + type: string + nullable: true + description: > + Accounting ledger code for this line item's category/subcategory (Aplifisa integration only — + present only when the owner has Aplifisa activated). + isp: + x-internal: true + type: boolean + description: > + Whether reverse charge (Inversión del Sujeto Pasivo) applies to this line item — always `false` + for outbound documents. (Aplifisa integration only.) + retention_key: + x-internal: true + type: string + nullable: true + description: > + IRPF retention key code configured in the owner's preferences for this item's retention percentage + (e.g. "01" for professional activities). Null when the item has no retention. + (Aplifisa integration only.) + model_190_key: + x-internal: true + type: string + nullable: true + description: > + Modelo 190 key for this item's retention, looked up by year and retention percentage. Null when the + item has no retention. (Aplifisa integration only.) + model_190_subkey: + x-internal: true + type: string + nullable: true + description: > + Modelo 190 subkey paired with `model_190_key`. Null when the item has no retention. + (Aplifisa integration only.) + tax_form: + x-internal: true + type: string + nullable: true + enum: ["00", "01", "02", "03", "05", "09", "11"] + description: > + IRPF/VAT reporting form code for this line item: `00` (non-EU import), `01` (standard), + `02` (EU goods), `03` (transport), `05` (other services), `09` (1% retention professional), + `11` (EU services). Null for tickets and disbursement items. (Aplifisa integration only.) + equalization_tax_rate: + x-internal: true + type: number + nullable: true + description: > + Equivalence surcharge (recargo de equivalencia) rate matching this item's VAT rate + (e.g. 5.2 for 21% VAT, 1.4 for 10% VAT). Null when the item has no surcharge. + (Aplifisa integration only.) + equalization_tax_amount: + x-internal: true + type: string + description: > + Equivalence surcharge amount for this line item. "0.0" when none applies. + (Aplifisa integration only.) + retention_rate: + x-internal: true + type: number + description: > + Retention percentage for this line item (e.g. 21, 15, 7). 0 when no retention applies. + (Aplifisa integration only.) ItemWritableAttributes: type: object @@ -1979,6 +2043,12 @@ components: Computed per-request from the caller's abilities (not a persistent flag); typically `true` when the contact has no associated book entries and the caller can destroy it. + account_code: + type: string + nullable: true + description: > + Accounting ledger code assigned to this contact (client or supplier account, + depending on context). ContactResource: type: object @@ -2108,6 +2178,131 @@ components: recipient_country_code: type: string nullable: true + accounting_account_code: + type: string + nullable: true + description: > + Full accounting ledger code for this document's main accounting category/subcategory. + economic_activity_id: + x-internal: true + type: integer + nullable: true + description: > + ID of the economic activity (epígrafe) associated with this document. + (Aplifisa integration only.) + document_type: + x-internal: true + type: string + nullable: true + description: > + Verifactu document type code: `F1` (standard invoice/document), `F2` (simplified invoice), + `R1`–`R5` (credit notes). (Aplifisa integration only.) + contact_account_code: + x-internal: true + type: string + nullable: true + description: > + Accounting ledger code for the counterpart contact on this document (client account for + outbound documents, supplier account for inbound). Null when the document has no contact. + (Aplifisa integration only.) + account_breakdown: + x-internal: true + type: object + nullable: true + description: > + Document total broken down by accounting account code, mirroring the account selection used + for A3/Sage export (asset reclassification, non-deductible expense routing, disbursement + accounts included). Always `null` for paysheets — breakdowns are only computed for invoices, + tickets, simplified invoices and additional incomes. (Aplifisa integration only.) + properties: + base: + type: object + description: Map of accounting account code (string) to the amount booked to it (number). + additionalProperties: + type: number + vat_breakdown: + x-internal: true + type: object + nullable: true + description: > + Document's VAT groups keyed by VAT rate (e.g. "21", "10", "0"), each carrying its Verifactu + fiscal classification. Always `null` for paysheets — breakdowns are only computed for + invoices, tickets, simplified invoices and additional incomes. (Aplifisa integration only.) + additionalProperties: + type: object + properties: + tax: + type: number + description: Total VAT amount for this rate group. + base: + type: number + description: Total taxable base for this rate group. + operation_classification: + type: string + nullable: true + enum: [S1, S2, N1, N2] + description: > + CalificaciónOperación code. Null when the group is exempt (see `exempt_operation_type`) + or has no current-kind items. + exempt_operation_type: + type: string + nullable: true + enum: [E1, E2, E3, E5, E6] + description: > + OperaciónExenta code. Null when `operation_classification` is present, or the group has + no current-kind items. + regime_key: + type: string + nullable: true + description: > + ClaveRégimen code ("01"–"20", e.g. "18" for equivalence surcharge, "17" for OSS). + Empty string when it could not be determined; null when the group has no current-kind + items. + equalization_rate: + type: number + nullable: true + description: Equivalence surcharge rate for this VAT group. Null when no item in the group has a surcharge. + equalization_amount: + type: number + description: Total equivalence surcharge amount for this VAT group. 0 when none. + rectification_type: + x-internal: true + type: string + nullable: true + enum: [S, I] + description: > + For credit notes: `S` (full substitution — same absolute total as the countered document) or + `I` (by differences). Null for non-credit-note documents. (Aplifisa integration only.) + rectification_kind: + x-internal: true + type: string + nullable: true + enum: [R1, R2, R3, R4, R5] + description: > + For credit notes: `R1`–`R5`, matching `document_type`. Null for non-credit-note documents. + (Aplifisa integration only.) + operation_description: + x-internal: true + type: string + description: > + Human-readable summary of the document's current-kind line items, formatted as + "account - concept - amount€" per item and joined with " | ", truncated to 500 characters. + Falls back to "Operación" when there are no current-kind items. (Aplifisa integration only.) + retention_rates: + x-internal: true + type: array + description: > + Retention breakdown across this document's line items, grouped by retention rate. + (Aplifisa integration only.) + items: + type: object + properties: + rate: + type: number + description: Retention percentage (e.g. 15.0). + amount: + type: number + description: Total amount withheld at this rate across the document's items. BookEntryResource: type: object @@ -2546,6 +2741,8 @@ components: type: string company_ss_amount: type: string + retention_amount: + type: string PaysheetResource: type: object @@ -2709,6 +2906,11 @@ components: type: string kind: type: string + accounting_digits_number: + type: integer + nullable: true + description: > + Number of digits used in this owner's chart-of-accounts codes (e.g. 8). AccountingCategoryResource: type: object From dec5679a9fc92b1eaf5bf24b56ba320d43ffaed5 Mon Sep 17 00:00:00 2001 From: David Martin Garcia <11503528+dmartingarcia@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:34:44 +0200 Subject: [PATCH 03/13] ci: build filtered spec before publishing, upload full spec as private artifact CI now runs npm test + scripts/filter-internal.js before the Pages upload, so the public site only ever gets the filtered openapi.yaml. The full spec (x-internal fields included, markers stripped) is uploaded separately as a private, non-published GitHub Actions artifact for handing to Aplifisa. --- .github/workflows/deploy.yml | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b1bc003..ec0b5ff 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,6 +3,7 @@ name: Deploy to GitHub Pages on: push: branches: [main] + pull_request: workflow_dispatch: permissions: @@ -15,7 +16,26 @@ concurrency: cancel-in-progress: false jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - name: Install dependencies + run: npm ci + - name: Validate openapi.yaml is valid YAML + run: npm run lint:spec + - name: Run tests + run: npm test + deploy: + needs: test + if: github.event_name == 'push' && github.ref == 'refs/heads/main' environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} @@ -23,6 +43,30 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - name: Install dependencies + run: npm ci + - name: Build public and full specs + run: npm run build + # dist-internal/openapi.json has every field, x-internal included. Uploaded as a + # private Actions artifact only — never published to Pages. Requires repo access + # to download (this repo is private); hand it to Aplifisa manually. + - name: Upload full spec (private, for Aplifisa) + uses: actions/upload-artifact@v4 + with: + name: openapi-full + path: dist-internal/openapi.json + retention-days: 90 + # Everything below here is what actually goes public — remove build tooling and + # the full spec from disk first so the Pages artifact can only ever contain the + # filtered openapi.yaml, regardless of what upload-pages-artifact does or doesn't + # exclude on its own. + - name: Strip build tooling and internal spec before publishing + run: rm -rf node_modules dist-internal - name: Setup Pages uses: actions/configure-pages@v5 - name: Upload artifact From 787400240c2ce3c081abb489e2f0c320ace24a8e Mon Sep 17 00:00:00 2001 From: David Martin Garcia <11503528+dmartingarcia@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:34:50 +0200 Subject: [PATCH 04/13] docs: explain x-internal fields and the build process in README Documents the rule of thumb for tagging Aplifisa-gated fields x-internal, why partner names must never leak into public field descriptions, and how the CI build/filter/publish pipeline works end to end. --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/README.md b/README.md index 7a6d6a3..7fd91b9 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,59 @@ npx serve . Edit `openapi.yaml` and push to `main`. The docs will be deployed automatically. +## Internal-only fields (`x-internal`) + +Some API v1 fields only exist for a specific integration partner (e.g. Aplifisa) and are gated +server-side in the Rails app — they only appear in a response when the owner has that integration +activated (`owner.aplifisa?` in the serializers, passed down from the API v1 controllers as +`with_aplifisa_data`). This repo is public documentation, so those fields must never ship in the +published spec, and their descriptions must never name the partner integration by name (a public +field's description saying "Aplifisa" would leak the association even if the field itself stays +public). + +**Rule of thumb:** if a field is only serialized `if: -> { instance_options[:with_aplifisa_data] }` +(or the equivalent gate) in `app/serializers/api/v1/**` in the main app, tag it here: + +```yaml +some_field: + x-internal: true + type: string + description: > + Whatever it does. (Aplifisa integration only.) +``` + +Fields that are *always* returned regardless of any integration flag (even if they were added +alongside integration work) are NOT `x-internal` — document them normally, without naming any +partner. + +### How it's built + +`openapi.yaml` (committed, with `x-internal` tags visible) is the single source of truth. CI +(`.github/workflows/deploy.yml`) runs `scripts/filter-internal.js` before publishing, which produces +two artifacts from it: + +- **Public spec** — every `x-internal`-tagged property removed entirely (not hidden, not present). + This overwrites `openapi.yaml` in the ephemeral CI checkout right before the GitHub Pages upload — + the committed source file is never touched. Formatting of this generated file will differ from the + committed source (it's a machine-dumped copy); don't diff the two for style, only for content. +- **Full spec** (`dist-internal/openapi.json`) — every field present, `x-internal` markers stripped. + Uploaded as a private GitHub Actions artifact (`openapi-full`, 90-day retention) — never published + to Pages. This repo is private, so downloading it requires repo access. Whoever manages the Aplifisa + integration downloads it from the workflow run and sends it to Aplifisa directly. + +Both a CI job (`test`, runs on every push and PR) and local `npm test` run +`scripts/filter-internal.test.js`, which includes a check against the real `openapi.yaml` — so a +field with a bad tag, a missing tag, or a stray partner-name leak into a public description fails CI +before it can reach `main`. + +```bash +npm install +npm test # unit tests for the filter script, incl. a check against the real spec +npm run build # writes the filtered openapi.yaml + dist-internal/openapi.json (do this in CI, not locally — + # it overwrites your local openapi.yaml with the machine-formatted public version) +npm run lint:spec # quick YAML-validity check +``` + ## Future improvements - Auto-generate `openapi.yaml` from request specs using `rspec-openapi` From c13a8ac3efbdb7b0823af4064d3f985d0610698e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Gonz=C3=A1lez=20Rivera?= Date: Tue, 21 Jul 2026 12:46:02 +0200 Subject: [PATCH 05/13] feat: detailed description of values --- openapi.yaml | 53 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index 39d8abd..2676869 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2194,9 +2194,16 @@ components: x-internal: true type: string nullable: true - description: > - Verifactu document type code: `F1` (standard invoice/document), `F2` (simplified invoice), - `R1`–`R5` (credit notes). (Aplifisa integration only.) + description: | + Verifactu document type code. + + - `F1` — Invoices and simplified invoices with name, country, type of tax id type and tax id number. + - `F2` — Invoices and simplified invoices without NIF in the recipient (client) and missing the tax id type. Also, Additional Income entries always, regardless of whether the contact has a complete NIF or not. If the Additional Income is itself a credit note, the credit-note criteria below take precedence. + - `R1` — Credit notes due to an error founded in law, misapplied VAT or other errors. Also the default for expense (purchase invoice) credit notes when no motive is selected. + - `R2` — Credit notes due to a bad debt (customer does not pay). Customer does not pay after 6 months/year, company recovers VAT. + - `R3` — Credit note due to customer's bankruptcy or insolvency. + - `R4` — Rest of credit notes. + - `R5` — Simplified invoices credit notes. contact_account_code: x-internal: true type: string @@ -2241,23 +2248,43 @@ components: type: string nullable: true enum: [S1, S2, N1, N2] - description: > - CalificaciónOperación code. Null when the group is exempt (see `exempt_operation_type`) - or has no current-kind items. + description: | + Classification code + + - `S1` — Subject and not exempt — no reverse charge (inversión del sujeto pasivo). + - `S2` — Subject and not exempt — with reverse charge. + - `N1` — Not subject — Articles 7, 14, or other. + - `N2` — Not subject — localization rules. + - `null` — Exempt — see `exempt_operation_type` for the specific exemption. exempt_operation_type: type: string nullable: true enum: [E1, E2, E3, E5, E6] - description: > - OperaciónExenta code. Null when `operation_classification` is present, or the group has - no current-kind items. + description: | + Exemption code. + + - `E1` — VAT-exempt owner operating with a Spanish contact, at 0% rate. + - `E2` — 0%-rated operation with a non-EU contact. + - `E3` — Intracommunity supply of goods (not services) to an EU contact with a valid VAT-IVA number. + - `E5` — Explicit exemption reason set by the user in the "Imported by" field (e.g. diplomats, universities — see exempt_reason r1/r2/r3). + - `E6` — Exemption with no more specific reason determined. + - `null` — A classification code is present for this rate (mutually exclusive with `operation_classifications`). regime_key: type: string nullable: true - description: > - ClaveRégimen code ("01"–"20", e.g. "18" for equivalence surcharge, "17" for OSS). - Empty string when it could not be determined; null when the group has no current-kind - items. + description: | + Regime key code. + + - `01` — General VAT regime. + - `02` — Export. + - `08` — IPSI / IGIC. + - `10` — Collections by third parties. + - `11` — Business leases — VAT subject and not exempt. + - `17` — OSS and IOSS. + - `18` — Equivalence surcharge regime. + - `19` — Agricultural regime. + - `20` — Simplified regime. + - `null` — Not determined (group has no current items). equalization_rate: type: number nullable: true From 7767a92d63a3e58f93a27af348672fca08bd55db Mon Sep 17 00:00:00 2001 From: David Martin Garcia <11503528+dmartingarcia@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:20:32 +0200 Subject: [PATCH 06/13] ci: render spec to PDF, public and full MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a CI step that builds a PDF from both the filtered public spec and the full internal spec (Redocly build-docs + wkhtmltopdf), uploaded as artifacts alongside the existing JSON ones — public PDF openly, full PDF gated the same way as openapi-full (90-day retention, private). --- .github/workflows/deploy.yml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ec0b5ff..d2fda7c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -61,12 +61,33 @@ jobs: name: openapi-full path: dist-internal/openapi.json retention-days: 90 + - name: Install PDF tooling + run: sudo apt-get update && sudo apt-get install -y wkhtmltopdf + - name: Render public and full spec to PDF + run: | + npx --yes @redocly/cli build-docs openapi.yaml -o public-docs.html + wkhtmltopdf --enable-local-file-access public-docs.html openapi-public.pdf + npx --yes @redocly/cli build-docs dist-internal/openapi.json -o full-docs.html + wkhtmltopdf --enable-local-file-access full-docs.html openapi-full.pdf + - name: Upload public spec PDF + uses: actions/upload-artifact@v4 + with: + name: openapi-public-pdf + path: openapi-public.pdf + # Same access restriction as the openapi-full JSON artifact above — x-internal + # fields included, never published to Pages. + - name: Upload full spec PDF (private, for Aplifisa) + uses: actions/upload-artifact@v4 + with: + name: openapi-full-pdf + path: openapi-full.pdf + retention-days: 90 # Everything below here is what actually goes public — remove build tooling and # the full spec from disk first so the Pages artifact can only ever contain the # filtered openapi.yaml, regardless of what upload-pages-artifact does or doesn't # exclude on its own. - name: Strip build tooling and internal spec before publishing - run: rm -rf node_modules dist-internal + run: rm -rf node_modules dist-internal *.html *.pdf - name: Setup Pages uses: actions/configure-pages@v5 - name: Upload artifact From 1f837d5cc3e73b967b6f5115dc0e24557d7ddf30 Mon Sep 17 00:00:00 2001 From: David Martin Garcia <11503528+dmartingarcia@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:23:01 +0200 Subject: [PATCH 07/13] ci: split spec/PDF build into its own job, run on PRs too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves spec building, artifact uploads, and PDF rendering out of the main-only deploy job into a build job that runs on every push and PR — so reviewers get the JSON/PDF artifacts without waiting for a merge to main. The actual Pages bundling/deploy stays gated to push-to-main. --- .github/workflows/deploy.yml | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d2fda7c..962e7ec 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -33,12 +33,8 @@ jobs: - name: Run tests run: npm test - deploy: + build: needs: test - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - name: Checkout @@ -83,17 +79,30 @@ jobs: path: openapi-full.pdf retention-days: 90 # Everything below here is what actually goes public — remove build tooling and - # the full spec from disk first so the Pages artifact can only ever contain the - # filtered openapi.yaml, regardless of what upload-pages-artifact does or doesn't - # exclude on its own. + # the full spec/PDFs from disk first so the Pages artifact can only ever contain + # the filtered openapi.yaml, regardless of what upload-pages-artifact does or + # doesn't exclude on its own. Only bundled on push to main — no point producing a + # Pages artifact that nothing will deploy from a PR run. - name: Strip build tooling and internal spec before publishing + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: rm -rf node_modules dist-internal *.html *.pdf - name: Setup Pages + if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: actions/configure-pages@v5 - - name: Upload artifact + - name: Upload Pages artifact + if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: actions/upload-pages-artifact@v3 with: path: '.' + + deploy: + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 From e59eb71d0779ed2a9ed7664444eb89c142044e0b Mon Sep 17 00:00:00 2001 From: David Martin Garcia <11503528+dmartingarcia@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:51:05 +0200 Subject: [PATCH 08/13] ci: render PDFs with Scalar (Puppeteer) instead of Redoc/wkhtmltopdf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redoc's static HTML + wkhtmltopdf silently dropped deeply nested schema content (verified: fields like vat_breakdown were present in the source HTML but produced zero matches in the rendered PDF's text layer — likely lost during the old WebKit engine's pagination of Redoc's collapsed JSON example viewer). scripts/render-pdf.js instead serves the project's actual index.html (Scalar) locally and drives it with Puppeteer. Scalar's "modern" layout keeps every sidebar group collapsed by default and never mounts a collapsed group's content, so it expands every group (and any other collapsed/expandable element) in rounds before printing. Verified against both specs: the public PDF has zero occurrences of internal-only fields (e.g. vat_breakdown) and the full PDF has all of them, both as real selectable/searchable text. --- .github/workflows/deploy.yml | 13 +- package-lock.json | 1191 +++++++++++++++++++++++++++++++++- package.json | 6 +- scripts/render-pdf.js | 126 ++++ 4 files changed, 1326 insertions(+), 10 deletions(-) create mode 100644 scripts/render-pdf.js diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 962e7ec..935002c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -57,14 +57,13 @@ jobs: name: openapi-full path: dist-internal/openapi.json retention-days: 90 - - name: Install PDF tooling - run: sudo apt-get update && sudo apt-get install -y wkhtmltopdf + # Renders the actual Scalar site (same index.html as production) with Puppeteer, + # expanding every collapsed sidebar group first — Scalar's "modern" layout mounts + # nothing for a collapsed group, so a naive print only captures the intro. - name: Render public and full spec to PDF run: | - npx --yes @redocly/cli build-docs openapi.yaml -o public-docs.html - wkhtmltopdf --enable-local-file-access public-docs.html openapi-public.pdf - npx --yes @redocly/cli build-docs dist-internal/openapi.json -o full-docs.html - wkhtmltopdf --enable-local-file-access full-docs.html openapi-full.pdf + npm run render-pdf -- openapi.yaml openapi-public.pdf + npm run render-pdf -- dist-internal/openapi.json openapi-full.pdf - name: Upload public spec PDF uses: actions/upload-artifact@v4 with: @@ -85,7 +84,7 @@ jobs: # Pages artifact that nothing will deploy from a PR run. - name: Strip build tooling and internal spec before publishing if: github.event_name == 'push' && github.ref == 'refs/heads/main' - run: rm -rf node_modules dist-internal *.html *.pdf + run: rm -rf node_modules dist-internal *.pdf - name: Setup Pages if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: actions/configure-pages@v5 diff --git a/package-lock.json b/package-lock.json index 8aaaeea..b92ba3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,120 @@ "name": "api-v1-docs", "version": "1.0.0", "devDependencies": { - "js-yaml": "^4.1.0" + "js-yaml": "^4.1.0", + "puppeteer": "^24.15.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", + "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/argparse": { @@ -18,6 +131,546 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz", + "integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.6.tgz", + "integrity": "sha512-iQxPClE07hETVpbRoX7JXX3v/ZQViCxe/SYCxylRLzdEx1xJAufPptfiOqR8tqiCtmbtMDANKWszzjLu1PMAZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1608973", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", + "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", @@ -40,6 +693,542 @@ "bin": { "js-yaml": "bin/js-yaml.js" } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "24.43.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", + "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.2", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1608973", + "puppeteer-core": "24.43.1", + "typed-query-selector": "^2.12.2" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.43.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", + "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.2", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1608973", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.20.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 0acaeb7..4a87abd 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,11 @@ "scripts": { "test": "node --test scripts/", "build": "node scripts/filter-internal.js", - "lint:spec": "node -e \"require('js-yaml').load(require('fs').readFileSync('openapi.yaml', 'utf8')); console.log('openapi.yaml: valid YAML')\"" + "lint:spec": "node -e \"require('js-yaml').load(require('fs').readFileSync('openapi.yaml', 'utf8')); console.log('openapi.yaml: valid YAML')\"", + "render-pdf": "node scripts/render-pdf.js" }, "devDependencies": { - "js-yaml": "^4.1.0" + "js-yaml": "^4.1.0", + "puppeteer": "^24.15.0" } } diff --git a/scripts/render-pdf.js b/scripts/render-pdf.js new file mode 100644 index 0000000..457d645 --- /dev/null +++ b/scripts/render-pdf.js @@ -0,0 +1,126 @@ +// Renders an OpenAPI spec to PDF using the project's real index.html (Scalar), +// served locally and driven with Puppeteer. +// +// Scalar's "modern" layout keeps every sidebar group collapsed by default, and +// collapsed groups never mount their content in the DOM — so a naive +// page.pdf() only captures the intro paragraph. expandAll() clicks every +// "Open Group" toggle (and any other collapsed/expandable element) in +// rounds, since expanding a group reveals further collapsed children, until +// a round finds nothing left to click. +// +// Usage: node scripts/render-pdf.js +import fs from 'node:fs' +import http from 'node:http' +import os from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import puppeteer from 'puppeteer' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const ROOT = path.resolve(__dirname, '..') + +const MIME = { + '.html': 'text/html', + '.json': 'application/json', + '.yaml': 'text/yaml', + '.yml': 'text/yaml', + '.png': 'image/png', + '.ico': 'image/x-icon', + '.svg': 'image/svg+xml' +} + +function serveDir(dir) { + return new Promise((resolve) => { + const server = http.createServer((req, res) => { + const reqPath = decodeURIComponent(req.url.split('?')[0]) + const filePath = path.join(dir, reqPath === '/' ? 'index.html' : reqPath) + fs.readFile(filePath, (err, data) => { + if (err) { + res.writeHead(404) + res.end() + return + } + res.writeHead(200, { 'Content-Type': MIME[path.extname(filePath)] || 'application/octet-stream' }) + res.end(data) + }) + }) + server.listen(0, () => resolve(server)) + }) +} + +function buildSiteDir(specPath) { + const siteDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scalar-pdf-')) + const specName = 'spec' + path.extname(specPath) + + for (const asset of fs.readdirSync(ROOT)) { + if (['.png', '.ico', '.svg'].includes(path.extname(asset))) { + fs.copyFileSync(path.join(ROOT, asset), path.join(siteDir, asset)) + } + } + fs.copyFileSync(specPath, path.join(siteDir, specName)) + + const html = fs + .readFileSync(path.join(ROOT, 'index.html'), 'utf8') + .replace(/data-url="\.\/openapi\.yaml(\?v=\d+)?"/, `data-url="./${specName}"`) + fs.writeFileSync(path.join(siteDir, 'index.html'), html) + + return siteDir +} + +async function expandAll(page) { + for (let round = 0; round < 20; round++) { + const clicked = await page.evaluate(() => { + const buttons = Array.from(document.querySelectorAll('button, a')).filter( + (el) => + el.innerText.includes('Open Group') || + el.innerText.trim() === 'Expand all' || + el.getAttribute('aria-expanded') === 'false' + ) + buttons.forEach((b) => { + try { + b.click() + } catch { + // element detached by a previous click in this round — skip it + } + }) + return buttons.length + }) + await new Promise((r) => setTimeout(r, 600)) + if (clicked === 0) break + } +} + +async function main() { + const [, , specPath, outputPath] = process.argv + if (!specPath || !outputPath) { + console.error('Usage: node scripts/render-pdf.js ') + process.exit(1) + } + + const siteDir = buildSiteDir(path.resolve(specPath)) + const server = await serveDir(siteDir) + const { port } = server.address() + + const browser = await puppeteer.launch({ args: ['--no-sandbox'] }) + try { + const page = await browser.newPage() + await page.setViewport({ width: 1280, height: 900 }) + await page.goto(`http://localhost:${port}/`, { waitUntil: 'networkidle0', timeout: 60_000 }) + await new Promise((r) => setTimeout(r, 3000)) // let Scalar parse and mount the spec + await expandAll(page) + await page.evaluate(() => window.scrollTo(0, 0)) + await new Promise((r) => setTimeout(r, 800)) + await page.pdf({ path: path.resolve(outputPath), printBackground: true, format: 'A4' }) + } finally { + await browser.close() + server.close() + fs.rmSync(siteDir, { recursive: true, force: true }) + } + + console.log(`Wrote ${outputPath}`) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) From b7ccf0c444f8c0b87da7fa30b55f890149cd4feb Mon Sep 17 00:00:00 2001 From: David Martin Garcia <11503528+dmartingarcia@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:45:32 +0200 Subject: [PATCH 09/13] ci: comment on PR with links to the rendered PDFs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses the job's own GITHUB_TOKEN (actions/github-script) to post a PR comment pointing at the run's Artifacts section — no external auth needed. Upserts via a marker so repeated pushes update one comment instead of piling up a new one each time. --- .github/workflows/deploy.yml | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 935002c..bcd241f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,6 +10,7 @@ permissions: contents: read pages: write id-token: write + pull-requests: write concurrency: group: "pages" @@ -77,6 +78,45 @@ jobs: name: openapi-full-pdf path: openapi-full.pdf retention-days: 90 + # Artifacts aren't at a stable public URL — the API token used to fetch them expires + # with the run, so the comment links to the run's Artifacts section instead of the + # files directly. Upserts (via the marker) instead of piling up a new comment per push. + - name: Comment PR with PDF links + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const marker = '' + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + const body = [ + marker, + '**API spec PDFs for this run are ready.**', + `Download them from the [workflow run](${runUrl})'s Artifacts section:`, + '- `openapi-public-pdf` — public spec, what ships to the docs site', + '- `openapi-full-pdf` — full spec including `x-internal` (Aplifisa) fields' + ].join('\n') + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }) + const existing = comments.find((c) => c.body.includes(marker)) + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body + }) + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }) + } # Everything below here is what actually goes public — remove build tooling and # the full spec/PDFs from disk first so the Pages artifact can only ever contain # the filtered openapi.yaml, regardless of what upload-pages-artifact does or From 8364c15bbeeffe8530ed22bf787e8737f3f795b6 Mon Sep 17 00:00:00 2001 From: David Martin Garcia <11503528+dmartingarcia@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:47:33 +0200 Subject: [PATCH 10/13] docs: address Copilot review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README: clarify the naming rule applies to publicly-shipped fields, not x-internal ones (which do name the partner throughout openapi.yaml already) - deploy.yml: fix comments claiming the full spec/PDF keep the x-internal marker — filter-internal.js strips the marker, only the field survives - openapi.yaml: fix operation_classification(s) typo in its own description --- .github/workflows/deploy.yml | 6 +++--- README.md | 8 ++++---- openapi.yaml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bcd241f..499bf95 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -49,7 +49,7 @@ jobs: run: npm ci - name: Build public and full specs run: npm run build - # dist-internal/openapi.json has every field, x-internal included. Uploaded as a + # dist-internal/openapi.json has every field, with x-internal markers stripped. Uploaded as a # private Actions artifact only — never published to Pages. Requires repo access # to download (this repo is private); hand it to Aplifisa manually. - name: Upload full spec (private, for Aplifisa) @@ -70,8 +70,8 @@ jobs: with: name: openapi-public-pdf path: openapi-public.pdf - # Same access restriction as the openapi-full JSON artifact above — x-internal - # fields included, never published to Pages. + # Same access restriction as the openapi-full JSON artifact above — internal-only + # fields included (x-internal markers stripped), never published to Pages. - name: Upload full spec PDF (private, for Aplifisa) uses: actions/upload-artifact@v4 with: diff --git a/README.md b/README.md index 7fd91b9..e1173a1 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,10 @@ Edit `openapi.yaml` and push to `main`. The docs will be deployed automatically. Some API v1 fields only exist for a specific integration partner (e.g. Aplifisa) and are gated server-side in the Rails app — they only appear in a response when the owner has that integration activated (`owner.aplifisa?` in the serializers, passed down from the API v1 controllers as -`with_aplifisa_data`). This repo is public documentation, so those fields must never ship in the -published spec, and their descriptions must never name the partner integration by name (a public -field's description saying "Aplifisa" would leak the association even if the field itself stays -public). +`with_aplifisa_data`). This repo is public documentation, so those partner-gated fields must never +ship in the published spec (CI removes every `x-internal: true` field before deploying). Any field +that *does* ship publicly (i.e. not `x-internal`) must never name the partner integration by name in +its description — that would leak the association even though the field itself stays public. **Rule of thumb:** if a field is only serialized `if: -> { instance_options[:with_aplifisa_data] }` (or the equivalent gate) in `app/serializers/api/v1/**` in the main app, tag it here: diff --git a/openapi.yaml b/openapi.yaml index 2676869..7dd4834 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2268,7 +2268,7 @@ components: - `E3` — Intracommunity supply of goods (not services) to an EU contact with a valid VAT-IVA number. - `E5` — Explicit exemption reason set by the user in the "Imported by" field (e.g. diplomats, universities — see exempt_reason r1/r2/r3). - `E6` — Exemption with no more specific reason determined. - - `null` — A classification code is present for this rate (mutually exclusive with `operation_classifications`). + - `null` — A classification code is present for this rate (mutually exclusive with `operation_classification`). regime_key: type: string nullable: true From 939946e9befb9d806877d84c408b7c06c769e141 Mon Sep 17 00:00:00 2001 From: David Martin Garcia <11503528+dmartingarcia@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:50:02 +0200 Subject: [PATCH 11/13] ci: link directly to each PDF artifact in the PR comment Uses actions/upload-artifact's artifact-id output to build a direct .../artifacts/{id} link per PDF instead of just pointing at the run page. --- .github/workflows/deploy.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 499bf95..597b401 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -66,6 +66,7 @@ jobs: npm run render-pdf -- openapi.yaml openapi-public.pdf npm run render-pdf -- dist-internal/openapi.json openapi-full.pdf - name: Upload public spec PDF + id: upload-public-pdf uses: actions/upload-artifact@v4 with: name: openapi-public-pdf @@ -73,27 +74,32 @@ jobs: # Same access restriction as the openapi-full JSON artifact above — internal-only # fields included (x-internal markers stripped), never published to Pages. - name: Upload full spec PDF (private, for Aplifisa) + id: upload-full-pdf uses: actions/upload-artifact@v4 with: name: openapi-full-pdf path: openapi-full.pdf retention-days: 90 - # Artifacts aren't at a stable public URL — the API token used to fetch them expires - # with the run, so the comment links to the run's Artifacts section instead of the - # files directly. Upserts (via the marker) instead of piling up a new comment per push. + # Deep-links straight to each artifact (.../artifacts/{id}) instead of just the run — + # still requires repo access to download (private repo), same as before. Upserts via + # the marker so repeated pushes update one comment instead of piling up a new one. - name: Comment PR with PDF links if: github.event_name == 'pull_request' uses: actions/github-script@v7 + env: + PUBLIC_ARTIFACT_ID: ${{ steps.upload-public-pdf.outputs.artifact-id }} + FULL_ARTIFACT_ID: ${{ steps.upload-full-pdf.outputs.artifact-id }} with: script: | const marker = '' const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + const publicUrl = `${runUrl}/artifacts/${process.env.PUBLIC_ARTIFACT_ID}` + const fullUrl = `${runUrl}/artifacts/${process.env.FULL_ARTIFACT_ID}` const body = [ marker, '**API spec PDFs for this run are ready.**', - `Download them from the [workflow run](${runUrl})'s Artifacts section:`, - '- `openapi-public-pdf` — public spec, what ships to the docs site', - '- `openapi-full-pdf` — full spec including `x-internal` (Aplifisa) fields' + `- [openapi-public-pdf](${publicUrl}) — public spec, what ships to the docs site`, + `- [openapi-full-pdf](${fullUrl}) — full spec including \`x-internal\` (Aplifisa) fields` ].join('\n') const { data: comments } = await github.rest.issues.listComments({ From 1229eb66942e8a73fc19c62f451e07b14022dbae Mon Sep 17 00:00:00 2001 From: David Martin Garcia <11503528+dmartingarcia@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:02:04 +0200 Subject: [PATCH 12/13] ci: ship a single self-contained HTML doc instead of a PDF, drop Puppeteer Printing Scalar's SPA to PDF hits page-break pagination that cuts content mid-element (verified visually). Replaces it with scripts/build-standalone-html.js: one HTML file per spec with the OpenAPI document embedded inline (no fetch, no other assets) and defaultOpenAllTags: true, so it opens directly in a browser with nothing to convert and nothing to cut. Drops the puppeteer dependency and scripts/render-pdf.js entirely. --- .github/workflows/deploy.yml | 43 +- package-lock.json | 1191 +----------------------------- package.json | 5 +- scripts/build-standalone-html.js | 79 ++ scripts/render-pdf.js | 126 ---- 5 files changed, 104 insertions(+), 1340 deletions(-) create mode 100644 scripts/build-standalone-html.js delete mode 100644 scripts/render-pdf.js diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 597b401..0e98957 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -58,37 +58,38 @@ jobs: name: openapi-full path: dist-internal/openapi.json retention-days: 90 - # Renders the actual Scalar site (same index.html as production) with Puppeteer, - # expanding every collapsed sidebar group first — Scalar's "modern" layout mounts - # nothing for a collapsed group, so a naive print only captures the intro. - - name: Render public and full spec to PDF + # Single self-contained HTML file per spec: Scalar UI + spec embedded inline, no + # fetch, no other files. Open directly in a browser — nothing to serve, nothing to + # convert. (Previously rendered to PDF via Puppeteer, but printing an SPA hits page + # breaks that cut content mid-element; HTML has no pages, so nothing to cut.) + - name: Build public and full standalone HTML docs run: | - npm run render-pdf -- openapi.yaml openapi-public.pdf - npm run render-pdf -- dist-internal/openapi.json openapi-full.pdf - - name: Upload public spec PDF - id: upload-public-pdf + npm run build-html -- openapi.yaml openapi-public.html + npm run build-html -- dist-internal/openapi.json openapi-full.html + - name: Upload public HTML doc + id: upload-public-html uses: actions/upload-artifact@v4 with: - name: openapi-public-pdf - path: openapi-public.pdf + name: openapi-public-html + path: openapi-public.html # Same access restriction as the openapi-full JSON artifact above — internal-only # fields included (x-internal markers stripped), never published to Pages. - - name: Upload full spec PDF (private, for Aplifisa) - id: upload-full-pdf + - name: Upload full HTML doc (private, for Aplifisa) + id: upload-full-html uses: actions/upload-artifact@v4 with: - name: openapi-full-pdf - path: openapi-full.pdf + name: openapi-full-html + path: openapi-full.html retention-days: 90 # Deep-links straight to each artifact (.../artifacts/{id}) instead of just the run — # still requires repo access to download (private repo), same as before. Upserts via # the marker so repeated pushes update one comment instead of piling up a new one. - - name: Comment PR with PDF links + - name: Comment PR with doc links if: github.event_name == 'pull_request' uses: actions/github-script@v7 env: - PUBLIC_ARTIFACT_ID: ${{ steps.upload-public-pdf.outputs.artifact-id }} - FULL_ARTIFACT_ID: ${{ steps.upload-full-pdf.outputs.artifact-id }} + PUBLIC_ARTIFACT_ID: ${{ steps.upload-public-html.outputs.artifact-id }} + FULL_ARTIFACT_ID: ${{ steps.upload-full-html.outputs.artifact-id }} with: script: | const marker = '' @@ -97,9 +98,9 @@ jobs: const fullUrl = `${runUrl}/artifacts/${process.env.FULL_ARTIFACT_ID}` const body = [ marker, - '**API spec PDFs for this run are ready.**', - `- [openapi-public-pdf](${publicUrl}) — public spec, what ships to the docs site`, - `- [openapi-full-pdf](${fullUrl}) — full spec including \`x-internal\` (Aplifisa) fields` + '**API docs for this run are ready** (single self-contained HTML file each — download and open in a browser):', + `- [openapi-public-html](${publicUrl}) — public spec, what ships to the docs site`, + `- [openapi-full-html](${fullUrl}) — full spec including \`x-internal\` (Aplifisa) fields` ].join('\n') const { data: comments } = await github.rest.issues.listComments({ @@ -130,7 +131,7 @@ jobs: # Pages artifact that nothing will deploy from a PR run. - name: Strip build tooling and internal spec before publishing if: github.event_name == 'push' && github.ref == 'refs/heads/main' - run: rm -rf node_modules dist-internal *.pdf + run: rm -rf node_modules dist-internal openapi-public.html openapi-full.html - name: Setup Pages if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: actions/configure-pages@v5 diff --git a/package-lock.json b/package-lock.json index b92ba3b..8aaaeea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,120 +8,7 @@ "name": "api-v1-docs", "version": "1.0.0", "devDependencies": { - "js-yaml": "^4.1.0", - "puppeteer": "^24.15.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "26.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", - "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "undici-types": "~8.3.0" - } - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "js-yaml": "^4.1.0" } }, "node_modules/argparse": { @@ -131,546 +18,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/b4a": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", - "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, - "node_modules/bare-events": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", - "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz", - "integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", - "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/bare-stream": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", - "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.6.tgz", - "integrity": "sha512-iQxPClE07hETVpbRoX7JXX3v/ZQViCxe/SYCxylRLzdEx1xJAufPptfiOqR8tqiCtmbtMDANKWszzjLu1PMAZQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-path": "^3.0.0" - } - }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "mitt": "^3.0.1", - "zod": "^3.24.1" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, "node_modules/js-yaml": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", @@ -693,542 +40,6 @@ "bin": { "js-yaml": "bin/js-yaml.js" } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", - "typed-query-selector": "^2.12.2" - }, - "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", - "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", - "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/streamx": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", - "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", - "dev": true, - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/tar-stream": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", - "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.21.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", - "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } diff --git a/package.json b/package.json index 4a87abd..42223fa 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,9 @@ "test": "node --test scripts/", "build": "node scripts/filter-internal.js", "lint:spec": "node -e \"require('js-yaml').load(require('fs').readFileSync('openapi.yaml', 'utf8')); console.log('openapi.yaml: valid YAML')\"", - "render-pdf": "node scripts/render-pdf.js" + "build-html": "node scripts/build-standalone-html.js" }, "devDependencies": { - "js-yaml": "^4.1.0", - "puppeteer": "^24.15.0" + "js-yaml": "^4.1.0" } } diff --git a/scripts/build-standalone-html.js b/scripts/build-standalone-html.js new file mode 100644 index 0000000..e004f10 --- /dev/null +++ b/scripts/build-standalone-html.js @@ -0,0 +1,79 @@ +// Bundles a single self-contained HTML file: Scalar UI + the OpenAPI spec +// embedded inline (no fetch, no other files). Open it directly in any +// browser — nothing to serve, nothing to convert. +// +// Replaces the earlier Puppeteer/PDF pipeline: printing an SPA to PDF hits +// page-break pagination that cuts content mid-element. HTML has no pages, so +// there's nothing to cut. +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import yaml from 'js-yaml' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const ROOT = path.resolve(__dirname, '..') + +function loadSpecAsObject(specPath) { + const raw = fs.readFileSync(specPath, 'utf8') + return specPath.endsWith('.json') ? JSON.parse(raw) : yaml.load(raw) +} + +function main() { + const [, , specPath, outputPath] = process.argv + if (!specPath || !outputPath) { + console.error('Usage: node scripts/build-standalone-html.js ') + process.exit(1) + } + + const spec = loadSpecAsObject(path.resolve(specPath)) + const specJson = JSON.stringify(spec).replace(/" inside the embedded JSON + const logoBase64 = fs.readFileSync(path.join(ROOT, 'logo.png')).toString('base64') + + const html = ` + + + ${spec.info?.title ?? 'API Reference'} + + + + + + + + + + +` + fs.writeFileSync(path.resolve(outputPath), html) + console.log(`Wrote ${outputPath}`) +} + +main() diff --git a/scripts/render-pdf.js b/scripts/render-pdf.js deleted file mode 100644 index 457d645..0000000 --- a/scripts/render-pdf.js +++ /dev/null @@ -1,126 +0,0 @@ -// Renders an OpenAPI spec to PDF using the project's real index.html (Scalar), -// served locally and driven with Puppeteer. -// -// Scalar's "modern" layout keeps every sidebar group collapsed by default, and -// collapsed groups never mount their content in the DOM — so a naive -// page.pdf() only captures the intro paragraph. expandAll() clicks every -// "Open Group" toggle (and any other collapsed/expandable element) in -// rounds, since expanding a group reveals further collapsed children, until -// a round finds nothing left to click. -// -// Usage: node scripts/render-pdf.js -import fs from 'node:fs' -import http from 'node:http' -import os from 'node:os' -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import puppeteer from 'puppeteer' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const ROOT = path.resolve(__dirname, '..') - -const MIME = { - '.html': 'text/html', - '.json': 'application/json', - '.yaml': 'text/yaml', - '.yml': 'text/yaml', - '.png': 'image/png', - '.ico': 'image/x-icon', - '.svg': 'image/svg+xml' -} - -function serveDir(dir) { - return new Promise((resolve) => { - const server = http.createServer((req, res) => { - const reqPath = decodeURIComponent(req.url.split('?')[0]) - const filePath = path.join(dir, reqPath === '/' ? 'index.html' : reqPath) - fs.readFile(filePath, (err, data) => { - if (err) { - res.writeHead(404) - res.end() - return - } - res.writeHead(200, { 'Content-Type': MIME[path.extname(filePath)] || 'application/octet-stream' }) - res.end(data) - }) - }) - server.listen(0, () => resolve(server)) - }) -} - -function buildSiteDir(specPath) { - const siteDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scalar-pdf-')) - const specName = 'spec' + path.extname(specPath) - - for (const asset of fs.readdirSync(ROOT)) { - if (['.png', '.ico', '.svg'].includes(path.extname(asset))) { - fs.copyFileSync(path.join(ROOT, asset), path.join(siteDir, asset)) - } - } - fs.copyFileSync(specPath, path.join(siteDir, specName)) - - const html = fs - .readFileSync(path.join(ROOT, 'index.html'), 'utf8') - .replace(/data-url="\.\/openapi\.yaml(\?v=\d+)?"/, `data-url="./${specName}"`) - fs.writeFileSync(path.join(siteDir, 'index.html'), html) - - return siteDir -} - -async function expandAll(page) { - for (let round = 0; round < 20; round++) { - const clicked = await page.evaluate(() => { - const buttons = Array.from(document.querySelectorAll('button, a')).filter( - (el) => - el.innerText.includes('Open Group') || - el.innerText.trim() === 'Expand all' || - el.getAttribute('aria-expanded') === 'false' - ) - buttons.forEach((b) => { - try { - b.click() - } catch { - // element detached by a previous click in this round — skip it - } - }) - return buttons.length - }) - await new Promise((r) => setTimeout(r, 600)) - if (clicked === 0) break - } -} - -async function main() { - const [, , specPath, outputPath] = process.argv - if (!specPath || !outputPath) { - console.error('Usage: node scripts/render-pdf.js ') - process.exit(1) - } - - const siteDir = buildSiteDir(path.resolve(specPath)) - const server = await serveDir(siteDir) - const { port } = server.address() - - const browser = await puppeteer.launch({ args: ['--no-sandbox'] }) - try { - const page = await browser.newPage() - await page.setViewport({ width: 1280, height: 900 }) - await page.goto(`http://localhost:${port}/`, { waitUntil: 'networkidle0', timeout: 60_000 }) - await new Promise((r) => setTimeout(r, 3000)) // let Scalar parse and mount the spec - await expandAll(page) - await page.evaluate(() => window.scrollTo(0, 0)) - await new Promise((r) => setTimeout(r, 800)) - await page.pdf({ path: path.resolve(outputPath), printBackground: true, format: 'A4' }) - } finally { - await browser.close() - server.close() - fs.rmSync(siteDir, { recursive: true, force: true }) - } - - console.log(`Wrote ${outputPath}`) -} - -main().catch((err) => { - console.error(err) - process.exit(1) -}) From eff75f639a254befd9b56b63eaf5d484691200ac Mon Sep 17 00:00:00 2001 From: David Martin Garcia <11503528+dmartingarcia@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:04:15 +0200 Subject: [PATCH 13/13] ci: rename PR comment dedup marker to match HTML docs, not PDF --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0e98957..6c80e19 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -92,7 +92,7 @@ jobs: FULL_ARTIFACT_ID: ${{ steps.upload-full-html.outputs.artifact-id }} with: script: | - const marker = '' + const marker = '' const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` const publicUrl = `${runUrl}/artifacts/${process.env.PUBLIC_ARTIFACT_ID}` const fullUrl = `${runUrl}/artifacts/${process.env.FULL_ARTIFACT_ID}`