@focusgts/eds-mcp-server
Advanced tools
@@ -10,3 +10,3 @@ /** | ||
| */ | ||
| import type { DaClientOptions, DaSourceEntry, DaSourceContent, DaVersion, DaOperationResponse } from './types.js'; | ||
| import type { DaClientOptions, DaSourceEntry, DaSourceContent, DaVersion, DaOperationResponse, DaDocument, DaExportResult, DaPushResult } from './types.js'; | ||
| /** Friendly message shown when a DA operation is attempted without a token. */ | ||
@@ -26,4 +26,13 @@ export declare const NEEDS_DA_TOKEN_MESSAGE = "No DA token configured. Set EDS_DA_TOKEN to a Document Authoring API token to use the DA content tools \u2014 see the README."; | ||
| get hasToken(): boolean; | ||
| /** List sources/directories under a path — GET /list/{org}/{repo}[/{path}]. */ | ||
| /** | ||
| * List sources/directories under a path — GET /list/{org}/{repo}[/{path}]. | ||
| * | ||
| * DA paginates the listing (S3's 1000-key default) via the | ||
| * `da-continuation-token` header, in and out. We follow it to completion so | ||
| * large folders aren't silently truncated (which would make an export report | ||
| * "complete" while missing files). | ||
| */ | ||
| listSources(path?: string): Promise<DaSourceEntry[]>; | ||
| /** True when a listing entry is a file (has a file extension); DA folders have none. */ | ||
| private hasExtension; | ||
| /** Remove the leading /{org}/{repo} from a DA-returned path. */ | ||
@@ -48,2 +57,23 @@ private stripSitePrefix; | ||
| /** | ||
| * Export a whole DA subtree in one call: recursively list every document | ||
| * under `path` and fetch its source concurrently. This is the agent-native | ||
| * "clone" read — one call instead of the agent orchestrating N list+get | ||
| * calls. Bounded by `maxFiles` (flags `truncated`) and resilient to | ||
| * individual fetch failures (reported in `failed`, never dropped silently). | ||
| */ | ||
| exportTree(path: string, options?: { | ||
| maxFiles?: number; | ||
| concurrency?: number; | ||
| }): Promise<DaExportResult>; | ||
| /** | ||
| * Push many documents back to DA in one call, concurrently. The "push" half | ||
| * of the bulk model. Returns per-document succeeded/failed (partial failures | ||
| * never abort the whole batch). | ||
| */ | ||
| pushDocuments(documents: DaDocument[], options?: { | ||
| concurrency?: number; | ||
| }): Promise<DaPushResult>; | ||
| /** Run `fn` over `items` with at most `concurrency` in flight at once. */ | ||
| private mapWithConcurrency; | ||
| /** | ||
| * Normalize a folder/collection path: drop empty segments (leading, trailing, | ||
@@ -50,0 +80,0 @@ * and internal `//`), reject traversal, and percent-encode each segment. |
+156
-15
@@ -41,21 +41,47 @@ /** | ||
| // ------------------------------------------------------------------------- | ||
| /** List sources/directories under a path — GET /list/{org}/{repo}[/{path}]. */ | ||
| /** | ||
| * List sources/directories under a path — GET /list/{org}/{repo}[/{path}]. | ||
| * | ||
| * DA paginates the listing (S3's 1000-key default) via the | ||
| * `da-continuation-token` header, in and out. We follow it to completion so | ||
| * large folders aren't silently truncated (which would make an export report | ||
| * "complete" while missing files). | ||
| */ | ||
| async listSources(path) { | ||
| const suffix = path ? `/${this.normalizePath(path)}` : ''; | ||
| const res = await this.request(`/list/${this.org}/${this.repo}${suffix}`, { | ||
| method: 'GET', | ||
| }); | ||
| const body = (await res.json().catch(() => null)); | ||
| // The API may return a bare array or a { sources: [...] } wrapper. | ||
| let entries = []; | ||
| if (Array.isArray(body)) { | ||
| entries = body; | ||
| const endpoint = `/list/${this.org}/${this.repo}${suffix}`; | ||
| const all = []; | ||
| let token = null; | ||
| // Safety cap so a misbehaving token loop can't run forever (~50k entries). | ||
| for (let page = 0; page < 50; page++) { | ||
| const res = await this.request(endpoint, { | ||
| method: 'GET', | ||
| headers: token ? { 'da-continuation-token': token } : undefined, | ||
| }); | ||
| const body = (await res.json().catch(() => null)); | ||
| // The API may return a bare array or a { sources: [...] } wrapper. | ||
| let entries = []; | ||
| if (Array.isArray(body)) { | ||
| entries = body; | ||
| } | ||
| else if (body && typeof body === 'object' && Array.isArray(body.sources)) { | ||
| entries = body.sources; | ||
| } | ||
| // DA list paths include the /{org}/{repo} prefix; strip it so a listed | ||
| // path is site-relative and can be passed back to get_source/put_source. | ||
| for (const e of entries) | ||
| all.push({ ...e, path: this.stripSitePrefix(e.path) }); | ||
| token = res.headers.get('da-continuation-token') || null; | ||
| if (!token) | ||
| break; | ||
| } | ||
| else if (body && typeof body === 'object' && Array.isArray(body.sources)) { | ||
| entries = body.sources; | ||
| } | ||
| // DA list paths include the /{org}/{repo} prefix; strip it so a listed path | ||
| // is site-relative and can be passed straight back to get_source/put_source. | ||
| return entries.map((e) => ({ ...e, path: this.stripSitePrefix(e.path) })); | ||
| return all; | ||
| } | ||
| /** True when a listing entry is a file (has a file extension); DA folders have none. */ | ||
| hasExtension(entry) { | ||
| if (typeof entry.ext === 'string' && entry.ext.length > 0) | ||
| return true; | ||
| const last = (entry.path ?? '').split('/').pop() ?? ''; | ||
| return last.lastIndexOf('.') > 0; | ||
| } | ||
| /** Remove the leading /{org}/{repo} from a DA-returned path. */ | ||
@@ -139,2 +165,117 @@ stripSitePrefix(path) { | ||
| // ------------------------------------------------------------------------- | ||
| // Bulk content operations (the agent-native "clone" model, ADR-008) | ||
| // ------------------------------------------------------------------------- | ||
| /** | ||
| * Export a whole DA subtree in one call: recursively list every document | ||
| * under `path` and fetch its source concurrently. This is the agent-native | ||
| * "clone" read — one call instead of the agent orchestrating N list+get | ||
| * calls. Bounded by `maxFiles` (flags `truncated`) and resilient to | ||
| * individual fetch failures (reported in `failed`, never dropped silently). | ||
| */ | ||
| async exportTree(path, options = {}) { | ||
| const maxFiles = options.maxFiles ?? 100; | ||
| const concurrency = options.concurrency ?? 6; | ||
| // Only descend into folders under the requested root, so a stray or | ||
| // self-referential listing entry can't walk us out of the subtree. | ||
| const rootNorm = path.replace(/^\/+|\/+$/g, ''); | ||
| const rootPrefix = rootNorm === '' ? '/' : `/${rootNorm}/`; | ||
| const inTree = (p) => rootNorm === '' || p === `/${rootNorm}` || p.startsWith(rootPrefix); | ||
| const dirKey = (p) => p.replace(/^\/+|\/+$/g, ''); | ||
| // Breadth-first discovery of file paths (folders end with '/'). A `visited` | ||
| // set defends against cycles/duplicates; folder listings are guarded so one | ||
| // unreadable folder is recorded and skipped, not fatal to the whole export. | ||
| const files = []; | ||
| const failed = []; | ||
| const visited = new Set(); | ||
| const queue = [path]; | ||
| let skippedFile = false; | ||
| while (queue.length > 0 && files.length < maxFiles) { | ||
| const dir = queue.shift(); | ||
| const key = dirKey(dir); | ||
| if (visited.has(key)) | ||
| continue; | ||
| visited.add(key); | ||
| let entries; | ||
| try { | ||
| entries = await this.listSources(dir); | ||
| } | ||
| catch (error) { | ||
| failed.push({ path: dir, error: error instanceof Error ? error.message : String(error) }); | ||
| continue; | ||
| } | ||
| for (const entry of entries) { | ||
| if (!entry.path) | ||
| continue; | ||
| // DA marks folders by the ABSENCE of a file extension (verified against | ||
| // adobe/da-admin formatList: CommonPrefixes → { path, name } with no | ||
| // `ext`; folder paths have no trailing slash). A trailing-slash check | ||
| // would never match a real folder and silently flatten the export. | ||
| const isFolder = !this.hasExtension(entry); | ||
| if (isFolder) { | ||
| if (!visited.has(dirKey(entry.path)) && inTree(entry.path)) { | ||
| queue.push(entry.path); | ||
| } | ||
| } | ||
| else if (!inTree(entry.path)) { | ||
| // A file outside the requested subtree (shouldn't happen, but the | ||
| // containment invariant applies to files too, not just folders). | ||
| continue; | ||
| } | ||
| else if (files.length < maxFiles) { | ||
| files.push(entry.path); | ||
| } | ||
| else { | ||
| skippedFile = true; | ||
| } | ||
| } | ||
| } | ||
| // Truncated only when we genuinely stopped short: a file was skipped, or we | ||
| // hit the cap with folders still unexplored. | ||
| const truncated = skippedFile || (files.length >= maxFiles && queue.length > 0); | ||
| const documents = []; | ||
| await this.mapWithConcurrency(files, async (filePath) => { | ||
| try { | ||
| const src = await this.getSource(filePath); | ||
| documents.push({ path: src.path, content: src.content, contentType: src.contentType }); | ||
| } | ||
| catch (error) { | ||
| failed.push({ path: filePath, error: error instanceof Error ? error.message : String(error) }); | ||
| } | ||
| }, concurrency); | ||
| return { documents, fileCount: files.length, truncated, failed }; | ||
| } | ||
| /** | ||
| * Push many documents back to DA in one call, concurrently. The "push" half | ||
| * of the bulk model. Returns per-document succeeded/failed (partial failures | ||
| * never abort the whole batch). | ||
| */ | ||
| async pushDocuments(documents, options = {}) { | ||
| const concurrency = options.concurrency ?? 6; | ||
| const succeeded = []; | ||
| const failed = []; | ||
| await this.mapWithConcurrency(documents, async (doc) => { | ||
| try { | ||
| await this.putSource(doc.path, doc.content, doc.contentType); | ||
| succeeded.push(doc.path); | ||
| } | ||
| catch (error) { | ||
| failed.push({ path: doc.path, error: error instanceof Error ? error.message : String(error) }); | ||
| } | ||
| }, concurrency); | ||
| return { succeeded, failed }; | ||
| } | ||
| /** Run `fn` over `items` with at most `concurrency` in flight at once. */ | ||
| async mapWithConcurrency(items, fn, concurrency) { | ||
| let next = 0; | ||
| const worker = async () => { | ||
| while (next < items.length) { | ||
| const index = next; | ||
| next += 1; | ||
| await fn(items[index]); | ||
| } | ||
| }; | ||
| const size = Math.max(1, Math.min(concurrency, items.length)); | ||
| await Promise.all(Array.from({ length: size }, () => worker())); | ||
| } | ||
| // ------------------------------------------------------------------------- | ||
| // Helpers | ||
@@ -141,0 +282,0 @@ // ------------------------------------------------------------------------- |
@@ -62,1 +62,34 @@ /** | ||
| } | ||
| /** A single authored document (path + its source content). */ | ||
| export interface DaDocument { | ||
| /** Site-relative document path (e.g. /blog/post.html). */ | ||
| path: string; | ||
| /** Raw source content. */ | ||
| content: string; | ||
| /** MIME type, when known. */ | ||
| contentType?: string; | ||
| } | ||
| /** Result of a bulk export of a DA subtree. */ | ||
| export interface DaExportResult { | ||
| /** Every document fetched successfully under the exported path. */ | ||
| documents: DaDocument[]; | ||
| /** Number of files attempted (bounded by `maxFiles`), not necessarily all that exist. */ | ||
| fileCount: number; | ||
| /** True when the subtree exceeded the `maxFiles` cap — some files omitted. */ | ||
| truncated: boolean; | ||
| /** Files discovered but not fetchable (e.g. deleted mid-export), not dropped silently. */ | ||
| failed: Array<{ | ||
| path: string; | ||
| error: string; | ||
| }>; | ||
| } | ||
| /** Result of a bulk push of many documents. */ | ||
| export interface DaPushResult { | ||
| /** Paths written successfully. */ | ||
| succeeded: string[]; | ||
| /** Paths that failed, with the error message. */ | ||
| failed: Array<{ | ||
| path: string; | ||
| error: string; | ||
| }>; | ||
| } |
+3
-0
@@ -17,3 +17,6 @@ #!/usr/bin/env node | ||
| * EDS_DOMAIN_KEY — OpTel domain key for CWV / 404 / experiment queries | ||
| * EDS_DA_TOKEN — Document Authoring IMS token (enables the eds_da_* tools) | ||
| * EDS_DA_ORG — DA org (default: EDS_OWNER) | ||
| * EDS_DA_REPO — DA repo/site (default: EDS_REPO) | ||
| */ | ||
| export {}; |
+4
-0
@@ -17,2 +17,5 @@ #!/usr/bin/env node | ||
| * EDS_DOMAIN_KEY — OpTel domain key for CWV / 404 / experiment queries | ||
| * EDS_DA_TOKEN — Document Authoring IMS token (enables the eds_da_* tools) | ||
| * EDS_DA_ORG — DA org (default: EDS_OWNER) | ||
| * EDS_DA_REPO — DA repo/site (default: EDS_REPO) | ||
| */ | ||
@@ -109,2 +112,3 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; | ||
| ' EDS_DOMAIN_KEY OpTel domain key for analytics queries', | ||
| ' EDS_DA_TOKEN Document Authoring token (enables the eds_da_* tools)', | ||
| '', | ||
@@ -111,0 +115,0 @@ ].join('\n')); |
@@ -60,2 +60,23 @@ /** | ||
| }>; | ||
| export declare function handleDaExport(client: DaClient, args: { | ||
| path: string; | ||
| maxFiles?: number; | ||
| }): Promise<{ | ||
| content: { | ||
| type: "text"; | ||
| text: string; | ||
| }[]; | ||
| }>; | ||
| export declare function handleDaPush(client: DaClient, args: { | ||
| documents: Array<{ | ||
| path: string; | ||
| content: string; | ||
| contentType?: string; | ||
| }>; | ||
| }): Promise<{ | ||
| content: { | ||
| type: "text"; | ||
| text: string; | ||
| }[]; | ||
| }>; | ||
| export declare function handleDaGetVersions(client: DaClient, args: { | ||
@@ -62,0 +83,0 @@ path: string; |
@@ -25,5 +25,7 @@ /** | ||
| for (const e of entries) { | ||
| // Paths are site-relative and already carry a file extension (files) or a | ||
| // trailing slash (folders), so they read cleanly as-is. | ||
| lines.push(` ${e.path ?? e.name ?? '(unnamed)'}`); | ||
| const p = e.path ?? e.name ?? '(unnamed)'; | ||
| // DA folders have no extension; mark them with a trailing slash so the | ||
| // listing distinguishes folders from files at a glance. | ||
| const isFolder = !e.ext && !/\.[^/]+$/.test(p); | ||
| lines.push(` ${p}${isFolder && !p.endsWith('/') ? '/' : ''}`); | ||
| } | ||
@@ -81,2 +83,66 @@ return textResult(lines.join('\n')); | ||
| } | ||
| export async function handleDaExport(client, args) { | ||
| try { | ||
| const result = await client.exportTree(args.path, { maxFiles: args.maxFiles }); | ||
| const root = `/${args.path.replace(/^\/+/, '')}`; | ||
| if (result.documents.length === 0 && result.failed.length === 0 && !result.truncated) { | ||
| return textResult(`No documents found under ${root}.`); | ||
| } | ||
| const header = [ | ||
| `Exported ${result.documents.length} document${result.documents.length === 1 ? '' : 's'} from ${root}.`, | ||
| ]; | ||
| if (result.truncated) { | ||
| header.push(`(May be incomplete — hit the maxFiles cap with folders left. Narrow the path or raise maxFiles.)`); | ||
| } | ||
| if (result.failed.length > 0) { | ||
| header.push(`(${result.failed.length} item(s) could not be read: ${result.failed.map((f) => f.path).join(', ')})`); | ||
| } | ||
| // Bound the response size: inline document content up to a byte budget, then | ||
| // list the remaining paths so the agent knows they exist (fetch individually). | ||
| const MAX_OUTPUT_CHARS = 800_000; | ||
| const blocks = []; | ||
| const omitted = []; | ||
| let used = header.join('\n').length; | ||
| for (const d of result.documents) { | ||
| const block = `=== ${d.path} ===\n${d.content}`; | ||
| if (blocks.length === 0 || used + block.length <= MAX_OUTPUT_CHARS) { | ||
| blocks.push(block); | ||
| used += block.length + 1; | ||
| } | ||
| else { | ||
| omitted.push(d.path); | ||
| } | ||
| } | ||
| if (omitted.length > 0) { | ||
| const shown = omitted.slice(0, 30).join(', '); | ||
| header.push(`(${omitted.length} document(s) omitted from this response to stay within size limits — fetch individually with eds_da_get_source: ${shown}${omitted.length > 30 ? ', …' : ''})`); | ||
| } | ||
| return textResult([...header, '', ...blocks].join('\n')); | ||
| } | ||
| catch (error) { | ||
| return errorResult(error); | ||
| } | ||
| } | ||
| export async function handleDaPush(client, args) { | ||
| try { | ||
| const result = await client.pushDocuments(args.documents); | ||
| const lines = [ | ||
| `Pushed ${result.succeeded.length} document${result.succeeded.length === 1 ? '' : 's'}; ${result.failed.length} failed.`, | ||
| ]; | ||
| if (result.succeeded.length > 0) { | ||
| lines.push('', 'Succeeded:'); | ||
| for (const p of result.succeeded) | ||
| lines.push(` ✓ ${p}`); | ||
| } | ||
| if (result.failed.length > 0) { | ||
| lines.push('', 'Failed:'); | ||
| for (const f of result.failed) | ||
| lines.push(` ✗ ${f.path} — ${f.error}`); | ||
| } | ||
| return textResult(lines.join('\n')); | ||
| } | ||
| catch (error) { | ||
| return errorResult(error); | ||
| } | ||
| } | ||
| export async function handleDaGetVersions(client, args) { | ||
@@ -83,0 +149,0 @@ try { |
| /** | ||
| * MCP server factory for the EDS MCP server. | ||
| * | ||
| * Creates a {@link McpServer} instance with all 28 tools registered. | ||
| * Creates a {@link McpServer} instance with all 30 tools registered. | ||
| * Tool naming follows the `eds_{verb}_{noun}` convention used by Adobe's | ||
@@ -6,0 +6,0 @@ * first-party MCP servers. |
+34
-1
| /** | ||
| * MCP server factory for the EDS MCP server. | ||
| * | ||
| * Creates a {@link McpServer} instance with all 28 tools registered. | ||
| * Creates a {@link McpServer} instance with all 30 tools registered. | ||
| * Tool naming follows the `eds_{verb}_{noun}` convention used by Adobe's | ||
@@ -248,3 +248,36 @@ * first-party MCP servers. | ||
| }, async (args) => daHandlers.handleDaGetVersions(daClient, args)); | ||
| server.tool('eds_da_export', 'Bulk-export a whole Document Authoring subtree in one call: recursively fetch every document under a path and return all their sources together. The efficient "clone" read for operating on many pages at once (vs. one get per page). Requires EDS_DA_TOKEN.', { | ||
| path: z | ||
| .string() | ||
| .refine((v) => !v.split('/').some((s) => { | ||
| let decoded; | ||
| try { | ||
| decoded = decodeURIComponent(s); | ||
| } | ||
| catch { | ||
| decoded = s; | ||
| } | ||
| return decoded === '..' || decoded === '.'; | ||
| }), { message: 'Path must not contain traversal segments (.. or .)' }) | ||
| .describe('DA folder path to export (e.g., "blog"); use "" for the whole site'), | ||
| maxFiles: z | ||
| .number() | ||
| .int() | ||
| .min(1) | ||
| .max(1000) | ||
| .optional() | ||
| .describe('Maximum documents to fetch (default 100); result flags truncation if exceeded'), | ||
| }, async (args) => daHandlers.handleDaExport(daClient, args)); | ||
| server.tool('eds_da_push', 'Bulk-push many edited Document Authoring documents back in one call. The "push" half of the bulk workflow — write a whole batch of {path, content} at once. Returns per-document succeeded/failed. Requires EDS_DA_TOKEN.', { | ||
| documents: z | ||
| .array(z.object({ | ||
| path: daSourcePath.describe('DA document path to write'), | ||
| content: z.string().describe('The full source content (typically HTML)'), | ||
| contentType: z.string().optional().describe('MIME type (default: text/html)'), | ||
| })) | ||
| .min(1) | ||
| .max(1000) | ||
| .describe('The documents to write back'), | ||
| }, async (args) => daHandlers.handleDaPush(daClient, args)); | ||
| return server; | ||
| } |
+1
-1
| { | ||
| "name": "@focusgts/eds-mcp-server", | ||
| "version": "0.5.0", | ||
| "version": "0.6.0", | ||
| "mcpName": "io.github.focusgts/eds-mcp-server", | ||
@@ -5,0 +5,0 @@ "description": "MCP server for Adobe Edge Delivery Services — preview, publish, metrics, and content operations", |
+39
-6
@@ -13,3 +13,3 @@ <div align="center"> | ||
| **28 tools. No extra dependencies beyond the MCP SDK. Works with any EDS site.** | ||
| **30 tools. No extra dependencies beyond the MCP SDK. Works with any EDS site.** | ||
| The first MCP server purpose-built for Edge Delivery Services. | ||
@@ -34,2 +34,3 @@ | ||
| > *"Find every page about pricing and list the ones missing a description."* | ||
| > *"Export the whole `/blog` folder, fix every heading, and push it back."* | ||
@@ -44,9 +45,11 @@ That's it — no local AEM, no scripts, no glue code. | ||
| flowchart LR | ||
| A["AI agent<br/>(Claude Code · Cursor · Copilot)"] -- MCP / stdio --> B["eds-mcp-server<br/>28 tools"] | ||
| A["AI agent<br/>(Claude Code · Cursor · Copilot)"] -- MCP / stdio --> B["eds-mcp-server<br/>30 tools"] | ||
| B --> C["Admin API<br/>admin.hlx.page"] | ||
| B --> D["Content API<br/>*.aem.live"] | ||
| B --> E["RUM / OpTel<br/>Core Web Vitals"] | ||
| B --> G["Document Authoring<br/>admin.da.live"] | ||
| C --> F["Your EDS site"] | ||
| D --> F | ||
| E --> F | ||
| G --> F | ||
| ``` | ||
@@ -78,4 +81,6 @@ | ||
| ## 🛠️ The 28 tools | ||
| ## 🛠️ The 30 tools | ||
| ### Edge Delivery Services — publish, content, analytics | ||
| <table> | ||
@@ -118,5 +123,33 @@ <tr><td valign="top" width="33%"> | ||
| **Document Authoring (DA)** — direct access to the authored source, not the rendered output (requires `EDS_DA_TOKEN`): | ||
| `eds_da_list_sources` · `eds_da_get_source` · `eds_da_put_source` · `eds_da_delete_source` · `eds_da_copy_source` · `eds_da_move_source` · `eds_da_get_versions` | ||
| ### Document Authoring (DA) — the authored *source*, not the rendered output | ||
| Nine tools reach a site's Document Authoring source directly (`admin.da.live`), the source of truth behind an EDS site. Requires `EDS_DA_TOKEN`. | ||
| <table> | ||
| <tr><td valign="top" width="33%"> | ||
| **Read** | ||
| - `eds_da_list_sources` | ||
| - `eds_da_get_source` | ||
| - `eds_da_get_versions` | ||
| </td><td valign="top" width="33%"> | ||
| **Write** | ||
| - `eds_da_put_source` | ||
| - `eds_da_delete_source` | ||
| - `eds_da_copy_source` | ||
| - `eds_da_move_source` | ||
| </td><td valign="top" width="33%"> | ||
| **Bulk ("clone")** | ||
| - `eds_da_export` | ||
| - `eds_da_push` | ||
| </td></tr> | ||
| </table> | ||
| > **`eds_da_export` / `eds_da_push`** bring the efficiency of `aem content clone` to agents: export a whole DA subtree in **one** call, operate on it, and push the batch back in **one** call — no local checkout, no `aem-cli`. Same model, network-native. | ||
| > | ||
| > `EDS_DA_TOKEN` is an Adobe IMS access token for Document Authoring — grab it from an authenticated [da.live](https://da.live) session (the IMS `access_token`). Document paths assume `.html` when no extension is given (`index` → `index.html`). | ||
@@ -179,3 +212,3 @@ | ||
| | `EDS_DOMAIN_KEY` | No | OpTel domain key for analytics queries (CWV, 404s, experiments) | | ||
| | `EDS_DA_TOKEN` | No | Document Authoring API token — enables the `eds_da_*` source tools | | ||
| | `EDS_DA_TOKEN` | No | Document Authoring IMS access token — enables the `eds_da_*` source & bulk tools | | ||
| | `EDS_DA_ORG` | No | DA org (defaults to `EDS_OWNER`) | | ||
@@ -182,0 +215,0 @@ | `EDS_DA_REPO` | No | DA repo/site (defaults to `EDS_REPO`) | |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
183462
9.28%4012
8.99%268
14.04%